☰
吸烟数据集带标注:YOLOv8训练与小目标召回提升实战
2026/9/25 2:18:13 网站建设 项目流程

简介:这份「吸烟数据集带标注」面向人工智能与机器学习方向的开发者、算法工程师及高校学生,用于训练和评估吸烟行为识别与预测模型。数据集以监督学习为目标,每个样本均带有吸烟者或非吸烟者的标注信息,可支撑分类、特征分析等典型任务。压缩包共1567个文件,包含783个xml标注文件与783个jpg图像文件,另有1个txt说明文件,整体约31.71MB;xml与jpg一一对应,便于直接用于目标检测或图像分类流程。目前已有158人学习下载,属于小众但结构完整的实战数据。读者可借助该数据集完成从数据读取、标注解析到模型训练与验证的完整链路,理解个人信息、生活习惯、健康状况、社会经济状态及环境因素等特征对吸烟行为的影响,并在此基础上练习数据划分、过拟合控制与隐私合规处理,适合作为课程设计、毕业项目或算法入门的练手素材。

1. 吸烟数据集带标注:从“找不到”到“跑得通”的那条路

做行为识别项目的工程师大多经历过这个场景:算法框架搭好了,模型结构也调通了,结果卡在数据上——尤其是吸烟这类细粒度动作,公开数据少、标注质量参差、场景单一,翻遍几个常用数据集平台,要么只有分类标签没有检测框,要么全是摆拍正脸,跟真实监控画面差着十万八千里。吸烟数据集带标注这件事,核心要解决的就是“让模型真的能学到烟支、手部、嘴部之间的空间关系”,而不是只记住“有个人站在那儿”。它适合三类人:做智慧安防、工地/加油站禁烟区行为检测的算法工程师;需要快速验证检测方案可行性的产品原型开发者;以及带学生做课程设计、毕设的高校老师。这一章不展开具体操作,先把“为什么吸烟检测的数据这么难搞”和“一份能用的带标注数据集应该长什么样”说清楚,后面几章再一步步落到格式转换、训练配置和踩坑排查上。

2. 吸烟行为检测的数据集到底该标什么:从业务需求反推标注规范

2.1 先想清楚模型要输出什么,再决定标什么

很多团队拿到一批监控截图就开始标,标完发现模型学出来的东西跟业务对不上。问题出在标注之前没有做“输出定义”。吸烟检测在实际落地中通常有三种输出形态,对应的标注方式完全不同。

第一种是图像分类:只判断整张图里有没有人吸烟。这种标注成本最低,一个文件夹放正样本、一个放负样本就行,但误报率极高——手里拿根笔、叼根牙签都可能触发。第二种是目标检测:在图中框出“吸烟行为”这个整体,或者分别框出“烟支”和“手部-嘴部区域”。这是目前工地、加油站场景最常用的方案,因为可以直接在画面上画框告警。第三种是关键点+检测:除了框,还要标出烟支的两个端点、手部关键点、嘴部位置,用于判断烟支是否真的在嘴边。这种标注成本最高,但误报率能压到很低。

我一般建议:如果只是做区域入侵式的粗筛,目标检测足够;如果要做到“烟支在嘴边持续超过2秒才告警”,那至少要把烟支和嘴部区域分别框出来,让模型自己去学空间关系。标注规范里必须写清楚:烟支被手指遮挡超过50%时怎么标、烟支只露出一小截时标不标、多人同时吸烟时是每人一个框还是一个整体框。这些边界情况不提前定好,后面返工的成本远高于标注本身。

2.2 一份可用的吸烟数据集应该包含哪些场景维度

公开渠道能拿到的吸烟数据,绝大多数是正面、近距离、光照良好的摆拍图。拿这种数据训出来的模型,一到真实监控画面就翻车——侧脸、低头、远距离、逆光、夜间红外,全是没见过的分布。所以自建或筛选数据集时,场景维度比数量更重要。

我通常会按下面这张表来检查一个吸烟数据集是否“够用”:

维度最低要求理想覆盖
拍摄角度正面、侧面各30%正面、侧面、背面、俯视、仰视
光照条件白天室内/室外白天、黄昏、夜间红外、逆光
距离近景(人脸可辨)近景、中景、远景(人占画面1/10)
遮挡程度无遮挡手部遮挡烟支、口罩下拉、烟雾遮挡
分辨率不低于640×4801080P为主,少量低分辨率负样本
负样本不吸烟人像拿笔、叼牙签、打电话、喝水、戴口罩

负样本的重要性经常被低估。一个只见过正样本的模型,会把所有“手靠近脸”的动作都判成吸烟。负样本里必须包含“手拿笔靠近嘴”“叼吸管”“打电话”“摸下巴”这几类硬负样本,数量至少是正样本的1.5倍。标注时负样本不需要画框,但要在文件名或标注文件里明确标记为negative,训练时作为背景图参与。

2.3 标注格式选型:COCO、YOLO、VOC到底用哪个

标注格式没有绝对优劣,取决于你下游用什么训练框架。但有一个原则:原始标注只存一份,用脚本转成其他格式,不要手工维护多份,否则改一个框要同步三个地方,迟早出错。

常见做法是:标注阶段用LabelImg或CVAT导出VOC格式(XML),因为工具成熟、标注员上手快;然后写一个转换脚本,同时生成YOLO格式(txt)和COCO格式(json)。YOLO格式每行是class_id x_center y_center width height,全部归一化到0-1;COCO格式是json,包含images、annotations、categories三个顶层字段。VOC格式则是每个图一个XML,包含filename、size、object等节点。

下面这个Python脚本是我常用的VOC转YOLO+COCO的转换核心逻辑,假设你的VOC XML放在annotations/目录,图片放在images/目录:

import os import xml.etree.ElementTree as ET import json from PIL import Image # 类别映射:根据你的标注类别修改 CLASS_MAP = {"smoking": 0, "cigarette": 1, "mouth": 2} def voc_to_yolo(xml_path, img_dir, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find("filename").text img_path = os.path.join(img_dir, img_name) w, h = Image.open(img_path).size # 用实际图片尺寸,不要信XML里的size lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in CLASS_MAP: continue cls_id = CLASS_MAP[cls_name] bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 归一化并转为中心点+宽高 x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") out_txt = os.path.join(out_dir, img_name.replace(".jpg", ".txt")) with open(out_txt, "w") as f: f.write("\n".join(lines)) # 批量处理 for xml_file in os.listdir("annotations"): if xml_file.endswith(".xml"): voc_to_yolo(os.path.join("annotations", xml_file), "images", "labels")

这段代码的关键点有三个。第一,图片尺寸必须从实际图片读取,不能直接用XML里写的width/height,因为标注工具有时会写错或者图片被重新缩放过后没更新XML。第二,归一化后的坐标要保留6位小数,YOLO训练时对精度敏感,尤其是小目标。第三,类别映射表要跟你的data.yaml里的names顺序完全一致,否则训练出来的类别全是乱的。

参数方面,CLASS_MAP里的类别名必须和XML里<name>节点的文本完全匹配,大小写都不能差。如果你的标注里烟支叫“cigarette”但映射表里写的是“cig”,那这个框会被直接跳过,训练时表现为“某些图明明标了却学不到”。转换完成后,建议随机抽10张图,用labelImg打开对应的txt可视化检查一遍,确认框的位置和类别都对。

3. 用YOLOv8跑通吸烟检测:从数据组织到训练命令的完整链路

3.1 数据目录结构怎么摆才不会被框架坑

YOLO系列对目录结构有固定要求,摆错了不会报错,但会静默跳过数据,表现为loss不下降或者mAP恒为0。标准结构如下:

smoking_dataset/ ├── images/ │ ├── train/ # 训练集图片 │ ├── val/ # 验证集图片 │ └── test/ # 测试集图片(可选) ├── labels/ │ ├── train/ # 训练集标注txt │ ├── val/ │ └── test/ └── data.yaml # 数据集配置文件

注意images/train/和labels/train/是平级目录,不是嵌套关系。YOLOv8在加载时会自动把images替换成labels去找对应的txt文件,所以文件名必须一一对应,abc.jpg对应abc.txt。如果某张图没有标注(纯负样本),也要放一个空的txt文件,否则框架会报“找不到标签”。

data.yaml的内容如下:

path: /absolute/path/to/smoking_dataset train: images/train val: images/val test: images/test names: 0: smoking 1: cigarette 2: mouth

path必须写绝对路径,写相对路径在部分环境下会解析到框架安装目录去。names的索引必须从0开始连续,不能跳号。如果你的类别只有“smoking”一个,那就只写0: smoking,不要留空行。

3.2 训练命令与关键参数:batch、imgsz、lr0怎么定

数据摆好之后,训练命令本身不复杂,但参数设错会导致显存溢出或者模型不收敛。下面是我在单卡24G显存上跑吸烟检测的常用命令:

yolo detect train \ data=smoking_dataset/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ augment=True \ mosaic=1.0 \ mixup=0.1 \ device=0 \ project=runs/smoking \ name=exp1

逐项说明:model=yolov8s.pt是轻量级模型,吸烟检测这种单类或三类任务,s版本在精度和速度上比较平衡;如果部署在边缘设备上,可以换yolov8n.pt,但小目标召回会降。imgsz=640是输入分辨率,吸烟场景里烟支往往只占几十个像素,如果显存允许,建议提到imgsz=960,小目标召回能明显提升。batch=16在24G卡上跑640分辨率基本不会溢出,如果报CUDA out of memory,先降到8,再不行就降到4。lr0=0.01是初始学习率,YOLOv8默认就是0.01,但如果你的数据集小于2000张,建议降到0.005,否则容易过拟合。patience=30表示30个epoch验证指标不提升就早停,吸烟数据量通常不大,这个值设20-30比较合理。mosaic=1.0是马赛克增强,对小目标检测很有帮助,但如果你的图片里烟支特别小,mosaic后可能小到看不见,可以降到0.5。mixup=0.1是混合增强,负样本多的时候可以适当提高,但不要超过0.3,否则正样本被过度稀释。

训练过程中重点看三个指标:metrics/mAP50、metrics/mAP50-95和train/box_loss。mAP50在第一个epoch通常就能到0.3以上,如果一直是0,八成是数据路径或类别映射出了问题。box_loss如果持续在1.0以上不降,检查标注框的归一化坐标是否在0-1之间,超出范围的框会被框架忽略。

3.3 推理验证:用训练好的权重跑单张图和视频流

训练完成后,权重默认保存在runs/smoking/exp1/weights/best.pt。先用单张图验证:

yolo detect predict \ model=runs/smoking/exp1/weights/best.pt \ source=test_images/ \ conf=0.25 \ iou=0.45 \ save=True \ project=runs/predict

conf=0.25是置信度阈值,吸烟检测建议先设0.25看召回,如果误报多再往上调。iou=0.45是NMS的IoU阈值,如果同一个人身上出现多个重叠框,把这个值降到0.3-0.4。推理结果会保存在runs/predict/下,直接看画框的图,重点检查三类错误:漏检(烟支在嘴边但没框)、误检(拿笔被判成吸烟)、框位置偏移(框到了手但没框到烟支)。如果漏检集中在远景小目标,回去把imgsz提到960重新训;如果误检集中在硬负样本,把那些负样本加进训练集,重新跑。

视频流推理把source换成视频文件路径或摄像头编号即可,但要注意帧率。YOLOv8s在640分辨率下,单张1080P图在RTX 3060上大约15-20ms,换算成视频流大概50-60FPS,足够处理25FPS的监控流。如果部署在Jetson这类边缘设备上,建议导出TensorRT引擎,速度能翻倍。

4. 吸烟数据集标注与训练中的五个血泪坑

4.1 坑一:标注框把整个头都框进去,模型学不到烟支

现象:训练loss正常下降,mAP50能到0.7以上,但推理时只要人把手举到脸附近就报警,烟支有没有根本不在乎。

原因:标注时图省事,直接把“吸烟的人头”整个框进去,模型学到的是“手靠近脸=吸烟”,而不是“烟支在嘴边=吸烟”。这是吸烟检测里最隐蔽的坑,因为指标看起来很好,上线后误报率却高得离谱。

解决:标注规范里强制要求框住烟支本身,或者至少框住“烟支+嘴部”的联合区域,不要框整个头。如果烟支太小不好标,先把图片放大到200%再标。已经标完的数据,写脚本检查框的宽高比,如果宽高比接近1:1且面积占图片比例超过15%,大概率是框了整个头,需要返工。

4.2 坑二:负样本里没有“手拿笔”“叼吸管”这类硬负样本

现象:模型在测试集上表现很好,一到真实场景,工人拿笔记录、叼着吸管喝水,全触发吸烟告警。

原因:训练集里的负样本只有“正常站立的人”,模型没见过“手拿细长物体靠近嘴”的负样本,把“细长物体+嘴”这个组合直接映射成了吸烟。

解决:专门收集一批硬负样本,包括手拿笔、叼牙签、叼吸管、打电话、摸下巴、戴口罩下拉到下巴。每类至少200张,混进训练集的负样本目录。如果实在找不到,可以用数据增强合成:把烟支的标注框去掉,只保留手和嘴的区域,作为负样本加入。重新训练后,误报率通常能降一半以上。

4.3 坑三:VOC转YOLO时图片尺寸用了XML里的旧值

现象:转换后的txt里坐标全是0或者大于1,训练时框架直接跳过这些标注,表现为“某些图明明标了但模型完全没学到”。

原因:标注工具在图片被裁剪或缩放后,XML里的<size>节点没有同步更新,转换脚本直接读了XML里的width/height,导致归一化分母错误。

解决:转换脚本里强制用PIL.Image.open(img_path).size读取实际图片尺寸,不要信XML里的值。转换完成后,写一个校验脚本,遍历所有txt,检查每行坐标是否在0-1之间,超出范围的打印出来人工核对。

4.4 坑四:训练集和验证集里出现了同一段视频的相邻帧

现象:验证集mAP很高,但换一个完全不同的场景测试,指标断崖式下跌。

原因:数据划分时按图片随机分,同一段视频的相邻帧被分到了训练集和验证集。相邻帧几乎一模一样,模型在验证集上相当于“见过原题”,指标虚高。

解决:按视频源划分,同一段视频的所有帧只能出现在训练集或验证集其中一个里。如果数据来自多个摄像头,按摄像头编号划分。划分完成后,检查训练集和验证集的图片文件名前缀,确保没有重叠的视频ID。

4.5 坑五:推理时conf设太高,夜间红外画面全漏检

现象:白天场景检测正常,一到夜间红外画面,所有吸烟行为都漏检。

原因:夜间红外画面对比度低、烟支边缘模糊,模型输出的置信度普遍偏低。如果conf沿用白天的0.25,夜间可能只有0.1-0.15,全被过滤掉。

解决:分场景设阈值。白天conf=0.25,夜间conf=0.1,同时把NMS的iou降到0.3避免重叠框。更好的做法是在训练集里加入夜间红外样本,让模型自己适应低对比度。如果夜间样本少于总数据的10%,建议单独微调一个夜间模型,不要和白天混在一起训。

5. 把吸烟检测推到可用:小目标召回提升与误报压制的几个实操技巧

5.1 用切片推理把远景小烟支捞回来

监控画面里人占画面1/10的时候,烟支可能只有10-20个像素。YOLOv8在640输入下,经过5次下采样,20像素的烟支在特征图上只剩不到1个像素,召回率自然低。我常用的做法是切片推理:把原图切成4块或9块,每块单独推理,再把结果映射回原图做NMS。这样每块里的烟支相对变大,召回率能提升15-25个百分点。

实现上不需要改模型,写一个推理后处理脚本即可:

import cv2 import numpy as np from ultralytics import YOLO model = YOLO("best.pt") img = cv2.imread("test.jpg") h, w = img.shape[:2] tile_size = 640 overlap = 128 # 切片重叠,避免边缘目标被切断 all_boxes = [] for y in range(0, h, tile_size - overlap): for x in range(0, w, tile_size - overlap): tile = img[y:y+tile_size, x:x+tile_size] if tile.shape[0] < 32 or tile.shape[1] < 32: continue results = model(tile, conf=0.15, verbose=False) for box in results[0].boxes: xyxy = box.xyxy[0].cpu().numpy() xyxy[0] += x xyxy[1] += y xyxy[2] += x xyxy[3] += y all_boxes.append((xyxy, float(box.conf[0]), int(box.cls[0]))) # 对所有框做全局NMS def nms(boxes, iou_thresh=0.4): boxes = sorted(boxes, key=lambda x: x[1], reverse=True) keep = [] while boxes: best = boxes.pop(0) keep.append(best) boxes = [b for b in boxes if iou(best[0], b[0]) < iou_thresh] return keep def iou(a, b): x1 = max(a[0], b[0]); y1 = max(a[1], b[1]) x2 = min(a[2], b[2]); y2 = min(a[3], b[3]) inter = max(0, x2-x1) * max(0, y2-y1) area_a = (a[2]-a[0]) * (a[3]-a[1]) area_b = (b[2]-b[0]) * (b[3]-b[1]) return inter / (area_a + area_b - inter + 1e-6) final = nms(all_boxes, iou_thresh=0.4)

这段代码的关键参数是tile_size和overlap。tile_size建议跟训练时的imgsz一致,训练用640就切640。overlap设128是为了让切片边缘的目标至少完整出现在某一块里,太小会切断烟支,太大计算量翻倍。切片推理的代价是推理时间变成原来的4-9倍,如果对实时性要求高,可以只对画面中“人”的区域做切片,先用一个人体检测模型框出人,再在人框区域做切片推理,计算量能降一个数量级。

5.2 用时序滤波压掉单帧误报

单帧检测难免有误报,但吸烟是一个持续动作,真实吸烟至少持续几秒。利用这个先验,可以做一个简单的时序滤波:维护一个长度为N的滑动窗口,记录每一帧是否检测到吸烟,只有当窗口内检测到吸烟的帧数超过阈值时,才触发告警。

我一般设N=15(约0.5秒@30FPS),阈值=10。也就是说,15帧里至少10帧检测到吸烟才报警。这个参数可以根据业务容忍度调:工地禁烟区要求零漏报,可以把阈值降到6;加油站要求零误报,把阈值提到12。时序滤波的代码很简单,一个collections.deque就能实现,但效果立竿见影,单帧误报基本能被压掉80%以上。

5.3 模型导出与边缘部署的注意事项

如果最终要部署到Jetson、瑞芯微这类边缘设备,训练完的.pt需要导出成对应格式。YOLOv8支持导出ONNX、TensorRT、OpenVINO等:

yolo export model=best.pt format=engine half=True device=0

format=engine是TensorRT引擎,half=True开启FP16量化,速度能提升1.5-2倍,精度损失通常在1%以内。注意TensorRT引擎跟设备架构绑定,在RTX 3060上导出的引擎不能直接拿到Jetson上跑,必须在目标设备上重新导出。如果目标设备是Jetson,建议在Jetson上装好TensorRT和ultralytics后,直接在Jetson上执行导出命令。

导出后一定要用同一批测试图对比.pt和.engine的推理结果,确认框的位置和置信度没有明显偏移。我遇到过FP16量化后小目标置信度整体下降0.1的情况,这种时候要么关掉half,要么把conf阈值相应调低。

5.4 一个我反复用的验证习惯

每次训完一个新版本,我不会只看mAP指标,而是固定做三件事:第一,从验证集里挑20张最难的正样本(远景、遮挡、夜间)和20张硬负样本,单独跑一遍,人眼过一遍框;第二,把模型接到一段真实监控视频上,跑完整段,统计告警次数和实际吸烟次数的比例;第三,把上一版模型和这一版模型在同一段视频上跑,对比告警时间点和数量,确认新版本没有引入新的误报模式。这个习惯帮我省了很多次“指标涨了但上线翻车”的后悔药。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询