
简介针对表格行、列与合并单元格三类结构一套标注良好的数据集面向文档数字化与表格结构识别场景适合目标检测方向的研究者或开发智能文档处理系统的工程师使用。数据集中训练集包含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, encodingutf-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 modelyolov8s.yaml datadata.yaml epochs1 batch1这一步只为了验证数据通路不会真的训练出一个有效模型。3.3 正式训练命令与关键参数验证无误后正式训练命令如下yolo detect train \ modelyolov8s.pt \ datadata.yaml \ imgsz1024 \ batch8 \ epochs150 \ patience30 \ device0 \ seed42 \ projecttable_structure \ nameyolov8s_1024参数逐个解释modelyolov8s.pt加载 COCO 预训练权重。表格检测任务里底层边缘纹理特征可以直接复用迁移学习能显著缩短收敛时间。imgsz1024表格结构检测建议不要低于 1024。表中细线、小字、窄列往往只有 10-20 像素缩到 640 就糊成一团行列边框信息基本丢失。我实测过同一份数据 1024 比 640 的 mAP50 高 6-8 个百分点。batch8由显存决定。24G 卡可以到 168G 卡建议降到 4。batch 太小会导致 BN 统计量不稳尤其是验证集只有 48 张的情况下。patience30验证集连续 30 轮无提升就早停。因为验证集小mAP 波动很大patience 设置过小容易被单次随机波动触发提前停止。seed42固定随机种子保证多次实验之间可比方便调参。训练过程中观察results.csv里的metrics/mAP50(B)和metrics/mAP50-95(B)这两个是主要精度指标。cls_loss下降不一定代表检测准最终以验证集 mAP 为准。3.4 断点恢复与二次训练训练到一半中断了不需要从头跑直接加上 resumeyolo detect train \ modeltable_structure/yolov8s_1024/weights/last.pt \ datadata.yaml \ imgsz1024 batch8 epochs150 \ resumeTrue如果训练完成了但精度不理想不要直接重新训练。用当前best.pt作为预训练权重再继续训练 50 到 100 轮是一种有效做法。原因是这个数据集场景和 COCO 差异大已经学到的行列结构特征比 ImageNet 预训练权重更有价值yolo detect train \ modeltable_structure/yolov8s_1024/weights/best.pt \ datadata.yaml \ imgsz1024 batch8 epochs50 \ lr00.0005这里把初始学习率从默认的 0.01 降到 0.0005避免在已有最优权重附近大步长震荡。3.5 类别不平衡时的训练策略spanning cell 样本少模型会对这个类偏向保守。训练阶段有两个常用手段一是增大输入分辨率让小尺寸的合并单元格也能显露细节二是关闭 Mosaic 增强或降低其概率因为 Mosaic 拼接容易把表格切碎导致合并单元格跨越多张图标签语义被破坏。yolo detect train \ modelyolov8s.pt datadata.yaml \ imgsz1024 batch8 epochs150 \ mosaic0.5 close_mosaic10这里mosaic0.5表示后文 Mosaic 使用概率 0.5close_mosaic10表示最后 10 轮关闭 Mosaic。表格文献里靠近训练结束阶段关闭 Mosaic 可以稳定 final 精度。如果你想显式增加 minority class 的loss 权重ultralytics 没有现成参数我建议不动 loss把注意力放到后处理上。4. 重建表格结构行列网格与合并单元格后处理4.1 为什么不能直接用检测框输出表格模型推理得到的是一堆边界框坐标不是表格。要把这些框转换成 HTML 表格或 Excel 表格必须做结构重建。最简单的方法是取所有 column 框的左边界和右边界作为列线取所有 row 框的上边界和下边界作为行线列线和行线的所有交点构成网格。之后每个网格单元就是一个候选单元格。这个流程遇到第一个问题是检测框有冗余。同一个列可能被输出两个重叠框由于没有后处理边界会乱。先做一次同类别的 NMS 和边界合并是必须的。4.2 合并相似框并精炼边界我一般会写一个专门用于结构重建的合并函数而不是直接用 ultralytics 内置 NMSdef 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_thres0.4): boxes sorted(boxes, keylambda b: (b[2]-b[0])*(b[3]-b[1]), reverseTrue) 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[i1]col_edges[j]到col_edges[j1]围成的矩形。然后遍历所有 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), dtypeint) 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[c1] cell_y1 row_edges[r]; cell_y2 row_edges[r1] 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 border1 tr td rowspan2合并单元格A/td td colspan2合并单元格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 \ modeltable_structure/yolov8s_1024/weights/best.pt \ datadata.yaml \ imgsz1024输出里除了mAP50和mAP50-95还有每个类别的单独指标。重点关注table spanning cell一行的 recall。由于这个类样本少模型常常表现为 precision 高、recall 低——有 70% 概率把真正的合并框找出来但 30% 会漏掉。如果 recall 低于 0.5先去检查标注框是否过小或过短而不是急着加模型宽度。5.2 用预测结果做可视化对比数值指标只能说明整体水平表格检测这种强结构任务必须看图。执行预测并输出标签yolo detect predict \ modeltable_structure/yolov8s_1024/weights/best.pt \ sourcevalid/images \ save_txtTrue \ save_confTrue预测结果会写到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 2093YOLO 训练时默认不保持长宽比直接 resize 到 1024x1024细线会被压断。解决方案是在 data.yaml 里设置augment: false配合手动预处理保持长边或者用rectTrue参数走矩形训练yolo detect train ... rectTrue第三个坑是类别索引错位。Roboflow 导出的 YOLO 标签从 0 开始但有些脚本或手改的 label 从 1 开始导致整体类别错一位。训练前打印一下所有 txt 里 class_id 的最大值和 names 数量做比对只花一分钟能省下整晚的排错时间。先把预测可视化这一步做扎实再去动训练参数——这样你看到的每一次精度变化都是真实结构差异的反映而不是随机噪声。本文还有配套的精品资源点击获取