news 2026/10/5 8:55:36

基于YOLOv8的吸烟行为检测:991张图片实现88.3%识别率的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YOLOv8的吸烟行为检测:991张图片实现88.3%识别率的实战指南

简介:这份吸烟行为检测数据集面向计算机视觉开发者与目标检测学习者,可用于训练和验证抽烟场景下的目标识别模型,适用于安防监控、公共场所行为分析等应用方向。资源共包含991张原始图片,每张图片均配有对应的YOLO格式标注文件,另附1个yaml配置文件,压缩包内文件总数1983个,以jpg图像与txt标注为主,整体约44.7MB,可直接接入YOLOv8训练流程。据描述,该数据集在常规训练条件下平均识别率可达88.3%,标注质量与场景覆盖具备一定参考价值。目前已有110人学习下载,适合需要快速搭建吸烟检测基线、开展模型微调或进行算法对比实验的读者使用,也可作为课程设计与毕业设计的数据支撑。

1. 吸烟数据集 991 张原始图片:88.3% 识别率背后的真实门槛

手上只有 991 张原始图片,标注成 yolov8 格式,跑出平均识别率 88.3%——这个数字放在公开数据集上不算亮眼,但放在一个自采的小规模吸烟行为数据集上,已经能支撑一个可用的原型系统。问题在于,很多人拿到这类数据集的第一反应是直接yolo train,然后发现验证集 mAP 忽高忽低,换一批测试图就崩。88.3% 不是终点,它更像一个基准线:告诉你这个数据规模下模型能学到什么、学不到什么。吸烟行为检测的难点不在“烟”本身,而在手部遮挡、烟雾干扰、光照变化和拍摄角度。991 张图意味着每个类别平均不到 1000 个样本,如果场景分布再偏一点,模型很容易记住背景而不是行为。这篇笔记面向的是手里有类似小规模数据集、想用 yolov8 跑出稳定结果的工程师,从数据检查、标注格式转换、训练参数到部署前的验证,把 88.3% 这个数字拆开看,哪些是数据决定的,哪些是参数能救的。

2. 从 991 张原始图片到 yolov8 可训练格式:数据检查与标注转换

2.1 先别急着训练:991 张图的分布检查

拿到“991 张原始图片”这个数字,第一件事不是解压,而是统计。吸烟行为检测的数据集通常来自监控截图、手机拍摄或网络爬取,场景一致性差。我一般会先跑一个脚本,输出图片尺寸分布、亮度均值、以及标注框的宽高比。这一步能提前暴露三类问题:尺寸极端(比如大量 1920×1080 和少量 640×480 混在一起)、亮度断层(白天和夜间样本比例失衡)、标注框异常(宽高比接近 1:1 的框很可能是误标的人脸或手部)。

import os from PIL import Image import numpy as np img_dir = "raw_images" sizes = [] brightness = [] for fname in os.listdir(img_dir): if not fname.lower().endswith((".jpg", ".png", ".jpeg")): continue path = os.path.join(img_dir, fname) with Image.open(path) as im: w, h = im.size sizes.append((w, h)) gray = np.array(im.convert("L")) brightness.append(gray.mean()) print("唯一尺寸数量:", len(set(sizes))) print("尺寸样本:", list(set(sizes))[:10]) print("亮度均值范围: %.1f - %.1f" % (min(brightness), max(brightness))) print("亮度标准差: %.1f" % np.std(brightness))

这段脚本不依赖任何深度学习框架,纯 PIL + numpy 就能跑。重点看两个输出:唯一尺寸数量如果超过 5 种,说明数据来源杂,训练前必须统一 resize 或做多尺度增强;亮度标准差超过 40,说明光照变化剧烈,需要针对性做亮度抖动增强。991 张图里如果夜间样本少于 150 张,模型在暗光下的漏检率会明显上升,这时候要么补数据,要么在训练时把hsv_v调高。

2.2 yolov8 标注格式的四个字段与常见转换错误

yolov8 的标注格式是每张图对应一个.txt文件,每行五个值:class_id x_center y_center width height,全部归一化到 0-1。很多人从 LabelImg 或 CVAT 导出时选错格式,得到的是 VOC 的xmin ymin xmax ymax绝对值,直接丢给 yolov8 训练会报错或学出乱框。转换逻辑不复杂,但有两个坑:一是坐标越界,xmax超过图片宽度时归一化后大于 1,yolov8 虽然不报错但会裁切,导致框偏移;二是类别名和data.yaml里的names顺序不一致,模型会把“吸烟”和“未吸烟”学反。

import os from PIL import Image def voc_to_yolo(img_path, xml_boxes, class_map, out_path): """ img_path: 图片路径,用于读取宽高 xml_boxes: list of (class_name, xmin, ymin, xmax, ymax) class_map: dict, 类别名到 id 的映射 out_path: 输出 txt 路径 """ with Image.open(img_path) as im: W, H = im.size lines = [] for cls_name, xmin, ymin, xmax, ymax in xml_boxes: # 裁剪越界坐标 xmin = max(0, min(xmin, W - 1)) ymin = max(0, min(ymin, H - 1)) xmax = max(0, min(xmax, W - 1)) ymax = max(0, min(ymax, H - 1)) if xmax <= xmin or ymax <= ymin: continue # 跳过无效框 xc = (xmin + xmax) / 2.0 / W yc = (ymin + ymax) / 2.0 / H bw = (xmax - xmin) / W bh = (ymax - ymin) / H lines.append(f"{class_map[cls_name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines))

参数说明:class_map必须和data.yaml里的names完全一致,建议用{"smoking": 0, "not_smoking": 1}这种显式映射,不要依赖字母序。xmin等坐标裁剪到[0, W-1]是为了防止标注工具导出的浮点坐标越界。如果转换后某个 txt 文件为空,说明该图所有框都被判为无效,需要回查原标注。991 张图转换完,建议统计一下每类框的数量,如果某一类少于 300 个框,训练时该类别的召回率会明显偏低。

2.3 data.yaml 的路径写法与类别顺序

data.yaml是 yolov8 训练的入口配置,三个关键字段:train、val、names。路径建议用绝对路径,相对路径在yolo train切换工作目录时容易找不到。names的顺序就是class_id的顺序,写错一个位置,整个模型的输出语义就反了。常见做法是把train和val指向两个文件夹,每个文件夹下放images和labels子目录,yolov8 会自动匹配同名文件。

path: /data/smoking_dataset train: /data/smoking_dataset/train/images val: /data/smoking_dataset/val/images nc: 2 names: 0: smoking 1: not_smoking

注意nc必须等于names的长度。如果只有“吸烟”一个类别,nc: 1,names: {0: smoking}。991 张图按 8:2 划分,训练集约 793 张,验证集约 198 张。如果某些场景在验证集里完全没出现,88.3% 这个数字会虚高,实际部署时遇到新场景就翻车。建议按场景分层抽样,而不是随机划分。

3. yolov8 训练参数怎么设:从 88.3% 往回推

3.1 小数据集必须改的四个超参数

yolov8 的默认参数是针对 COCO 这种十万级数据集设计的,991 张图直接套默认值,过拟合几乎必然。我一般会改四个:epochs、batch、lr0、patience。epochs默认 100,小数据集可以拉到 200-300,但必须配合patience早停,否则后面全是过拟合。batch默认 16,如果显存够,保持 16 或降到 8,小 batch 对小数据集的梯度噪声反而有正则效果。lr0默认 0.01,小数据集建议降到 0.001-0.005,避免初期震荡。patience设 30-50,验证集 mAP 连续不升就停。

yolo detect train \ data=/data/smoking_dataset/data.yaml \ model=yolov8n.pt \ epochs=250 \ batch=8 \ imgsz=640 \ lr0=0.003 \ patience=40 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ degrees=10 \ translate=0.1 \ scale=0.5 \ fliplr=0.5 \ mosaic=1.0 \ close_mosaic=20 \ device=0

参数说明:hsv_v=0.4比默认 0.4 略高,针对吸烟场景的光照变化;degrees=10允许小角度旋转,因为拍摄角度不可能完全正对;close_mosaic=20表示最后 20 个 epoch 关闭 mosaic 增强,让模型在真实分布上收敛。model=yolov8n.pt是最小模型,991 张图用 n 或 s 就够了,用 m 以上反而容易过拟合。如果显存只有 6GB,batch=4,imgsz=512,但识别率可能掉 2-3 个点。

3.2 训练过程看什么:损失曲线与 mAP 的对应关系

yolov8 训练时会输出box_loss、cls_loss、dfl_loss和mAP50、mAP50-95。小数据集最典型的曲线是:前 50 个 epoch 损失快速下降,mAP 爬到 80% 左右,然后开始震荡。如果cls_loss持续下降但mAP50不升,说明模型在记忆训练集,验证集没泛化。这时候看val/box_loss,如果它开始上升,就是过拟合信号,早停该生效了。88.3% 的识别率如果是mAP50,对应mAP50-95大概在 55-65% 之间,这个差距说明框的定位精度还有提升空间,但分类本身已经比较稳。

from ultralytics import YOLO import matplotlib.pyplot as plt model = YOLO("runs/detect/train/weights/best.pt") metrics = model.val(data="/data/smoking_dataset/data.yaml", split="val") print("mAP50:", metrics.box.map50) print("mAP50-95:", metrics.box.map) print("每类 AP:", metrics.box.ap_class_index, metrics.box.ap)

这段代码加载训练好的best.pt,在验证集上重新评估。metrics.box.ap会输出每个类别的 AP,如果“吸烟”类的 AP 明显低于“未吸烟”,说明吸烟样本的特征更难学,可能需要单独补吸烟的正样本,或者在损失函数里给吸烟类更高权重。注意val的split参数要和训练时一致,否则评估结果不可比。

3.3 识别率 88.3% 的拆解:哪些图在拉低指标

88.3% 是一个平均值,真正要关心的是哪些图被分错了。我一般会把验证集里置信度低于 0.5 的预测框和对应的真值框导出来,人工看一遍。常见错误类型有三种:手部遮挡导致烟头只露出一小截,模型漏检;烟雾和背景颜色接近,模型把烟雾当成烟;夜间红外模式下烟头高亮,模型过检。这三种错误对应的改进方向不同:漏检补数据,误检加负样本,过检调conf阈值。

from ultralytics import YOLO import cv2 model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="/data/smoking_dataset/val/images", conf=0.25, iou=0.45, save=True, save_txt=True, project="runs/error_analysis" ) # 低置信度样本会保存在 runs/error_analysis 下,人工复查

conf=0.25是导出低置信度预测的常用阈值,比默认 0.25 更低可以暴露更多边缘案例。save_txt=True会把预测框写成 yolov8 格式,方便和真值对比。如果发现某类错误集中在特定场景(比如全是夜间),那就针对该场景做数据增强或补采,而不是盲目调参。991 张图里如果有 100 张夜间图,模型在夜间的识别率可能只有 70%,但白天能到 95%,平均下来 88.3% 就是这么来的。

4. 吸烟检测的避坑与排查:991 张图踩过的五个坑

4.1 坑一:验证集 mAP 很高,换视频流就崩

现象:训练完mAP50到 88.3%,但拿一段监控视频逐帧推理,漏检率超过 40%。原因:验证集和训练集来自同一批图片,场景分布几乎一样,模型学到的是这批图的背景特征,而不是吸烟行为的通用特征。解决:按场景划分验证集,比如训练集用室内监控,验证集用室外手机拍摄;或者用yolo val时指定split=test,提前留出一批完全没参与训练的图。如果数据量实在不够,至少保证验证集里有 20% 的图来自不同设备或不同光照条件。

4.2 坑二:标注框把整只手都框进去

现象:模型推理时把整个手部区域判为吸烟,烟头反而没框出来。原因:标注时为了省事,把拿烟的手整体框住,而不是只框烟头或烟身。yolov8 学到的特征是“手+烟”的组合,当手部动作类似但不拿烟时,误检率飙升。解决:重新标注,只框烟头或烟身可见部分,手部不框。如果烟头太小(小于 16×16 像素),考虑放大图片或提高imgsz到 1280。991 张图里如果超过 30% 的框是“手+烟”,这个坑几乎必踩。

4.3 坑三:类别不平衡导致“未吸烟”类霸屏

现象:推理结果几乎全是“未吸烟”,吸烟类的召回率不到 50%。原因:991 张图里“未吸烟”样本远多于“吸烟”样本,模型倾向于预测多数类。解决:在data.yaml里给吸烟类更高权重,或者用yolo train的cls参数调整分类损失权重。更直接的办法是过采样吸烟类图片,复制到训练集里,但要注意不要复制验证集里的图。常见做法是保持验证集分布不变,只在训练集里做重采样。

4.4 坑四:imgsz 和原始图片尺寸不匹配

现象:训练时imgsz=640,但原始图片是 1920×1080,小烟头被 resize 后只剩几个像素,模型学不到。原因:yolov8 默认会把图片缩放到imgsz,小目标在缩放后信息丢失。解决:如果烟头在原始图中占比小于 1%,把imgsz提到 1280 或 1536,或者用切片推理(SAHI)。但提高imgsz会显著增加显存和训练时间,991 张图用 1280 训练,batch 可能要降到 4。折中方案是保持 640 训练,推理时用 1280,但效果不如训练时就匹配。

4.5 坑五:数据增强过度导致烟头变形

现象:训练时mosaic=1.0、degrees=45、scale=0.9,模型在验证集上表现还行,但实际图片里烟头稍微倾斜就漏检。原因:过度的几何增强让模型没见过正常角度的烟头,学到的特征被扭曲。解决:小数据集增强要克制,degrees不超过 15,scale不超过 0.5,mosaic可以在最后 20 个 epoch 关闭。吸烟检测的关键特征是烟头的形状和颜色,过度旋转和缩放会破坏这些特征。我一般会先用默认增强跑一版,看验证集错误类型,再针对性加增强。

5. 把 88.3% 推到 90% 以上的三个进阶技巧

5.1 用热力图定位模型的注意力盲区

yolov8 本身不直接输出热力图,但可以用Grad-CAM对 backbone 的最后一层做可视化。如果热力图集中在背景而不是烟头区域,说明模型学偏了。这个技巧在 991 张图这种小数据集上特别有用,因为模型很容易走捷径。我一般会挑 10 张验证集里预测正确的图,看热力图是否落在烟头上;再挑 10 张预测错误的图,看模型在看哪里。如果错误图的热力图集中在手部或脸部,说明标注或数据分布有问题。

import torch import cv2 import numpy as np from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") model.model.eval() # 取 backbone 最后一层 target_layer = model.model.model[-2] features = [] grads = [] def hook_fn(module, input, output): features.append(output) output.register_hook(lambda grad: grads.append(grad)) handle = target_layer.register_forward_hook(hook_fn) img = cv2.imread("/data/smoking_dataset/val/images/sample.jpg") img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results = model.predict(img_rgb, conf=0.25) # 反向传播取梯度 loss = results[0].boxes.conf.sum() loss.backward() feat = features[0].detach().cpu().numpy()[0] grad = grads[0].detach().cpu().numpy()[0] weights = np.mean(grad, axis=(1, 2)) cam = np.sum(weights[:, None, None] * feat, axis=0) cam = np.maximum(cam, 0) cam = cv2.resize(cam, (img.shape[1], img.shape[0])) cam = cam / cam.max() heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) overlay = cv2.addWeighted(img, 0.6, heatmap, 0.4, 0) cv2.imwrite("heatmap.jpg", overlay) handle.remove()

这段代码的关键是target_layer的选择,model.model[-2]通常是 backbone 的最后一个 C2f 模块。weights是每个通道的梯度均值,cam是加权后的特征图。热力图红色区域就是模型关注的地方。如果红色在背景,说明模型没学到烟头特征,需要检查标注或增加烟头区域的裁剪增强。

5.2 用 TTA 和模型融合榨出最后两个点

测试时增强(TTA)对小数据集提升明显,因为模型对同一张图的不同变换预测可以互补。yolov8 的predict支持augment=True,会做水平翻转和多尺度推理。但 TTA 会拖慢推理速度,适合离线分析或对实时性要求不高的场景。模型融合更直接:用yolov8n和yolov8s各训一版,推理时把两版的框做 NMS 融合。991 张图训两个模型,总时间可控,融合后 mAP 通常能涨 1-2 个点。

from ultralytics import YOLO import numpy as np model_n = YOLO("runs/detect/train_n/weights/best.pt") model_s = YOLO("runs/detect/train_s/weights/best.pt") img = "/data/smoking_dataset/val/images/sample.jpg" res_n = model_n.predict(img, conf=0.25, augment=True)[0] res_s = model_s.predict(img, conf=0.25, augment=True)[0] # 合并两版框,按置信度排序后做 NMS boxes = np.concatenate([res_n.boxes.xyxy.cpu().numpy(), res_s.boxes.xyxy.cpu().numpy()]) scores = np.concatenate([res_n.boxes.conf.cpu().numpy(), res_s.boxes.conf.cpu().numpy()]) classes = np.concatenate([res_n.boxes.cls.cpu().numpy(), res_s.boxes.cls.cpu().numpy()]) # 简单按类别做 NMS,实际可用 torchvision.ops.nms from torchvision.ops import nms import torch keep = nms(torch.tensor(boxes), torch.tensor(scores), iou_threshold=0.5) final_boxes = boxes[keep.numpy()] final_scores = scores[keep.numpy()] print("融合后框数:", len(final_boxes))

augment=True会做水平翻转和多尺度,推理时间大概是原来的 3 倍。nms的iou_threshold=0.5是常用值,如果两版模型框重叠度高,可以降到 0.4。融合的前提是两个模型的错误不相关,所以最好用不同 backbone 或不同数据增强策略训出来的模型。如果两个模型都是yolov8n且训练参数一样,融合提升有限。

5.3 部署前的最后一关:用真实视频流做影子测试

88.3% 是验证集上的数字,部署到实际场景前,必须用真实视频流做影子测试。我一般会拿一段 5-10 分钟的监控视频,逐帧推理,统计每类的检出次数和误报次数。重点看三个指标:吸烟行为的检出率、误报率(把非吸烟判为吸烟)、以及连续帧的一致性。如果某一帧检出吸烟,下一帧又消失,说明模型不稳定,需要加时序平滑。影子测试不需要标注,人工看一遍就能发现大部分问题。991 张图训出来的模型,在真实视频流上能保持 85% 以上的检出率,这个方案就值得继续投入;如果掉到 70% 以下,优先补数据而不是调参。

我自己的习惯是,每次训完一个版本,先不急着看 mAP,而是拿一段没参与训练的监控视频跑一遍,把误报和漏报的帧截出来,按场景分类。这个习惯帮我省了很多“验证集虚高”的后悔药。小数据集做吸烟检测,数据质量比模型结构重要得多,991 张图如果标注干净、场景覆盖全,yolov8n 足够用;如果标注混乱,换多大的模型都救不回来。希望帮到你。

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

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

PyTorch从零搭建UNet:掌握编码器、跳跃连接与图像分割实战

简介&#xff1a;面向深度学习初学者与图像分割研究人员&#xff0c;这是一份基于PyTorch搭建U-Net网络并训练自定义数据集的完整工程资源&#xff0c;可应用于医学图像分割、卫星影像分析、视频目标分割等场景。资源系统梳理了U-Net的核心结构&#xff1a;编码器通过卷积与池化…

作者头像 李华
网站建设 2026/10/5 8:53:37

Codex Agent自动解决Git冲突后为什么测试能过,业务逻辑却丢了?

使用 ChatGPT、Codex Agent 做合并、Rebase 或处理多人协作代码时&#xff0c;经常会遇到一种非常隐蔽的问题&#xff1a;Git冲突看起来已经解决了&#xff0c;测试也全部通过&#xff0c;但上线以后才发现&#xff0c;一段原本应该保留的业务逻辑没了。常见表现包括&#xff1…

作者头像 李华
网站建设 2026/10/5 8:53:07

淘宝商品详情字段解析:SKU、价格、库存接口实战与避坑指南

首先说明一下这个项目的实际背景&#xff1a;我做电商数据这块有几年了&#xff0c;经常要跟淘宝、天猫、京东这些平台的商品数据打交道。前阵子有个朋友问我&#xff0c;说自己想做个商品比价的小工具&#xff0c;但是卡在淘宝商品详情字段解析上&#xff0c;SKU、价格、库存这…

作者头像 李华
网站建设 2026/10/5 8:52:28

Hadoop+Spark电商用户行为分析实战:从集群部署到可视化大屏

如果你手里正好有一批电商用户行为日志&#xff0c;比如几十万甚至上千万条带用户ID、商品ID、行为类型和时间戳的记录&#xff0c;老板或者导师只丢给你一句话&#xff1a;分析一下用户都在干什么&#xff0c;再做一个可视化大屏。我最近刚把一个HadoopSpark基于Python的电商用…

作者头像 李华
网站建设 2026/10/5 8:52:24

ArcGIS“保存栅格数据集失败”报错排查与修复全攻略

搞地理配准想快速校正一张影像&#xff0c;结果在最后一步点了保存&#xff0c;ArcGIS直接弹一句“保存栅格数据集失败”&#xff0c;任谁都得懵一下。这个报错我在ArcGIS 10.x和ArcGIS Pro里都踩过&#xff0c;而且不是一次两次。说实话&#xff0c;“保存栅格数据集失败”本身…

作者头像 李华
网站建设 2026/10/5 8:52:05

TongWeb部署JSP报ClassCastException:JDT类加载器冲突排查与解决

看到这条堆栈的时候&#xff0c;我第一反应是&#xff1a;又是类加载器打架。TongWeb 7049m10 上部署应用&#xff0c;日志里突然冒出一句java.lang.ClassCastException: xxx cannot be cast to com.tongweb.eclipse.jdt.internal.compiler.lookup.TypeBinding&#xff0c;如果…

作者头像 李华