ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多模态OCR大模型实测:手写印刷混排与多字体适应性

多模态OCR大模型实测:手写印刷混排与多字体适应性

最近一段时间,OCR 圈子里讨论度最高的话题之一,就是一款新开源的多模态 OCR 大模型到底能不能扛住"手写体混合印刷体识别"这种脏活。我手上正好有一批历史表单——印刷体表头配手写体填写栏,中间还夹着打印批注和手写批注——之前用传统检测+识别两段式管线跑,效果一直不太稳定。于是我干脆把这款模型(下文统一用代号 H-OCR 指代,具体权重和版本以各人手上仓库的 README 为准)拉下来,围绕"多字体适应性"做了一轮结构化实测,重点看它在纯印刷、纯手写、混排、退化四种场景下的表现差异。这篇就把测试集怎么造、参数怎么调、结果怎么看、坑在哪,一次性讲清楚,适合正在评估 OCR 选型的技术同学,也适合只想拿现成脚本跑一遍的中小团队。

1. 为什么值得单独做一次多字体适应性实测

1.1 混排场景才是真实业务的主战场

做过表单数字化的朋友应该都有体会,纯印刷体识别早就不是什么难题,随便一个轻量检测模型加 CRNN,在干净扫描件上都能跑到 97% 以上的行准确率。真正让人头疼的是"混合字体":一张纸上有印刷体的固定字段名,有手写体的填写内容,还有人手写划掉的修改痕迹,偶尔再盖个章、贴个便签。这类图像对 OCR 的挑战不在于单个字认不认得,而在于模型要同时理解"这块区域是印刷的、那块是手写的、这两者属于同一行"。

传统的两段式管线在这里会很别扭:检测模型按文本框切分,手写体和印刷体挨得近的时候框会粘连,识别模型又缺乏上下文,切出来的半截手写内容单独送进去基本靠猜。而端到端的多模态 OCR 模型是把整页图当上下文一起喂进去的,理论上能利用版面结构做联合推断——这正是我想验证的核心假设。多字体适应性不是一个宣传词,它直接决定了这套方案能不能替代老管线。

1.2 我的评价口径:三个指标缺一不可

评估 OCR 最容易犯的错就是只看一个"准确率"。我这次设了三层指标,每层回答一个不同的问题:

指标计算方式回答什么问题权重
字符错误率 CER(替换+删除+插入) / 真值字符数字认得准不准高
行级完全正确率整行零错误的行数 / 总行数能不能直接用高
结构还原率表格行列还原正确的单元格数 / 总单元格数版面保不保得住中

之所以把行级完全正确率放这么重,是因为业务上 OCR 结果通常是往下游系统里灌的。CER 5% 听起来很美,但如果错误均匀散步在每一行里,那就意味着每一行都要人工校对,等于没省事。反过来,行级准确率 90%、错误集中在少数几行,人工只需要看那 10%,这才是能落地的状态。结构还原率则是针对表格类图像专门加的,混排场景里手写内容经常写进表格单元格,如果模型把列对齐搞错,数据就会串到别的字段上去,这种错误比认错字危险得多。

注意:CER 的分母是真值字符数,不是预测字符数,否则模型疯狂输出的时候 CER 会被人为压低,这是很多人做对比时踩过的坑。

1.3 关于代号的说明

为了避免把注意力引到具体产品上,全文用 H-OCR 指代被测模型,用"基线管线"指代我那套传统检测+识别的老方案。所有结论都是在我自己构造的测试集上跑出来的,不同数据分布下数字会有浮动,你更该关注的是"哪类输入会掉点、掉多少、怎么补",而不是记住某个具体百分比。

2. 测试集怎么造:混排样本的分层设计

2.1 四层样本,把变量拆开

自己造测试集最大的价值是可控。我用了 240 张图,分成四层,每层 60 张,每层只改变一个主要变量,这样出问题的时候能快速定位是哪一类输入导致的:

  • L1 纯印刷体:标准打印文档、表格、票据,字体覆盖宋体、黑体、楷体、仿宋,字号 9pt 到 22pt。
  • L2 纯手写体:分成两小类,工整楷书 30 张,连笔行草 30 张,写在无格线和有格线纸上各一半。
  • L3 混排:印刷表头 + 手写填写、印刷正文 + 手写批注、印刷表格 + 手写勾选与数字,三类各 20 张。
  • L4 退化:在 L3 基础上叠加低分辨率、轻微倾斜(3 到 8 度)、盖章遮挡、折痕阴影,模拟手机翻拍的真实状态。

L2 为什么要拆楷书和行草?因为这两类的难度差异极大。楷书的手写体识别现在很多模型都能做到 95% 以上,行草则是另一回事,连笔、简写、个人笔迹习惯叠加在一起,人类自己看都要靠上下文猜。如果测试集里混着这两种,平均出来的数字会掩盖真实问题——一个模型可能楷书 96%、行草 78%,平均 87%,看着还行,实际业务里行草那批完全不能用。

2.2 标注与真值对齐的两个细节

标注这块我踩过坑,说两个容易忽略的点。第一,手写涂改怎么标。有人把划掉的字也算进真值,有人完全忽略。我的做法是:删除线清晰可辨的不计入真值,但如果只是随手划一道、原字仍然可读的,保留原字并在备注里标记。这个规则必须提前定死,否则不同人标出来的 CER 没法横向比。第二,全角半角与空格。OCR 输出里空格数量经常和真值对不上,如果严格按字符比对,一个多余空格就算一次插入错误。我在评测脚本里做了归一化:连续空格压成一个,行首行尾空格去掉,中文之间的空格直接删除。不做这一步,CER 会被空格噪声抬高 1 到 2 个百分点。

2.3 环境准备与耗时预算

硬件是一张 24GB 显存的卡,环境用 Python 3.10 + PyTorch 2.x,依赖按仓库 README 装。整个 240 张图的推理,在长边 1600 的设置下,单图平均 1.1 秒,加上预处理和后处理,全量跑完约 5 分钟。这个耗时意味着我可以一天内迭代几十轮参数对比,效率上是够的。

项目配置说明
显存24GB长边 2048、单图推理峰值约 12GB
系统内存32GB 起批量预处理时 PIL 解码会吃内存
Python3.103.8 部分依赖版本不兼容
推理精度fp16相比 fp32 显存降约 40%,CER 差异在 0.1% 内

3. 推理链路搭建:从一张图到一份结构化结果

3.1 三段式流程与每段的作用

整个链路我拆成三段:预处理、模型推理、后处理。预处理负责分辨率归一和方向校正;模型推理负责把图像转成带结构的文本;后处理负责坐标还原、重复框去重和阅读顺序整理。很多人只关注中间那段,结果被预处理和后处理拖了后腿。

预处理的第一个决策是缩放策略。我的做法是"长边限制 + 短边保底":长边缩到 1600,同时保证短边不低于 960,如果按长边缩完短边太小,就反过来按短边放大。为什么不直接固定尺寸?因为 OCR 对字符的绝对像素高度敏感,一张 A4 扫描件缩到长边 1600,正文小四号字的字高大约剩 16 到 18 像素,这个尺度基本够用;但一张手机拍的远景照片,同样的长边 1600 下字高可能只有 8 像素,直接废掉。短边保底就是为了兜住这种情况。

3.2 推理脚本骨架

下面这段是通用骨架,类名和参数名按你手上仓库的实际接口替换。我把批量、精度、任务提示词都做成可配置项,方便后面做横向对比:

import torch from PIL import Image def load_model(model_path, device="cuda"): # 类名按仓库 README 替换 processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True) model = AutoModelForVision2Seq.from_pretrained( model_path, torch_dtype=torch.float16, device_map=device, trust_remote_code=True, ).eval() return processor, model def preprocess(img_path, long_side=1600, short_side=960): img = Image.open(img_path).convert("RGB") w, h = img.size scale = long_side / max(w, h) if min(w, h) * scale < short_side: scale = short_side / min(w, h) new_size = (int(w * scale), int(h * scale)) return img.resize(new_size, Image.LANCZOS), scale def infer(processor, model, img, prompt): inputs = processor(images=img, text=prompt, return_tensors="pt").to(model.device) with torch.inference_mode(): out = model.generate(**inputs, max_new_tokens=4096, do_sample=False) return processor.batch_decode(out, skip_special_tokens=True)[0]

这里有两个参数值得单独说。max_new_tokens设成 4096 是因为整页表格的 OCR 结果很容易超过 2000 token,设小了会被硬截断,表现为"页面下半部分凭空消失",这个 bug 我第一次跑就撞上了,排查了半小时才反应过来是长度限制。do_sample=False必须开,OCR 是确定性任务,采样会让同一张图两次结果不一致,评测就没法复现。

3.3 提示词对结果的影响比想象中大

多模态模型的输出形态很大程度由提示词决定。我试了三版提示词,差别非常明显:

提示词写法输出形态问题
"识别图中文字"纯文本流表格行列信息全丢,混排区域顺序错乱
"识别图中所有文字,保留版面结构"带换行和缩进结构基本可用,但手写批注容易游离
"识别图中所有文字,区分印刷体与手写体,表格用 Markdown 输出"结构化 + 标签结构最好,代价是输出更长、偶有标签遗漏

第三版提示词是我最终采用的。它多出来的那句"区分印刷体与手写体"看起来只是加了个要求,实际效果是模型在做识别时会更主动去判断笔画特征,行草那批样本的 CER 从 11.3% 降到了 9.8% 左右,降幅不算夸张但稳定。这就是所谓的"提示词即任务定义"——你不说清楚,模型就按最省事的方式给你一段平铺文本。

注意:提示词不是越长越好。我试过往里塞十几个约束条件(字体、字号、颜色、置信度),结果模型开始漏掉实际内容,因为注意力被分散了。约束控制在三到四条以内比较稳。

3.4 后处理里最容易被忽略的三件事

第一件是坐标还原。模型输出的框坐标是针对缩放后图像的,要除以之前算出的 scale 才能映射回原图。这一步忘了,下游画框会整体偏移。第二件是重复框去重。模型偶发会对同一区域输出两次,尤其在手写和印刷贴得很近的地方。我用 IoU 加文本相似度双重判定,IoU 大于 0.7 且编辑距离小于 2 的,保留置信度高的那个。第三件是阅读顺序。表格类图像的输出顺序不一定是人眼的阅读顺序,如果是给下游结构化系统用,最好按"先上后下、同行从左到右"重新排序一遍,否则插库的时候字段会错位。

4. 四组实测结果与逐层分析

4.1 纯印刷体:基线稳,但别高兴太早

L1 的 60 张图跑下来,CER 0.79%,行级完全正确率 97.3%,结构还原率 96.1%。这个成绩说明模型的多字体适应性在标准印刷体上没有短板,宋黑楷仿四种字体之间的差异基本被抹平了。

但有两个细节值得记下来。一是密集小字:字号低于 9pt、行距又紧的段落,CER 会跳到 2.4% 左右,主要错在形近字上,比如"己/已/巳"、"未/末"。这个不是模型的问题,是像素信息本来就快没了,我试过把长边提到 2048,CER 回落到 1.3%,代价是单图耗时从 1.1 秒涨到 2.3 秒。二是竖排文字,L1 里有几张竖排标签,模型的输出顺序会乱,横竖混排时尤其明显。如果你的业务里有竖排票据,这一块要单独做方向预处理。

4.2 纯手写体:楷书能打,行草是分水岭

这是我这次实测最关心的部分。结果拆开看:

子类CER行级正确率典型错误
工整楷书(无格线)4.2%88.6%数字 1/7、字母 l/I
工整楷书(有格线)3.6%91.2%少量连笔处
连笔行草(无格线)11.9%62.4%简化写法、字间粘连
连笔行草(有格线)9.8%68.1%行内粘连、笔画吞并

有格线的比无格线的稳定高出 2 到 3 个百分点,原因很直观:格线给了模型明确的行边界和字高参照,少了分割歧义。这个发现对落地很有指导意义——如果业务能控制输入,比如让填写人写在带格线的表单里,识别效果会明显好于白纸手写。

行草那两档的数字看着不太美好,但要说句公道话:行草识别本身就不该用"通用模型直接上"的方式解决。我做过一个小实验,把公司内部几十份历史手写单据里出现频率最高的 200 个连笔写法,作为上下文示例塞进提示词,行草那批的 CER 从 11.9% 降到 8.7%。这说明模型有很强的少样本适应能力,缺的是"你告诉我这个人怎么写"的信息。

4.3 混排:真正拉开差距的地方

L3 是这次测试的重点。60 张混排图,整体 CER 6.1%,行级正确率 79.5%,结构还原率 84.3%。和基线管线对比:

方案混排 CER行级正确率结构还原率单图耗时
基线管线(检测+识别)9.4%64.2%61.8%0.4s
H-OCR(长边 1600)6.1%79.5%84.3%1.1s
H-OCR(长边 2048)5.3%82.7%86.9%2.3s

差距最大的是结构还原率,从 61.8% 提到 84.3%,涨了 22 个百分点。这个提升不是靠"字认得更多",而是靠"版面理解得更全"。基线管线遇到"印刷表头 + 手写填写"的时候,检测框经常把表头和填写内容切成两个独立块,后面的结构重建就丢了对应关系;端到端模型是整页一起看的,它能把"姓名:"和右边的手写"张某某"关联起来。

不过混排的错误类型很有意思,值得单独列一下。最多的是归属错位——手写批注写在页面空白处,模型把它插到了相邻的段落中间,内容是对的,位置错了。这类错误占混排总错误的 41%。其次是手写印刷判错标签,模型把工整的手写体标成印刷体,或者把印刷的斜体标成手写,这个对内容识别没影响,但如果你下游依赖这个标签做区分,就会出问题,占 23%。剩下的是常规的字符错误。

4.4 退化场景:掉点主要来自预处理不到位

L4 的 60 张退化图,CER 14.8%,行级正确率 51.3%。这个数字单看很吓人,但拆解之后发现,其中很大一部分是可以通过预处理抢救回来的:

退化类型原始 CER加预处理后 CER关键处理手段
低分辨率(长边小于 1000)18.2%12.4%超分 + 长边拉到 2048
倾斜 3-8 度13.6%6.9%自动倾斜校正
盖章遮挡21.5%17.8%颜色通道分离,弱化红色印章
折痕阴影12.1%9.2%局部对比度增强

倾斜校正的收益最大,几乎把错误砍半。道理很简单:模型是在正常排版的图像上训出来的,整体倾斜 5 度,文字的笔画方向就全变了,识别网络会把它当成另一种笔迹。盖章遮挡最难救,因为印章的红色线条和黑色手写笔画重叠之后,信息是真的丢了,我试过通道分离,能把 CER 从 21.5% 压到 17.8%,再往下就得上专门的去印章模型了。

还有一个特别容易忽略的点:翻拍时的透视形变。手机拍纸张,如果角度偏了,图像会变成梯形,上下两端的字高不一致。这类图像我放在倾斜类里一起算了,处理方式是先做四角点检测加透视变换,拉回矩形再送模型。这一步不做,长文档的下半部分基本全废。

5. 参数调优:哪些旋钮真的有用

5.1 分辨率是收益最高也最贵的旋钮

我把长边从 640 一路拉到 2560,看 CER 和耗时的关系:

长边设置混排 CER单图耗时显存峰值建议场景
64014.7%0.4s5GB不推荐,仅用于粗略预览
10249.2%0.7s7GB大批量粗筛
12807.4%0.9s9GB日常默认
16006.1%1.1s12GB质量优先
20485.3%2.3s16GB小字密集、归档级
25605.1%4.1s22GB边际收益很低

从 1600 到 2048,CER 降了 0.8 个百分点,耗时翻倍;从 2048 到 2560 基本没意义。所以我的建议是:大部分场景 1280 到 1600 就够了,只有确认小字密集或归档要求高的时候才上 2048。这个曲线的形状很好理解——OCR 需要的是"字够清楚",不是"图够大",超过一定尺度之后,多出来的像素对识别没有增量信息,只是白白吃算力。

5.2 显存和吞吐的估算方法

很多人上来就问"这模型要多少显存"。给你一个可复算的估算方式:模型把图像切成 patch 处理,如果 patch 是 14 像素见方,一张 1600×1200 的图就是 114×86 约 9800 个视觉 token,加上提示词和输出,总序列长度在一万二左右。fp16 下每个 token 的 KV 缓存占用随层数和隐藏维度变化,按经验值折算,这个长度大概对应 10 到 12GB 的峰值。所以 24GB 卡跑单图是宽裕的,跑 batch=4 就有点紧。

吞吐方面,batch=1 时 1600 长边约 1.1 秒一页,batch=8 时约 4.6 秒一批,折合 0.58 秒一页。批量的收益不是线性的,因为序列长度不一致导致 padding 浪费,实际加速比大概 1.9 倍。如果需要处理万级页面的存量资料,建议按"批量 8 + 长边 1280"配置,单卡一天能跑十几万页,这个吞吐对大多数业务是够的。

5.3 结构化输出的取舍

模型支持输出 Markdown 表格、JSON 等结构化格式。我实测下来的结论是:表格类图像用 Markdown,非表格用纯文本加换行。JSON 虽然好解析,但输出长度会膨胀 1.5 到 2 倍,长文档容易触到 token 上限被截断,而且模型偶尔会漏逗号导致解析失败。

Markdown 表格的还原率我测到 86.9%,失败的情况主要是合并单元格和手写内容跨行。如果业务必须严格结构化,建议的做法是让模型输出 Markdown,再写一个轻量解析器转成目标结构,比直接要 JSON 稳得多。

6. 踩坑记录与常见问题速查

6.1 问题速查表

下面这些是我这两周实际撞到的问题,按出现频率排序:

现象可能原因排查动作解决方式
页面下半部分内容丢失输出 token 被截断检查 max_new_tokens提到 4096 以上
同一区域出现两次后处理没去重打印所有识别框坐标IoU + 编辑距离去重
表格行列错位提示词未要求结构化看原始输出形态换成带 Markdown 要求的提示词
手写数字 0 认成 O缺少上下文约束看是否只在孤立数字上出现提示词里声明"数字按数字输出"
竖排文字顺序乱未做方向预处理判断文字走向竖排图先旋转再送模型
大批量跑显存溢出batch 太大 + 长边过高监控显存峰值降 batch 到 4 或降长边到 1280
同图两次结果不一致开启了采样检查 generate 参数do_sample=False
红章区域大面积漏识颜色干扰分离通道观察通道分离 + 对比度增强

6.2 几条用血换来的经验

第一条:先用小样本把提示词调好,再上批量。我最开始是拿 240 张全量跑,跑完发现提示词写错了,全部重来,浪费一个多小时。正确做法是固定 10 张有代表性的图(每层挑两三张),提示词在这 10 张上迭代,稳定了再铺开。

第二条:把中间结果落盘。每次推理把原图路径、缩放比例、原始输出、耗时全部写进一个 JSONL,不要只存最终解析结果。后面发现某个指标异常的时候,能直接从落盘数据里捞出来复盘,不用重跑。这个习惯帮我定位了好几个隐蔽的坐标偏移问题。

第三条:警惕"看起来很对"的错误。混排场景里最阴险的是归属错位——内容一个字没错,但被放到了错误的位置。这类错误 CER 指标完全看不出来,只有做结构比对才能发现。所以评测一定要人工抽检一部分,纯靠自动指标会给你虚假的安全感。

第四条:手写体评测别用通用测试集。公开的手写数据集和你的业务笔迹分布可能差得很远。同一个模型在公开集上行草 CER 15%,换到某个行业的特定笔迹上可能 25%。如果业务重要,一定自己标 50 到 100 张真实样本做验证。

提示:如果发现某类样本错误率异常高,先别急着换模型,把这类样本单独拎出来看二十张,八成能发现是某个共性预处理问题,而不是模型能力问题。

7. 落地建议:什么场景该上,什么场景别硬上

7.1 按场景给出选择建议

跑完这一轮,我对这套方案的适用边界有了比较清楚的判断。给一个直白的分类:

场景特征建议理由
印刷为主,偶有手写批注直接上,收益明显结构还原优势大,CER 可控
工整手写填表推荐上,配合格线模板楷书 CER 3.6%,够用
大量连笔手写单据谨慎,需配少样本提示原始 CER 12%,需降到 8% 以内才划算
极端退化翻拍件先做预处理再说不做倾斜校正和超分,效果会很差
超高吞吐、成本敏感建议分级:粗筛用轻量模型大模型单页 1 秒,十万页就是 28 小时单卡
强结构化入库可上,但需自建解析层Markdown 转结构比直接要 JSON 稳

成本这块要算清楚。按单卡单页 1.1 秒算,一天满负荷大约 7.8 万页。如果日均处理量在几千页以内,单卡完全够;上万页就得上多卡或者做分级——先用轻量检测模型判断页面复杂度,简单页面走小模型,复杂混排页面才送给大模型,这样综合成本能降一半以上。

7.2 一个实用的混合架构思路

如果你的业务同时有"量大"和"质量要求高"两个诉求,我建议不要指望一个模型通吃。我目前验证下来比较靠谱的组合是:

前置一级用轻量的方向检测和倾斜校正,成本几乎为零但收益很大;中间一级用简单的版面分析判断页面属于纯印刷、纯手写还是混排;纯印刷页面走传统管线(快且准),混排和手写页面才路由到大模型。这套组合在我自己的测试集上,整体耗时比全量大模型低 40%,而质量几乎没有下降,因为传统管线在纯印刷场景本来就不输。

7.3 后续还能怎么扩展

这次实测只覆盖了单页图像。实际业务里还有两块没测:一是多页文档的跨页上下文,比如一份合同的第 3 页引用了第 1 页的条款号,模型能不能利用跨页信息纠正识别错误;二是表格的跨页续行,长表格翻页之后表头缺失,识别结果的行列对应关系会不会崩。这两个问题在真实档案数字化项目里非常常见,我打算下一轮补上,先把单页的稳定性做扎实再说。

另外有个方向值得一试:把业务词表(人名、地名、产品型号)做成一份小词典,在提示词里给出十几个高频词作为示例,让模型在识别时有个先验。我前面提到行草 CER 从 11.9% 降到 8.7% 就是靠这个思路,成本几乎为零,值得所有垂直场景都试一遍。

最后的体会是:多字体适应性这件事,模型能力只是其中一环,预处理和数据分布对齐占的比重比很多人想象的大。我这次把倾斜校正加上之后,退化场景的 CER 直接从 14.8% 掉到 9% 出头,比换参数、换提示词的收益都高。所以真要落地,别一上来就纠结模型选型,先把输入图像的质量管住,很多时候问题就解决了一大半。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进