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.
Files changed (33) hide show
  1. lipla_jp-0.2.0/LICENSE +21 -0
  2. lipla_jp-0.2.0/PKG-INFO +90 -0
  3. lipla_jp-0.2.0/README.md +65 -0
  4. lipla_jp-0.2.0/lipla/__init__.py +5 -0
  5. lipla_jp-0.2.0/lipla/configs/characters.yml +208 -0
  6. lipla_jp-0.2.0/lipla/core/fixed_field_recognizer.py +286 -0
  7. lipla_jp-0.2.0/lipla/core/image_processing.py +164 -0
  8. lipla_jp-0.2.0/lipla/core/license_plate_recognizer.py +385 -0
  9. lipla_jp-0.2.0/lipla/core/ocr_result_parser.py +373 -0
  10. lipla_jp-0.2.0/lipla/core/plate_normalizer.py +110 -0
  11. lipla_jp-0.2.0/lipla/core/pose_postprocessor.py +123 -0
  12. lipla_jp-0.2.0/lipla/core/recognition_result.py +313 -0
  13. lipla_jp-0.2.0/lipla/inferencers/ec_pose.py +326 -0
  14. lipla_jp-0.2.0/lipla/inferencers/execution_provider.py +91 -0
  15. lipla_jp-0.2.0/lipla/inferencers/model_loader.py +52 -0
  16. lipla_jp-0.2.0/lipla/inferencers/ppocr.py +679 -0
  17. lipla_jp-0.2.0/lipla_jp.egg-info/PKG-INFO +90 -0
  18. lipla_jp-0.2.0/lipla_jp.egg-info/SOURCES.txt +31 -0
  19. lipla_jp-0.2.0/lipla_jp.egg-info/dependency_links.txt +1 -0
  20. lipla_jp-0.2.0/lipla_jp.egg-info/requires.txt +11 -0
  21. lipla_jp-0.2.0/lipla_jp.egg-info/top_level.txt +1 -0
  22. lipla_jp-0.2.0/pyproject.toml +62 -0
  23. lipla_jp-0.2.0/setup.cfg +4 -0
  24. lipla_jp-0.2.0/tests/test_execution_provider.py +124 -0
  25. lipla_jp-0.2.0/tests/test_fixed_field_recognizer.py +77 -0
  26. lipla_jp-0.2.0/tests/test_huggingface_space.py +85 -0
  27. lipla_jp-0.2.0/tests/test_image_processing.py +66 -0
  28. lipla_jp-0.2.0/tests/test_inference_validation.py +166 -0
  29. lipla_jp-0.2.0/tests/test_model_loading.py +178 -0
  30. lipla_jp-0.2.0/tests/test_ocr_result_parser.py +92 -0
  31. lipla_jp-0.2.0/tests/test_pose_postprocessor.py +106 -0
  32. lipla_jp-0.2.0/tests/test_recognition_result.py +70 -0
  33. 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.
@@ -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&amp;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)
@@ -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&amp;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,5 @@
1
+ """Lipla-jpの公開API。"""
2
+
3
+ from .core.license_plate_recognizer import Recognizer, Result
4
+
5
+ __all__ = ["Result", "Recognizer"]
@@ -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))