MinerU印章OCR-MPS-fp32补丁.md 8.7 KB

MinerU 印章 OCR 空结果与 MPS fp32 运行时补丁

关联文档: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 引入:

# OCR 推理精度开关:auto 表示 CPU 使用 fp32,非 CPU 自动使用 fp16。
OCR_INFERENCE_PRECISION = "auto"

Mac 上 get_device() 默认选 mpsauto 切到 fp16seal_PP-OCRv4_detPFHeadLocal)概率图输出 全 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 模式)

核心逻辑:

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.pySealOCRRecognizer.initialize()
  2. mineru_adapter.pyMinerUOCRRecognizer.initialize()

    # 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 环境下:

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

期望输出(节选):

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