简介:面向公共场所与室内监控场景的抽烟检测任务,这份资源整合了一千张真实与合成混合的高质量图片,覆盖街景、写字楼、办公室、楼道以及遮挡行人、严重遮挡行人等多样场景,其中包含密集人群、遮挡等复杂情况,并附有VOC、COCO、YOLO三种格式的标注,采用labelimg完成高质量标注,可直接用于主流目标检测模型训练。包体为一个PDF文件(8.92MB),用于呈现数据集详情与百度网盘下载方式,避免直接下载大文件;同时内含YOLO11一键训练脚本,已配置好数据集路径与超参数,支持GPU(GPUs)、CPU、Mac(M芯片)三平台运行,并带有作者训练日志供参考。已有七百五十九人学习浏览,适合算法工程师、学生及监控场景项目开发者用于抽烟检测模型的构建与数据补充,作为通用监控数据集的场景补充尤为实用。
1. 抽烟检测数据集:从标好的1000张图到三格式训练管线
做公共场所抽烟检测的人都知道,真正的难点不是“有没有烟”,而是暗光、逆光、手部遮挡和远处的小目标。我拿到这套抽烟检测数据集时,第一反应是“又是一堆普通人脸特写”。翻完才发现,它把街景、写字楼、办公室、楼道抽烟都放进去了,还有严重遮挡行人的样本,这正好是监控场景最常翻车的地方。数据集本身只有 1000 张图,但提供了 VOC(xml)、COCO(json)、YOLO(txt) 三套标签,省掉了几乎所有格式转换时间。配套的 YOLO11 一键训练脚本直接兼容 GPU、CPU 和 Mac M 芯片,意味着本地笔记本也能先把流程跑通,再上服务器跑大 batch。这篇文章我会把数据格式、脚本参数、训练日志和部署验证完整拆一遍,尤其会讲清楚三套标签之间容易踩的坑。
2. 数据集构成与标注格式转换:VOC/COCO/YOLO 三套标签不是简单改名
2.1 1000张图里有哪些容易翻车的场景
这套数据集的缩略图我看过一遍,真实场景占了大部分,合成场景补足了“手完全挡住烟头”这一类极端情况。分类别统计下来,单人抽烟、多人聊天抽烟、行人边走边抽都有覆盖。最值得说的是两类:一类是写字楼玻璃幕墙前的逆光抽烟,烟雾几乎看不清,但手部姿态仍然能标注出烟盒或烟蒂;另一类是严重遮挡行人抽烟,人过半身体被栏杆或绿植挡住,目标框只有一小截,这直接决定了模型不能只靠“人脸+烟”这种粗暴特征。
从目标检测的训练角度看,这类样本让模型必须学会用上下文推理。比如在楼道场景里,烟头小、对比度低,但手指弯曲的轮廓和烟头的高亮区域是稳定的局部信号。正因为如此,我在实际项目里不会把 1000 张图直接切 7:3,而是会优先把严重遮挡样本单独分一个验证集,避免验证集被简单样本“喂饱”,导致上线后遇遮挡就漏检。
2.2 labelimg标注的字段与坐标基准
数据集说明里写了用 labelimg 标注。我用 labelimg 大量标过数据,它的流程是画矩形框,然后保存为 Pascal VOC 的 xml 文件。一个典型 xml 包含folder、filename、size和多个object节点。每个 object 里除了name和difficult,最关键的是bndbox下的四个整数坐标:
<object> <name>smoking</name> <bndbox> <xmin>112</xmin> <ymin>68</ymin> <xmax>356</xmax> <ymax>402</ymax> </bndbox> </object>这四个值都是绝对像素坐标,基于图片原始尺寸。labelimg 会读图片的宽高写入size节点,所以后续转 YOLO 格式时可以直接拿width和height做归一化。有一点容易被忽略:labelimg 的矩形框在图像边缘时,xmin 或 ymin 可能为 0,xmax 可能等于图片宽度,这种情况下直接转 YOLO 会得到1.0的边界值,训练时有些版本会告警甚至忽略样本。正确做法是在转换脚本里对坐标做一次min(max(x, 0), width-1)的裁剪。
2.3 从VOC到YOLO:归一化坐标转换的坑
虽然数据集本身已经提供了 YOLO txt,但做目标检测的人经常会自己补标注,或者合并别的数据集。我一般会先写一个转换脚本,把新增的 VOC xml 转成 YOLO 格式,这样做的好处是后面统一用 YOLO 训练,不需要同时维护两套解析逻辑。下面这段是核心转换代码:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, output_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in class_names: continue cls_id = class_names.index(name) box = obj.find('bndbox') xmin = int(float(box.find('xmin').text)) ymin = int(float(box.find('ymin').text)) xmax = int(float(box.find('xmax').text)) ymax = int(float(box.find('ymax').text)) xmin = max(0, min(xmin, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) xmax = max(0, min(xmax, img_w - 1)) ymax = max(0, min(ymax, img_h - 1)) box_w = xmax - xmin box_h = ymax - ymin if box_w <= 1 or box_h <= 1: continue x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w_norm = box_w / img_w h_norm = box_h / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}") if lines: base_name = os.path.splitext(os.path.basename(xml_path))[0] out_path = os.path.join(output_dir, base_name + '.txt') with open(out_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) class_names = ['smoking'] voc_to_yolo('img_001.xml', './labels', class_names)这段代码的核心逻辑是:先从 xml 里读出图片宽高,再遍历每个 object,把 xmin/ymin/xmax/ymax 统一转成中心点加宽高的归一化值。参数class_names决定了类别索引,索引必须与训练时的 data.yaml 里类别顺序一致,否则会出现“标签对不上类别”的经典错误。裁剪操作放在宽高计算之前,避免越界框导致数值失真。过滤掉宽高小于等于 1 的框是常识性保护,因为这类框通常是误标,留着只会增加损失震荡。
2.4 COCO格式的JSON结构校验
COCO json 不像 VOC 那样一眼能看懂,它需要images、annotations、categories三个顶层字段。images里每项要有id、file_name、width、height;annotations里每项要有image_id、category_id、bbox,而bbox的格式是[x, y, width, height],注意是左上角坐标加宽高,不是中心点。这一点和 YOLO 的表示完全不一样,很多人直接把 COCO 的 bbox 当成 YOLO 用,导致训练时框全偏。
拿到数据集后,我会用一段简短代码验证标签和图片是否对得上:
import json from PIL import Image with open('annotations/instances_train.json') as f: coco = json.load(f) img_map = {img['id']: img for img in coco['images']} for ann in coco['annotations']: img = img_map[ann['image_id']] x, y, w, h = ann['bbox'] if x < 0 or y < 0 or x + w > img['width'] or y + h > img['height']: print(f"越界标注: {img['file_name']} bbox={ann['bbox']}")这段代码用PIL读取图片宽高和 json 里的图片信息做比对,越界框会直接打印出来。更严格的做法是打开图片逐张查看,但 1000 张图人工复查太耗时。用这种脚本先做机器校验,再抽查几十张,基本能保证标签质量。三套格式的字段差异可以归纳成下面这张表:
| 格式 | 坐标类型 | 存储载体 | 类别映射 | 典型问题 |
|---|---|---|---|---|
| VOC xml | 绝对像素 xmin/ymin/xmax/ymax | XML 标签 | 类别名字符串 | 边界框越界、difficult 遗漏 |
| COCO json | 绝对像素 x/y/width/height | JSON 数组 | 数字 category_id | bbox 格式混淆 |
| YOLO txt | 归一化 x_center/y_center/w/h | 每行一个目标 | 数字类别索引 | 索引顺序错乱、浮点精度 |
如果数据集里同时给了三种格式,最稳妥的做法是随机抽 20 张图,分别用三个格式读同一个目标,确认像素坐标一致。我自己碰到过两次三套标签之间因为坐标取整方式不同产生 1 像素偏移,虽然影响不大,但对小目标检测来说,1 像素误差就足以让 IoU 从 0.51 掉到 0.47。
3. YOLO11 一键训练脚本:GPU、CPU、Mac 三平台怎么选设备
3.1 脚本整体流程与设备探测
这套资源附带的是 YOLO11 一键训练脚本。所谓“一键”,核心是脚本先探测当前机器的计算设备,再根据设备类型决定是否调用 GPU、CPU 或 MPS。常见的做法不是让用户手动改 device,而是通过一个简单的环境变量或自动检测来完成。我看过的脚本结构大致是这样:
#!/bin/bash # 自动选择设备 if command -v nvidia-smi &> /dev/null; then DEVICE="0" echo "检测到 NVIDIA GPU,使用 device=0" elif [[ "$(uname)" == "Darwin" ]]; then DEVICE="mps" echo "检测到 macOS,使用 MPS 加速" else DEVICE="cpu" echo "未检测到 GPU,使用 CPU" fi # 加载 YOLO11 训练命令 python train.py \ --data smoke.yaml \ --model yolo11s.pt \ --epochs 100 \ --batch 16 \ --imgsz 640 \ --device $DEVICE这段脚本最关键的DEVICE变量决定了模型跑在哪个后端。nvidia-smi存在说明有 NVIDIA 驱动,这时直接用device=0指向第一张卡;如果机器是 mac 且不是 Intel 集显,就用mps,这是 PyTorch 在 Apple Silicon 上的 Metal 加速后端。最容易被忽略的是 Mac 在第一次运行 MPS 时,有些 YOLO 版本会报mps not supported,这时候需要往PYTORCH_ENABLE_MPS_FALLBACK=1环境变量兜底,让不支持的操作回退到 CPU。这个处理通常会写在脚本的export一行里。
3.2 data.yaml 与超参数设置
训练开始前,脚本会先读一个smoke.yaml文件。这个文件告诉 YOLO11 数据集在哪里、有几类、类别名是什么。我拿到数据集后一定会先检查这份 yaml 里的路径是不是绝对路径,如果是项目里的相对路径,脚本一旦换目录就会训练直接报错。规范的 data.yaml 长这样:
path: /home/user/smoke_dataset train: images/train val: images/val test: images/test nc: 1 names: ['smoking']path是数据集根目录,train、val、test是相对path的图片目录。nc=1表示单类别,names列表的顺序必须和标签 txt 里的类别索引完全一致。如果数据是分好的目录,脚本通常会自动识别图片和同名 txt。注意 YOLO11 的新版本里还支持val为空时自动从 train 里切分,但比赛中还是显式指定更稳妥。
超参数方面,脚本里最常见的配置是epochs=100 batch=16 imgsz=640。这是基准值,但不同平台差别很大。下表是我实际跑过的推荐参数:
| 平台 | 设备参数 | 推荐 batch | 推荐 imgsz | 备注 |
|---|---|---|---|---|
| NVIDIA GPU (8G显存) | device=0 | 16 ~ 32 | 640 | 显存不足时降 batch |
| NVIDIA GPU (24G显存) | device=0 | 64 | 640 | 可开启多卡 DDP |
| CPU (6核) | device=cpu | 4 ~ 8 | 512 | 训练慢,建议先测试 |
| Mac M1/M2 (8G内存) | device=mps | 8 ~ 16 | 640 | 需开启 PYTORCH_ENABLE_MPS_FALLBACK |
如果你用batch=64却显存溢出,最常见的报错是RuntimeError: CUDA out of memory。这时候不要马上降 imgsz,先看是不是开了太多数据加载线程。脚本里通常会有workers参数,默认 8,在 CPU 平台上workers设成 0 反而更稳定,因为多线程任务调度会占大量 CPU 时间片。
3.3 不同平台训练脚本写法差异
跨平台脚本最容易出错的是路径分隔符和 Python 包冲突。Linux 下用os.path.join没问题,但 Windows 下如果数据路径里有反斜杠,yaml 解析会出问题。我把训练脚本里的所有路径统一改成Path对象来拼接,能省很多麻烦。下面是一个更健壮的设备选择片段:
import platform import torch if torch.cuda.is_available(): device = "0" elif platform.system() == "Darwin" and torch.backends.mps.is_available(): device = "mps" else: device = "cpu" print(f"使用设备: {device}")torch.backends.mps.is_available()是 PyTorch 官方 API,比只看platform.system()更可靠。有些 Mac 上的 Python 是 Intel 版本,即使系统是 macOS,MPS 也不能用。脚本训练过程中如果发现 loss 完全不下降,可以先跑一行测试代码确认 mps 是否真的参与计算:
python -c "import torch; print(torch.zeros(1).to('mps'))"能正常返回 tensor 才代表 Metal 后端可用。CPU 平台最容易忽略的是torch.set_num_threads设置。默认 PyTorch 会占用所有核心,反而导致内存带宽不足。我一般会在 CPU 训练前固定线程数为物理核心数的一半,比如 6 核机器设为 3,训练速度反而提高 10%。
4. 训练日志解读与遮挡场景调优
4.1 看懂 YOLO11 训练输出
YOLO11 训练过程中的标准输出一般会隔一段时间打印一组指标。很多人看到满屏 loss 就开始焦虑,其实只需要盯几个关键值。典型输出长这样:
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 49/100 4.21G 0.7324 0.6153 1.0214 18 640 Class Images Instances BoxP R mAP50 all 300 512 0.631 0.588 0.604 0.351box_loss是边界框回归损失,cls_loss是分类损失,dfl_loss是分布焦点损失。这三个值在训练初期会快速下降,到了中后期会在一个区间内波动,不用追求无限趋近于零。真正决定模型好不好用的是下面那行mAP50和mAP50-95。map50表示 IoU 阈值 0.5 时的平均精度,对于抽烟检测这种单类别任务,map50到 0.85 以上才具备落地价值。map50-95则是在多个 IoU 阈值下的平均,更能反映框的贴合精度。
如果你发现box_loss一直不降,但cls_loss降了,说明模型已经能分辨出抽烟目标,但框的位置总偏。这种问题通常出在数据集的标注框本身不规整上,比如有些人习惯把整只手框进去,有些人只框烟头,模型学到的是一个“平均框”,自然两边都不满意。
4.2 用日志判断模型是否欠拟合/过拟合
训练脚本生成的results.csv会把每个 epoch 的指标都存下来。我拿到结果日志后,最先做的是生成一张 loss 曲线图。如果在第 60 个 epoch 训练 loss 还在下降,但验证集 mAP 开始下降,说明过拟合了。抽烟检测数据集只有 1000 张图,这是非常容易出现的情况。解决办法不是加数据,而是先把epochs从 100 降到 60,同时打开早停机制。
YOLO11 内置patience参数,设置为 10 代表连续 10 个 epoch 验证集 mAP 不提升就自动停止。脚本里通常这样配置:
python train.py --data smoke.yaml --model yolo11s.pt --epochs 100 --patience 10patience=10在这种小数据量任务上很有效。第 40 个 epoch 前后如果 mAP 还没有明显提升,早停会直接结束训练,省下大量时间。另一个容易被忽略的指标是BoxP和R之间的差距。如果 P 高但 R 低,说明模型漏检严重,尤其对遮挡行人。对这种场景,我一般会把置信度阈值调低到 0.15 重新评估,看 R 能不能拉上去。
4.3 针对遮挡行人的数据增强和YOLO11参数
这套数据集里严重遮挡行人抽烟占了相当比例,这类目标的特点是框小、上下文少。我在训练时不会用默认的数据增强参数,而是重点调三个:mosaic、mixup、hsv_v。mosaic把四张图拼成一张,能提升模型对遮挡目标的鲁棒性,因为目标在拼接边界处经常被切半。mixup则让两张图叠加,对烟雾这样半透明目标很有用。YOLO11 在超参文件hyp.scratch-low.yaml中默认开启 mosaic,但遮挡场景可以再增大概率。
具体训练时,我会在命令行里直接覆盖超参:
yolo detect train data=smoke.yaml model=yolo11s.pt epochs=100 batch=32 imgsz=640 device=0 \ mosaic=1.0 mixup=0.2 hsv_v=0.3 fliplr=0.5mosaic=1.0代表每次迭代都应用 mosaic,mixup=0.2代表 20% 的概率做图像混合。hsv_v=0.3是亮度扰动幅度,因为楼道场景光线暗,增大亮度扰动可以模拟更多暗光条件。fliplr=0.5是水平翻转,默认 0.5 即可。注意不要开degrees随机旋转,抽烟目标本身没有固定朝向,旋转反而会让烟头形状失真。
yolo11s.pt是 YOLO11 的小模型,参数量少,在 CPU 上也勉强能跑。如果追求精度,可以换成yolo11m.pt,但 Mac 上训练时间会暴涨。我用小模型在 1000 张图上训练 80 个 epoch,mAP50 能到 0.79,换成中模型提升到 0.83,但训练时间多了一倍。对小目标偏多的监控画面,中模型反而更值得。
5. 验证与部署:用训练好的权重做推理和导出
5.1 验证验证集效果与单张图推理
训练完 r 目录下会有best.pt,但不要急着拿去部署。我习惯先看验证集的表现,因为训练日志里的 mAP 是整体均值,不能反映某个特定场景的漏检。把严重遮挡样本单独拿出来,用下面命令跑一次推理:
python detect.py --source ./val_hard/ --weights runs/detect/train/weights/best.pt --conf 0.25 --imgsz 640conf=0.25是置信度下限,遮挡场景可以降到 0.1,但对误检敏感的项目不建议低于 0.2。跑完之后生成的结果图里,我会特别注意两类目标:一个是手部完全挡住烟头但烟头在指缝间露出的图,另一个是远处小目标。如果这两类都能稳定框出来,模型基本就能上线。
5.2 导出ONNX/CoreML时的格式与后端注意点
YOLO11 训练好的best.pt可以导出成 ONNX、OpenVINO、CoreML 等格式。CPU 服务器上最推荐 ONNX,配合 ONNX Runtime 比 PyTorch 原始推理快 1.5 到 2 倍。导出命令:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 opset=12opset=12是为了兼容旧版 ONNX Runtime 的常用设置。如果要在 Mac 上做本地应用,导成 CoreML 更合适:
yolo export model=best.pt format=coreml imgsz=640 nms=Truenms=True会在 CoreML 模型里合并非极大值抑制逻辑,这样读取模型的人不需要额外写后处理。不过 CoreML 导出时容易遇到torch 1.13 与 coremltools 版本不兼容的报错,常见做法是用 conda 新建环境安装coremltools=7.1,再重新执行导出。这个问题我在不同机器上遇到过多次,几乎每次都是版本问题。无论导出哪种格式,都要拿同一张测试图对比原始 PyTorch 模型和导出模型的 bbox,数值差在 0.01 以内才算成功。
本文还有配套的精品资源,点击获取