简介:面向AI模型训练与行为识别场景的吸烟检测数据集,包含吸烟/非吸烟图像及其对应的XML标注框,适合用于目标检测、图像分类等任务,也方便AI开发者快速搭建和验证吸烟行为识别模型。压缩包内共1567个文件,主体为783张jpg图像和783个配套xml标签文件,另含1个txt说明文件,整体大小31.71MB,结构简单紧凑,易于下载和本地处理。目前已有161人学习下载,数据规模适中,适合入门级和中阶算法工程师实践模型训练全流程。图像样本均以smoke前缀命名,内容覆盖多种实际姿态与环境,每张图都有对应标注信息,可直接用于YOLO、Faster R-CNN等主流框架的数据加载,省去人工标注环节;同时便于按文件名划分训练集与验证集,帮助使用者聚焦模型调优与部署。
1. 吸烟数据集带标注:为什么公开样本少,你却仍需从一张标注原图入手
做工厂安全、楼道监控、公共场所禁烟这类场景的工程师,想跑通吸烟行为检测,第一道坎往往不是模型选型,而是数据。搜“吸烟数据集带标注”能直接下载并带完整标注的公开包其实很少,很多是零散原图、没有类别定义的截图,或者标注口径和你的业务对不上——有人标“人”,有人标“烟头”,还有人把“拿手机”也框了进去。真正可控的落地路径,是把公开可用的样本和你自己的监控画面揉成一套规范的 YOLO 数据集,而第一步是把标注结构读懂。下文按目标检测数据集的标准做法,从标注格式拆到校验脚本、训练参数,再写到五个高频坑,适合正在搭吸烟检测、又不想在数据阶段反复返工的工程师直接照着排。
2. 吸烟数据集长什么样:标签结构、bbox 范围与两种主流标注格式
拿到一份带标注的吸烟数据集,顺序很重要:先翻 labels 目录,再打开图片。图片只是给人看的,训练时真正被网络吃进去的,是旁边那个同名.txt或.xml里的几行数字。搞错这一步,后面所有操作都是白做。
2.1 一张标注原图里到底放了什么信息
用最常见的 YOLO 格式举例,打开任意一张吸烟原图对应的 txt,会看到一行这样的记录:
0 0.5234 0.4121 0.0892 0.1030
这串数字依次是:类别ID、归一化中心点x、归一化中心点y、归一化框宽、归一化框高。注意“归一化”三个字,它们不是像素值,而是相对于原图宽高的比例。一张 1080p 的图里烟头占 80 像素宽,对应到 640 分辨率训练输入就只剩 47 像素,网络看到的烟远比人眼看到的模糊。所以数据集的 bbox 范围定义,直接决定后续 imgsz 怎么设、增强怎么做。
吸烟数据集和一般目标检测数据集有个明显不同点:烟支又细又长,在监控画面里常常只占几十个像素,绝大多数 GT 框是小目标框。我在整理时会把标注对象固定成“可见的烟支或烟头区域”,不标整个人。好处是校验指标、查误报时因果清晰,坏处是要接受小目标检测的精度天花板。另外数据里通常还会带一个 classes.txt 或 yaml,它决定类别ID的顺序。这个顺序一旦变了,整份标签都要重新生成,是后面避坑章节会反复提到的误操作源头。
2.2 VOC 格式与 YOLO 格式互换:用 Python 脚本把标注归一化
公开下载或协同方提供的数据,经常是 VOC 风格:图片在同目录,旁边一个 XML 文件,bbox 用绝对像素坐标 xmin、ymin、xmax、ymax 表示。YOLO 训练器不认这种格式,所以拿到“吸烟数据集带标注”后的第一个正式动作,我会先写一个转换脚本,把所有 XML 统一成 YOLO 的归一化 txt,让后面所有处理和训练只面对一种格式。
# voc_to_yolo.py import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_file, class_names, out_dir): tree = ET.parse(xml_file) root = tree.getroot() # 原图尺寸来自 XML 的 size 节点 img_w = int(root.find("size").find("width").text) img_h = int(root.find("size").find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue cls_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) box_w = xmax - xmin box_h = ymax - ymin cx = xmin + box_w / 2.0 cy = ymin + box_h / 2.0 lines.append(f"{cls_id} {cx/img_w:.6f} {cy/img_h:.6f} {box_w/img_w:.6f} {box_h/img_h:.6f}") out_txt = Path(out_dir) / (Path(xml_file).stem + ".txt") out_txt.write_text("\n".join(lines))逻辑说明:脚本先读 XML 的 size 节点拿原图真实宽高,再遍历所有 object 标签,用 bndbox 里的四个绝对角点算出中心坐标和框宽高,最后统一除以原图宽高得到归一化值。整个过程只用标准库的 xml.etree.ElementTree,不依赖第三方包,离线环境也能直接跑。
参数说明:class_names列表的顺序就是类别ID顺序,必须和后续训练用的 smoking.yaml 里的 names 完全一致;xmin、ymin 要先转成 float 再做算术,否则字符串拼接会直接报错;输出的 txt 文件名复用了 XML 的 stem,所以图片、XML、txt 三个文件必须同名,训练时只按文件名前缀匹配标签。我实际用的时候还会在外面再包一层遍历目录的递归,跳过没有 object 的 XML——空标注文件同样要输出,但内容是空字符串,而不是一个换行符。
2.3 吸烟数据集的类别体系:单类二分类还是细粒度多类
先想清楚一个问题:你要检测的是“烟”还是“吸烟行为”。这两者的标注口径差别很大。检测“烟”,单类别就够;检测“行为”,需要烟、手、嘴甚至打火机的空间关系。我看过不少标着“吸烟数据集带标注”的包,类别定义五花八门,最常见的有三种,各有取舍:
| 方案 | 标注目标 | 优点 | 最需要提防的问题 |
|---|---|---|---|
| 单类 cigarette | 烟支/烟头 | 标注一致、收敛快 | 无法表达是否在吸 |
| 双类 unlit/lit | 未点燃/点燃 | 能做行为判定 | 点没点着的边界难标 |
| 多类 person+cigarette | 人+烟 | 行为语义完整 | 正样本暴增、标注成本高 |
我的建议很直接:第一版数据集只做单类 cigarette,把标注目标定义为“可见烟支或烟头区域”。先用单类跑通整个训练、验证、部署链路,等现场反馈里“烟在人手上但没点着”这类难例变多,再把单类拆成 unlit、lit 两个子类。不要一上来就搞五个类——类别一旦多了,样本不均衡会吃掉你大部分调试时间,而且最终效果未必比单类加规则好。这个选择也直接影响第 5 章里“该框烟还是框手”的一系列标注争议。
3. 从零构建带标注的吸烟数据集:采集、标注工具与质检流程
公开包不够用是常态,怎么补齐?补自己现场的、补真实角度的。这一章按采集、标注、校验、增强四个步骤,把一套能自给自足的数据集流程讲清楚。
3.1 采集优先模拟真实监控角度:俯视、逆光、遮挡
公开下载的吸烟样本多为平视摆拍、背景整洁,而现场部署往往在楼道、车间、站台。摄像头安装高度普遍在两米五以上,斜俯视让烟支透视变短;白天强逆光时烟头红点几乎看不到;夜间只能靠烟头微光。所以采集的第一步是角度,不是数量。我一般把采集段分成三组:正常机位、斜俯视机位、8 米以上远距离机位,每组拍够时长再统一抽帧。宁可每组只留几百张有效帧,也不要堆几千张同一角度的高清大图,后者只会让 val 指标好看,一上线就露馅。这和遥感或鸟类小目标数据集的逻辑一致:让样本分布贴近真实分布,模型才有泛化的起点。
3.2 标注工具与拉框边界:该框烟还是框人
标注工具上,我常用 X-AnyLabeling 做视频预标注再人工修正;样本体量小的时候,LabelImg 也完全够用。预标注由一个通用检测模型生成,它会倾向把烟盒、打火机、笔都框出来,复核时第一件事是清掉这些噪声标签。
真正要花功夫定下来的是框选规则,我的规则如下:两米内烟支清晰时,框烟支本体,长宽比跟着烟走;距离远、烟支小于 15 像素时,框“手持烟”区域,不强行框烟;烟叼在嘴里时,框嘴部到烟头一小段,不框整张脸。多人同时吸烟的场景,这个规则尤其重要,不然重叠的人头会让 ground truth 相互矛盾,训练出的模型在人群里会随机跳框。
3.3 标注质量校验:用 Python 做三类自动检查
标注完成后,10 分钟跑一遍检查脚本,能省下训练后返工的一两天。我最先做的三个检查是:字段格式、坐标越界、小目标标定。
# validate_labels.py from pathlib import Path import cv2 def check_labels(img_dir, label_dir, min_side=5): for label_file in Path(label_dir).glob("*.txt"): img_path = Path(img_dir) / (label_file.stem + ".jpg") if not img_path.exists(): print(f"missing image: {img_path}") continue h, w = cv2.imread(str(img_path)).shape[:2] for line in label_file.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: print(f"bad format: {label_file}: {line}") continue _, cx, cy, bw, bh = map(float, parts) x1, y1 = (cx - bw / 2) * w, (cy - bh / 2) * h x2, y2 = (cx + bw / 2) * w, (cy + bh / 2) * h if x1 < 0 or y1 < 0 or x2 > w or y2 > h: print(f"out of bounds: {label_file}: {line}") if bw * w < min_side or bh * h < min_side: print(f"too small: {label_file}: {line}")逻辑说明:脚本用每个 txt 对应的原图尺寸,把归一化坐标反算回像素,检查框是否越界;字段数不等于 5,说明标注文件被截断或混入了其他格式;框宽或框高低于 min_side,说明目标太小,在 640 分辨率下几乎学不到纹理。三类问题都只打印不中断,方便一次性收集完问题清单再统一修。
参数说明:min_side=5是经验值,针对 imgsz=640 的训练配置;如果训练分辨率提到 1280,这个阈值可以放宽到 8 到 10,不要低于 3。脚本里为了可读性直接拼了.jpg去找图,真实数据里图片可能是.png或.webp,更稳妥的做法是传入一个图片扩展名列表,或者直接用 label 的 stem 在 images 目录里做 glob 匹配。另外,cv2.imread读到空图时取 shape 会抛异常,健壮版本要在读取后先判断if img is None。
3.4 数据增强:为什么旋转、翻转和 HSV 扰动对吸烟检测特别有效
数据增强不要无脑套。吸烟场景里烟的姿态随人转动,水平翻转和 ±30° 以内的随机旋转很有效,但旋转一旦超过 30°,烟的细长形态开始失真,模型会把斜着的人手和竖着的烟支学混。更强的增强是 HSV 扰动:白天强光下烟头发白,傍晚阴影里偏红,H 偏移 ±20、S 偏移 ±25、V 偏移 ±15 基本能覆盖一天的光照变化。用 YOLOv8 时这些变换可以直接在 albumentations 配置里声明,不需要自己写循环。反过来,马赛克增强对烟支这种细结构不太友好,拼接会让烟支被切断,这一类增强在吸烟数据上我通常只保留 50% 概率,优先保证目标完整。
4. 用 YOLOv8 训练自己的吸烟数据集:最小命令、参数与验证
数据集整理好,就可以开训了。这一章给的是能在本地单卡跑起来的最小路径,不绕。
4.1 把数据组织成 YOLO 目录格式
YOLO 系列对数据目录有固定要求:images 和 labels 是两个并列的顶层目录,下面再按 train/val 分子目录,图片与标签文件名必须一致(不含扩展名)。目录大概长这样:
smoking_dataset/ ├── images/ │ ├── train/ # 1200 张 │ └── val/ # 300 张 └── labels/ ├── train/ └── val/train 与 val 的划分我习惯按 4:1 左右,但 val 不能随机抽,必须保证远距离、逆光、多人重叠这三个困难子场景都出现在 val 里。随机抽的 val 会普遍偏简单,最终指标好看但没有参考意义。
接着写训练配置文件:
# smoking.yaml path: ../datasets/smoking_dataset train: images/train val: images/val nc: 1 names: - cigarette参数说明:path 若用相对路径,要确认启动 yolo 命令时的当前工作目录,否则会抛出找不到数据集的错误;names 的顺序与标注 txt 里的第一列数字绑定,顺序变就等于全部标签语义变。训练时如果日志出现“found 2 sources for label”的警告,多半是同一张图被复制或软链接到了两个子目录,YOLO 会把重复样本当独立样本,间接放大某些难例的权重。
4.2 训练最小命令与五个必调参数
单卡、8G 显存的环境下,最简训练命令:
# 最小可用训练命令(YOLOv8) yolo detect train \ data=smoking.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=32 \ lr0=0.01 \ patience=15这组参数里,真正每次都要手调的其实是五个,逐个说:
epochs:100 轮起步。自采数据集规模通常不大,50 轮左右 mAP 基本不再涨,100 轮足够观察过拟合拐点。如果跑到 80 轮 val 指标还在缓慢上升,说明数据还在被学习,可以追加到 150。
imgsz:默认 640。但吸烟数据集大量 bbox 是 10 像素级的小目标,直接在 640 上训练,小框信息会被压缩到几乎不可识别。显存够用就上 1280;显存不够,我的替代方案是跑 640 训练完之后,单独裁取标注目标的局部区域补充进训练集,而不是硬上高分辨率。
batch:32 是针对 12G 显存左右消费卡给的稳值;8G 卡直接砍半到 16。调增 batch 有可能让收敛变快,但前提是总迭代次数不变,光改 batch 不调 epochs 没有意义。
lr0:0.01 对 yolov8s 是比较稳的起点。换 nano 模型可以适当上到 0.02,但我不建议在调学习率上花太多时间,数据问题没有清干净之前,调 lr 都是黑匣子里碰运气。
patience:15 负责早停,意思是连续 15 轮 val 指标不提升就自动停止,避免 100 轮无脑跑完。自采数据噪声多,早停阈值可以放到 20,防止因为一两轮的抖动提前退出。
训练开始前,我还会做一次冒烟验证:先跑yolo detect val model=yolov8s.pt data=smoking.yaml,用预训练权重在自建 val 上过一遍。这一步不追求指标,只看有没有“标签与图像尺寸不匹配”“标签文件找不到”这类低级错误。等训练启动后,日志里的 P 和 R 两列在第一个 epoch 就能看出正负样本是否失衡,如果 P 很高但 R 只有 0.1 左右,大概率是负样本太多,回到第 5.3 条处理。
4.3 验证指标只盯两个:mAP50 与 mAP50-95
训练结束,终端会刷出一串指标,我只看两个。mAP50 是 IoU 阈值 0.5 下的平均精度,对吸烟检测而言,能到 0.85 才算基础可用;mAP50-95 把 IoU 从 0.5 到 0.95 按步长 0.05 取平均,对框位置要求更严格。如果 mAP50 不错但 mAP50-95 只有 0.3 左右,说明检测框大概位置对,但边缘贴合差,需要回头检查标注框是不是普遍画宽了一两像素。
还有一个容易被忽略的对比:precision 和 recall 的差距。precision 高、recall 低,说明漏检集中在模糊帧和远距离帧;反过来 recall 高、precision 低,说明误报偏多,要去查负样本里是不是混了大量手机亮屏这类难负例。这两个方向的处理方式完全相反,只盯着 mAP 数字会分不清该补什么数据。
提示:把 val 的 PR 曲线存下来,和下一次训练后的曲线叠着看,比记几十个数字更直观。
5. 吸烟数据集避坑:标注定位、类别不平衡与常识性漏检
下面这些坑来自实际跑吸烟数据的踩坑记录,按出现频率排序。每条都是现象、原因、解决三步。
5.1 烟头只有几个像素的翻车现场:小目标标注失真
现象:第一轮训练 mAP50 不到 0.3,画预测框出来看,模型把手的轮廓当烟。
原因:训练集里有一大批 8 像素以下的烟头标注框。这个尺寸在 640 分辨率下只能看到一个亮点,网络学到的是“亮斑特征”而不是“烟的特征”。本质上不是模型问题,是标注目标定错了。
解决:先跑第 3.3 节的校验脚本,把 min_side 提到 8,过滤掉过小框;再从原图里按标注坐标裁剪局部区域,放大后重新标注成新图并入数据集。让网络先在局部图上学会烟的纹理,再在全图上做检测关联。这类小目标问题的处理思路,和遥感目标检测、鸟类目标检测数据集遇到的情况是一样的,靠的都是补局部尺度特征,不是靠换模型结构硬扛。
5.2 烟头被误判成手机亮屏或硬币反光:标注缺上下文
现象:室内灯光场景里,误报集中在圆形白色反光物体上。
原因:标注时把烟支单独框得太小,网络没有上下文可依。烟头能被认出来,靠的是“手部或嘴部 + 红色端头 + 细长烟身”的组合特征,单独给一个红色圆点,必然和一切亮斑混淆。
解决:标注规则改成框选范围包含握烟的手部区域;烟叼在嘴里时,至少包含下唇一小段。类别名可以仍然叫 cigarette,但拉框必须带上上下文。改完这版标注,误报下降的幅度比换任何网络结构都明显。
5.3 正负样本失衡:抽了 9 万帧,能用的就几千张
现象:训练集正样本只有 600 张,负样本却有 4 万帧,训练后 recall 一直上不去。
原因:视频按固定帧间隔抽帧,大量帧里本来就没有烟,正负比拉到 1:60。YOLO 的损失被多数类主导,模型倾向把所有框都判为背景。
解决:先拿一个粗筛模型把所有帧过一遍,把疑似有烟的帧挑出来,再对负样本做随机抽样,把训练集正负比控制在 1:3 以内。负样本不是越多越好,多样性比数量重要:加一批逆光、夜间、遮挡的负样本,比加十万张重复的正常帧更有用。这个比例是多个目标检测数据集上验证过的起点,具体可以按现场误报情况再调。
5.4 抽帧抽到运动模糊帧,烟变成残影
现象:白天的户外楼梯间,模型检测率比室内低了接近一半。
原因:固定抽帧把吸烟瞬间的手臂运动轨迹采了进来,烟在帧里是一条残影。训练时网络把残影和正常烟都当成同一个正类,等于往数据集里注入了大量标注不一致的样本。
解决:抽帧后先过清晰度过滤——用拉普拉斯方差算图像梯度,低于阈值直接丢弃。如果这类模糊帧确实有业务意义,就把它们单独放一个“模糊烟”子集,不参与主干训练,只用来做最后的鲁棒性回归。抖动摄像头场景下我基本都保留这道过滤,省下来的返工时间足够覆盖实现成本。
5.5 loss 突然变 NaN:先检查 labels,别急着怪网络
现象:训练到第 20 轮,loss 突然变 nan,后续几个 epoch 都拉不回来。
原因:最常见的是 bbox 越界或类别ID越界。txt 里第一列写的是 3,而 smoking.yaml 的 nc 只有 1;或者某个框的中心点加半宽之后超过了原图宽度,归一化坐标大于 1。
解决:跑一遍第 3.3 节的校验脚本,重点看 out of bounds 和 bad format 两行输出;再确认训练配置里 names 的顺序和标注脚本 class_names 完全一致。另外注意空标注文件:空 txt 是合法的,但内容绝不能是单个换行符,某些解析器会把换行符当一条空标签。遇到 NaN 先看数据,再怀疑优化器,这条顺序能省大半天的玄学调参时间。
6. 升级数据集的两种思路:难样本补充与误检闭环回归
数据集第一个版本能跑通后,真正的功力在后续迭代。这里说两个我常用来“让带标注数据集越滚越准”的思路。
6.1 从误检区反向补标注,比加新图更值
训练完第一版模型,不要急着加新场景。先拿它去跑一段未参与训练的现场视频,把所有置信度在 0.3 到 0.7 之间的预测框打印成图,人工过一遍:框错了的,保存成难负样本标为背景;人眼能看到烟但模型没检出的,裁剪下来补成难正样本。这一批数据直接决定第二轮精度的上限,而且采样成本只有通勤时间。我每次跑完现场验证,都会把误检截成一个回灌包,下一轮训练直接拉进去,这个习惯比调任何超参都管用。
6.2 半自动标注 + MD5 去重,避免重复拉框
第二轮以后新增数据,通常不再需要纯手工。先让旧模型对新视频做预标注,人工只负责修改错框和补漏框,标注速度至少快一半。但这套流程最容易埋雷的是数据泄漏:同一帧被抽到两个片段里,分别进了 train 和 val,模型在 val 上表现得像开挂,上线就现原形。我一般会在合并数据集时按文件 MD5 做一次全局去重,保证任何一张图只出现在 train 或 val 的一侧。train 和 val 的划分比例经常会有人问,标准做法是 4:1 左右,但比比例更重要的,是 val 覆盖了远距离、逆光、多人、夜间这几个最能把模型打回原形的场景。
从标题里那句“带标注”往回看,真正该维护的不是标注文件本身,而是一套从标注到验证的闭环:每次模型更新,都把旧 val 重跑一遍,看回归项有没有被改坏。我个人的教训是,十次里有八次精度下降都是数据版本混了,而不是模型退化了。给每个数据集版本打上日期和场景标签,定期跑一次回归,比反复调权重更值得养成习惯。希望帮到你。
本文还有配套的精品资源,点击获取