ARTICLE DETAIL

资讯详情

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

发票表格检测数据集处理与YOLOv8训练实战

发票表格检测数据集处理与YOLOv8训练实战 简介面向发票文档自动化处理的一套YOLO格式目标检测数据集专注于表格区域识别。数据集包含909张真实发票图片其中训练集795张、验证集76张、测试集38张类别为单一“表格”覆盖多样化的表格版式。适用于财务票据自动识别、文档结构化提取、办公流程自动化以及计算机视觉教学实训等场景。包体共1820个文件含909个jpg图片和909个txt标注文件另附1个yaml配置文件和1份docx说明文档压缩包大小36.29MB。yaml文件便于模型配置docx文档介绍数据集结构与使用细节。标注采用YOLO格式以中心坐标和宽高描述边界框边界框定位准确确保模型训练可靠性。可直接用于YOLOv5、YOLOv8、YOLOv12等模型训练与微调数据已按训练、验证、测试划分便于模型评估与迭代。特别适合处理复杂文档布局下的表格定位任务。目前已有305人学习适合用于文档结构识别算法研究、财务系统集成以及目标检测教学。1. 发票表格检测数据集.zip解压只是起点标注粒度决定复用价值做财务票据 OCR 或 RPA 自动报销的团队拿到一份“发票表格检测数据集.zip”时最常见的动作是解压、看图、直接丢给 YOLO 训练。我建议换一下顺序先别急着解压把压缩结构、标注格式和类别清单摸清楚。发票表格检测要解决的不是“找出一张发票”而是把整票框、表格区域、单元格、字段级元素发票代码、号码、金额、税额、日期同时定位出来供下游 OCR 做结构化。这类数据集与通用目标检测数据集的差异非常明显目标小、密集、长宽比极端不同发票版式差异还大。直接照搬 COCO 流程训练大概率会在验证集上翻车。这篇按我处理票据数据集的习惯从勘察 zip 讲到训练、踩坑和上线验证适合算法工程师、RPA 团队和文档版面分析研究者。2. 先勘察再训练目录结构、标注格式与发票版式差异拿到 zip 后我一般会把它当成一个待拆解的黑匣子先看里面有什么、标签可信度如何再决定后续路线。很多数据集交付时带着多层目录、空标注、中文文件名、混合格式老老实实先做勘察能省掉后面几天排错时间。2.1 解压前的完整性检查与目录预览动解压命令之前先花十秒钟做完整性测试。数据集从同事或网盘传过来zip 在传输中被截断并不少见截断后解压到一半才报错容易造成“文件缺失”的错觉。cd ~/datasets unzip -t invoice_table_detection.zip-t是 test 模式只检查各文件的 CRC 与目录结构不输出实际文件。看到No errors detected in compressed data再继续。如果报了invalid compressed data to inflate优先重新下载而不是尝试修复修复出来的标签缺文件后面更难排查。测试通过后用列表模式看顶层结构避免直接解压出一堆无层级散文件unzip -l invoice_table_detection.zip | head -50通过unzip -l可以快速判断顶层是单个目录还是散文件文件名是英文还是中文图片与标注是否在同一层或分属images/与annotations/。先把路径规则记住后面解析脚本里要用。2.2 常见目录结构与标签格式识别发票表格检测数据集的目录结构常见的组织方式大概是这样invoice_table_detection/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ ├── 0002.jpg │ │ └── ... │ ├── val/ │ └── test/ ├── annotations/ │ ├── train/ │ │ ├── 0001.xml │ │ ├── 0002.xml │ │ └── ... │ ├── val/ └── label_map.txtimages/放扫描图或拍照图annotations/放 VOC 风格的 XML 或 COCO 风格的 JSON也可能直接在labels/下放 YOLO 格式的 txt。文件命名通常是对应关系0001.jpg对应0001.xml或0001.txt。第一件事是对齐图片与标注数量。用 find 命令分别统计find images -type f \( -name *.jpg -o -name *.png -o -name *.jpeg \) | wc -l find annotations -type f -name *.xml | wc -l两数不一致很常见。图片多于标注说明部分图片没有标签标注多于图片通常来自重复导出或中间版本。无论哪种情况都别直接补或删而是先把“没有对应关系的文件”列出来再决定是否过滤for f in $(find images -type f -name *.jpg); do base$(basename $f .jpg) [ ! -f annotations/$base.xml ] echo $f done | head -20这个循环逐个检查图片是否有同名 XML打印缺失项。注意不同 zip 的命名后缀不同.jpg/.jpeg/.png都可能跑之前先看几张图片的真实后缀别上来就套。还要看标注类别是否统一。用一条命令统计 XML 里所有name的频次grep -rh name annotations/ | sed s/name//;s/\/name// | sort | uniq -c | sort -rn这条命令对annotations/下所有 XML 做无递归匹配提取类别名并计数。结果里如果出现invoice、table、invoice_code、amount等名称后面类别映射就按这些写如果出现Table与TABLE并存、中文类别名与英文类别名混用那就必须先做统一。我在实际数据里见过同一种字段被标成amount和金额两个类训练时模型被强制当成两个不同目标效果自然好不了。标签格式也需要抽查。VOC XML 的bndbox存像素绝对坐标COCO JSON 的bbox存[x, y, width, height]YOLO txt 存归一化中心点。如果数据集本身就带 txt 标签抽一个看内容head -5 labels/0001.txt类似0 0.582215 0.721960 0.052088 0.018381的输出第一列是类别索引值后面四列是归一化的x_center, y_center, width, height。如果碰到polygon或segmentation字段说明该数据集更适合实例分割任务而不是单纯目标检测。2.3 发票版式差异决定数据质量下限发票表格检测数据集最微妙的一点是版式差异对模型泛化能力的直接影响。增值税专用发票一般是 A4 比例、表格线细且密集出租车发票窄而长纸张位置经常歪斜电子发票有 PDF 渲染成图的边缘出血与字体渲染差异手写发票还有印章压线、手写字符叠在表格线上的情况。在勘察阶段建议随机抽 30 到 50 张图用 Python 或图像查看工具快速看一遍记录三类信息图像尺寸分布、是否带旋转、是否存在印章或褶皱。发票数据集中不同宽度比例混在一起时YOLO 训练默认的letterbox会把长条形的出租车发票补黑边到正方形字段位置发生形变。这种情况下要么统一 crop 到表格区域再训要么按宽高比分组训多个模型。图像尺寸分布可以用一条 Python 命令统计from PIL import Image import glob for path in glob.glob(images/**/*.jpg, recursiveTrue): try: w, h Image.open(path).size print(f{path}: {w}x{h}) except Exception as e: print(f损坏: {path} {e})代码逻辑很简单遍历所有 jpg读取真实像素宽高单张图读取失败说明文件损坏可能是 zip 解压不完整或原图本身坏。在实际项目里这一步能提前暴露“大部分图片是 1000x1500少数几张是 4000x3000”这类极端不均衡问题。如果出现建议把超大图按比例缩到与主流尺寸接近而不是让 YOLO 的 stride 去硬扛。发票表格检测数据集的常见标注粒度有三层整票框、表格区域、字段级。整票框标注适合做发票分类和图像矫正但解决不了字段提取表格区域标注适合做版面分析和表格还原字段级标注才适合做结构化 OCR。如果 zip 里的标注以table为主下游却要提取金额字段就需要做一轮字段级人工标注。这是一条前置判断决定了整个项目的时间成本。3. 把标注统一成 YOLO 格式VOC XML 转 txt 的脚本和四个边界坑如果你决定用 YOLO 系列跑发票检测第一步是把五花八门的标注统一成 txt。多数公开票据数据集交付的是 VOC XML 或 COCO JSON而 YOLO 的标签是每张图一个 txt。直接手工转是不可接受的脚本必须一次处理整个目录。3.1 先建立类别映射表转换前先确定类别顺序顺序即模型的输出顺序。建议从数据的自然语义排序发票整体、表格区、字段元素。我这里写一个通用但清晰的类别映射class_map { invoice: 0, table: 1, invoice_code: 2, invoice_number: 3, amount: 4, tax: 5, date: 6, seller: 7, buyer: 8, }class_map里键名必须与 XML 中的name文本完全一致。如果统计结果里出现空格或大小写差异比如Invoice建议在转换脚本里先做一次字符串归一化统一转小写再去掉首尾空格避免 key 查不到而丢标签。3.2 VOC XML 转 YOLO txt 的核心函数下面这段是我常用的转换函数它只做一类事情读取单个 XML输出与该 XML 同名的 txt 文件。import xml.etree.ElementTree as ET def voc2yolo(xml_path, out_txt, class_map, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip().lower() if name not in class_map: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) xmin max(0, min(xmin, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) xmax max(0, min(xmax, img_w - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: continue x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines))逻辑说明XML 里xmin/ymin/xmax/ymax是像素坐标YOLO txt 要求归一化的中心点与宽高所以先求中心点再分别除以img_w与img_h。注意两点坐标先裁剪到图像合法范围避免因为标注手误出现负数或超宽高宽高小于等于 0 的标注直接跳过避免训练时损失函数算到无效框。参数说明img_w和img_h必须来自图片真实尺寸不要使用 XML 里size标签给出的宽高。很多标注工具写的是缩略图或屏幕显示尺寸不是原图尺寸一旦用错所有标签会整体偏移这种现象在发票数据集中格外常见因为标注员经常在缩略图上操作。真实尺寸应该这样获取from PIL import Image image_path images/train/0001.jpg img_w, img_h Image.open(image_path).size3.3 批量转换与空标签过滤有了核心函数剩下的就是遍历目录逐张读取图片并转换import os, glob from PIL import Image image_dir images/train xml_dir annotations/train out_dir labels/train os.makedirs(out_dir, exist_okTrue) for image_path in sorted(glob.glob(os.path.join(image_dir, *.jpg))): base os.path.splitext(os.path.basename(image_path))[0] xml_path os.path.join(xml_dir, base .xml) txt_path os.path.join(out_dir, base .txt) if not os.path.exists(xml_path): print(f缺少标注: {base}) continue img_w, img_h Image.open(image_path).size voc2yolo(xml_path, txt_path, class_map, img_w, img_h)这里边收集“有图无标注”的样本转换完统一决定是补标还是排除。把训练图片和标注放在不同目录是一个习惯方便后续出问题时重新生成标签而不污染原图。如果你更习惯把 txt 与图片放同一目录YOLO 也能直接读但建议保留独立目录因为后续增量标注时只需替换 labels。转换结束后做一轮空文件检查find labels/train -type f -name *.txt -size -10c | wc -l空 txt 文件基本都来自 XML 中没有任何有效目标或者所有目标都被 filter 掉。如果数量超过总标注的 5%就要回头核对图片中是否真没有发票内容若原图明显有表格而 XML 为空说明标注漏标不是可以直接丢掉的脏数据。3.4 划分训练验证集时的随机方式划分时要注意不要按文件名顺序机械截断。发票数据集的同批次图片容易具备相似版式按时间顺序截断会导致训练集全是增值税专用发票验证集全是出租车发票。最好是先对图片名做随机 shuffle再按比例拆python -c import random, glob, os files sorted(glob.glob(labels/train/*.txt)) random.seed(42) random.shuffle(files) print(len(files)) 也可以直接用 sklearn 的train_test_split但普通 shuffle 足够。关键是随机种子固定保证多次运行结果一致。拆分后验证集里要挑几张肉眼确认过版式差异的图确保字段类目标有覆盖。如果验证集只包含整票框样本后续 mAP 会虚高上线就露馅。4. 训练发票表格检测模型用 YOLOv8-seg 跑通最小复现方案发票表格检测我一般直接采用 YOLOv8 系列。因为 YOLO 生态对格式要求清晰训练和导出链路完整调参目标明确。如果标注里带polygon或你要做单元格分割就选-seg版本如果只有 bbox就用标准检测版。下面以-seg为例展开。4.1 为什么发票表格任务优先考虑实例分割发票表格检测的最终产物通常不只是画个框而是把表格区域、单元格边界的轮廓提供给 OCR。实例分割比单纯目标检测多输出一层像素级 mask后续对表格线断裂、印章压线都有更多后处理空间。对发票字段区域而言检测框足以完成裁剪对表格线而言mask 能直接反映线像素位置便于按行合并单元格。如果 zip 里只提供了 bbox也可以直接训检测模型。换模型时只需要把权重文件从yolov8n-seg.pt换成yolov8n.pt数据文件不变。判断标准很简单先检查标注中是否包含多边形坐标。如果包含用-seg没有不必强行转换 mask生成伪 mask 会引入噪音。4.2 训练配置与数据文件先准备invoice_table.yaml。这是 YOLO 读取数据集的入口也是新手最容易出错的地方path: /home/user/invoice_table_detection train: train/images val: val/images names: 0: invoice 1: table 2: invoice_code 3: invoice_number 4: amount 5: tax 6: date 7: seller 8: buyer注意path必须写绝对路径YOLO 拼接 train 路径时不会去猜。train指向 images 目录而不是 labels 目录YOLO 会自动寻找同级或按约定路径找 labels。如果你的目录是train/images与train/labels并存它会自动匹配同名 txt这也是上一章把 label 和 image 分开存放的原因——YOLO 原生支持这种组织方式。names的索引顺序必须与转换时class_map的值一致错一位模型不会报错但 mAP 曲线无法解读。训练命令如下yolo train modelyolov8n-seg.pt datainvoice_table.yaml epochs80 imgsz1024 batch8 device0参数说明imgsz1024是发票表格检测里最值得强调的参数。发票文字小、字段密集通用检测常设 640但发票场景下缩到 640 会让多个字段框重叠或丢失。batch8视显存调整如果出现CUDA out of memory先降到 4。训练轮次epochs80在中小规模数据集上够用不需要一上来就 300 轮。还有一个建议开启的增强设置关掉 mosaic。yolo train modelyolov8n-seg.pt datainvoice_table.yaml epochs80 imgsz1024 batch8 mosaic0.0mosaic0.0强制关闭马赛克增强。通用目标检测里 mosaic 能提升模型鲁棒性但发票是版面结构固定、纸张比例固定的文档mosaic 会拼出大量不存在的混合版面让模型学到错误上下文。我在几个客户项目里都遇到过开 mosaic 后字段框偏移的问题关掉后稳定不少。这个操作不是玄学是文档类检测的常识。4.3 评估指标不要只盯着 mAP50训练完先看验证结果yolo val modelruns/detect/train12/weights/best.pt datainvoice_table.yaml imgsz1024输出会包含每个类别的mAP50与mAP50-95。在发票表格检测里invoice与table是大目标AP 很容易接近 1。真正决定项目能不能上线的是invoice_code、amount、tax这些小字段的 AP50。如果模型大目标 AP 很高、小字段很低不要调普通学习率重点是从输入分辨率和增强上解决。用results.csv查看逐类指标更清楚cat runs/detect/train12/results.csv | head -20我一般关注的三项是metrics/mAP50(B)、metrics/mAP50-95(B)、metrics/precision(B)。字段类别的 AP50 低于 0.6先排查标注是否完整标注没问题则对字段区域做裁剪放大单独训练一个字段检测模型。两步走比强行调大模型更稳定。训练过程中还有一个容易忽略的参数save_period10。每 10 个 epoch 保存一次权重避免最后几个 epoch 过拟合被选成 best 后找不到历史权重yolo train modelyolov8n-seg.pt datainvoice_table.yaml epochs80 imgsz1024 batch8 mosaic0.0 save_period10参数save_period在官方文档里有说明实际用起来对发票数据特别有用因为票据数据量小通常在第 40 到 60 轮之间出现过拟合有中间权重可以回溯。5. 发票表格数据集避坑五条从现场换来的血泪经验数据和模型流程跑通只是第一步。真实发票数据集中混杂了大量“来源不明”的坑下面五条是我在多个票据项目中反复遇到并验证过的。5.1 解压后大量 txt 为空训练时被静默忽略现象训练日志里显示有很多图片没有目标训练能跑完但验证集 AP 长期偏低。原因转换脚本把 XML 中name不在映射表里的目标全部跳过加上没有做空文件清理导致空 txt 混在训练集里。这些图片在损失计算中被当作负样本扰动梯度。解决转换后马上执行空文件检查并列出文件名单find labels/train -type f -name *.txt -size -10c empty_labels.txt wc -l empty_labels.txt如果空文件较多逐个打开对应原图确认。真没有表格内容的可以删除原图存在表格但标签为空的属于标注漏标需要在清洗表里记录不能简单丢弃。5.2 标注类别名混乱训练时字段类别被一拆为二现象训练完发票代码和发票号码两个字段的 AP 远低于预期且混淆矩阵上这两个类互相误判。原因不同批次的标注员用了不同英文或中文别名如code与invoice_code、金额与amount。转换脚本把它当成两个类训练时同类目标被拆散样本量减半。解决在转换阶段做类别名归一化。所有类别文本统一小写并去空格再用别名映射表合并alias_map { code: invoice_code, 发票代码: invoice_code, amount: amount, 金额: amount, }参数说明alias_map的键可以是多种写法值统一指向数据集的主类别名。这一步必须放在class_map查询之前先执行否则 XML 里原始 name 直接进 class_map 会 key 缺失。5.3 原图旋转过但标注没跟着旋转现象模型在验证集上框的位置整体偏转发票本身是倾斜的检测框却总是正矩形有时框到了印章或背景上。原因标注阶段标注员看的是旋转矫正后的图但数据集交付时不小心把原始倾斜图打包了或者标注平台自动旋转预览图但没有同步保存坐标。解决先用脚本找出尺寸与主流尺寸差别明显的图片再用图像旋转矫正工具统一处理。旋转图像的同时必须对标注框做同角度变换如果不做最简单的做法是把旋转后的图重新标注不要在脏数据上硬训。5.4 扫描件表格线断裂实例分割 mask 不连续现象电子发票渲染成图片后线条清晰但扫描件或拍照件中表格线时断时续预测出的单元格 mask 缺角OCR 拿到的区域不完整。原因扫描反光、纸张褶皱、印章遮盖导致二值化后的表格线断裂实例分割把断裂的线段视为不同实例mask 自然不连续。解决训练前的 mask 预处理用 OpenCV 的形态学闭运算补线这是一个通用做法import cv2 mask cv2.imread(mask.png, 0) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)MORPH_CLOSE先膨胀再腐蚀能把断点连接起来同时保留单元格形状。参数(5, 5)的核大小可以根据图片分辨率调扫描图分辨率低核可以小一点高分辨率大图用(3, 3)就够。预测后处理同样可以套一遍成本极低。5.5 类别极不平衡字段类 AP 长期上不去现象训练完成后table类 AP 很高invoice_code类 AP 不到 0.3。原因发票数据集里表格区数量远大于字段类像出租车发票可能一张图只有几个字段增值税发票却有十几个字段字段样本天然不平衡。解决优先对少数类做复制粘贴增强把字段区域 cut 出来贴到背景发票上并生成对应 txtimport random def paste_field(src_img, src_box, dst_img): x1, y1, x2, y2 src_box crop src_img[y1:y2, x1:x2] h, w crop.shape[:2] out_x random.randint(0, dst_img.shape[1] - w) out_y random.randint(0, dst_img.shape[0] - h) dst_img[out_y:out_yh, out_x:out_xw] crop return (out_x, out_y, out_x w, out_y h)这段代码从一个发票样本裁出字段区域粘贴到另一张发票的空白位置并返回新的坐标用于写 txt。注意粘贴位置要尽量避开原有表格线最好随机落在非表格区域。这种增强对字段类 AP 的提升比简单复制整图更直接。以上五条不是一次性踩完的它们通常分散在项目不同阶段。比较稳妥的做法是把它们写进数据检查脚本每次拿到新 zip 先跑一遍检查项再进训练流程。6. 从能跑到能上线字段级微调与我每次开训前的一个习惯训练好的模型能出框不等于能满足发票审核场景。上线前我通常再做一轮字段级微调把精力放在最容易被业务追责的字段上。6.1 冻结主干先训字段类发票表格检测部署到现场最常遇到新版发票版式。这时不需要重新标一大批数据可以在原模型基础上做迁移微调。常见做法是把 backbone 冻结只训练 head用 30 到 50 张新发票样本跑 20 个 epochyolo train modelruns/detect/train12/weights/best.pt datainvoice_table_new.yaml epochs20 imgsz1024 freeze10freeze10表示冻结前 10 层YOLOv8 中大致对应 backbone后面的 neck 与 head 继续更新。这样能快速适配新版式同时避免小样本把主干知识冲掉。如果新版发票与旧版差异过大比如从增值税专用发票换成出租车发票冻结层数可以减到 5让更多层参与调整。6.2 验证看什么上线验证不要去算整套指标直接抽查几十张发票看两个点字段框是否压在文字上、表格裁切是否完整。这两个点比 mAP 数字更能反映业务可用性。我会跑一个随机抽样脚本把检测结果画到图上yolo predict modelweights/best.pt sourcesamples/ save_txtTrue save_confTrue预测结果会生成同名 txt 和带检测框的图片肉眼扫 20 张即可发现整体偏移或漏检。这一步动作很小但能拦住绝大部分“指标不错、现场不行”的模型。我自己的习惯是每次训练前先在debug/目录里画 50 张 GT 图把 XML 转回的框叠加在原图上。这个习惯救过我很多次多的是标签坐标错位、类别名混用的问题在训练之前就看到比训练完再分析矩阵省力得多。维护好这套检查流程发票表格检测数据集才能从压缩包变成稳定的训练资产。希望帮到你。本文还有配套的精品资源点击获取
返回列表