# 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 仓库 | **无改动** |