
简介发票信息识别图像数据集是一份面向OCR与深度学习信息抽取的中小型高质量数据资源主要供算法工程师、科研人员及学习者用于构建和验证发票关键字段识别模型。数据集共1560个文件包含1040个XML标注文件与520张TIFF发票图像压缩包体积仅34.06MB轻量易用。XML文件与对应的发票扫描图像同名逐一映射内部记录了从原始票面中提取出的结构化数据包括发票号码、开票日期、开票方与收票方公司名称、公司电话号码、地址等核心实体。这样的组织方式便于开发者直接进行数据集的载入、拆分与对齐可用于信息抽取模型的训练、验证和测评也可作为OCR版面解析、财务票据自动化处理等实际项目的基准数据。数据量适中既适合快速迭代实验又覆盖多种真实票面版式对于希望从事财务票据识别或流程自动化的初学者能够帮助快速理解标注结构与关键字段定义。目前已有257人学习下载是一份实用价值明确的发票识别数据集。1. 发票信息识别图像数据集为什么你的OCR模型总在报销单上翻车发票信息识别是OCR落地最频繁、也最容易被低估的场景一张增值税普票、机动车销售发票或电子行程单送到模型面前要的不是“把字认出来”而是精准输出发票号码、开票日期、购买方名称、税额、价税合计等结构化字段。很多团队把公开数据集跑通就以为完成了可真到报销系统里一上线识别率立刻掉到让人怀疑人生的水平——问题往往不在模型结构而在图像数据集本身票种单一、版式固定、拍摄角度太正、光照太均匀。这篇笔记不讲大道理直接说清楚一套能支撑发票信息识别落地的图像数据集该怎么搭、怎么标、怎么切、怎么验以及每一个环节里最常见的坑。2. 数据集里到底该装什么把发票图像拆成四层信息2.1 发票版式分类固定版式与开放版式的数据集取舍发票信息识别和通用OCR最大的差异在于版式。增值税专用发票和普通发票的版面高度规范票面元素位置几乎是固定的而卷式发票、出租车票、银行回单、行程单这类开放版式则没有统一模板。数据集设计的第一步就要想清楚你的系统要面对哪一种。如果只做增值税发票识别图像数据集的标注量可以小很多因为检测模型只需要定位固定区域甚至可以用基于模板的关键点回归方案。但如果你面对的是财务报销场景里混着一堆票据的情况数据集就必须覆盖多票种而且每个票种都要保留足够的样本量。我见过不少团队用纯增值税发票训练上线后被一张出租车票直接击穿——不是模型差是数据集压根没覆盖这种分布。数据集覆盖面决定系统边界这是发票信息识别和自然场景OCR最不同的地方。2.2 字段级标注从“识别准”到“字段对”的关键跨越通用OCR数据集的标注是四边形的文本行框加一行字符串够用但发票信息识别数据集如果只标到文本行下游结构化会非常痛苦。因为发票字段是“键值对”模型最终要输出的是某个语义字段的文字内容而不是一堆散落的文本行。比较稳妥的做法是标三层文本行级每个文本区域一个多边形框字符串字段语义级每个框对应哪个字段名如“发票号码”“开票日期”“价税合计”图像级票种分类标签如“增值税专用发票”“出租车票”“行程单”以 JSON 为例一个完整的标注样例长这样{ image_name: invoice_0001.jpg, category: vat_special, fields: [ {name: invoice_code, text: 031002100411, points: [[156,323],[428,323],[428,348],[156,348]]}, {name: invoice_number, text: 12345678, points: [[156,368],[428,368],[428,393],[156,393]]}, {name: date, text: 2024年06月18日, points: [[156,523],[320,523],[320,548],[156,548]]} ] }这里的points是四个角的像素坐标顺序为左上、右上、右下、左下category是票种分类fields是字段级标签。训练时检测模型用points学定位识别模型用text学文字结构化模型用name学语义映射——三层信息各司其职缺一层后面都补不回来。2.3 硬件与规模基线一万张还是十万张才够训练关于数据量有一条我反复验证过的经验线单一固定版式发票字段级标注达到5000张以上检测识别的准确率就能到可用的边缘要稳定到生产级2万3万张往上走。多票种混合的场景单票种至少3000张否则模型会倾向于把所有票都认成样本量最大的那类。3. 数据从哪来自建标注、合成数据与开源数据集的组合路径3.1 自建标注的流水线采集、清洗、标注工具选型自己采集发票是最可控但最重的路径。采集阶段要特别注意几点真实报销场景里发票有褶皱、有折叠阴影、有订书钉孔、有书写笔迹覆盖这些必须作为正样本收进来不能觉得“脏”就删掉。收进来的图要按“单张发票”切好避免一张照片里出现多张发票——否则检测模型会学出幻觉。标注工具我用过 Labelme 和 PPOCRLabel实际推荐 PPOCRLabel因为它是为OCR设计的支持四点框标注、文本框旋转、自动建议框标注效率高很多。自建标注的核心不是工具而是标注规范文档框必须紧贴文字不能留白超过2像素如果有盖章遮挡导致文字不完整照常框出完整文字区域并原样标注文字内容一票多联发票联、抵扣联算不同样本分别标注凡是票面出现但肉眼都看不懂的字做“忽略区域”标掉不要硬标这份规范写清楚后标注质量会稳定很多。否则每个人按自己理解标模型训出来四处漏风。3.2 合成数据怎么做让训练集覆盖现实的脏乱差真实样本不够合成数据是最实用的补充手段。常用的做法是“票据模板真实文本内容图像退化”三板斧把真实发票的版式抽成模板替换文本内容再做几何变换和噪声扰动。这个思路能批量生成几万张看起来像真的、标注完全准确的样本。下面是一个最小可跑的合成脚本示例import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont def synth_invoice(template_path, texts_dict, font_path, out_path): template_path: 干净的发票模板图 texts_dict: {invoice_code: 031002100411, ...} font_path: 字体文件路径 img cv2.imread(template_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) pil_img Image.fromarray(img) draw ImageDraw.Draw(pil_img) for field_name, text in texts_dict.items(): # 每个字段在模板上的位置预设好 pos FIELD_POS[field_name] # (x, y) font ImageFont.truetype(font_path, FONT_SIZE[field_name]) draw.text(pos, text, fill(0, 0, 0), fontfont) # 图像退化透视变换 高斯噪声 光照不均 rows, cols pil_img.size pts1 np.float32([[0,0],[cols,0],[0,rows],[cols,rows]]) pts2 np.float32([[np.random.randint(0,10), np.random.randint(0,10)], [cols-np.random.randint(0,10), np.random.randint(0,10)], [np.random.randint(0,10), rows-np.random.randint(0,10)], [cols-np.random.randint(0,10), rows-np.random.randint(0,10)]]) M cv2.getPerspectiveTransform(pts1, pts2) warped cv2.warpPerspective(np.array(pil_img), M, (cols, rows)) noise np.random.normal(0, 5, warped.shape).astype(np.uint8) out cv2.add(warped, noise) cv2.imwrite(out_path, out)参数说明pts2里每个角的偏移量控制透视强度一般控制在010像素太大生成的图像会明显失真模型学到的是“畸形发票”而不是“有点歪的发票”noise的均值5是高斯噪声标准差太大会淹没文字笔画。字段位置FIELD_POS需要从真实标注里统计均值得到——这一步很像做“叶片病害图像数据集划分”时先按病害类型聚类总之先摸清数据分布再做样本生成不要凭空定位置。3.3 先用开源数据跑通流程再决定是否自建建立发票信息识别图像数据集时优先建议先拿公开可用的票据OCR数据集做流程验证。虽然公开数据的票种和你的场景不一定完全匹配但足以验证模型选型、训练流程、评测指标是否跑得通。等流程顺畅了再投入人力做真实场景数据采集和标注效率会高很多。4. 数据集划分的艺术不能随机切得按“票据来源”切4.1 为什么随机划分会在发票场景泄漏数据常规的图像数据集划分是随机按比例切出 train/val/test比如7:2:1。这个做法在自然场景分类里没问题但在发票场景会直接翻车。原因在于发票样本往往来自同一个开票方版式几乎完全一致只是开票日期和金额不同。如果同一来源的200张发票里180张进了训练集、20张进了验证集模型看到的验证集和训练集本质上高度重复验证分数会虚高。真实上线后模型面对的是从未见过的开票方版式——之前随机切分练出来的模型实际表现会远低于验证分数。这个误差不是小数目。做作物病害图像数据集划分时大家强调“按叶片来源分组再划分”发票数据集同理必须按开票方/采集批次分组不能让同一来源的样本同时出现在训练集和验证集里。4.2 按来源分组的划分配置一行代码保住验证集的可信度按来源分组进行数据集划分的做法代码实现并不复杂import pandas as pd from sklearn.model_selection import GroupShuffleSplit # df 需要包含列: image_name, source_id(开票方或批次), split df pd.read_csv(dataset_meta.csv) gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, groupsdf[source_id])) df.loc[train_idx, split] train df.loc[val_idx, split] val # 按 source_id 统计切分结果确认没有泄漏 print(df.groupby([split, source_id]).size())参数说明GroupShuffleSplit的groups参数传的是每个样本的来源ID切分时整个来源组会被完整划到同一侧不会拆散test_size0.2指验证集占比20%random_state42固定随机种子保证每次实验切分一致。最后一步打印分组统计是为了肉眼确认每个source_id只出现在一个 split 里这是发票数据划分最关键的验证动作。4.3 划分比例怎么调小样本场景下验证集不能将就发票场景里我实际用的比例通常是训练集70%、验证集10%、测试集20%。测试集单独留出来等所有模型实验都做完才动它——这就是“后悔药”。验证集用来反复调参测试集只用来做最终评估防止调参过程中过拟合到验证集上。如果你的总样本量只有几千张验证集比例调到15%左右也可以但绝对不要低于10%否则字段级准确率的波动会大到无法判断模型改动是好是坏。5. 数据标注和训练避坑3个让我返工到崩溃的真实问题5.1 多联发票被当成同一张票现象模型对同一张发票的发票联和抵扣联识别结果不一致两个联的号码都对不上。 原因采集时把同一张发票的两个联拍成两张照片但标注时没有区分联别数据清洗时也不知道它们来自同一张原始发票导致模型把两个联的微小差异当成本质差异去学。 解决在元数据里为每个样本增加invoice_parent_id字段同一张发票的不同联共享同一个父ID。划分数据集时按invoice_parent_id而不是source_id分组确保同一张发票的多个联不会跨训练集和验证集出现。这个教训是我在第二版数据集上踩的改完之后验证集的可信度才真正立住。现象模型在验证集上字段识别率高达99%上线后全乱了。 原因标注的文本里带了隐形字符比如发票号码里的全角空格、日期里的中文年月日与大写数字混排。模型在训练时“背”住了这些字符分布换到真实场景就露馅。 解决在标注后处理阶段加一步字符级清洗import re def clean_invoice_text(raw_text): # 统一全角半角、去空格、去零宽字符 text raw_text.replace(\u3000, ).replace(\ufeff, ) text re.sub(r[\x00-\x1f\x7f], , text) # 发票号码/代码只保留数字和字母 if re.fullmatch(r[0-9A-Za-z]{8,20}, text): text text.upper() return text for rec in annotations: rec[text] clean_invoice_text(rec[text])参数说明\u3000是全角空格\ufeff是零宽不换行字符这两个是发票OCR标注里最常见的隐形字符长度820的纯字母数字串会统一转大写因为发票号码本身不区分大小写。跑完这步再看验证分数掉了2个百分点但上线后的真实表现反而稳定了——之前的高分是“作弊”出来的。现象模型把发票上的两个相近文本框合并成一个字段错位。 原因标注框之间距离太近检测模型的后处理NMS参数太宽松把两个相邻框合并了。 解决调低NMS的IoU阈值检不出来的框宁可漏不要错# 以 PaddleOCR 的检测阈值为例 det_db_thresh0.3 # 检测得分阈值漏检时调低 det_db_box_thresh0.5 # 框阈值框太多时调高 det_db_unclip_ratio1.8 # 框扩展系数框太小文字被截断时调大这三个参数的调法是我最常用的组合注意det_db_unclip_ratio增大后相邻框更容易粘连要配合det_db_box_thresh一起调。每次只动一个参数观察验证集上的字段级准确率变化再决定下一步。6. 验证集上怎么看效果字段级评估才是发票识别的照妖镜发票信息识别模型在验证集上的评估不能只看整张图“识别对没对”要看字段级指标。我给项目组定过一个简单可复用的评估脚本先按字段类别计算精确率和召回率再做汇总。def evaluate_field(pred_dict, gt_dict, field_name): pred_dict: {image_name: {field_name: text}} gt_dict: 与 pred_dict 同构 tp fp fn 0 for img_name in gt_dict: gt_text gt_dict[img_name].get(field_name, ) pred_text pred_dict.get(img_name, {}).get(field_name, ) if gt_text : if pred_text ! : fp 1 else: if pred_text gt_text: tp 1 elif pred_text : fn 1 else: fp 1 # 识别了但内容错误算假阳性 precision tp / (tp fp) if (tp fp) 0 else 0 recall tp / (tp fn) if (tp fn) 0 else 0 f1 2 * precision * recall / (precision recall) if (precision recall) 0 else 0 return {precision: precision, recall: recall, f1: f1}代码里的关键判断是fp 1那个分支识别出了内容但和真值不一致这是发票场景里最隐蔽的错误——系统看起来很流畅结果全是错的。所以评估时一定要分开统计“没识别出来”和“识别错了”两种失败模式。我在第一版模型上就被发票金额这个字段坑过一次当时只看整体准确率是97%拆到字段级发现金额字段的F1只有81%原因就是大写的“壹贰叁”识别不稳。训练过程中我的固定动作是每轮保存模型后在验证集上跑字段级评估看每个字段的F1曲线。如果某个字段两轮训练不涨就先停下来去查这个字段的标注质量——多数情况下是标注框偏移导致文字被截断。这个习惯帮我少走了很多弯路。真实项目里字段级F1稳定在95%以上、单张推理耗时在300毫秒以内基本上可以进入小范围试点。希望这套方法对你有用也祝你少踩我踩过的坑。本文还有配套的精品资源点击获取