尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VOC转YOLO实战:钢筋计数数据集的密集小目标检测训练指南

VOC转YOLO实战:钢筋计数数据集的密集小目标检测训练指南 简介这组VOC格式标注文件面向钢筋计数与智能盘点场景供计算机视觉算法工程师、深度学习研究者及相关专业学生用于钢筋目标检测与计数模型的训练与验证。压缩包内共568个xml标注文件打包后仅1.07MBrar格式便于快速下载与本地部署。标注内容以工地钢筋场景的边界框信息为主可直接转换为YOLO、Faster R-CNN等主流检测框架所需格式也可编写脚本转成COCO或TFRecord形式是钢筋计数算法开发、模型微调、数据增强及精度评估环节的关键基础语料。已有659人学习下载作者在关联博客中提供图片质量预览读者可先查看实拍图像效果结合自身应用场景判断适配度再决定下载使用。1. 钢筋计数数据集密集小目标标注为什么值得单独收集做工地物资盘点、钢筋加工厂智能管理的人大概率被“数钢筋”这件事折磨过一捆几十上百根端面紧密排列光线一差连人眼都会数错。这里要拆的这套人工智能钢筋计数数据集就是针对这类痛点做成的一套VOC格式标注资产。训练集图片都带xml边界框标注测试集有意不给标签逼着你把目标检测的最后一公里落到“总数对得上”而不是只看框画得准。对于正在做钢筋盘点、建材计数算法选型或者想拿一个真实工业小目标场景练手的人这套数据比动辄几十万的通用数据集更有参考价值。人工智能正从尝鲜工具变成日常帮手密集目标计数恰好是落地价值最直接的一类任务值得把每个标注字段都抠明白。2. VOC格式解读先用十分钟看透标注文件2.1 四个必须记住的XML节点这套资源的实体是成百上千个.xml文件文件名是一长串十六进制字符串别被吓到那只是防止重名的随机编号。我拿到手后的第一件事不是急着转训练格式而是随机拆开一个 XML确认标注到底标了什么、坐标怎么组织。VOC 格式里真正会影响落地的节点就四个其余基本都是冗余信息。annotation folderimages/folder filename33DDB09455AB4E1CA72B21ADFBBC30A2.jpg/filename size width1920/width height1080/height depth3/depth /size object namerebar/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin164/xmin ymin512/ymin xmax238/xmax ymax602/ymax /bndbox /object /annotation代码块里的namerebar/name是钢筋类别名有些版本的钢筋数据集会把类名写成bar或steel解析时先打印一遍别写死在代码里。size节点记录的是图片真实宽高后面做坐标归一化时用到的就是这两个值。bndbox里四个坐标是左上角和右下角的像素位置x 轴向右、y 轴向下这与 OpenCV 的画图坐标系一致直接用即可。最后object节点的数量就是这张图里的钢筋根数一根一根对过去你就能发现数据集在标注密度上卡在什么水平。这套数据集里一张 1920×1080 的图通常会标几十甚至上百个 objectXML 文件体积并不小。解析时最忌讳的做法是把整个文件一次性读进内存再用正则硬抠正确做法是用自带的 ElementTree 迭代解析内存占用和数据规模基本线性。下面这段逻辑是我处理 VOC 标注时必跑的体检脚本。2.2 先看预览图再动手拍摄角度决定算法上限摘要里提过下载前可以在参考博客里看图片质量。这一步不要省。我一般会重点观察三个点镜头与钢筋端面的夹角是正对还是侧对光照是均匀还是局部过曝端面是规则的圆形截面还是已经被切割变形。第一个点直接决定你要用水平框 YOLO 还是旋转框检测器第二个点决定要不要在训练管线里加颜色扰动第三个点决定目标在端面尺度上的可区分度。钢筋计数用水平框通常够了。钢筋虽然是长条状但计数任务看的是端面端面近似圆形或椭圆形水平框的紧致度尚可。你要是看到预览里大量钢筋互相遮挡、端面重叠那水平框会很难受需要考虑分割或者密度回归路线。我在数据选型时给团队的建议是先花十分钟看 20 张预览图再决定是否下载因为标注质量比数量更影响训练曲线。如果下载包里只有 XML 标注文件没有对应 JPG那使用时要自己把图片和 XML 放到同一级目录并以 XML 的filename与图片文件名一一对应。文件名不一致是最常见的翻车源头后面避坑章节会展开。2.3 批量解析XML标注合法性检查跑训练之前我习惯先用一个统计脚本把整批标注的“健康度”扫一遍重点检查三件事有没有空标注的图、有没有坐标越界或宽高为零的框、单图目标数分布是否合理。import xml.etree.ElementTree as ET import glob from collections import Counter xml_files glob.glob(train_labels/*.xml) per_image_counts [] box_sizes [] errors [] for xml_path in xml_files: try: tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) objs root.findall(object) per_image_counts.append(len(objs)) for obj in objs: bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) w xmax - xmin h ymax - ymin box_sizes.append((w, h)) if xmin 0 or ymin 0 or xmax img_w or ymax img_h: errors.append((xml_path, coords out of range)) if w 0 or h 0: errors.append((xml_path, zero width/height)) except Exception as e: errors.append((xml_path, str(e))) print(图片总数:, len(xml_files)) print(单图目标数 分布Top5:, Counter(per_image_counts).most_common(5)) print(平均框宽高:, sum(s[0] for s in box_sizes)/len(box_sizes), sum(s[1] for s in box_sizes)/len(box_sizes)) print(异常标注数量:, len(errors)) for err in errors[:10]: print(err)这个脚本的价值在于把玄学问题变成数据问题。img_w和img_h是从 XML 的 size 节点读取的如果 xml 与图片实际尺寸不一致脚本里要先对图片做一次cv2.imread校准否则坐标范围检查没有意义。per_image_counts的分布如果出现大量“单图只有 5 个以内”的图片说明标注策略可能偏向稀疏场景训练出来的模型在密集场景会明显力不从心。反过来如果单图目标数集中在 50150那么训练时输入分辨率就要往高里走否则下采样后一个目标只剩几个像素谁也认不出来。3. VOC转YOLO转换脚本与三个容易出错的归一化细节3.1 为什么要转成YOLO格式VOC 格式谈不上错但在实际训练里YOLO 系列的官方仓库默认吃的是每行一个目标的 txt数据加载器读取路径、mosaic 增强、anchor 自动计算都基于这个格式。与其在训练脚本里写自定义 Dataset 去解析 XML不如花五分钟把标注统一转成 txt后续换模型、换框架都不用再碰标注文件。圈里有人喜欢硬啃 VOC 直接训也能跑通但一旦要开 mosaic 或者调数据增强就会卡在格式适配层属于纯浪费时间。转换的核心是坐标归一化。VOC 里是绝对值YOLO 要求的是相对图片宽高的比例x_center、y_center、w、h。四个值都在 0 到 1 之间这也是后续最容易出边界坑的地方。3.2 XML转TXT完整脚本import xml.etree.ElementTree as ET import os def voc2yolo(xml_path, output_dir, category_id0): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) out_lines [] for obj in root.findall(object): name obj.find(name).text # 钢筋数据集通常只有一个类若有多个类别按映射表改 category_id class_id category_id bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h out_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) base os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(output_dir, base .txt), w) as f: f.write(\n.join(out_lines)) if __name__ __main__: os.makedirs(yolo_labels, exist_okTrue) for xml_name in os.listdir(train_labels): if xml_name.endswith(.xml): voc2yolo(os.path.join(train_labels, xml_name), yolo_labels)这段代码逻辑简单但有两个参数需要留意。category_id默认是 0对应 YOLO 体系里的第一个类如果你的数据集里钢筋是唯一类别0 没问题如果后续加入“损坏钢筋”等第二类就得写一个字典做名称到 id 的映射。另外out_lines用.6f做了六位小数保留这对 1920×1080 图片足够了但如果你做的是千万像素级别的超大图建议保留八位否则归一化误差会被放大。输出文件名直接复用了 XML 的文件名也就是那串十六进制 ID这样与图片文件名天然对齐。转换后要检查标签文件数量是否与 XML 数量一致别在循环里因为某个 XML 解析失败而静默跳过最好加一层 try-except 并把失败文件名打印出来。3.3 归一化值域检查与常见错误转换完别急着训练先跑一段数值体检。YOLO 格式下所有坐标值合法范围是 [0, 1]而且需要满足x_center w/2 1、y_center h/2 1两个约束否则目标中心跑到图外。常规做法是拿一个脚本扫一遍生成的 txt把所有大于 1 或者小于 0 的行打出来再追回到原始 XML 定位问题。最容易写反的是除以宽还是除以高。x 方向除以图片宽度y 方向除以图片高度这俩一旦互换所有框都会偏斜。我见过有人为了省事直接把x_center除以img_h结果训练出来所有预测框像贴在一条斜线上损失曲线还正常下降特别具有迷惑性。圈子里把这种问题叫“可怕的黑匣子”——loss 在降模型在学但学的是错误的坐标空间。所以脚本里建议在每行写入前做一次断言assert 0 x_center 1 and 0 y_center 1 assert 0 w 1 and 0 h 1另一个坑是关于图片是否已经做过缩放或裁剪。很多工业数据集的原始采集图经过预处理后被裁掉一部分但 XML 没有同步更新导致标注框超出裁剪后的图像范围。转换脚本只管读 XML 和 size 节点无法发现这类错位必须在转换前遍历一次图片与 XML 的尺寸对应关系。尺寸不一致的样本直接踢出去数量不多时不值得迁就。4. 训练钢筋计数模型YOLOv8 参数与面向密集目标的预处理4.1 划分训练集与验证集的目录编排钢筋计数数据的目录结构建议一开始就按 YOLO 惯例铺好避免训练到一半再挪文件。mkdir -p datasets/Rebar/{images/{train,val},labels/{train,val}}569 张训练标注图按接近 9:1 的比例划分即可。我一般会固定随机种子做划分保证每次实验的验证集完全一致不然对比实验时同样的代码跑两次结果不同很难判断是模型改进还是数据划分漂移。Python 脚本里用random.seed(42)再做 shuffle把划分结果保存成一份 txt 清单后续重跑实验直接用这份清单连文件复制都省了。85 张未标注测试集的用途要提前想清楚。它没有标签意味着你不能用它做本地验证只能当作“最终考试”。常见做法是先用训练集划分出的验证集把模型调好最后用训练好的模型在这 85 张上预测并仅从可视化角度人工检查计数合理性。如果你做企业项目这 85 张还承担了给甲方演示“新场景可用性”的责任所以模型泛化能力比验证集分数更重要。4.2 密集小目标的增强策略mosaic、随机裁剪与亮度扰动569 张图对深度学习来说不算充裕尤其目标是密集小物体模型很容易过拟合到特定排列方式。数据增强是这块的重头。YOLOv8 的 mosaic 增强默认开启语法上只需在配置里指定概率即可真正需要动脑筋的是输入分辨率和随机裁剪策略。# rebar_aug.yaml 片段 mosaic: 1.0 mixup: 0.2 copy_paste: 0.3 degrees: 5.0 translate: 0.1 scale: 0.2 fliplr: 0.5 hsv_h: 0.02 hsv_s: 0.5 hsv_v: 0.4degrees只给了 5 度钢筋端面不希望你做大幅旋转旋转过多会让目标形状变得不真实。copy_paste是我比较推荐开启的项它能把一张图里的钢筋实例“复制粘贴”到另一张图上对密集计数任务相当于变相扩充样本数。这个增强在实例分割场景用得更多但检测框场景同样有效只要粘贴时避开已经存在的目标位置即可。hsv_v给了 0.4亮度扰动幅度稍大原因是钢筋端面在不同光照下明暗差异极大模型需要适应这种拍摄环境差异。有些同学喜欢用大尺度随机裁剪来模拟特写镜头这在钢筋场景要谨慎。裁剪会把密集上下文切断模型学到的是“局部几个钢筋”而不是“一整捆钢筋”推理时反而数漏。相比之下我更推荐保持整图输入把输入分辨率从默认 640 提到 1280让每个小目标有足够像素。4.3 训练参数参考与anchor处理训练命令以 YOLOv8s 为例yolo detect train \ datarebar.yaml \ modelyolov8s.pt \ imgsz1280 \ epochs300 \ batch16 \ patience60 \ optimizerAdamW \ lr00.002 \ close_mosaic15imgsz1280是关键项。钢筋端面在 1080p 原图里可能只占 2040 像素640 输入下采样后只剩 10 像素左右特征图上的信息损失严重。1280 输入会让显存占用翻倍如果 16 batch 放不下可以把 batch 降到 8。close_mosaic15表示最后 15 个 epoch 关闭 mosaic 增强让模型在接近真实分布的数据上收敛避免 mosaic 拼接痕迹影响最终精度。patience60提前停止也建议留下否则 300 epoch 很容易白等。anchor 方面YOLOv8 默认开启自动 anchor 学习。如果你在自己的数据上发现收敛慢可以先统计已转换 txt 里的长宽比分布多数钢筋框的长宽比在 1.02.0 之间非常集中默认 anchor 基本够用。这里不建议照搬 mmrotate 训练 DOTA 那套旋转框流程钢筋计数用水平框更省事数据支持也是最成熟的。参数参考值说明imgsz1280小目标密集场景优先保分辨率batch816按显存决定低于 8 时 BN 不稳定optimizerAdamW收敛快配合低 lr 更稳lr00.002比 SGD 默认值低一档mosaic1.0配合 close_mosaic15 使用5. 钢筋计数踩坑记录四个真实翻车点与补救方案5.1 转换后txt出现负数或大于1的坐标现象转换完扫描 txt发现某些行的 x_center 或 w 落在 [-0.1, 1.1] 这类越界范围训练时模型出现 NaN 损失。原因最常见的是 XML 的 bndbox 坐标与 size 节点不匹配比如一张图片采集后被裁剪或缩放但标注坐标还停留在原图坐标系里。还有一部分原因是标注工具导出时把坐标写成了相对值而 XML 结构里没有标注这是相对值导致解析脚本按像素处理。解决先跑掉 2.3 节里的合法性脚本把报错样本单独拎出来对比原图。优先剔除跨图尺寸不一致的样本如果这类样本超过总量的 5%就需要考虑重新生成标注而不是硬训。转换脚本里的 assert 建议保留到正式训练前不要因为“训练能起来”就删掉。5.2 训练loss正常下降但图片里漏检一多半现象训练损失曲线平滑下降验证集 mAP 也能到 0.5 左右但把预测框可视化到原图上后不少钢筋端面完全没框计数结果比实际少 30% 以上。原因小目标在特征图上的响应本来就弱加上密集排列导致 NMS 阶段相互抑制很多低置信度框被直接压掉。训练集单图目标极多时模型会把“漏检”当作更安全的策略因为漏检不算大损失乱检反而会被惩罚。解决先在验证集上画预测框统计预测框数量与真值数量之比。如果显著小于 1把置信度阈值从 0.25 降到 0.1同时把 NMS 的 IoU 阈值从 0.5 提到 0.7让重叠框更容易被保留下来。另外回到训练参数上把输入分辨率从 640 提到 1280 往往比调任何增强都有效。5.3 预测框大面积重叠计数结果虚高现象输出可视化后同一根钢筋被五六个框层层覆盖统计出来的“预测数量”比真值翻了两三倍根本没法用。原因这正是 IOU 阈值设得过低或 NMS 不适用的典型症状。密集目标天然存在大量高重叠框后处理若只做了朴素 NMS前后两个框 IoU 较小就都会被保留实际上指向同一根钢筋。解决先用统计法看预测框两两 IoU 的分布把 NMS 的 IoU 阈值调高到 0.70.85。如果问题依然明显换 Soft-NMS 或者直接改用 DETR 这类无 NMS 的模型省掉这一层玄学报复。钢筋端面是近似圆形框与框之间重叠度极高标准 NMS 在这个场景向来是重灾区。5.4 验证集分数高换到85张未标注测试集却翻车现象训练和本地验证都表现不错但把模型部署到另一环境采集的图片上漏检和误检同时飙升数量偏差大到不能接受。原因这是典型的数据分布漂移。训练图与测试图可能来自不同工地、不同拍摄距离或不同光照条件钢筋表面颜色和背景差异大模型学到的是训练集的颜色纹理而不是“圆形端面”这个抽象概念。解决出结果前建议把所有训练图片做一次亮度、对比度直方图统计与测试图对比。差异大时在训练管线里加入更激进的颜色增强甚至对钢筋图像做灰度化通道拼接削弱颜色依赖。更稳妥的做法是收集少量目标场景图做微调哪怕几十张效果都明显好过纯调参。6. 用MAE验证计数效果AP之外更要看总数量对没对目标检测的 mAP 在钢筋计数场景里只是一个中间指标业务方真正关心的是“你数的总数准不准”。一张图里有 120 根钢筋你框对了 110 根AP 可能已经不错但盘点就是错了 10 根照样没法交付。因此我最后始终会加一道计数误差验证统计预测框的总数与真实数量的差距用 MAE(Mean Absolute Error) 和 RMSE 来评价模型可用性。import csv def load_count(csv_path): with open(csv_path, r) as f: reader csv.DictReader(f) return {row[image_id]: int(row[count]) for row in reader} preds load_count(pred_counts.csv) gts load_count(gt_counts.csv) errors [] for img_id, true_count in gts.items(): pred_count preds.get(img_id, 0) errors.append(abs(pred_count - true_count)) mae sum(errors) / len(errors) rmse (sum(e * e for e in errors) / len(errors)) ** 0.5 print(fMAE: {mae:.2f} 根, RMSE: {rmse:.2f} 根)这份pred_counts.csv的生成方式很简单把验证集图片过一次推理统计每张图经过 NMS 后最终保留的框数量。注意这里的数量是“最终保留框数”不是推理原始输出框数一定要在后处理之后统计。MAE 以“根”为单位业务上更容易沟通比如 MAE 2.5 根意味着平均每张图数错 2.5 根在百根级别的盘点上就是可接受的误差水平。RMSE 则对少数极端偏差更敏感如果 RMSE 远大于 MAE说明某些图出现整体漏检或重复数问题出在后处理或某些特定光照条件。除了 MAE我还习惯抽查几十张图让算法只输出“预测总数”而不画框让现场人员直接核对总数。这样一个简单的验收动作能快速判断模型在真实场景下是否可用而不是停留在一堆指标里自我感动。从那以后我每次跑钢筋计数项目都会强制走一遍 MAE 校验不只看测试集 mAP如果 MAE 超过 1%我会回头检查 NMS 阈值、输入分辨率以及训练集覆盖的光照条件。这套数据集的标注资产是可复制的模型效果却要每个场景单独调希望帮到你。本文还有配套的精品资源点击获取
返回列表