ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PaddleOCR票据信息智能提取:从文本检测到结构化字段实战

PaddleOCR票据信息智能提取:从文本检测到结构化字段实战 简介面向图像识别与深度学习课程设计/毕业设计场景这是一套基于PaddleOCR的票据信息智能提取项目包适合需要完成OCR票据识别任务或入门PaddlePaddle技术栈的学生与开发者。项目围绕文本检测与文本识别两大核心流程提供票据图像预处理、模型加载、结果输出等完整代码框架并配有README说明文档便于理解整体设计思路和运行步骤。压缩包共12个文件包含4个Python脚本app.py、main.py、test.py、reTest.py等、4个工程配置文件xml/iml、1个Git忽略文件、1个Markdown说明文档及1张示例截图整体仅262KB轻量便携。已有37人学习下载。通过这一项目包可获取可直接运行的OCR票据提取演示程序、可复用的测试与验证脚本以及清晰的工程目录结构既能辅助快速搭建实验环境也能为票据关键字段的自动化识别方案提供实践范本。1. 票据信息智能提取PaddleOCR 为什么是这条路上最稳的起点把一堆发票、回单、结算单从图片变成结构化字段这事听起来简单做起来全是细节。基于 PaddleOCR 的票据信息智能提取设计核心就是围绕“文本检测 文字识别 信息结构化”三段式链路把票据里的关键字段抽成可入库的 JSON。经历过 Tesseract 对中文票据的识别率翻车、自己标注训练成本过高之后PaddleOCR 是目前开源阵营里中文票据场景综合成本最低的起点预训练模型覆盖常见印刷体、表格和印章且支持用少量业务数据微调。这篇笔记适合正在做财务票据识别、OCR 质检、报销自动化并且想绕过重复造轮子的开发者。2. 票据提取的方案骨架从图片输入到结构化字段的完整链路2.1 为什么选 PaddleOCR 而不是 Tesseract 或者商用 API先说选型。Tesseract 对英文和清晰印刷体有不错的表现但中文票据存在大量异形字体、压线文字、红章遮盖Tesseract 在没有专门训练的情况下识别准确率会在 60% 到 80% 之间波动且对表格结构的解析几乎为零。商用云 OCR 接口的识别效果好但票据往往涉及客户隐私和财务数据把图片传到外部服务这一点在不少企业里过不了合规审批。PaddleOCR 正好落在两者之间本地部署、中文预训练权重成熟、底层是 DB 文本检测加上 CRNN/SVTR 识别网络发布时就已经覆盖了常见中文票据的字体形态并且 PP-Structure 系列还提供了版面分析和表格还原能力。另一个选型理由是文档生态。PaddleOCR 的模型仓库里从 lightweight 到 high-accuracy 都有检测模型 MobileNetV3 版本的推理时间在 CPU 上可以压到 100ms 以内识别模型 SVTR 在准确率上能对标商用接口。整个方案允许我从“先跑通默认模型”过渡到“用 200 张标注数据微调”不需要更换技术栈。对于票据这类强结构化的文档开源方案加上自己做后处理规则完全可以把字段级 F1 推到 95% 以上而这一步在商用 API 上反而是黑匣子。2.2 完整处理链路图像预处理、文本检测、文字识别、信息结构化一套可落地的票据提取系统输入不只是一张图片而是“图片 票据类型 期望输出的字段清单”。链路拆开有四段。图像预处理负责矫正倾斜、去噪、增强对比度票据扫描件常见的问题包括歪斜超过 5 度、灰度不均、阴影压字如果不预处理检测框会整体偏移。文本检测负责把每一行文字定位成矩形框坐标PaddleOCR 的 DB 算法输出的是带二值化的概率图通过后处理得到文本框。文字识别则是把检测框内的图像裁剪出来转成字符串。信息结构化是把识别出的所有文本结合坐标关系映射到发票号、日期、金额、收款人、税号等业务字段。在这四段里检测和识别可以完全依赖 PaddleOCR 预训练模型但信息结构化必须结合票据知识。比如一张增值税发票“金额合计”字样旁边的数字才是目标值标题区的“发票号码”后面的字符串才是发票号。如果只把所有 OCR 文本平铺输出字段根本无法直接使用。所以我在方案里会在 OCR 后面加一层规则引擎先按关键词定位再按坐标邻域取数最后用正则校验格式。2.3 结构化部分用规则引擎还是用序列标注模型关于信息结构化项目的常见做法是先上规则引擎后考虑模型。原因很直接票据版式相对固定同一个开票软件出来的发票字段位置高度一致规则引擎可解释、可调试、出错了能直接定位是哪个正则没匹配上这在项目验收阶段非常重要。规则引擎做的事包括把 OCR 结果按 y 坐标分行按 x 坐标排序然后检索“发票号码”“开票日期”“价税合计”等锚点再从锚点行或锚点邻域截取目标文本。只有当版式超过 20 种且规则维护成本过高时才值得用序列标注模型比如 BERT 加 CRF 做命名实体识别。成本差别很大规则引擎写一个版式平均半天序列标注模型要标注 500 张以上票据、训练加调参至少一周。我见过不少项目一开始就上实体识别模型最后瓶颈全在标注数据不够和长尾版式上回头又补规则。所以这里的取舍结论是先做规则把规则的置信度和覆盖度量化出来再决定要不要上模型。方案适配版式数量开发成本维护方式适用阶段关键词坐标规则30 种以内每版式半天改 Python 映射表项目初期与中期版式模板匹配50 种以内每版式 1 天模板文件管理版式固定场景序列标注模型不限标注 500 张训练一周数据迭代版式多、规则维护不动时3. 用 PaddleOCR 跑通第一版票据提取最小命令与落地参数3.1 环境准备和一条命令跑起 OCR 推理安装 PaddleOCR 的常规做法是通过 PaddlePaddle 的 Python 包直接走 pip。要注意的一点是 PaddlePaddle 的 CPU 和 GPU 版本是分开的默认安装 CPU 版本如果你的机器有 N 卡建议先换 GPU 版否则批量处理票据的速度会劝退你。# 建议在 Python 3.8~3.10 环境下操作用了虚拟环境更容易隔离依赖 python -m venv ocr_env source ocr_env/bin/activate pip install --upgrade pip # CPU 版 PaddlePaddle pip install paddlepaddle2.6.0 # GPU 版请替换成对应 CUDA 版本的安装命令这里以 CUDA 11.8 为例 # pip install paddlepaddle-gpu2.6.0 -i https://www.paddlepaddle.org.cn/packages/cu118 pip install paddleocr装完后验证一下是否正常。PaddleOCR 的 Python API 设计得很简洁PaddleOCR 类初始化时传入检测和识别模型参数调用 predict 方法直接出结果。第一次运行时它会自动下载检测、识别、方向分类三个模型的权重网络环境差的话建议先手动用 wget 把模型下载到~/.paddleocr/目录免得运行时卡住。from paddleocr import PaddleOCR # det_model_dir 指定检测模型rec_model_dir 指定识别模型 # 如果不传会自动下载默认的轻量级中文模型 ocr PaddleOCR( det_model_dirNone, rec_model_dirNone, use_angle_clsTrue, # 方向分类器票据扫描件有颠倒图时建议打开 langch, det_db_thresh0.3, # DB 二值化阈值调低能找回更多文本框 det_db_box_thresh0.5 # 文本框过滤阈值调低会让框变多 ) result ocr.predict(invoice_sample.jpg) for item in result: res item[rec_texts] boxes item[rec_boxes] for text, box in zip(res, boxes): print(f{box.tolist()} : {text})这段代码里最需要理解的是use_angle_cls和两个det_db参数。票据拍照件经常出现 180 度颠倒方向分类器能做矫正det_db_thresh管的是像素级二值化的敏感度值越小越容易把浅色文字也检测出来但同时会引入更多背景噪声。对扫描质量差的票据我一般先把det_db_box_thresh从 0.5 往下降到 0.3再看检测框是否出现大面积粘连。3.2 从 OCR 结果到结构化输出坐标分组与字段提取OCR 出来的只是一堆文本和坐标要变成可入库的字段还得做一层后处理。票据版式的天然优势是文字有对齐关系同一行的文本 y 坐标基本一致同字段的标签和值通常在同一行或紧邻行。我的做法是先按 y 坐标把文本分出行再按 x 坐标排序然后基于锚点词找值。import re from collections import defaultdict def group_lines(items, y_tolerance10): items: 由 (text, box) 组成box 是 [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] 返回: 按 top-left y 坐标聚类的行列表每行按 x 排序 groups [] used [False] * len(items) for i, (_, box) in enumerate(items): if used[i]: continue y_center (box[0][1] box[2][1]) / 2 line [i] used[i] True for j, (_, box2) in enumerate(items): if used[j]: continue y_center2 (box2[0][1] box2[2][1]) / 2 if abs(y_center - y_center2) y_tolerance: line.append(j) used[j] True line.sort(keylambda idx: items[idx][1][0][0]) groups.append([items[idx] for idx in line]) return groups def extract_field(lines, anchor, field_pattern, offset0): 在行列表中定位锚点返回匹配字段模式的内容 for line in lines: texts [item[0] for item in line] joined .join(texts) if anchor in joined: for text in texts: if re.search(field_pattern, text): return re.search(field_pattern, text).group(0) return None这里有一个很重要的参数y_tolerance。票据扫描件经过透视矫正后同行的 y 坐标误差通常在 5 个像素以内如果图片分辨率是 300 DPI可以放宽到 10。y_tolerance设置太大会把相邻两行合并设置太小同一行会断成多个碎片。我通常的做法是先打印一行文本的 y 中心坐标分布取间隙明显小于行内差的值为默认值。字段提取时的offset参数是给跨行场景准备的比如“价税合计”和金额之间隔了一个字符需要对锚点词所在行做截取而不是全行匹配。3.3 识别精度上不去的排查顺序先检测后识别别盲目换模型第一版跑完你会发现有些字段提取不出来不一定是模型问题而是检测阶段就把文字漏了。排查顺序我的习惯是固定的。第一步看检测框把ocr.predict结果的文本框画到原图上如果漏了目标文字调整det_db_box_thresh或直接用中文检测模型里更重的服务器版第二步看识别文本如果框在但文字识别错了比如“0”识别成“O”、“伍”识别成“五”那要检查识别模型和字典或者走下一步微调第三步才考虑后处理也就是说正则写错或者锚点词在票据上不叫这个名字。这一步最容易踩的坑是把所有问题都归到“模型不够准”然后马上开始训练自己数据。实际上扫描件的倾斜和光照问题用预处理就能解决一大半。如果图片倾斜超过 10 度DB 检测框会产生大量断裂我的做法是先用 OpenCV 的minAreaRect检测票据外边框做透视矫正再做 OCR简单粗暴但非常有效。识别置信度低的区域可以做多尺度推理即对图像做 1.5 倍放大后再识别一次票面小字效果明显。4. 训练自己的票据模型数据标注、配置文件和调参路径4.1 数据准备标注工具和训练集格式PaddleOCR 支持检测和识别两个模型分别训练所以要准备两种标注。检测模型需要的标注是每张图上所有文本行的四边形框用 PPOCRLabel 工具标注最顺它可以直接加载 PaddleOCR 预训练模型做预标注人工只需要修正位置和转写文字。识别模型需要的标注则是“整行文本图片 字符串”PPOCRLabel 在标注完检测框后可以直接导出识别训练集。票据数据的收集常见来源是历史业务单据扫描件。这里要提醒一点票据数据的隐私属性很强用真实客户数据训练前要确认合规边界。如果拿不到足够多的真实票可以用开票软件生成样票或者把样本做透视变换、加噪声、模拟盖章遮挡来扩充。对于起步阶段我的经验是检测模型有 300 张标注图就能把版面检测做得比较稳识别模型则需要按字符覆盖度来算至少要保证票据里的常见字体、数字、中文大写金额都有样本。标注导出后的目录结构建议保持与 PaddleOCR 官方数据格式一致这样脚本不用改train_data/ det/ images/ # 检测训练原图 label.txt # 每行: 图片路径 标签JSON rec/ images/ # 识别训练裁剪图 label.txt # 每行: 图片路径 标签文本检测的label.txt里每一行是图片路径加一个 JSON 数组数组里每个元素是四点坐标加文本内容。识别则简单很多英文版的默认格式用制表符分开路径和标签中文标签直接写 UTF-8 字符串。4.2 微调检测模型配置文件里的关键参数PaddleOCR 训练使用 yaml 配置管理。官方仓库的configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml可以作为起点。训练前需要改的配置项有全局的num_classes、训练数据路径、batch_size、learning_rate和pretrained_model。检测模型的num_classes是 1因为只需要区分前景文字和背景不需要改类别数。Global: pretrained_model: ./pretrain_models/ch_PP-OCRv3_det_distill_train.tar save_model_dir: ./output/det_finetune/ save_epoch_step: 10 eval_batch_step: [0, 500] use_amp: True Architecture: model_type: det algorithm: DB Transform: null Backbone: name: MobileNetV3 scale: 0.5 model_name: large Neck: name: DBFPN out_channels: 96 Head: name: DBHead k: 50 Loss: name: DBLoss balance_loss: True main_loss_type: DiceLoss Optimizer: name: Adam beta1: 0.9 beta2: 0.999 lr: name: Cosine learning_rate: 0.001 warmup_epoch: 2 regularizer: name: L2 factor: 0 Train: dataset: name: SimpleDataSet data_dir: ./train_data/det label_file_list: - ./train_data/det/label.txt transforms: - DecodeImage - DetLabelEncode - IaaAugment - EastRandomCropData - MakeBorderMap - MakeShrinkMap - NormalizeImage - ToCHWImage loader: batch_size_per_card: 8 num_workers: 4这段配置里隐藏的调参关键点是IaaAugment和MakeShrinkMap的shrink_ratio。IaaAugment负责随机旋转、亮度、噪声扰动票据扫描件的训练集必须开着否则泛化到实际扫描仪输出时性能会掉。shrink_ratio决定文本实例收缩比例默认 0.4 适合普通文本票据里文字间距密、笔画细我会把它改到 0.3让收缩后的标签更容易被网络拟合。训练命令走官方脚本注意把配置文件路径换成你自己的# 单卡训练config 是改好的 yaml 路径 python tools/train.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml # 训练 20 个 epoch 想先做评估用 evaluate 脚本 python tools/eval.py -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml \ -o Global.checkpoints./output/det_finetune/best_accuracy训练完成后模型保存在save_model_dir下评估脚本会输出precision、recall和hmean三个指标。做检测模型微调时我的目标是把recall拉上去因为票据提取漏检的代价远大于误检如果recall已经高于 95%再往上提很容易造成检测框碎片化此时更值得关注precision和hmean的均衡。4.3 微调识别模型字典和序列解码是成败关键识别模型和检测模型的训练机制完全不同。PaddleOCR 的识别模型是 CTC 解码输出的是“每个时间步对应字典里的哪个字符”所以字典文件里的字符顺序直接影响训练效果。票据场景有大量中文大写数字“壹贰叁肆伍陆柒捌玖拾”、货币符号“”和单位“圆”如果默认字典没有这些字符识别结果会强制映射成别的字或变成空。因此训练前先把自己数据里的所有字符收集出来合并到字典里再去训练。# 收集训练标签里的全部字符生成自定义字典 python tools/gen_dict.py --label_path ./train_data/rec/label.txt \ --save_path ./train_data/rec/dict.txt生成字典后修改识别模型的配置文件重点在character_dict_path和num_classes两个参数。num_classes会自动从字典长度计算但仍然建议检查一遍数值不对会导致模型加载报错。识别模型的输入图片高度一般固定为 48宽度动态变化max_text_length要留意票据行长度增值税发票的“货物或应税劳务名称”这一行可能超过 30 个字符把max_text_length设到 40 比较稳。Global: pretrained_model: ./pretrain_models/ch_PP-OCRv3_rec_train.tar save_model_dir: ./output/rec_finetune/ character_dict_path: ./train_data/rec/dict.txt Architecture: model_type: rec algorithm: SVTR Transform: name: TPS num_fiducial: 20 Backbone: name: MobileNetV1 scale: 0.5 Head: name: MultiHead head_list: - CTCHead - NRTRHeadalgorithm是 SVTR 时Transform里的TPS模块是薄板样条变换能对弯曲文本做矫正。票据里虽然很少有弯曲文本但印章压字导致的局部形变很常见建议保留。CTCHead和NRTRHead的 MultiHead 设计是 PP-OCRv3 的官方结构CTC 主头保证传统解码路径可用NRTR 辅助头在训练结束后可以去掉只保留 CTC 分支做推理。训练命令与检测相同区别在-c指定的配置文件。数据量少于 1000 张时learning_rate建议从 0.0005 起步因为预训练权重已经很强了学习率太大会破坏原有特征。warmup_epoch必须大于 0否则前几个 epoch 的 loss 容易飞掉。4.4 调参优先级数据、字典、图片尺寸、batch size、学习率调参要有顺序不然翻车了都不知道是哪一步的问题。我的优先级排序是固定的。数据层面的优先级最高先去查训练集里类别分布是否覆盖了目标场景比如有没有手写金额样本、有没有红色印章底的发票其次是字典很多中文识别训练效果差的根因是字典缺字再次是图片尺寸识别模型的rec_image_height对 48 默认值如果票据小字多、行高小裁剪出来的图会低于这个高度导致 padding 填充过多识别性能骤降这时候我会把高度降到 32配合放大预处理。batch size 的调整逻辑是显存够就把 batch size 开大比如从 8 开到 32能明显提升训练稳定性显存不够就先降分辨率而不是升 batch size。学习率的调整最靠后模型结构预训练权重决定了初始能力微调阶段通常只需要降低学习率就好不需要调学习率策略。用我这套顺序去排查90% 的训练问题都能落在数据和字典这两个层面而不是模型结构。调整对象默认值票据场景建议调整依据det_db_box_thresh0.50.3~0.5低阈值找回浅色字高阈值过滤噪声rec_image_height4832~48小字票据用 32需配合放大预处理max_text_length2540~50长行文本如货物名称栏batch_size_per_card88~32显存允许时优先加大learning_rate0.0010.0005~0.001小数据集取下限防破坏预训练权重5. 票据提取落地的 5 个常见坑现象、原因和解决办法5.1 印章遮挡导致关键字段识别成乱码现象是发票号码或金额区域被红色印章压住OCR 输出一串乱码或者漏识别。根因是印章的红色通道亮度低文字灰度特征被淹没。我的解决方法是先做颜色通道分离检测红色印章区域再用形态学操作把红色区域填充为背景色然后再送 OCR。具体做法是用 OpenCV 的inRange提取红色掩膜膨胀后用inpaint填充这样印章底下的文字轮廓能够被部分恢复。注意不要对整张票做颜色过滤只对印章所在的局部区域处理否则票面上的红色文字也会被误伤。5.2 表格线干扰导致文本检测框割裂票据里的表格线是检测模型的“天敌”。DB 算法会把表格线也判定为文本区域导致一行金额被横向切割成多个碎片或者线和文字粘连成一个框。现象是同一行文本检测出 3 到 5 个框后处理分组时行聚合失效。我常用的做法是把检测结果按高度和宽度过滤窄长条且面积小于阈值的框直接丢弃更有效的办法是在预处理阶段用形态学检测水平线和垂直线把它们从图片上抹掉再送检测表格内的文字检测会干净很多。这两种方法同时用效果最稳。5.3 不同票据版式混批规则引擎频繁误匹配一张增值税专用发票、一张银行回单、一张费用报销单同时进来规则引擎的全文字匹配经常把“日期”锚点匹配到别的字段上去。解决方法是引入票据分类前置步骤。先用 PP-Structure 的版面分析或者简单图像特征判断票据类型再按类型加载不同的规则模板。特征可以用模板匹配、标题区 OCR 文本、或者版面块位置分布来判断。在规则引擎里不要做全局匹配每个模板只匹配自己版式内允许出现的锚点词且限制锚点的坐标范围。这样误匹配率能从 30% 降到 5% 以下。5.4 模板匹配失效同一个字段在票面位置有偏移票据扫描时纸张位置不可能每次都完全一致哪怕用模板匹配定位也会有几像素到几十像素的偏移。现象是锚点词能找到但按预设坐标偏移去取值时取到了空框或者相邻字段。原因很简单模板匹配是刚性对齐票据的实际偏移是线性的但不恒定。我后来把取值逻辑从“固定坐标偏移”改成“锚点词相对坐标 语义约束”也就是先找到“价税合计大写”这几个字的位置再向右扫描下一个文本块作为候选金额同时用“金额必须包含圆/整或数字小数点”做格式校验。这个改动让模板失效的情况大幅减少。5.5 长票据图片裁剪后字段丢失有些结算单是 A3 或者流水打印的长条纸图片宽度大、字段跨区域分布。直接整图识别会出现检测框丢失、识别内存不足的问题。常见做法是把长图按高度切成多段每段重叠 20 到 30 像素分别识别后再合并坐标。合并时需要注意坐标偏移补偿否则后处理分行的 y 坐标会断层。我的做法是在切图时记录像素偏移并把识别出的框坐标加回偏移值再做行聚合。重叠区域出现重复文本时用置信度较高的结果覆盖另一个这样长图场景下字段召回能回到正常水平。6. 一套可用的验证方法用字段级 F1 盯住业务目标而不是整体 OCR 精度最后这一步是把方案从“能跑”推向“可验收”。我看到很多项目汇报时只报 OCR 字符准确率结果业务方拿真实票据一测字段级提取效果根本不到位。正确的验证方法是单独构造一张字段级评估表每张票据标注出目标字段的期望值然后跑完整个提取链路后逐字段比对。import re import json from difflib import SequenceMatcher def evaluate_fields(predictions, ground_truth): predictions: [{field_name, value}] ground_truth: {field_name: expected_value} 返回字段级准确率与相似度方便定位哪类字段最容易翻车 results {} for field, expected in ground_truth.items(): pred next((p[value] for p in predictions if p[field_name] field), None) exact 1 if pred expected else 0 similarity SequenceMatcher(None, pred, expected).ratio() if pred else 0.0 results[field] { exact_match: exact, similarity: round(similarity, 4) } return results评估集不需要很大每个版式准备 30 到 50 张票据就行但必须覆盖印章遮挡、褶皱、低分辨率三类典型场景。跑完这个脚本你会直观看到哪类字段是短板日期容易因格式不一致判错金额容易因千分位符号丢失判错发票号容易因字母 O 和数字 0 混淆判错。针对这些失败样本再去调预处理和后处理的正则而不是盲目重新训练模型。还有一个值得养成的习惯是给每个字段设置置信度阈值。PaddleOCR 的识别结果里有rec_scores字段低于 0.7 的文本默认不采纳让系统输出“该字段未提取成功”而不是强行给一个错误值。这个设计在财务对账场景极其重要一个错误的发票号比没有发票号危害大得多。最终这套流程能让你在真实数据上稳定跑到字段级准确率 95% 以上剩下的就见招拆招靠数据迭代慢慢磨。这也是我做完几个票据项目后最想分享的经验别在最开始就追求大而全的模型先把链路跑通、把评估立起来再逐步优化。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进