ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLO抽烟检测数据集实战:从目录解析到训练调优与部署避坑

YOLO抽烟检测数据集实战:从目录解析到训练调优与部署避坑

简介:这是一套面向YOLO目标检测任务的抽烟行为检测数据集,适合计算机视觉初学者或算法工程师用于模型训练、验证与调优。资源聚焦“抽烟”类别,提供297张jpg原始图像与297个xml标注文件,二者一一对应,可直接转换为YOLO格式并进入训练流程。压缩包共594个文件,大小约59.11MB,数据规模适中,便于快速下载、本地实验与算力要求不高的环境使用。目前已有375人学习使用,可作为目标检测入门练习或算法效果对比的基准数据。借助成对图片与标注,使用者无需自行采集和标注,即可开展数据集划分、模型训练、mAP评估等操作;同时可参考已有标注风格,熟悉目标框选规范、类别定义与数据组织方式,为后续构建自定义数据集打下基础。

1. 这份YOLO抽烟目标检测数据集:图与标签齐备,解压就能进训练

工地安全巡检、加油站禁烟区监控、车间工位违规行为提醒,抽烟检测往往是这些场景里第一个被提上需求的功能。真去自采数据才发现坑很多:办公区不方便拍,室外雨天画面糊,光组织人力标注几百张图就得耗掉一周。这份YOLO抽烟目标检测数据集把图片和标定数据都打包好了,图片是贴近真实监控角度的抽烟场景,标签是YOLO格式的txt,类别定义清晰,既可以当练手的模板素材,也可以直接作为训练集复用到自己的检测流程里。适合刚入门目标检测、想快速跑通全流程的开发者,也适合做安防项目但不想在数据准备上耗时间的工程师。下面从数据集结构开始拆,讲清怎么用、参数怎么设,以及我实际训练时踩过的几个坑。

2. 数据集内部结构:从目录看到单张标注的归一化坐标

2.1 解压后的目录:images 与 labels 一一对应

拿到压缩包先别急着开训,第一步是把目录结构看清楚。解压后最常见的组织方式是这样:

dataset/ ├── images/ │ ├── train/ │ │ ├── smoke_001.jpg │ │ ├── smoke_002.jpg │ │ └── ... │ ├── val/ │ │ └── ... │ └── test/ │ └── ... ├── labels/ │ ├── train/ │ │ ├── smoke_001.txt │ │ ├── smoke_002.txt │ │ └── ... │ ├── val/ │ │ └── ... │ └── test/ │ └── ... └── README.md

注意 images 和 labels 下的子目录名必须一一对应,同名图片与同名 txt 配对。有些版本会把 train、val 合并到一个文件夹里,靠文件名前缀区分,这种情况后面需要自己重新划分。还有的包会附带 classes.txt,里面写的是类别名,比如smoking,这个文件建议保留,后面写 data.yaml 时能直接抄。

我一般会先跑一条命令确认没有孤儿文件——有图片没标签,或者有标签没图片,都会在训练时报错:

cd dataset for img in images/train/*.jpg; do name=$(basename "${img%.jpg}") [ -f "labels/train/${name}.txt" ] || echo "missing label: ${img}" done

这段脚本遍历 train 目录下每张 jpg,检查同名 txt 是否存在,缺了就打印图片路径。批量数据最怕这种隐性缺失,YOLO 训练时遇到缺标签的图片不会直接崩,但会跳过它,导致实际参与训练的样本比预期少,曲线看着正常,精度却上不去。

2.2 YOLO txt 标注格式:归一化坐标的读法

挑一个 txt 打开,内容大致是这样:

0 0.5123 0.3842 0.1531 0.2167

每行是一个目标框,共五个数字:class_id center_x center_y width height。这里的关键是坐标全部做了归一化,除以了图片的宽和高,值域在 0 到 1 之间。比如0.5123表示目标中心点在图片横向 51.23% 的位置,0.2167表示框高占图片高度的 21.67%。

如果你之前用过 VOC 格式的 XML 或者 COCO 的 JSON,会注意到三种格式的差异:VOC 存的是绝对值xmin ymin xmax ymax,COCO 存的是x y w h,YOLO 统一存中心点加宽高的归一化值。转换关系很简单:

x = int(center_x * image_width) y = int(center_y * image_height) w = int(width * image_width) h = int(height * image_height)

实际做可视化验证时,我都是先把 txt 转回像素坐标再画框,确认标注框跟人、烟的位置对得上,这个习惯能排查掉一大批低级错误。

2.3 用脚本统计样本量、类别分布与图片规格

建议先写个短脚本把家底盘清楚:总共多少张图、多少个标注框、类别分布是否均衡。不要靠肉眼数,几百个文件数错很正常。

import os from collections import Counter from pathlib import Path label_root = Path("labels/train") counter = Counter() total_files = 0 for txt in label_root.glob("*.txt"): if txt.name == "classes.txt": continue total_files += 1 with open(txt, encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue cls = int(line.split()[0]) counter[cls] += 1 print(f"标注文件数: {total_files}") print(f"目标框总数: {sum(counter.values())}") for cls_id, count in sorted(counter.items()): print(f"类别 {cls_id}: {count} 个目标")

这段代码遍历 labels/train 下所有 txt,逐行读取类别编号并计数。Counter统计出来的分布能直接告诉你数据是否失衡:如果某个类别只有几十个框,而主类别有上千个,后面训练就要考虑加权或增广。顺带注意,有些 txt 文件可能是空文件,表示这张图没有任何目标,line.strip()会跳过空行,不会误判。

图片尺寸也要确认一下,混用 1920x1080 和 1280x720 本身问题不大,YOLO 会统一 resize,但如果图片尺寸差异过大,比如混入竖屏截图,训练时会被强行拉成正方形,导致目标变形。

from PIL import Image import os img_dir = "images/train" sizes = {} for name in os.listdir(img_dir)[:50]: with Image.open(os.path.join(img_dir, name)) as im: sizes[name] = im.size unique_sizes = set(sizes.values()) print("抽样图片尺寸集合:", unique_sizes)

抽样前 50 张图片,打印尺寸集合,如果出现(720, 1280)这种竖屏尺寸,就要考虑单独处理或直接剔除,否则会影响预训练权重迁移的效果。

3. 接入 YOLO 训练流程:数据划分、配置与超参数怎么设

3.1 划分训练集与验证集:固定随机种子保证可复现

如果压缩包里已经分好了 train 和 val,可以跳过这一步。如果没分,或者你想重新划分,我习惯用脚本按 85:15 拆,而不是手动拖文件——手动拖出来的验证集往往会带着人类自己的偏好,比如总把目标大的图片放进验证集,最后指标虚高,部署现场露馅。

import random import shutil from pathlib import Path random.seed(42) img_root = Path("images") label_root = Path("labels") imgs = sorted(img_root.glob("*.jpg")) random.shuffle(imgs) split_idx = int(len(imgs) * 0.85) for split, file_list in [ ("train", imgs[:split_idx]), ("val", imgs[split_idx:]), ]: (img_root / split).mkdir(exist_ok=True) (label_root / split).mkdir(exist_ok=True) for img_path in file_list: txt_path = label_root / (img_path.stem + ".txt") if not txt_path.exists(): print(f"跳过缺失标签的图片: {img_path.name}") continue shutil.move(str(img_path), str(img_root / split / img_path.name)) shutil.move(str(txt_path), str(label_root / split / txt_path.name))

random.seed(42)是关键,固定住 shuffle 的顺序,这样每次划分结果都一样,方便对比实验。split_idx按 85% 切分,留 15% 做验证集——样本量大可以放宽到 90:10,但几百张图的量级,15% 的验证集大约有几十张,已经够看出模型趋势了。移动文件时检查标签是否存在,缺失标签的图片宁可跳过,也别带进训练集。

3.2 data.yaml 配置:路径、类别名与类别数

YOLOv5 和 YOLOv8 的训练入口不同,但都需要一个 data.yaml 文件来告诉模型数据在哪、有几类。写法如下:

path: D:/work/smoke_dataset # 数据集根目录,必填绝对路径 train: images/train val: images/val nc: 1 names: 0: smoking

nc是类别总数,这个数据集如果只有抽烟一类,写 1 就行。names字典的索引必须和 txt 里的class_id对应:txt 里写0,那names的键必须是0。我见过有人把 names 写成{smoking: 0},顺序反了,训练时装扮跑完,精度曲线直接横盘,查了半天才发现是配置问题。

路径这里多说一句:path写绝对路径最稳。用相对路径在本地能跑,换台机器或挪了目录就裂开。Windows 下路径分隔符用正斜杠/或双反斜杠\\,不要写单反斜杠\,YAML 解析会出幺蛾子。

3.3 训练命令与超参数:从预训练权重开始收敛更快

数据量不大的前提下,不要从空权重开始训练,强烈建议用预训练权重做迁移学习。命令如下:

# YOLOv5 写法 python train.py --data data.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --device 0 # YOLOv8 写法 yolo detect train data=data.yaml model=yolov8s.pt imgsz=640 batch=16 epochs=100 device=0

几个参数按经验说:imgsz=640是速度和精度的平衡点,监控画面里烟占比很小,设成 960 或 1280 能明显提升小目标召回率,但显存占用和推理耗时同步上涨;batch取 2 的幂,16 在 8GB 显存下配 yolov5s 没压力,如果显存紧张先降到 8 或 4;epochs=100对几百张图的数据集是够的,再多就有过拟合风险,后面看曲线再决定要不要加。

还有个容易被忽略的细节:训练时尽量保持终端窗口在前台跑完,不要一边训练一边开一堆软件。YOLO 训练过程中如果显存被其他程序抢走,可能中途报 CUDA OOM,前面跑的几十个 epoch 白搭。建议把训练日志用tee存一份:

python train.py --data data.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 2>&1 | tee train.log

这样训练过程实时打印到屏幕,同时落盘到 train.log,后面画 loss 曲线时直接解析日志文件,不用靠记忆复现。

4. 训练效果的验证与调优:曲线、可视化与增广手段

4.1 训练曲线怎么读:过拟合的信号是 val loss 掉头向上

训练结束后,YOLOv5 会生成runs/detect/train/results.png,YOLOv8 对应runs/detect/train/results.csv和曲线图。先把曲线分成两组看:train loss 和 val loss。train loss 一直降是正常的,关键是 val loss 降到某个点后开始反弹——这个反弹点就是过拟合的起点,最佳权重通常保存在 val loss 最低的那个 epoch。

import csv with open("runs/detect/train/results.csv") as f: rows = list(csv.DictReader(f)) best = max(rows, key=lambda r: float(r["metrics/mAP50(B)"])) print("mAP50 最高的 epoch:", best["epoch"])

用脚本直接找出 mAP50 最高的记录,比肉眼盯曲线快得多。mAP50是 IoU 阈值 0.5 时的平均精度,mAP50-95是 0.5 到 0.95 逐档算的平均,后者对框的定位精度要求更高。抽烟检测这种场景,mAP50 足够衡量,但如果做的是精确测量类任务,就得盯 mAP50-95。

顺带注意 P 和 R 两个指标:P(精确率)高、R(召回率)低,说明模型框出来的基本都对,但漏掉了很多目标,常见于烟在画面里太小的情况;反之 P 低 R 高,就是误报多,把类似烟的东西也框进来了。评估时两个都要看,只看一个容易被带偏。

4.2 把标注框画回原图:验证标签可信度,排除配置错误

训练前我总要把原始标注画回图片上检查一遍。方法很多,最简单的是直接读 txt 画框:

import cv2 img = cv2.imread("images/val/sample.jpg") h, w = img.shape[:2] with open("labels/val/sample.txt") as f: for line in f: parts = line.split() if len(parts) != 5: continue cls, xc, yc, bw, bh = parts xc, yc, bw, bh = map(float, (xc, yc, bw, bh)) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f"cls{cls}", (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) cv2.imwrite("check_annotation.jpg", img)

这段代码把归一化坐标乘回图片宽高,还原成像素坐标画出矩形框。len(parts) != 5的过滤条件能顺手找出格式异常的标注行。如果画出来的框明显偏移,比如框在人的胸口而不是嘴附近,大概率是标注导出时坐标算错,这类污染数据留在训练集里会直接拉低精度。

4.3 数据增强与类别不均衡:用对参数比堆数据更划算

YOLOv5 的增强参数集中在hyp.yaml,几个影响大的我列一下:

hsv_h: 0.015 # 色调扰动 hsv_s: 0.7 # 饱和度扰动 hsv_v: 0.4 # 明度扰动 fliplr: 0.5 # 水平翻转概率 mosaic: 1.0 # 马赛克增强 mixup: 0.1 # 混合增强

对于抽烟检测,fliplr=0.5可以开,抽左手烟和右手烟在监控里都有。但flipud(上下翻转)别开,监控摄像头装在屋顶,视角固定,目标永远在画面下方三分之一区域,上下翻转会生成现实中不存在的构图,白白增加学习难度。

类别不均衡的常见解法是给少数类加权。如果数据里有两类,比如smoking和cigarette,smoking有 800 个框而cigarette只有 60 个,可以在 loss 里给cigarette更高的权重,或者在增强时对少数类样本做复制粘贴增强。我自己遇到这种情况,第一反应是先检查标注,确认少数类是不是漏标了——漏标的样本加到权重里,只会放大错误。

5. 抽烟检测数据集避坑清单:五个高频问题的排查路径

5.1 loss 正常下降但 mAP 全程为 0

现象:训练跑得很顺,train loss 一路降到 0.02 左右,但验证集 mAP 始终为 0,P 和 R 也都是 0。

原因:最常见的是类别配置错位。data.yaml 里nc值写大了,比如只有 1 类却写了nc: 2,模型强行学一个不存在的第二类。另一种可能是 txt 里的 class_id 越界,标注里写的是2,而 names 只定义到1。

解决:先跑 5.2 的可视化脚本确认标注坐标正常,再检查 data.yaml:nc必须和标注里的类别数完全一致,names索引从 0 开始连续编号。然后用grep -rn "^1" labels/train/ | head看有没有类别 1 的标注,有的话就要决定是合并类别还是修改 nc。

5.2 中文路径导致图片读取失败

现象:训练刚开始就报错,FileNotFoundError或者cv2.error: can't open file,单独打开图片却能正常显示。

原因:Windows 下数据集放在D:\抽烟检测数据集\images这类带中文的目录里,OpenCV 底层用的是imread,对中文路径支持不好,读不到文件。

解决:把整个数据集目录改成纯英文路径,比如D:/work/smoke_dataset,同时检查 data.yaml 里path字段不要出现中文。这是血泪教训,分类任务可能不受影响,目标检测训练几乎必炸。

5.3 标签框坐标出现负值或大于 1

现象:训练日志里频繁出现WARNING: ignoring corrupt label或者类似提示,训练能跑但收敛很慢。

原因:标注导出过程出错,坐标归一化时除错了图片尺寸,导致width或height变成负数,或者中心点坐标超出 0-1 范围。也可能是某些标注软件保存了0 0 0 0 0这种空框。

解决:写个清洗脚本,把所有越界框裁剪回 0-1 范围内,完全在画面外的直接删掉:

def clean_label(txt_path): lines = [] with open(txt_path) as f: for line in f: parts = line.split() if len(parts) != 5: continue cls, xc, yc, bw, bh = parts[0], *map(float, parts[1:]) x1, y1 = xc - bw / 2, yc - bh / 2 x2, y2 = xc + bw / 2, yc + bh / 2 # 完全越界直接丢弃 if x2 < 0 or y2 < 0 or x1 > 1 or y1 > 1: continue # 部分越界则裁剪边界 x1, y1 = max(0.0, x1), max(0.0, y1) x2, y2 = min(1.0, x2), min(1.0, y2) nw, nh = x2 - x1, y2 - y1 if nw <= 0 or nh <= 0: continue lines.append( f"{cls} {(x1+nw/2):.6f} {(y1+nh/2):.6f} {nw:.6f} {nh:.6f}" ) with open(txt_path, "w") as f: f.write("\n".join(lines))

清洗完再跑一次统计脚本,确认没有空文件和异常坐标再训练。

5.4 小目标漏检:烟在远处只占几个像素

现象:验证集 mAP 有 0.8 以上,但放到真实监控视频里,画面远端的人嘴上叼着烟,检测框不出来。

原因:输入图片 resize 到 640x640 后,远处的烟可能只有 3x3 像素,特征信息基本丢失。YOLO 默认的 anchor 和特征层对这种极小目标不敏感。

解决:优先把imgsz提到 960 或 1280,训练和推理都用同一尺寸。显存不够就切成两段跑:先训练 640 尺寸,确定模型能收敛,再微调时用 1280 尺寸跑几十轮。另外把conf_thres从默认 0.25 降到 0.15 能挽救一部分召回,代价是误报增加,需要配合 NMS 的iou_thres一起调。

5.5 训练到一半显存 OOM

现象:前几个 epoch 正常,跑到第 40 个 epoch 左右突然报CUDA out of memory,进程被杀。

原因:mosaic 增强会在训练途中动态拼接多张图,导致实际输入分辨率波动,显存占用峰值出现在 epoch 中段而不是开头。如果batch=16刚好卡在显存边缘,增强一加强直接溢出。

解决:把 batch 降到 8,或者开启梯度累积。YOLOv5 可以用--nbs 64参数,自动把大 batch 拆成多次前向累积梯度。再不行就把--cache从ram改成disk,把图片缓存从内存挪到硬盘,能再省出几个 GB。

6. 部署时的压缩与验证技巧:从 ONNX 导出到现场视频逐帧确认

6.1 导出 ONNX 和半精度模型:体积减半,速度翻倍

训练完的 PyTorch 权重(best.pt)不适合直接扔到生产环境,体积大、依赖重。常规做法是导出成 ONNX,再根据部署端转成 TensorRT 或 OpenVINO 格式:

yolo export model=runs/detect/train/weights/best.pt format=onnx half=True imgsz=640

half=True会把权重转成 FP16,模型体积直接减半。注意这一步需要推理端支持 FP16,NVIDIA 的 GPU 没问题,CPU 推理反而可能变慢。导出后用 onnxruntime 做个快速推理,确认输出张量形状和预期一致再继续集成。

如果部署目标是 Jetson 这类嵌入式设备,继续转 TensorRT 能再提速一截,但 TensorRT 的 engine 是跟 GPU 型号绑定的,换一块卡就要重新转。工程上我一般保留 ONNX 作为中间格式,设备端跑转换脚本,不直接分发 engine 文件。

6.2 用现场真实视频逐帧验证:测试集指标好看不等于能用

模型训练完,最后一个动作是用一段未参与训练的真实场景视频逐帧跑推理,观察误报和漏报:

from ultralytics import YOLO import cv2 model = YOLO("best.pt") cap = cv2.VideoCapture("site_test.mp4") fps = cap.get(cv2.CAP_PROP_FPS) out = cv2.VideoWriter( "site_test_result.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (1280, 720), ) while True: ret, frame = cap.read() if not ret: break results = model.predict(frame, conf=0.25, iou=0.5, imgsz=640, device=0) annotated = results[0].plot() out.write(annotated) cap.release() out.release()

conf=0.25是置信度阈值,低于这个值的框直接丢弃;iou=0.5是 NMS 的交并比阈值,多个重叠框合并时用。现场光线差时把conf调到 0.2 能降低漏检,但也要接受更多误报。

想起有回给某仓储项目做抽烟检测,测试集 mAP 刷到 0.9 出头,结果拿现场实拍视频一过,货架阴影里人手的轮廓被框了一堆。仔细排查发现验证集里大多是目标明显的近景样本,现场那种远距离影子场景根本没覆盖。从那以后我每次训完模型,都会强制跑一段没参与训练的现场视频逐帧确认,误报率能接受才交付。希望这个习惯和这份数据集一起帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进