ocr-pos.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. #!/usr/bin/env python3
  2. """
  3. 使用仓库内 ``python/RapidOCR`` 对**图片**做 OCR。
  4. ``ocr_find_text_center``:在单行识别结果中找完整包含目标子串的框(与 CLI 一致)。
  5. ``ocr_find_text_center_allowing_note_title_wrapped_across_two_ocr_lines``(笔记标题锚点):
  6. 仅使用用户字符串 **第一逻辑行**(首个换行符之前);第二行及以后 **不参与** 匹配与几何计算。
  7. 对第一行先做 **NFKC**,再抠掉难以 OCR 的字符得到多段可识别文本。整图 **只跑一次** OCR,
  8. 每段取面积最小匹配框中心;同一段内多段则取首末段中心的中点作为代表点(不再跨多逻辑行拼接)。
  9. 与 Playwright / 浏览器 DOM 无关;仅输入图片路径或字节。
  10. 用法:``python workplace/ocr-pos.py <目标文字> <图片路径>``
  11. 成功时 stdout 一行:``cx cy``(整数)。
  12. 可调模块级常量:``OCR_RAPIDOCR_GLOBAL_MAX_SIDE_LEN``、``OCR_RAPIDOCR_INFERENCE_ENGINE_NAME``、
  13. ``OCR_RAPIDOCR_ONNX_RUNTIME_USE_CUDA``(仅 ``onnxruntime`` 引擎时对应 ``EngineConfig.onnxruntime.use_cuda``)。
  14. 可选推理依赖见 ``python/RapidOCR/requirements-inference-*.txt``。
  15. """
  16. from __future__ import annotations
  17. import sys
  18. import unicodedata
  19. from pathlib import Path
  20. from typing import Any
  21. import cv2
  22. import numpy as np
  23. _REPO_ROOT = Path(__file__).resolve().parent.parent
  24. _RAPIDOCR_PKG_ROOT = _REPO_ROOT / "python" / "RapidOCR" / "python"
  25. if str(_RAPIDOCR_PKG_ROOT) not in sys.path:
  26. sys.path.insert(0, str(_RAPIDOCR_PKG_ROOT))
  27. REPO_ROOT: Path = _REPO_ROOT
  28. # OCR_RAPIDOCR_GLOBAL_MAX_SIDE_LEN: int = 2000 # 提速可试 1280 或 960(顶栏 OCR 常够用;过小易丢小字)
  29. # OCR_RAPIDOCR_INFERENCE_ENGINE_NAME: str = "onnxruntime" # 可选 openvino(Intel CPU 常较快)/ tensorrt(需 NVIDIA 环境)
  30. # OCR_RAPIDOCR_ONNX_RUNTIME_USE_CUDA: bool = False # 已装 onnxruntime-gpu 且要用 GPU 时改为 True
  31. OCR_RAPIDOCR_GLOBAL_MAX_SIDE_LEN: int = 2000 # 提速可试 1280 或 960(顶栏 OCR 常够用;过小易丢小字)
  32. OCR_RAPIDOCR_INFERENCE_ENGINE_NAME: str = "onnxruntime" # 与 requirements 中 onnxruntime 一致;本机已装 openvino 可改 openvino
  33. OCR_RAPIDOCR_ONNX_RUNTIME_USE_CUDA: bool = False # 已装 onnxruntime-gpu 且要用 GPU 时改为 True
  34. # 下方为 RapidOCR 单例缓存(勿改此变量)。可切换的只有上面三个常量;改完后须重启进程才会重新建引擎。
  35. _ocr_engine_singleton = None
  36. def _build_rapid_ocr_params_dictionary() -> dict[str, Any]:
  37. from rapidocr.utils.typings import EngineType
  38. inference_engine_name_normalized = (
  39. OCR_RAPIDOCR_INFERENCE_ENGINE_NAME.strip().lower()
  40. )
  41. rapid_ocr_params: dict[str, Any] = {
  42. "Global.max_side_len": int(OCR_RAPIDOCR_GLOBAL_MAX_SIDE_LEN),
  43. "Global.use_cls": False,
  44. }
  45. if inference_engine_name_normalized == "openvino":
  46. rapid_ocr_params["Det.engine_type"] = EngineType.OPENVINO
  47. rapid_ocr_params["Cls.engine_type"] = EngineType.OPENVINO
  48. rapid_ocr_params["Rec.engine_type"] = EngineType.OPENVINO
  49. return rapid_ocr_params
  50. if inference_engine_name_normalized == "tensorrt":
  51. rapid_ocr_params["Det.engine_type"] = EngineType.TENSORRT
  52. rapid_ocr_params["Cls.engine_type"] = EngineType.TENSORRT
  53. rapid_ocr_params["Rec.engine_type"] = EngineType.TENSORRT
  54. return rapid_ocr_params
  55. rapid_ocr_params["Det.engine_type"] = EngineType.ONNXRUNTIME
  56. rapid_ocr_params["Cls.engine_type"] = EngineType.ONNXRUNTIME
  57. rapid_ocr_params["Rec.engine_type"] = EngineType.ONNXRUNTIME
  58. rapid_ocr_params["EngineConfig.onnxruntime.use_cuda"] = bool(
  59. OCR_RAPIDOCR_ONNX_RUNTIME_USE_CUDA
  60. )
  61. return rapid_ocr_params
  62. def _get_rapid_ocr():
  63. global _ocr_engine_singleton
  64. if _ocr_engine_singleton is None:
  65. from rapidocr import RapidOCR
  66. _ocr_engine_singleton = RapidOCR(
  67. params=_build_rapid_ocr_params_dictionary(),
  68. )
  69. return _ocr_engine_singleton
  70. def _rapidocr_input_from_path_bytes_or_bgr_numpy(
  71. image: str | Path | bytes | np.ndarray,
  72. ):
  73. if isinstance(image, np.ndarray):
  74. return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
  75. return image
  76. def _quad_center_xy(box) -> tuple[float, float]:
  77. """
  78. ``box`` 为四角点 (4,2);返回几何中心点。
  79. """
  80. xs = [float(p[0]) for p in box]
  81. ys = [float(p[1]) for p in box]
  82. return sum(xs) / len(xs), sum(ys) / len(ys)
  83. def _normalize_ocr_match_string(s: str) -> str:
  84. """去掉首尾空白并去掉内部空白;再将全角标点与半角对齐,便于 metadata 与 RapidOCR 输出对齐。"""
  85. collapsed_whitespace_string = "".join((s or "").strip().split())
  86. punctuation_aligned_string = (
  87. collapsed_whitespace_string.replace(",", ",")
  88. .replace("?", "?")
  89. .replace("(", "(")
  90. .replace(")", ")")
  91. .replace(":", ":")
  92. .replace(";", ";")
  93. .replace("!", "!")
  94. )
  95. return punctuation_aligned_string.casefold()
  96. def _ocr_line_contains_full_target(ocr_line: str, target_text: str) -> bool:
  97. """
  98. 仅当 **整段** ``target_text`` 连续出现在本行识别结果中才算匹配。
  99. 禁止再用「识别串是目标子串」(如仅「小红书」)去碰「搜索小红书」,
  100. 避免中心点落到不完整的字块上(例如「小红书上」里截出的「小红书」)。
  101. """
  102. line = _normalize_ocr_match_string(ocr_line)
  103. target = _normalize_ocr_match_string(target_text)
  104. if not target:
  105. return False
  106. return target in line
  107. def _ocr_dom_anchor_line_matches_normalized_text(
  108. ocr_line: str,
  109. anchor_norm: str,
  110. ) -> bool:
  111. """与 ``clear-input`` 一致:多字用子串匹配,单字要求整行一致。"""
  112. norm = _normalize_ocr_match_string(ocr_line)
  113. if not anchor_norm:
  114. return False
  115. if len(anchor_norm) >= 2:
  116. return anchor_norm in norm
  117. return norm == anchor_norm
  118. def ocr_smallest_line_center_for_dom_text(
  119. image: str | Path | bytes | np.ndarray,
  120. anchor_dom_text: str,
  121. ) -> tuple[int, int] | None:
  122. """
  123. 在 OCR 结果中找归一后包含 ``anchor_dom_text`` 的行;多行命中时取**框面积最小**的一行,
  124. 返回该行中心(屏幕/图像像素)。与 ``clear-input`` 锚点规则一致。
  125. """
  126. anchor_norm = _normalize_ocr_match_string(anchor_dom_text)
  127. if not anchor_norm:
  128. return None
  129. ocr = _get_rapid_ocr()
  130. result = ocr(_rapidocr_input_from_path_bytes_or_bgr_numpy(image))
  131. if result is None or not result.txts or result.boxes is None:
  132. return None
  133. anchor_indices: list[int] = []
  134. for i, txt in enumerate(result.txts):
  135. if _ocr_dom_anchor_line_matches_normalized_text(txt, anchor_norm):
  136. anchor_indices.append(i)
  137. if not anchor_indices:
  138. return None
  139. def _box_area(idx: int) -> float:
  140. box = result.boxes[idx]
  141. xs = [float(p[0]) for p in box]
  142. ys = [float(p[1]) for p in box]
  143. return (max(xs) - min(xs)) * (max(ys) - min(ys))
  144. best_i = min(anchor_indices, key=_box_area)
  145. cx, cy = _quad_center_xy(result.boxes[best_i])
  146. return int(round(cx)), int(round(cy))
  147. _CJK_AND_COMMON_TITLE_PUNCTUATION_CHARACTER_FROZEN_SET = frozenset(
  148. ",。、:;?!""''()《》·—…-—「」『』【】〈〉&%#@…—",
  149. )
  150. def _is_single_character_treated_as_recognizable_by_rapidocr_line_text_anchor(
  151. single_unicode_character: str,
  152. ) -> bool:
  153. if len(single_unicode_character) != 1:
  154. return False
  155. code_point_integer = ord(single_unicode_character)
  156. if single_unicode_character.isascii():
  157. return single_unicode_character.isalnum() or single_unicode_character in " \t.-_/:&!?"
  158. if (
  159. 0x4E00 <= code_point_integer <= 0x9FFF
  160. or 0x3400 <= code_point_integer <= 0x4DBF
  161. or 0x20000 <= code_point_integer <= 0x2CEAF
  162. ):
  163. return True
  164. if single_unicode_character in _CJK_AND_COMMON_TITLE_PUNCTUATION_CHARACTER_FROZEN_SET:
  165. return True
  166. return False
  167. def _split_user_anchor_string_into_recognizable_text_segment_string_list(
  168. raw_anchor_string_from_user: str,
  169. ) -> list[str]:
  170. unicode_nfkc_normalized_anchor_string = unicodedata.normalize(
  171. "NFKC",
  172. raw_anchor_string_from_user or "",
  173. )
  174. contiguous_recognizable_character_list: list[str] = []
  175. recognizable_text_segment_string_list: list[str] = []
  176. for single_character in unicode_nfkc_normalized_anchor_string:
  177. if _is_single_character_treated_as_recognizable_by_rapidocr_line_text_anchor(
  178. single_character,
  179. ):
  180. contiguous_recognizable_character_list.append(single_character)
  181. else:
  182. if contiguous_recognizable_character_list:
  183. recognizable_text_segment_string_list.append(
  184. "".join(contiguous_recognizable_character_list),
  185. )
  186. contiguous_recognizable_character_list = []
  187. if contiguous_recognizable_character_list:
  188. recognizable_text_segment_string_list.append(
  189. "".join(contiguous_recognizable_character_list),
  190. )
  191. return recognizable_text_segment_string_list
  192. def _split_anchor_raw_string_into_logical_line_then_recognizable_segment_nested_string_list(
  193. raw_anchor_string_from_user: str,
  194. ) -> list[list[str]]:
  195. anchor_string_with_normalized_newline_only = (
  196. (raw_anchor_string_from_user or "")
  197. .replace("\r\n", "\n")
  198. .replace("\r", "\n")
  199. )
  200. logical_line_raw_string_list = anchor_string_with_normalized_newline_only.split("\n")
  201. nested_recognizable_segment_string_list: list[list[str]] = []
  202. for single_logical_line_raw_string in logical_line_raw_string_list:
  203. stripped_logical_line_string = single_logical_line_raw_string.strip()
  204. if not stripped_logical_line_string:
  205. continue
  206. recognizable_segment_list_for_one_logical_line = (
  207. _split_user_anchor_string_into_recognizable_text_segment_string_list(
  208. stripped_logical_line_string,
  209. )
  210. )
  211. if recognizable_segment_list_for_one_logical_line:
  212. nested_recognizable_segment_string_list.append(
  213. recognizable_segment_list_for_one_logical_line,
  214. )
  215. return nested_recognizable_segment_string_list
  216. def _midpoint_float_xy_pair_from_first_and_last_center_in_ordered_float_pair_list(
  217. ordered_center_xy_float_pair_list: list[tuple[float, float]],
  218. ) -> tuple[float, float] | None:
  219. if not ordered_center_xy_float_pair_list:
  220. return None
  221. if len(ordered_center_xy_float_pair_list) == 1:
  222. lone_center_x, lone_center_y = ordered_center_xy_float_pair_list[0]
  223. return lone_center_x, lone_center_y
  224. first_center_x, first_center_y = ordered_center_xy_float_pair_list[0]
  225. last_center_x, last_center_y = ordered_center_xy_float_pair_list[-1]
  226. return (
  227. (first_center_x + last_center_x) / 2.0,
  228. (first_center_y + last_center_y) / 2.0,
  229. )
  230. def _midpoint_float_xy_pair_from_closest_pair_of_points_in_float_pair_list(
  231. center_xy_float_pair_list: list[tuple[float, float]],
  232. ) -> tuple[float, float] | None:
  233. point_count = len(center_xy_float_pair_list)
  234. if point_count == 0:
  235. return None
  236. if point_count == 1:
  237. lone_x, lone_y = center_xy_float_pair_list[0]
  238. return lone_x, lone_y
  239. best_squared_euclidean_distance_between_pair: float | None = None
  240. midpoint_x_between_closest_pair = 0.0
  241. midpoint_y_between_closest_pair = 0.0
  242. for first_point_index in range(point_count):
  243. first_x, first_y = center_xy_float_pair_list[first_point_index]
  244. for second_point_index in range(first_point_index + 1, point_count):
  245. second_x, second_y = center_xy_float_pair_list[second_point_index]
  246. delta_x = first_x - second_x
  247. delta_y = first_y - second_y
  248. squared_distance = delta_x * delta_x + delta_y * delta_y
  249. if (
  250. best_squared_euclidean_distance_between_pair is None
  251. or squared_distance < best_squared_euclidean_distance_between_pair
  252. ):
  253. best_squared_euclidean_distance_between_pair = squared_distance
  254. midpoint_x_between_closest_pair = (first_x + second_x) / 2.0
  255. midpoint_y_between_closest_pair = (first_y + second_y) / 2.0
  256. return midpoint_x_between_closest_pair, midpoint_y_between_closest_pair
  257. def _ocr_smallest_area_line_index_where_normalized_line_contains_substring(
  258. ocr_result: Any,
  259. substring_normalized: str,
  260. ) -> int | None:
  261. if not substring_normalized:
  262. return None
  263. best_line_index: int | None = None
  264. best_area_pixels = float("inf")
  265. for line_index, line_text in enumerate(ocr_result.txts):
  266. line_normalized = _normalize_ocr_match_string(line_text)
  267. if substring_normalized not in line_normalized:
  268. continue
  269. box = ocr_result.boxes[line_index]
  270. xs = [float(p[0]) for p in box]
  271. ys = [float(p[1]) for p in box]
  272. area_pixels = (max(xs) - min(xs)) * (max(ys) - min(ys))
  273. if area_pixels < best_area_pixels:
  274. best_area_pixels = area_pixels
  275. best_line_index = line_index
  276. return best_line_index
  277. def _first_logical_line_only_from_multiline_anchor_string(raw_anchor_string: str) -> str:
  278. """只保留第一逻辑行(首个换行前),用于列表页标题锚点:第二行起忽略。"""
  279. normalized_newlines_only = (raw_anchor_string or "").replace("\r\n", "\n").replace(
  280. "\r",
  281. "\n",
  282. )
  283. return normalized_newlines_only.split("\n", 1)[0].strip()
  284. def ocr_find_text_center_allowing_note_title_wrapped_across_two_ocr_lines(
  285. image: str | Path | bytes | np.ndarray,
  286. target_text: str,
  287. ) -> tuple[int, int] | None:
  288. anchor_first_logical_line_only = _first_logical_line_only_from_multiline_anchor_string(
  289. target_text,
  290. )
  291. nested_recognizable_segment_string_list = (
  292. _split_anchor_raw_string_into_logical_line_then_recognizable_segment_nested_string_list(
  293. anchor_first_logical_line_only,
  294. )
  295. )
  296. if not nested_recognizable_segment_string_list:
  297. return None
  298. ocr = _get_rapid_ocr()
  299. result = ocr(_rapidocr_input_from_path_bytes_or_bgr_numpy(image))
  300. if result is None or not result.txts or result.boxes is None:
  301. return None
  302. logical_line_representative_center_xy_float_pair_list: list[tuple[float, float]] = []
  303. for recognizable_segment_raw_string_list_for_one_logical_line in nested_recognizable_segment_string_list:
  304. segment_center_xy_float_pair_list_for_one_logical_line: list[tuple[float, float]] = []
  305. for raw_segment_string in recognizable_segment_raw_string_list_for_one_logical_line:
  306. normalized_segment_string = _normalize_ocr_match_string(raw_segment_string)
  307. if not normalized_segment_string:
  308. continue
  309. matched_line_index = _ocr_smallest_area_line_index_where_normalized_line_contains_substring(
  310. result,
  311. normalized_segment_string,
  312. )
  313. if matched_line_index is None:
  314. return None
  315. segment_center_x, segment_center_y = _quad_center_xy(
  316. result.boxes[matched_line_index],
  317. )
  318. segment_center_xy_float_pair_list_for_one_logical_line.append(
  319. (segment_center_x, segment_center_y),
  320. )
  321. if not segment_center_xy_float_pair_list_for_one_logical_line:
  322. continue
  323. one_logical_line_representative_center_xy_float_pair = (
  324. _midpoint_float_xy_pair_from_first_and_last_center_in_ordered_float_pair_list(
  325. segment_center_xy_float_pair_list_for_one_logical_line,
  326. )
  327. )
  328. if one_logical_line_representative_center_xy_float_pair is not None:
  329. logical_line_representative_center_xy_float_pair_list.append(
  330. one_logical_line_representative_center_xy_float_pair,
  331. )
  332. if not logical_line_representative_center_xy_float_pair_list:
  333. return None
  334. final_anchor_center_xy_float_pair = (
  335. _midpoint_float_xy_pair_from_closest_pair_of_points_in_float_pair_list(
  336. logical_line_representative_center_xy_float_pair_list,
  337. )
  338. )
  339. if final_anchor_center_xy_float_pair is None:
  340. return None
  341. return (
  342. int(round(final_anchor_center_xy_float_pair[0])),
  343. int(round(final_anchor_center_xy_float_pair[1])),
  344. )
  345. def ocr_find_text_center(
  346. image: str | Path | bytes | np.ndarray,
  347. target_text: str,
  348. *,
  349. prefer_highest_score: bool = True,
  350. ) -> tuple[int, int] | None:
  351. """
  352. 对 ``image``(路径、``Path``、图像字节或 OpenCV BGR ``numpy`` 数组)跑 RapidOCR,找到与 ``target_text`` 最匹配的一行,
  353. 返回该行四边形框的**中心点** ``(cx, cy)``(与图像像素坐标一致)。
  354. 匹配规则:识别行经空白归一后,必须 **完整包含** ``target_text``(连续子串),
  355. 不要求逐字与目标一致,但必须整段关键词都在同一 OCR 框对应的文字里。
  356. 多行命中时:优先与目标 **完全一致** 的行,否则取识别串 **更短** 的行(更接近整块占位符),
  357. 再按 ``prefer_highest_score`` 用置信度打破平局。
  358. """
  359. ocr = _get_rapid_ocr()
  360. result = ocr(_rapidocr_input_from_path_bytes_or_bgr_numpy(image))
  361. if result is None or not result.txts or result.boxes is None:
  362. return None
  363. target_norm = _normalize_ocr_match_string(target_text)
  364. if not target_norm:
  365. return None
  366. matches: list[tuple[int, float, int, int]] = []
  367. for i, txt in enumerate(result.txts):
  368. if not _ocr_line_contains_full_target(txt, target_text):
  369. continue
  370. line_norm = _normalize_ocr_match_string(txt)
  371. exact = 0 if line_norm == target_norm else 1
  372. line_len = len(line_norm)
  373. score = float(result.scores[i]) if result.scores and i < len(result.scores) else 0.0
  374. matches.append((i, score, exact, line_len))
  375. if not matches:
  376. return None
  377. if len(matches) > 1:
  378. if prefer_highest_score:
  379. matches.sort(key=lambda row: (row[2], row[3], -row[1]))
  380. else:
  381. matches.sort(key=lambda row: (row[2], row[3], row[1]))
  382. idx = matches[0][0]
  383. box = result.boxes[idx]
  384. cx, cy = _quad_center_xy(box)
  385. return int(round(cx)), int(round(cy))
  386. def ocr_find_text_center_by_shortening_prefix_of_anchor_until_match(
  387. image: str | Path | bytes | np.ndarray,
  388. anchor_text: str,
  389. *,
  390. minimum_raw_character_count_inclusive: int = 6,
  391. prefer_highest_score: bool = True,
  392. ) -> tuple[int, int] | None:
  393. """
  394. 在整段 ``anchor_text`` 无法命中时,从去掉末尾一字的前缀起,逐步缩短,
  395. 直到某一前缀在单行 OCR 中可子串匹配(仍用 ``ocr_find_text_center`` 规则)。
  396. 用于识别结果漏掉标题末尾一两字、或个别字与 DOM 不一致时仍能落到标题行。
  397. """
  398. stripped_anchor_text = (anchor_text or "").strip()
  399. character_count = len(stripped_anchor_text)
  400. if character_count <= minimum_raw_character_count_inclusive:
  401. return None
  402. for end_exclusive in range(
  403. character_count - 1,
  404. minimum_raw_character_count_inclusive - 1,
  405. -1,
  406. ):
  407. prefix_only_anchor_text = stripped_anchor_text[:end_exclusive]
  408. found_center_xy_integer_pair = ocr_find_text_center(
  409. image,
  410. prefix_only_anchor_text,
  411. prefer_highest_score=prefer_highest_score,
  412. )
  413. if found_center_xy_integer_pair is not None:
  414. return found_center_xy_integer_pair
  415. return None
  416. def start(argv: list[str] | None = None) -> int:
  417. rest = argv if argv is not None else sys.argv[1:]
  418. if len(rest) < 2:
  419. print("用法: ocr-pos.py <目标文字> <图片路径>", file=sys.stderr)
  420. return 2
  421. target = (rest[0] or "").strip()
  422. image_path = Path(rest[1]).expanduser()
  423. if not target:
  424. print("目标文字不能为空。", file=sys.stderr)
  425. return 2
  426. if not image_path.is_file():
  427. print(f"图片不存在: {image_path}", file=sys.stderr)
  428. return 2
  429. pt = ocr_find_text_center(image_path, target)
  430. if pt is None:
  431. print(f"未在图中找到与「{target}」匹配的文字。", file=sys.stderr)
  432. return 1
  433. print(f"{pt[0]} {pt[1]}")
  434. return 0
  435. def main() -> int:
  436. return start()
  437. __all__ = [
  438. "REPO_ROOT",
  439. "OCR_RAPIDOCR_GLOBAL_MAX_SIDE_LEN",
  440. "OCR_RAPIDOCR_INFERENCE_ENGINE_NAME",
  441. "OCR_RAPIDOCR_ONNX_RUNTIME_USE_CUDA",
  442. "ocr_find_text_center",
  443. "ocr_find_text_center_allowing_note_title_wrapped_across_two_ocr_lines",
  444. "ocr_find_text_center_by_shortening_prefix_of_anchor_until_match",
  445. "ocr_smallest_line_center_for_dom_text",
  446. "start",
  447. "main",
  448. ]
  449. if __name__ == "__main__":
  450. raise SystemExit(start())