
简介目标检测领域的抽烟行为识别经常因场景复杂导致样本不足。这份PDF文档用于介绍一份1000张真实与合成结合的抽烟图片数据集该数据集覆盖街景、写字楼、办公室、楼道以及严重遮挡行人等典型监控场景并配套提供VOC、COCO、YOLO三种格式标签可直接用于主流目标检测框架的训练。文档内含labelimg标注截图和数据集缩略图可直观评估标注质量与场景多样性同时附赠GPU(GPUs)、CPU、Mac(M芯片)三平台下的YOLO11一键训练脚本以及博主实测训练日志能够帮助使用者快速完成环境部署、参数验证和结果对比减少从数据到落地的中间环节。包体为单个PDF文件大小仅8.92MB却包含完整的数据集介绍、获取方式与训练示例目前已有761人学习下载适合在公共安全、智慧安防等方向开展抽烟检测项目的开发者。1. 1000张图的抽烟检测数据集三格式标签与跨平台脚本能解决什么问题工地安全帽底下的烟头、加油站禁烟区的打火机、网吧监控画面里的火星这类需要实时告警的细粒度目标检测需求特别容易在数据集上翻车网上能找的公开抽烟数据集要么像素不高要么只给一张图片列表标签格式还得自己猜。而标题里这套方案把目标检测数据集的最后一公里补齐了——1000张图不算大但搭配好类定义、干净的三格式标签VOC/COCO/YOLO和一份能在GPU/CPU/Mac上直接跑的YOLO11一键训练脚本完全足够撑起一个可用的安检告警基线。适合刚立项想快速验证的算法工程师也适合安防集成商在交付前自己训一版看看效果。真正上手前最值得花时间的是先搞清楚这份数据集的边界条件而不是急着把训练命令敲下去。2. 抽烟检测数据集的第一道坎类边界、标注颗粒度与训练集划分2.1 类边界先定义“烟”还是“抽烟”很多人拿到一个标志好的数据集直接打开标注文件统计类别数量看到数字就开训。但抽烟检测的难点根本不在画框而在于类边界怎么定——是“嘴里有烟”还是“手里有烟”这两个定义训出来的模型落地的表现完全不同。我把这个问题拆成三条判断每次做项目讨论时都能吵起来。第一条检测目标是“烟支”还是“抽烟动作”如果你只要抓“正在抽”那烟在嘴边、有烟气、手部有遮挡都算但视频抽帧里大量出现的是“手指夹着烟没放到嘴边”这时候模型如果把拿烟也算正样本告警就会把开会时手里夹烟的人全部触发现场根本没法用。第二种电子烟、雪茄、烟斗要不要纳入同一个类别部署方要的告警语义说了算禁烟场所通常把电子烟也算烟那就一起标成smoking不要试图让模型自己发现区别。第三种笑起来露出的棒棒糖、白色笔杆、手指交叉这些负样本在真实监控里远比正样本多类边界的取舍会直接决定误报率。所以拿到这个1000张图的数据集第一件事不是拆压缩包而是打开标签文件统计类别名和数量确认它是单类还是多类。如果是单类训练脚本里nc: 1和names: [smoking]必须对得上如果它把拿烟和抽烟拆成两个类你的告警逻辑就要回到决策层去处理而不是在模型里硬压。玩手机目标检测也有同样的纠结检测手机本身还是检测“正在玩”这个动作数据集的标注口径直接决定交付效果这一点在抽烟检测上更明显因为烟比手机小得多类边界错了后续所有调参都是在错误前提上打转。2.2 标注颗粒度bbox 框到手和烟而不是框整个人抽烟检测常用的锚框策略直接影响YOLO这类单阶段检测器能不能在它的正负样本分配机制下真正学会这个目标。我用的标注口径是框住“持烟的手 烟支”的小区域而不是框整个人或半身人。先说推理侧。监控画面里人通常是直立的中远景整个人框高度可能占300像素以上而烟支只有二三十像素YOLO的输出特征图在640分辨率下的最小网格大约对应80×80像素整人框和烟支的目标尺度差得太远训练时不同尺度的样本会互相干扰。再说训练侧若整人框和“烟在嘴边”的姿态耦合太紧模型学会的是“有个人站着或坐着就是烟”而不是“嘴边的亮点是烟”。以后换了机位、换了服装误检立刻暴露。如果摄像头装在低角度或近景人可以占到画面很高比例这时我会把bbox退一步框“嘴部手持烟”的范围或者直接用上半身框。这种改动不改变标注文件结构改的是目标语义批量修改标注时只需要把原始框按比例缩放但前提是标注源头本身用了统一规则。拿到数据集后值得先抽样看20张图确认标注框都落在手部或口部附近如果发现有一部分是人框有一部分是烟框建议把不一致的样本先挑出来重标而不是直接开训。标注颗粒度还会影响后续的数据增强。YOLO11默认的Mosaic增强会把四张图拼在一起如果小框离图像边缘很近剪裁时会切掉烟支出现“烟被腰斩”的增强样本。为了减少这种坏样本标注时框边缘至少要留几个像素的余量不能死死贴紧烟支。很多公开数据集的框是紧贴目标的这类图我通常会单独抽出来看一眼必要时做一次边缘膨胀再进训练。2.3 从原始图到可训练集清洗、去重与train/val/test划分1000张图听起来体量不大但真实项目里原始素材往往是从监控视频抽帧来的相邻帧之间的相似度高得吓人。如果偷懒直接全部拿去做训练验证集和训练集里会出现同一场景几乎重复的图验证指标虚高部署时换个机位就露出原形。我一般会先跑一遍图像去重不用复杂的哈希用感知哈希pHash按汉明距离小于阈值的图片直接删掉只留一眼能区分视角和动作的样张。清洗完的正样本里还要人为掺入一批负样本。抽烟检测最典型的误报来自没有烟、没有火点的手部特写手抓栏杆、拿笔、接电话、搓手指、捧杯子。如果数据集里全是“人手烟”模型找的不是烟而是手势。我习惯从负样本池里随机抽20%30%的空手图放进全部划分里让train/val/test三个集合都保持相同的负样本比例否则验证集的背景分布漂移会在最终评估时被放大。负样本图不用做任何标注直接放在images目录下YOLO训练会把它当作纯背景处理。划分比例我用的是8:1:1。1000张图的规模下训练集约800张验证集100张测试集100张。如果某个场景明显是难例比如夜间、逆光、戴口罩吸烟就应该把它按比例平均放到各集里不要一次性全塞进验证集——那等于提前剧透评估会失真。这一步做完再看一眼每个集里不同动作的分布如果“拿烟”和“烟在嘴边”的比例不均匀训练会偏向多的一方验证集里自然也就看不出来。3. 把抽烟检测标注统一成VOC/COCO/YOLO三格式转换脚本与边界坑3.1 三种格式的差异这三套格式不是同一件事的三种写法坐标体系、类别编号规则和文件组织都不同混着用一定出问题。我习惯用LabelImg做标注它默认导出的是VOC格式的XML但ultralytics的YOLO训练直接吃的是TXT归一化坐标如果后面要接Detectron2或MMDetection做对比实验又需要COCO的JSON。把转换链路理清是这份数据集能真正用起来的前提。格式文件载体坐标表示类别编号常见工具VOC每张图一个XML像素绝对值 xmin, ymin, xmax, ymax字符串类名LabelImg、Pascal VOC生态COCO整个数据集一个JSON像素绝对值 x, y, w, hcategory_id 从1开始Detectron2、MMDetectionYOLO每张图一个TXT归一化 cx, cy, w, hclass_id 从0开始ultralytics 原生三格式都保留不是形式主义。训练时YOLO的TXT最直接验证评估阶段COCO格式更权威因为它自带一套标准的mAP计算方式VOC则是最容易和同事、客户交换的中间格式。很多标注团队交付时只给一种格式但做安防项目的人都知道换一次训练框架就得重新转一次格式踩过的坑不会少。标题里直接把三种都给齐意味着你可以从任意一种格式出发不需要再写一次转换脚本。3.2 从VOC转出COCO和YOLO一个脚本搞定下面的脚本把VOC的XML作为源头一次性生成COCO的JSON和YOLO的TXT。这也是最主流的流转路径LabelImg手工标注后得到的原始产物就是XML。转换时有两个关键点一是类别id偏移二是用opencv而不是PIL读图像尺寸后面会解释为什么。 抽烟检测标注格式转换VOC(xml) - COCO(json) YOLO(txt) 假定目录结构 datasets/ images/ # 所有图片 annotations/ # 所有xml import os import json import cv2 import xml.etree.ElementTree as ET from glob import glob # 类别表顺序就是 YOLO 的 class id 顺序不要随意调整 LABEL_MAP {smoking: 0} YOLO_LABELS [name for name, _ in sorted(LABEL_MAP.items(), keylambda x: x[1])] def voc_to_coco_and_yolo(xml_dir, img_dir, output_json, yolo_dir): images, annotations [], [] ann_id 1 os.makedirs(yolo_dir, exist_okTrue) for xml_path in sorted(glob(os.path.join(xml_dir, *.xml))): tree ET.parse(xml_path) root tree.getroot() filename root.findtext(filename) img_path os.path.join(img_dir, filename) # 用 cv2 读尺寸避免 PIL 遇到 EXIF 旋转给错宽高 h, w cv2.imread(img_path).shape[:2] image_id len(images) 1 images.append({ id: image_id, file_name: filename, width: w, height: h }) yolo_lines [] for obj in root.findall(object): name obj.findtext(name) if name not in LABEL_MAP: continue bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) # 归一化后裁剪到 [0,1]YOLO 不允许数值越界 cx ((xmin xmax) / 2) / w cy ((ymin ymax) / 2) / h bw (xmax - xmin) / w bh (ymax - ymin) / h cx, cy, bw, bh map(lambda v: max(0, min(1.0, v)), [cx, cy, bw, bh]) yolo_lines.append( f{LABEL_MAP[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f} ) annotations.append({ id: ann_id, image_id: image_id, category_id: LABEL_MAP[name] 1, # COCO 类 id 从 1 开始 bbox: [xmin, ymin, xmax - xmin, ymax - ymin], area: (xmax - xmin) * (ymax - ymin), iscrowd: 0 }) ann_id 1 # 空标注图保留TXT 写空内容作为负样本供训练使用 out_txt os.path.join(yolo_dir, os.path.splitext(filename)[0] .txt) with open(out_txt, w) as f: f.write(\n.join(yolo_lines)) categories [ {id: i 1, name: YOLO_LABELS[i]} for i in range(len(YOLO_LABELS)) ] with open(output_json, w) as f: json.dump( {images: images, annotations: annotations, categories: categories}, f ) if __name__ __main__: voc_to_coco_and_yolo( xml_dir./datasets/annotations, img_dir./datasets/images, output_json./datasets/smoking_coco.json, yolo_dir./datasets/labels )这段代码的逻辑很直白遍历XML把每个object解析成一条标注记录COCO那边用像素坐标YOLO那边除以图像宽高做归一化。需要注意LABEL_MAP的顺序就是YOLO类别id的顺序中途加类别会导致已经生成的TXT全部错位所以类别表定下来之后就不要改。COCO的category_id在代码里做了1处理因为COCO从1开始YOLO从0开始这是手动转换最容易漏的一步。cv2.imread那两行不是随便写的。用PIL读图时带有EXIF旋转信息的jpg会返回旋转前的宽高而YOLO画框用的是旋转后的实际像素坐标两边对不上标注会整体偏移。用opencv读一次shape拿到的就是解码后图像的真实尺寸不会踩这个坑。脚本没有做目标检测式的自动校验转换完记得抽几张图把框画出来看一眼再进训练。3.3 转换路上会翻车的四个细节第一类别id偏移。YOLO的class id从0开始COCO的category_id从1开始VOC里是字符串。只要有一个地方漏了1训练时类别就对不上。最坑的表现是推理结果里框的位置准、类别名错乱这种“半错”最难排查。建议转换后打印一行日志把每个类别的id和名称输出一遍对着确认。第二坐标越界。手工标注时偶有超出图边界的框转换脚本里如果不做clip归一化后cx或cy可能大于1模型训练时这部分锚框直接失效loss曲线会出现莫名其妙的震荡。上面的脚本用map(lambda v: max(0, min(1.0, v)), ...)把所有归一化值约束到[0,1]区间。如果你拿到的是别人转好的标签可以用一个简单脚本扫描TXT凡是出现大于1.0或小于0.0的坐标值都说明源头数据有脏框。第三空标注图处理。一个XML里没有object或一张图没有对应XML在YOLO训练里都要特别处理。最稳妥的做法是让每张图都有一个TXT哪怕是空文件否则ultralytics的数据加载器会把缺标签的图跳过训练集数量会悄悄缩水。如果你希望把这类图当作负样本保持TXT为空即可程序会把它视为背景参与训练。第四图像尺寸取错。前面说过EXIF旋转的问题转换脚本里用cv2.imread就是为了避开它。但还有一种情况是图片本身损坏cv2.imread返回None直接取shape会抛异常。转换前先跑一轮检查把所有打不开的图片文件列出来该删的删该补的补不要把这批脏文件留在训练集里。4. 用YOLO11一键脚本训抽烟检测GPU/CPU/Mac三平台的参数差异4.1 为什么这个场景适合选YOLO11YOLO11是ultralytics在现代统一接口下发布的检测模型对抽烟检测这种1000张图级别的单类任务来说选择它的理由有三层。第一模型体积可以压得很小yolo11n和yolo11s两个尺寸在这个数据规模下足够用先拿nano跑通流程再换s提精度不用一上来就上大模型相比SSD那一代需要单独编译检测头、还要手动处理anchor的生成逻辑YOLO11把前置工作全封装掉了。第二它原生支持三种计算设备NVIDIA GPU走CUDAApple Silicon走MPS无显卡机器走CPU训练脚本里只需要改一个device参数数据路径、模型配置、评估流程全部复用。第三YOLO11的评估输出本身就是COCO风格的指标mAP50、mAP50-95这些目标检测评价指标直接生成不用自己再写一套验证代码。选型还有一层考虑是后续部署。工地、加油站这类场景最终往往要跑到NVR或边缘盒子上去yolo11n导出的模型相对轻量在CPU推理也能维持可用帧率。如果你已经在用YOLOv8的部署管线YOLO11在模型结构上不兼容旧权重需要重新训练一次但数据集和标签格式不用动代价可控。所以这个项目标题把“YOLO11一键训练脚本”作为卖点本质上是照顾了从训练到部署的整条链路。4.2 一键训练脚本目录检查、device选择与启动#!/usr/bin/env bash # 一键训练抽烟检测适配 gpu / mac / cpu 三种平台 # 用法: ./train_smoking.sh gpu # NVIDIA GPU默认0号卡 # ./train_smoking.sh mac # Apple Silicon MPS # ./train_smoking.sh cpu # 纯CPU调试 set -e DATA./datasets/smoking.yaml # 数据配置 MODELyolo11n.pt # 预训练权重首次运行自动下载 EPOCHS100 IMGSZ640 BATCH16 PROJECT./runs case $1 in gpu) DEVICE0; WORKERS4 ;; mac) DEVICEmps; WORKERS2 ;; cpu) DEVICEcpu; BATCH8; WORKERS0 ;; *) echo 用法错误: $0 gpu|mac|cpu; exit 1 ;; esac # 检查数据配置文件是否存在防止路径错误白跑一次 test -f $DATA || { echo 缺少 $DATA; exit 1; } test -d ./datasets/images || { echo 缺少 images 目录; exit 1; } yolo detect train \ model$MODEL data$DATA \ epochs$EPOCHS imgsz$IMGSZ batch$BATCH \ device$DEVICE workers$WORKERS \ project$PROJECT namesmoking_exp第一行set -e保证脚本中途任何一条命令失败就立即停住不会让你等了一晚上才发现第二步就断了。case语句把三种设备的差异集中在一个地方GPU默认用0号卡workers设4Mac走MPSCPU强制把batch降到8、workers设0——workers大于0时CPU数据加载的瓶颈比GPU更明显反而拖慢训练。多卡机器把DEVICE0改成DEVICE0,1batch会自动按卡数均分不需要额外改学习率。yolo detect train这段命令的参数都是按1000张图规模设的。EPOCHS100是一个合理起点约800张训练图跑100轮单卡几十分钟能看完曲线趋势。IMGSZ640是速度与精度的平衡如果你的场景里烟头在画面里经常小于20像素建议改到960代价是训练时间变长。BATCH16对8G显存比较稳显存小就改成8或4学习率不用跟着变因为YOLO11对batch的敏感度没有旧版高。PROJECT和name决定权重输出位置最终模型在./runs/smoking_exp/weights/best.pt。配套的smoking.yaml是这样写的# datasets/smoking.yaml path: ./datasets train: images/train val: images/val test: images/test nc: 1 names: 0: smokingpath是数据集根目录train和val写的是相对路径ultralytics会自动拼成./datasets/images/train。nc: 1表示单个类别names里的索引必须和第三章转换脚本里LABEL_MAP的顺序一致否则类别名会张冠李戴。4.3 三平台实测参数怎么调才不白跑平台device 值batch 建议workers 建议第一版建议NVIDIA GPU8G0164直接跑epochs 100NVIDIA 多卡0,1每卡164~8先单卡验证再多卡MacApple Siliconmps82用 nano 模型跑通纯 CPUcpu80只跑 10~20 轮调试GPU平台最省心8G显存跑yolo11n加batch 16没有压力。显存只有4G的话batch降到8同时把workers降到2防止数据加载阻塞。多卡时先确认nvidia-smi能看到两块卡再传device0,1如果显存大小不一致batch要按最小卡来算。Mac平台用MPS理论上比CPU快不少但有两个前提一是模型尺寸不要超过sm和l在MPS上内存占用会很难看二是给环境变量加一行export PYTORCH_ENABLE_MPS_FALLBACK1让不支持的算子自动回退到CPU否则某些新版本torch会在训练中段直接报错中断。MPS在小batch下反而可能比CPU还慢因为kernel启动开销在那里所以batch设8而不是16。CPU平台建议只用来验证流程不要指望它在合理时间内训完100轮。做法是把epochs临时改成10确认数据加载、loss下降、权重保存这条链路没问题后再换到GPU或Mac上正式跑。如果你手头只有CPU那就要有心里准备yolo11n在纯CPU上跑100轮1000张图可能是小时级起步建议先看一眼前10轮的loss曲线再决定要不要继续。5. YOLO11训练抽烟检测常见问题四条踩坑记录与排查思路5.1 现象loss变成NaN或训练中段突然发散训练跑到几十轮loss直接从个位数跳到NaN日志里出现nan字样。第一次遇到这个往往觉得是玄学其实原因基本就三类。第一学习率过大YOLO11在默认配置下的学习率对1000张图的小数据集来说偏高我一般直接改成0.0001试。第二标注里有零面积框或空框第三章转换脚本里虽然做了clip但如果你拿到的是别人转好的标签可能没做这一层处理。第三数据集中有破损图片或全黑图灰度方差为0的图在某些增强路径下会触发除零问题。解决顺序也很固定先跑一个扫描脚本检查TXT里有没有宽或高小于等于0的框有就直接删掉该标注或修复再把学习率调低一个量级最后把AMP关掉试一轮。YOLO11默认开混合精度CPU或某些老GPU上AMP实现有bug加ampFalse就能绕过去。关掉AMP后如果loss恢复正常说明问题出在精度不是数据。# 扫描 YOLO 标签目录里所有坏框 import os label_dir datasets/labels for f in os.listdir(label_dir): path os.path.join(label_dir, f) with open(path) as fh: for line in fh: parts line.strip().split() if len(parts) 5: print(格式错误:, f, line.strip()) continue w, h float(parts[3]), float(parts[4]) if w 0 or h 0 or w 1 or h 1: print(坏框:, f, line.strip())这个脚本会在训练前把所有脏标注暴露出来价值在于省掉一次几小时的无效训练。跑完后把输出的文件名汇总回到VOC的XML里对照检查修复后重新执行第三章的转换脚本而不是直接手改TXT。5.2 现象mAP50超过0.9但实拍画面里把栏杆、手机误报成烟这是抽烟检测最典型的翻车现场离线指标漂亮上线全是误报。原因在于训练集里的负样本太单一1000张图如果全部是“人手烟”的正样本背景永远是那几个固定机位模型学会的是“画面里出现一个亮色细长物体配合手势就是烟”而不是“嘴里有火点才算烟”。真实监控里栏杆、手机屏幕反光、白墙上的污渍都会触发它。解决从两个方向入手。一是在数据集里扩充硬负样本把实拍误检的图片收集起来不加标注直接丢进训练集让模型在后续训练里看到“没有烟的手部特写”和“没有烟的长条物体”并不罕见。二是把“手持烟但没抽”这类中间状态单独作为一个类标出来让模型学习的是“烟支与嘴部接近”这个组合特征而不是“有个亮条就是烟”。如果数据集本身只有smoking一个类那就退一步至少保证训练集中含有大量烟在嘴边的特写帧弱化持烟手势的占比。5.3 现象Mac MPS训练中途报错或者速度比CPU还慢Apple Silicon跑YOLO11经常会遇到两个极端要么训练中途抛一个底层算子错误要么GPU占用没上去、速度感人。算子错误的原因是PyTorch的MPS后端覆盖度还达不到CUDA的水平某些在GPU上没实现的前向/反向算子会直接中断速度慢则是batch太小MPS的kernel启动开销被放大了。解决方法是脚本开头加一行export PYTORCH_ENABLE_MPS_FALLBACK1让不支持的算子自动落到CPU执行损失一点单步速度换来不中断。如果加了还是报错就把devicemps改成devicecpu同时模型换成yolo11n。MPS内存是共享的batch从8降到4也能缓解。Mac这条路线更适合做验证和权重导出真要大规模实验还是建议拉到GPU机器上MPS本就不是为长时间训练设计的。5.4 现象近景的烟检测正常远处吸烟的人完全漏掉监控场景里远处目标十几像素近处目标几百像素同一个类里尺度跨度极大。YOLO在640分辨率下对小目标的定义是像素尺寸小于32×32远处烟头通常只有10到20像素特征图下采样后信息几乎没了。这种情况不是模型能力不足而是输入分辨率跟不上目标尺寸。处理办法有三条。第一把IMGSZ从640升到960小目标在特征图里的像素占比会提升YOLO11的C2PSA模块对多尺度输入还是友好的代价是训练时间增加约一倍。第二对大图做滑窗切块推理把1920×1080的监控画面切成四块640×640分别跑再把检测框坐标映射回原图。第三种是治本的路子在数据增强里加入随机裁剪模拟不同距离下的目标尺度让训练集本身就覆盖“远小近大”的分布。这三种方法可以叠加但第一步永远是把imgsz提上去其余都是后话。6. 让抽烟检测模型再稳一步混淆矩阵、难例回流与下一轮迭代6.1 先看验证阶段的混淆矩阵与PR曲线而不是只看mAP训练结束后runs/detect/smoking_exp/下会生成confusion_matrix.png和PR_curve.png这两个图比mAP一个数字有用得多。打开混淆矩阵看“smoking被误判为背景”那一格的比例如果漏检集中在背景说明正样本不够或者小目标丢失如果集中在其他类别说明类边界定义有问题。PR曲线则直接告诉你置信度阈值设在哪里最合适——曲线开始掉头的位置就是告警门限的合理取值设低了误报多设高了漏检多。我个人的习惯是每次训练完先看这两张图再决定下一步不看就直接调参等于猜。mAP50到0.9只代表“验证集上平均表现好”但混淆矩阵会把每一类、每个方向的错误拆开你才知道瓶颈是在数据还是在模型。6.2 硬负样本回流把误检图补成下一轮的训练集训练完第一版YOLO11后拿真实监控片段做一次批量推理把所有置信度在0.25以上但框错的图自动截下来人工快速过一遍后拆成两类应该检出但漏掉的是难正样本不该检出却框上的是硬负样本。把它们各自加进数据集的train目录和val目录然后重新跑一遍第三章的转换脚本让新图生成对应的TXT。硬负样本图保持TXT为空即可程序会把它当作背景难正样本则需要简单标注一下烟支区域批量标注时用LabelImg打开就能完成。往复两轮这种难例回流经验上能让mAP50从0.8附近往0.9走而且部署处的误报会明显减少。比盲目加大数据增强或者调anchor参数管用得多。这个习惯我保持到了每个单类检测项目上先清洗再训练先看混淆矩阵再谈调参把每一轮翻车的图片都喂回数据集。希望帮到你。本文还有配套的精品资源点击获取