最近一段时间,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 解码会吃内存 |
| Python | 3.10 | 3.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 | 单图耗时 | 显存峰值 | 建议场景 |
|---|---|---|---|---|
| 640 | 14.7% | 0.4s | 5GB | 不推荐,仅用于粗略预览 |
| 1024 | 9.2% | 0.7s | 7GB | 大批量粗筛 |
| 1280 | 7.4% | 0.9s | 9GB | 日常默认 |
| 1600 | 6.1% | 1.1s | 12GB | 质量优先 |
| 2048 | 5.3% | 2.3s | 16GB | 小字密集、归档级 |
| 2560 | 5.1% | 4.1s | 22GB | 边际收益很低 |
从 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% 出头,比换参数、换提示词的收益都高。所以真要落地,别一上来就纠结模型选型,先把输入图像的质量管住,很多时候问题就解决了一大半。