Skip to content

Commit 50ae7f3

Browse files
authored
feat: paddleocr v6 (#2320)
* feat: intro PP-OCRv6 * perf: enchante OCR components * feat: add 'ppocrv6_small' to OCR model options * feat: optimize PP-OCRv6 inference, add directdml and warmups * perf: revoke bad changes * feat: update model path handling in ONNX OCR integration * fix(ocr): fix OCR model hot reload config not taking effect * feat(ocr): add support for ppocrv6_tiny model * fix: update self logging module * fix(ocr): remove duplicate shadowed log import in predict_base * refactor(ocr): enforce strict list pairing using zip(strict=True) * fix(ocr): support medium size fallback and handle missing dict file in OnnxOcrParam * fix(ocr): invoke cleanup during OCR reinitialization to prevent memory leaks * style(ocr): add type annotations for ocr_model_size and to_dict in OnnxOcrParam * refactor(ocr): replace **kwargs with explicit params and add type annotations/docstring for ocr() * refactor(ocr): use ppocrv6 small model only * refactor(ocr): remove redundant ppocrv6 model normalization
1 parent 4db7903 commit 50ae7f3

14 files changed

Lines changed: 508 additions & 148 deletions

src/one_dragon/base/config/basic_model_config.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from one_dragon.base.config.config_item import ConfigItem
22
from one_dragon.base.config.yaml_config import YamlConfig
3-
from one_dragon.base.matcher.ocr.onnx_ocr_matcher import DEFAULT_OCR_MODEL_NAME, get_ocr_model_dir, \
4-
get_ocr_download_url_github, get_ocr_download_url_gitee, get_final_file_list
3+
from one_dragon.base.matcher.ocr.onnx_ocr_matcher import (
4+
DEFAULT_OCR_MODEL_NAME,
5+
PPOCRV6_MODEL_NAME,
6+
get_final_file_list,
7+
get_ocr_download_url_gitee,
8+
get_ocr_download_url_github,
9+
get_ocr_model_dir,
10+
)
511
from one_dragon.base.web.common_downloader import CommonDownloaderParam
612

713

@@ -34,7 +40,7 @@ def using_old_model(self) -> bool:
3440
pass
3541

3642
def get_ocr_opts() -> list[ConfigItem]:
37-
models_list = [DEFAULT_OCR_MODEL_NAME]
43+
models_list = [DEFAULT_OCR_MODEL_NAME, PPOCRV6_MODEL_NAME]
3844
config_list: list[ConfigItem] = []
3945
for model in models_list:
4046
model_dir = get_ocr_model_dir(model)

src/one_dragon/base/matcher/ocr/onnx_ocr_matcher.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44
from collections.abc import Callable
55
from logging import DEBUG
6+
from typing import Any
67

78
from cv2.typing import MatLike
89

@@ -17,6 +18,7 @@
1718
from one_dragon.utils.log_utils import log
1819

1920
DEFAULT_OCR_MODEL_NAME: str = 'ppocrv5'
21+
PPOCRV6_MODEL_NAME: str = 'ppocrv6'
2022
GITHUB_DOWNLOAD_URL: str = 'https://github.com/OneDragon-Anything/OneDragon-Env/releases/download'
2123
GITEE_DOWNLOAD_URL: str = 'https://gitee.com/OneDragon-Anything/OneDragon-Env/releases/download'
2224

@@ -37,20 +39,35 @@ def get_ocr_download_url(website: str, ocr_model_name: str) -> str:
3739
return f'{website}/{ocr_model_name}/{ocr_model_name}.zip'
3840

3941

42+
def get_ocr_model_dict_name(ocr_model_name: str) -> str | None:
43+
"""
44+
获取模型对应的字典文件名。直接扫描本地文件夹中的字典文件。
45+
"""
46+
base_dir = get_ocr_model_dir(ocr_model_name)
47+
if os.path.exists(base_dir):
48+
for f in os.listdir(base_dir):
49+
if f.endswith('_dict.txt'):
50+
return f
51+
return None
52+
53+
4054
def get_final_file_list(ocr_model_name: str) -> list[str]:
4155
"""
4256
下载成功后 整个模型的所有文件
4357
:param ocr_model_name: 模型名称
4458
:return:
4559
"""
4660
base_dir = get_ocr_model_dir(ocr_model_name)
47-
return [
61+
files = [
4862
os.path.join(base_dir, 'det.onnx'),
4963
os.path.join(base_dir, 'rec.onnx'),
5064
os.path.join(base_dir, 'cls.onnx'),
51-
os.path.join(base_dir, 'ppocrv5_dict.txt'),
5265
os.path.join(base_dir, 'simfang.ttf'),
5366
]
67+
dict_name = get_ocr_model_dict_name(ocr_model_name)
68+
if dict_name is not None:
69+
files.append(os.path.join(base_dir, dict_name))
70+
return files
5471

5572

5673
class OnnxOcrParam:
@@ -65,14 +82,20 @@ def __init__(
6582
det_model_name: str = 'det.onnx',
6683
rec_model_name: str = 'rec.onnx',
6784
cls_model_name: str = 'cls.onnx',
68-
dict_name: str = 'ppocrv5_dict.txt',
85+
dict_name: str | None = None,
6986
font_name: str = 'simfang.ttf',
7087
use_gpu: bool = False,
7188
use_angle_cls: bool = False,
7289
det_limit_side_len: float = 960.0,
90+
ocr_model_size: str | None = None,
7391
):
7492
self.ocr_model_name: str = ocr_model_name
7593
self.models_dir: str = get_ocr_model_dir(ocr_model_name)
94+
if dict_name is None:
95+
dict_name = get_ocr_model_dict_name(ocr_model_name)
96+
if dict_name is None:
97+
# 首次运行未下载时,根据模型名推导一个默认的字典文件名,避免崩溃
98+
dict_name = f"{self.ocr_model_name}_dict.txt"
7699
# ===================================================================
77100
# I. 设备与性能 (Device & Performance)
78101
# ===================================================================
@@ -91,13 +114,16 @@ def __init__(
91114
# III. 核心功能开关 (Core Feature Switches)
92115
# ===================================================================
93116
self.use_angle_cls = use_angle_cls # 是否加载并使用方向分类模型
117+
if self.ocr_model_name == PPOCRV6_MODEL_NAME or ocr_model_size is None:
118+
ocr_model_size = 'small'
119+
self.ocr_model_size: str | None = ocr_model_size
94120

95121
# ===================================================================
96122
# IV. 文字检测超参数 (Detection Hyperparameters)
97123
# ===================================================================
98124
self.det_limit_side_len = det_limit_side_len # 输入图像的长边限制
99125

100-
def to_dict(self):
126+
def to_dict(self) -> dict[str, Any]:
101127
"""将OCR配置转换为字典格式"""
102128
return {
103129
'use_gpu': self.use_gpu,
@@ -108,6 +134,7 @@ def to_dict(self):
108134
'vis_font_path': self.vis_font_path,
109135
'use_angle_cls': self.use_angle_cls,
110136
'det_limit_side_len': self.det_limit_side_len,
137+
'ocr_model_size': self.ocr_model_size,
111138
}
112139

113140

@@ -213,6 +240,15 @@ def init_model(
213240
log.error('OCR模型加载出错', exc_info=True)
214241
return False
215242

243+
def cleanup(self) -> None:
244+
"""
245+
释放底层模型实例资源,协助 GC 回收 ONNX 会话
246+
"""
247+
with self._init_lock:
248+
if self._model is not None:
249+
del self._model
250+
self._model = None
251+
216252
def update_use_gpu(self, use_gpu: bool) -> None:
217253
"""
218254
更新是否使用GPU
@@ -358,7 +394,7 @@ def match_words(
358394
"""
359395
all_match_result: dict = self.run_ocr(image, threshold, merge_line_distance=merge_line_distance)
360396
match_key = set()
361-
for k in all_match_result.keys():
397+
for k in all_match_result:
362398
for w in words:
363399
ocr_result: str = k
364400
ocr_target = gt(w, 'ocr')
@@ -491,7 +527,7 @@ def _emit_overlay_vision_from_ocr_results(
491527
return
492528

493529
offset_x, offset_y = bus.crop_offset
494-
for i, result in enumerate(ocr_results[:60]):
530+
for result in ocr_results[:60]:
495531
label = str(result.data or "").strip()
496532
if len(label) > 32:
497533
label = label[:29] + "..."

src/one_dragon/base/operation/one_dragon_context.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,22 @@ def init_ocr(self) -> None:
484484
初始化OCR
485485
:return:
486486
"""
487-
self.ocr.update_use_gpu(self.model_config.ocr_use_gpu)
487+
# 清理旧实例资源
488+
if hasattr(self, 'ocr') and self.ocr is not None:
489+
if hasattr(self.ocr, 'cleanup'):
490+
self.ocr.cleanup()
491+
492+
self.ocr = OnnxOcrMatcher(
493+
OnnxOcrParam(
494+
ocr_model_name=self.model_config.ocr,
495+
use_gpu=self.model_config.ocr_use_gpu,
496+
det_limit_side_len=max(self.project_config.screen_standard_width, self.project_config.screen_standard_height),
497+
)
498+
)
499+
self.ocr.overlay_debug_bus = self.overlay_debug_bus
500+
self.ocr_service.ocr_matcher = self.ocr
501+
if 'cv_service' in self.__dict__:
502+
self.cv_service.ocr = self.ocr
488503
self.ocr.init_model(
489504
ghproxy_url=self.env_config.gh_proxy_url if self.env_config.is_gh_proxy else None,
490505
proxy_url=self.env_config.personal_proxy if self.env_config.is_personal_proxy else None,

src/onnxocr/inference_engine.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import os
2+
import platform
3+
from collections.abc import Sequence
4+
from enum import Enum
5+
from typing import Any
6+
7+
import onnxruntime
8+
9+
from onnxocr.logger import get_logger
10+
11+
log = get_logger("inference_engine")
12+
13+
14+
Provider = str | tuple[str, dict[str, Any]]
15+
16+
17+
InferenceSession = onnxruntime.InferenceSession
18+
SessionOptions = onnxruntime.SessionOptions
19+
GraphOptimizationLevel = onnxruntime.GraphOptimizationLevel
20+
21+
22+
class EP(Enum):
23+
CPU = "CPUExecutionProvider"
24+
CUDA = "CUDAExecutionProvider"
25+
DIRECTML = "DmlExecutionProvider"
26+
CANN = "CANNExecutionProvider"
27+
28+
29+
DEFAULT_CPU_EP_CFG: dict[str, Any] = {}
30+
DEFAULT_CUDA_EP_CFG: dict[str, Any] = {
31+
"cudnn_conv_algo_search": "DEFAULT",
32+
"device_id": 0,
33+
}
34+
DEFAULT_DML_EP_CFG: dict[str, Any] = {}
35+
DEFAULT_CANN_EP_CFG: dict[str, Any] = {}
36+
37+
38+
def get_available_providers() -> list[str]:
39+
return onnxruntime.get_available_providers()
40+
41+
42+
def get_device() -> str:
43+
return onnxruntime.get_device()
44+
45+
46+
def is_session(value: Any) -> bool:
47+
return isinstance(value, InferenceSession)
48+
49+
50+
def build_providers(
51+
use_gpu: bool = False,
52+
gpu_id: int = 0,
53+
providers: Sequence[Provider] | None = None,
54+
) -> list[Provider]:
55+
if providers is not None:
56+
return list(providers)
57+
58+
if use_gpu:
59+
available = get_available_providers()
60+
if EP.CUDA.value in available:
61+
cuda_cfg = dict(DEFAULT_CUDA_EP_CFG)
62+
cuda_cfg["device_id"] = gpu_id
63+
return [(EP.CUDA.value, cuda_cfg), EP.CPU.value]
64+
elif EP.DIRECTML.value in available:
65+
return [EP.DIRECTML.value, EP.CPU.value]
66+
elif EP.CANN.value in available:
67+
return [EP.CANN.value, EP.CPU.value]
68+
return [EP.CPU.value]
69+
70+
71+
def build_providers_from_engine_cfg(engine_cfg: Any) -> list[Provider]:
72+
"""Build ONNXRuntime execution providers from the common engine_cfg shape.
73+
74+
RapidTable, RapidLayout and RapidDoc use slightly different vendored copies of
75+
an ``engine_cfg`` object. This function is the single compatibility point so
76+
downstream vendors only need to customize provider behavior here.
77+
"""
78+
79+
available = get_available_providers()
80+
providers: list[Provider] = [(EP.CPU.value, _cfg_dict(engine_cfg, "cpu_ep_cfg", DEFAULT_CPU_EP_CFG))]
81+
82+
if _cfg_bool(engine_cfg, "use_cuda") and EP.CUDA.value in available:
83+
providers.insert(0, (EP.CUDA.value, _cfg_dict(engine_cfg, "cuda_ep_cfg", DEFAULT_CUDA_EP_CFG)))
84+
85+
if _cfg_bool(engine_cfg, "use_dml") and _is_windows() and EP.DIRECTML.value in available:
86+
providers.insert(0, (EP.DIRECTML.value, _cfg_dict(engine_cfg, "dm_ep_cfg", DEFAULT_DML_EP_CFG)))
87+
88+
if _cfg_bool(engine_cfg, "use_cann") and EP.CANN.value in available:
89+
providers.insert(0, (EP.CANN.value, _cfg_dict(engine_cfg, "cann_ep_cfg", DEFAULT_CANN_EP_CFG)))
90+
91+
return providers
92+
93+
94+
class ProviderConfig:
95+
"""Compatibility wrapper used by vendored RapidAI modules."""
96+
97+
def __init__(self, engine_cfg: Any):
98+
self.engine_cfg = engine_cfg
99+
100+
def get_ep_list(self) -> list[Provider]:
101+
return build_providers_from_engine_cfg(self.engine_cfg)
102+
103+
def verify_providers(self, session_providers: Sequence[str]) -> None:
104+
if not session_providers:
105+
raise ValueError("Session providers is empty.")
106+
107+
108+
def _cfg_bool(engine_cfg: Any, key: str, default: bool = False) -> bool:
109+
value = _cfg_get(engine_cfg, key, default)
110+
return bool(value)
111+
112+
113+
def _cfg_dict(engine_cfg: Any, key: str, default: dict[str, Any]) -> dict[str, Any]:
114+
result = dict(default)
115+
value = _cfg_get(engine_cfg, key, default)
116+
if value is not None:
117+
result.update(dict(value))
118+
119+
prefix = f"{key}."
120+
if hasattr(engine_cfg, "items"):
121+
for cfg_key, cfg_value in engine_cfg.items():
122+
if isinstance(cfg_key, str) and cfg_key.startswith(prefix):
123+
result[cfg_key[len(prefix) :]] = cfg_value
124+
return result
125+
126+
127+
def _cfg_get(engine_cfg: Any, key: str, default: Any = None) -> Any:
128+
if engine_cfg is None:
129+
return default
130+
if hasattr(engine_cfg, "get"):
131+
return engine_cfg.get(key, default)
132+
return getattr(engine_cfg, key, default)
133+
134+
135+
def _is_windows() -> bool:
136+
return platform.system() == "Windows"
137+
138+
139+
def _default_session_options() -> SessionOptions:
140+
"""Build a SessionOptions with graph optimization and memory pattern enabled.
141+
142+
These settings provide 10-30% CPU inference speedup with no accuracy impact.
143+
"""
144+
opts = SessionOptions()
145+
opts.graph_optimization_level = GraphOptimizationLevel.ORT_ENABLE_ALL
146+
opts.enable_mem_pattern = True
147+
return opts
148+
149+
150+
def create_session(
151+
model_path: str,
152+
providers: Sequence[Provider] | None = None,
153+
use_gpu: bool = False,
154+
gpu_id: int = 0,
155+
sess_options: SessionOptions | None = None,
156+
) -> InferenceSession:
157+
if not os.path.exists(model_path):
158+
raise FileNotFoundError(
159+
f"Model file not found: {model_path}. "
160+
f"Please download models first: python scripts/download_models.py"
161+
)
162+
if sess_options is None:
163+
sess_options = _default_session_options()
164+
session_providers = build_providers(
165+
use_gpu=use_gpu,
166+
gpu_id=gpu_id,
167+
providers=providers,
168+
)
169+
log.info("Creating ONNX session: {}, providers={}", model_path, session_providers)
170+
try:
171+
return InferenceSession(
172+
model_path,
173+
sess_options=sess_options,
174+
providers=session_providers,
175+
)
176+
except Exception as e:
177+
log.error("Failed to load model: {}, error: {}", model_path, e)
178+
raise

0 commit comments

Comments
 (0)