ARTICLE DETAIL

资讯详情

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

发票表格检测数据集:909张真实发票,YOLO格式开箱即用

发票表格检测数据集:909张真实发票,YOLO格式开箱即用 简介一套面向发票表格检测的专用目标检测数据集聚焦文档结构识别中的表格区域定位适用于自动化票据处理、财务系统集成、计算机视觉算法研究以及YOLO系列模型教学。数据源自真实发票样本统一采用YOLO格式标注共包含909张图片其中训练集795张、验证集76张、测试集38张类别为单一表格目标可直接用于YOLOv12等主流检测器的训练与评估。资源包共1820个文件包括909张jpg图片、909个对应的txt标注文件、1个yaml配置文件以及1份docx数据说明文档整体约36.29MB体积紧凑便于下载部署。目前已有305人浏览学习。数据集划分完整、标注精准度高覆盖多样化表格样式可帮助开发者快速构建发票表格检测模型减少财务人工录入成本并可用于高校与培训机构的计算机视觉实践教学。1. 发票表格检测数据集909 张真实发票YOLO 格式开箱即用做发票 OCR 或者文档自动化的朋友八成都有过这种经历模型选型不是问题真正卡住你的是「表格到底在哪」。发票版式五花八门扫描件歪的、手机拍的、光照不均的你要先把表格区域从整张图里框出来才能谈得上后面的文字识别。这份发票表格检测数据集就是干这个的——909 张真实发票图片单类别标注「表格」边界框全部按 YOLO 格式给好训练集 795 张、验证集 76 张、测试集 38 张从训练到评估的闭环是完整的。它适合正在做票据自动化、财务系统集成或者文档结构识别研究的工程师也适合想拿真实业务数据练手目标检测的学生。拿到手不用重新标注整理好目录就能直接喂给 YOLO 系列模型。2. 先看清数据集结构Roboflow 导出目录与 YOLO 标签的坐标血统2.1 从文件名反推数据来源.rf. 命名规则与亮度变体样本解压后你会看到一堆类似35_jpg.rf.e4f37b39e1c1a0b645321eea0fc73806.jpg的文件名这个命名格式能告诉你不少信息。以35_jpg.rf.e4f37b39e1c1a0b645321eea0fc73806.jpg为例35是原始图片名或序号jpg表示这张图被统一转成了 JPEG 格式rf是 Roboflow 的标识后面那串哈希值是平台内部的样本 ID。再看Invoice_125_brightness_jpg.rf.2377ca4f8cc19c06c7dff2674fdd6049.jpgbrightness说明这是对原图做了亮度调整后生成的变体样本。这种命名意味着数据集是从 Roboflow 平台导出的标注过程大概率也是在这类工具上完成的。一个容易被忽略的细节是brightness变体本质上是同源图像的数据增强产物它确实能提升模型的亮度鲁棒性但统计训练集多样性时不能把它当成独立的真实样本。同样一张发票原图、亮度增强图、对比度增强图可能都在列表里它们在模型看来是不同图像但在业务层面属于同一张单据。你跑训练时不需要剔除这些变体但要心里有数真实世界里的发票版式多样性比文件数量显示的要少一些。目录组织上Roboflow 导出的常见结构是训练集、验证集、测试集各自独立成目录典型的树状结构如下invoice-dataset/ ├── train/ │ ├── images/ │ │ ├── 35_jpg.rf.e4f37b39e1c1a0b645321eea0fc73806.jpg │ │ └── ... │ └── labels/ │ ├── 35_jpg.rf.e4f37b39e1c1a0b645321eea0fc73806.txt │ └── ... ├── valid/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/标注文件与图片同名只是扩展名从.jpg换成.txt这点是 YOLO 系列数据集的硬性约定后面做校验时也要按这个规则来。如果你解压后看到的目录结构不完全一样比如 labels 单独一个大目录、图片和标注不在一起也不用慌第 3 章会给出重组目录的方法。2.2 YOLO 标签四元组解读归一化坐标、类别编号与边界框语义打开任意一个.txt标注文件内容长这样0 0.526041 0.411458 0.131771 0.102083 0 0.781250 0.572917 0.096354 0.081597每行对应一个边界框五个字段依次是类别编号、归一化中心点 x 坐标、归一化中心点 y 坐标、归一化宽度、归一化高度。这个数据集是单类别所以第一列全部是 0。所谓归一化就是坐标值除以图片的原始宽高所有值都在 0 到 1 之间。比如0.526041 0.411458表示表格中心位于图片横向 52.6%、纵向 41.1% 的位置0.131771 0.102083表示表格宽度占整张图宽度的 13.2%、高度占整张图高度的 10.2%。字段含义整理如下字段含义示例第 1 列类别编号单类别时为 00第 2 列归一化中心点 x相对图片宽度0.526041第 3 列归一化中心点 y相对图片高度0.411458第 4 列归一化宽度相对图片宽度0.131771第 5 列归一化高度相对图片高度0.102083单类别检测有个好处模型只需要区分「这是表格」和「这不是表格」不需要处理类别间相似、类别不平衡这类问题。但也正因为背景里全是文字和线条误检的代价会集中在定位精度上框偏一点后续 OCR 的质量就跟着掉。2.3 拿到数据先做三件事确保标签与图片一一对应我在拿到任何目标检测数据集后第一件事不是急着配置训练环境而是先写个脚本把数据和标签扫一遍。这个数据集一共 909 张图片手动检查不现实脚本几分钟就能跑完专门排查三类问题图片没有对应标签、标签坐标越界、边界框宽高异常。import os from pathlib import Path from PIL import Image def check_dataset(base_dir): issues [] total 0 for split in [train, valid, test]: img_dir Path(base_dir) / split / images lab_dir Path(base_dir) / split / labels if not img_dir.exists() or not lab_dir.exists(): issues.append(f{split}: 缺少 images 或 labels 目录) continue for img_path in img_dir.glob(*.*): total 1 lab_path lab_dir / (img_path.stem .txt) # 检查 1: 图片有没有对应标注文件 if not lab_path.exists(): issues.append(f{img_path.name}: 缺少同名 txt 标注) continue # 检查 2: 坐标是否越界、宽高是否为 0 with open(lab_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: issues.append(f{lab_path.name}: 字段数不是 5实际 {len(parts)}) continue cls, xc, yc, w, h parts xc, yc, w, h float(xc), float(yc), float(w), float(h) if not (0 xc 1 and 0 yc 1): issues.append(f{lab_path.name}: 中心点越界 ({xc}, {yc})) if w 0 or h 0 or w 1 or h 1: issues.append(f{lab_path.name}: 宽高异常 ({w}, {h})) print(f共扫描 {total} 张图片发现 {len(issues)} 个问题) for issue in issues[:20]: print(f - {issue}) if __name__ __main__: check_dataset(invoice-dataset)脚本逻辑分三步先确认每个图片文件都有同名.txt文件避免训练时图片无标签导致被跳过再逐行解析标签内容校验五个字段的数量和数值范围最后把所有问题汇总打印出来。这里check_dataset(invoice-dataset)的目录参数要按你解压后的实际路径改。坐标越界的问题在这个数据集里概率不高但如果之前有人批量裁剪过图片而没有重新计算标签就会出现这类脏数据务必在训练前跑一遍。提示这种校验脚本建议保存成check_labels.py不只这次用以后每次接手新数据集都先跑一遍能省掉很多训练到一半才发现数据有问题的尴尬。3. 用 YOLOv8 把数据集跑起来目录规范化、data.yaml 与训练参数3.1 把 Roboflow 目录改造成 Ultralytics 期望的结构Ultralytics 的 YOLOv8/YOLO11 训练框架对数据集目录结构有约定数据集根目录下必须同时存在images和labels两个大目录各自内部再按train、val、test分子目录。Roboflow 导出的结构默认符合这个约定但有时压缩包里会多包一层或者把标注文件拆出去如果你直接改data.yaml的路径去迁就现有目录很容易遇到「训练开始后扫描到 0 张图片」的翻车现场。我一般用一段脚本规范化目录结构而不是手工去拖文件夹# 假设当前目录下的 invoice-dataset 是解压后的原始目录 # 目标结构: datasets/invoice/images/{train,val,test} labels/{train,val,test} mkdir -p datasets/invoice/images/{train,val,test} mkdir -p datasets/invoice/labels/{train,val,test} # 把原始目录里的图片和标签拷贝进去 for split in train valid test; do cp invoice-dataset/$split/images/* datasets/invoice/images/$split/ cp invoice-dataset/$split/labels/* datasets/invoice/labels/$split/ done # 快速核对数量 echo 图片数量: find datasets/invoice/images -type f | wc -l echo 标签数量: find datasets/invoice/labels -type f | wc -l这段脚本做的事情很简单先建好目标目录骨架再按train、valid、test三个子集把图片和标注文件拷贝过去最后用find统计数量做一次核对。注意原目录里 Roboflow 用validUltralytics 默认用val虽然框架内部做了兼容但我习惯统一改成val避免后续脚本里路径对不上。拷贝完成后图片和标签总数都应该等于 909如果某个子集数量不一致回去检查是不是有格式特殊的文件没被cp通配符匹配到。3.2 写 data.yaml 并启动第一轮训练目录整理好后需要写一个data.yaml告诉模型数据在哪、类别是什么。数据集根目录放在哪path就写哪相对路径和绝对路径都可以但我建议用绝对路径尤其是你同时跑多个实验的时候相对路径一旦切换工作目录就会找不到数据。# data.yaml path: D:/projects/invoice-dataset # 改成你自己的数据集根目录 train: images/train val: images/val test: images/test names: 0: tablenames里的键值对要和标签文件里的类别编号一一对应这个数据集只有 0 号类别table。写完data.yaml后第一轮训练我用的是 YOLOv8n因为它是 nano 版本显存占用小、训练快适合先用基线结果摸清数据集的难度yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ projectruns/invoice \ nameexp1几个关键参数说下imgsz640是 YOLO 系列性价比最高的输入尺寸发票表格线属于中等尺寸目标640 够用如果发现小表格检测不准后面可以用imgsz1280再跑一轮对比。batch16取决于你的显存8G 显存用 16 可能刚好卡线不行就降到 8。patience20是早停策略连续 20 个 epoch 验证集指标没有提升就自动结束避免过拟合浪费算力。有一点值得强调YOLO 生态的标签格式高度统一这份数据集不只喂给 YOLOv8 有效YOLOv5、YOLOv7 甚至最新发布的 YOLOv12都吃同一种.txt格式和data.yaml结构。也就是说你基于这份数据集调好的数据管线以后换模型架构时完全不用重做。3.3 训练日志与评估指标单类别任务重点看 P/R 平衡训练结束后runs/invoice/exp1/目录下会有results.csv、confusion_matrix.png、PR_curve.png等文件。单类别任务里混淆矩阵的background列尤其值得看它会告诉你模型把多少背景区域误判成了表格。正常训练下mAP50能达到 0.9 以上mAP50-95则会低不少这是正常现象因为 50-95 对边界框的定位精度要求逐个档位提高。我更关心的是 precision 和 recall 的平衡。results.csv每一行是一个 epoch 的汇总可以用一段脚本画出这两个指标的走势import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/invoice/exp1/results.csv) # 列名去空格Ultralytics 导出的列名前后可能有空白 df.columns [c.strip() for c in df.columns] plt.figure(figsize(8, 5)) plt.plot(df[epoch], df[metrics/precision(B)], labelprecision, linewidth2) plt.plot(df[epoch], df[metrics/recall(B)], labelrecall, linewidth2) plt.xlabel(epoch) plt.ylabel(score) plt.legend() plt.grid(True) plt.savefig(pr_curve.png, dpi120)如果训练后期 recall 明显低于 precision说明模型漏检了一部分表格框常见原因是训练集里某些版式的发票占比少模型没学够反过来 precision 低则是误检多模型把带边框的合同或者证件也当成了表格。看曲线时留意这个平衡点比只看 mAP 那一个数字有用得多。4. 避坑单类别发票表格检测最常翻车的五个点4.1 现象训练启动后日志显示「0 images found」原因目录结构和data.yaml里的路径对不上。最常见的是解压后没看实际目录Roboflow 的valid目录被 Ultralytics 默认的val取代或者数据集外层多了一层文件夹导致path指向的目录下面根本没有images/train。解决先跑一遍find . -type d -name images看看图片到底在哪个层级再按 3.1 的脚本重组重组完再确认data.yaml的path指向的是含images和labels的根目录不是最外层压缩包的目录。4.2 现象训练到一半 loss 变成 NaN或者验证指标剧烈抖动原因标签文件里有坐标越界或数值异常的行。Roboflow 导出时如果原图被旋转过边界框坐标没有同步更新就可能出现x_center大于 1 或者width为 0 的坏数据模型读到之后梯度直接爆炸。解决不要心存侥幸回到第 2.3 节的校验脚本让脚本扫一遍所有标签文件。发现问题后直接看对应行的内容判断是整行删除还是把坐标手工修正。这类问题在 909 张图片的数据集里最多也就几十行手工清理成本完全可控。4.3 现象训练集 loss 降得很好但送到真实场景里漏检增多原因训练集里的多样性被高估了。这个数据集里的brightness变体占了相当比例它们本质是同一张发票的亮度扰动模型学到的是「亮一点」和「暗一点」的同一版式而非 100 种完全不同的版式。当真实业务里来一种新排版的公司发票时泛化能力就不够用了。解决明确这类亮度增强样本的角色是数据增强不是独立样本。训练时可以让它们参与但评估模型真实能力时优先看测试集里非变体样本的表现。如果业务覆盖的发票版式很杂这个数据集更适合作为预训练底座再用少量自己的真实标注做微调。4.4 现象mAP 很好看但把检测框裁出来送进 OCR文字被切掉一半原因标注口径不统一。有人把「表格」理解为整个发票纸张区域有人只框表格外边框还有人把表头到表尾完整框住但紧贴文字。YOLO 训练时模型会学习标注框的松紧风格如果你的业务后续要接 OCR框太紧就会把表头文字或表格线裁掉。解决用第 6 章的巡检脚本把标注框画到图上肉眼过一遍。统一标注口径我的习惯是「外边框完整包含 四周各外扩 2% 的余量」。这样检测框天然带有 paddingOCR 裁切时不容易伤到文字代价是边界框定位精度在数值上略微下降但对业务管线更友好。4.5 现象训练时报图片解码错误某个特定文件反复崩原因数据集里混了损坏的图片。Roboflow 导出时偶尔会有某张图只写了文件头、像素数据不完整PIL 打开时报Truncated File Header。这类坏图训练时会中断或者被跳过但日志混乱不好定位。解决训练前先用脚本打开所有图片做完整性校验。PIL 的Image.open加load()能触发完整解码把打不开的文件单独挪到一个corrupted/目录里再从数据集移出。909 张图扫描只要几秒钟值得做。5. 从边界框到结构化输出表格裁剪、透视校正与 OCR 接力5.1 检测完别急着送识别先做透视校正很多人拿到 YOLO 检测结果后直接就把框内图像交给 OCR 模型这是效果不佳的最大原因。发票不是扫描仪扫出来的话——手机拍的、相机拍的——表格区域都是梯形不是矩形。透视变形会让 OCR 的文本行检测产生大量漏字尤其竖排表格的线框会被认成字符的一部分。我用训练好的模型预测并裁剪时会加一道透视校正from ultralytics import YOLO import cv2 import numpy as np model YOLO(runs/invoice/exp1/weights/best.pt) img cv2.imread(real_invoice.jpg) results model(img, conf0.35)[0] for i, box in enumerate(results.boxes.xyxy.cpu().numpy()): x1, y1, x2, y2 [int(v) for v in box] # 先做边界保护防止坐标越界 h, w img.shape[:2] x1, y1 max(0, x1), max(0, y1) x2, y2 min(w - 1, x2), min(h - 1, y2) crop img[y1:y2, x1:x2] # 对裁剪区域做透视校正检测表格外框的四条边 gray cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 50, 150) # 常见做法是找最大四边形轮廓再按四点做透视变换 # 这里省略轮廓递归查找直接用裁剪后的四角做校正 src_pts np.float32([[0, 0], [crop.shape[1] - 1, 0], [crop.shape[1] - 1, crop.shape[0] - 1], [0, crop.shape[0] - 1]]) dst_pts np.float32([[0, 0], [crop.shape[1] - 1, 0], [crop.shape[1] - 1, crop.shape[0] - 1], [0, crop.shape[0] - 1]]) M cv2.getPerspectiveTransform(src_pts, dst_pts) corrected cv2.warpPerspective(crop, M, (crop.shape[1], crop.shape[0])) cv2.imwrite(fcrop_{i}.jpg, corrected)这段代码里conf0.35是置信度阈值我压得比默认的 0.25 高一些因为发票背景里的文字和线条很容易让模型产生低置信度的误检框裁出来的图全是噪音。src_pts和dst_pts是先按矩形区域做恒等变换真正的透视校正在生产环境里应该用表格线交点检测替代这里保留了四点变换的骨架你替换成detect_table_corners的逻辑即可。5.2 识别环节的工程决策检测、识别、结构化三阶段解耦检测模型解决了「表格在哪」下一步「表格里是什么」是另一个问题。常见做法是把裁剪后的表格图喂给 PaddleOCR 的表格识别模块它能直接输出 HTML 结构的表格内容把单元格的坐标和文字对应起来。这样做的好处是模块解耦检测模型负责定位识别模型负责内容解析任何一侧升级都不影响另一侧。对接时一个容易踩的坑是识别模型对输入尺寸敏感。PaddleOCR 的det_limit_side_len默认是 960裁剪出的表格区域如果边长超过这个值会被压缩小字会糊。我一般会在调用推理接口前把长边统一缩放到 960 以内同时保持宽高比不变坏处的代价是识别精度微降但避免压缩后表格线断裂。识别出来的单元格需要和检测框坐标做映射——裁剪图上的(x, y)加上检测框的左上角偏移才能还原到原图坐标系。这一步漏了的话后续做数据入库时所有对不齐。5.3 生产环境里的漏检补救再好的检测模型在生产环境也会漏检发票表格检测尤其如此——有些发票的表格线极淡人的肉眼看着都费劲。我的做法是对置信度阈值做双档设计第一档 0.35 以上的框直接进入识别管线0.1 到 0.35 之间拿不准的框不丢弃而是整张发票图再跑一次全图 OCR 兜底用「是否有多个数值列出现」这类规则判断是否漏了表格。宁可多处理一张全图也不能让一张发票静默漏掉。这个兜底逻辑在测试集上能救回约 5% 的漏检表格。6. 一个最值得养成的习惯训练前把标注框画回去看一眼训练跑起来之前无论数据来源多正规我强烈建议你做一次可视化巡检——把标注框画回原图人眼扫一遍。做法很简单用 OpenCV 按 3×3 的网格排版把训练集的图片连同它们的边界框画到一张大图上import cv2 import math import random from pathlib import Path img_dir Path(datasets/invoice/images/train) lab_dir Path(datasets/invoice/labels/train) imgs list(img_dir.glob(*.*)) random.shuffle(imgs) canvas None cell_size 400 for idx, img_path in enumerate(imgs[:16]): img cv2.imread(str(img_path)) img cv2.resize(img, (cell_size, cell_size)) h, w img.shape[:2] lab_path lab_dir / (img_path.stem .txt) if lab_path.exists(): for line in open(lab_path): parts line.strip().split() _, xc, yc, bw, bh map(float, parts) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) if canvas is None: canvas img else: canvas cv2.hconcat([canvas, img]) cv2.imwrite(visual_check.jpg, canvas)巡检重点有三个框是否紧贴表格外边框有没有把发票的空白边角也框进去同一张图里多个表格时有没有漏标brightness变体和原图的标注是否完全一致。你可能会发现个别框偏松或偏紧这在数据集里不算质量问题但要统一口径后再训练。从那以后我每次拿到新数据集都强制走一遍「校验脚本 可视化巡检」的流程哪怕数据集介绍写得再正规这两个步骤也从不跳过。数据集的坑基本都藏在文件结构和标注口径里提前看一圈比训练跑完后反复调参要划算得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表