700张VOC皮带破损数据集:从标注到YOLO训练的工业视觉落地指南
简介面向工业视觉检测与输送带智能运维场景提供一套基于PASCAL VOC格式标注的传送带皮带破损图像数据集适合算法研究人员及个人学习者用于缺陷检测模型训练。压缩包共1682个文件大小68.16MB包含700张皮带损伤jpg原图、700个xml标注文件以及zbak备份和txt说明文件jpg图片提供原始视觉输入xml精准标记裂纹、撕裂等缺陷区域的边界框与类别可直接配合YOLO、Faster R-CNN等检测框架使用。数据按7:2:1划分为训练集、验证集与测试集保证模型训练、调参和泛化评估的规范性。目前已有140人浏览学习。借助该数据集可以完成输送带表面损伤从标注到检测的全流程实验减少自行采集图像和人工标注的时间成本也可为工业实时监测、设备预测性维护等智能制造应用提供实验支撑。资源来源于网络分享仅供学习交流请勿商业使用。1. 700张VOC破损数据集做皮带视觉检测前先想清楚它到底能帮你回答什么传送带皮带在煤矿、港口、水泥厂里直接决定产线能不能连续跑而皮带破损导致的非计划停机损失往往远大于一条皮带本身的价格。“基于VOC格式的700张传送带皮带破损检测数据集”这类项目听起来朴素实际指向的却是一个非常具体的落地需求用深度学习目标检测模型在皮带表面找到裂纹、撕裂、孔洞这些损伤再决定是紧急停车还是安排检修。VOC格式是这个数据集承载标注信息的容器700张是它的规模边界破损检测是它要服务的任务。这套数据适合三类人。刚入门想做工业视觉落地的算法工程师想验证YOLOv8在自己小样本工业缺陷数据上能跑到什么程度的在校研究生以及负责输送带巡检方案选型的现场工程师。它回答的问题不是“模型能不能在实验室里复现”而是“700张的真实工业图像从标注、格式转换到训练最少要走完哪几步每一步最容易在哪里翻车”。接下来的内容就沿着这条链路展开。2. VOC格式装载传送带破损数据目录结构、XML字段与700张的容量边界2.1 为什么选VOC而不是COCO或DOTA与工业小样本的匹配度VOC格式之所以是这类带标注小数据集最常见的载体不只是因为历史惯性而是它在“人可读、易修改、训练链顺”这三件事上平衡得最好。COCO的标注通常是一个巨大的JSON文件一张图的所有信息挤在数组里漏一个逗号整个文件解析失败对于只有几百张图像的工业数据手写JSON几乎不可能靠肉眼复查。DOTA格式是为遥感旋转框设计的像传送带破损这种目标虽然有方向性但工业现场第一版检测基本都用正矩形框旋转框在标注成本、模型复杂度、后处理上都更重反而是用mmrotate这类工具训练DOTA数据集的进阶玩法才有优势。VOC的XML是树状结构每个object独立成节点标注软件可以逐条写入人也可以直接打开改坐标。更重要的是不管后续是转YOLO的txt还是转COCO的JSONVOC都处在一个“信息最完整”的位置它同时保留绝对像素坐标、图片尺寸、类别名和difficult标记转换做错了能回查原始标注。700张这种量级的数据修改成本比存储成本更关键选VOC最稳。2.2 用一条命令还原标准VOC目录JPEGImages、Annotations 与 ImageSets拿到这类数据集先别急着训练第一步是把目录结构摆对。常见的做法是建一个VOCdevkit根目录里面按VOC2007的组织方式拆分。下面这段命令可以在Linux或macOS下直接执行mkdir -p VOCdevkit/VOC2007/JPEGImages mkdir -p VOCdevkit/VOC2007/Annotations mkdir -p VOCdevkit/VOC2007/ImageSets/Main tree VOCdevkitJPEGImages目录放原始图片文件名一般不超过32个字符且不带空格避免后续脚本转义出错Annotations目录放与图片同名的XML标注文件文件名必须一一对应后缀差异会导致训练时找不到标签ImageSets/Main目录下放train.txt、val.txt、trainval.txt这些划分文件每行是一个不带后缀的图片名。这里要强调一个容易被忽略的细节VOC原本还有Labels目录用于放分割任务数据目标检测用不到不需要创建。目录名的大小写也要严格保持JPEGImages里的JPEG全大写ImageSets里的Sets首字母大写Linux系统下大小写敏感写错一个字母后面所有路径拼接都会报错。数据量小的时候这些问题不明显等自动化脚本跑起来才发现目录名对不上回头改路径才是真浪费时间。2.3 XML主体字段拆解bndbox 与 difficult 如何影响训练VOC的XML里真正决定训练效果的字段其实就几个。一份标准的标注文件结构如下annotation folderJPEGImages/folder filenamebelt_20240512_081.jpg/filename size width1920/width height1080/height depth3/depth /size object nametear/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin842/xmin ymin366/ymin xmax921/xmax ymax401/ymax /bndbox /object /annotationsize里的width和height是转换脚本的参考基准但也只是参考后面我会讲到为什么不能完全信任它。object里的name是类别名破损检测通常只有一类但数据集里如果区分了tear和hole两个子类这里的名字决定了最终模型的类别数。bndbox是目标检测最关键的字段四个值是像素级绝对坐标xmin、ymin是左上角xmax、ymax是右下角后续做VOC转YOLO格式时就是靠这四个值算归一化中心坐标和宽高。difficult这个字段在Pascal VOC时代表示“难例”转换脚本通常会直接跳过因为difficult1的样本可能是严重遮挡或雾化严重的区域硬喂给模型容易让梯度震荡。truncated表示目标是否被图像边界截断工业视频帧里皮带破损经常出现在画面边缘这类框是否参与训练要看具体场景我的建议是第一版全部保留等漏检分析时再单独看边缘样本的贡献。2.4 规模边界700张的标注工作量与训练含义700张图对目标检测来说属于小样本。按每张图1到2个破损目标估算正样本总量大约在700到1400个之间而检测头需要学习的特征空间远比这个数大。手工标注一张工业图平均要3到7分钟遇到传送带表面纹理复杂、裂纹不清晰的图标注员还需要对照原图反复确认边界700张图至少需要2到3个工作日这还不算复查和修正的时间。这个规模决定了训练策略必须走迁移学习。从零初始化权重训练一个YOLO检测头几百个正样本几乎必然过拟合用ImageNet或COCO上预训练过的模型权重做微调才能让backbone已经学到的基础纹理特征迁移到皮带破损上。另外700张的拍摄来源也很重要如果这些图全是从同一条输送带的同一台相机拍的模型的泛化能力会非常有限换一条皮带、换一种光照可能就失效。这个边界在拿到数据的第一天就要想清楚。3. 手工标注与批量检查用 labelImg 把传送带破损框成 VOC XML3.1 labelImg 的安装与 PascalVOC 模式设置把原始图像变成VOC标注实际动手时最常用的工具还是labelImg它够简单、出XML稳定不需要引入复杂的在线标注平台。安装和启动的命令是pip install labelImg labelImg启动前先确认界面右侧的标注模式是PascalVOClabelImg默认用这个模式但也有YOLO模式可以直接存txt。第一版标注我建议坚持存成VOC XML原因很简单XML保存的是绝对像素坐标万一标注工具在归一化环节有bug原始坐标还在能补救直接存txt一旦坐标换算错了找回原始信息的成本就高得多。进入标注后需要设置的几个关键项默认输出目录指向Annotations图片目录指向JPEGImages。标注时按W创建一个矩形框把破损本体框进去后在弹出的对话框里输入类别名。按A和D切换上一张、下一张图按CtrlS保存当前XML。还有一个容易被新手忽略的点labelImg左侧有个Advanced Mode勾选项勾上后才会显示自动保存和单类别模式。做单类别破损检测时打开单类别模式能省掉每框一次输入类别名的重复操作。3.2 标框规范什么算一个合格的“破损”对象标注质量的统一性比数量更重要尤其是只有700张的小数据集同一个破损区域在不同标注员手里画出的框如果差异很大模型会学到互相矛盾的边界。我按三套规则约束标注行为。第一框住破损本体不把背景阴影、油渍、托辊压痕和皮带接缝框进来。传送带表面常年有矿粉、水渍和机械摩擦痕迹这些区域颜色深、纹理杂乱容易被误认为破损。标注现场如果发现某块区域“像破损但不确定”宁可标出来在difficult里置1也比把它当背景漏掉强。第二多个破损距离很近时取决于连通性而非视觉数量。一条裂纹延伸出两条分支算一个对象框要覆盖整体外轮廓两处独立破损中间隔着完好的皮带表面即使距离只有几个像素也要分成两个框。这个规则的目的是避免框里同时包含目标和大量背景背景比例过高会稀释正样本特征。第三破损被图像边界截断时标出可见部分truncated置1。工业现场的飞拍图像经常拍到皮带边缘的损伤被切掉半边这种框对模型学习有噪声但保留了目标的位置信息。这里也顺带提一句采集传送带运行状态下用飞拍取图相机的快门速度要压得住带速否则图像运动模糊标注员连破损边界都看不出来后面所有工作都是白做。3.3 批量检查XML用Python找出漏标、尺寸异常与路径错误手工标注完成后不能直接拿去训练第一步是批量体检。最常踩的三类错误XML里filename指向的图片不存在、XML记录的尺寸和真实图片不一致、整张图没有任何object。下面这段脚本可以快速扫一遍import os import xml.etree.ElementTree as ET from PIL import Image ann_dir Annotations img_dir JPEGImages for xml_name in os.listdir(ann_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(ann_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text img_path os.path.join(img_dir, filename) if not os.path.exists(img_path): print(f[路径错误] {xml_name} 中引用的 {filename} 不存在) continue real_w, real_h Image.open(img_path).size size root.find(size) xml_w int(size.find(width).text) xml_h int(size.find(height).text) if (xml_w, xml_h) ! (real_w, real_h): print(f[尺寸不一致] {xml_name}: XML{xml_w}x{xml_h}, 实际{real_w}x{real_h}) obj_count 0 for obj in root.iter(object): box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) if xmin 0 or ymin 0 or xmax real_w or ymax real_h: print(f[越界] {xml_name} 中 {obj.find(name).text} 超出图像边界) obj_count 1 if obj_count 0: print(f[空标注] {xml_name} 没有任何object)这段脚本的思路是把XML当作文本树逐节点读取绕过任何转换逻辑直接检查原始数据。尺寸不一致是转换脚本计算归一化坐标时最常见的错误来源越界框则可能是标注时手滑拖出了图像范围。跑完一遍后把输出结果按错误类型分类先修路径问题再修尺寸问题最后处理越界框。空标注的XML如果确认是漏标就直接删掉对应图片如果确认是背景样本就保留图片但不生成标签。4. 从VOC到YOLO训练转换脚本、数据集划分与类别平衡参数4.1 为什么训练时要把VOC转成YOLO txt格式市面上主流YOLO训练框架无论是ultralytics还是darknet的分支版本默认的标签格式都是txt每张图对应一个同名txt文件每一行由class_id加归一化的中心点坐标、宽度、高度组成。VOC的XML虽然信息完整但解析XML本身有开销训练时每读一张图就要走一遍XML解析数据量小时无所谓调参阶段反复跑几百个epoch就会感受到IO压力。更重要的是归一化坐标本身就是模型输出的目标形式。YOLO系模型的预测结果是相对于网格的偏移量和缩放量训练标签必须预先归一化到0到1之间。如果不做VOC转YOLO这一步每次训练前都要在加载器里临时算出错时定位困难。提前转换出一个干净的labels目录训练配置、后续验证、模型导出都基于同一套坐标体系排错路径最短。4.2 voc2yolo 转换脚本不信任XML里的size转换脚本的核心是把bndbox的绝对坐标换算成归一化的中心点坐标和宽高。我在这个脚本里刻意做了一件事不读取XML里的size字段而是用cv2直接从图片文件读取真实宽高。import os import cv2 import xml.etree.ElementTree as ET IMG_DIR JPEGImages ANN_DIR Annotations OUT_DIR labels os.makedirs(OUT_DIR, exist_okTrue) classes [] # 类别名列表保持顺序与训练配置一致 def xml_to_yolo_one(xml_path): tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text img cv2.imread(os.path.join(IMG_DIR, filename)) if img is None: print(f[警告] 无法读取图像 {filename}) return h, w img.shape[:2] lines [] for obj in root.iter(object): name obj.find(name).text if name not in classes: classes.append(name) difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: 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) # 边界裁剪防止标注越界导致负坐标或大于1的归一化值 xmin max(xmin, 0) ymin max(ymin, 0) xmax min(xmax, w) ymax min(ymax, h) cx ((xmin xmax) / 2) / w cy ((ymin ymax) / 2) / h bw (xmax - xmin) / w bh (ymax - ymin) / h class_id classes.index(name) lines.append(f{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(OUT_DIR, out_name), w) as f: f.write(\n.join(lines) \n) for xml_name in os.listdir(ANN_DIR): if xml_name.endswith(.xml): xml_to_yolo_one(os.path.join(ANN_DIR, xml_name)) with open(classes.txt, w) as f: f.write(\n.join(classes))逻辑说明有三处。第一真实宽高的读取放在每次转换时做而不是在脚本开头读一次因为700张图里完全可能出现一两张图片被误压缩过XML里的尺寸和实际文件不一致动态读取能暴露这个问题。第二边界裁剪用的是max和min钳制把越界坐标强行拉回图像范围内。如果XML里的坐标超出原图边界一两百像素裁剪后虽然损失了一部分真实区域但比让归一化值大于1导致训练崩溃要好。第三difficult字段要处理缺失情况不是所有标注工具都会写入这个字段直接取int可能抛异常所以先判断是否为None。这个脚本还顺手生成了classes.txt一行一个类别名。类别的顺序决定了训练时class_id的编号比如classes.txt第一行是tear那tear在训练配置里就必须是0这个顺序是敏感的后面修改类别名或增删类别后旧的txt标签编号也会变必须重新执行转换脚本不能用同一个labels目录硬扛。4.3 划分train/val并统计类别分布分的是“皮带”而不是“图片”小数据集划分里的一个经典错误是随机分图片。传送带的破损往往在连续多帧里反复出现同一处裂纹在第100帧和第101帧的画面几乎一样随机划分会把这两帧分到训练集和验证集两边验证集的mAP自然好看但换到一条新皮带时模型立刻现出原形。正确做法是按皮带编号分组划分。import os import random from collections import Counter labels_dir labels files [f for f in os.listdir(labels_dir) if f.endswith(.txt)] # 假设文件名格式为 belt01_001.jpg - belt01 是皮带编号 def get_belt_id(filename): return os.path.basename(filename).split(_)[0] groups {} for f in files: belt_id get_belt_id(f) groups.setdefault(belt_id, []).append(f) val_ratio 0.2 val_count max(1, int(len(groups) * val_ratio)) val_belt_ids set(random.sample(list(groups.keys()), val_count)) train_files, val_files [], [] for belt_id, fs in groups.items(): if belt_id in val_belt_ids: val_files.extend(fs) else: train_files.extend(fs) def count_classes(file_list): counter Counter() for f in file_list: with open(os.path.join(labels_dir, f), r) as fh: for line in fh: class_id line.split()[0] counter[class_id] 1 return counter train_counter count_classes(train_files) val_counter count_classes(val_files) print(训练集图片数:, len(train_files), 验证集图片数:, len(val_files)) print(训练集类别分布:, dict(train_counter)) print(验证集类别分布:, dict(val_counter))这里的group逻辑是按文件名前缀的belt_id分组前提是数据集的命名带皮带编号信息。如果原始文件名是纯数字序列建议在标注前就重命名把皮带编号或拍摄批次写进文件名这样后续的划分、统计、可视化都能挂上现场信息。训练集和验证集的图片量比例按8比2是常规操作但不要只追求比例要先把分组结果打印出来看一眼验证集里破损框的数量如果低于50个评估出的mAP波动会非常大此时应该增大验证集比例或改用K折交叉验证。5. 700张小样本的5条踩坑记录传送带破损检测训练排障700张的小样本工业检测训练中遇到的绝大多数问题不是模型结构选错而是数据准备阶段埋下的雷。下面五条是我反复踩过的坑按现象、原因、解决展开能帮你省掉不少来回试错的时间。5.1 坑一loss持续下降验证mAP却纹丝不动现象是训练到第50个epoch时训练集loss一路走低验证集mAP0.5却卡在0.2附近不动看起来像是模型欠拟合实际是标注质量有问题。原因大多是标注员把传送带表面的深色阴影、油渍、锈迹当成了破损框进去模型学到的是“深色破损”的颜色关联一旦验证集里出现同样颜色但完好的区域预测框就乱套。工业图像里光照不均和设备油污非常严重这种标注噪声会被模型当成真理学进去。解决方法是停下来做一次标注质量复查把每个XML的标注框画回原图导出成一张拼接图人工过一遍。用Python循环读取每张图片和对应XML用OpenCV的rectangle画框并保存到一个check目录重点看两类样本框面积明显大于破损本体的以及框只盖住破损一部分的。修正这些标注之后重新训练验证mAP往往能回归到0.5以上。这个复查动作要放在训练之前做不要等模型跑完一轮才回头看数据。5.2 坑二图像被resize后XML没有同步更新坐标全部漂移现场原始图像尺寸往往是5472乘3648为了训练效率缩到1280但XML里记录的size和bndbox还是原图的尺寸。转换脚本如果直接读取XML里的size计算归一化坐标计算结果会整体偏移且偏移量和图像缩放比例成正比。这类错误最坑的地方在于训练过程完全不报错loss也能正常下降模型输出框的位置却系统性偏差。解决的关键是转换脚本里一律用cv2.imread读取转换时所用的实际图片宽高XML里的size只用于校验不直接参与计算。更深一层的建议是训练用的图片复制一份resize后的版本单独建一个train_images目录XML标注也基于这份resize后的图片重新生成一套。图像尺寸、标注坐标、模型输入尺寸三者必须同源任何一环各自缩放都会让坐标系错乱。质检时如果发现所有预测框都整体向右下角偏移优先检查这个坑。5.3 坑三破损只占图像面积0.3%小目标漏检严重传送带宽度1.4米相机横向覆盖2米一条3厘米的裂缝在整张原图里只占几十个像素直接用默认的640输入尺寸训练模型对这类小目标几乎无感。根本原因是YOLO系模型经过多次下采样后浅层特征图上的小目标信息衰减严重而小样本数据集又放大了这个问题的敏感性。700张图里如果大部分是这种远景画面破损区域的标注框面积占整图比例经常小于1%。解决思路有两个方向。一是把输入分辨率从640提高到1280甚至1536让破损目标在输入图像中占据更多像素代价是显存占用和训练时间翻倍。二是更推荐的思路把原图按固定步长裁剪成512或640的patch图裁剪后的图像单独标注、单独训练小破损在patch里的相对尺寸会大好几倍。传送带表面纹理本身就是重复结构裁剪不会破坏语义信息这种方案对显存的需求也更温和。5.4 坑四验证集划分不分组mAP虚高出假象验证集mAP0.5高达0.85部署到现场却频繁漏检这是划分方式埋的雷。随意按图片20%随机抽分验证集会让连续帧中画面高度相似的图像同时出现在训练集和验证集里同一处裂纹的形态在两边几乎一模一样验证集实质上是在背答案。工业检测里这个问题比公开数据集更严重因为现场拍摄的图像往往来自同一批次的连续帧。解决方法是按皮带编号或拍摄时间分组划分同一条皮带的所有帧要么全部进训练集要么全部进验证集。这样验证集代表的才是“没见过的皮带表面”评估出的mAP才有迁移参考价值。设计划分脚本时group字段的粒度要按场景定如果同一个皮带编号下的图像是在同一天同一光照条件下拍的可参考价值也有限更稳妥的做法是按拍摄批次或按不同产线划分。5.5 坑五沿用默认Mosaic增强小目标被拼图尺寸吞掉用ultralytics默认参数训练训练和验证指标都正常实际部署后对细小裂纹的漏检却明显增多。默认训练配置里Mosaic增强把四张图拼成一张所有目标都要再缩放一遍原本就只占几十像素的小破损在拼图后可能只剩几个像素模型在大量极端小目标里学会了把它们当背景忽略。对小目标检测任务Mosaic并非总是正向增益。解决方法是训练后期关闭Mosaic比如在最后20轮设置mosaic0只保留随机平移、翻转这类轻微的几何增强。如果任务对速度和显存不敏感改成随机裁剪加resize的方式破损目标在增强后能保持合理尺寸对小目标检测更有利。这个参数的调整要配合验证集提前观察不要等模型训练完才在测试集上发现。6. 让700张发挥出2000张的效果增强参数、迁移学习与小目标验证6.1 增强参数起点用ultralytics训练时我把增强参数从默认值改成了下面这组作为小目标破损检测的起点参数建议值理由mosaic0.5最后20轮改为0保留拼接增强但降低强度末期关闭避免小目标失真hsv_h0.015轻微色相扰动模拟不同光照translate0.1小幅平移不破坏破损与皮带纹理的相对位置fliplr0.5水平翻转皮带纹理对称增强安全scale0.5缩放幅度过大容易让破损变成噪点收小一点这些参数不是拍脑袋定的。传送带破损检测和通用目标检测不同数据里目标小、背景模式重复、光照变化剧烈增强幅度过大容易破坏“破损在皮带纹理上的位置关系”。用过大的缩放和裁剪模型反而学到错误的空间关联。6.2 迁移学习与冻结层策略700张数据不能从零训练。常见做法是加载COCO预训练的yolov8s权重先冻结前10层的backbone参数只训练检测头和neck部分等loss趋于平稳后再解除冻结、用更低的学习率微调全部参数。训练命令大致是yolo detect train datadataset.yaml modelyolov8s.pt \ epochs100 imgsz1280 batch8 freeze10 \ mosaic0.5 patience20这个命令里imgsz用了1280而不是默认640原因是前面提到的小目标问题在小样本情况下输入分辨率对破损检出率的影响比模型宽度更明显。freeze10表示冻结前10层冻结的层数要按实际模型结构调整层数太多欠拟合太少则预训练信息被快速覆盖。6.3 验证习惯mAP之外还要看预测框落到哪里700张数据的最终评估不能只看验证集mAP一个数字。我现在的习惯是训练结束后跑一次验证集可视化把预测框和真实框画在同一张图上按置信度从高到低排序看错误类型。破损检测最容易出现的错误是置信度很高的误检框落到皮带边缘的托辊上或落到矿粉堆积处这些错误在mAP里被平均掉但在可视化图里一眼就能看出来。检查PR曲线时如果召回率在置信度阈值0.3附近急剧下降说明有小部分破损正样本的预测置信度分布异常低通常对应的是标注时框得不完整的那几张图。工业场景里宁可把置信度阈值设低、增加人工复核也不能漏检现场的真实破损。做完这些检查一个700张的破损检测数据集才算是真正被用透了。这个检查步骤落地之后换到其他小样本缺陷检测项目时我也会先把这五类坑快速过一遍再开始调参。希望帮到你。本文还有配套的精品资源点击获取