简介:针对表格行、列与合并单元格三类结构,一套标注良好的数据集面向文档数字化与表格结构识别场景,适合目标检测方向的研究者或开发智能文档处理系统的工程师使用。数据集中训练集包含1279张真实场景图片,验证集48张,每张图均带有YOLO格式的txt边界框标注,严格控制表格行、表格列以及跨行/跨列的合并单元格,能够直接输入YOLOv5、YOLOv8乃至当前最新的YOLOv12进行训练和效果验证。压缩包内共2000个文件,主要构成为671张jpg原图、1327个配对txt标注,同时包含1个yaml训练配置与1份docx数据说明文档,整体大小38.51MB,虽轻量但足以作为表格检测算法快速迭代的起步语料。目前已有235人学习。对需要快速打通表格结构识别流程的团队而言,既能节省收集和清洗数据的精力,又能利用其多样化的表格布局来排查跨行跨列等难点,适合作为预研、课程实验或小规模业务验证的基准数据集。
1. 表格结构检测:把发票变成结构化数据的第一道坎
做发票或报表自动化的时候,OCR只告诉你这一片区域有哪些字,不告诉你这些字属于哪一列、哪一行。碰到合并单元格,普通文本框检测基本当场失效——合并后的表头既不在某一条列线上,也不在某一条行线上。这个名为「表格结构检测数据集」的压缩包里,整理了1,279张训练图和48张验证图,全部按YOLO格式做了边界框标注,标签分为 table column、table row、table spanning cell 三类。它直接面向表格结构识别和文档布局分析场景,适合正在用 YOLOv8 训练自己的数据集做文档数字化的工程师,也适合做票据结构化、RPA流程自动化的人。拿到这份 ZIP 之后怎么解析标签、怎么训练、怎么把检测框还原成可读表格,下面直接给能跑的方案。
2. 解压后第一件事:读懂YOLO标签里的三类框
2.1 目录结构与文件命名规律
ZIP 解压后,首先看目录是不是 Roboflow 导出的标准结构。典型布局是train/images、train/labels、valid/images、valid/labels,外加一个data.yaml。示例文件名长这样:INV_conv1086_jpg.rf.c2db229eba155d44adac51ad3ae30f47.jpg。中间那个.rf.是 Roboflow 导出的固定标记,后面的长哈希是图片唯一 ID,用来避免不同来源的图片重名互相覆盖。
注意到这些文件都以INV_开头,说明原始图像主要来自发票(Invoice)类文档。INV 前缀对训练有实际影响:发票表格的行列结构相对规范,边框清晰,但合并单元格和异形表头比普通报表多,这正是 three-class 标注设计的原因。
先检查一下data.yaml里的 path 和 names 是否存在。如果路径是绝对路径,而你移动了整个目录,训练时就会提示found no labels。我一般会直接重写这个文件,保证路径指向当前机器的实际位置。
2.2 三个类别到底在标什么
先别急着训练,把标签语义搞清楚。这个数据集不是简单的「目标检测」,而是面向表格结构重建的检测任务。
table column:列区域框。标记的是一个或几个列在水平方向上的延展范围,框的宽度通常覆盖一列,高度覆盖整个表格数据区。table row:行区域框。标记的是表格中的一行,宽度横跨整个表格,高度只覆盖该行。table spanning cell:跨越多个行或列的合并单元格。它的边界框会同时压住多个 row 和 column,是破坏规则网格的元凶。
为什么不用一个cell类来标所有单元格?因为直接检测每个单元格,你只能得到一堆矩形,得不到它们之间的行列拓扑关系。行列框提供骨架,spanning cell 标记骨架上的合并区域,这样后续重建表格时才能知道某行某列交点应该合并还是分开。这个思路本质上是用目标检测的方式完成表格结构识别里的 region 级解析,比像素分割好落地很多。
2.3 读取YOLO标注并可视化
YOLO 格式每行是class_id cx cy w h,中心点坐标和宽高都做了归一化。要画到图上必须转换回原图像素坐标。下面这个函数是我处理这类数据时常用的,额外加了越界检查:
def load_yolo_label(txt_path, img_w, img_h, img_name=""): boxes = [] with open(txt_path, encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue parts = line.split() cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:5]) x1 = (cx - bw / 2) * img_w y1 = (cy - bh / 2) * img_h x2 = (cx + bw / 2) * img_w y2 = (cy + bh / 2) * img_h if x1 < 0 or y1 < 0 or x2 > img_w or y2 > img_h: print(f"[warn] {img_name}: box out of boundary -> {cls_id}, {x1:.1f}, {y1:.1f}, {x2:.1f}, {y2:.1f}") boxes.append((cls_id, x1, y1, x2, y2)) return boxes代码逻辑:按行拆分标签,前 5 个值缺一不可。中心点归一化坐标减去半宽得到左上角像素坐标,加上半宽得到右下角。越界检查很关键,Roboflow 导出偶尔会因为旋转增强产生超出画布 1-2 像素的框,不检查会在训练时触发警告甚至丢失标注。
用 OpenCV 画出来的可视化代码如下:
import cv2 img = cv2.imread("INV_conv1086_jpg.rf.c2db229eba155d44adac51ad3ae30f47.jpg") h, w = img.shape[:2] boxes = load_yolo_label( "INV_conv1086_jpg.rf.c2db229eba155d44adac51ad3ae30f47.txt", w, h ) colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255)] names = ["column", "row", "spanning"] for cls_id, x1, y1, x2, y2 in boxes: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), colors[cls_id], 2) cv2.putText(img, names[cls_id], (int(x1), max(18, int(y1) - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[cls_id], 2) cv2.imwrite("label_vis.jpg", img)这段代码把三类框用红绿蓝区分,column 是蓝色,row 是绿色,spanning cell 是红色。随机抽 30 张图跑一遍可视化,重点看两个地方:spanning cell 框是否精确覆盖了合并区域,row 框有没有出现上下半行重叠。标注质量直接影响最终模型上限,先人工确认一遍比训练后反复调参更有用。
2.4 标注分布统计
表格场景里三个类别天然不平衡,特别是 spanning cell。合并单元格在发票中通常集中在表头和合计行,一个版面里可能只有一到两个。我用下面这段代码统计每个类别的框数量:
from collections import Counter from pathlib import Path label_dir = Path("train/labels") cls_counter = Counter() for txt_path in label_dir.glob("*.txt"): for line in txt_path.read_text().strip().splitlines(): cls_counter[int(line.split()[0])] += 1 print(cls_counter)执行后你会发现 spanning cell 类的样本数可能只有 row 类的十分之一。这不是数据缺陷,而是真实场景长尾分布的体现。后面训练时不要期望这个类能和 row 类达到同样高的 recall,而是要在后处理中给 spanning cell 单独设更低的置信度阈值,避免它被 NMS 过滤掉。
3. 用YOLOv8训练表格结构检测模型:从data.yaml到参数调优
3.1 环境准备与模型选择
训练环境建议直接用 ultralytics 库,它是 YOLOv8 / YOLO11 的官方实现,命令相对统一。安装命令很简单:
pip install -U ultralytics显存 12G 以上建议用yolov8s起步,24G 直接上yolov8m。这个数据集涉及细长的行框和列框,模型感受野太小可能只看到局部纹理,学不出完整的行列跨度。如果你是第一次跑通流程,用yolov8n也没问题,只是精度上限略低。
3.2 重写data.yaml避免路径坑
ZIP 里自带的 data.yaml 需要检查,我建议无论如何都重写一遍,尤其是 path 和目录结构:
# data.yaml path: /data/table_dataset train: train/images val: valid/images nc: 3 names: 0: table column 1: table row 2: table spanning cell一个非常容易踩的坑:train 路径写的是train/labels而不是train/images。ultralytics 会去train/images对应的兄弟目录train/labels自动找标签,你如果显式写成 labels 反而找不到图片。命名要求很死板:图片abc.jpg对应的标签必须是abc.txt,且文件名完全一致,包括.rf.那段哈希后缀。
改完之后先跑一个 1 epoch 的冒烟测试,确认没有found no labels或Label class 3 missing报错:
yolo detect train model=yolov8s.yaml data=data.yaml epochs=1 batch=1这一步只为了验证数据通路,不会真的训练出一个有效模型。
3.3 正式训练命令与关键参数
验证无误后,正式训练命令如下:
yolo detect train \ model=yolov8s.pt \ data=data.yaml \ imgsz=1024 \ batch=8 \ epochs=150 \ patience=30 \ device=0 \ seed=42 \ project=table_structure \ name=yolov8s_1024参数逐个解释:
model=yolov8s.pt:加载 COCO 预训练权重。表格检测任务里,底层边缘纹理特征可以直接复用,迁移学习能显著缩短收敛时间。imgsz=1024:表格结构检测建议不要低于 1024。表中细线、小字、窄列往往只有 10-20 像素,缩到 640 就糊成一团,行列边框信息基本丢失。我实测过同一份数据 1024 比 640 的 mAP50 高 6-8 个百分点。batch=8:由显存决定。24G 卡可以到 16,8G 卡建议降到 4。batch 太小会导致 BN 统计量不稳,尤其是验证集只有 48 张的情况下。patience=30:验证集连续 30 轮无提升就早停。因为验证集小,mAP 波动很大,patience 设置过小容易被单次随机波动触发提前停止。seed=42:固定随机种子,保证多次实验之间可比,方便调参。
训练过程中观察results.csv里的metrics/mAP50(B)和metrics/mAP50-95(B),这两个是主要精度指标。cls_loss下降不一定代表检测准,最终以验证集 mAP 为准。
3.4 断点恢复与二次训练
训练到一半中断了,不需要从头跑,直接加上 resume:
yolo detect train \ model=table_structure/yolov8s_1024/weights/last.pt \ data=data.yaml \ imgsz=1024 batch=8 epochs=150 \ resume=True如果训练完成了但精度不理想,不要直接重新训练。用当前best.pt作为预训练权重,再继续训练 50 到 100 轮是一种有效做法。原因是这个数据集场景和 COCO 差异大,已经学到的行列结构特征比 ImageNet 预训练权重更有价值:
yolo detect train \ model=table_structure/yolov8s_1024/weights/best.pt \ data=data.yaml \ imgsz=1024 batch=8 epochs=50 \ lr0=0.0005这里把初始学习率从默认的 0.01 降到 0.0005,避免在已有最优权重附近大步长震荡。
3.5 类别不平衡时的训练策略
spanning cell 样本少,模型会对这个类偏向保守。训练阶段有两个常用手段:一是增大输入分辨率,让小尺寸的合并单元格也能显露细节;二是关闭 Mosaic 增强或降低其概率,因为 Mosaic 拼接容易把表格切碎,导致合并单元格跨越多张图,标签语义被破坏。
yolo detect train \ model=yolov8s.pt data=data.yaml \ imgsz=1024 batch=8 epochs=150 \ mosaic=0.5 close_mosaic=10这里mosaic=0.5表示后文 Mosaic 使用概率 0.5,close_mosaic=10表示最后 10 轮关闭 Mosaic。表格文献里,靠近训练结束阶段关闭 Mosaic 可以稳定 final 精度。如果你想显式增加 minority class 的loss 权重,ultralytics 没有现成参数,我建议不动 loss,把注意力放到后处理上。
4. 重建表格结构:行列网格与合并单元格后处理
4.1 为什么不能直接用检测框输出表格
模型推理得到的是一堆边界框坐标,不是表格。要把这些框转换成 HTML 表格或 Excel 表格,必须做结构重建。最简单的方法是取所有 column 框的左边界和右边界作为列线,取所有 row 框的上边界和下边界作为行线,列线和行线的所有交点构成网格。之后每个网格单元就是一个候选单元格。
这个流程遇到第一个问题是检测框有冗余。同一个列可能被输出两个重叠框,由于没有后处理,边界会乱。先做一次同类别的 NMS 和边界合并是必须的。
4.2 合并相似框并精炼边界
我一般会写一个专门用于结构重建的合并函数,而不是直接用 ultralytics 内置 NMS:
def iou(a, b): x1 = max(a[0], b[0]); y1 = max(a[1], b[1]) x2 = min(a[2], b[2]); y2 = min(a[3], b[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) union = (a[2]-a[0])*(a[3]-a[1]) + (b[2]-b[0])*(b[3]-b[1]) - inter return inter / union if union > 0 else 0 def merge_boxes(boxes, iou_thres=0.4): boxes = sorted(boxes, key=lambda b: (b[2]-b[0])*(b[3]-b[1]), reverse=True) kept = [] while boxes: cur = boxes.pop(0) remaining = [] for b in boxes: if iou(cur, b) > iou_thres: cur = [ min(cur[0], b[0]), min(cur[1], b[1]), max(cur[2], b[2]), max(cur[3], b[3]) ] else: remaining.append(b) boxes = remaining kept.append(cur) return kept这段代码的思路是:把大框优先拿出来,和它重叠超过 40% 的小框合并到一起,合并后取所有框的并集范围。因为 row 框和 column 框都是长条,重叠通常发生在相邻框的边界,合并后能得到更干净的行列边界。注意这里没有使用置信度排序,因为结构重建更看重空间完整性,不是最大得分。
4.3 构建行列网格并标记合并区
有了干净的 row 框和 column 框,接下来抽取边界坐标并排序。对 column 框,按 x 中心点从左到右排序,再将所有左边界和右边界去重后保存为col_edges。同样处理 row 框得到row_edges。每个网格单元就是row_edges[i]到row_edges[i+1],col_edges[j]到col_edges[j+1]围成的矩形。
然后遍历所有 spanning cell 框,把被它完整覆盖的网格单元标记为合并区域:
import numpy as np def make_table_grid(col_edges, row_edges, spanning_cells): rows = len(row_edges) - 1 cols = len(col_edges) - 1 grid = np.zeros((rows, cols), dtype=int) for sx1, sy1, sx2, sy2 in spanning_cells: for r in range(rows): for c in range(cols): cell_x1 = col_edges[c]; cell_x2 = col_edges[c+1] cell_y1 = row_edges[r]; cell_y2 = row_edges[r+1] if (cell_x1 >= sx1 and cell_x2 <= sx2 and cell_y1 >= sy1 and cell_y2 <= sy2): grid[r][c] = 1 return grid标记逻辑是:只要网格单元的四个边界都落在同一个 spanning cell 框内部,就认为这个单元是合并单元格的一部分。最终 grid 矩阵里二维为 1 的区域会聚成一个合并块。
这里有一个非常实际的问题:spanning cell 框只要偏移几个像素,就可能标记不到边缘网格,甚至会吞掉不该合并的格子。我一般会对输出做一步膨胀:把 grid 中上下左右相邻的值也置 1,再做连通域分析,只保留面积大于等于 2 的合并区。这样可以容忍少量检测框偏移。
4.4 生成HTML表格输出
得到 grid 之后,把连续为 1 的行列范围转换成rowspan和colspan。最简单的做法是遍历每个未合并网格单元,向右向下找连续合并区域,输出时保留行列跨度最小单元。HTML 表格如下:
<table border="1"> <tr> <td rowspan="2">合并单元格A</td> <td colspan="2">合并单元格B</td> </tr> <tr> <td>普通单元格</td> <td>普通单元格</td> </tr> </table>转换代码的核心就是遍历grid,遇到grid[r][c] == 0时向右侧和下方扩展,直到找到边界。对table column与table row的置信度要求不同,我建议在进入重建之前先把置信度低于 0.25 的 row 框过滤掉,而 column 框可以放宽到 0.15,因为 table panel 的长条形检测天然会比行框更脆弱。
5. 验证模型效果:mAP、可视化与三个常见坑
5.1 验证命令与指标解读
训练结束后,先跑一遍标准验证:
yolo detect val \ model=table_structure/yolov8s_1024/weights/best.pt \ data=data.yaml \ imgsz=1024输出里除了mAP50和mAP50-95,还有每个类别的单独指标。重点关注table spanning cell一行的 recall。由于这个类样本少,模型常常表现为 precision 高、recall 低——有 70% 概率把真正的合并框找出来,但 30% 会漏掉。如果 recall 低于 0.5,先去检查标注框是否过小或过短,而不是急着加模型宽度。
5.2 用预测结果做可视化对比
数值指标只能说明整体水平,表格检测这种强结构任务必须看图。执行预测并输出标签:
yolo detect predict \ model=table_structure/yolov8s_1024/weights/best.pt \ source=valid/images \ save_txt=True \ save_conf=True预测结果会写到runs/detect/predict/labels下,直接复用第 2 章的load_yolo_label读取并画框。我通常把预测框和 Ground Truth 框画在同一张图上,左边是标签,右边是预测,看的是 row 框有没有整体偏移、column 框有没有漏掉窄列。有时 mAP 很高但 visual 效果差,是因为 mAP 对边界框偏移不敏感,而表格重建对边界偏移极其敏感。
5.3 三个真实项目里踩过的坑
第一个坑是验证集太小。48 张图算出的 mAP 波动非常大,同一权重换个随机种子可能差 4-5 个点。建议把训练集里再留 100 张出来做 frozen validation,或者跑 5 折交叉验证,不要只看官方那份 validation 结果。
第二个坑是大图直接 resize。原始发票图可能 2849 x 2093,YOLO 训练时默认不保持长宽比直接 resize 到 1024x1024,细线会被压断。解决方案是在 data.yaml 里设置augment: false配合手动预处理保持长边,或者用rect=True参数走矩形训练:
yolo detect train ... rect=True第三个坑是类别索引错位。Roboflow 导出的 YOLO 标签从 0 开始,但有些脚本或手改的 label 从 1 开始,导致整体类别错一位。训练前打印一下所有 txt 里 class_id 的最大值和 names 数量做比对,只花一分钟,能省下整晚的排错时间。
先把预测可视化这一步做扎实,再去动训练参数——这样你看到的每一次精度变化,都是真实结构差异的反映,而不是随机噪声。
本文还有配套的精品资源,点击获取