lipla-jp 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lipla_jp-0.2.0/LICENSE +21 -0
- lipla_jp-0.2.0/PKG-INFO +90 -0
- lipla_jp-0.2.0/README.md +65 -0
- lipla_jp-0.2.0/lipla/__init__.py +5 -0
- lipla_jp-0.2.0/lipla/configs/characters.yml +208 -0
- lipla_jp-0.2.0/lipla/core/fixed_field_recognizer.py +286 -0
- lipla_jp-0.2.0/lipla/core/image_processing.py +164 -0
- lipla_jp-0.2.0/lipla/core/license_plate_recognizer.py +385 -0
- lipla_jp-0.2.0/lipla/core/ocr_result_parser.py +373 -0
- lipla_jp-0.2.0/lipla/core/plate_normalizer.py +110 -0
- lipla_jp-0.2.0/lipla/core/pose_postprocessor.py +123 -0
- lipla_jp-0.2.0/lipla/core/recognition_result.py +313 -0
- lipla_jp-0.2.0/lipla/inferencers/ec_pose.py +326 -0
- lipla_jp-0.2.0/lipla/inferencers/execution_provider.py +91 -0
- lipla_jp-0.2.0/lipla/inferencers/model_loader.py +52 -0
- lipla_jp-0.2.0/lipla/inferencers/ppocr.py +679 -0
- lipla_jp-0.2.0/lipla_jp.egg-info/PKG-INFO +90 -0
- lipla_jp-0.2.0/lipla_jp.egg-info/SOURCES.txt +31 -0
- lipla_jp-0.2.0/lipla_jp.egg-info/dependency_links.txt +1 -0
- lipla_jp-0.2.0/lipla_jp.egg-info/requires.txt +11 -0
- lipla_jp-0.2.0/lipla_jp.egg-info/top_level.txt +1 -0
- lipla_jp-0.2.0/pyproject.toml +62 -0
- lipla_jp-0.2.0/setup.cfg +4 -0
- lipla_jp-0.2.0/tests/test_execution_provider.py +124 -0
- lipla_jp-0.2.0/tests/test_fixed_field_recognizer.py +77 -0
- lipla_jp-0.2.0/tests/test_huggingface_space.py +85 -0
- lipla_jp-0.2.0/tests/test_image_processing.py +66 -0
- lipla_jp-0.2.0/tests/test_inference_validation.py +166 -0
- lipla_jp-0.2.0/tests/test_model_loading.py +178 -0
- lipla_jp-0.2.0/tests/test_ocr_result_parser.py +92 -0
- lipla_jp-0.2.0/tests/test_pose_postprocessor.py +106 -0
- lipla_jp-0.2.0/tests/test_recognition_result.py +70 -0
- lipla_jp-0.2.0/tests/test_recognizer.py +385 -0
lipla_jp-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ikeboo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lipla_jp-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lipla-jp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Japanese license plate recognition
|
|
5
|
+
Author: ikeboo
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ikeboo/Lipla-jp
|
|
8
|
+
Project-URL: Repository, https://github.com/ikeboo/Lipla-jp
|
|
9
|
+
Project-URL: Issues, https://github.com/ikeboo/Lipla-jp/issues
|
|
10
|
+
Keywords: license-plate-recognition,ocr,onnx,japan
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: huggingface-hub<2,>=0.20.3
|
|
15
|
+
Requires-Dist: numpy>=1.26.4
|
|
16
|
+
Requires-Dist: opencv-python>=4.9.0.80
|
|
17
|
+
Requires-Dist: pillow>=10.2.0
|
|
18
|
+
Requires-Dist: matplotlib>=3.8.2
|
|
19
|
+
Requires-Dist: onnxruntime>=1.24.4
|
|
20
|
+
Requires-Dist: shapely>=2.0.3
|
|
21
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
22
|
+
Provides-Extra: webgpu
|
|
23
|
+
Requires-Dist: onnxruntime-ep-webgpu>=0.1.0; extra == "webgpu"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
<div align="center">
|
|
27
|
+
<img src="https://raw.githubusercontent.com/ikeboo/Lipla-jp/main/samples/logo.png">
|
|
28
|
+
<img src="https://raw.githubusercontent.com/ikeboo/Lipla-jp/main/samples/results.jpg">
|
|
29
|
+
</div>
|
|
30
|
+
<p align="center"><a href="https://pypi.org/project/lipla-jp/"><img src="https://img.shields.io/pypi/v/lipla-jp" alt="PyPI"></a> <a href="https://github.com/ikeboo/Lipla-jp"><img src="https://img.shields.io/badge/GitHub-Lipla--jp-181717?logo=github&logoColor=white" alt="GitHub"></a> <a href="https://huggingface.co/bukuroo/Lipla-jp"><img src="https://img.shields.io/badge/Hugging%20Face-Lipla--jp-FFD21E?logo=huggingface" alt="Hugging Face"></a> <a href="https://huggingface.co/spaces/bukuroo/Lipla"><img src="https://img.shields.io/badge/Hugging%20Face-Spaces-FFD21E?logo=huggingface" alt="Hugging Face Spaces"></a> <a href="https://colab.research.google.com/drive/1YUG36Q8kpGtsolwp0ZfqqitfIhmMBZ1E?usp=sharing"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"></a> <a href="https://github.com/ikeboo/Lipla-jp/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a></p>
|
|
31
|
+
|
|
32
|
+
### 🖐️ What's Lipla?
|
|
33
|
+
日本の自動車ナンバープレート認識用Pythonライブラリ。
|
|
34
|
+
高精度、オープンソース、商用利用可能。
|
|
35
|
+
|
|
36
|
+
### ⚡️ Quick Start
|
|
37
|
+
- セットアップ
|
|
38
|
+
```sh
|
|
39
|
+
pip install lipla-jp
|
|
40
|
+
# 高速化オプション
|
|
41
|
+
pip install "lipla-jp[webgpu]"
|
|
42
|
+
```
|
|
43
|
+
WebGPU利用時の推論高速化 x3.8 @Corei5-1235U, x1.8 @M5Pro
|
|
44
|
+
|
|
45
|
+
- 検出実行
|
|
46
|
+
初回のみ[モデルウェイト](https://huggingface.co/bukuroo/Lipla-jp)が自動ダウンロードされます。
|
|
47
|
+
```python
|
|
48
|
+
import lipla
|
|
49
|
+
|
|
50
|
+
rec = lipla.Recognizer()
|
|
51
|
+
results = rec("samples/00.jpg") # パスまたはcv2image
|
|
52
|
+
```
|
|
53
|
+
- 出力データ
|
|
54
|
+
```python
|
|
55
|
+
result = results[0] # 複数の検出結果が格納されています
|
|
56
|
+
result.area # 世田谷
|
|
57
|
+
result.class_number # 種別(300)
|
|
58
|
+
result.kana # かな(あ)
|
|
59
|
+
result.number # 番号(1234)
|
|
60
|
+
result.plate_image # 正規化したプレート画像
|
|
61
|
+
result.original_image # 入力元画像
|
|
62
|
+
result.det_image # 検出領域を描画した元画像
|
|
63
|
+
result.result_image # 正規化画像と認識結果の表示画像
|
|
64
|
+
result.visualize() # 検出画像と認識結果を並べて表示
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### ✏️Tips
|
|
68
|
+
認識精度を高めるために入力画像は以下の条件が推奨されます。
|
|
69
|
+
- アスペクト比は4:3
|
|
70
|
+
- ナンバープレートの幅が画像幅の1/10から1/4、100ピクセル以上
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
### 🧠 Features
|
|
74
|
+
- MIT-licenseで商用利用、再配布可能。
|
|
75
|
+
- 2026年時点で最新の検出、OCRモデルを利用した高精度な認識を実現。
|
|
76
|
+
|
|
77
|
+
|タスク|モデル名|
|
|
78
|
+
|----|----|
|
|
79
|
+
|プレート検出|EdgeCrafter Pose|
|
|
80
|
+
|OCR|PPOCRv6 medium|
|
|
81
|
+
- ONNXベースのライブラリ、PyTorch依存なし
|
|
82
|
+
|
|
83
|
+
### 📄 License
|
|
84
|
+
このプロジェクトは[MIT License](LICENSE)のもとで公開されています。
|
|
85
|
+
|
|
86
|
+
### 🙏 Acknowledgements
|
|
87
|
+
このプロジェクトでは、以下のオープンソースプロジェクトを利用しています。
|
|
88
|
+
|
|
89
|
+
- [EdgeCrafter](https://github.com/Intellindust-AI-Lab/EdgeCrafter)
|
|
90
|
+
- [PaddleOCR](https://github.com/PADDLEPADDLE/PADDLEOCR)
|
lipla_jp-0.2.0/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/ikeboo/Lipla-jp/main/samples/logo.png">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/ikeboo/Lipla-jp/main/samples/results.jpg">
|
|
4
|
+
</div>
|
|
5
|
+
<p align="center"><a href="https://pypi.org/project/lipla-jp/"><img src="https://img.shields.io/pypi/v/lipla-jp" alt="PyPI"></a> <a href="https://github.com/ikeboo/Lipla-jp"><img src="https://img.shields.io/badge/GitHub-Lipla--jp-181717?logo=github&logoColor=white" alt="GitHub"></a> <a href="https://huggingface.co/bukuroo/Lipla-jp"><img src="https://img.shields.io/badge/Hugging%20Face-Lipla--jp-FFD21E?logo=huggingface" alt="Hugging Face"></a> <a href="https://huggingface.co/spaces/bukuroo/Lipla"><img src="https://img.shields.io/badge/Hugging%20Face-Spaces-FFD21E?logo=huggingface" alt="Hugging Face Spaces"></a> <a href="https://colab.research.google.com/drive/1YUG36Q8kpGtsolwp0ZfqqitfIhmMBZ1E?usp=sharing"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"></a> <a href="https://github.com/ikeboo/Lipla-jp/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a></p>
|
|
6
|
+
|
|
7
|
+
### 🖐️ What's Lipla?
|
|
8
|
+
日本の自動車ナンバープレート認識用Pythonライブラリ。
|
|
9
|
+
高精度、オープンソース、商用利用可能。
|
|
10
|
+
|
|
11
|
+
### ⚡️ Quick Start
|
|
12
|
+
- セットアップ
|
|
13
|
+
```sh
|
|
14
|
+
pip install lipla-jp
|
|
15
|
+
# 高速化オプション
|
|
16
|
+
pip install "lipla-jp[webgpu]"
|
|
17
|
+
```
|
|
18
|
+
WebGPU利用時の推論高速化 x3.8 @Corei5-1235U, x1.8 @M5Pro
|
|
19
|
+
|
|
20
|
+
- 検出実行
|
|
21
|
+
初回のみ[モデルウェイト](https://huggingface.co/bukuroo/Lipla-jp)が自動ダウンロードされます。
|
|
22
|
+
```python
|
|
23
|
+
import lipla
|
|
24
|
+
|
|
25
|
+
rec = lipla.Recognizer()
|
|
26
|
+
results = rec("samples/00.jpg") # パスまたはcv2image
|
|
27
|
+
```
|
|
28
|
+
- 出力データ
|
|
29
|
+
```python
|
|
30
|
+
result = results[0] # 複数の検出結果が格納されています
|
|
31
|
+
result.area # 世田谷
|
|
32
|
+
result.class_number # 種別(300)
|
|
33
|
+
result.kana # かな(あ)
|
|
34
|
+
result.number # 番号(1234)
|
|
35
|
+
result.plate_image # 正規化したプレート画像
|
|
36
|
+
result.original_image # 入力元画像
|
|
37
|
+
result.det_image # 検出領域を描画した元画像
|
|
38
|
+
result.result_image # 正規化画像と認識結果の表示画像
|
|
39
|
+
result.visualize() # 検出画像と認識結果を並べて表示
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### ✏️Tips
|
|
43
|
+
認識精度を高めるために入力画像は以下の条件が推奨されます。
|
|
44
|
+
- アスペクト比は4:3
|
|
45
|
+
- ナンバープレートの幅が画像幅の1/10から1/4、100ピクセル以上
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### 🧠 Features
|
|
49
|
+
- MIT-licenseで商用利用、再配布可能。
|
|
50
|
+
- 2026年時点で最新の検出、OCRモデルを利用した高精度な認識を実現。
|
|
51
|
+
|
|
52
|
+
|タスク|モデル名|
|
|
53
|
+
|----|----|
|
|
54
|
+
|プレート検出|EdgeCrafter Pose|
|
|
55
|
+
|OCR|PPOCRv6 medium|
|
|
56
|
+
- ONNXベースのライブラリ、PyTorch依存なし
|
|
57
|
+
|
|
58
|
+
### 📄 License
|
|
59
|
+
このプロジェクトは[MIT License](LICENSE)のもとで公開されています。
|
|
60
|
+
|
|
61
|
+
### 🙏 Acknowledgements
|
|
62
|
+
このプロジェクトでは、以下のオープンソースプロジェクトを利用しています。
|
|
63
|
+
|
|
64
|
+
- [EdgeCrafter](https://github.com/Intellindust-AI-Lab/EdgeCrafter)
|
|
65
|
+
- [PaddleOCR](https://github.com/PADDLEPADDLE/PADDLEOCR)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
areas:
|
|
2
|
+
- 札幌
|
|
3
|
+
- 函館
|
|
4
|
+
- 旭川
|
|
5
|
+
- 室蘭
|
|
6
|
+
- 苫小牧
|
|
7
|
+
- 釧路
|
|
8
|
+
- 知床
|
|
9
|
+
- 帯広
|
|
10
|
+
- 十勝
|
|
11
|
+
- 北見
|
|
12
|
+
- 青森
|
|
13
|
+
- 弘前
|
|
14
|
+
- 八戸
|
|
15
|
+
- 盛岡
|
|
16
|
+
- 岩手
|
|
17
|
+
- 平泉
|
|
18
|
+
- 仙台
|
|
19
|
+
- 宮城
|
|
20
|
+
- 秋田
|
|
21
|
+
- 山形
|
|
22
|
+
- 庄内
|
|
23
|
+
- 福島
|
|
24
|
+
- 会津
|
|
25
|
+
- 郡山
|
|
26
|
+
- 白河
|
|
27
|
+
- いわき
|
|
28
|
+
- 水戸
|
|
29
|
+
- 土浦
|
|
30
|
+
- つくば
|
|
31
|
+
- 宇都宮
|
|
32
|
+
- 日光
|
|
33
|
+
- 那須
|
|
34
|
+
- とちぎ
|
|
35
|
+
- 前橋
|
|
36
|
+
- 高崎
|
|
37
|
+
- 群馬
|
|
38
|
+
- 大宮
|
|
39
|
+
- 川口
|
|
40
|
+
- 川越
|
|
41
|
+
- 所沢
|
|
42
|
+
- 熊谷
|
|
43
|
+
- 春日部
|
|
44
|
+
- 越谷
|
|
45
|
+
- 千葉
|
|
46
|
+
- 成田
|
|
47
|
+
- 市川
|
|
48
|
+
- 船橋
|
|
49
|
+
- 習志野
|
|
50
|
+
- 袖ケ浦
|
|
51
|
+
- 市原
|
|
52
|
+
- 松戸
|
|
53
|
+
- 野田
|
|
54
|
+
- 柏
|
|
55
|
+
- 品川
|
|
56
|
+
- 世田谷
|
|
57
|
+
- 練馬
|
|
58
|
+
- 杉並
|
|
59
|
+
- 板橋
|
|
60
|
+
- 足立
|
|
61
|
+
- 江東
|
|
62
|
+
- 葛飾
|
|
63
|
+
- 江戸川
|
|
64
|
+
- 八王子
|
|
65
|
+
- 多摩
|
|
66
|
+
- 横浜
|
|
67
|
+
- 川崎
|
|
68
|
+
- 湘南
|
|
69
|
+
- 相模
|
|
70
|
+
- 山梨
|
|
71
|
+
- 富士山
|
|
72
|
+
- 新潟
|
|
73
|
+
- 長岡
|
|
74
|
+
- 上越
|
|
75
|
+
- 富山
|
|
76
|
+
- 金沢
|
|
77
|
+
- 石川
|
|
78
|
+
- 福井
|
|
79
|
+
- 長野
|
|
80
|
+
- 松本
|
|
81
|
+
- 諏訪
|
|
82
|
+
- 南信州
|
|
83
|
+
- 安曇野
|
|
84
|
+
- 岐阜
|
|
85
|
+
- 飛騨
|
|
86
|
+
- 静岡
|
|
87
|
+
- 浜松
|
|
88
|
+
- 沼津
|
|
89
|
+
- 伊豆
|
|
90
|
+
- 名古屋
|
|
91
|
+
- 豊橋
|
|
92
|
+
- 岡崎
|
|
93
|
+
- 三河
|
|
94
|
+
- 豊田
|
|
95
|
+
- 一宮
|
|
96
|
+
- 尾張小牧
|
|
97
|
+
- 春日井
|
|
98
|
+
- 三重
|
|
99
|
+
- 四日市
|
|
100
|
+
- 伊勢志摩
|
|
101
|
+
- 鈴鹿
|
|
102
|
+
- 滋賀
|
|
103
|
+
- 京都
|
|
104
|
+
- 大阪
|
|
105
|
+
- なにわ
|
|
106
|
+
- 堺
|
|
107
|
+
- 和泉
|
|
108
|
+
- 神戸
|
|
109
|
+
- 姫路
|
|
110
|
+
- 奈良
|
|
111
|
+
- 飛鳥
|
|
112
|
+
- 和歌山
|
|
113
|
+
- 鳥取
|
|
114
|
+
- 島根
|
|
115
|
+
- 出雲
|
|
116
|
+
- 岡山
|
|
117
|
+
- 倉敷
|
|
118
|
+
- 広島
|
|
119
|
+
- 福山
|
|
120
|
+
- 下関
|
|
121
|
+
- 山口
|
|
122
|
+
- 徳島
|
|
123
|
+
- 高松
|
|
124
|
+
- 香川
|
|
125
|
+
- 愛媛
|
|
126
|
+
- 高知
|
|
127
|
+
- 福岡
|
|
128
|
+
- 北九州
|
|
129
|
+
- 久留米
|
|
130
|
+
- 筑豊
|
|
131
|
+
- 佐賀
|
|
132
|
+
- 長崎
|
|
133
|
+
- 佐世保
|
|
134
|
+
- 熊本
|
|
135
|
+
- 大分
|
|
136
|
+
- 宮崎
|
|
137
|
+
- 鹿児島
|
|
138
|
+
- 奄美
|
|
139
|
+
- 沖縄
|
|
140
|
+
hiragana:
|
|
141
|
+
- あ
|
|
142
|
+
- い
|
|
143
|
+
- う
|
|
144
|
+
- え
|
|
145
|
+
- か
|
|
146
|
+
- き
|
|
147
|
+
- く
|
|
148
|
+
- け
|
|
149
|
+
- こ
|
|
150
|
+
- さ
|
|
151
|
+
- す
|
|
152
|
+
- せ
|
|
153
|
+
- そ
|
|
154
|
+
- た
|
|
155
|
+
- ち
|
|
156
|
+
- つ
|
|
157
|
+
- て
|
|
158
|
+
- と
|
|
159
|
+
- な
|
|
160
|
+
- に
|
|
161
|
+
- ぬ
|
|
162
|
+
- ね
|
|
163
|
+
- の
|
|
164
|
+
- は
|
|
165
|
+
- ひ
|
|
166
|
+
- ふ
|
|
167
|
+
- ほ
|
|
168
|
+
- ま
|
|
169
|
+
- み
|
|
170
|
+
- む
|
|
171
|
+
- め
|
|
172
|
+
- も
|
|
173
|
+
- や
|
|
174
|
+
- ゆ
|
|
175
|
+
- よ
|
|
176
|
+
- ら
|
|
177
|
+
- り
|
|
178
|
+
- る
|
|
179
|
+
- れ
|
|
180
|
+
- ろ
|
|
181
|
+
- わ
|
|
182
|
+
- を
|
|
183
|
+
alphabet:
|
|
184
|
+
- A
|
|
185
|
+
- C
|
|
186
|
+
- F
|
|
187
|
+
- H
|
|
188
|
+
- K
|
|
189
|
+
- L
|
|
190
|
+
- M
|
|
191
|
+
- P
|
|
192
|
+
- X
|
|
193
|
+
- Y
|
|
194
|
+
numbers:
|
|
195
|
+
- "0"
|
|
196
|
+
- "1"
|
|
197
|
+
- "2"
|
|
198
|
+
- "3"
|
|
199
|
+
- "4"
|
|
200
|
+
- "5"
|
|
201
|
+
- "6"
|
|
202
|
+
- "7"
|
|
203
|
+
- "8"
|
|
204
|
+
- "9"
|
|
205
|
+
symbols:
|
|
206
|
+
- "-"
|
|
207
|
+
- "·"
|
|
208
|
+
- "・"
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""ナンバープレートの固定領域に特化した文字認識。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Final, Protocol
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from lipla.inferencers.ppocr import preprocess_rec
|
|
11
|
+
|
|
12
|
+
from .image_processing import validate_bgr_image
|
|
13
|
+
from .ocr_result_parser import (
|
|
14
|
+
CandidateMap,
|
|
15
|
+
OCRResultParser,
|
|
16
|
+
TextCandidate,
|
|
17
|
+
create_candidate_map,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
FIELD_RECTS: Final = {
|
|
21
|
+
"area": (0.194, 0.00, 0.519, 0.325),
|
|
22
|
+
"class_number": (0.51, 0.00, 0.81, 0.36),
|
|
23
|
+
"number": (0.15, 0.34, 1.00, 1.00),
|
|
24
|
+
}
|
|
25
|
+
KANA_RECTS: Final = (
|
|
26
|
+
((0.009, 0.438, 0.197, 1.00), 1.0),
|
|
27
|
+
((0.016, 0.469, 0.197, 1.00), 0.7),
|
|
28
|
+
((0.000, 0.375, 0.219, 1.00), 0.7),
|
|
29
|
+
)
|
|
30
|
+
FIELD_FILTERS: Final = {
|
|
31
|
+
"class_number": ["numbers", "alphabet"],
|
|
32
|
+
"number": ["numbers", "symbols"],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RecognitionSession(Protocol):
|
|
37
|
+
"""文字認識セッションに必要なインターフェース。"""
|
|
38
|
+
|
|
39
|
+
def run(
|
|
40
|
+
self,
|
|
41
|
+
output_names: None,
|
|
42
|
+
input_feed: dict[str, np.ndarray],
|
|
43
|
+
) -> list[np.ndarray]: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RecognitionDecoder(Protocol):
|
|
47
|
+
"""文字認識デコーダーに必要なインターフェース。"""
|
|
48
|
+
|
|
49
|
+
character: list[str]
|
|
50
|
+
|
|
51
|
+
def decode(
|
|
52
|
+
self,
|
|
53
|
+
predictions: np.ndarray,
|
|
54
|
+
filters: Iterable[str] | None = None,
|
|
55
|
+
) -> tuple[str, float]: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RecognitionModel(Protocol):
|
|
59
|
+
"""固定領域OCRに必要なモデルインターフェース。"""
|
|
60
|
+
|
|
61
|
+
rec_session: RecognitionSession
|
|
62
|
+
rec_input_name: str
|
|
63
|
+
rec_target_height: int
|
|
64
|
+
decoder: RecognitionDecoder
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def logsumexp(values: np.ndarray) -> float:
|
|
68
|
+
"""オーバーフローを避けながら指数の和の対数を計算する。
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
values: 対数空間の値。
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
``log(sum(exp(values)))`` の計算結果。
|
|
75
|
+
"""
|
|
76
|
+
maximum = float(np.max(values))
|
|
77
|
+
if not np.isfinite(maximum):
|
|
78
|
+
return -np.inf
|
|
79
|
+
return maximum + float(np.log(np.exp(values - maximum).sum()))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def ctc_log_probability(
|
|
83
|
+
log_probabilities: np.ndarray, tokens: tuple[int, ...]
|
|
84
|
+
) -> float:
|
|
85
|
+
"""1つの語彙に対するCTC前向き確率を対数空間で計算する。
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
log_probabilities: 各時刻・各文字の対数確率。Blankは添字0とする。
|
|
89
|
+
tokens: 評価する文字列の文字インデックス。
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
指定した文字列を生成する全CTCパスの対数確率。
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: ``tokens`` が空、または時間ステップが不足している場合。
|
|
96
|
+
"""
|
|
97
|
+
if not tokens:
|
|
98
|
+
raise ValueError("tokens must not be empty")
|
|
99
|
+
if len(log_probabilities) == 0:
|
|
100
|
+
raise ValueError("log_probabilities must contain at least one timestep")
|
|
101
|
+
|
|
102
|
+
extended = [0]
|
|
103
|
+
for token in tokens:
|
|
104
|
+
extended.extend((token, 0))
|
|
105
|
+
|
|
106
|
+
previous = np.full(len(extended), -np.inf, dtype=np.float64)
|
|
107
|
+
previous[0] = log_probabilities[0, 0]
|
|
108
|
+
previous[1] = log_probabilities[0, extended[1]]
|
|
109
|
+
for timestep in range(1, len(log_probabilities)):
|
|
110
|
+
current = np.full_like(previous, -np.inf)
|
|
111
|
+
for state, token in enumerate(extended):
|
|
112
|
+
incoming = [previous[state]]
|
|
113
|
+
if state > 0:
|
|
114
|
+
incoming.append(previous[state - 1])
|
|
115
|
+
if state > 1 and token != 0 and token != extended[state - 2]:
|
|
116
|
+
incoming.append(previous[state - 2])
|
|
117
|
+
current[state] = logsumexp(np.asarray(incoming))
|
|
118
|
+
current[state] += log_probabilities[timestep, token]
|
|
119
|
+
previous = current
|
|
120
|
+
return logsumexp(previous[-2:])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class FixedFieldRecognizer:
|
|
124
|
+
"""プレートの既知レイアウトから4項目を個別認識する。
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
ocr_model: PP-OCRの文字認識セッションとデコーダーを持つオブジェクト。
|
|
128
|
+
parser: 地名とひらがなの語彙を保持するOCR結果パーサー。
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self, ocr_model: RecognitionModel, parser: OCRResultParser) -> None:
|
|
132
|
+
self.ocr_model = ocr_model
|
|
133
|
+
self.parser = parser
|
|
134
|
+
decoder_characters = self.ocr_model.decoder.character
|
|
135
|
+
self.area_tokens = tuple(
|
|
136
|
+
(
|
|
137
|
+
area,
|
|
138
|
+
tuple(decoder_characters.index(character) for character in area),
|
|
139
|
+
)
|
|
140
|
+
for area in parser.areas
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def recognize(self, image: np.ndarray) -> CandidateMap:
|
|
144
|
+
"""固定領域を切り出して項目別の文字列候補を生成する。
|
|
145
|
+
|
|
146
|
+
必要な文字認識インターフェースをOCRモデルが持たない場合は、全項目が
|
|
147
|
+
空の候補辞書を返す。これにより、文字検出だけを実装した差し替えモデル
|
|
148
|
+
でも空間OCR解析を利用できる。
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
image: 射影・色補正済みのBGRプレート画像。
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
項目別の文字列候補。
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
TypeError: 画像の型またはデータ型が不正な場合。
|
|
158
|
+
ValueError: 画像が空、またはBGR画像ではない場合。
|
|
159
|
+
"""
|
|
160
|
+
validate_bgr_image(image)
|
|
161
|
+
candidates = create_candidate_map()
|
|
162
|
+
required = (
|
|
163
|
+
"rec_session",
|
|
164
|
+
"rec_input_name",
|
|
165
|
+
"rec_target_height",
|
|
166
|
+
"decoder",
|
|
167
|
+
)
|
|
168
|
+
if not all(hasattr(self.ocr_model, name) for name in required):
|
|
169
|
+
return candidates
|
|
170
|
+
|
|
171
|
+
height, width = image.shape[:2]
|
|
172
|
+
for field_name, rect in FIELD_RECTS.items():
|
|
173
|
+
crop = self._crop(image, rect)
|
|
174
|
+
if field_name == "area":
|
|
175
|
+
candidate = self._recognize_area(crop)
|
|
176
|
+
elif field_name == "class_number":
|
|
177
|
+
candidate = self._recognize_class_number(crop)
|
|
178
|
+
else:
|
|
179
|
+
candidate = self._recognize_crop(crop, FIELD_FILTERS[field_name])
|
|
180
|
+
if candidate.text:
|
|
181
|
+
candidates[field_name].append(candidate)
|
|
182
|
+
|
|
183
|
+
kana_candidate = self._recognize_kana(image)
|
|
184
|
+
if kana_candidate.text:
|
|
185
|
+
candidates["kana"].append(kana_candidate)
|
|
186
|
+
|
|
187
|
+
top_row = image[
|
|
188
|
+
: max(1, int(round(height * 0.41))),
|
|
189
|
+
int(round(width * 0.14)) : int(round(width * 0.85)),
|
|
190
|
+
]
|
|
191
|
+
self.parser.split_top_text(self._recognize_crop(top_row, None), candidates)
|
|
192
|
+
return candidates
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _crop(image: np.ndarray, rect: tuple[float, float, float, float]) -> np.ndarray:
|
|
196
|
+
height, width = image.shape[:2]
|
|
197
|
+
x1, y1, x2, y2 = rect
|
|
198
|
+
left = max(0, min(width - 1, int(round(x1 * width))))
|
|
199
|
+
top = max(0, min(height - 1, int(round(y1 * height))))
|
|
200
|
+
right = max(left + 1, min(width, int(round(x2 * width))))
|
|
201
|
+
bottom = max(top + 1, min(height, int(round(y2 * height))))
|
|
202
|
+
return image[top:bottom, left:right]
|
|
203
|
+
|
|
204
|
+
def _recognize_crop(
|
|
205
|
+
self, crop: np.ndarray, filters: list[str] | None
|
|
206
|
+
) -> TextCandidate:
|
|
207
|
+
if crop.size == 0:
|
|
208
|
+
return TextCandidate("", 0.0)
|
|
209
|
+
rec_input = preprocess_rec(crop, target_height=self.ocr_model.rec_target_height)
|
|
210
|
+
rec_outputs = self.ocr_model.rec_session.run(
|
|
211
|
+
None, {self.ocr_model.rec_input_name: rec_input}
|
|
212
|
+
)
|
|
213
|
+
text, score = self.ocr_model.decoder.decode(rec_outputs[0], filters=filters)
|
|
214
|
+
return TextCandidate(str(text).strip(), float(score))
|
|
215
|
+
|
|
216
|
+
def _recognize_area(self, crop: np.ndarray) -> TextCandidate:
|
|
217
|
+
if crop.size == 0:
|
|
218
|
+
return TextCandidate("", 0.0)
|
|
219
|
+
rec_input = preprocess_rec(crop, target_height=self.ocr_model.rec_target_height)
|
|
220
|
+
rec_outputs = self.ocr_model.rec_session.run(
|
|
221
|
+
None, {self.ocr_model.rec_input_name: rec_input}
|
|
222
|
+
)[0]
|
|
223
|
+
probabilities = np.asarray(rec_outputs, dtype=np.float64)[0]
|
|
224
|
+
log_probabilities = np.log(np.clip(probabilities, 1e-30, 1.0))
|
|
225
|
+
scores = np.asarray(
|
|
226
|
+
[
|
|
227
|
+
ctc_log_probability(log_probabilities, tokens) / len(tokens)
|
|
228
|
+
for _, tokens in self.area_tokens
|
|
229
|
+
],
|
|
230
|
+
dtype=np.float64,
|
|
231
|
+
)
|
|
232
|
+
best_index = int(np.argmax(scores))
|
|
233
|
+
normalizer = logsumexp(scores)
|
|
234
|
+
confidence = float(np.exp(scores[best_index] - normalizer))
|
|
235
|
+
return TextCandidate(self.area_tokens[best_index][0], confidence)
|
|
236
|
+
|
|
237
|
+
def _recognize_class_number(self, crop: np.ndarray) -> TextCandidate:
|
|
238
|
+
if crop.size == 0:
|
|
239
|
+
return TextCandidate("", 0.0)
|
|
240
|
+
rec_input = preprocess_rec(crop, target_height=self.ocr_model.rec_target_height)
|
|
241
|
+
rec_outputs = self.ocr_model.rec_session.run(
|
|
242
|
+
None, {self.ocr_model.rec_input_name: rec_input}
|
|
243
|
+
)[0]
|
|
244
|
+
text, score = self.ocr_model.decoder.decode(
|
|
245
|
+
rec_outputs, filters=FIELD_FILTERS["class_number"]
|
|
246
|
+
)
|
|
247
|
+
return TextCandidate(str(text).strip().upper(), float(score))
|
|
248
|
+
|
|
249
|
+
def _single_character_scores(
|
|
250
|
+
self, crop: np.ndarray, allowed_characters: Iterable[str]
|
|
251
|
+
) -> dict[str, float]:
|
|
252
|
+
if crop.size == 0:
|
|
253
|
+
return {}
|
|
254
|
+
rec_input = preprocess_rec(crop, target_height=self.ocr_model.rec_target_height)
|
|
255
|
+
rec_outputs = self.ocr_model.rec_session.run(
|
|
256
|
+
None, {self.ocr_model.rec_input_name: rec_input}
|
|
257
|
+
)[0]
|
|
258
|
+
probabilities = np.asarray(rec_outputs)[0]
|
|
259
|
+
character_indices = {
|
|
260
|
+
character: self.ocr_model.decoder.character.index(character)
|
|
261
|
+
for character in allowed_characters
|
|
262
|
+
if character in self.ocr_model.decoder.character
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
character: max(0.0, float(np.max(probabilities[:, index])))
|
|
266
|
+
for character, index in character_indices.items()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
def _recognize_kana(self, image: np.ndarray) -> TextCandidate:
|
|
270
|
+
combined = {character: 0.0 for character in self.parser.kana_characters}
|
|
271
|
+
for rect, weight in KANA_RECTS:
|
|
272
|
+
scores = self._single_character_scores(
|
|
273
|
+
self._crop(image, rect), self.parser.kana_characters
|
|
274
|
+
)
|
|
275
|
+
score_sum = sum(scores.values())
|
|
276
|
+
if score_sum <= 0.0:
|
|
277
|
+
continue
|
|
278
|
+
for character, score in scores.items():
|
|
279
|
+
combined[character] += weight * score / score_sum
|
|
280
|
+
|
|
281
|
+
if not combined:
|
|
282
|
+
return TextCandidate("", 0.0)
|
|
283
|
+
character = max(combined, key=combined.get)
|
|
284
|
+
score_sum = sum(combined.values())
|
|
285
|
+
confidence = combined[character] / score_sum if score_sum > 0.0 else 0.0
|
|
286
|
+
return TextCandidate(character, float(confidence))
|