简介:面向目标检测方向研究者及工业质检工程的一款香烟破损检测数据集,采用YOLOV5标准目录格式存储,按6个破损类别划分,涵盖头部破损、滤嘴破损等缺陷类型,图片为3024×4032分辨率的高清RGB图,适合直接训练与验证。数据总量388MB,共803个文件,包含401个jpg图片、401个对应的txt标注以及1个可视化py脚本,其中训练集320张、验证集80张。标注txt按YOLO格式生成,py脚本可随机读取一张图片绘制边界框并保存到当前目录,方便快速检查标注质量。当前已有140人浏览学习,适合刚接触YOLOV5训练流程或需要缺陷检测数据做迁移学习的开发者使用,收到后无需额外转换即可开展模型训练与效果验证。
1. 香烟破损检测数据集:YOLOv5格式、6个类别,拿到就能训练的缺陷检测资源
在烟草产线的质检环节,烟包侧边的破损、滤嘴缺角、头部裂口这类瑕疵,靠人工肉眼很容易漏掉,而且每个人判定标准还不太一样。这份「香烟破损检测数据集」就是为这个场景准备的:6 个破损类别,训练集 320 张、验证集 80 张,全部按 YOLOv5 的目录格式存好,图片是 3024×4032 的 RGB 大图,旁边配好了同名的 txt 标签文件。拿到手之后不需要再做 VOC 转 YOLO 之类的格式折腾,补一个 data.yaml 就能直接丢给 YOLOv5 训练。它特别适合做工业缺陷检测的工程师、拿 YOLO 跑量写毕设的学生,以及第一次接触目标检测、想跑通完整流程的新手——既可以当数据资源,也能当 YOLOv5 的入门练习项目。
2. 数据到底长什么样:YOLOv5 目录约定与标签格式拆解
2.1 目录结构:train/val 分离与 images-labels 配对规则
拿到压缩包解压后,先别急着一通操作,把目录树完整看一遍。这份数据是按 YOLOv5 官方约定的 images 和 labels 分目录存放的,没有额外的 JSON 或 XML,省掉了从 VOC 转 YOLO 这一层麻烦。展开后大致是这样一个结构:
smoke_damage/ ├── images/ │ ├── train/ │ │ ├── IMG_8363.jpeg │ │ ├── IMG_8384.jpeg │ │ ├── MVIMG_20220720_111325.jpeg │ │ └── ... │ └── val/ │ ├── IMG_8354.jpeg │ └── ... ├── labels/ │ ├── train/ │ │ ├── IMG_8363.txt │ │ └── ... │ └── val/ │ ├── IMG_8354.txt │ └── ... └── visualize.py关键点是 images/train 和 labels/train 里的文件是一一对应的:图片叫 IMG_8363.jpeg,标签就叫 IMG_8363.txt,文件名完全相同,大小写也要一致。YOLOv5 在读取标注时,是根据同名图片去匹配 labels 下的 txt 文件。只要后缀名不一致,例如图片是 .jpeg 而标签文件被标注工具写成了 .jpg 对应关系,这张图就会被训练流程自动跳过,而且日志里只出现一行 warning,不仔细看根本发现不了。
所以拿到数据后的第一件事,我建议先跑一遍配对检查。下面这段 bash 脚本可以快速找出没有对应标签的图片:
cd smoke_damage for f in images/train/*.jpeg; do base=$(basename "$f" .jpeg) if [ ! -f "labels/train/$base.txt" ]; then echo "missing label for $base" fi done这段脚本的原理很简单:用 basename 去掉图片路径和后缀,得到文件名主干,再判断 labels 目录下是否存在同名 txt。如果循环结束没有任何输出,说明训练集的标签配对是完整的。实际操作时,建议把*.jpeg和*.jpg都覆盖到,写成for f in images/train/*.jpeg images/train/*.jpg; do这样,避免两种后缀混用导致的遗漏。我当时拿到手第一轮跑出来的结果没有缺失,说明数据集整理得还算规范,但这一步不能省。
2.2 标签txt解析:YOLO格式的归一化坐标怎么读
随便打开一个标签文件,比如 labels/train/IMG_8363.txt,内容长这样:
0 0.542356 0.367812 0.120534 0.098213 1 0.823450 0.451231 0.080156 0.073245每一行代表一个目标框,一共 5 个值,依次是:类别 id class_id、中心点 x 坐标、中心点 y 坐标、框宽度、框高度。这里全部是归一化坐标,范围是 0 到 1,不是像素坐标。x_center 的真实含义是“目标中心点在图像宽方向上的相对位置”,也就是像素中心 x 坐标除以图像宽度;width 是框的像素宽度除以图像宽度,height 同理。
这也是 YOLO 系列格式和 VOC 格式最大的区别。VOC 的 XML 里给的是x_min, y_min, x_max, y_max的绝对像素值,一旦图像分辨率变了,标签就失效了。而 YOLO 的 txt 里全是相对值,所以这份数据即使将来你想从 3024×4032 这个原始分辨率改成训练时常用的 640×640 输入尺寸,也不用手动换算任何坐标。YOLOv5 内部会把图片 letterbox 缩放,标签跟着等比缩放,完全对得上。
不过有一个隐患:如果标签生成工具在写 txt 时搞错了归一化基准(比如用宽高反了),或者直接把像素坐标写进去,可视化时所有框都会飞掉。为了提前排查这类问题,我写了一个标签合法性检查脚本:
import glob import os root = 'smoke_damage' label_dirs = ['labels/train', 'labels/val'] cls_set = set() min_vals = [1.0, 1.0, 1.0, 1.0] max_vals = [0.0, 0.0, 0.0, 0.0] bad_lines = 0 for d in label_dirs: for txt in glob.glob(os.path.join(root, d, '*.txt')): with open(txt, 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f'bad line in {txt}: {line}') bad_lines += 1 continue cls = int(parts[0]) vals = list(map(float, parts[1:])) cls_set.add(cls) for i, v in enumerate(vals): min_vals[i] = min(min_vals[i], v) max_vals[i] = max(max_vals[i], v) print('classes:', sorted(cls_set)) print('x_center min/max:', min_vals[0], max_vals[0]) print('y_center min/max:', min_vals[1], max_vals[1]) print('width min/max:', min_vals[2], max_vals[2]) print('height min/max:', min_vals[3], max_vals[3]) print('bad lines:', bad_lines)逻辑说明:程序遍历 labels/train 和 labels/val 下所有 txt,按行解析出类 ID 和四个坐标值。min_vals和max_vals分别统计四个维度在全部标签中的最小值和最大值,并打印出来。如果这份数据的标签规范,那么 x_center、y_center 应该在 0 到 1 之间,width、height 也应该在 0 到 1 之间(且 height 不可能是 0)。只要看到最小值小于 0 或最大值大于 1,基本可以断定有个别标签写成了像素坐标,必须修掉再训练。参数说明里 root 改成你的数据集根目录;如果只想检查一个目录,就把 label_dirs 改成['labels/train']即可。
2.3 为什么选 YOLOv5 格式而不是 VOC/COCO
很多从计算机视觉课程过来的人,习惯用 LabelImg 标出 VOC 的 XML,或者用 labelme 生成 JSON,最后到训练阶段才发现数据格式压根不对。VOC 需要专门的 Dataset 类去解析 XML,COCO 则要处理一个巨大的 annotations.json,看类别 id 映射看到头晕。而 YOLOv5 格式是所有主流 YOLO 版本通用的“最小公约数”——一个 txt 一行一个目标,不需要额外依赖解析库,也不需要维护复杂的目录嵌套。
这个优势在只有 400 张图的小规模数据上尤其明显。数据量小的时候,管理成本比模型结构对结果的影响更大。如果你把一半时间耗在格式转换上,后面训练的时间和心情都会大打折扣。另外,这份数据本身的分辨率是 3024×4032,属于高分辨率大图,缩到 640 时目标信息仍然保留得不错,算是对“高分辨率小目标”场景的一个很好的压测样本。如果你以后要接手像素更高的工业相机图像,先拿这份数据跑通全流程比盲目改模型结构更有价值。
提示:训练前可以先用
awk '{print $1}' labels/train/*.txt | sort | uniq -c枚举一遍标签里实际出现的类别 id 和数量分布,确保与期望的 6 类一一对应,这一步能避免后面训练了半天才发现类别 id 错位。
3. 直接开跑:用这份数据集训练 YOLOv5 的完整流程与参数解读
3.1 准备环境:Python、PyTorch 与 YOLOv5 仓库
这份数据集本身不依赖任何特殊库,但训练 YOLOv5 还是需要配一套环境。之前踩过的坑是 PyTorch 版本和 CUDA 版本不匹配,装完以后一跑 train.py 就报torch.cuda.is_available()为 False。所以建议用 conda 单独建一个环境,别污染系统的 Python:
conda create -n yolo python=3.8 conda activate yolo pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu117 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt参数说明:这里指定了 torch 1.13.1 和对应的 torchvision,主要原因是它和 CUDA 11.7 配套稳定,N 卡驱动只要是 450 以上基本都能跑。如果你不想折腾 conda 或显存不够,也可以直接用 CPU 版 torch,但 320 张图训练一轮会慢到怀疑人生。Python 3.8 到 3.10 都可以,不要用 3.11 以上的版本,部分依赖库的编译版本对不上。
装完后把 smoke_damage 数据集放到一个独立目录,比如放在 yolov5 的同级目录下,后面 data.yaml 里用相对路径或绝对路径都行。我的习惯是把它放到项目根目录之外,避免和 yolov5 仓库的文件混在一起,清理起来也方便。
3.2 写 data.yaml:类别名必须与标签 id 严格对应
YOLOv5 的训练入口需要你提供一个 data.yaml。新建一个文件,比如 smoke_damage.yaml,内容如下:
train: ../smoke_damage/images/train val: ../smoke_damage/images/val names: 0: head_damage 1: filter_damage 2: body_damage 3: side_damage 4: inner_damage 5: other_damage有两个地方最容易出错。第一,train 和 val 的值要写 images 目录的路径,不要写 labels 目录。YOLOv5 的 DataLoader 会自动把路径里的 images 替换成 labels,然后去对应目录找同名 txt。如果写成 labels 目录,它会去 labels/labels 下面找文件,直接报错。第二,names 列表的序号必须和标签文件里的 class_id 完全一致。比如标签里 class_id 是 0,names 里第 0 项就是 head_damage。如果顺序反了,训练和推理时类别名显示就会错位。数据集解压后如果有一个 classes.txt,一定要优先以里面的文件为准,我给的这段只是演示命名,实际类别名以你手里这份数据提供的为准。
data.yaml 写好后,可以先用下面这段 Python 快速验证路径是否有效:
import yaml with open('smoke_damage.yaml', 'r') as f: cfg = yaml.safe_load(f) import os for split in ['train', 'val']: img_dir = cfg[split] print(split, img_dir, os.path.exists(img_dir), len(os.listdir(img_dir)))这段代码通过 yaml 读取 data.yaml 中的路径,再用 os.path.exists 判断目录是否存在,并输出目录下图片数量。正常应该看到 train 对应目录存在且有 320 个文件,val 对应目录存在且有 80 个文件。如果数量对不上,多半是目录路径没配对。
3.3 训练命令与关键参数:img、batch、epochs 怎么调
环境准备好、data.yaml 写好后,就可以启动训练。在 yolov5 目录下运行:
python train.py \ --data smoke_damage.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 150 \ --patience 20 \ --cache disk \ --device 0解释一下这些参数的实际意义:
--img 640表示把所有图片统一 letterbox 缩放到 640×640 输入网络。这不是简单粗暴地拉伸,而是等比缩放后填充灰边。标签坐标不受影响,所以原始 3024×4032 的大图可以放心用。--batch 16是经验值。显卡显存 16G 以上可以尝试 32,但小数据集上 batch 太大容易更快过拟合。如果你用的是 8G 显存,建议调成 8。--epochs 150对于 320 张训练图来说,一般 100 到 150 轮就能收敛。如果看到 mAP 在 100 轮以后还在涨,可以加到 200 轮。别一上来就 300 轮,浪费时间和显卡寿命。--patience 20表示连续 20 轮 mAP 都没有提升时自动停止训练。这是我最推荐的习惯,尤其你晚上睡觉前挂上去,早上起来看结果,而不用盯着一行行日志。--cache disk会把图片以缓存形式预先加载到磁盘,避免每轮重复读图造成 IO 瓶颈。如果不加 cache,320 张大图读到内存里也不会爆,但 epoch 之间会慢一些。
权重建议直接用yolov5s.pt作为预训练起点,不要从零开始。YOLOv5s 在 COCO 上学过通用特征,迁移到香烟破损这种表面纹理场景,收敛速度和最终精度都会明显更好。
训练结束后,结果会保存在runs/train/exp目录下,里面包括每轮的 loss 曲线、验证集 PR 曲线、混淆矩阵,还有 best.pt 和 last.pt 两个权重。判断训练是否正常,主要看results.png里 mAP50 曲线有没有平稳上升,以及 train/loss 曲线有没有震荡下降。如果 mAP50 在 20 轮以内就冲得很高,并且 val 曲线和 train 曲线差距越来越大,那就是过拟合过早,回头要把 epochs 调小或者开更强的数据增强。
3.4 可视化脚本:先跑一遍,确认标签真的贴到了图上
数据集里带的 visualize.py 是我每次上手必跑的工具。它的作用就是把一张图片和它的 txt 标签叠加画出来,方便人工确认框是不是正好框在破损区域上。这里我写了一个增强版,支持批量处理一个目录下的所有图片,并把结果输出到单独文件夹:
import cv2 import glob import os import sys def draw_boxes(image_path, label_path, class_names): img = cv2.imread(image_path) h, w = img.shape[:2] colors = [(0, 0, 255), (0, 255, 0), (255, 0, 0), (255, 255, 0), (0, 255, 255), (255, 0, 255)] with open(label_path, 'r') as f: lines = f.readlines() for line in lines: parts = line.strip().split() if len(parts) != 5: continue cls = int(parts[0]) xc, yc, bw, bh = map(float, parts[1:]) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) label = class_names[cls] if cls < len(class_names) else str(cls) cv2.rectangle(img, (x1, y1), (x2, y2), colors[cls % len(colors)], 2) cv2.putText(img, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 1.0, colors[cls % len(colors)], 2) return img class_names = ['head_damage', 'filter_damage', 'body_damage', 'side_damage', 'inner_damage', 'other_damage'] if __name__ == '__main__': image_root = sys.argv[1] if len(sys.argv) > 1 else 'smoke_damage/images/train' out_dir = 'visualized' os.makedirs(out_dir, exist_ok=True) files = [] for ext in ('*.jpeg', '*.jpg', '*.png'): files.extend(glob.glob(os.path.join(image_root, ext))) for f in files: label = f.replace('/images/', '/labels/').rsplit('.', 1)[0] + '.txt' if not os.path.exists(label): print('skip', f, 'label not found') continue img = draw_boxes(f, label, class_names) out = os.path.join(out_dir, os.path.basename(f)) cv2.imwrite(out, img) print('saved', out)运行方式很简单:
python visualize.py ../smoke_damage/images/train代码里有几处关键设计:label = f.replace('/images/', '/labels/')是靠目录约定定位标签路径;xc, yc, bw, bh是归一化坐标,必须乘上当前图片的实际宽高 w 和 h 才能得到像素框位置;class_names 传入的列表顺序必须和 data.yaml 一致,画图时会直接把类别名打在框上。输出会保存到当前目录下的 visualized 文件夹。如果你看到框和图像中破损部位错位,比如框偏了一半,那就不是画框代码的问题,而是标签文件本身存在坐标基准错误,一定要在训练前修正,否则训练出来的模型精度会被带偏。
4. 避坑指南:香烟破损数据的五个高频翻车点
4.1 显存OOM:分辨率别贪高
现象:训练命令里把 --img 设为 3000 或 4032,程序运行不到几分钟就报RuntimeError: CUDA out of memory,显卡直接被打爆。原因:YOLOv5 的模型输入尺寸由 --img 决定,如果你把输入设成接近原始分辨率,特征图尺寸会指数级上升,显存占用远远超过正常范围。解决:默认 640 就够用,想获得更好的小目标检测效果可以往上加 1280,前提是你的显卡显存至少在 12G 以上。千万不要觉得“原图是 3024×4032 就一定要用原图训练”,YOLOv5 的 letterbox 逻辑已经处理好了缩放,标签也会同步缩放。
4.2 标签和图片文件名后缀不一致
现象:训练日志里频繁出现类似WARNING: Could not find label file: xxx,而且最终训练样本数明显少于 320。原因:图片是 .jpeg,标签生成器却写成了 .jpg,或相反;还有可能是文件名包含空格或特殊字符,导致路径拼接失败。解决:一拿到数据先统一后缀。在 Linux 下用rename或脚本批量修改,在 Windows 下先检查图片扩展名再跑配对脚本。另外路径中尽量不要带中文、空格、#这类字符,YOLO 的某些底层 IO 对特殊字符处理很脆弱。
4.3 验证集只有 80 张,mAP 波动剧烈
现象:每轮训练在 val 集上评估的结果忽高忽低,mAP50 可能在 80%、65%、78% 之间跳来跳去,看上去模型像“抽风”。原因:80 张验证图对于 6 类目标检测任务来说太少,平均每类分到手只有十来张,单张难例的表现会显著拉高或拉低整体指标。解决:不要死守官方验证集。我建议做一次 5 折交叉验证——把整个 400 张数据随机分成 5 份,每次用其中 4 份训练 1 份验证,跑 5 次取平均。虽然耗时会增加到 5 倍,但指标可信度高很多。如果想要快速部署,直接把 val 并入训练集用全部 400 张微调,然后自己再拍一批真实产线照片当外部验证集。
4.4 类别不平衡:少数类完全被模型忽略
现象:训练完成后,用模型跑验证集,发现某几类检测效果很好,但“头部破损”这种类别几乎一个都检测不出来,召回率极低。原因:6 个类别在 320 张训练图里的样本数量是不均衡的,常出现的类别可能占了一半以上,冷门类别只有几十张甚至更少。模型在训练时被多数类别主导,少数类很难学出泛化特征。解决:第一,训练时不要把 epochs 拉太长,否则多数类过拟合而少数类欠拟合;第二,用 YOLOv5 的 mosaic 增强配合--multi-scale,能在一定程度上增加小目标样本的多样性;第三,针对少数类单独做离线增强,例如旋转、平移、亮度扰动,扩到和多数类接近的数量后再训练。注意,离线扩增后要保证新增图片也有对应 txt,否则 YOLO 会跳过它们。
4.5 手机拍摄图的 EXIF 旋转隐患
现象:可视化脚本跑某张图片时,发现所有边界框都整体偏转 90 度或 180 度,框的位置明显和画面内容对不上。原因:文件名像 IMG_8363、MVIMG_20220720 这种多来自手机相机,而手机 JPEG 常在 EXIF 信息里写一个 Orientation 旋转方向。图像查看器会按 EXIF 自动摆正图片,开源 CV 库默认不处理这个信息,于是读取到的像素矩阵和标注时看到的画面方向不一致。解决:用 Pillow 的ImageOps.exif_transpose把所有图片重写一遍,去掉 EXIF 方向信息,让像素本身变成正的方向:
from PIL import Image, ImageOps import os def fix_exif(root): for dirpath, _, files in os.walk(root): for fname in files: if fname.lower().endswith(('.jpg', '.jpeg')): path = os.path.join(dirpath, fname) img = Image.open(path) img = ImageOps.exif_transpose(img) img.save(path, quality=95) print('fixed', path)这段代码会遍历指定目录下所有 jpg/jpeg,按 EXIF 信息重新排列像素后覆盖保存。标签坐标本来就是在摆正后的画面上标注的,所以处理后框就能对齐。如果你用这份数据没遇到这个问题,不代表以后不会遇到,跑可视化脚本时多留意几张就行了。
注意:做完 EXIF 修复后,图片文件的 MD5 会变,如果你已经把数据交给别人训练,记得同步通知;不要在同一份数据上再跑一遍这个脚本,否则可能二次旋转。
5. 验证模型和进阶:从可视化到扩展训练集
训练跑完以后,第一件要做的事不是盯着 mAP 数字高兴,而是拿验证集图片跑一遍推理,肉眼对比模型预测框和真实标签的差异。YOLOv5 自带的 detect.py 就能做到:
python detect.py --weights runs/train/exp/weights/best.pt \ --source ../smoke_damage/images/val \ --conf-thres 0.25 \ --save-txt命令的含义是在验证集图片上做一次前向推理,置信度高于 0.25 的框会画在输出图上,同时把预测的 txt 保存下来。跑完之后,打开 runs/detect/exp 里的图片,重点看两类问题:一是漏检,即图片里有破损但模型没框出来;二是误检,即框住了一个看起来完好的区域。漏检通常说明该样本的特征在训练集中出现得不够,误检则可能是背景纹理干扰。这两种情况都值得回去翻一翻训练集里对应类别的数量和多样性。
如果你想把这套数据往更高版本迁移,比如 YOLOv8 或 YOLO11,其实非常直接,因为这份数据本身就是 YOLO 格式的 txt,只需要把 data.yaml 从 YOLOv5 的写法调整成 ultralytics 库能识别的写法,然后调用 API 训练:
from ultralytics import YOLO model = YOLO('yolov8n.yaml') model.train(data='smoke_damage.yaml', imgsz=640, epochs=150, batch=16)这里要注意,迁移训练时不要直接读取 YOLOv5 的 best.pt 权重给 YOLOv8 用,模型结构不同,权重不能直接加载。建议重新初始化一个新的输出目录,避免缓存文件相互干扰。
进阶思路上,香烟破损检测本质是一个小而典型的工业外观缺陷场景。这份 400 张的数据可以作为冷启动的种子库,真实产线上拍到的每一批新图片,都可以先让已训练模型打上初标签,再人工修改微调,然后回灌进训练集继续迭代。这个流程比重新标注几百张新图要省一半以上时间。我每次拿到新数据集都会强制按“可视化确认标签 → 跑基线 → 看失败 case → 补数据”四步走,而不是一上来就调超参数。记得有一次我为了图省事跳过可视化,结果训练了两天才发现某个类别的标签 id 整体错了,白白浪费大量时间。从那以后,我拿到任何一份标注好的 YOLO 数据集,第一件事永远是先跑一遍可视化脚本把标签错位问题排除掉,希望帮到你。
本文还有配套的精品资源,点击获取