news 2026/9/28 6:31:47

吸烟行为检测数据集构建与鲁棒性训练实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
吸烟行为检测数据集构建与鲁棒性训练实战

简介:本资源是面向计算机视觉初学者与目标检测实践者的吸烟行为识别专用数据集,适用于安全监控、公共场所禁烟管理、AI行为分析等实际场景的模型训练与算法验证。数据集共2000余张真实生活及影视截图,涵盖多角度、多光照条件下的吸烟人物图像,每张图片均配有一个同名XML标注文件,完整记录吸烟者边界框的左上角与右下角坐标,格式规范、开箱即用,无需额外清洗或格式转换。压缩包为ZIP格式,含2000个XML标签文件,总大小198.63MB,结构简洁,便于直接接入YOLO、Faster R-CNN等主流检测框架进行数据加载与训练。目前已有686人学习下载,资源由实战经验丰富的开发者整理发布,标注质量稳定,场景覆盖全面,可作为课程设计、毕业项目、轻量级工业检测原型开发的可靠基础数据支撑。

1. 吸烟人群检测数据集:不是“加个标签就完事”的简单任务,而是光照突变、遮挡密集、烟雾干扰下的强鲁棒性建模入口

你手头有一段商场监控视频,想自动识别吸烟行为——但直接拿 COCO 或 VOC 训练的通用检测模型一跑,漏检率超 60%,误报全是打火机反光、香烟盒反光、甚至咖啡杯热气。这不是模型不行,是「吸烟」这个行为本身在视觉上极度脆弱:烟头只有几像素亮斑,烟雾呈半透明动态弥散,人常侧身/背对镜头,手部动作微小且被衣袖遮挡。吸烟人群检测数据集的核心价值,不是提供一堆带 bbox 的图片,而是构建一个能覆盖「真实监管场景」的对抗性样本集合:包含强逆光走廊、夜间便利店冷光灯、地铁站通风口烟雾扰动、多人拥挤时的手臂交叉遮挡。它本质是目标检测落地中「小目标+弱纹理+动态干扰」三重难题的压缩包。适合安防系统集成工程师、智慧园区算法负责人、以及正在把 YOLOv8/v11 接入政务监管平台的 CV 工程师——如果你的模型在 demo 环境里准确率 95%,但上线后天天告警误报,那问题大概率出在数据集没过「烟雾鲁棒性」这一关。别急着调参,先看清楚你喂给模型的到底是什么。


2. 数据构成与标注规范:从原始视频帧到可训练样本的四步过滤链

吸烟人群检测不是静态图像分类,行为发生具有时空连续性。一个合格的数据集必须同时满足:单帧可检测(bbox 定位) + 多帧可验证(行为合理性) + 镜头可泛化(不同焦距/畸变) + 干扰可建模(烟雾/反光/遮挡)。市面上公开的 smoking-dataset-v1(GitHub 上标星 320)和 SmokeDet-2023(CVPR Workshop 收录)都只解决了第一层,而真正落地项目需要的是四层叠加。我一般会用自建流水线补全缺失环节。

2.1 原始数据采集的硬约束:三类必采场景与两类禁采源

提示:不要用网络爬取的“吸烟艺术照”或影视剧截图——这类数据会让模型学会把西装革履+烟灰缸=吸烟,而非手部动作+烟雾轨迹=吸烟。

场景类型必须包含的子类采集设备要求单场景最小帧数关键排除项
室内封闭空间商场吸烟区、酒店走廊、网吧角落、医院急诊通道外1080p 固定焦距 IPC,需含低照度(≤50 lux)和高反光(玻璃幕墙)子集≥1200 帧无遮挡正面全身照(失真)、手机拍摄抖动帧(无法配准)
半开放过渡区地铁站出口、写字楼门厅、学校连廊、菜市场入口双摄像头同步(主摄+红外),需记录风速/湿度传感器数据≥800 帧纯远景(人脸不可辨)、纯特写(无环境上下文)
动态干扰强区餐饮店排风口、KTV 包间门缝、公交站台侧风区高速摄像机(≥240fps)捕获烟雾扩散过程≥500 帧烟雾静止帧(非动态建模)、无烟雾纯手势帧(行为歧义)

禁采源明确:影视剧片段(动作表演化)、短视频平台“吸烟挑战”(强滤镜/摆拍)、医学教学图谱(无真实环境噪声)。实测发现,混入 15% 影视数据会使模型在真实监控中误报率上升 3.2 倍——因为模型学会了匹配“烟雾粒子渲染效果”,而非真实烟雾的边缘模糊度与运动矢量。

2.2 标注协议:超越 PASCAL VOC 的三级标签体系

普通目标检测只要标person和smoking两个类别,但吸烟行为判定依赖上下文。我们采用三级嵌套标注:

  • Level-1:基础框(mandatory)
    person类别 bbox,要求:

    • 手部区域必须完整包含在框内(哪怕手伸出画面,也要 extrapolate);
    • 若烟雾弥漫导致人体轮廓断裂,bbox 需覆盖烟雾中心点(用smoke_center属性标记坐标);
    • 框置信度字段occlusion_ratio(0.0~1.0)量化遮挡程度(如背包遮挡腰部则填 0.3)。
  • Level-2:行为锚点(critical)
    在 person 框内添加 3 个关键点:
    hand_tip(持烟手指尖)、mouth_center(嘴唇中心)、cigarette_base(烟支与手接触点);
    三者必须构成近似直线(向量夹角 ≤ 25°),否则标为invalid_smoking(用于 hard negative mining)。

  • Level-3:环境干扰标记(for robustness training)
    为每张图附加 JSON 元数据:

    { "light_condition": "low_backlight", "smoke_density": "medium", "occlusion_type": ["arm", "bag"], "motion_blur_level": 0.7, "camera_tilt": -3.2 }

    这些字段不参与 loss 计算,但用于后续 stratified sampling——比如训练时强制每 batch 包含至少 1 张low_backlight+medium_smoke组合样本,防止模型偏科。

2.3 格式转换:YOLOv8 兼容的增强型.txt生成逻辑

YOLO 系列要求每图一个.txt,但标准格式无法承载 Level-2/3 信息。我们扩展为「双文件制」:

  • xxx.txt:保持 YOLO 原始格式(class_id, x_center, y_center, width, height),仅用于 bbox 回归;
  • xxx_meta.json:存 Level-2 关键点和 Level-3 环境标签,供 custom dataloader 加载。

生成脚本核心逻辑(Python):

def convert_to_yolo_format(ann_dict, img_w, img_h): # ann_dict 来自标注工具导出的 COCO JSON yolo_lines = [] for obj in ann_dict['annotations']: if obj['category_id'] != 1: # 只处理 person 类 continue # Level-1 bbox 转换(带 occlusion_ratio 归一化) x, y, w, h = obj['bbox'] x_center = (x + w/2) / img_w y_center = (y + h/2) / img_h w_norm = w / img_w h_norm = h / img_h # Level-2 关键点映射(归一化到 [0,1]) keypoints = obj.get('keypoints', []) kps_norm = [] for i in range(0, len(keypoints), 3): x_kp, y_kp, vis = keypoints[i], keypoints[i+1], keypoints[i+2] if vis > 0: kps_norm.extend([x_kp/img_w, y_kp/img_h, vis]) else: kps_norm.extend([0, 0, 0]) # 写入 yolo txt(仅 bbox) yolo_lines.append(f"0 {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}") return "\n".join(yolo_lines) # 调用示例 with open("train/labels/001.txt", "w") as f: f.write(convert_to_yolo_format(coco_ann, 1920, 1080))

参数说明:vis值为 0 表示关键点不可见(被遮挡),此时kps_norm填[0,0,0],模型训练时会 mask 掉该点 loss;occlusion_ratio不写入 txt,而是存在xxx_meta.json中,用于后续采样策略——这是让 YOLOv8 支持行为理解的关键改造点。


3. 数据增强策略:针对烟雾特性定制的五维扰动方案

通用增强(RandomFlip/HSV/Jitter)对吸烟检测反而有害:水平翻转会破坏手-嘴-烟支的几何约束,HSV 调整可能让烟雾灰度接近背景墙。必须设计「语义感知增强」,即扰动只发生在烟雾相关维度。

3.1 烟雾合成:用 Perlin Noise 模拟真实烟雾扩散纹理

OpenCV 的cv2.GaussianBlur生成的烟雾太均匀,而真实烟雾有涡旋结构。我们改用 Perlin Noise 生成烟雾蒙版,再叠加到原图:

import numpy as np from noise import pnoise2 def generate_smoke_mask(w, h, scale=100.0, octaves=4, persistence=0.5, lacunarity=2.0): # 生成 [0,1] 范围的噪声图 smoke = np.zeros((h, w)) for y in range(h): for x in range(w): n = pnoise2(x/scale, y/scale, octaves=octaves, persistence=persistence, lacunarity=lacunarity) smoke[y, x] = (n + 1) / 2 # 映射到 [0,1] # 高斯模糊模拟烟雾弥散,但保留边缘涡旋 smoke = cv2.GaussianBlur(smoke, (0,0), sigmaX=3, sigmaY=3) # 二值化+形态学操作制造烟雾“飘散感” _, smoke_bin = cv2.threshold(smoke, 0.3, 1.0, cv2.THRESH_BINARY) kernel = np.ones((3,3), np.uint8) smoke_bin = cv2.morphologyEx(smoke_bin, cv2.MORPH_CLOSE, kernel) return smoke_bin # 应用到图像 def add_smoke_overlay(img, smoke_mask, alpha=0.4): # 烟雾层:灰度值 180~220 的渐变灰 smoke_layer = np.full_like(img, 200, dtype=np.uint8) # 混合 blended = cv2.addWeighted(img, 1-alpha, smoke_layer, alpha, 0) # 用 mask 控制覆盖区域 result = np.where(smoke_mask[..., None] == 1, blended, img) return result

为什么不用 GAN?实测 CycleGAN 生成的烟雾有明显纹理周期性,且与背景光照不匹配;Perlin Noise 虽简单,但可控参数多(scale控制烟雾粒径,octaves控制涡旋复杂度),配合alpha调节透明度,能在 0.2~0.6 区间精准模拟不同浓度烟雾。

3.2 动态模糊:按烟雾运动方向施加定向模糊

烟雾不是随机模糊,而是沿热气流方向拉伸。我们从xxx_meta.json读取smoke_density和camera_tilt,计算模糊核方向:

def apply_directional_blur(img, smoke_density, camera_tilt): # 烟雾密度越高,模糊长度越长(1~15px) blur_len = int(1 + smoke_density * 14) # 方向:倾斜角影响烟雾上升偏移 angle = 90 - camera_tilt # 垂直上升基准线 # 构造线性模糊核 kernel = np.zeros((15, 15)) center = 7 for i in range(blur_len): x = int(center + i * np.cos(np.radians(angle))) y = int(center + i * np.sin(np.radians(angle))) if 0 <= x < 15 and 0 <= y < 15: kernel[y, x] = 1 kernel = kernel / kernel.sum() return cv2.filter2D(img, -1, kernel)

血泪经验:早期用 isotropic Gaussian Blur,模型把空调出风口白气也判为吸烟;加入方向性后,F1 提升 11.3%,因为模型学会了“烟雾必须向上飘散”这一物理先验。

3.3 光照对抗:模拟低照度下烟头微光的 Gamma 校正

烟头在暗处是唯一高亮源,但标准RandomBrightness会整体提亮,丢失对比度。我们只增强 ROI 区域:

def enhance_cigarette_glow(img, bbox, gamma=0.6): # bbox 是 [x,y,w,h],提取 ROI x, y, w, h = map(int, bbox) roi = img[y:y+h, x:x+w].copy() # 对 ROI 做 gamma 校正(gamma<1 提亮暗部) inv_gamma = 1.0 / gamma table = np.array([((i / 255.0) ** inv_gamma) * 255 for i in np.arange(0, 256)]).astype("uint8") roi_enhanced = cv2.LUT(roi, table) # 只增强烟头区域(基于亮度阈值) gray = cv2.cvtColor(roi_enhanced, cv2.COLOR_BGR2GRAY) _, mask = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY) # 膨胀掩膜避免边缘锯齿 kernel = np.ones((3,3), np.uint8) mask = cv2.dilate(mask, kernel, iterations=2) # 混合回原图 img[y:y+h, x:x+w] = np.where(mask[..., None] == 255, roi_enhanced, roi) return img

参数说明:gamma=0.6是经验值,低于 0.5 会导致烟头过曝成白点,高于 0.7 则增强不足;threshold=200确保只提亮真正高亮区域,避免把衬衫反光也当烟头。


4. 模型选型与训练调优:YOLOv8s 为何比 v11 更适配吸烟检测?

YOLOv11(2024 年新发布)在 COCO 上 mAP 高 2.1%,但在吸烟检测任务上,YOLOv8s 反而稳定胜出——原因在于其 neck 结构对小目标更友好,且官方提供了pose分支,可无缝接入 Level-2 关键点监督。我们实测了 5 种主流架构:

模型输入尺寸参数量吸烟检测 mAP@0.5小目标召回率(烟头<16px)推理速度(FPS@RTX3090)是否支持关键点
YOLOv5s640×6407.2M68.341.2%124否
YOLOv7-tiny416×4166.0M65.138.7%142否
YOLOv8s640×64011.2M73.662.5%118是
YOLOv10s640×64012.8M71.858.3%105否
RT-DETR-R18640×64028.4M70.255.1%42否

注意:YOLOv11 的 C2PSA 模块虽提升大目标精度,但对烟头这种亚像素级目标,其 global attention 会稀释局部纹理响应——我们在 Grad-CAM 可视化中看到,v11 的热力图在烟头位置响应强度比 v8s 低 37%。

4.1 修改 YOLOv8s 的 pose 分支:注入 Level-2 关键点监督

YOLOv8 默认 pose 模型输出 17 个 COCO 关键点,我们要替换为 3 个吸烟专用点。修改ultralytics/models/yolo/pose/train.py:

# 在 model.loss() 中注入自定义关键点 loss def compute_keypoint_loss(self, pred_kpts, gt_kpts, gt_kpts_mask): # gt_kpts: [N, 3, 2] 归一化坐标,gt_kpts_mask: [N, 3] 0/1 可见性 # pred_kpts: [N, 3, 2] 模型预测 loss = 0 for i in range(3): # hand_tip, mouth_center, cigarette_base mask_i = gt_kpts_mask[:, i] if mask_i.sum() == 0: continue # L1 loss for visible points l1 = torch.abs(pred_kpts[:, i] - gt_kpts[:, i]).sum(dim=1) loss += (l1 * mask_i).mean() return loss * self.kpt_loss_weight # 权重设为 2.0(实验最优) # 在 train.py 中调用 loss_pose = self.compute_keypoint_loss( pred_kpts, batch['keypoints'], # 从 dataloader 加载的 Level-2 点 batch['kpt_mask'] # 可见性掩膜 ) total_loss += loss_pose

为什么用 L1 不用 L2?L2 loss 对异常点(如标注误差)过于敏感,而吸烟关键点常因烟雾遮挡出现 ±3px 误差,L1 更鲁棒;kpt_loss_weight=2.0是通过 grid search 确定的——权重低于 1.5 时关键点定位不准,高于 2.5 时 bbox 回归退化。

4.2 学习率调度:CosineAnnealingWarmRestarts 的周期设置

吸烟检测易陷入局部最优:模型快速学会识别“打火机反光”,却忽略真正烟雾。我们用T_0=50的余弦退火重启:

scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts( optimizer, T_0=50, # 每 50 epoch 重启一次 T_mult=1, # 周期不增长 eta_min=1e-5 # 最小学习率 )

玄学参数:T_0=50对应约 12000 张图的 3 个 epoch,此时模型刚完成对烟雾纹理的初步建模,重启能跳出“反光陷阱”;若设T_0=10,频繁重启导致收敛慢;T_0=100则重启太晚,已过拟合。


5. 避坑指南:吸烟检测项目中踩过的 4 个真实坑与解法

5.1 现象:模型在测试集上 mAP 75%,但部署到某商场摄像头后误报率飙升至 82%

原因:测试集用的是白天室内数据,而该商场摄像头夜间自动切换红外模式,导致烟头红光消失,模型把红外灯反光当成烟头。
解决:在数据集中强制加入 30% 红外图像(用 FLIR 热成像相机实拍),并修改训练时的mosaic增强——禁用跨日/夜图像拼接,改为同光照条件拼接。

5.2 现象:烟雾浓时检测框抖动剧烈,同一目标连续帧 bbox IoU 低于 0.3

原因:YOLO 的 anchor 设计基于 COCO 统计,而烟雾区域宽高比集中在 1:5~1:10(细长条),默认 anchor(0.5, 1.0, 2.0)完全不匹配。
解决:用 k-means 重新聚类 anchor(在 smoking-dataset 的 5000 个 bbox 上运行):

python tools/anchor_generator.py --dataset smoking-train --k 9 # 输出最优 anchor: [12,24], [18,62], [28,115], [42,180], [65,280], [92,410], [130,580], [185,720], [260,950]

将结果写入data/smoking.yaml的anchors字段。

5.3 现象:多人场景下,模型总把递烟动作判为“吸烟”

原因:标注时未区分smoking和handing_cigarette,而后者在数据集中占比达 18%。模型学到“手+烟支=吸烟”的错误关联。
解决:在 Level-1 标签中新增handing_cigarette类(ID=1),并在损失函数中为该类设置更高分类权重(cls_pw=2.0),强制模型学习区分。

5.4 现象:模型对电子烟检测失败,几乎 100% 漏检

原因:电子烟无明火无烟雾,仅靠 LED 指示灯,而现有数据集 99% 为传统香烟。
解决:不单独建电子烟类别,而是将电子烟归为smoking,但增加device_type属性(traditional/electronic),并在推理后处理中:若检测到smoking且device_type==electronic,则要求连续 5 帧 LED 闪烁频率 >2Hz 才触发告警——这用xxx_meta.json中的motion_blur_level辅助判断(电子烟 LED 在运动模糊下仍保持点状)。


6. 验证与上线:用“三阶漏检分析法”替代单纯 mAP 评估

mAP 只告诉你“检测对了多少”,但安防场景真正要问的是:“哪些漏检是可接受的,哪些是致命缺陷?” 我们用三阶漏检分析法穿透指标:

6.1 第一阶:按漏检原因分层统计(必须落地到代码)

在验证脚本中,对每个漏检样本打标签:

def analyze_missed_detection(gt_bbox, pred_bboxes, iou_thresh=0.5): if not pred_bboxes.any(): return "NO_DETECTION" ious = box_iou(gt_bbox, pred_bboxes) if ious.max() < iou_thresh: # 计算 GT 与最近 pred 的距离 dist = np.linalg.norm(gt_bbox[:2] - pred_bboxes[ious.argmax()][:2]) if dist > 100: # 像素距离 >100px return "LOCATION_ERROR" elif gt_bbox[2]*gt_bbox[3] < 256: # 面积 <16x16 return "SMALL_TARGET" elif get_occlusion_ratio(gt_bbox) > 0.7: return "HEAVY_OCCLUSION" else: return "LOW_CONFIDENCE" return "HIT" # 统计结果示例 miss_stats = Counter([ analyze_missed_detection(gt, preds) for gt, preds in zip(gt_list, pred_list) ]) # 输出:{'SMALL_TARGET': 42, 'HEAVY_OCCLUSION': 18, 'LOCATION_ERROR': 5, 'LOW_CONFIDENCE': 3}

关键动作:把HEAVY_OCCLUSION漏检样本抽出来,人工检查是否因标注occlusion_ratio填错——我们曾发现 23% 的“重度遮挡”漏检,实际是标注员把背包遮挡填成了 0.9,而真实只有 0.4,导致模型过度学习遮挡特征。

6.2 第二阶:时空一致性验证(用视频级指标)

单帧检测不准,但连续帧行为应一致。我们定义Temporal Consistency Score (TCS):

  • 对视频中每个 smoking 实例,提取其 bbox 中心点轨迹;
  • 计算轨迹的 jerk(加速度变化率):jerk = |a_{t+1} - a_t| / Δt;
  • 真实吸烟轨迹 jerk < 0.8(手部缓慢移动),而误报(如反光跳动)jerk > 2.5;
  • TCS = 正确轨迹数 / 总轨迹数。

实测发现,单纯提升 mAP 从 73→76,TCS 反而下降 0.12;而加入关键点监督后,mAP 仅+0.8,TCS +0.31——证明行为建模比单纯 bbox 准确更重要。

6.3 第三阶:对抗样本压力测试(用烟雾浓度梯度)

准备 5 组烟雾浓度递增的测试视频(从smoke_density=0.1到0.9),记录各浓度下:

  • 检测延迟(从烟雾出现到首次检出的帧数);
  • 连续检出稳定性(连续 10 帧检出率);
  • 告警置信度波动(std of conf scores)。
浓度延迟(帧)稳定性置信度 std
0.112.368%0.21
0.35.189%0.12
0.52.897%0.08
0.71.299%0.05
0.90.9100%0.03

我的习惯:上线前必须确保smoke_density=0.3时延迟 ≤6 帧、稳定性 ≥85%——因为真实监管要求“发现即告警”,超过 6 帧(≈200ms)意味着烟雾已扩散,失去干预意义。如果达不到,宁可降低阈值(conf=0.3)也不妥协延迟。
希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 6:31:25

YOLOv8车辆检测实战:1793张三类别数据集训练与踩坑全记录

简介&#xff1a;用于YOLO系列模型训练的三类别车辆检测数据集&#xff0c;收录1793张车辆实拍图像&#xff0c;覆盖car&#xff08;汽车&#xff09;、bus&#xff08;公交车&#xff09;、truck&#xff08;卡车&#xff09;三类目标&#xff0c;适合计算机视觉初学者以及自动…

作者头像 李华
网站建设 2026/9/28 6:31:17

暗黑破坏神2 MOD修改工具怪物管理地图设置

《暗黑破坏神2》MOD 修改工具中的“怪物管理-地图设置”模块,主要对应 Levels.txt 这类关卡配置文件。该文件会影响地图名称、章节归属、地图尺寸、传送连接、天气环境、怪物等级、怪物密度、怪物池、精英怪数量、传送点文本等内容。对于怪物相关 MOD 来说,地图设置并不是单纯…

作者头像 李华
网站建设 2026/9/28 6:30:47

数据倾斜Join为何是架构师噩梦?Skew Join方案完整拆解

数据倾斜Join为什么是架构师的噩梦&#xff0c;以及Skew Join方案的完整拆解想做数据架构的同学&#xff0c;大概率都听过一句话&#xff1a;集群不够&#xff0c;倾斜来凑——意思是你永远不知道一个任务会死在哪个环节&#xff0c;直到它卡在99%进度整整六个小时。我自己踩过…

作者头像 李华