简介:面向行李箱质检场景的目标检测专用数据集,包含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框数=199,good_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(f"xml数量: {len(xmls)}") print(f"txt数量: {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_ok=True) 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(f"datasets/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(f"datasets/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 \ model=yolov8n.pt \ data=datasets/luggage/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=15 \ seed=42 \ project=luggage_detection \ name=exp_luggage几个参数的选择说明一下。模型用yolov8n而不是yolov8s或更大的版本,是因为这张数据集只有650张图、936个框,模型大了必然过拟合。n版本参数量大约310万,对小数据集来说容量刚刚够用。imgsz用640,行李箱在画面里通常占比较大,不需要更高分辨率,太高反而拖慢训练。epochs设100配合patience=15,也就是连续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 \ model=luggage_detection/exp_luggage/weights/best.pt \ source=datasets/luggage/images/val \ conf=0.25 \ save=True # 直接在验证集上评估模型,生成混淆矩阵和PR曲线 yolo detect val \ model=luggage_detection/exp_luggage/weights/best.pt \ data=datasets/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,逐个类别看AP,damaged的AP没过0.6就得回头补数据或加强增强。
5.3 验证集划分不佳,随机种子没固定导致复现困难
现象:训练两次参数完全一样,但模型效果差很多,排查时发现两次数据划分结果不一致。
原因:划分脚本里没有设随机种子,每次运行shuffle的顺序都不同,导致train/val的构成不一样。甚至可能某次划分后验证集里damaged框数量为0,模型完全没有破损样本可验证。
解决:训练测试全链路固定seed,包括数据划分的seed、YOLO训练命令的seed参数。我习惯在划分脚本开头加random.seed(42),同时训练命令里加seed=42。这样数据集划分、增强顺序、权重初始化全部可复现。改任何参数前先跑一版基线,后续对比才有意义。
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_thres=0.25): img = cv2.imread(image_path) results = model.predict(img, conf=conf_thres, verbose=False) 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, key=lambda x: x[0]) return "damaged", max_damaged_conf[0]如果发现小缺陷漏检,我通常把推理改成滑窗模式。把原图按512x512窗口切块,每个窗口独立推理,再把窗口内检测到的目标坐标映射回原图坐标系。窗口之间留50%重叠,确保缺陷落在窗口边缘时不会被截断。代价是推理时间翻倍,但产线上一张图多花几十毫秒通常是可接受的。配合这个方法,damaged类别mAP0.5大概比全图推理高5到8个百分点,对小划痕尤其有效。
小数据集的检测项目,从来不是模型越大越复杂效果越好,而是要让每个样本都被榨干。从那以后我每次做工业缺陷检测,都会强制走一遍“先摸数据分布、再定类别权重、训练后逐类看AP、最后对badcase做人工复核”这套流程。这个习惯帮我躲掉过不少无效训练,也帮我把这份650张的数据集用出了接近两千张的效果。希望帮到你。
本文还有配套的精品资源,点击获取