openocr-mlt-cuda 0.4.0__py3-none-any.whl

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.
@@ -0,0 +1,11 @@
1
+ """Offline, serial, FP32 ScriptMoE recognition."""
2
+ from .api import OpenOCRMLT
3
+ from .config import RecognitionConfig
4
+ from .types import RecognitionResult, DetectionResult, OCRLine, OCRResult
5
+ from .detection import TextDetector, DetectionConfig
6
+
7
+ __version__ = "0.4.0"
8
+ __all__ = ["OpenOCRMLT", "RecognitionConfig", "RecognitionResult", "DetectionResult", "OCRLine", "OCRResult", "TextDetector", "DetectionConfig"]
9
+
10
+ from .model_fetch import fetch_models
11
+ __all__.append("fetch_models")
@@ -0,0 +1,2 @@
1
+ from .cli import main
2
+ raise SystemExit(main())
openocr_mlt/api.py ADDED
@@ -0,0 +1,275 @@
1
+ """ScriptMoE recognition, optionally composed with pinned ONNX detection."""
2
+ import os
3
+ from dataclasses import asdict
4
+ from PIL import Image
5
+ import numpy as np
6
+ from .assets import resolve_assets
7
+ from .config import MODEL_ID, RecognitionConfig, resolve_device
8
+ from .processing import read_image, resize_max_side
9
+ from .types import RecognitionResult, DetectionResult, OCRLine, OCRResult
10
+ from .geometry import crop_quad
11
+
12
+ class _ConfiguredOCR:
13
+ def __init__(self, *, model_dir=None, weights_path=None, character_dict_path=None,
14
+ rec_backend="onnx", rec_device=None, rec_batch_size=1,
15
+ offline=True, preset="default", rec_threshold=None, max_side=None,
16
+ det_backend=None, det_model_dir=None,
17
+ det_model_id="ppocrv6-medium-det-bce-v1", det_device=None,
18
+ det_config=None, page_max_side=0, device=None, threads=4):
19
+ if rec_backend not in ("torch","onnx"):
20
+ raise ValueError("rec_backend must be torch or onnx")
21
+ if isinstance(threads,bool) or not isinstance(threads,int) or threads<1:
22
+ raise ValueError("threads must be a positive integer")
23
+ common = device if device is not None else "cpu"
24
+ rec_device = common if rec_device is None else rec_device
25
+ det_device = common if det_device is None else det_device
26
+ if det_backend is None and det_model_dir is not None:
27
+ det_backend = "onnx"
28
+ self.backend=rec_backend
29
+ self._closed=False
30
+ if rec_batch_size != 1:
31
+ raise ValueError("Only rec_batch_size=1 is supported; lists are processed serially")
32
+ if offline is not True:
33
+ raise ValueError("Only offline=True is supported; no online downloader is implemented")
34
+ if det_backend not in (None,"onnx"):
35
+ raise ValueError("det_backend must be None or 'onnx'")
36
+ if det_backend is None and det_model_dir is not None:
37
+ raise ValueError("det_model_dir requires det_backend='onnx'")
38
+ if det_backend=="onnx" and det_model_dir is None:
39
+ raise ValueError("det_backend='onnx' requires det_model_dir")
40
+ if isinstance(page_max_side,bool) or not isinstance(page_max_side,int) or page_max_side<0:
41
+ raise ValueError("page_max_side must be an integer >=0")
42
+ self.config = RecognitionConfig.from_preset(preset, rec_threshold=rec_threshold, max_side=max_side)
43
+ self.page_max_side=page_max_side
44
+ self._detector=None
45
+ if rec_backend=="torch":
46
+ from .recognition.torch_backend import TorchRecognizer
47
+ self.device=resolve_device(rec_device)
48
+ weights,dictionary=resolve_assets(model_dir,weights_path,character_dict_path)
49
+ self._recognizer=TorchRecognizer(weights,dictionary,self.device,self.config)
50
+ self.model_id=MODEL_ID
51
+ else:
52
+ if weights_path is not None:
53
+ raise ValueError("ONNX requires model_dir, not a safetensors weights_path")
54
+ from .recognition.onnx_backend import OnnxRecognizer
55
+ self._recognizer=OnnxRecognizer(model_dir,character_dict_path,rec_device,self.config,threads)
56
+ self.device=self._recognizer.device
57
+ self.model_id=self._recognizer.model_id
58
+ if det_backend=="onnx" and det_config is None:
59
+ from .detection import DetectionConfig
60
+ det_config=DetectionConfig(intra_threads=threads)
61
+ if det_backend=="onnx":
62
+ from .detection import TextDetector
63
+ self._detector=TextDetector(det_model_dir,model_id=det_model_id,device=det_device,config=det_config,offline=offline)
64
+
65
+ @property
66
+ def info(self):
67
+ info={"model_id":self.model_id,"backend":self.backend,"device":str(self.device),
68
+ "precision":"float32","effective_batch_size":1,"offline":True,"config":asdict(self.config)}
69
+ if self._detector is not None:
70
+ info["detector"]=self._detector.info
71
+ info["page_max_side"]=self.page_max_side
72
+ if self.backend=="onnx": info["recognizer"]=self._recognizer.info
73
+ return info
74
+
75
+ def _recognize_loaded(self,image,*,resize):
76
+ if self._closed: raise RuntimeError("OpenOCRMLT is closed")
77
+ height,width=image.shape[:2]
78
+ if resize:
79
+ image=resize_max_side(image,self.config.max_side)
80
+ raw,score,termination,steps=self._recognizer.recognize(image)
81
+ accepted=score>=self.config.rec_threshold
82
+ status="empty" if not raw else ("ok" if accepted else "filtered")
83
+ return RecognitionResult(raw if accepted else "",raw,score,status,termination,steps,
84
+ (width,height),self.model_id,self.backend,str(self.device))
85
+
86
+ def recognize(self,images,*,color_order="BGR"):
87
+ """Unchanged rec-only semantics, independent of any configured detector."""
88
+ if isinstance(images,(str,os.PathLike,Image.Image,np.ndarray)):
89
+ images=[images]
90
+ if not isinstance(images,(list,tuple)):
91
+ raise TypeError("images must be an image or a list/tuple of images")
92
+ loaded=[read_image(image,color_order) for image in images]
93
+ return [self._recognize_loaded(image,resize=True) for image in loaded]
94
+
95
+ def _detect_page(self,image):
96
+ if self._detector is None:
97
+ raise RuntimeError("Configure det_backend='onnx' and det_model_dir before detect/ocr")
98
+ height,width=image.shape[:2]
99
+ working=resize_max_side(image,self.page_max_side)
100
+ detections=self._detector._detect_bgr(working)
101
+ sy,sx=height/working.shape[0],width/working.shape[1]
102
+ mapped=[DetectionResult(tuple((min(max(x*sx,0.0),float(width)),min(max(y*sy,0.0),float(height))) for x,y in d.points),
103
+ d.score,d.source_id,d.model_id,d.backend,d.device) for d in detections]
104
+ return working,detections,mapped
105
+
106
+ def detect(self,image,*,color_order="BGR"):
107
+ _,_,mapped=self._detect_page(read_image(image,color_order))
108
+ return mapped
109
+
110
+ def ocr(self,image,*,color_order="BGR"):
111
+ bgr=read_image(image,color_order)
112
+ height,width=bgr.shape[:2]
113
+ working,detections,mapped=self._detect_page(bgr)
114
+ items=[]
115
+ for detection,original in zip(detections,mapped):
116
+ crop,error=crop_quad(working,detection.points)
117
+ if error is not None:
118
+ items.append(OCRLine(original,None,error))
119
+ else:
120
+ # Legacy whole-page pipeline resizes the page, not every crop.
121
+ result=self._recognize_loaded(crop,resize=False)
122
+ items.append(OCRLine(original,result,None))
123
+ return OCRResult((width,height),tuple(items),self.info)
124
+
125
+ def __call__(self,image,*,color_order="BGR"):
126
+ """One crop without a detector; one page with an explicitly configured detector."""
127
+ if getattr(self,"_detector",None) is not None:
128
+ return self.ocr(image,color_order=color_order)
129
+ return self.recognize([image],color_order=color_order)[0]
130
+
131
+ def close(self):
132
+ if self._detector is not None:
133
+ self._detector.close()
134
+ if self._recognizer is not None:
135
+ if hasattr(self._recognizer,"close"):self._recognizer.close()
136
+ elif hasattr(self._recognizer,"model"):self._recognizer.model=None
137
+ self._closed=True
138
+
139
+ def __enter__(self):
140
+ if self._closed:raise RuntimeError("OpenOCRMLT is closed")
141
+ return self
142
+
143
+ def __exit__(self,*args):
144
+ self.close()
145
+
146
+
147
+ class OpenOCRMLT(_ConfiguredOCR):
148
+ """Use an explicit model configuration or select a task with lazy setup."""
149
+
150
+ def __init__(
151
+ self, *, task=None, device=None, offline=None, cache_dir=None,
152
+ model_source=None, model_dir=None, det_model_dir=None, **options
153
+ ):
154
+ self._task = task
155
+ if task is None:
156
+ if cache_dir is not None or model_source is not None:
157
+ raise ValueError("cache_dir and model_source require a task")
158
+ super().__init__(
159
+ device=device, offline=True if offline is None else offline,
160
+ model_dir=model_dir, det_model_dir=det_model_dir, **options
161
+ )
162
+ return
163
+
164
+ if task not in ("ocr", "det", "rec"):
165
+ raise ValueError("task must be ocr, det, or rec")
166
+ if offline is not None and not isinstance(offline, bool):
167
+ raise TypeError("offline must be a bool")
168
+ if task == "det":
169
+ unsupported = set(options) - {"det_model_id", "det_config", "threads", "det_device"}
170
+ if unsupported or model_dir is not None:
171
+ raise ValueError("The detection task accepts detector configuration only")
172
+ elif task == "rec" and (det_model_dir is not None or options.get("det_backend") is not None):
173
+ raise ValueError("The recognition task accepts recognizer configuration only")
174
+ if options.get("rec_backend", "onnx") != "onnx":
175
+ raise ValueError("Task mode uses ONNX; provide explicit model paths for Torch")
176
+
177
+ self._requested_device = device if device is not None else "auto"
178
+ self._offline = bool(offline)
179
+ self._cache_dir = cache_dir
180
+ self._model_source = model_source
181
+ self._model_dir = model_dir
182
+ self._det_model_dir = det_model_dir
183
+ self._options = options
184
+ self._engine = None
185
+ self._closed = False
186
+
187
+ def _initialize(self):
188
+ from .model_fetch import fetch_models
189
+ from .onnx_runtime import _CUDAUnavailableError, resolve_ort_device
190
+
191
+ model_dir = self._model_dir
192
+ det_model_dir = self._det_model_dir
193
+ if (self._task != "det" and model_dir is None) or (
194
+ self._task != "rec" and det_model_dir is None
195
+ ):
196
+ assets = fetch_models(
197
+ source=self._model_source, cache_dir=self._cache_dir, offline=self._offline
198
+ )
199
+ if model_dir is None:
200
+ model_dir = assets["recognition_dir"]
201
+ if det_model_dir is None:
202
+ det_model_dir = assets["detector_dir"]
203
+
204
+ def create(device):
205
+ if self._task == "det":
206
+ from .detection import DetectionConfig, TextDetector
207
+
208
+ config = self._options.get("det_config")
209
+ if config is None:
210
+ config = DetectionConfig(intra_threads=self._options.get("threads", 4))
211
+ return TextDetector(
212
+ det_model_dir, config=config,
213
+ model_id=self._options.get("det_model_id", "ppocrv6-medium-det-bce-v1"),
214
+ device=self._options.get("det_device") or device,
215
+ )
216
+ return _ConfiguredOCR(
217
+ model_dir=model_dir,
218
+ det_model_dir=det_model_dir if self._task == "ocr" else None,
219
+ device=device, **self._options
220
+ )
221
+
222
+ requested = self._requested_device
223
+ device = resolve_ort_device(requested)
224
+ try:
225
+ return create(device)
226
+ except _CUDAUnavailableError:
227
+ if requested != "auto" or device == "cpu" or any(
228
+ self._options.get(key) for key in ("det_device", "rec_device")
229
+ ):
230
+ raise
231
+ return create("cpu")
232
+
233
+ def _task_engine(self, task):
234
+ if self._closed:
235
+ raise RuntimeError("OpenOCRMLT is closed")
236
+ if self._task != task:
237
+ raise ValueError(f"This instance was configured for task={self._task!r}")
238
+ if self._engine is None:
239
+ self._engine = self._initialize()
240
+ return self._engine
241
+
242
+ def __call__(self, image=None, *, image_path=None, color_order="BGR"):
243
+ if (image is None) == (image_path is None):
244
+ raise ValueError("Provide exactly one image or image_path")
245
+ image = image if image_path is None else image_path
246
+ if self._task is None:
247
+ return super().__call__(image, color_order=color_order)
248
+ method = {"ocr": self.ocr, "det": self.detect, "rec": self.recognize}[self._task]
249
+ return method(image, color_order=color_order)
250
+
251
+ def recognize(self, images, *, color_order="BGR"):
252
+ engine = super() if self._task is None else self._task_engine("rec")
253
+ return engine.recognize(images, color_order=color_order)
254
+
255
+ def detect(self, image, *, color_order="BGR"):
256
+ engine = super() if self._task is None else self._task_engine("det")
257
+ return engine.detect(image, color_order=color_order)
258
+
259
+ def ocr(self, image, *, color_order="BGR"):
260
+ engine = super() if self._task is None else self._task_engine("ocr")
261
+ return engine.ocr(image, color_order=color_order)
262
+
263
+ @property
264
+ def info(self):
265
+ if self._task is None:
266
+ return super().info
267
+ info = {} if self._engine is None else self._engine.info
268
+ return {**info, "task": self._task, "requested_device": self._requested_device}
269
+
270
+ def close(self):
271
+ if self._task is None:
272
+ return super().close()
273
+ if self._engine is not None:
274
+ self._engine.close()
275
+ self._closed = True
@@ -0,0 +1,26 @@
1
+ # Third-Party Model Sources and Licensing
2
+
3
+ ## ScriptMoE Recognition
4
+
5
+ Recognition models are derived from [ScriptMoE](https://github.com/YesianRohn/ScriptMoE) at upstream revision `5dde99867aaee6b4af3e75a3020b225eab17ac53`. ONNX conversion preserves the character dictionary and autoregressive decoding rules and uses KV caching and optimized MoE execution.
6
+
7
+ | Artifact | SHA256 |
8
+ |---|---|
9
+ | Original recognition weights | 60c74a924404b652c3a9550ec370c74d78850151785341729bdac268c014650d |
10
+ | Character dictionary | 7b4eb2ca5fd6c9b6c6ecf423f69d35d8da48088697303e53624af196ee9ab6c0 |
11
+
12
+ ## PP-OCRv6 Medium Detection
13
+
14
+ The detector is distributed by Paddle as [PP-OCRv6_medium_det_onnx.tar](https://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/tmp/PP-OCRv6_medium_det_onnx.tar).
15
+
16
+ | Property | Value |
17
+ |---|---|
18
+ | File | inference.onnx |
19
+ | Size | 62,026,563 bytes |
20
+ | SHA256 | a0d14865d3a297970899b5c246f0e47c43879bb406c3a1fa4e505aba84b134e4 |
21
+
22
+ The accompanying Python package adapts detection preprocessing and postprocessing from [ppocrv6_onnx](https://github.com/AIwork4me/ppocrv6_onnx), revision `d638eb0bb5d8222522abcc1bae4394cbeeb636f0`, under its MIT license. The package includes the license text for that code.
23
+
24
+ ## Licensing Status
25
+
26
+ A unified redistribution license has not been specified for this combined model bundle. The MIT license of the detection adapter applies to that code; model weights have separate licensing requirements.
@@ -0,0 +1,37 @@
1
+ {
2
+ "schema_version": 1,
3
+ "models": {
4
+ "ppocrv6-tiny-det-bce-v1": {
5
+ "filename": "inference.onnx",
6
+ "size": 1780621,
7
+ "sha256": "a56a3430a96a6c691f8bfbbb297208bb9573c3d70083d6382071cbd92d0a152e",
8
+ "source_url": "https://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/tmp/PP-OCRv6_tiny_det_onnx.tar",
9
+ "source_revision": null,
10
+ "artifact_etag": "700445c8a6ebcaeac88d4622ddb68dba",
11
+ "runtime_profile": "ppocrv6-reference-v1",
12
+ "config_sha256": "626e4e3b45bd2f943d2282114b461ef5804f4620ae54d7ff7ff9fb6365f3a608",
13
+ "license_status": "official family model card states Apache-2.0; BCE binary redistribution not independently resolved"
14
+ },
15
+ "ppocrv5-server-det-hf-v1": {
16
+ "filename": "inference.onnx",
17
+ "size": 88116791,
18
+ "sha256": "10803475a591f7dc623e24670fb5752ec94d39a1f8cf069aac1b6f0ce19cfc85",
19
+ "source_url": "https://huggingface.co/PaddlePaddle/PP-OCRv5_server_det_onnx",
20
+ "source_revision": "dcf248c4dcc064c6d030db241255f9e8fbc6733a",
21
+ "runtime_profile": "ppocrv6-reference-v1",
22
+ "license_status": "official model card Apache-2.0; no public release in this task"
23
+ },
24
+ "ppocrv6-medium-det-bce-v1": {
25
+ "name": "inference.onnx",
26
+ "size": 62026563,
27
+ "sha256": "a0d14865d3a297970899b5c246f0e47c43879bb406c3a1fa4e505aba84b134e4",
28
+ "filename": "inference.onnx",
29
+ "source_url": "https://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/tmp/PP-OCRv6_medium_det_onnx.tar",
30
+ "source_revision": null,
31
+ "source_etag": "\"33e0718cf119c8fcc545eb249bc69e06\"",
32
+ "archive_sha256": "46849788192d7882e52440d81cdfa437156f433368411c04b832ffa5433e5418",
33
+ "runtime_profile": "ppocrv6-reference-v1",
34
+ "license_status": "Source attribution preserved; no public redistribution performed"
35
+ }
36
+ }
37
+ }