简介一份用于行李箱表面缺陷检测的图像数据集共包含650张清晰拍摄的行李箱图片采用VOC与YOLO双格式标注可直接用于目标检测模型训练。压缩包内设JPEGImages、Annotations、labels三个文件夹分别保存jpg原图、xml标注文件与txt标签文件三类文件各650个合计1953个文件压缩包大小约25.16MB。标注共分两类damaged损坏与good_condition完好矩形框总数936个其中damaged框199个、good_condition框737个类别划分明确适合训练行李箱破损检测模型。数据集未做增强处理保留了原始场景分布便于使用者按需扩展或进行数据预处理实验。目前已有48人学习下载适用于计算机视觉入门实践、毕业设计及工业质检场景下的缺陷识别任务。1. 行李箱缺陷检测数据集650张图、2类标签、YOLOVOC双格式拿来就能训练做质检类目标检测的同行都有体会工业缺陷样本最难找的往往不是算法而是标注干净的数据。这份行李箱缺陷检测数据集一共650张JPEG图片每张都有对应的VOC格式xml和YOLO格式txt标签分damaged和good_condition两类合计936个矩形框。图片清晰度不错没有做过增强意味着分布相对原始。适合两类人一是刚接触目标检测、想用一套规范双格式数据跑通训练全流程的初学者二是手头有检测项目、需要真实工业样本做预训练或迁移评估的工程师。下面我把压缩包内部结构、格式转换、训练前处理和踩坑点完整拆一遍。2. 拆开压缩包目录结构、标注分布与数据自检2.1 三个文件夹各装什么解压后是JPEGImages、Annotations、labels三个目录分别对应图片、VOC格式xml、YOLO格式txt。这是目标检测数据集最常见的三种组织方式文件命名用xyxr_image加编号三个目录里的文件名一一对应没有额外的子目录嵌套。unzip 数据集-行李箱缺陷检测数据集650张2类YOLOVOC格式.zip -d luggage_defect cd luggage_defect ls -l JPEGImages | wc -l # 650 ls -l Annotations | wc -l # 650 ls -l labels | wc -l # 650数量和目录结构没问题上但实际生产环境里经常遇到文件对不齐的情况所以拿到任何数据集我的习惯都是先跑一遍自检脚本而不是直接开训。2.2 标签分布damaged只有199框接近1:3.7的不平衡类别名称是damaged和good_conditiondamaged框数199good_condition框数737总框数936。按框数算正常样本是缺陷样本的3.7倍存在明显不平衡。这个比例在工业质检里太常见了——产线上绝大多数产品是良品缺陷本身是小概率事件。但直接影响是如果直接训练模型会偏向预测good_condition因为把所有框都判成正常就能拿到约73%的正确率。后面我会讲具体怎么处理这里先记住一个数字199。另外936个框分布在650张图里平均每张1.44个框说明大部分图片要么只有一个框要么干脆没有标注框。如果你的训练框架要求每张图至少有一个目标这批数据里就可能存在空标注图片需要在预处理阶段统计出来。2.3 用脚本核对图、xml、txt三者的一致性文件数量对得上不代表内容对得上。xml里的文件名、尺寸和实际图片是否匹配txt里的类别索引是否在合法范围内这些都要批量检查。import os import xml.etree.ElementTree as ET img_dir JPEGImages xml_dir Annotations txt_dir labels img_names {f.split(.)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)} xml_names {f.split(.)[0] for f in os.listdir(xml_dir) if f.endswith(.xml)} txt_names {f.split(.)[0] for f in os.listdir(txt_dir) if f.endswith(.txt)} missing_xml img_names - xml_names missing_txt img_names - txt_names print(缺xml:, len(missing_xml), 缺txt:, len(missing_txt)) # 抽查10个xml的尺寸是否与图片匹配 from PIL import Image import random for name in random.sample(sorted(img_names), 10): img Image.open(os.path.join(img_dir, name .jpg)) tree ET.parse(os.path.join(xml_dir, name .xml)) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) if (w, h) ! img.size: print(f尺寸不匹配: {name}, xml{w}x{h}, 实际{img.size}) print(抽检完成)这段脚本的核心逻辑是集合差运算快速找出三个目录里对不上的文件。后面那段是抽查xml里记录的width和height与图片实际分辨率是否一致防止源数据标注时用了缩放后的尺寸。注意到这个数据集只有650张全量校验xml也不耗时我一般会直接去掉sample把random.sample换成全量遍历。3. VOC与YOLO互转坐标归一化、类别映射与边界校验3.1 两种格式的本质差异VOC格式用xml存储每个目标用xmin、ymin、xmax、ymax表示左上角和右下角的绝对像素坐标YOLO格式用txt存储每行是“class_id x_center y_center width height”其中中心点坐标和宽高都除以图片宽高做了归一化。区别在于VOC给人看的YOLO给模型吃的。实际训练时不同框架对标注格式要求不同。YOLOv5、YOLOv8系列原生读txt而某些检测框架或开源代码直接读VOC xml。拿到这份双格式数据后大多数时候不需要转换但如果你要自己调整数据、合并其他数据集或者修改类别就必须手工转格式。我一般保留VOC作为“母版”因为xml里信息全转为txt只是算术题反过来要补回xml反而麻烦。3.2 xml转txt的完整脚本与参数说明import os import xml.etree.ElementTree as ET classes [damaged, good_condition] # 必须与数据集标签顺序一致 def convert_xml_to_txt(xml_path, txt_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: print(f未知类别: {name} in {xml_path}) continue class_id classes.index(name) 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) # 归一化并转成中心点宽高格式 x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h # 修正浮点误差导致的越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) width min(width, 1.0) height min(height, 1.0) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) # 批量转换 xml_dir Annotations txt_dir labels img_dir JPEGImages from PIL import Image for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue name xml_name[:-4] img Image.open(os.path.join(img_dir, name .jpg)) w, h img.size convert_xml_to_txt( os.path.join(xml_dir, xml_name), os.path.join(txt_dir, name .txt), w, h )这段代码里最值得注意的不是坐标换算公式而是两个细节。第一classes列表的顺序定义了txt里class_id的编号0对应damaged1对应good_condition一旦顺序错了训练时类别就全乱了。第二xmin和xmax相加除以2再除以宽度如果标注框刚好贴在图片边缘浮点运算可能算出1.0000001这种越界值所以输出前做了clip操作。这种小问题不会让训练崩溃但会让后续的mAP计算出现奇怪误差。3.3 反向转换与坐标合法性检查反过来txt转xml核心是乘回去xmin (x_center - width/2) * img_w。但这里有个坑txt里的坐标被归一化到0到1之间如果原标注框尺寸很小转回整数像素时可能因为四舍五入变成0宽或0高的退化框需要强制检查width和height大于0。def txt_to_voc_bbox(txt_line, img_w, img_h): parts txt_line.strip().split() class_id int(parts[0]) x_center float(parts[1]) y_center float(parts[2]) width float(parts[3]) height float(parts[4]) xmin int((x_center - width / 2) * img_w) ymin int((y_center - height / 2) * img_h) xmax int((x_center width / 2) * img_w) ymax int((y_center height / 2) * img_h) if xmax xmin or ymax ymin: raise ValueError(f非法框: {txt_line}) return xmin, ymin, xmax, ymax这个反向转换在数据合并场景下很有用。比如你要把另一批行李箱图片加进来那边的标注是YOLO格式想统一成VOC再做二次清洗就必须经历这道换算。抛出异常而不是静默跳过是为了让问题暴露出来否则脏数据混进去后期排查成本更高。4. 训练前准备数据集划分、有效增强与类别不平衡处理4.1 划分train/val/test别让同一张图出现在两个集合650张图对深度学习来说属于小规模数据集划分更要讲究。常见做法是train:val:test按8:1:1切分但必须保证三个集合里没有重复图片而且damaged和good_condition的比例在train和val里都要维持原分布。import os import random from collections import Counter random.seed(42) # 固定随机种子保证结果可复现 img_dir JPEGImages txt_dir labels img_names [f[:-4] for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(img_names) # 先切test再从剩余里切val test_split int(len(img_names) * 0.1) val_split int(len(img_names) * 0.1) train_names img_names[test_split val_split:] val_names img_names[test_split:test_split val_split] test_names img_names[:test_split] # 统计每个集合里damaged框的数量 def count_damaged(names): cnt 0 for name in names: txt_path os.path.join(txt_dir, name .txt) if not os.path.exists(txt_path): continue for line in open(txt_path): if line.split()[0] 0: cnt 1 return cnt print(train damaged:, count_damaged(train_names)) print(val damaged:, count_damaged(val_names)) print(test damaged:, count_damaged(test_names))随机种子42是约定俗成的选择也可以用其他值但一旦定了就别改否则每次跑出来的划分结果不一样实验对比就没有意义了。我见过有人用dict来存储划分结果list不同集合间出现重叠没发现训练时验证集里混进训练图片mAP虚高模型一上产线就露馅。4.2 650张太少增强策略怎么选这份数据集标注是“是否增强否”说明图片都是原始帧没做过处理。训练时增强操作得自己做。Mosaic增强在YOLOv5系列里是默认选项把小图拼成大图能有效缓解小目标漏检但对行李箱这类中等大小目标效果远不如对密集小目标明显。另外行李箱表面纹理单一大面积随机擦除反而会破坏缺陷特征。我一般建议轻度几何增强水平翻转、小角度旋转、轻微缩放。色彩抖动要谨慎因为damaged类里的划痕、凹陷对光照变化敏感调饱和度或亮度可能把浅划痕直接抹掉。下面是用Albumentations库的配置示例import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomRotate90(p0.5), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit10, p0.5), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), ], bbox_paramsA.BboxParams(formatyolo, label_fields[class_labels]))注意bbox_params里的formatyolo对应txt的归一化中心点格式。如果前面转换时用了VOC像素坐标这里就要写formatpascal_voc。增强参数不是越大越好rotate_limit超过15度后行李箱的边框形状会扭曲到不像实物模型反而学到错误的几何特征。4.3 damaged框太少复制粘贴增强与损失函数权重damaged只有199框这是比图片少更棘手的问题。每张图平均0.3个缺陷框意味着大部分图片里压根没有damaged目标模型很容易收敛到“什么都别预测”的懒惰解。两种常见处理方案第一种是复制粘贴增强把含damaged区域的图片裁剪出来随机粘贴到不含缺陷的行李箱图片上同时复制对应的标注框。这种做法在工业缺陷检测中效果显著因为缺陷和背景往往是独立分布的拼接后视觉上不会穿帮。注意粘贴时不要让缺陷框之间重叠过多否则训练时NMS会把两个目标当成一个。第二种是调整损失权重。YOLO系列在分类损失上支持给每个类别设置权重把damaged的权重调高到3到5倍迫使模型更关注这个样本量少的类别。实际操作中我一般两种都做但只在验证集上盯着damaged的AP变化防止损伤good_condition的精度。5. 避坑与常见问题标注校验、格式幻觉与训练翻车5.1 图片、xml、txt数量对不上现象解压后发现JPEGImages有650张Annotations却只有648个xmllabels文件夹也只有645个txt。原因在数据采集过程中照片拍了但标注漏了或者标注完成后导出中断。这种问题在网上下载的数据集里很常见上传压缩包时也容易因为文件名编码问题丢失个别文件。解决先用第2.3节的集合差脚本找出缺失名单再去三个目录里核对。如果只是个别缺失直接补一个空标注文件对齐数量或者从训练列表里剔除这几张图。千万不要让训练脚本直接遍历JPEGImages目录然后用find(xml_path)的方式硬找xml找到不存在的路径报错还是小事更坑的是某些脚本里这种情况会返回None然后在解析时崩掉。5.2 归一化坐标出现负数或大于1现象训练时报错“box out of image bounds”或者loss在初期就出现NaN。原因原始VOC标注里有个别框的xmin、ymin小于0或者xmax、ymax超出了图片宽度高度。可能是标注软件操作时框拉过头了也可能是批量标注脚本的边界处理逻辑有bug。转YOLO格式时归一化后就变成负坐标或超过1的坐标。解决第3.2节的转换脚本里已经加了clip操作但那只适合框偏差很小的情况。如果某个框整体都在图片外部clip之后变成一条贴在边缘的细线同样会污染训练。正确做法是在转换时计算框的面积如果clip前后面积变化超过20%直接丢弃这个框并在日志里输出文件名和坐标值。5.3 类别索引错位现象训练时loss下降正常但验证集上damaged的召回率几乎为0而good_condition的AP却异常高。原因txt里class_id是0和1但训练配置文件里把类别列表写成了[good_condition, damaged]顺序和数据集标注时的classes列表不一致。模型实际把damaged的框当成good_condition来学语义全部错位但loss曲线看起来完全正常。解决训练前用脚本统计一遍txt里每个class_id的出现次数和数据集描述里的框数对比。damaged应为199good_condition应为737。如果发现数字对不上立即检查classes映射顺序。这个检查只需两分钟但能省下十几个小时的无效训练。另一个类似坑是模型的类别数写成3用了索引0、1、2最后那个空类别被强行塞进计算图里mAP输出会有一列恒为0。5.4 增强把“缺陷”增强没了现象训练几百轮后验证集mAP不低但把模型放到真实产线图片上划痕和凹陷一个都检测不出来。原因选用了过强的色彩抖动或模糊增强。比如把RandomBrightnessContrast的contrast_limit设成0.5浅划痕和背景的对比度被拉平模型在增强图上看到的damaged样本已经丧失了关键视觉特征。这是我见过最隐蔽的翻车点因为训练集上loss一直在降只有到现场测试才暴露。解决对缺陷检测任务控制几何增强强度色彩类增强要么不用要么在验证集上做A/B对比。我在某次行李箱检测实验里把亮度变化上限从0.3调到0.1damaged的AP直接涨了6个点。增强的目的是增加样本多样性不是制造新样本。5.5 训练mAP高但实际检测差先怀疑数据划分现象test集上mAP_0.5有0.85但换了另一批行李箱图片检测效果大幅下降误检漏检一大堆。原因test集太干净了和训练集几乎同分布。这份数据集只有650张如果划分时随机种子固定每次实验都用同一份val和test模型很容易在test上过拟合。另外如果图片是从视频里逐帧抽的相邻帧高度相似train和test之间可能存在隐性重复。解决用第4.1节脚本划分后再检验一下test集和train集的图片相似度。如果发现连续性过强应该按时间或批次分组而不是纯随机划分。做交叉验证也行5折每折130张测试虽然训练成本高5倍但评估结果靠谱得多。6. 最后一道工序把标注画回原图做抽检6.1 抽检脚本一眼看出标注质量训练前画的框是给人看的直接决定模型学什么。我每次拿到新数据集不管描述里说标注多准确都会抽50张图把框画回去人工看一遍。这一步看起来原始但确实能发现脚本校验发现不了的问题比如框打偏了、框太小只框住缺陷的一部分、或者把行李箱的轮子也标成了damaged。import cv2 import os img_dir JPEGImages txt_dir labels out_dir visual_check os.makedirs(out_dir, exist_okTrue) colors {damaged: (0, 0, 255), good_condition: (0, 255, 0)} for name in os.listdir(img_dir)[:50]: img_path os.path.join(img_dir, name) img cv2.imread(img_path) h, w img.shape[:2] txt_path os.path.join(txt_dir, name.replace(.jpg, .txt)) if not os.path.exists(txt_path): continue for line in open(txt_path): parts line.strip().split() cls_id int(parts[0]) x_center float(parts[1]) * w y_center float(parts[2]) * h box_w float(parts[3]) * w box_h float(parts[4]) * h x1 int(x_center - box_w / 2) y1 int(y_center - box_h / 2) x2 int(x_center box_w / 2) y2 int(y_center box_h / 2) label damaged if cls_id 0 else good_condition cv2.rectangle(img, (x1, y1), (x2, y2), colors[label], 2) cv2.putText(img, label, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, colors[label], 1) cv2.imwrite(os.path.join(out_dir, name), img)画框脚本不涉及任何训练逻辑只是把txt的归一化坐标还原成像素坐标然后画矩形和类别文字。抽检时重点关注三类问题框是否贴合目标边缘、damaged框是否把正常部位也包进去了、有没有目标完全没被框住。如果这50张里发现问题超过5处这份数据的标注质量就要打问号宁可花时间清洗也别直接开训。6.2 从抽检延展出来的一个习惯从那以后我每次拿到新数据集都强制走一遍完整流程解压、数量校验、格式转换复查、划分、可视化抽检最后才轮到训练。这五步看起来繁琐但对650张这种小数据集来说总共也就半小时工作量却能避免大部分训练翻车。希望帮到你。这份行李箱缺陷检测数据集素材本身是规范的主要工作量在训练前的数据工程上把不平衡问题和格式校验处理好后面训练就是水到渠成的事。本文还有配套的精品资源点击获取