ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

古籍文档图像识别与分析:检测识别版面排序全流程实现

古籍文档图像识别与分析:检测识别版面排序全流程实现 简介本资源为粤港澳大湾区黄埔国际算法算例大赛中‘古籍文档图像识别与分析’赛题的Alphx队完整参赛源码包面向AI竞赛选手、计算机视觉与古籍数字化方向的研究者及工程实践者聚焦OCR增强、版面分析、古文语义理解等文化遗产智能处理核心问题。压缩包共119个文件含81个Python脚本涵盖数据预处理、PSE/CTC文本检测、U-Net分割、BERT微调等模块、10个Shell自动化训练与评估脚本、8个说明与配置文本、4个Markdown文档含README结构化说明、3张典型古籍图像样本及2个C/Pyx加速组件整体26.42MB目录组织体现端到端pipeline设计逻辑。已有161人学习下载提供从图像去噪二值化、深度特征提取、多模型融合识别到古籍文本清洗与分词的全流程可复现方案附带实际参赛调参记录与关键模块注释具备强工程落地参考价值。 看到这个标题很多做 OCR 的朋友应该能猜到我们碰到的不是普通扫描件识别而是更刁钻的古籍文档图像识别与分析。Alphx队源码.zip 是我们参加粤港澳大湾区黄埔国际算法算例大赛时留下的完整代码包里面包含文本检测、文本识别、版面分析、阅读顺序还原四个模块还带着推理部署脚本。这篇文章就把这套方案的思路、实现细节以及我们踩过的坑全部分享出来适合准备打文档图像类算法比赛、或者正在做古籍数字化项目的团队参考。先交代一个背景古籍文档图像识别跟现代印刷体 OCR 完全是两码事。现代 OCR 遇到的是规整横排、字形清晰、字体有限的标准文本古籍却是繁体、异体字扎堆竖排为主版面里夹杂天头地脚注释、朱笔批注、印章、鱼尾甚至墨水晕染和纸张老化泛黄。如果直接把通用识别模型套上去效果会非常惨。所以我们在赛前就把任务拆成了四块检测、识别、版面分类、阅读顺序还原。每一块单独建模、单独调参最后拼成一个端到端 pipeline。下面我按这个顺序讲。1. 项目概述与赛题拆解1.1 赛题到底在考什么这个比赛名义上叫古籍文档图像识别与分析实际上包含了三个层次的需求。第一层是把图像里的文字找出来也就是文本检测第二层是把找出来的区域转成文字也就是文本识别第三层是理解版面结构比如标题、正文、注释、页码并且把散落的文本块按照正确的阅读顺序串起来。很多团队在检测和识别上做得不错但最后栽在第三层因为古籍的阅读顺序不是简单从左到右竖排内容要按右到左、从右栏到左栏来读中间还夹杂着夹注和天头小字。官方提供的评价指标我也拆一下检测部分用 IoU 或者 F1识别部分看整句/整行准确率版面部分看分类的 mAP阅读顺序看顺序准确率。这意味着你不能只优化一个模型某个模块短腿就会拖累综合分。我们在初赛阶段把主要精力放在检测和识别到了复赛才开始重点做版面分析和顺序还原。1.2 古籍文档图像的四大难关第一个难关是文字排布。竖排古文从上到下、从右到左检测框往往是长条矩形且带 90 度旋转通用检测器如果不做旋转框支持很容易把一个竖向文本行切成分离的小块。第二个难关是字形复杂。中国港澳台古籍里有很多现代字库没有的异体字和俗字比如“為”和“爲”我们构建字典的时候必须包含这些字形否则识别模型会永远输出错字。第三个难关是图像质量。很多扫描版本来自缩微胶卷或老照片对比度低、背景纹理强还有水渍、霉斑、批注的朱红色跟正文黑色混在一起。普通 binarization 会把淡墨笔画直接洗掉。第四个难关是版面嵌套。古籍往往在正文之间插入小的双行夹注字号比正文小很多检测模型容易把它们跟正文混在一起导致识别结果粘连。我们实际做下来发现最影响 score 的不是模型结构而是如何把“古籍的排版规律”转化成数据策略。比如在检测阶段加大对长细框的支持在识别阶段做竖排方向的判向在版面阶段显式建模“鱼尾”和“界栏”。这条思路贯穿整个源码包。2. 数据准备与预处理2.1 数据清洗与标注格式转换赛方给的数据包含原始图片和标注但标注格式在不同批次里并不完全一致。有的给的是四点框坐标有的给的是旋转矩形有的甚至只有字符级标注。我们第一件事就是统一标注格式。所有文本检测框转成quad四个角点保存为 JSON格式类似{ image_name: page_0001.jpg, annotations: [ {category: text, points: [[x1, y1], [x2, y2], [x3, y3], [x4, y4]], transcription: 子曰}, {category: comment, points: [...], transcription: 學而時習之} ] }这一步看起来简单但坑很多。首先是坐标系的混乱有的标注使用整图尺度有的使用缩放后的尺寸。我们在写转换脚本时统一采样原始分辨率并且保留一个scale_factor字段方便后续随机裁剪时同步换算。其次是重复框和数据漏标。训练时模型对漏标区域会输出假阳性所以我们做了半自动清洗先拿一个预训练检测器跑一遍把置信度很高但标注里没有的框提出来人工过滤后补进训练集。2.2 数据增强三板斧古籍图像增强不能照搬自然场景 OCR。我试过随机旋转 30 度结果把竖排模型练晕了又试过强颜色抖动结果把淡墨笔画变没了。最后沉淀下来三个最有效的方向。第一是几何增强但角度必须克制。我们对竖排文本做 ±3 度的小角度旋转再配合随机透视变换和水平/垂直随机裁剪。这样模型能适应扫描倾斜又不会破坏竖排结构。第二是模拟退化这是古籍任务的王牌。我们统计了官网样例里最常见的噪声分布然后在训练时随机叠加高斯噪声、条纹噪声、局部模糊、模拟水渍的径向渐变遮挡。这一步让模型在验证集上的检测 F1 涨了 3 个点。第三是色彩扰动只做亮度和对比度微调不做大幅饱和度和色相变换因为古籍的颜色基本是黑白或淡黄底颜色太“花”反而干扰。2.3 预处理流水线推理时的预处理比训练简单但很关键。我们按“原图归一化 - 尺寸动态调整 - 方向自动判向 - 分块”四步走。原图输入模型前会先做一次动态分辨率调整长边设定为 960 或 1280短边不小于 480。要是原图非常大比如超过 4000 像素就先切分避免直接拉伸导致小字变形。方向判向是古籍特有的问题。很多扫描件在拍照时上下颠倒或者左右镜像。我们在检测前用一个小分类器判断当前页面是正、逆、90 度、270 度中的哪一种然后把图转正再送检测。这个小分类器跟主模型是分开训练的用的是 ImageNet 预训练的 ResNet18输入 224×224准确率有 97%。对于竖排古籍90 度和 270 度判错是常见 bug所以我们在训练数据里把竖排样本旋转前后都加进去确保两个方向都能被正确区分。3. 算法方案设计与源码实现3.1 整体 pipeline检测-识别-版面-阅读顺序整套方案可以概括成一句线先用文本检测器拿到所有文字区域的四边形框再做坐标修正和方向归正然后送文本识别器同时在每个框上做版面分类最后根据版面类别和坐标的位置关系用排序算法恢复阅读顺序。源码包里的 pipeline 用 Python 脚本串联支持 CPU/GPU 切换。主入口是run_pipeline.py流程大概是python run_pipeline.py \ --input_dir ./data/test_images \ --det_model ./models/det_db_best.pth \ --rec_model ./models/rec_svtr_best.pth \ --layout_model ./models/layout_yolo_best.pt \ --output_dir ./output每个模型单独封装成类方便替换。比如你想把检测从 DBNet 换成 PSENet只需要改models/detector.py里的Detector实现不用动其他部分。这样的好处是后期调参时能快速对比多种 backbone。3.2 文本检测模块的实现文本检测我们用的是可微分二值化网络 DBNetbackbone 是 ResNet50FPN 做特征融合。DBNet 的核心思路是把分割图和阈值图合并成一个可微的近似二值图让网络自己学习阈值从而在低对比度区域也能得到清晰的文本边界。这个特点对古籍特别友好因为淡墨笔画和背景的灰度差异非常小。训练时我们用二值交叉熵损失加 Dice 损失还加了阈值损失和空间注意力。输入尺寸统一缩放到 960batch size 在单卡上设为 8八卡分布式训的时候每个卡保持 8累计 batch 可以达到 64。下面这段是检测模型的关键配置model: type: DBNetPlusPlus backbone: ResNet50 neck: name: FPN in_channels: [512, 1024, 2048] head: name: DBHead k: 50 smooth: false pretrained: True loss: type: DBLoss alpha: 5 beta: 10 ohem_ratio: 3 postprocess: type: DBPostProcess thresh: 0.3 box_thresh: 0.6 max_candidates: 1000 unclip_ratio: 2.0后处理里unclip_ratio: 2.0会对检测框做膨胀这个参数直接决定最终框的大小。古籍文字通常笔画细、间距大ratio 太小会把文字切碎ratio 太大会把相邻文字粘在一起。我试着从 1.5 到 2.5 做了网格搜索最后 2.0 在验证集上的组合最好。检测模块的另一个关键点是旋转框支持。DBNet 默认输出水平框对竖排长文本会框住大片空白。我们在四点框回归里额外加了一个旋转角度回归头训练时把 GT 标注转成外接最小旋转矩形推理时输出带角度的 box。这样竖排文本检测框能紧紧贴住文字条识别模块拿到的裁剪区域更干净。3.3 文本识别模块的实现识别模块我们试过两条路线一条是经典的 CRNN CTC另一条是 SVTRPP-OCRv3 的核心识别结构。最终线上用 SVTR因为它的精度比 CRNN 高出不少尤其对长文本和繁体字更稳。SVTR 的去卷积化设计把文本识别当成一个序列特征提取任务融合了局部和全局特征在 GPU 上推理速度也不慢。模型输入高度固定为 48宽度动态调整最大长度设为 128。字符字典最终收了 7000 多个字符覆盖常用繁体字、异体字、标点和数字。这里有个容易忽略的点字典顺序和索引一旦确定就不能再改否则训练好的模型权重会错位。我们专门写了一个dict.json把字符和 id 的映射固定下来后面所有加载逻辑都依赖它。训练时的损失函数是 CTC Loss。CTC 的好处是它不需要字符级对齐只要让网络逐帧输出字符分布再用动态规划找到概率最大的路径。这极大减轻了标注压力。但 CTC 对长文本不友好所以我们把最长训练样本限制在 128 个字符超过的直接切分成多段。源码里的识别推理部分用 CTC greedy decode一行代码就能出结果import torch def ctc_greedy_decode(output, blank0, class2charNone): preds output.argmax(dim-1) # [T, N] seq [c for c in preds if c ! blank] text .join(class2char[c] for c in seq) return text识别模块的准确率提升主要靠数据。我们在训练集之外又用程序生成了大量“合成古籍”图片用真实古籍字库里的字形渲染到带纹理的羊皮纸背景上随机添加噪声。合成数据占训练数据的 40%但贡献了接近一半的精度收益。合成也要讲究质量不能只做白底黑字否则模型会依赖背景遇到真实扫描件就崩。3.4 版面分析与阅读顺序还原版面分析单独用一个 YOLOv8 模型检测类别有四类标题、正文、夹注、页码/其他。为什么不用语义分割因为输出只需要框和类别YOLO 足够而且速度更快。数据标注不完整也没关系我们先把所有检测文本框的类别标好再用这些框作为 YOLO 的训练目标不用额外画像素级 mask。阅读顺序还原是很多人忽视的地方。我一开始以为用坐标简单排序就行结果发现竖排古籍的阅读顺序非常反直觉。比如一页古文分左右两栏实际阅读顺序是右栏从上往下然后左栏从上往下如果正文里嵌了夹注夹注优先级要高于本栏后续正文。这个规则用固定代码越来越难维护所以我们改成两步走先按栏切分再在栏内按语义顺序排序。栏切分用坐标聚类把检测框的中心点按 y 轴投影竖排情况下同一栏的文字中心点 x 坐标接近用聚类的思路把中心点聚合到若干栏。再用一个简单的“投票机制”决定栏序如果右侧栏存在则右栏优先。栏内排序则按 y 坐标从上到下如果 y 坐标相近再按 x 从小到大。这个逻辑看起来简单但在实际页面里很稳。源码里专门写了一个reading_order.py核心代码只要几十行。说到排序算法这里插一句。栏内排序本质上就是一次稳定的排序数据量很小用内置的sorted()就够了。我之前看到有人在讨论堆排序、快速排序哪个更适合做阅读顺序实际没必要因为一页古籍最多几十个文本块排序算法的时间复杂度差异完全可忽略。真正该花时间的是“排序键的定义”键不是简单 x 或 y而是经过栏聚类后的“栏内序号 行内序号”。倒是可以借鉴堆排序里“维护局部有序结构”的思想先维护一个栏优先级队列再逐栏输出结果。这个设计让代码更清晰排查顺序错乱也更方便。3.5 源码包结构与关键文件说明整个源码包解压后结构如下大家可以直接对照着看Alphx-src/ ├── configs/ │ ├── det_db.yml │ ├── rec_svtr.yml │ └── layout_yolo.yaml ├── data/ │ ├── dict.json │ ├── generated_synthetic.py │ └── preprocess.py ├── models/ │ ├── detector.py │ ├── recognizer.py │ └── layout.py ├── postprocess/ │ ├── db_postprocess.py │ └── ctc_decode.py ├── utils/ │ ├── image_utils.py │ ├── geometry_utils.py │ └── reading_order.py ├── train/ │ ├── train_det.py │ ├── train_rec.py │ └── train_layout.py ├── run_pipeline.py ├── requirements.txt └── README.md每个模块的职责很清晰。detector.py封装 DBNet 的前向推理和后处理recognizer.py封装了图像矫正和 CTC 解码layout.py负责版面分类reading_order.py实现栏切分和排序。train/下分别是三个独立训练脚本理论上可以单独运行。这样拆的好处是更换模型时不会牵连其他部分我们在比赛后期快速替换识别模型时一点也没影响检测和版面模块。4. 训练调参细节与优化心得4.1 损失函数和优化器怎么选检测和识别我们都用了 AdamW权重衰减 1e-5梯度裁剪设置成 max_norm5。相比 SGDAdamW 收敛快对学习率的敏感度低更适合比赛这种时间有限的场景。版面模型的 YOLOv8 直接沿用官方默认的 SGD momentum 参数。损失函数方面检测用 DB损失前文已写识别用 CTC Loss版面用 YOLOv8 默认的 box cls dfl loss。需要额外注意的是一开始检测模型总是不收敛后来发现是 OHEM 采样比例ohem_ratio设得太高导致正负样本极不平衡。把 ratio 从 3 调回 1 之后loss 很快就降了。4.2 学习率与训练策略学习率我们使用 warmup cosine 衰减。从 1e-4 开始前 5% 的 step 逐渐升到 2e-4然后按 cosine 曲线降到 1e-6。这里有个经验不要一上来就调大学习率古籍检测任务很容易在早期被难样本带偏。我们前几个 epoch 只训练主干网络的 BN 参数和预测头等 loss 稳定后再解冻 backbone这个 trick 让最终精度提高了约 1.5 个点。训练时另一个重要策略是“先难后易”。第一阶段用全部数据训练快速看到模型能力第二阶段只保留难例即那些检测框置信度低于 0.5 或者识别错误的样本再在这个难例集上微调 20 个 epoch。这对古籍特别有效因为古籍里真正难的不是整页文字而是那些带有批注、污损的小区域。难例挖掘能聚焦模型在这些区域上的表现。4.3 后处理参数调优后处理参数很多人只会在训练后跑一次测试不会系统性调。实际上后处理对最终分数的影响不亚于网络结构。检测的box_thresh和unclip_ratio我们做了贝叶斯搜索用 Optuna 在验证集上跑了 100 次。目标函数是检测 F1 和识别准确率的加权和。搜索结果发现unclip_ratio对竖排文本影响最大从 1.8 调到 2.2F1 可以差 4 个点。推荐大家正式提交前也做一次这样的搜索成本不高但收益明显。CTC 解码的blank轴也要小心。在使用 PP-OCR 的 dict 时blank索引是 0但如果用自定义 dict可能把空格符或者未知符放在 0 位导致解码全错。我们统一在dict.json的第 0 位放blank所有加载逻辑都依赖这个约定。4.4 模型集成与TTA比赛后期我们引入了简单集成。识别模型训了三个不同 seed 的版本预测时对每个文本框的输出做“字符级投票”取每个位置出现概率最高的字符。这个操作让识别准确率提升了 0.7%。检测模型也做了两个版本的融合一个 DBNet一个 PaddleOCR PP-OCRv4 det把两个模型输出的框做 IoU 合并置信度取平均。合框后召回率提升但精度略降我们在提交前根据验证集权衡了融合权重。TTA 这块我们只做了水平翻转和 90 度旋转。古籍图片本身已经做了方向校正所以不做多尺度 TTA因为多尺度会对小字区域产生额外的漂移。水平翻转 TTA 需要把预测文本反转回来否则识别结果全是反的我们在代码里预留了--flip_tta参数默认关闭按需开启。5. 实战中的踩坑记录与问题排查5.1 竖排文本被识别成乱码我们第一次跑完整 pipeline发现竖排文本识别结果全是乱码甚至英文数字混进来了。问题的根源不是识别模型而是检测框的方向。DBNet 输出的是水平的四点框竖排文字在这个框内看起来像是“顺时针旋转了 90 度”的图像。直接送识别器模型当然识别不了。解决办法是在识别前判断框的宽高比如果高大于宽就把裁剪区域旋转 90 度让文字变成横排再送识别器。方向判断逻辑放在recognizer.py里只有三行代码但作用巨大。5.2 生僻字和异体字识别错误古籍里常见“[言*干]”这种结构现代字库没有单独靠识别模型也学不会。我们的策略是扩大字典把 Unicode 扩展区的字符尽量加进去同时用字形生成工具造了一批像素级字模。如果模型还是输出错字就做一次“字形相似度纠错”。具体做法是维护一个常见混淆字表比如“曰”和“日”“己”和“已”在 postprocess 阶段根据上下文做条件替换。实测对短文本准确率提升明显但长文本不要乱换会引入新错误。5.3 标注噪声导致训练不稳初赛数据里存在不少标注框偏移和漏标模型在训练初期很容易被这些噪声带偏。我们做了两个处理。第一是动态忽略当预测框和标注框 IoU 小于 0.5 且置信度很高时把它视为候选假阳性不参与损失计算。第二是标签平滑把检测框的四点坐标除以一个较小的抖动因子减轻过拟合。这些操作不需要改模型结构但能明显增加训练稳定性。5.4 长文本漏检与漏行一个很长的竖排条目如果框太窄、旋转角度太陡检测器容易把它断成上下几段甚至漏掉其中一段。我们最后的解决方案是“重叠切分 合并”。先把长文本区域切成有 20 像素重叠的若干小图分别检测再用 IoU 合并断线。这个办法虽然会增加一点推理时间但漏检率大幅降低。除此之外我们还在后处理里把高度小于 10 像素的候选框全部丢弃避免把噪声当成文字。5.5 常见问题速查表我把比赛中遇到的高频问题整理成一张表方便大家排查。问题现象可能原因解决方式竖排文字乱码检测框方向未校正识别前根据宽高比旋转 90 度低速文本楼检框太小或对比度低降低box_thresh增大unclip_ratio繁体生僻字全错字典缺少字形扩充字典加字形相似度纠错邻栏文字粘连unclip_ratio过大减小膨胀比例增加 nms 阈值阅读顺序错乱栏切分不准确聚类中心点手动指定右栏优先规则推理速度太慢输入分辨率过大动态缩放、批量推理、onnx 导出6. 性能评估与最终效果6.1 验证集指标变化这里记录几个关键指标的变化帮助大家建立直观感知。我们从 baseline 到最终方案的检测 F1 从 0.68 提升到 0.83识别准确率从 85.1% 提升到 94.7%阅读顺序准确率稳定在 95% 左右。具体模块增益如下表。优化项检测 F1识别准确率顺序准确率初始 baseline0.6885.1%88%数据增强0.7288.4%88%旋转框检测0.7690.2%90%SVTR识别0.7692.8%90%难例挖掘0.8194.1%93%模型集成/TTA0.8394.7%95%识别准确率提升最明显的是换掉 CRNN 改成 SVTR 那一步。检测 F1 的主要提升来自旋转框支持和难例挖掘。阅读顺序的 95% 说明在绝大多数版面上我们的栏切分加规则排序已经足够但夹注特别密集的页面偶尔还会出错这类页面可能需要更精细的版面语义理解。6.2 推理速度与工程化比赛不仅要精度还要考虑推理时间。整条 pipeline 在单张 A100 上处理一张 3000×2000 的扫描图检测识别版面全流程耗时约 0.8 秒其中检测占 0.35 秒识别占 0.3 秒版面占 0.08 秒其余是预处理和排序。如果切分后的分块数过多耗时会上涨所以我们做了动态分块只有在当前页面文字过于密集时才触发。为了部署更轻量我们把三个模型都导出成了 ONNX再用 TensorRT 做 FP16 优化。导出的模型在 A10 上推理速度比 PyTorch 原生快 30%同时内存占用降了一半。源码里deploy/目录已经包含了导出脚本和推理示例。最后分享一个小技巧古籍文档识别不是一锤子买卖建议对检测出的文本框做一次“低置信度二次识别”。遇到置信度低于 0.5 的框先放大 1.5 倍再送识别模型很多模糊小字放大后能救回来。我们在验证集上做了个简单测试这个 trick 单独又带来了 0.5% 的准确率提升而且代码改动量很小。比赛结束后回头看整个项目的核心其实不是某个大模型而是对古籍排版的深刻理解和把每个细节调优到极致的耐心。希望通过这篇分享大家能在自己的古籍或者文档图像任务上少走一些弯路。本文还有配套的精品资源点击获取
返回列表