|
@@ -0,0 +1,234 @@
|
|
|
|
|
+# MinerU 印章 OCR 空结果与 MPS fp32 运行时补丁
|
|
|
|
|
+
|
|
|
|
|
+> 关联文档:[`PaddleOCR-VL表格文字丢失-OTSL补丁.md`](./PaddleOCR-VL表格文字丢失-OTSL补丁.md)(同类 monkey-patch 模式)
|
|
|
|
|
+
|
|
|
|
|
+## 1. 背景与现象
|
|
|
|
|
+
|
|
|
|
|
+升级 MinerU(OCR 升到 PP-OCRv6,并引入动态推理精度)后,银行流水场景
|
|
|
|
|
+(如 `bank_statement_yusys_local`)中:
|
|
|
|
|
+
|
|
|
|
|
+- Layout 能正确检出印章(`type: "seal"`,bbox 正常);
|
|
|
|
|
+- 最终 JSON 里印章字段却是空的:`"text": ""`, `"confidence": 0.0`。
|
|
|
|
|
+
|
|
|
|
|
+样例:`彭_广东兴宁农村商业银行_page_001.json` 中印章框
|
|
|
|
|
+`[325, 124, 538, 325]` 可见红章文字,但识别结果为空。
|
|
|
|
|
+
|
|
|
|
|
+## 2. 根因分析
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 印章 OCR 调用链
|
|
|
|
|
+
|
|
|
|
|
+`ocr_platform` 印章识别走 `SealOCRRecognizer` → MinerU
|
|
|
|
|
+`PytorchPaddleOCR(lang="seal")`:
|
|
|
|
|
+
|
|
|
|
|
+| 阶段 | 模型 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 检测 det | `seal_PP-OCRv4_det_server_infer.pth`(仍是 v4) |
|
|
|
|
|
+| 识别 rec | `ch_PP-OCRv6_medium_rec_infer.safetensors`(升级后换成 v6) |
|
|
|
|
|
+
|
|
|
|
|
+检测失败时不会进入识别,直接得到空文本。
|
|
|
|
|
+
|
|
|
|
|
+### 2.2 真正炸掉的点:MPS + fp16
|
|
|
|
|
+
|
|
|
|
|
+MinerU 在 `mineru/model/utils/pytorchocr/base_ocr_v20.py` 引入:
|
|
|
|
|
+
|
|
|
|
|
+```python
|
|
|
|
|
+# OCR 推理精度开关:auto 表示 CPU 使用 fp32,非 CPU 自动使用 fp16。
|
|
|
|
|
+OCR_INFERENCE_PRECISION = "auto"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Mac 上 `get_device()` 默认选 `mps` → `auto` 切到 **fp16** →
|
|
|
|
|
+`seal_PP-OCRv4_det`(`PFHeadLocal`)概率图输出 **全 NaN** → 检测框数 = 0 →
|
|
|
|
|
+`text: ""`。
|
|
|
|
|
+
|
|
|
|
|
+证据:
|
|
|
|
|
+
|
|
|
|
|
+| 条件 | 结果 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 默认 `mps` + fp16 | det 概率图 `nan`,boxes=0,FULL=`[None]` |
|
|
|
|
|
+| `MINERU_DEVICE_MODE=cpu`(fp32) | 正常识别出「广东兴宁…」「电子回单专用章」 |
|
|
|
|
|
+| `mps` + 强制 fp32 | 同样正常 |
|
|
|
|
|
+
|
|
|
|
|
+> 重要结论:问题**不是**「v6 rec 认不出字」,也不是 layout 漏检。
|
|
|
|
|
+> 是 **seal det 在 MPS/fp16 数值不稳定**。普通 `ch` OCR 在同一裁剪图上
|
|
|
|
|
+> 仍能认出横排「电子回单专用章」,进一步说明图本身可读。
|
|
|
|
|
+
|
|
|
|
|
+### 2.3 为何不能直接改 MinerU
|
|
|
|
|
+
|
|
|
|
|
+本仓库需要持续同步上游 MinerU。直接改 MinerU 源码会导致:
|
|
|
|
|
+
|
|
|
|
|
+- 每次 `git pull` / 升级都要手工重放改动;
|
|
|
|
|
+- 团队环境不一致;
|
|
|
|
|
+- 与「ocr_platform 适配第三方、MinerU 保持可同步」的约定冲突。
|
|
|
|
|
+
|
|
|
|
|
+## 3. 方案选型
|
|
|
|
|
+
|
|
|
|
|
+| 方案 | 说明 | 结论 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| 改 MinerU `base_ocr_v20.py` | 同步升级成本高 | ❌ 不采用 |
|
|
|
|
|
+| 跑批前 `export MINERU_DEVICE_MODE=cpu` | 有效但全局强制 CPU,拖慢其它 OCR | ⚠️ 仅临时 |
|
|
|
|
|
+| 全局把 `OCR_INFERENCE_PRECISION="fp32"` | cuda 也会失去 fp16 加速 | ⚠️ 过粗 |
|
|
|
|
|
+| **运行时 monkey-patch:仅 mps→fp32** | 不改 MinerU、随本仓库版本化、cuda 仍走上游 auto | ✅ 采用 |
|
|
|
|
|
+
|
|
|
|
|
+### 为什么 monkey-patch 打在 `_resolve_inference_dtype`
|
|
|
|
|
+
|
|
|
|
|
+精度决策集中在 `BaseOCRV20._resolve_inference_dtype(device)`:
|
|
|
|
|
+`_apply_inference_precision` 在模型加载时调用它,再决定是否
|
|
|
|
|
+`self.net.to(dtype=torch.float16)`。
|
|
|
|
|
+
|
|
|
|
|
+因此只要在 **OCR 模型初始化之前** 替换该方法:
|
|
|
|
|
+
|
|
|
|
|
+- `mps` → 固定返回 `torch.float32`;
|
|
|
|
|
+- 其它设备 → 仍调用原始实现(cuda 继续 auto/fp16)。
|
|
|
|
|
+
|
|
|
|
|
+即可在不改 MinerU 文件的前提下修正错误行为。
|
|
|
|
|
+
|
|
|
|
|
+## 4. 最终实现
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 补丁模块
|
|
|
|
|
+
|
|
|
|
|
+新增:
|
|
|
|
|
+
|
|
|
|
|
+`ocr_tools/universal_doc_parser/models/adapters/_mineru_ocr_patches.py`
|
|
|
|
|
+
|
|
|
|
|
+(对齐已有的 `_mineru_vl_patches.py` 模式)
|
|
|
|
|
+
|
|
|
|
|
+核心逻辑:
|
|
|
|
|
+
|
|
|
|
|
+```python
|
|
|
|
|
+def _patch_mps_ocr_fp32() -> None:
|
|
|
|
|
+ import torch
|
|
|
|
|
+ import mineru.model.utils.pytorchocr.base_ocr_v20 as base
|
|
|
|
|
+
|
|
|
|
|
+ if not hasattr(base.BaseOCRV20, "_resolve_inference_dtype"):
|
|
|
|
|
+ raise RuntimeError(
|
|
|
|
|
+ "mineru 接口已变更:找不到 BaseOCRV20._resolve_inference_dtype,"
|
|
|
|
|
+ "请检查 MinerU 版本并更新 ocr_platform OCR 精度补丁。"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 可选:环境变量整库覆盖(auto/fp32/fp16)
|
|
|
|
|
+ override = os.getenv("OCR_PLATFORM_OCR_INFERENCE_PRECISION") or \
|
|
|
|
|
+ os.getenv("MINERU_OCR_INFERENCE_PRECISION")
|
|
|
|
|
+ if override:
|
|
|
|
|
+ base.OCR_INFERENCE_PRECISION = override.strip().lower()
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ orig = base.BaseOCRV20._resolve_inference_dtype
|
|
|
|
|
+
|
|
|
|
|
+ def _resolve_inference_dtype(self, device):
|
|
|
|
|
+ if str(device).lower().startswith("mps"):
|
|
|
|
|
+ return torch.float32
|
|
|
|
|
+ return orig(self, device)
|
|
|
|
|
+
|
|
|
|
|
+ _resolve_inference_dtype.__wrapped__ = orig
|
|
|
|
|
+ base.BaseOCRV20._resolve_inference_dtype = _resolve_inference_dtype
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+通过 `apply_once()` 应用,特性:
|
|
|
|
|
+
|
|
|
|
|
+- **幂等**:模块级 `_applied`,仅首次真正打补丁。
|
|
|
|
|
+- **失败大声**:找不到 `_resolve_inference_dtype` 时抛 `RuntimeError`,
|
|
|
|
|
+ 避免上游改名后补丁静默失效、印章再次变空。
|
|
|
|
|
+- **可覆盖**:环境变量可强制 `fp32` / `fp16` / `auto`(调试用)。
|
|
|
|
|
+- **保留原方法**:`__wrapped__` 指向原始实现,便于排查与还原。
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 调用点(必须在 OCR 模型创建之前)
|
|
|
|
|
+
|
|
|
|
|
+补丁必须在 `AtomModelSingleton.get_atom_model(AtomicModel.OCR, ...)` /
|
|
|
|
|
+`PytorchPaddleOCR(...)` **之前**执行,因为精度在构造时写入网络 dtype。
|
|
|
|
|
+
|
|
|
|
|
+当前挂载点:
|
|
|
|
|
+
|
|
|
|
|
+1. `seal_ocr_adapter.py` → `SealOCRRecognizer.initialize()`
|
|
|
|
|
+2. `mineru_adapter.py` → `MinerUOCRRecognizer.initialize()`
|
|
|
|
|
+
|
|
|
|
|
+```python
|
|
|
|
|
+# seal_ocr_adapter.py / mineru_adapter.py 的 initialize() 内
|
|
|
|
|
+from ._mineru_ocr_patches import apply_once as _apply_mineru_ocr_patches
|
|
|
|
|
+_apply_mineru_ocr_patches()
|
|
|
|
|
+
|
|
|
|
|
+self.xxx_model = self.atom_model_manager.get_atom_model(
|
|
|
|
|
+ atom_model_name=AtomicModel.OCR,
|
|
|
|
|
+ ...
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> 印章路径与整页 OCR 路径都会创建 OCR 原子模型;两处都挂是为了
|
|
|
|
|
+> 「无论先初始化哪条路径,补丁都已生效」。`apply_once()` 幂等,重复调用安全。
|
|
|
|
|
+
|
|
|
|
|
+### 4.3 环境变量
|
|
|
|
|
+
|
|
|
|
|
+| 变量 | 作用 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| (不设) | 默认:`mps`→fp32,其它设备走 MinerU 原逻辑 |
|
|
|
|
|
+| `OCR_PLATFORM_OCR_INFERENCE_PRECISION=fp32\|fp16\|auto` | 本仓库优先覆盖 |
|
|
|
|
|
+| `MINERU_OCR_INFERENCE_PRECISION=...` | 兼容通用名 |
|
|
|
|
|
+| `MINERU_DEVICE_MODE=cpu` | 整进程强制 CPU(临时绕过,非推荐长期方案) |
|
|
|
|
|
+
|
|
|
|
|
+## 5. 验证
|
|
|
|
|
+
|
|
|
|
|
+在 `mineru` 环境下:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+conda run -n mineru python - <<'PY'
|
|
|
|
|
+import os, sys, cv2
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+os.environ.pop("MINERU_DEVICE_MODE", None)
|
|
|
|
|
+os.environ.pop("OCR_PLATFORM_OCR_INFERENCE_PRECISION", None)
|
|
|
|
|
+
|
|
|
|
|
+root = Path("/Users/zhch158/workspace/repository.git/ocr_platform")
|
|
|
|
|
+sys.path[:0] = [str(root), str(root / "ocr_tools/universal_doc_parser")]
|
|
|
|
|
+
|
|
|
|
|
+from models.adapters._mineru_ocr_patches import apply_once
|
|
|
|
|
+from mineru.backend.pipeline.model_init import AtomModelSingleton
|
|
|
|
|
+from mineru.backend.pipeline.model_list import AtomicModel
|
|
|
|
|
+from mineru.utils.config_reader import get_device
|
|
|
|
|
+
|
|
|
|
|
+print("device", get_device())
|
|
|
|
|
+print("apply_once", apply_once()) # True
|
|
|
|
|
+print("apply_once again", apply_once()) # False
|
|
|
|
|
+
|
|
|
|
|
+page = ("/Users/zhch158/workspace/data/流水分析/"
|
|
|
|
|
+ "彭_广东兴宁农村商业银行/bank_statement_yusys_local/"
|
|
|
|
|
+ "彭_广东兴宁农村商业银行/彭_广东兴宁农村商业银行_page_001.png")
|
|
|
|
|
+crop = cv2.imread(page)[124:325, 325:538]
|
|
|
|
|
+model = AtomModelSingleton().get_atom_model(
|
|
|
|
|
+ atom_model_name=AtomicModel.OCR, lang="seal")
|
|
|
|
|
+print("dtype", next(model.text_detector.net.parameters()).dtype) # float32 on mps
|
|
|
|
|
+full = model.ocr(crop, det=True, rec=True)
|
|
|
|
|
+texts = [it[1][0] for it in (full[0] or [])]
|
|
|
|
|
+print(texts)
|
|
|
|
|
+assert any("兴宁" in t or "电子回单" in t for t in texts)
|
|
|
|
|
+PY
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+期望输出(节选):
|
|
|
|
|
+
|
|
|
|
|
+```text
|
|
|
|
|
+device mps
|
|
|
|
|
+已应用 MinerU OCR 补丁:mps 默认使用 fp32(避免 seal det NaN)
|
|
|
|
|
+apply_once True
|
|
|
|
|
+dtype torch.float32
|
|
|
|
|
+['广东兴宁农村商业银行股份有限公司', '电子回单专用章']
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 6. 维护注意事项
|
|
|
|
|
+
|
|
|
|
|
+1. **不要改 MinerU 源码**解决此问题。修复集中在 `_mineru_ocr_patches.py`,
|
|
|
|
|
+ 随 `ocr_platform` 版本化,方便同步升级 MinerU。
|
|
|
|
|
+2. **升级 MinerU 后**重跑第 5 节验证。若上游已对 mps 默认 fp32 / 修复 seal det NaN,
|
|
|
|
|
+ 可考虑移除本补丁;若 `_resolve_inference_dtype` 被改名或删除,
|
|
|
|
|
+ `apply_once()` 会抛 `RuntimeError` 提示更新补丁。
|
|
|
|
|
+3. **新增 MinerU OCR 相关运行时修补**统一加到 `_mineru_ocr_patches.py`,
|
|
|
|
|
+ 由 `apply_once()` 串联,保持「补丁集中、可开关、可追溯」。
|
|
|
|
|
+ (VL / OTSL 类补丁仍放在 `_mineru_vl_patches.py`。)
|
|
|
|
|
+4. 补丁是**进程级**的:必须在首次创建 OCR 模型前调用;模型已用 fp16 建好后再打补丁无效,
|
|
|
|
|
+ 需重启进程。
|
|
|
|
|
+
|
|
|
|
|
+## 7. 涉及文件
|
|
|
|
|
+
|
|
|
|
|
+| 文件 | 变更 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `models/adapters/_mineru_ocr_patches.py` | 新增:MinerU OCR 运行时补丁(mps→fp32) |
|
|
|
|
|
+| `models/adapters/seal_ocr_adapter.py` | `initialize()` 在创建 seal OCR 前调用 `apply_once()` |
|
|
|
|
|
+| `models/adapters/mineru_adapter.py` | `MinerUOCRRecognizer.initialize()` 同样接入 |
|
|
|
|
|
+| MinerU 仓库 | **无改动** |
|