
最近一段时间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, devicecuda): # 类名按仓库 README 替换 processor AutoProcessor.from_pretrained(model_path, trust_remote_codeTrue) model AutoModelForVision2Seq.from_pretrained( model_path, torch_dtypetorch.float16, device_mapdevice, trust_remote_codeTrue, ).eval() return processor, model def preprocess(img_path, long_side1600, short_side960): 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(imagesimg, textprompt, return_tensorspt).to(model.device) with torch.inference_mode(): out model.generate(**inputs, max_new_tokens4096, do_sampleFalse) return processor.batch_decode(out, skip_special_tokensTrue)[0]这里有两个参数值得单独说。max_new_tokens设成 4096 是因为整页表格的 OCR 结果很容易超过 2000 token设小了会被硬截断表现为页面下半部分凭空消失这个 bug 我第一次跑就撞上了排查了半小时才反应过来是长度限制。do_sampleFalse必须开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% 左右主要错在形近字上比如己/已/巳、未/末。这个不是模型的问题是像素信息本来就快没了我试过把长边提到 2048CER 回落到 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.4sH-OCR长边 16006.1%79.5%84.3%1.1sH-OCR长边 20485.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关键处理手段低分辨率长边小于 100018.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 到 2048CER 降了 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 卡跑单图是宽裕的跑 batch4 就有点紧。吞吐方面batch1 时 1600 长边约 1.1 秒一页batch8 时约 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_sampleFalse红章区域大面积漏识颜色干扰分离通道观察通道分离 对比度增强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% 出头比换参数、换提示词的收益都高。所以真要落地别一上来就纠结模型选型先把输入图像的质量管住很多时候问题就解决了一大半。