MiniCPM-OCR 识别中文票据,先摸清它的版面和语言边界

文章导读
手里有一批中文票据和表格,想知道 MiniCPM-OCR 能不能直接用,答案通常不在模型名上,而在票据的版面和语言组合上。稳妥的做法是先挑 6 张左右能覆盖横排与竖排、有线表格与无线表格、印刷与手写的图片跑一遍,看它在哪一类版式上开始丢行、错列或串行,再决定是接入现有流程、加人工复核,还是换方案。下面这套流程只依赖配置文件、日志和脚本输出,不需要额外环境就能判断边界。
📋 目录
  1. 壹 挑三类票据各两张做最小样本集
  2. 贰 跑通单张图的推理脚本骨架
  3. 叁 逐张标注识别失败的版面类型
  4. 肆 用同一批样本做图片预处理对照
  5. 伍 整理能否接入的判断清单
A A

手里有一批中文票据和表格,想知道 MiniCPM-OCR 能不能直接用,答案通常不在模型名上,而在票据的版面和语言组合上。稳妥的做法是先挑 6 张左右能覆盖横排与竖排、有线表格与无线表格、印刷与手写的图片跑一遍,看它在哪一类版式上开始丢行、错列或串行,再决定是接入现有流程、加人工复核,还是换方案。下面这套流程只依赖配置文件、日志和脚本输出,不需要额外环境就能判断边界。

MiniCPM-OCR 对中文票据的可用性,应按版面类型分别判断,而不是笼统问“识别得准不准”。建议先取横排印刷、有线表格、含手写或竖排的票据各两张跑通单张推理,记录输出形态与失败位置;对失败样本再做放大、灰度、二值化、旋转校正的对照,能把输入质量问题和模型能力边界分开。只有在可用版面清单明确、且失败类型可枚举之后,再谈接入;印章覆盖、密集手写和无框线表格这类版面,需要预留人工复核位。

挑三类票据各两张做最小样本集

不要拿一张图下结论。单张图往往同时具备“印刷体、横排、有表格线”几个有利条件,跑通之后很容易误以为整批票据都能过。最小样本集的作用是把版面维度拉开,让失败在早期暴露出来。

建议按下面几个维度各取一份,总数控制在 6 到 10 张:

  • 排版方向:横排为主、竖排或混合排版;
  • 表格形态:有完整框线、只有横线或竖线、完全无框线靠留白对齐;
  • 文字来源:印刷体、手写填单、印刷与手写混排;
  • 附加干扰:盖章覆盖文字、二维码或条码、折痕与阴影、透视倾斜;
  • 语言构成:纯中文、中英混排、数字与中文大写金额并存。

文件命名直接带上维度,后面排查时不用反复打开图片猜。建议用“类别_版式_干扰_序号.png”的形式,类别如 invoice、receipt、form,版式用 hr 表示横排、vt 表示竖排、tbl 表示有线表格、notbl 表示无框线,干扰用 stamp、hand、qr 标注:

MiniCPM-OCR 识别中文票据,先摸清它的版面和语言边界
samples/
  invoice_tbl_stamp_01.png
  invoice_notbl_02.png
  receipt_hr_hand_01.png
  receipt_vt_02.png
  form_tbl_hand_03.png
  form_notbl_qr_04.png

目录保持平铺即可,不要一开始就做多层分类,命名已经足够区分。每张图额外记一条备注,写清“这张我预期它能不能识别”,跑完后用来对照自己的预判是否成立。

跑通单张图的推理脚本骨架

先确认输入输出形态,再谈识别效果。MiniCPM-OCR 这类模型在不同仓库里的加载方式和返回结构不一致,有的返回纯文本,有的返回带坐标的 JSON。脚本不要写死接口名,按实际仓库 README 替换加载和调用两处即可。

from pathlib import Path
import json
from PIL import Image

MODEL_DIR = "path/to/MiniCPM-OCR"      # 替换为实际下载路径
IMG_DIR = Path("samples")
OUT_DIR = Path("runs/baseline")

def load_model(model_dir):
    """按仓库文档替换:可能是 AutoModel + trust_remote_code,
    也可能是仓库自带 pipeline 或 CLI 封装。"""
    raise NotImplementedError

def read_image(path):
    return Image.open(path).convert("RGB")

def run_ocr(model, image, prompt=None):
    """返回结构由仓库决定,这里统一包一层 {text, raw}。"""
    raw = model.generate(image, prompt=prompt)   # 按实际签名替换
    return {"text": raw if isinstance(raw, str) else None, "raw": raw}

def save_result(name, result):
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    (OUT_DIR / f"{name}.json").write_text(
        json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
    )

if __name__ == "__main__":
    model = load_model(MODEL_DIR)
    for img_path in sorted(IMG_DIR.glob("*.png")):
        result = run_ocr(model, read_image(img_path))
        save_result(img_path.stem, result)
        print(img_path.name, type(result["raw"]).__name__)

第一次运行只跑一张,先打印返回对象的类型和字段名(是 str、dict 还是带 bbox 的 list),确认结果里有没有坐标信息。落盘时保留原始返回,不要只存清洗后的文本,否则后面标注失败位置时缺少定位依据。执行位置建议在模型目录下的虚拟环境中,输出统一进 runs/baseline,便于和预处理版本对比。

逐张标注识别失败的版面类型

“效果差”不是可用信息。要把失败归到具体版式和一个可描述的位置上,才能判断是偶发还是结构性限制。

MiniCPM-OCR 识别中文票据,先摸清它的版面和语言边界

建议用一个 csv 或表格逐张记录,字段固定下来:

图片名失败类型位置描述图片特征
receipt_vt_02串行第 3 行金额并入第 2 行竖排、行距窄、无框线
invoice_tbl_stamp_01缺行印章覆盖处整行缺失红色印章压在表格中部
form_tbl_hand_03错列手写金额落到相邻列手写倾斜、跨列书写

失败类型建议收敛成四类,便于统计:缺行(整行没出来)、错列(内容出现但归错字段)、串行(两行合并成一行或顺序错乱)、字符级错误(文字位置对但个别字或数字识别错)。每条记录都要能回答“这张图长什么样”,而不是只写“识别不准”。如果模型返回了坐标,把异常框的坐标一并记下,后续调预处理时有参照。

用同一批样本做图片预处理对照

失败到底来自输入质量还是模型能力,靠对照能分开。做法是让原始样本和预处理后的样本走同一个推理脚本,输出到不同目录,逐张比对同一位置是否还错。

MiniCPM-OCR 识别中文票据,先摸清它的版面和语言边界
from PIL import Image, ImageOps

def upscale(img, factor=2):
    w, h = img.size
    return img.resize((w * factor, h * factor), Image.LANCZOS)

def to_gray(img):
    return ImageOps.grayscale(img).convert("RGB")

def binarize(img, thresh=180):
    g = ImageOps.grayscale(img)
    return g.point(lambda p: 255 if p > thresh else 0).convert("RGB")

def rotate(img, angle):
    return img.rotate(angle, expand=True, fillcolor="white")

VARIANTS = {
    "raw": lambda im: im,
    "up2x": upscale,
    "gray": to_gray,
    "bin180": binarize,
    "rot_deg": lambda im: rotate(im, 2),   # 角度按肉眼或投影估计填
}

每个变体输出到 runs/up2x、runs/gray 这类独立目录,文件主名保持不变,方便和 baseline 一对一对照。比较时不用看全文,只盯失败记录表里那几行:某张图在 baseline 缺行,放大后补齐了,说明是分辨率不足;灰度或二值化后印章覆盖处恢复,说明干扰主要来自颜色;旋转校正后串行消失,说明是倾斜导致的行切分错误;四种变体都没改善,才更可能是版式或语言本身超出了模型的处理范围。二值化阈值不要一次调很多档,先在 150 到 200 之间取一两档看趋势,避免把调参当成结论。

整理能否接入的判断清单

试跑之后的产出应该是一份分档清单,而不是一句“可以用”。三档划分如下,判定理由必须来自上面的记录表,不能凭印象。

档位版面特征判定理由接入方式
可用横排印刷、有线表格、无遮挡样本中未出现缺行错列,字段顺序稳定自动入库,抽检
需人工复核含手写、印章覆盖、轻微倾斜失败集中在特定字段,预处理可部分改善先出结果再核对金额与日期
不建议使用竖排密集、无框线多列、大面积手写预处理后仍串行或错列,错误不可预测走人工录入或换专用方案

落到执行时,把每张样本的图片名填进对应档位,清单本身就是接入依据:可用档先接入并保留原图与输出日志,用于事后追查;复核档在流程里加一个字段级校验位,金额、日期、票号三类字段重点比对;不建议使用的版面直接在前置环节分流出去,不要让它进模型再靠后处理兜底。另外提醒一点,边界会随票据来源变化,换一批供应商或地区的票据时,用同样 6 张的样本集重跑一次这套流程,比沿用旧结论稳妥。