YOLOv8森林火灾烟雾检测全流程:数据训练、界面部署与误报抑制
2026/9/13 1:38:46 网站建设 项目流程

简介:基于YOLOv8的森林火灾早期烟雾预警项目,面向计算机视觉、深度学习方向的毕设与课程设计场景,适合计科、人工智能、自动化等专业学生作为完整实践参考。包内集成源码、可视化界面、完整数据集和部署说明,一并提供核心指标曲线、混淆矩阵、F1分数曲线、PR曲线、验证集预测结果及标签分布图,可直接支撑论文图表与答辩展示。资源共8个文件,主要包括3个Python脚本(模型训练、视频检测、可视化页面)、3个模型权重文件(yolov8n、best、yolo11n)以及2个说明文档,压缩包15.91MB,结构精简、即下即用。已有53人学习浏览,代码经测试运行成功,对于希望快速搭建森林火灾预警演示系统的读者,能节省大量调试时间,拿来即可二次开发。

1. 为什么烟雾比明火更难预警:YOLOv8 在森林火灾早期发现里的定位

森林火灾预警最值钱的时间窗口在烟雾阶段,明火出现时往往已经错过了最佳处置时机。但烟雾恰好是最不适合目标检测的目标:半透明、缺少硬边缘,颜色与云、雾、山间水汽重叠,晴天高光还会让烟柱在几百毫秒内从清晰变成不可见。这也是为什么这类毕设、课设项目普遍选择 YOLOv8——它生态完善、权重文件不大、训练到部署的链路短,配合 PyQt5 或 Web 界面就能在几天内包出一套能演示的预警系统。对要交付课程设计或毕业设计的人,难点从来不只在训练精度,而在于把数据、训练、界面和误报控制串成一条可复现的路径。下面按这条路径展开,先立住数据基础,再讲训练验证,最后落到部署和压误报。

2. 训练自己的烟雾数据集:标注格式、目录划分与 data.yaml 配置

监控视频里的早期烟雾目标通常很小,标注质量直接决定最终效果。训练开始前,先要确认数据以 YOLOv8 要求的格式存在:数据集根目录下并列 images 和 labels 两个目录,train 与 val 里图像和标签一一对应,每个 txt 与同名图片对应,每行是class x_center y_center width height,四个坐标都相对图片宽高做 0 到 1 归一化。很多第一次跑的人在这里出错:从标注软件导出的是 VOC 的 xml 或 COCO 的 json,没有转成 txt;或者是标注时把整棵树冠框进去,导致模型学习到的不是烟雾轮廓而是大片背景。早期烟雾检测的标注原则是框要贴住烟团主体,宁可多标几个小框,也不要扩展到大片天空。

2.1 数据来源与负样本比例:不能只收集“好看的烟”

公开的 smoke detection、森林火灾检测类数据集里,晴天蓝天背景的白烟占绝大多数。用这类数据训出来的模型,遇到阴天晨雾、山体阴影、逆光云缝就会疯狂误报。常见做法是:拿一到两个场景较丰富的公开烟雾集做底,再从自然监控录像里截取几百张完全没有烟的林区画面作为负样本。负样本不需要标注,仅仅放在 images 的 train 和 val 目录里,模型会自动把它们归为背景。正负样本比例我一般控制在 3:1 到 5:1,负样本太少,部署阶段摄像头稍微转个角度,模型就会把云和山影识别成烟。

把样本收集齐之后,需要检查是否有损坏图片、空标签、标签文件缺失。空标签在早期烟雾场景里属于正常(负样本没有标签),但如果正样本里有空 txt,说明标注遗漏,这种样本在训练时会直接被当作背景,模型会因此认为“这张图没有烟”,对降低召回率影响很明显,需要补标或直接删掉。

2.2 目录划分与标签格式检查脚本

我一般会先用脚本把数据集按 90% / 10% 拆成 train 和 val,再扫一遍标签,把坐标越界、类别 ID 超范围、字段数量不对的文件单独列出来。下面的脚本可以直接改路径运行,使用 shutil 复制而不是移动文件,保证原始标注数据不会被破坏。

import os import random import shutil from pathlib import Path SRC_IMG = Path("datasets/images_all") SRC_LAB = Path("datasets/labels_all") DST = Path("datasets") VAL_RATIO = 0.1 random.seed(42) for split in ("train", "val"): # 创建 images 与 labels 的子目录 (DST / "images" / split).mkdir(parents=True, exist_ok=True) (DST / "labels" / split).mkdir(parents=True, exist_ok=True) imgs = sorted(SRC_IMG.glob("*.jpg")) + sorted(SRC_IMG.glob("*.png")) random.shuffle(imgs) val_n = max(1, int(len(imgs) * VAL_RATIO)) for i, img_path in enumerate(imgs): split = "val" if i < val_n else "train" label_path = SRC_LAB / (img_path.stem + ".txt") if not label_path.exists(): # 缺标签时打印警告,便于定位漏标样本 print(f"[WARN] label not found: {label_path}") continue shutil.copy(img_path, DST / "images" / split / img_path.name) shutil.copy(label_path, DST / "labels" / split / label_path.name)

这段脚本里VAL_RATIO = 0.1表示验证集占 10%,数据集在一两千张级别时这个比例够用,样本再多可以用 8:2。随机种子固定为 42 是为了让每次划分完全一致,训练结果可以横向对比;如果打印出来的 WARN 太多,说明源头数据缺标注,不要直接训练,回到标注阶段补齐。

划分之后还要做一次标签内容检查,重点看坐标是否越界、类别 ID 是否从 0 开始连续排列。单类别烟雾项目里,类别 ID 只有 0;如果项目同时检测 smoke 和 fire,则 0 是 smoke,1 是 fire,类别映射需要与 data.yaml 保持一致。

def check_label(file, class_num=1): errors = [] with open(file, encoding="utf-8") as f: for line in f: parts = line.split() if len(parts) != 5: errors.append(("field", line)) continue cls, xc, yc, w, h = parts xc, yc, w, h = map(float, (xc, yc, w, h)) if int(cls) >= class_num: errors.append(("class", line)) if not (0 < xc < 1 and 0 < yc < 1 and 0 < w <= 1 and 0 < h <= 1): errors.append(("bbox", line)) return errors

class_num是类别总数,单类烟雾写 1,遇到报错行可以直接输出文件名和内容定位。坐标检查这一步很容易被跳过,尤其是用半自动标注工具时,偶尔会出现框超出图片边界,归一化后 w 或 h 大于 1 的数据,YOLOv8 的训练过程不会直接报错,但 mAP 会被异常框拉低,且问题非常隐蔽。

2.3 data.yaml 与训前必改的 4 个参数

数据格式就绪后,写 data.yaml 是训练前最后一步。它的作用是把数据根目录、子目录、类别名告诉训练入口。

path: D:/datasets/smoke_data # 数据集根目录,用绝对路径 train: images/train # 相对 path 的训练图像目录 val: images/val # 相对 path 的验证图像目录 names: 0: smoke # 类别 ID 从 0 开始 # 1: fire # 如果有明火类别,取消注释并保证标注一致

这里最容易踩坑的 4 个点:第一,path必须是绝对路径,且不要包含中文和空格,Windows 下中文路径在 PyTorch DataLoader 中偶尔会产生编码报错,排查起来很花时间;第二,trainval是相对path的子路径,不要写绝对路径,也不要写./images/train这种带当前目录标识的写法;第三,names的顺序必须和标签文件里的类别 ID 完全对应,容量不大时经常出现标注软件导出 ID 从 1 开始的情况;第四,如果 Windows 下训练时报共享内存或 worker 相关错误,在训练参数里加workers=2,并在第一次运行时观察 cache 是否正常生成。

提示:训练前先跑一条yolo detect train data=smoke.yaml model=yolov8s.pt epochs=1作为 smoke test。它会启动训练流程但立刻结束,如果 10 秒内没有报错,说明数据目录、YAML 格式、依赖库都是通的,再跑正式训练。

3. 训练与验证:C2f 结构特点、训练命令与损失曲线判断

数据准备好之后,训练阶段的核心不是把命令跑起来,而是知道每个参数在改什么,以及训练结束后如何从曲线里判断模型状态。这一章从 YOLOv8 backbone 的 C2f 结构讲起,给出适配普通显卡的训练参数组合,最后落到 loss 曲线读取。

3.1 YOLOv8 的 C2f 结构对烟雾目标提取的影响

YOLOv8 在 backbone 中用 C2f 替换了之前的 C3 模块。C3 是 split 后走一条 Bottleneck 支路再 concat,C2f 则让输入经过多个 Bottleneck 后,把不同层级的输出拼到一起,再做跨阶段融合。这意味着梯度可以沿多条支路回传,浅层纹理信息能更完整地保留到后续层。烟雾这种目标纹理弱、边缘模糊,靠的是“灰白渐变区域”的整体上下文,C2f 对这种低频特征的保留能力比 C3 有明显改善。

但如果漏检集中在极远处的细烟柱,不要指望改 C2f 结构就能解决。小目标问题在 YOLOv8 上更多受限于输入分辨率和特征金字塔的 stride。常见做法是把 imgsz 从 640 提到 896,或者给检测头加一个 stride=4 的 P2 输出层;再激进一点可以引入注意力模块,比如在 backbone 输出后接一个轻量的通道注意力层。做这些改进前先跑出基线,用同一份验证集对比,否则无法判断改动是否有效。

3.2 用命令行跑通训练:GTX 1660 Ti 也能用的参数组合

训练烟雾检测推荐从 yolov8s 开始,不要直接上 yolov8x。烟雾目标相对单一,类数少,yolov8s 的容量足够拟合几千张数据,显存占用也友好。命令行示例如下:

yolo detect train \ model=yolov8s.pt \ data=smoke.yaml \ imgsz=640 \ epochs=100 \ batch=8 \ device=0 \ patience=20 \ workers=2 \ amp=True \ project=runs/smoke \ name=train
参数作用GTX 1660 Ti 6G 推荐值
model预训练权重,启动速度快yolov8s.pt
imgsz训练输入分辨率,越高小目标越好640,显存够可试 896
epochs最大训练轮数100,配合早停足够
batch每轮迭代的图片数8,显存不足改 4
patience验证损失连续多少轮不降就停20
workers数据加载线程数2,Windows 下太高易卡死
amp混合精度训练True
deviceGPU 编号,CPU 用 -10

这段参数组合的核心逻辑是:用预训练权重做迁移学习,epochs 不需要太大,靠 patience 早停自动收敛;batch 优先保证能放进显存,gtx1660ti 6G 上 batch=8 加 imgsz=640 是稳定组合,out of memory 就降到 4,不要为了凑 batch 去关 amp。训练完成后,best.pt 和 last.pt 会存放在runs/smoke/train/weights/下,best.pt 是验证集指标最好的权重,部署时只用它。

训练中途如果想看当前效果,可以另开一个终端对单张图片做推理:

yolo detect predict \ model=runs/smoke/train/weights/best.pt \ source=test_images/smoke_001.jpg \ conf=0.25

这个小命令不需要重新加载训练环境,模型还在跑也能执行,适合在训练过程中抽查漏检情况。

3.3 从 results.csv 画损失函数曲线图判断过拟合

Ultralytics 在训练目录下会自动生成 results.csv,每一行是一个 epoch 的各项指标。直接用 pandas 和 matplotlib 读取即可,不需要自己写训练日志。

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/smoke/train/results.csv") df.columns = [col.strip() for col in df.columns] # 对 train loss 做 5 帧滑动平均,曲线更平滑 for col in ["train/box_loss", "train/cls_loss"]: df[col + "_smooth"] = df[col].rolling(5).mean() plt.figure(figsize=(10, 6)) plt.plot(df["epoch"], df["train/box_loss_smooth"], label="train/box_loss") plt.plot(df["epoch"], df["val/box_loss"], label="val/box_loss") plt.plot(df["epoch"], df["train/cls_loss_smooth"], label="train/cls_loss") plt.plot(df["epoch"], df["val/cls_loss"], label="val/cls_loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend() plt.grid(True) plt.show()

注意 train loss 是最后一个 batch 的瞬时值,抖动很大,做滑动平均再看趋势。判断要点:train loss 持续下降而 val loss 在第 20 轮左右开始回升,属于典型过拟合,可以把 epochs 降到 60 或把 patience 从 20 调到 10;cls_loss 降不下去而 box_loss 正常,优先怀疑负样本不足,补充云、雾、树影图片后重新训练;val loss 和 train loss 同步下降但 mAP50 不涨,问题出在验证集标注质量上,回去检查标签。

训练目录下的 confusion_matrix.png 也要看,重点看背景类被预测为 smoke 的比例。如果这个值高于 5%,说明误报风险高,部署时不能直接用默认 conf=0.25。

4. 可视化界面与部署:用 PyQt5 封装 best.pt 跑通图片、视频、摄像头

训练完成后进入交付阶段。毕设场景里的“可视化界面”有两个常见路线:Gradio Web 界面和 PyQt5 桌面界面。Gradio 启动快、三行代码就能用浏览器访问,但做监控预警的观感不如桌面程序,且摄像头输入要走额外桥接。PyQt5 的优势是画面渲染、检测画框、状态报警都在一个进程里,逻辑直观,部署依赖也简单。下面按 PyQt5 路线给出工程结构。

4.1 项目结构与推理引擎封装

一个最小可用的部署项目包含四个文件:main.py 负责启动界面,engine.py 封装模型推理,weights/best.pt 放训练产物,requirements.txt 固定依赖。推理引擎的核心是把单帧图像传进模型,返回边界框和置信度,不掺任何界面逻辑。

from ultralytics import YOLO import numpy as np class SmokeEngine: def __init__(self, weights="weights/best.pt", conf=0.25, iou=0.6, imgsz=640): self.model = YOLO(weights) self.conf = conf self.iou = iou self.imgsz = imgsz def predict(self, frame): # frame 是 OpenCV 读出来的 BGR ndarray results = self.model.predict( frame, imgsz=self.imgsz, conf=self.conf, iou=self.iou, verbose=False)[0] boxes = results.boxes.xyxy.cpu().numpy() scores = results.boxes.conf.cpu().numpy() return boxes, scores

这里 conf 和 iou 做成实例属性,是为了让界面上的阈值滑块可以直接改engine.conf,下一帧立即生效。iou=0.6是 NMS 阈值,烟雾检测场景目标重叠少,一般不需要动。conf 在训练时常用 0.25,部署阶段建议提到 0.4 以上,云层干扰会明显减少。

4.2 PyQt5 主窗口:画框、置信度与报警状态

界面部分的核心是 QTimer 周期性读取画面,送入推理引擎,再把检测结果画回图像。下面的代码是主窗口里的核心槽函数,按钮和布局逻辑按常规 PyQt5 写法补充即可。

import cv2 from PyQt5.QtCore import QTimer from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QMainWindow from engine import SmokeEngine class SmokeUI(QMainWindow): def __init__(self): super().__init__() self.engine = SmokeEngine(conf=0.40, iou=0.6) self.cap = None self.timer = QTimer(self) self.timer.timeout.connect(self.process_frame) def start_camera(self, index=0): self.cap = cv2.VideoCapture(index) self.timer.start(33) # 约 30 FPS 的取帧间隔 def process_frame(self): ok, frame = self.cap.read() if not ok: return boxes, scores = self.engine.predict(frame) alert = False for box, score in zip(boxes, scores): if score >= self.engine.conf: alert = True x1, y1, x2, y2 = box.astype(int) color = (0, 0, 255) if alert else (0, 165, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, f"smoke {score:.2f}", (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) self.set_alert_light(alert) self.show_frame(frame)

QTimer 的 33ms 只是取帧间隔,实际帧率取决于推理耗时。1660 Ti 上跑 yolov8s 大约 20 到 40 FPS,界面刷新会自动跟随推理速度,不需要额外做线程同步。要是出现界面卡顿,优先把cv2.putText和画框逻辑移到推理前的原图上,或者降低imgsz到 480 试跑。报警状态这里只做了单帧判断,下一章会加入帧间投票来压低误报。

4.3 部署清单与一键启动

requirements.txt 建议写成:

ultralytics>=8.0.0 PyQt5>=5.15 opencv-python>=4.8

启动流程是三条命令。conda 环境用 Python 3.10,兼容性和第三方库支持都比 3.12 稳妥。

conda create -n smoke python=3.10 -y conda activate smoke pip install -r requirements.txt python main.py

没有 N 卡的机器也能跑,CPU 推理单帧会到 500ms 以上,演示图片和视频足够,但摄像头实时预览会不流畅。要把模型部署到 RK3588 这类边缘设备,需要先转 ONNX 再用板端推理框架,这就不是毕设演示范畴的事了。

5. 误报抑制与验收:帧间投票、ROI 蒙版与置信度阈值扫描

训练集以晴天白烟为主时,部署后最常见的假报警来自云缝光斑、晨雾和山体阴影。这三类干扰在单帧检测里几乎无法和烟雾区分,可靠的做法是加时间约束和空间约束。

5.1 帧间投票 + ROI 蒙版:连续命中才报警

单帧置信度高的误检通常是随机出现的,而真实烟雾会在画面中连续存在几十秒到几分钟。用一个固定长度的队列记录最近若干帧是否检测到烟,只有投票数达到阈值才触发报警,同时用 ROI 蒙版把检测范围限制在山脊上空的重点区域。

from collections import deque class TemporalAlert: def __init__(self, window=5, vote=3, min_score=0.45, roi=None): self.window = window self.vote = vote self.min_score = min_score self.roi = roi # 二值蒙版,检测框中心落在蒙版内才生效 self.history = deque(maxlen=window) def update(self, detections): # detections: [(score, (cx, cy)), ...] hit = False for score, (cx, cy) in detections: if score < self.min_score: continue if self.roi is not None and self.roi[cy, cx] == 0: continue hit = True break self.history.append(1 if hit else 0) return sum(self.history) >= self.vote

window=5、vote=3 表示最近 5 帧里至少有 3 帧检测到烟雾才触发报警,单个帧的随机误报最多贡献 1 票,无法触发。真实烟雾持续存在,最多延迟 2 帧报警,对人眼几乎无感。释放报警也有延迟,烟雾消散后需要再连续 2 帧无检测才会释放,这个滞后特性对监控场景是合理的。ROI 蒙版用np.zeros建画布,再用cv2.fillPoly画一个覆盖山顶区域的多边形,区域外的云影直接不参与投票,误报率能再降一截。要特别注意:如果不用 ROI,窗口投票会被不同位置先后出现的误检累积票数,此时应改为按检测框中心距离做目标关联,只对同一位置的连续命中累计。

5.2 在验证集上扫描置信度阈值

调好时间维度后,还需要确定 conf 值。训练默认的 0.25 是目标检测通用值,不等于这个数据集的部署值。做法是对验证集跑一个循环,conf 从 0.05 到 0.90 按 0.05 步进,统计每次的 precision、recall 和 F1,选 F1 最大时的 conf 作为部署参数。这个流程不需要重新训练,几分钟就能跑完。实际项目中我的默认组合是 conf=0.45、window=5、vote=3,再用 ROI 覆盖重点监控区域。最后用一段完全没有参与训练的雾天录像做回归测试,统计误报次数和烟雾漏报情况,一小时内误报事件不超过两次,报警能在烟雾可见后 5 秒内触发,这套预警系统才算真正验收通过。

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

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

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

立即咨询