1. 先把问题拆开混排识别的难点在哪里手写体混合印刷体识别这件事我从几年前做票据和试卷数字化项目时就开始反复折腾。那时候的常规做法是两套模型各管一段——先用版式分析把区域切成印刷块和手写块再分别送进两个识别器最后按坐标拼回去。这套链路能跑但维护成本高得离谱切错的区域永远救不回来手写和印刷贴在一起的地方基本无解。所以当端到端的多模态 OCR 方案开始成熟我第一反应就是把 HunyuanOCR 拉过来做一轮多字体适应性实测看看它在这种最难缠的混排场景里到底能顶到什么程度。先把结论往前放一点它不是万能的但在手写体混合印刷体识别这个细分任务上端到端路线带来的收益是实实在在的尤其是省掉了先切块再识别这一层容易出错的中转。这篇文章我会把自己搭测试集、调推理参数、跑对比、复盘错例的全过程摊开讲包括我踩过的坑和最后沉淀下来的几条经验。适合两类人看一类是正在选型 OCR 方案、纠结自建还是调 API 的工程同学另一类是手上有大量手写批注、试卷、合同、表单需要结构化的业务方想搞清楚这条路现在能走到哪一步。1.1 三类难点字形、版式、上下文混排识别难很多人第一反应是手写字丑。这只是表层。实际拆下来至少三层。字形层是大家最容易想到的同一个字印刷体有宋黑楷仿圆几十种字形手写体更是千人千面连笔、简写、异体字、书写习惯带来的笔画粘连都会让基于字形模板匹配的老方案直接崩掉。更麻烦的是手写风格的字库字体——有些排版里会故意用手写体字库做装饰性文字肉眼一看是手写实际上它是规整的矢量字形这类样本特别容易让模型判断混乱。版式层才是真正拉开差距的地方。印刷体文档通常是横平竖直、行距均匀的检测框好切一旦手写批注插进来行基线立刻歪掉批注可能压着正文、压在页边、写在两行之间的缝隙里甚至绕着表格画圈。传统方案靠投影法或连通域做行切分在这种场景下错得离谱。上下文层是最容易被低估的。手写内容往往信息密度极高——签名、日期、金额、勾选项、修改痕迹一个字错了整条数据的业务含义就变了。比如金额栏1000被识别成10000或者日期1/5被读成7/5模型在字符层面可能只错了一个 token但在业务层面就是事故。注意评估混排识别时千万别只看整体字符准确率。手写区域往往只占整页面积的 10%但它承载的关键信息可能占 80%。分区统计错误率比整体统计有用得多。1.2 HunyuanOCR 的端到端路线与我的三个关注点公开资料里HunyuanOCR 是一个十亿参数量级的端到端多模态 OCR 模型把文本检测、识别、部分结构化理解放在同一个模型里完成而不是传统的检测网络 识别网络级联。这个设计选择对混排场景的意义在于模型在输出文字的同时能利用整页的视觉上下文做联合判断而不是被切块边界人为割裂。我这次实测主要盯三件事第一字体泛化边界。印刷体它肯定没问题我想知道的是在楷体、仿宋、圆体、手写风格字库这些非主流但真实存在的字体上性能衰减曲线长什么样。第二手写与印刷交界处的处理。同一行里前半段印刷、后半段手写或者手写直接把印刷字覆盖掉一半这类样本是级联方案的重灾区端到端能不能吃掉。第三推理成本与参数敏感性。端到端模型通常对输入分辨率敏感我想把分辨率—精度—耗时这条曲线跑出来找到工程上真正划算的甜点位置。2. 自建测试集把多字体适应性变成可量化的数字网上现成的 OCR 评测集大多是纯印刷体扫描件手写样本要么量少要么场景单一。想测多字体适应性必须自己造数据。这一步花的時間比调模型多得多但我觉得值——没有一套自己完全掌握的测试集后面所有结论都是道听途说。2.1 语料设计让每个字都有考察目的语料我没有随便找文章灌进去而是按考察目的分层设计。具体做法是准备一份覆盖常用字和易混字的语料池然后针对不同子集做定向填充。子集内容构成样本量主要考察点S1 印刷单字体通知、说明类文本分别用宋、黑、楷、仿宋、圆体排版3005 字体 × 60基础识别能力、字体间差异S2 真实手写8 位不同书写者工整到潦草梯度分布150手写泛化、连笔鲁棒性S3 混排印刷正文 手写批注/修改/签名120交界处理、阅读顺序S4 低质实拍手机拍摄含倾斜、阴影、折痕、摩尔纹80退化鲁棒性S5 表格单据带边框线的表单、票据类60结构还原合计 710 张。这个量级不算大结论只能看趋势不能当权威基准但足够支撑工程选型判断了。语料内容上我特意塞了几类地雷形近字串如己已巳、未末、戌戍、数字串金额、编号、身份证样式全部虚构、中英混排、全角半角混杂的标点、少量繁体异体字、竖排小段落。这些在真实业务里出现频率不高但一旦出错代价极大。2.2 手写样本的采集与劣化处理手写样本最难搞。我的做法是找 8 位书写习惯差异明显的人每人写同样的 20 段短文本覆盖签名、日期、数字、短句、勾选。然后按工整/一般/潦草三档人工打标方便后面分档看结果。采集完之后我没有直接拿去测而是做了一轮可控劣化模拟真实拍摄条件随机旋转 ±8 度模拟手持拍摄歪斜叠加高斯模糊核大小 3 到 7模拟对焦不准加侧向阴影梯度模拟台灯打光不均加 JPEG 压缩伪影质量因子 60 到 85局部遮挡模拟手指压页或印章覆盖这一步很关键。很多方案在实验室干净扫描件上表现漂亮一到手机实拍就原形毕露。劣化样本能提前把这类问题暴露出来。2.3 标注规范与评测口径标注我用的是行级文本框 文本内容加上印刷/手写的属性标签。属性标签不是给模型看的纯粹为了评测时能分区统计。评测口径定死三条字符错误率 CER基于编辑距离CER (替换 删除 插入) / 参考文本总字符数按行计算后加权平均。行级完全正确率 EM一行识别结果与标注完全一致才算对这个指标对业务最直观。分区 CER分别统计印刷区、手写区、交界区交界区定义为手写与印刷字符间距小于 1 个字宽的行。计算 CER 的代码很简单但要处理好空白字符归一化否则全角空格和半角空格会白送一堆错误def normalize(s): # 统一空白、全角半角差异避免评测口径污染 s s.replace(\u3000, ) s .join(s.split()) return s def edit_distance(ref, hyp): m, n len(ref), len(hyp) dp list(range(n 1)) for i in range(1, m 1): prev, dp[0] dp[0], i for j in range(1, n 1): cur dp[j] cost 0 if ref[i - 1] hyp[j - 1] else 1 dp[j] min(dp[j] 1, dp[j - 1] 1, prev cost) prev cur return dp[n] def cer(ref, hyp): ref, hyp normalize(ref), normalize(hyp) if not ref: return 0.0 if not hyp else 1.0 return edit_distance(ref, hyp) / len(ref)提示CER 一定要在归一化之后算。我第一版忘了处理全角标点结果楷体子集的错误率虚高两个百分点排查了半天才发现是评测脚本的问题不是模型的锅。3. 环境搭建与推理链路调优测试集准备好之后重点就转到推理链路上。这一部分其实比模型本身更影响最终结果我见过太多人模型没换、只调了输入分辨率和后处理准确率就涨了三五个点。3.1 部署与最小可跑通链路我是在一台单卡机器上跑的本地推理模型权重走官方发布的版本。这里不贴具体路径避免环境差异导致你直接复制报错。整体链路抽象成三步图像预处理、模型推理、结果后处理。import time from pathlib import Path class OCRPipeline: def __init__(self, model, processor, max_side1280): self.model model self.processor processor self.max_side max_side def preprocess(self, image): # 等比缩放到短边不超过 max_side保留原始尺寸用于坐标还原 w, h image.size scale min(self.max_side / max(w, h), 1.0) if scale 1.0: image image.resize((int(w * scale), int(h * scale))) return image, scale def infer(self, image): img, scale self.preprocess(image) inputs self.processor(imagesimg, return_tensorspt) t0 time.time() outputs self.model.generate(**inputs, max_new_tokens2048) cost time.time() - t0 text self.processor.batch_decode(outputs, skip_special_tokensTrue)[0] return {text: text, scale: scale, cost: cost}这个骨架故意做得很朴素因为我想先测裸模型的能力上限后面再加花活。实际工程里你还得补上批处理、并发控制、超长图切片、结果缓存这些但那些属于工程优化不影响模型能力的判断。3.2 分辨率与切图策略最影响结果的一档参数端到端 OCR 对输入分辨率极其敏感这是我在多轮实测里感受最深的一点。给太小笔画糊成一团手写体直接崩给太大显存吃紧、耗时暴涨而且收益迅速递减。我在 S1 和 S2 上跑了一组分辨率对比固定其他条件只改输入短边输入短边S1 印刷 CERS2 手写 CER单页均耗时显存峰值9600.71%12.4%1.9s约 6.2G12800.45%8.6%3.2s约 9.1G16000.44%7.9%5.1s约 13.4G20480.43%7.6%8.4s约 20G这张表信息量很大。印刷体在 960 就已经很稳了1280 之后基本没有增益手写体从 960 到 1280 掉了将近 4 个百分点1280 到 1600 只再降 0.71600 之后基本走平。1280 是这条曲线上的性价比拐点除非你的场景手写占比极高且字特别小否则没必要往 1600 以上堆。切图策略上我试了两种整页直接送和按 2×2 或横向长条切分后拼接。结论是——在版式复杂多栏、表格嵌套时切图反而更差因为切分边界会切断行模型在边界处容易漏字或多字。整页送虽然显存吃得多但上下文完整阅读顺序也更靠谱。只有在超长图比如扫描仪出来的 A3 长条时才考虑切切的时候必须留 5% 到 10% 的重叠区。3.3 后处理把原始输出修成能用的结构化数据模型的原始输出离能直接入库还有距离。我加了三层后处理每一层都是被错例逼出来的。第一层是字符级归一化。处理全角半角、易混字符、大小写。这里要小心不能无脑替换——比如 O 和 0 在编号里可能是字母 O在金额里一定是数字 0得靠上下文判断。第二层是结构化解析。把连续的文本行按位置聚类成段落识别键值对姓名XXX、日期、金额。这一步我用规则 正则便宜且可控。第三层是置信度过滤。对低于阈值的行做标记进入人工复核队列。实测下来把后 5% 的低置信行挑出来复核能把整体业务准确率拉高一大截复核工作量也可控。import re CONFUSABLE {l: 1, I: 1, O: 0} def normalize_field(text, field_type): text text.strip().replace(\u3000, ) if field_type amount: text re.sub(r[^\d.], , text) for k, v in CONFUSABLE.items(): text text.replace(k, v) elif field_type date: text re.sub(r[年月], -, text).rstrip(日) return text注意后处理规则一定要有版本管理和回归测试。我吃过亏——为了修一个特殊格式临时加的正则三个月后把另一批数据搞乱了排查成本极高。4. 实测结果多字体适应性到底几斤几两好了进入正题。下面所有数据都来自我那套 710 张的自建集样本量有限看趋势为主。4.1 印刷体单字体基准这里不是瓶颈S1 子集的结果比我预期的好一些。整体 CER 0.44%行级完全正确率 97.2%。分字体看字体CER行级 EM主要错误类型宋体0.38%98.1%标点位置黑体0.45%97.6%形近字楷体0.61%96.4%笔画细处粘连仿宋0.72%95.8%长横与下划线混淆圆体0.83%94.9%数字 6/8/9 混淆手写风格字库1.9%88.3%字库字与真实手写混淆这里有个很有意思的发现手写风格字库的 CER 是真实手写工整档的两倍。为什么我分析下来是模型对这种规整但又不是标准印刷的字形产生了判断摇摆偶尔会把它当真实手写去走另一套解码路径导致输出不稳定。这提示我如果你的文档里大量使用装饰性手写字体反而要重点测这一类。另外注意仿宋那 0.72%错误集中在下划线、填空横线被压进文本的情况。这类错误靠模型本身解决不了得靠版式规则在后处理里剔除。4.2 手写与混排真正的分水岭S2 手写按三档分开统计S3 混排按区域分开统计结果如下子集 / 分档CER行级 EM备注S2 工整手写4.8%71.5%错误多为标点与连笔S2 一般手写8.2%54.3%连笔粘连、简写S2 潦草手写11.6%38.7%大面积漏字S3 印刷区0.9%95.1%受批注干扰轻微S3 手写区9.4%49.8%与 S2 一般档接近S3 交界区13.1%36.2%覆盖、压字、方向混乱交界区的数据最值得说。我在测试集里专门做了一批手写压在印刷字上的样本比如把同意两个手写字直接写在正文的某一行中间。传统级联方案在这种样本上基本是废的因为检测框会打架。HunyuanOCR 在这批样本上能做到把两层文字都输出出来虽然顺序和分组经常错但至少没丢信息。这个能力对合同批改、试卷判分这类场景价值很高——信息不丢后处理就有得救。反过来交接区里我最不满意的是阅读顺序。当手写批注写在行间缝隙时模型有时会把它插到上一行末尾有时插到下一行开头非常随机。这个问题的本质是纯视觉模型对哪个字属于哪一行的判断依赖排版线索而手写批注恰恰破坏了这些线索。4.3 低质图像与极端字体边界在哪里S4 低质实拍整体 CER 9.5%行级 EM 52.8%。分退化类型拆开看退化类型相对基线 CER 增幅说明轻度倾斜±3°18%影响很小中度倾斜±8°65%行切分开始混乱高斯模糊核 590%小字基本糊掉侧向阴影120%暗部字符漏检折痕穿过文本180%折痕被误判为字符手指遮挡视面积而定遮挡区直接丢失折痕那一项是最糟的因为折痕在图像上表现为一条细长高对比度的线模型有时会把它当成一或-输出污染结果。这个问题我在别的方案上也见过属于共性问题只能靠预处理做折痕检测或者后处理里过滤掉孤立的横线字符。S5 表格子集我没用严格的结构指标只看单元格内容还原率和行列数是否正确。结果是内容还原率 88%行列数正确率 91%合并单元格错得比较多。表格这块目前还是得搭配专门的表格结构识别做兜底端到端 OCR 直接出 Markdown 表格的能力实用但不够稳。5. 典型故障复盘与排查手册跑完这一轮我把所有错例按失败模式归了类整理成下面这张速查表。这份东西我觉得比结果数据更有价值因为它是下次遇到问题先看哪里的清单。5.1 失败模式速查表现象可能原因排查动作处理建议整行漏识别分辨率过低 / 行太长被截断检查切图与短边设置提到 1280 以上超长行切片留重叠手写批注消失批注与正文重叠被合并对比原图逐区核对分区裁剪后二次推理折痕被识别成一高对比度细线干扰看输出里孤立横线预处理去线 / 后处理过滤单字符行阅读顺序错乱版式线索被批注破坏检查行间插批注样本用坐标重排别信模型给的顺序数字 0 与字母 O 混淆缺少上下文约束看字段类型按字段类型做定向替换全角标点全错归一化缺失检查评测脚本输出侧统一转半角表格行列错位合并单元格看结构输出引入专门表格识别兜底竖排文本乱序阅读方向判断单独测竖排样本先做方向分类再送模型印章区文字丢失遮挡检查遮挡面积接受丢失或做印章分离长图后半段丢失输入被压缩看原始尺寸与缩放比长图必须切片处理这张表是我从大约 200 条错例里归纳的覆盖了 85% 以上的失败情况。建议你也给自己的业务场景建一张类似的表比记一堆总体准确率有用得多。5.2 我的几条避坑心得第一条别一上来就怀疑模型。我刚开始测的时候发现某个子集错误率异常高第一反应是模型不行。结果查下来是自己切图时把图像宽度压到了 800笔画全糊了。后来我养成了一个习惯——任何异常结果先看送进模型的图长什么样肉眼看不清楚的字模型也看不清。第二条把原始坐标留住。端到端模型输出文本很方便但如果你只拿文本后面想按区域统计、想重排阅读顺序、想做字段对齐全都没法做。我在第一版里只存了纯文本后来要加字段抽取硬是重跑了一遍全量数据浪费了一整天。第三条建立错例池定期回看。我专门建了一个目录每次发现典型错例就存进去附带当时的参数配置。这个池子后来成了我调参和写规则的主要依据。人是会忘的尤其是上次那个报错是怎么解决的没有池子基本想不起来。第四条手写体想提升方向在数据不在参数。我试过各种提示词引导、各种后处理规则效果都有限。真正有明显提升的是往测试集里补充同类型书写者的样本做少量适配。手写这种高度个性化的问题参数调优的边际收益很低。第五条别迷信端到端能吃掉一切。端到端在混排上的优势很明显但在复杂表格、多栏期刊、竖排古籍这些场景版式分析仍然有不可替代的价值。我的建议是端到端为主版式分析兜底而不是非此即彼。第六条耗时和精度要分开测。我一开始把精度测试和性能测试混在一起跑结果因为显存回收不及时耗时数据全飘了。后来改成两轮独立跑精度看结果耗时看单次冷启动数据才稳。提示如果你的业务里手写占比超过 30%我建议把评测重点从整体 CER切换到关键字段完全正确率。整体指标会被大量简单印刷文本稀释看起来很漂亮实际业务体验却很差。最后分享一个我自己在用的评估小技巧。我会把错例按业务影响打个标A 类是完全不可接受金额、日期、证件号错B 类是可人工纠正普通文本错字C 类是无所谓标点、格式。实测下来HunyuanOCR 在我这套集子上 A 类错误率大约 2.1%其中大部分集中在潦草手写的数字串。这个数字比整体 CER 更能反映它能不能直接上生产。我个人在实际操作中的体会是只要 A 类错误能压到 2% 以下配合低置信度复核这条链路就已经具备落地价值了而如果想再往下压靠的不是换模型是把你的业务语料喂进去做针对性适配。