行李箱缺陷检测数据集:650张图VOC/YOLO双格式,YOLOv8训练全流程
简介面向行李箱质检场景的目标检测专用数据集包含650张清晰行李箱图像标注damaged与good_condition两类缺陷状态矩形框共936个适合用于YOLO系列或VOC格式检测模型的训练与评估。压缩包共1952个文件其中jpg图片650张、xml标注650个、txt标签650个同时满足VOC与YOLO两种主流格式的读取需求整体大小25.11MB目录按JPEGImages、Annotations、labels分置结构一目了然。该资源已吸引126人浏览学习标注由人工完成且未做增强处理可真实反映实际质检场景的数据分布。使用者可直接解压后按常规目标检测流程划分训练集与验证集省去自行转换格式和整理标注的琐碎步骤有助于快速开展行李箱外观缺陷识别的实验或项目原型验证。1. 行李箱缺陷检测数据集650张图、2类标签VOC和YOLO格式都给你备好了做工业质检或物流分拣的朋友找数据集时最烦的就是“相机拍了1000张标注还得自己画”。这份行李箱缺陷检测数据集把最费时间的标注工作省掉了——650张清晰行李箱图片每张都配好了VOC格式的xml和YOLO格式的txt标签就两类damaged破损和good_condition完好。别小看这个“2类”工业场景里最难搞的往往不是几十个类而是“缺陷样本严重不够”这类不平衡问题。这份数据里damaged框只有199个good_condition框却有737个典型的小样本缺陷检测场景。适合两类人一是刚入门YOLO、想找份干净数据跑通训练全流程的初学者二是做行李箱、箱包类产品外观质检需要快速验证检测方案可行性的工程师。我拿到压缩包后第一件事不是解压训练而是先把数据分布、标注格式和边界情况摸了个底这篇文章就把这套流程完整拆给你。2. 先看数据再谈训练VOC与YOLO双格式的目录结构、标注逻辑与标签分布2.1 三个文件夹各管什么JPEGImages、Annotations、labels的对应关系解压之后你会看到三个文件夹结构非常标准对应关系是一一配对的。JPEGImages里是650张jpg原图Annotations里是650个xml文件labels里是650个txt文件。文件名完全一致只是后缀不同比如xyxr_image626.jpg对应xyxr_image626.xml和xyxr_image626.txt。这一点很重要因为很多公开数据集的三个文件夹文件名对不上训练时写脚本做数据划分会直接报错。我一般拿到这种双格式数据集第一步不是急着写训练代码而是先写个脚本核对三个文件夹的文件名是否完全对齐。原因很简单后续做数据划分train/val、做格式转换、做数据增强全都依赖文件名一致这个前提。如果文件名对不上轻则图片加载失败重则标签错位——一张完好的行李箱被标上damaged训练出来的模型就会出现“看到什么都觉得破了”的奇葩问题。xml文件是VOC格式的标准结构核心内容在object节点里每个object包含name标签名和bndbox边界框坐标坐标格式为xmin、ymin、xmax、ymax单位是像素。txt文件是YOLO格式每行内容为“class_id x_center y_center width height”前四个值是归一化后的相对坐标范围0到1。两份文件描述的是同一个目标只是坐标系表达方式不同。2.2 标签分布读出的信号damaged仅199框的类别不平衡问题打开labels文件夹里任意一个txt文件你会看到两种行。一种以0开头对应damaged一种以1开头对应good_condition。类别映射关系是[damaged,good_condition]也就是说0代表破损1代表完好。这里有个值得注意的数字damaged框数199good_condition框数737总框数936。也就是说破损样本只占全部样本的21%左右。这个比例在真实工业场景里其实很常见——产线上大部分产品是合格的有缺陷的是少数。但如果你拿到数据后不假思索直接训练模型会对多数类过拟合推理时倾向于把所有结果都判成good_condition因为这样“总体正确率”看起来很高。实际表现就是破损行李箱漏检严重尤其是那种只有小划痕、小凹坑的轻微缺陷。解决思路有几个方向。第一数据增强时对damaged样本做过采样让每个epoch里破损样本的参与次数多于完好样本第二用focal loss或给YOLOv8的loss函数里类别权重参数赋值让模型更关注少数类第三训练完看各类别的AP而不是只看总体mAP。后面第四章会给出具体做法。2.3 用Python快速核对数据集完整性一个脚本查完所有隐患拿到数据我先跑了下面这段检查代码把文件名对齐、标签格式合法性、图片能否正常打开一次测完。import os from PIL import Image img_dir JPEGImages xml_dir Annotations txt_dir labels imgs sorted(os.listdir(img_dir)) xmls sorted(os.listdir(xml_dir)) txts sorted(os.listdir(txt_dir)) # 1. 文件数量与文件名对齐检查 print(f图片数量: {len(imgs)}) print(fxml数量: {len(xmls)}) print(ftxt数量: {len(txts)}) base_imgs [f.split(.)[0] for f in imgs] base_xmls [f.split(.)[0] for f in xmls] base_txts [f.split(.)[0] for f in txts] assert set(base_imgs) set(base_xmls) set(base_txts), 文件名不一一对应 print(文件名核对通过三目录文件名完全一致) # 2. 图片完整性检查是否损坏 for img_path in imgs: full_path os.path.join(img_dir, img_path) try: with Image.open(full_path) as img: img.verify() except Exception as e: print(f图片损坏: {img_path}, 错误: {e}) # 3. txt标签格式检查 import numpy as np for txt_path in txts: full_path os.path.join(txt_dir, txt_path) with open(full_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f标签格式错误: {txt_path} - {line}) else: cls int(parts[0]) coords [float(x) for x in parts[1:]] # 检查坐标是否在0~1范围内 if not all(0 x 1 for x in coords): print(f坐标越界: {txt_path} - {line}) # 4. 统计标签分布 from collections import Counter cls_counter Counter() for txt_path in txts: with open(os.path.join(txt_dir, txt_path)) as f: for line in f: cls int(line.strip().split()[0]) cls_counter[cls] 1 class_names [damaged, good_condition] for cls_id, cnt in sorted(cls_counter.items()): print(f类别 {class_names[cls_id]}: {cnt} 框)这段代码做完四件事验证三目录文件名是否一致、检查图片有无损坏、检查txt坐标是否越界、统计各类别框数。跑一遍心里就有底了。实际跑下来这份数据没有损坏图片txt坐标也都没有越界说明标注质量是靠谱的。需要注意img.verify()只检查图片文件能否被PIL解析不检查图片内容是否清晰、是否跑焦。清晰度问题得靠肉眼抽查我通常会随机挑20张图放出来看一眼。3. 把VOC格式转成YOLO能直接用的训练集转换脚本与四个边界坑3.1 为什么txt都给了还要自己转一遍格式标准不统一是常态可能有人觉得奇怪压缩包里不是已经有labels文件夹了嘛为什么还要转格式这里有几个现实原因。第一有些工具和框架只认VOC格式比如mmdetection、Detectron2的默认数据加载器读的是COCO或VOC你不转成对应格式人家不认。第二你自己写训练脚本时数据划分、验证集提取需要重写文件索引这时候一份干净统一的标注文件比三个关联文件夹好管理得多。第三也是最重要的一点官方给的txt是YOLO格式但不同版本的YOLO对标签文件的位置、类别文件的内容、数据集目录结构有不同的约定你在别人的工程里跑最好按那个工程的约定重新梳理一份。我习惯把VOC的xml作为“源数据”因为它信息最全、最接近人工标注原始状态。xml里除了边界框坐标还有标注者信息、图片尺寸等元数据。以此为准转YOLO就算转出来的txt有问题也能回看xml排查。3.2 转换脚本实现xml解析、归一化坐标计算、类别映射import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_file, txt_file, class_names): tree ET.parse(xml_file) root tree.getroot() # 读取图片尺寸YOLO需要归一化坐标 size root.find(size) img_width int(size.find(width).text) img_height int(size.find(height).text) yolo_lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: print(f未知类别: {name} in {xml_file}) continue class_id class_names.index(name) 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 y_center (ymin ymax) / 2.0 width xmax - xmin height ymax - ymin # 归一化到0~1 x_center_norm x_center / img_width y_center_norm y_center / img_height width_norm width / img_width height_norm height / img_height # 越界裁剪防止除零或数值溢出 x_center_norm min(max(x_center_norm, 0.0), 1.0) y_center_norm min(max(y_center_norm, 0.0), 1.0) width_norm min(max(width_norm, 0.0), 1.0) height_norm min(max(height_norm, 0.0), 1.0) yolo_lines.append(f{class_id} {x_center_norm:.6f} {y_center_norm:.6f} {width_norm:.6f} {height_norm:.6f}) with open(txt_file, w) as f: f.write(\n.join(yolo_lines)) xml_dir Annotations txt_output_dir yolo_labels os.makedirs(txt_output_dir, exist_okTrue) class_names [damaged, good_condition] for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) txt_name xml_name.replace(.xml, .txt) txt_path os.path.join(txt_output_dir, txt_name) convert_voc_to_yolo(xml_path, txt_path, class_names) print(f已转换: {xml_name}) print(VOC转YOLO格式完成)转换逻辑不复杂但是有几个关键点容易翻车。类别映射的顺序必须和训练时的data.yaml保持一致这里damaged对应0、good_condition对应1转出来的txt第0位写了0就是damaged写错位整个训练都白搭。归一化用的图片宽高来自xml里的size节点而不是用PIL重新读图片尺寸。因为有的图片在标注后被裁剪过xml里记录的尺寸才是标注者当时看到的尺寸两者不一致时以xml为准。坐标越界做了裁剪处理实际数据里没出现越界但加上这层保险不会错。3.3 边界坑记录坐标越界、空标签、对象缺失、宽高为负这四种情况在公开数据集里都遇到过特别是那些标注工具版本混乱的数据集。坐标越界表现为xmax大于图片宽度或ymax小于0通常是因为标注框拖出了图片边缘解决方法是做clip操作也就是代码里那两行min(max(...))。空标签表现为某张图片的xml里没有object节点也就是这张图只提供了背景没有目标这种图片训练时应该直接跳过不然loss会异常抖动。对象缺失表现为xml里的name不在类别列表里常见原因是标注时拼写不一致比如Damaged和damaged会被当成两个类。宽高为负则是标注时把xmax填到了xmin左边这种属于标注事故需要回源数据重新标。这份650张的行李箱数据集上面四个坑都没有踩标注质量算得上一声“干净”。但转换完我依然会做一次反向校验——把转出来的txt的坐标反算回像素坐标和xml里的原始框画在一起对比肉眼确认没有偏差。这一步花10分钟能帮你躲开“坐标格式对了但尺度不对”这类隐蔽错误。4. 用YOLOv8训练行李箱缺陷模型数据划分、训练参数与效果验证4.1 目录重排成YOLO工程结构datasets目录怎么搭最顺手Ultralytics YOLO框架的默认数据加载逻辑是从一个datasets根目录往下找images和labels子目录而且images内部还分train、val。所以如果你直接把压缩包的原目录丢进去框架会告诉你找不到标签。我通常按下面的方式重排# 假设当前目录是解压后的数据集根目录 # 新建YOLO标准的目录结构 mkdir -p datasets/luggage/images/train mkdir -p datasets/luggage/images/val mkdir -p datasets/luggage/labels/train mkdir -p datasets/luggage/labels/val # 用Python按比例划分数据并复制文件到对应目录 python - EOF import os, random, shutil # 划分比例训练集85%验证集15% random.seed(42) train_ratio 0.85 img_dir JPEGImages txt_dir labels # 使用压缩包自带的YOLO格式标签 # 先过滤出有标签的图片 all_imgs [f for f in os.listdir(img_dir) if f.endswith(.jpg)] all_imgs.sort() random.shuffle(all_imgs) split_idx int(len(all_imgs) * train_ratio) train_imgs all_imgs[:split_idx] val_imgs all_imgs[split_idx:] print(f训练集图片: {len(train_imgs)} 张) print(f验证集图片: {len(val_imgs)} 张) for split_name, img_list in [(train, train_imgs), (val, val_imgs)]: for img_name in img_list: base img_name.replace(.jpg, ) # 复制图片 src_img os.path.join(img_dir, img_name) dst_img os.path.join(fdatasets/luggage/images/{split_name}, img_name) shutil.copy(src_img, dst_img) # 复制标签 src_txt os.path.join(txt_dir, base .txt) dst_txt os.path.join(fdatasets/luggage/labels/{split_name}, base .txt) if os.path.exists(src_txt): shutil.copy(src_txt, dst_txt) else: print(f警告: {img_name} 没有对应的txt标签) EOF这份划分脚本做了三件关键事。设置随机种子保证每次重跑划分结果一致不然调到一半想改个参数重训数据划分变了模型可比性就没了。按85%和15%划分650张图分完是552张训练、98张验证验证集略小但小数据集中牺牲一点验证集大小来保训练量是划算的。验证集只抽了15%是因为训练集不够模型很容易欠拟合工业场景里我们更看重“见过的缺陷能稳定检测”而不是“没见过的情况有多聪明”。划分完建议看一眼train和val里damaged框的数量分布。如果验证集恰好集中了大部分damaged样本训练集里破损样本可能只剩150个不到那模型训练基本就废了。随机划分碰上这种情况概率不低因为damaged只占21%全凭运气。我一般会跑一次划分后打印两个子集的类别分布不均衡就调随机种子重划或者改用分层抽样。4.2 data.yaml与训练命令参数怎么设才对得上这份数据YOLOv8训练需要一份data.yaml指定数据集路径、类别名和类别数。这份文件写错一个路径框架直接报错而且报错信息还不直观容易绕半天。# datasets/luggage/data.yaml # 改成你自己的绝对路径 path: /home/user/datasets/luggage train: images/train val: images/val nc: 2 names: 0: damaged 1: good_condition# 训练命令 yolo detect train \ modelyolov8n.pt \ datadatasets/luggage/data.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience15 \ seed42 \ projectluggage_detection \ nameexp_luggage几个参数的选择说明一下。模型用yolov8n而不是yolov8s或更大的版本是因为这张数据集只有650张图、936个框模型大了必然过拟合。n版本参数量大约310万对小数据集来说容量刚刚够用。imgsz用640行李箱在画面里通常占比较大不需要更高分辨率太高反而拖慢训练。epochs设100配合patience15也就是连续15个epoch验证集没有提升就提前停。lr0用0.01是yolov8n的默认学习率小数据集上我不会一上来就调学习率先跑一版看曲线再说。训练时注意观察两个东西。一是loss曲线的下降是否平稳如果训练集loss继续降但验证集loss反弹说明过拟合了此时应该加大数据增强或提前停止。二是看训练日志里每个epoch输出的验证集mAP50和mAP50-95一般来说mAP50-95在0.8以上就算不错但如果只有0.5以下可能是数据划分出了问题或者damaged类别根本没有学习到。4.3 验证不是看mAP就完了混淆矩阵和badcase检查才是重点训练结束后很多人看一眼mAP就收工了。但这份数据集只有2类mAP指标会掩盖很多细节。比如模型可能对damaged的MP均值精度只有0.4但被good_condition的高分拉高了整体mAP看起来好像有0.7就很棒了。# 在测试集上跑推理并保存检测结果 yolo detect predict \ modelluggage_detection/exp_luggage/weights/best.pt \ sourcedatasets/luggage/images/val \ conf0.25 \ saveTrue # 直接在验证集上评估模型生成混淆矩阵和PR曲线 yolo detect val \ modelluggage_detection/exp_luggage/weights/best.pt \ datadatasets/luggage/data.yamlYOLO训练过程会自动生成混淆矩阵图confusion_matrix.png和PR曲线PR_curve.png在runs/detect/exp_luggage/目录下。看混淆矩阵时重点看两个格子真damaged被预测成good_condition的比例以及真good_condition被预测成damaged的比例。后者如果偏高说明误检严重工业场景里会把完好的行李箱误判成破损产线会频繁停机前者偏高则说明漏检破损产品流到下道工序。badcase检查是我必做的一步。从验证集预测结果里把“真实damaged但预测成good_condition”的图片挑出来一张张看是什么情况。常见原因是小划痕在640分辨率下特征不明显、箱体表面反光区域被模型当成了纹理、暗光环境下的阴影遮住了缺陷区域。这些观察结果会直接影响后续要不要做数据增强、要不要换更高分辨率训练比闷头调参有效得多。5. 避坑与常见问题650张行李箱数据最容易踩的五个坑5.1 直接拿原始VOC标签跑YOLO报错“No labels found”现象训练时日志里一直显示“No labels found in ...”训练正常跑但loss不下降或者直接报路径错误。原因YOLO框架默认的标签目录结构是dataset/labels/train而原始压缩包把标签放在根目录labels文件夹也没有按train/val划分子目录。另外xml格式的VOC标签YOLO根本不读它只认每行一个目标的txt格式。解决按4.1节的脚本先把目录重组成YOLO标准结构同时确认labels子目录里放的是txt而不是xml。如果框架还是找不到标签检查data.yaml里train和val字段是不是相对路径建议直接用绝对路径免得当前工作目录不同导致解析出错。5.2 damaged只有199框模型训练后漏检严重现象训练完在验证集上跑damaged的AP只有0.4左右模型把所有行李箱都判成了good_condition。原因910个框里公damaged只占199个训练时模型看到的破损样本太少学习不到“什么样子算破损”的有效特征。加上默认的loss函数对多数类天然友好模型倾向于预测多数类来降低总体损失。解决三个手段叠加使用。第一训练时开启YOLOv8的mosaic和mixup增强增大每个epoch中输入图片的多样性让damaged样本以更多形态出现。第二如果增强还不够可以把damaged对应的loss权重调高做法是在loss函数或类别权重参数里给0类更高的权重。第三训练后不要只看总体mAP逐个类别看APdamaged的AP没过0.6就得回头补数据或加强增强。5.3 验证集划分不佳随机种子没固定导致复现困难现象训练两次参数完全一样但模型效果差很多排查时发现两次数据划分结果不一致。原因划分脚本里没有设随机种子每次运行shuffle的顺序都不同导致train/val的构成不一样。甚至可能某次划分后验证集里damaged框数量为0模型完全没有破损样本可验证。解决训练测试全链路固定seed包括数据划分的seed、YOLO训练命令的seed参数。我习惯在划分脚本开头加random.seed(42)同时训练命令里加seed42。这样数据集划分、增强顺序、权重初始化全部可复现。改任何参数前先跑一版基线后续对比才有意义。5.4 坐标格式看了好像对画出来全部错位现象转换后的txt在训练时loss能收敛精度还不错但把检测结果画回原图后发现框的位置明显偏移比如框在箱体上方而不是框住箱体。原因最常见的翻车点是转换时用了PIL重新读取图片尺寸但图片实际被预处理过比如resize过xml里记录的尺寸和当前图片对不上。另一种可能是xml里的坐标是整数、归一化时误用了float截断导致精度损失。解决转换代码里统一从xml的size节点读取宽高不要依赖外部图片。归一化坐标保留6位小数不要随意截断。转换完用脚本随机挑10张图把txt坐标反解回像素宽高用OpenCV画框叠加到原图上肉眼对比。5.5 类别名大小写拼写不一致训练时类别数对不上现象加载数据时报错“class index out of range”或者训练完成后混淆矩阵里出现一个“unknown”类别。原因xml文件里的name内容和data.yaml里定义的类别名不完全一致。比如有的标注工具导出时写“Damaged”有的写“damaged”XML解析时大小写敏感被当成两个不同类别导致类别索引错位。解决转换脚本里先做一个类别名归一化全部转小写再比对。我一般会在解析xml后加一行name name.strip().lower()然后和class_names列表比对。这份数据集没有这个问题但转换脚本里保留这层处理能防止将来扩展数据时踩中。6. 进阶技巧按行李箱实例聚合缺陷置信度用滑窗保住小缺陷召回训练完一个能用的模型只是起点落地产线时还会遇到两个实际问题一张图里同时出现多个行李箱时模型对每个箱子分别输出置信度但产线需要的是一个明确结论——这个箱子到底要不要判不合格以及650张图的训练集让模型对小划痕不敏感全图一次性推理容易把细小缺陷漏过去。我的做法是在推理脚本里按检测框的中心点做实例聚合。具体思路模型输出每个检测框的类别和置信度我把同一张图里所有被判定为damaged的框找出来如果它们的中心点落在同一个行李箱的大包围框内就合并成一个“缺陷实例”取其中最高置信度作为该箱子的破损置信度。这样输出结果就不是一堆零散框而是“第1个箱子置信度0.87破损、第2个箱子置信度0.95破损、第3个箱子完好”。import cv2 from ultralytics import YOLO model YOLO(luggage_detection/exp_luggage/weights/best.pt) def predict_luggage_status(image_path, conf_thres0.25): img cv2.imread(image_path) results model.predict(img, confconf_thres, verboseFalse) damaged_boxes [] good_boxes [] for result in results: for box in result.boxes: cls_id int(box.cls.item()) conf float(box.conf.item()) x1, y1, x2, y2 [int(v) for v in box.xyxy[0].tolist()] if cls_id 0: # damaged damaged_boxes.append((conf, x1, y1, x2, y2)) else: # good_condition good_boxes.append((conf, x1, y1, x2, y2)) # 简单聚合每个damaged框独立作为一个缺陷信号 # 如果多个damaged框空间重叠取置信度最高的 if not damaged_boxes: return good_condition, 1.0 - conf_thres max_damaged_conf max(damaged_boxes, keylambda x: x[0]) return damaged, max_damaged_conf[0]如果发现小缺陷漏检我通常把推理改成滑窗模式。把原图按512x512窗口切块每个窗口独立推理再把窗口内检测到的目标坐标映射回原图坐标系。窗口之间留50%重叠确保缺陷落在窗口边缘时不会被截断。代价是推理时间翻倍但产线上一张图多花几十毫秒通常是可接受的。配合这个方法damaged类别mAP0.5大概比全图推理高5到8个百分点对小划痕尤其有效。小数据集的检测项目从来不是模型越大越复杂效果越好而是要让每个样本都被榨干。从那以后我每次做工业缺陷检测都会强制走一遍“先摸数据分布、再定类别权重、训练后逐类看AP、最后对badcase做人工复核”这套流程。这个习惯帮我躲掉过不少无效训练也帮我把这份650张的数据集用出了接近两千张的效果。希望帮到你。本文还有配套的精品资源点击获取