简介:面向深度学习目标检测与行为识别任务的多场景抽烟行为图像数据集,适用于智能监控、健康行为分析、公共场所安全巡检等场景,帮助研究者与开发者训练模型识别不同环境下的吸烟动作,提升模型在室内外、不同光线和拍摄角度下的泛化能力。压缩包共2000个文件,主体为jpg图像与配套的xml标注文件,xml中记录了吸烟行为的目标边界框及类别信息,可直接用于YOLO、Faster R-CNN、SSD等常见检测框架的训练与验证。资源整体984.58MB,目前已有423人学习下载。数据集场景覆盖较广,标注格式规范,文件组织方式清晰,便于按需筛选样本并划分训练集与验证集,也方便做数据增强与多尺度训练。对需要高质量吸烟行为数据的研究者或竞赛团队而言,这份资源省去了自行采集、清洗与人工标注的繁琐环节,拿到后即可投入模型训练、效果评估与算法迭代,是开展行为识别相关实验的实用基础数据包。
1. 多场景抽烟行为数据集:一套能直接喂给检测模型的行为样本库
做过工地监控或加油站安防的人都有体会:抽烟检测的模型结构不是瓶颈,能拿到多少“像样”的数据才是。同样是抽烟,白天和夜晚是两种目标,平视和俯视是两种形态,手夹烟和叼着烟又可能被模型学成两个东西。所谓“多场景抽烟行为数据集”,指的不是一堆从网上下载的图片拼盘,而是围绕抽烟行为在真实环境下的形态变化,把采集、清洗、标注、组织做成一套规范,产出一份能让深度学习模型直接训练的数据资产。它解决的问题很具体:让你的检测模型在换了个摄像头、换了个光照条件之后,不至于精度断崖式下跌。适合正在做安防监控、工业视觉、边缘设备行为识别,或者想用YOLO系模型训练自己数据集,但苦于原始素材太杂、标注口径不统一的从业者。
2. 拆开“多场景”三个字:数据采集与类别设计的四个关键维度
2.1 场景维度怎么定:光照、遮挡、视角、背景密度
多场景数据集的核心不是“图片多”,而是“变化维度覆盖得全”。以抽烟行为为例,模型需要泛化的变量至少包括四类:光照条件、遮挡程度、视角方向和背景复杂度。我一般会把每个维度拆成明确的档位,再按档位去采集素材,而不是凭感觉“多拍点”。
光照维度至少要覆盖白天强光、阴天、夜间路灯、室内日光灯和逆光剪影。遮挡维度重点记录手遮烟、侧脸遮挡、人群遮挡、雨伞或安全帽遮挡。视角维度在安防项目里尤其关键:枪机俯视、球机平视、手持设备近景,模型对视角极其敏感,只用平视数据训练出来的模型,放到高处俯视的监控画面上经常会漏检一大半。
背景密度指的是画面里的干扰目标数量。空旷工地、密集人群、车辆穿行、烟囱雾气,都是完全不同的噪声环境。建议在一开始就为每个场景建一个命名约定,比如smoke_front_day_dense、smoke_side_night_sparse,这样后面做数据分析和场景均衡时会省很多事。很多开源数据集只有一个smoke类别,但实际项目里你会发现,不同场景下的“烟”特征差异大到像两个类别。
2.2 类别设计:单类目标检测还是行为分类
类别设计决定了后续所有标注工作的成本,也直接影响误检率。常见的做法有两种:一是只标一个smoke类别,框住烟或烟头;二是拆成smoke和hand两个类别,让模型同时学习手和烟的相对关系。单类别方案简单,冷启动快,但在“手持笔/手指卷曲/嘴里叼棒棒糖”这类高相似度干扰下非常容易误检。两类别方案多花一倍标注时间,却能让模型学到“手部区域同时出现烟”的组合特征,误检明显下降。
我的建议是:如果项目周期紧、先跑通流程,第一版就做单类别;如果目标是长期迭代上线,至少从第二版开始加入hand类别。还有一种思路是用姿态估计算法先提取人手关键点,再加一个分类头判断手部区域是否含烟,这种方案对标注框的精度要求更低,但需要额外的关键点标注工具链。对大部分监控场景来说,检测方案仍然是落地最快、最容易部署到边缘设备的选择。
2.3 采集工具与镜头选型:从监控录像到手机素材
数据来源无非三种:现场监控录像、定向补拍、公开数据集。监控录像是最真实的,但通常分辨率不高、码流压缩严重,需要拿 RTSP 拉流或直接导出 MP4 再做抽帧。定向补拍是针对数据缺口专门设计动作:比如夜间素材不足,就带着补光灯去实地按固定脚本拍一轮。公开数据集可以参考,但几乎都绕不开版权和场景偏差的问题,不建议作为主力。
镜头选型直接决定图像质量上限。固定枪机适合加油站、出入口这类固定视角,俯视角度容易拍到完整的“手到嘴”动作轨迹;球机和手持设备适合覆盖多角度样本,但画面里目标相对小。一个实用的原则是:目标人体在画面中的高度不要少于图像短边的三分之一,否则抽烟的烟头只有十几个像素,再好的模型也救不回来。1080P 是底线,能做 2K 更好,因为后面还要做数据增强和随机裁剪。
2.4 数据量配比与场景均衡
很多人习惯把数据量当成第一指标,但多场景数据集的“配比”比“总量”重要得多。一个容易踩坑的配置是:室内样本占了 70%,工地和夜间只各占 5%,训练出来的模型在室内测试集上表现很好,一到真正部署的工地场景就翻车。这里的问题是采样偏差,不是模型能力问题。
我一般建议总样本量在 4000 到 8000 张之间就足够冷启动,但每个场景维度的占比要有硬约束:单一场景占比不超过 30%,夜间和强遮挡场景各至少 15%,远视角样本至少 10%。如果某些场景实在凑不够数,宁可做在线增强也不要硬凑重复帧。另一个容易被忽略的操作是“负样本采集”——专门收集手拿笔、手拿手机、嘴里叼吸管这类相似但不抽烟的图片,和正样本放在一起训练,这是抑制误检最直接的手段,成本低、见效快。
3. 从原始视频到标注数据:构建数据集的完整落地流程
3.1 视频抽帧策略:按固定帧率还是按场景抽帧
拿到原始视频后的第一步是抽帧。按固定帧率抽帧(比如每秒 1 帧)实现最简单,但问题很大:画面里人几乎不动时会产生大量几乎相同的重复帧,浪费标注预算;人快速走动或动作幅度大时,又可能恰好错过“烟在嘴边”的关键帧。更好的做法是用 ffmpeg 的场景检测抽帧,只在画面发生显著变化时抽一帧。
ffmpeg -i input.mp4 -vf "select='gt(scene,0.3)',setpts=N/(25*TB)" -fps_mode cfr -q:v 2 output_%04d.jpg这条命令的意思是:gt(scene,0.3)表示只在相邻帧差异超过 0.3 时保留当前帧;setpts=N/(25*TB)重新设置输出时间戳,按 25 帧每秒对齐;-fps_mode cfr保证输出帧率一致;-q:v 2控制 JPEG 质量为高质量。阈值 0.3 是个经验值,固定摄像头场景下建议降到 0.2,因为监控画面变化本来就少;手持拍摄、画面频繁运动的素材可以提高到 0.4,避免把镜头晃动导致的画面撕裂也抽进来。抽完帧之后,最好人工快速过一遍,把对焦模糊、运动模糊、画面被大面积遮挡的帧直接删掉,这类脏数据对训练没有正向帮助,只会加大模型拟合噪声的倾向。
3.2 标注规范与工具:框多大、标不标遮挡、怎么统一口径
标注规范必须在动笔之前写清楚,否则两三个标注员各按各的理解标,出来的数据会让训练过程反复震荡。以单类别smoke为例,我常用的规范是:烟体或烟头在画面中清晰可见就标,框住烟体本身,不包含手指;目标短边小于 20 像素不标;目标被遮挡超过一半不标;一张图里最多标注 20 个目标,再多也只保留最清晰的。如果你选择了smoke和hand双类别,则手部框要包含整个手部,允许与烟框高度重叠,这有助于模型学习“两者同时出现”的空间关系。
标注工具方面,LabelImg 用起来最省事,适合小批量冷启动,但逐框手标效率太低。我更推荐 X-AnyLabeling,它内置了 YOLO 系列的自动预标注模型,先让模型跑一遍预标注,人工只需要修正漏框和错框。对有经验的标注员来说,这个流程能把效率提升一倍以上。标注完导出时,建议直接导出为 VOC 格式的 XML,后续留一个转换脚本统一转 YOLO 格式,方便切换目标检测框架。不要直接在标注工具里存成 YOLO txt,因为少了类别名映射和坐标系说明,后面换工具时会吃亏。
3.3 把 VOC 标注转成 YOLO 格式:转换脚本与四个边界坑
YOLO 系列训练要求每张图片对应一个同名 txt 文件,每行格式为class_id x_center y_center width height,四个坐标值都是相对图片宽高的归一化比例。VOC 的 XML 存的是左上角和右下角绝对坐标,转换时需要注意四个坑:一是 YOLO 的x_center是框中心点的相对坐标,不是左上角;二是width和height是框的宽高,不是右下角坐标;三是归一化要除以原图宽高,不能除反;四是边界值必须限制在 0 到 1 之间,因为标注时偶尔会出现手滑把框拖出图片边缘的情况。下面的脚本处理了这些问题并做了合法性校验。
import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, img_dir, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find('filename').text img_path = os.path.join(img_dir, img_name) with Image.open(img_path) as img: img_w, img_h = img.size lines = [] for obj in root.iter('object'): cls = obj.find('name').text if cls not in class_names: continue cls_id = class_names.index(cls) box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 边界合法性校验:越界、零尺寸、反框全部跳过 if w <= 0 or h <= 0 or x_center < 0 or y_center < 0: continue if x_center > 1 or y_center > 1: continue lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: out_path = os.path.join(out_dir, img_name.rsplit('.', 1)[0] + '.txt') with open(out_path, 'w') as f: f.write('\n'.join(lines)) print(f"OK: {img_name} -> {len(lines)} targets")这个脚本的注意点在于:读取图片尺寸用的是 PIL 而不是直接解析 XML 里的size字段,因为有些标注工具写入的尺寸和原图实际尺寸不一致,一旦不一致,归一化坐标会整体偏移。转换后的 txt 如果为空,说明这张图没有有效标注,建议同步生成一个empty.txt清单,训练前用这个清单把空标注图片从数据集里剔除,或者单独作为负样本使用。跑完批量后还要抽查几十个文件,用可视化脚本把标签画回原图确认框位没有漂移,这一步是控制标注质量的最后一到防线。
3.4 数据集划分:按文件随机切分还是按场景切分
把全部图片随机划分成 train/val/test,看起来公平,实际上隐患很大:同一段监控视频抽出的相邻帧,内容高度相似,随机划分会让验证集里混入训练集的近亲样本,测出来的指标虚高。正确做法是按场景或者按视频片段划分,保证同一个场景的帧只出现在一个集合里。
import os import random from collections import defaultdict data_dir = 'dataset/images' groups = defaultdict(list) for fname in os.listdir(data_dir): if not fname.endswith('.jpg'): continue # 按文件名前缀的“场景ID”分组,例如 smoke_front_day_001.jpg -> smoke_front_day scene_id = fname.rsplit('_', 1)[0] groups[scene_id].append(fname) all_files = list(groups.keys()) random.shuffle(all_files) n = len(all_files) train_scenes = all_files[:int(n * 0.7)] val_scenes = all_files[int(n * 0.7):int(n * 0.85)] test_scenes = all_files[int(n * 0.85):] for split, scenes in [('train', train_scenes), ('val', val_scenes), ('test', test_scenes)]: split_dir = f'dataset/{split}' os.makedirs(split_dir, exist_ok=True) with open(f'dataset/{split}.txt', 'w') as f: for scene in scenes: for fname in groups[scene]: f.write(f'images/{fname}\n')这段代码把文件名中场景前缀相同的图片归为一组,再按“场景组”而不是“单张图片”做切分。这样训练时见过的场景,在验证时不会以近似帧的形式重新出现。实际项目中,我会再叠加一层约束:如果某个场景组里的图片特别多,比如超过总数 30%,先对该组做一次均匀抽样再参与划分,避免一个超大场景同时污染训练集和验证集。划分完成后,还可以统计一下三个集合里smoke框的数量分布,保证每个集合的正样本密度没有显著偏差。
4. 用 YOLOv8 训练抽烟检测模型:从命令行到参数调优
4.1 环境准备与数据校验:训练前把数据集体检一次
训练前先花十分钟做数据体检,能省掉后面排查诡异 loss 曲线的大量时间。体检项包括:图片文件是否能被正常解码、尺寸是否统一、标注 txt 是否与图片一一对应、标注框的类别 ID 是否在合法范围内。
import os from PIL import Image img_dir = 'dataset/images' label_dir = 'dataset/labels' bad_files = [] for fname in os.listdir(img_dir): if not fname.endswith('.jpg'): continue stem = fname.rsplit('.', 1)[0] label_path = os.path.join(label_dir, stem + '.txt') if not os.path.exists(label_path): bad_files.append(f'{fname}: label missing') continue try: with Image.open(os.path.join(img_dir, fname)) as img: img.verify() except Exception as e: bad_files.append(f'{fname}: corrupt image') continue with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: bad_files.append(f'{fname}: bad label line') break cls_id = int(parts[0]) if cls_id < 0 or cls_id >= 1: # 单类别时只允许 0 bad_files.append(f'{fname}: illegal class {cls_id}') break print(f'checked {len(os.listdir(img_dir))} files, {len(bad_files)} problems') for item in bad_files[:20]: print(item)校验脚本的逻辑很简单,但能发现一个很常见的问题:标注工具偶尔会生成空的 txt 文件,或者把类别 ID 写成 1 而实际上类别只有 0。另外,如果训练集里有 PNG 和 JPG 混合的情况,注意 YOLO 的图片读取是以*.jpg或*.png后缀去找对应标签的,混合格式容易漏掉部分标签。发现疑似损坏的图片,直接把整张图和它的标签一起移除,不要试图修复,这类问题样本对训练没有价值。
4.2 训练启动命令与必调参数
环境用 YOLOv8 的官方 pip 包即可,常见的组合是 Python 3.10 + PyTorch 2.x +ultralytics。数据配置用一个smoke.yaml文件,内容就是 train/val 的图片路径和类别名列表。启动训练的命令很简洁:
yolo detect train \ data=smoke.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=640 \ batch=16 \ device=0 \ patience=30 \ amp=True \ cos_lr=True \ mosaic=1.0 \ hsv_h=0.01 hsv_s=0.5 hsv_v=0.3几个参数说下我的选择逻辑。epochs=200是上限,实际配合patience=30,当验证集指标连续 30 轮不提升就自动早停,防止无效计算。imgsz=640在速度和精度之间比较平衡,如果你的目标在画面里偏小,可以提到 800,但显存占用会明显上升,先用 640 跑通再调。batch=16在 24GB 显存下用 YOLOv8s 没有压力;显存小就降batch而不是降imgsz,因为分辨率对检测精度的贡献比 batch 大。mosaic=1.0开启马赛克增强,对丰富小目标上下文很有帮助,但注意最后 15 个 epoch 把 mosaic 关闭,因为马赛克拼出来的图与真实场景分布不一致,一直开着会拉低最终收敛精度。hsv_h=0.01把色调扰动设得很低,因为烟的颜色在黄白到灰白之间,过度扰动会让模型学到错误的颜色关联;饱和度 0.5、亮度 0.3 保持默认中等水平。
4.3 训练过程监控:loss 曲线和指标怎么看
训练开始后,不要只盯着终端里那些数字。跑完一个完整训练后,ultralytics 会在runs/detect/train目录里留下results.csv,把每一轮的train/box_loss、val/box_loss、metrics/mAP50(B)等指标记录得清清楚楚。读这个文件用几行 Python 就能画出趋势图。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/detect/train/results.csv') fig, axes = plt.subplots(2, 2, figsize=(12, 8)) axes[0, 0].plot(df['train/box_loss']); axes[0, 0].set_title('train box_loss') axes[0, 1].plot(df['val/box_loss']); axes[0, 1].set_title('val box_loss') axes[1, 0].plot(df['metrics/mAP50(B)']); axes[1, 0].set_title('mAP50') axes[1, 1].plot(df['metrics/mAP50-95(B)']); axes[1, 1].set_title('mAP50-95') plt.tight_layout(); plt.savefig('training_curve.png')看曲线时重点观察两个信号。第一,训练集 loss 持续下降而验证集 loss 在第 80 轮左右开始回升,这是典型的过拟合,配合早停参数它会自动停掉,但你要意识到数据量不够或者增强强度不足。第二,mAP50 涨到 0.95 附近一直徘徊,而 mAP50-95 只有 0.6 左右,说明模型对框的定位精度不高,常见原因是标注框本身有偏差,或者训练分辨率偏低。如果 mAP50 本身就低于 0.8,先别调参数,回头查数据里有没有大量同类场景的漏标和错标,数据问题不解决,调参只是自我安慰。
4.4 backbone 选择:从 YOLOv8n 到 YOLOv8l 怎么权衡
如果你对 YOLOv8 的模型家族不熟,选型逻辑很简单:先看推理设备,再定模型规模。边缘设备比如 Jetson Nano 或者 RK3588,用yolov8n,参数最少、跑得最快,但小目标能力弱,适合画面里目标比较大的固定机位。普通服务器或者工控机带独立显卡,yolov8s是性价比最高的起点,多数自建数据集在几千张的量级上,s 和 m 的精度差距并不大,但 s 的训练和推理速度快接近一倍。只有当你发现夜间小目标漏检严重,或者 mAP50-95 长期提不上去,再换yolov8m或yolov8l。
还有一个常见做法是把训练拆成两段:先用yolov8n或yolov8s跑 100 轮,用验证集找出数据和标注里的明显问题;修完一轮数据后再用yolov8m做正式训练。用大模型直接冷启动不仅慢,而且会把数据里的噪声也学进去,后面反而不容易判断是模型容量不够还是数据质量不行。模型换大之后,imgsz可以同步提到 800,这两个变量对精度的影响比换 backbone 更大。
5. 多场景抽烟数据集的五类典型翻车现场与排查
5.1 误检:把手指、笔、卷状物都当成了烟
现象是模型在测试集上 mAP 挺高,一上真实视频就频繁把手指弯曲的动作或者笔杆误报成烟。原因通常是正样本里“手部特写”和“手夹烟”的姿态太单一,模型学到的是“手指附近有暗色长条物体”这个弱特征,而不是“烟在燃烧或冒烟”的语义特征。解决方法是专门采集一批“手拿笔、手拿筷子、手比姿势、嘴里叼吸管”的负样本,混进训练集里一起训练。还有一个取巧但有效的做法:给误检框做一个后处理规则,连续 3 帧以上都稳定检出同一个位置才输出报警,利用时序一致性过滤掉单帧闪烁的误报。
5.2 小目标漏检:夜间、远视角、逆光三座大山
现象是画面里人站得稍远、或者灯光很暗时,检测框完全丢失。原因有两个层面:一是数据层面夜间和远视角样本占比太少,模型没见过这种形态的“烟”;二是检测头在 640 分辨率下对小于 16x16 像素的目标表征能力很弱,烟头在远视角下往往只有 8 到 12 个像素。解决思路分两步走:先补齐夜间和远视角样本,再考虑把imgsz提到 800 并开启 YOLOv8 的SAHI切片推理思路,对大图做切片检测再拼接结果。如果部署设备性能有限,最实用的做法是降低置信度阈值到 0.25,配合跟踪算法做帧间投票,宁可在单帧上多检一些候选框,也要保证不漏掉真实目标。
5.3 标注不一致:同一个烟头,有人标整只手,有人只标烟体
现象是训练时 loss 曲线抖动明显,验证集指标上不去,打开标注文件一看,同一批图片里smoke框有的横跨整只手,有的只包住烟头,长宽比差异极大。原因很简单:标注规范写得不够细,或者标注员没有严格执行。解决办法是标注复核时用一个统计脚本画出所有框的宽度和高度分布,正常数据应该集中在一个窄区间内,如果出现明显的双峰分布,说明标注口径分裂了。把偏离主分布的点挑出来重新标注。另外在日常迭代中要把标注规范写进 README 里,每个新加入的标注员先标 50 张,由老手检查通过后再正式开工。
5.4 场景失衡:室内数据太多,工地数据被淹没
现象是室内测试集表现很好,室外工地场景的召回率低到不可接受。原因是训练集中室内占比超过 60%,模型把大量容量花在了室内背景纹理和光照模式上,工地样本因为占比少,对梯度贡献被其他样本稀释。最直接的解法是重采样,按场景对训练图片做加权采样,让每个场景在每轮训练中贡献大致相当的样本数。另一种有效做法是按场景分成子集训练多个模型再在推理端做集成,但部署成本翻倍,一般不建议。更省事的办法是在smoke.yaml里直接去掉部分冗余的室内帧,把数据量压到场景均衡,再重新训练。
5.5 数据增强反而拉低了精度:马赛克和颜色扰动过强
现象是开了增强训练后 mAP 反而不如不开,尤其是夜间场景的检测框出现偏移。原因很常见:mosaic=1.0把四张图拼成一张,夜间图和白天图被拼在一起,模型被迫学习跨场景的伪关联;hsv_h扰动过大,烟的黄白色被扭曲成紫红色,模型对颜色特征产生错误依赖。解决方法是降低增强强度而不是关闭:mosaic=0.5,hsv_h=0.01甚至 0,scale=0.5(限制随机缩放比例),并在最后 15 轮将 mosaic 清零。记住一个原则:自建场景数据集的增强参数要保守,因为你的数据分布本来就窄,过度增强是把照片变成抽象画,模型只能学到一堆无关的纹理噪声。
6. 把数据集做成可复用的数据管道,而不是一次性资产
多数人做深度学习数据集的习惯是把图片和标注堆在一个文件夹里,训练完就丢着不管。问题在于:一旦模型上线后发现新场景漏检,你要重新翻找素材、重新抽帧、重新标注,上一轮积累的经验全部变成隐性的。更好的做法是把数据集当作一个持续维护的资产,给它建一套简单的数据管道,用脚本把“新增素材 → 抽帧 → 预标注 → 人工修正 → 校验 → 切分 → 训练”串起来。我自己通常会在项目根目录放一个Makefile,核心命令就几个:make ingest把新视频按规则抽帧并按场景 ID 归档,make label打开标注工具并加载已有预标注配置,make check运行数据校验脚本,make train用固定参数启动训练。这套东西写起来不复杂,但能保证任何时候新增一批数据,整个流程是幂等的、可追踪的,不会出现“上次用的到底是什么参数”这种黑匣子问题。
再补一个落地的习惯:每个数据集版本都打一个 manifest 清单,记录图片数量、标注框数量、场景分布、类目分布、训练参数和验证集指标。模型训练时把这个 manifest 的 hash 写进实验记录里,这样无论过多久,翻到一条实验结果就能立刻知道它对应的是哪一版数据、训练了多久、指标是多少。我之前的项目吃过一次亏:数据集加了负样本重新标注后,没有更新版本号,结果一个同事用旧数据跑了一周实验,所有结论全部作废。所以我现在给数据集做版本管理和给代码做版本管理一样认真。如果你准备投入这个方向,我的建议是:先别追求最全的数据量,把一套从采集、标注到训练校验的管道跑通,比什么都重要。希望帮到你。
本文还有配套的精品资源,点击获取