从一张旧扫描件到可搜索档案:用 PyTorch 做好中文 OCR
中文档案识别可以拆成文字检测和内容转录两步。以旧扫描件为例,介绍如何准备标注、用 DBNet 定位文本行、用 CRNN 识别字串,以及怎样评估错误并用 ONNX 部署。
一、先把任务说清楚:扫描件里哪些字要读
一份旧目录扫成图片以后,电脑能打开它,却没法按书名搜索。人看一眼就知道哪里是标题,哪里是页码,模型面对的却只是一块块深浅不同的像素。要让扫描件变得可搜索,可以把工作拆开:文字检测负责找出页面上的文本行,文字识别负责把每一行转成字符。系统再按照版面顺序整理结果,把文本和原页关联起来。以后发现转录有误,校对人员可以直接找到那一页、那一行。
两步分开有个实在的好处:出了错,容易找到该修哪里。比如一页目录少了一行,先看检测框是否覆盖了那一行;框的位置没问题,再检查送进识别模型的裁图。假如裁图里只剩半个字,加大识别模型通常也帮不上忙。反过来,框得很准却总把“己”读成“已”,就该去看训练样本和字典。调试时保留原页、叠框图和裁图,比盯着最后的转录结果猜原因省事得多。
项目刚起步,范围可以小一点。先做横排印刷文档,明确标题、正文、页眉和页码要不要收录。遇到双栏目录时,先判断每行属于哪一栏,再在栏内从上往下排;如果只按纵坐标排序,左右两栏的内容会穿插在一起。手写批注、竖排正文和复杂表格可以单独归档,等基础流程稳定后再补。第一批支持哪些版式,最好写进验收标准,免得模型训练完才发现产品需要的其实是另一套阅读顺序。
PyTorch 适合用来搭这个原型。开发时可以直接看中间张量、打印批次里的标签,也可以单独跑检测器或识别器。框架解决的是训练和调试的便利性,效果还得靠数据与验收支撑。先挑几页真实档案,手工写出预期结果,再用它们走通输入、转录、搜索和回看原图。一个小闭环能提早暴露行序、字符和页码之间的接口问题。
二、准备数据时,先约定什么算一行
标注员拿到同一张目录页,有人会把书名和右侧页码框在一起,有人会拆成两个框。如果两种标法混着进训练集,检测器会收到互相冲突的信号。框怎么画,应当由最终用途决定:如果书名和页码分别存入两个字段,就分开标;如果交付的是逐行转录,则提前约定怎样记录点线和空白。可以先让两个人各标几页,对照他们意见不同的地方修订规则,再开始大批量标注。
每个文本区域保留四个顶点及对应转录,检测和识别便可共享一套标注。顶点按固定顺序记录,坐标以原始图像为准,转录尽量保留原文的字符。全角标点和半角标点要不要统一、异体字怎样登记、污损遮住的内容如何标记,都应提前约定。完全看不清的地方可以标为忽略,别让标注员凭上下文补出一个看似合理的字。搜索需要的字符归一化,可以另外生成索引字段,原始转录仍按原样保留。
采样时别只挑最清楚的页面。旧档案常见的麻烦包括装订处发暗、纸张泛黄、墨迹透到背面,以及小号字经反复复印后连在一起。先按来源、版式和扫描质量盘点样本,再划分训练集、验证集和测试集。同一本书或同一批近重复扫描件最好放在同一边。否则模型在训练时已经见过几乎一样的页面,测试成绩看着不错,换一批档案就可能下滑。
真样本不够时,可以用有权使用的文档素材补一些合成页面,模拟轻微倾斜、亮度波动、纸面阴影和压缩失真。旋转或缩放图像时,标注坐标也要一起变;裁剪如果切掉了行首,就不能继续把完整句子当标签。合成样本主要用于补齐少见情况,最后还是得用真实扫描件验收。尤其是旧印刷品里的断笔和墨团,简单加一点随机噪声,并不能完全还原它们的样子。
三、让 DBNet 找到文本行,别让裁图丢掉笔画
DBNet 是一种基于分割的文字检测模型。可以把它的概率图想成一张覆盖在扫描件上的热力图:某个位置越像文字区域,对应的分数就越高。网络先提取不同尺度的图像特征,再通过特征金字塔融合这些信息,让大标题和小字号正文都能得到关注。训练时,模型还会预测阈值图,并用可微的方式近似二值化,让与文字边界有关的误差能够参与反向传播。
常见部署实现会对概率图设阈值,再从连通区域提取轮廓、扩张边界并生成文本框。训练时的可学习阈值,并不意味着推理阶段就不用检查后处理参数。阈值过高,浅色小字可能被整行漏掉;阈值过低,页边阴影可能被当成文字。扩张幅度也需要配合看:框太紧会切掉撇捺,框太松则可能卷入上一行的标点。与其在同一张图上反复试,不如选几组清晰度和行距不同的页面,分别检查漏框、粘框和裁切。

输入尺寸要跟页面上的最小字号一起考虑。一页清晰的大图缩到固定尺寸后,正文小字可能只剩几像素高,再深的网络也找不回已经丢掉的细节。可以比较整页缩放和分区处理:前者步骤少,后者能留下更多细节,但需要合并重叠区域,并把框映射回原页。处理轻度倾斜时,可以用四边形裁出较平直的文本行;遇到书脊附近明显弯曲的页面,则应单独统计,别指望一次透视变换就能把整行拉平。
检测阶段可先用交并比匹配预测框和人工框,再看精确率与召回率。交并比就是两个框重叠面积除以它们合起来覆盖的面积。这个指标好算,却不完全等同于裁图质量:一个长条框就算整体重叠得不错,行尾少了半个字,识别结果照样会错。验收时应配几张叠框图,重点抽查浅色小字、紧挨着的两行和靠近装订线的文字,看看模型在哪种情况下最容易丢信息。
四、让 CRNN 把裁图读成字串
对横排印刷文字,CRNN 是一条好理解的路线。卷积网络先把裁图变成特征图,再沿宽度方向取出一串特征,交给双向循环网络理解前后关系。训练时使用 CTC 损失,它允许模型在没有逐字坐标的情况下学习:标注只需写出整行内容,不必指出每个字落在哪个位置。模型在每个时间步上都可以输出一个字符或空白标记,解码时再按规则处理连续重复和空白。
真正的重复字不能随手去重。以目录标题里的“人人”举例,如果采用最常见的贪心解码,两个相同字符之间需要有空白标记,解码后才会留下两个字。假如长行被压得太窄,模型得到的时间步数不够,重复字、数字串和密集标点就更容易丢失。实践中可以把裁图统一到相近高度,保留宽高比,并按相近宽度组织批次。遇到极长文本,要结合输入上限决定怎样分段,同时保留片段在原行里的顺序。
字典要贴着馆藏内容建。除了常用汉字,还应核对书名里的生僻字、旧版标点、卷册编号和常见的拉丁字母。CTC 的空白标记需要独立编号,不能占用普通空格的位置。训练、验证和部署使用的字符表也要同版;如果字符总数没变,排列顺序变了,程序可能依旧跑得起来,却会把某个编号解码成另一个字。发布权重时一起保存字符表和版本信息,比上线后对着整页错字排查省心。
排查时可以先用人工框裁图,测识别器本身,再换成检测模型的裁图,测整条链路。两次成绩差得大,先看漏框、裁切和行序;两次都不理想,再查字典覆盖、图像质量和标注。评估时可以用字符错误率,把替换、漏字和多字的次数合起来,再除以参考文本的字符数。书名和馆藏编号还适合单独统计完整匹配率,因为这里哪怕只错一个字,检索也可能找不到正确条目。
五、导出 ONNX 后,用真实档案验收
检测器和识别器可以分别导出为 ONNX,再用 ONNX Runtime 加载。导出成功只说明文件能生成,下一步要拿同一批输入对照 PyTorch 和部署端的结果。重点核对颜色通道顺序、图像归一化、缩放与填充方式,以及识别输出的时间维度。最好先比模型输出,再比最终文本。如果只看最后的字符串,两端偶尔给出同样的结果,也可能掩盖预处理上的差异。
服务耗时也要分段记。读文件、解码图像、检测、逐行裁图和识别都占时间,一页只有标题的封面与塞满小字的目录页,开销差别很大。可以把同页尺寸相近的裁图成批送入识别器,减少重复调度;测试时同时保留普通页和密集页,记录典型耗时及处理较慢页面时的耗时。若打算换成更轻的骨干网络,先看它是否还能读清小字号目录,别只比较两份模型的文件大小。
交付时,每页结果可以保留页码、文本行、原图位置和待复核状态。模型分数适合给校对任务排优先级,但高分也会认错形近字,低分也未必转录有误。给校对人员留一张原行裁图和回看整页的入口,他们才能分清一个字符究竟印成什么样,以及它和前后行是什么关系。正式入库前还可以抽查几个真实搜索词,确认索引能找到正确页,而不是只看离线识别分数。
后续遇到新来源的资料,先抽几页做对照,再决定是否整批处理。换扫描设备后,如果漏字集中在页边,先看扫描裁边和缩放;如果整行读得通顺,却总在某个卷册编号上出错,就把这类样本单独送审。人工修正留在原有页行记录上,再把确认过的错误样本加入下一轮数据。这样即使模型换了版本,档案人员也能追溯每一处转录来自哪张图、改过什么。