☰
YOLOv5单类烟雾检测:从权重推理到告警链路实战
2026/10/26 2:04:26 网站建设 项目流程

简介:这是一份基于YOLOv5的烟雾检测完整方案包,面向安全监控、火灾预警方向的开发者与算法学习者,提供从数据到模型的可运行素材。包内共2000个文件,以jpg图像、txt标注与xml标注为主,配合yaml配置、py脚本和pt权重等,覆盖数据组织、训练配置与推理部署所需环节,压缩包约360.52MB,目录便于按模块取用。已有2357人学习下载,说明其在烟雾检测入门与项目复现中具有一定参考价值。读者可直接使用训练好的模型对烟雾目标进行检测,也可依托带位置标注的图像数据开展再训练、微调或迁移学习,并结合不同光照、角度与背景样本观察泛化表现。方案还保留了训练脚本、配置文件与说明文档,方便理解YOLOv5的训练流程、参数调整和部署思路,适合快速搭建烟雾预警原型或作为课程设计、竞赛与工程验证的基础材料。

1. 薄雾先于明火出现:这套 YOLOv5 烟雾检测值不值得下载

真实场景里,明火往往不是第一现场。配电箱短路、电池鼓包、线缆过载,最先出现在画面里的是一层灰白色薄雾,慢慢扩散、变浓,几十秒后才见火苗。等点位烟感到阈值再报警,处置窗口已经过去一半。这个项目做的就是用 YOLOv5 做单类别烟雾检测,包里同时给了训好的权重和配套数据集——拿到手不用从零标注、从零训练,先跑推理看效果,觉得漏检多再拿数据集接着微调。它适合两类人:一是在仓库、车棚、机房这类场景做视频烟雾预警原型的开发者,二是想拿一份真实单类检测数据,把「训练—验证—部署」整条链路走一遍的人。下面按结构、数据、训练、排查、改造的顺序拆。

2. 目录与首次推理:权重、数据集、脚本分别在哪个目录里

拿到压缩包,我一般不会先开训练,而是先确认三件事:权重能不能正常加载、数据集路径有没有对上、推理脚本能不能出一张带框的图。这三件事任何一件不通,后面开训就是白烧显卡。这一章先把骨架摸清,再用包里自带的权重跑通第一次推理。

2.1 骨架分层:三个目录决定你八成的时间

YOLOv5 的工程结构比较固定,这个烟雾检测项目基本沿用了同一套布局,额外加了权重和数据集两块。常见做法是这样:

smoke-yolov5/ ├── train.py # 训练入口 ├── detect.py # 推理入口(图片/视频/摄像头/流) ├── val.py # 验证集评估 ├── export.py # 导出 ONNX / TensorRT 等 ├── models/ │ ├── yolov5s.yaml # 网络结构,s/m/l 对应不同容量 │ └── hub/ ├── data/ │ ├── smoke.yaml # 数据集描述文件:路径、类别数、类名 │ └── hyps/ │ └── hyp.scratch-low.yaml # 超参模板 ├── weights/ │ └── best.pt # 训好的烟雾检测权重 ├── datasets/ │ └── smoke/ │ ├── images/train/ # 训练图片 │ ├── images/val/ # 验证图片 │ ├── labels/train/ # 与图片同名的 .txt 标注 │ └── labels/val/ └── utils/ # 数据加载、增强、指标、绘图等

images和labels是严格镜像的两棵树:images/train/a.jpg对应labels/train/a.txt,少一个文件就等于这条样本废掉。weights/best.pt是整个包最值钱的东西,它决定你第一次推理能不能看到框;data/smoke.yaml决定训练时去哪里找图。剩下utils目录平时不用改,改动集中在detect.py和自定义脚本上就够了。

提示:先把整个项目复制一份再动手,runs/目录会不断产生新结果,路径一乱很难回溯到原始状态。

2.2 环境对齐:torch、CUDA 和 requirements 的版本三角

深度学习项目最玄学的地方就在版本。YOLOv5 依赖 torch、torchvision 和 CUDA 三者的对应关系,随便装最新版大概率报CUDA error或者算子不支持。我的习惯是建独立虚拟环境,先把 torch 装到和本机驱动匹配的版本,再装其余依赖。

python -m venv smoke-env # Linux / macOS source smoke-env/bin/activate # Windows smoke-env\Scripts\activate python -m pip install --upgrade pip # 先按本机 CUDA 版本装 torch 与 torchvision,两者版本必须成对 pip install torch torchvision # 再补齐项目其余依赖 pip install -r requirements.txt

装完别急着跑推理,先验证一下环境到底认不认卡:

import torch print("torch:", torch.__version__) print("cuda 可用:", torch.cuda.is_available()) print("设备数:", torch.cuda.device_count()) print("设备名:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU")

torch.cuda.is_available()返回False就先别往下走,常见原因是装成了 CPU 版 wheel,或者驱动版本低于 torch 编译时用的 CUDA runtime。纯 CPU 也能推理,一张 640 图大概几百毫秒到一两秒,够验证效果,训练就别想了。

2.3 detect.py 跑通第一张图与第一段视频

推理脚本的参数不多,但每个都影响结果。先用包里自带的权重跑单张图:

python detect.py \ --weights weights/best.pt \ --source samples/test.jpg \ --img-size 640 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --device 0 \ --save-txt
参数作用建议取值
--weights权重路径包内best.pt,不要用空字符串
--source输入源图片、目录、视频文件、0(摄像头)、流地址
--img-size推理输入边长必须和训练时一致,常见 640
--conf-thres置信度阈值验证阶段先降到 0.1 看召回上限
--iou-thresNMS 的 IoU 阈值0.45,烟团互相重叠多可适当调高
--device计算设备0表示第一张卡,cpu表示 CPU
--save-txt保存检测结果到 txt接下游告警逻辑时需要

跑完结果默认落在runs/detect/exp/下,带框的图或视频直接看。--save-txt生成的 txt 每行是类别 cx cy w h 置信度,坐标是归一化后的,接自己的业务代码时要乘回原图宽高。想直接看视频效果就把--source换成视频文件路径;想接实时流,把--source换成摄像头序号或 RTSP 地址,配合--view-img实时预览。

注意:--img-size不是越大越好。烟雾是弥散目标、边缘模糊,640 通常够用;盲拉到 1280 会明显掉帧,实时链路容易堆帧。

3. 数据集拆解:单类 smoke 的标注、校验与切分

权重好用,但真正决定你能不能二次开发的是数据集。包的标注是 YOLO 格式,一行一个框,字段是类别 id cx cy w h,全部归一化到 0~1。这一章讲清三件事:类别怎么设计、标注怎么校验、训练验证怎么切。

3.1 类别设计与样本分布:为什么先做单类

烟雾检测最容易踩的坑是一上来就分「白烟、黑烟、薄烟、浓烟」四类。分了之后你会发现标注一致性极差——同一团烟在不同帧里被判成不同类别,模型学到的决策边界是噪声。这个项目只设一个smoke类,是正确取舍:检测阶段只回答「这里有没有烟」,浓淡、颜色这类属性交给下游用面积占比、置信度均值去估,反而更可控。

样本分布上要盯两个方向。一是目标尺度,烟雾随距离和扩散程度变化很大,远处一点白烟可能只占几十个像素,近处浓烟能铺满半个画面,如果标注框尺寸的中位数特别小,说明小目标占比高,输入分辨率就得往上抬。二是背景干扰,雾天、蒸汽、逆光扬尘、白色墙面反光这些负样本缺了,模型就会把一切浅色弥散物当烟。包里数据集的负样本占比如果偏低,我一般会自己补一批纯背景图,配同名的空.txt文件,这在后面第 5 章会展开说。

3.2 标签校验:用一段脚本把脏标注挡在训练之前

我曾经因为一个坐标越界的标注,让模型训了六个小时才从 loss 曲线里看出不对劲——训练不报错,只是永远学不好。所以现在拿到任何数据集,第一步都是先跑校验,而不是先跑train.py:

from pathlib import Path IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp"} root = Path("datasets/smoke") problems = [] for split in ("train", "val"): img_dir, lbl_dir = root / "images" / split, root / "labels" / split for img in img_dir.iterdir(): if img.suffix.lower() not in IMG_EXTS: continue lbl = lbl_dir / f"{img.stem}.txt" if not lbl.exists(): # 图片有、标注没有 problems.append((str(img), "缺标签文件")) continue for ln, line in enumerate(lbl.read_text(encoding="utf-8").splitlines(), 1): if not line.strip(): # 空行跳过,空文件是合法的负样本 continue parts = line.split() if len(parts) != 5: # 字段数必须是 5 problems.append((str(lbl), f"第{ln}行字段数={len(parts)}")) continue cid = int(float(parts[0])) cx, cy, w, h = map(float, parts[1:]) if cid != 0: # 单类项目里类别 id 只能是 0 problems.append((str(lbl), f"第{ln}行类别id={cid}")) if not (0 < w <= 1 and 0 < h <= 1): # 宽高归一化越界,最常见 problems.append((str(lbl), f"第{ln}行宽高越界 w={w:.4f} h={h:.4f}")) if not (0 <= cx <= 1 and 0 <= cy <= 1): problems.append((str(lbl), f"第{ln}行中心点越界 cx={cx:.4f} cy={cy:.4f}")) print(f"共发现 {len(problems)} 条问题") for item in problems[:30]: print(item)

脚本逻辑很直白:先检查图片和标注的一一对应,再逐行检查字段数、类别 id、归一化范围。w或h大于 1 的标注,训练时会被裁到边界,框偏移以后模型学到的就是错的回归目标,指标自然上不去。跑完把problems清空,再进训练环节,能省掉大半排查时间。

3.3 data.yaml 写法与按视频源切分

数据集描述文件决定训练时去哪找图,字段不多但路径语义容易搞错:

# data/smoke.yaml path: ../datasets/smoke # 数据集根目录,相对 train.py 的运行目录 train: images/train # 相对 path val: images/val # 相对 path nc: 1 # 类别数,必须和 names 长度一致 names: ["smoke"]

path写错是最常见的启动失败原因。稳妥做法是写绝对路径,或者干脆在项目根目录执行python train.py,让相对路径的基准固定下来。nc和names不一致会直接抛断言错误,不会静默出错。

切分上有一条血泪经验:绝对不能随机按帧打乱切分。摄像头是连续采集的,相邻帧几乎是同一张图,随机切分会让同一段视频的帧同时出现在训练集和验证集里,验证 mAP 冲到 0.9 以上,一上真实场景立刻原形毕露。正确做法是按视频源切——先列出所有原始视频或采集批次,按批次划分 train/val,再在批次内部展开成帧。比例上 8:2 起步,验证集里至少保留三段完全不同场景的素材,指标才有参考价值。

4. 训练链路:从 anchors 到 train.py 的参数落点

推理跑通、数据干净,就可以动训练了。这一章讲三件事:anchors 要不要重聚类、微调和重训在命令上的差别、指标该盯哪几个。包里既然给了训好的权重,多数人其实只需要做微调,别一上来就把权重丢掉重训。

4.1 anchors 与输入尺寸:别直接套 COCO 的聚类结果

YOLOv5 默认 anchors 是在通用数据集上聚出来的,形状偏方正、尺寸偏中小。烟雾这个目标的形态差异很大:远处是一小团,近处是横向铺开的一大片,和通用 anchors 的匹配度一般。动手前先统计一下标注框的归一化宽高分布:

import glob import numpy as np wh = [] for f in glob.glob("datasets/smoke/labels/train/*.txt"): for line in open(f, encoding="utf-8"): p = line.split() if len(p) == 5: wh.append([float(p[3]), float(p[4])]) # 归一化后的宽、高 wh = np.array(wh) print("标注框数量:", len(wh)) for name, col in (("宽", wh[:, 0]), ("高", wh[:, 1])): print(f"{name} 中位数 {np.median(col):.4f} 90分位 {np.percentile(col, 90):.4f} 最大 {col.max():.4f}") print("小目标占比(<0.1):", float(((wh[:, 0] < 0.1) & (wh[:, 1] < 0.1)).mean()))

90 分位和小目标占比这两个数决定两件事。小目标占比超过三成,--img 640就偏小,可以试 960,代价是显存和耗时翻倍;如果宽的中位数明显大于高的中位数,说明烟团偏横向,这时用kmeans在自己数据上重聚 9 组 anchors 写回models/yolov5s.yaml里的anchors:字段,通常能涨一两个点。改动不大,但比盲目调学习率有用得多。

4.2 训练命令:微调和重训是两条路

包里给了best.pt,所以有两条路可选,命令差别只在--weights和--cfg:

# 路线一:在已有权重上继续微调(推荐先用这条) python train.py \ --weights weights/best.pt \ --data data/smoke.yaml \ --hyp data/hyps/hyp.scratch-low.yaml \ --img 640 --batch 16 --epochs 80 \ --device 0 --workers 4 \ --patience 20 \ --project runs/train --name smoke_ft # 路线二:从结构文件重新初始化(只有在权重和你的类别数不匹配时才用) python train.py \ --cfg models/yolov5s.yaml \ --weights "" \ --data data/smoke.yaml \ --img 640 --batch 16 --epochs 150 \ --device 0 --workers 4
参数含义微调时的调整思路
--weights初始权重微调给best.pt;重训给""
--cfg网络结构 yaml微调时不要和--weights同时给,结构从权重推断
--hyp超参模板微调建议用较低学习率的自定义 hyp
--img训练输入边长与推理保持一致,改了就两边一起改
--batch批大小显存不够就降,或设-1让脚本自动探测
--patience早停轮数20 比较稳,指标卡住就停,省卡
--workers数据加载进程Windows 上建议设 0 或 2,多了容易卡死

微调时最该改的是学习率。hyp.scratch-low.yaml里lr0: 0.01是给从头训练用的,在已收敛权重上继续用这么高的学习率,很可能把原有特征冲掉,指标先跌后涨甚至涨不回来。我一般复制一份 hyp,把lr0降到0.001左右、lrf保持0.01,warmup_epochs调到 1 以内,让它平滑过渡。另外mosaic增强在烟雾场景里是把双刃剑:它能提升泛化,但会把烟团拼到不相干的背景上,收敛后期反而干扰,常见做法是用--close-mosaic 10在最后十个 epoch 关掉。

4.3 指标怎么看:mAP 之外必须盯住召回

训练日志里那一串数字,最容易被误读的是mAP@0.5。烟雾检测的业务属性决定了漏检代价远大于误检——报一次假警顶多多看一眼,漏一次就是真事故。所以看结果时优先看recall,其次是mAP@0.5,mAP@0.5:0.95只当作定位精度的参考,不用太纠结。

具体怎么看:每个 epoch 结束会在runs/train/smoke_ft/下输出results.csv、混淆矩阵和val_batch*_pred.jpg可视化图。混淆矩阵里如果背景被大量判成 smoke,说明负样本不够;如果 smoke 有大量漏到背景,说明召回不足,可以试着把推理阈值降下来看 P-R 曲线上的工作点。val_batch*_pred.jpg是必看的,那些指标虚高但框明显偏到墙上的情况,只有看图才能发现。

5. 排查清单:烟雾检测从训练到部署最常翻车的五件事

下面这五条基本都是我在同类任务里踩过或者帮别人定位过的,按出现频率排序,每条按「现象—原因—解决」写,遇到问题可以直接对号入座。

5.1 现象:loss 一直在降,mAP 却长期贴着 0

原因九成在数据配置,不在模型。可能是labels目录名和images没有镜像对应,可能是data/smoke.yaml里的path相对基准错了导致训练时读到的是空目录,也可能是标注里类别 id 写成了 1 而nc只设了 1。训练脚本对这类问题并不总是报错,它会安静地拿一堆无标签图训下去。

解决办法是回到第 3 章的校验脚本,先确认图片和标注一一对应;再在train.py同目录下打印一次数据集统计,确认训练集样本数不为零;最后检查nc、names长度、标注里的类别 id 三者是否一致。

5.2 现象:推理脚本跑完,输出图上一个框都没有

先别怀疑权重坏了。第一顺位是--conf-thres给太高,尤其是换了场景以后模型置信度整体偏低;第二顺位是--img-size和训练时不一致,缩放比例变了,特征对不上;第三顺位是图片本身是 BGR/RGB 通道错乱或者 EXIF 旋转导致内容方向不对。

排查手法很土但有效:把--conf-thres降到0.05重跑一次,如果出现大量低置信度框,说明模型没问题,只是阈值不合适,再二分法找到合适的值;如果依然是零框,就换一张数据集 val 目录里的原图试,能出框说明是输入图的问题,不能出框才去怀疑权重和预处理。

5.3 现象:验证集指标漂亮,实拍视频误报成片

这是第 3.3 节那个切分问题的延迟爆发,也可能是负样本不足。典型表现是把白墙、地面反光、雾气、蒸汽管道全部框成 smoke,置信度还不低。根因在于训练集里这类背景从来没有以「不是烟」的身份出现过,模型没学过拒绝。

解决分两步:先按视频源重新切分 train/val,把泄漏堵掉,重新看一次真实指标;再补负样本,从误报最严重的场景里截一批纯背景图放进images/train,配同名空.txt(空文件就是合法的负样本标注),数量按正样本的 10%~20% 起步。补完再微调十几个 epoch,误报通常能明显压下去。

5.4 现象:视频里的框疯狂闪烁,一帧有一帧无

单帧独立推理的通病。烟雾本身是半透明、边界模糊的目标,相邻两帧的检测置信度在小数点后两位上下浮动,跨过阈值就表现为忽隐忽现。这不是模型不准,而是缺少时序约束。

解决思路是在detect.py外面包一层帧间确认,把「连续 N 帧命中」作为触发条件,具体实现放到第 6 章。另外一个辅助手段是把--iou-thres调高一点,减少同一团烟被 NMS 拆成多个框的情况。

5.5 现象:导出 ONNX 后同一张图检不出目标

导出链路的问题通常有三类:输入尺寸没固定成静态,动态轴导致后处理解析错位;导出的模型输出没带 NMS,下游直接当最终结果用;预处理没对齐,比如 letterbox 的填充值、归一化顺序和原脚本不一致。

正确姿势是用项目自带的export.py导出,明确指定--imgsz和--include onnx,需要的话加--simplify。导出后必须做一致性验证:同一张图分别走 torch 权重和 onnx 模型,把两边输出的框坐标打印出来比对,差值应该在极小范围内。对不上就回到预处理那一步逐行核对,别在解码逻辑上瞎试。

6. 二次开发:把逐帧检测改成能触发告警的判定链路

detect.py的输出是「这一帧有哪些框」,而业务要的是「现在要不要报警」,中间差一层判定。我一般会加一个独立的状态机,不侵入原有推理代码:

import time from collections import deque class SmokeAlarm: """帧间确认:need 帧命中且跨度不超过 max_gap 才触发,触发后冷却 cooldown 秒""" def __init__(self, need=5, max_gap=1.5, cooldown=30.0, conf=0.35): self.need, self.max_gap = need, max_gap # 命中帧数与时间窗 self.cooldown, self.conf = cooldown, conf # 冷却时间、置信度门限 self.hits = deque(maxlen=need) # 只保留最近 need 次命中时间戳 self.last_fire = 0.0 def update(self, dets, ts=None): ts = ts or time.time() # dets 每项格式:[x1, y1, x2, y2, conf, cls] confs = [d[4] for d in dets if d[4] >= self.conf] if confs: self.hits.append(ts) fired = False if len(self.hits) == self.need and ts - self.hits[0] <= self.max_gap: if ts - self.last_fire > self.cooldown: # 冷却期内不重复告警 self.last_fire = ts fired = True return fired, max(confs) if confs else 0.0

need和max_gap是一对:前者防偶然误检,后者防「攒够五帧但横跨十秒」这种伪连续,视频卡顿时尤其明显。cooldown决定告警抑制窗口,接短信或音柱时必须设,否则一次起火能推送几百条。conf建议比推理阈值略高,让判定层比展示层更保守。

改造完的验证方法别靠肉眼看视频:把包里 val 目录的图片按顺序拼成伪视频逐帧喂进去,记录每次fired的时间点,统计应触发场景的触发延迟和纯背景场景的误触发次数。两个数都达标再上现场。

我第一次做这类告警链路时图省事,直接把逐帧结果往消息队列里塞,测试环境好好的,上线第一天因为一段逆光视频刷了四百多条推送,被值班的人追着问了半天。从那以后我每次改判定逻辑,都强制先跑一遍伪视频统计触发次数,再谈上线。希望帮到你。

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

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

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

立即咨询