☰
YOLOv5火灾检测实战:从环境配置到树莓派5部署避坑指南
2026/9/28 1:50:24 网站建设 项目流程

简介:面向目标检测与安防场景的YOLOv5火灾检测项目包,适合计算机视觉开发者、消防预警系统设计者以及希望快速上手YOLOv5的研习者。压缩包内共135个文件,大小68.51MB,包含已训练好的pt模型权重、Python推理/训练脚本、YAML配置文件、测试图片与视频、Notebook演示及Dockerfile等;其中py脚本承载模型调用与结果解析逻辑,yaml记录网络结构与超参数,pt为可直接加载的火焰/烟雾检测权重,jpg、png和mp4则用于效果验证,解压后即可组成一条完整的推理链路。模型针对火焰和烟雾两类目标进行识别,结合YOLOv5的Mosaic数据增强、PANet结构、GIoU损失等改进点,在实时性和准确性上表现均衡,可输出带置信度的检测框,有利于接入监控摄像头或嵌入告警系统。当前已有3265人学习下载;整套资源既能作为目标检测实战的入门范例,也可为火灾监测项目提供基础组件,省去从零训练的时间成本,适合在此基础上做二次开发。

1. 火灾检测为什么选 YOLOv5 而不是更大更新的模型

用 YOLOv5 做火灾检测,是工地监控和森林防火两个方向我反复验证过最省心的组合,所以这个标题的检索热度一直没降过。火灾检测要解决的是从摄像头画面里尽早发现火焰和烟雾并触发报警,这个场景对模型的要求不是精度天花板,而是能在普通工控机甚至边缘设备上稳定跑、能快速接入现有监控系统。YOLOv5 源码生态成熟,训练脚本、导出工具、部署样例都现成,后处理和超参数调整也有大量公开踩坑记录可以直接参考。YOLOv9、RT-DETR 确实更新,但火灾场景样本量通常只有几千张量级,小模型加成熟工程链往往比大模型加新框架更稳。这篇笔记给的是从 conda 环境配置到树莓派5 部署的一整条落地路径,适合手里有摄像头、准备自己训练火灾检测模型的工程师。

2. 环境配置与数据集准备:conda 环境、源码拉取与标注转换

2.1 conda 创建 YOLOv5 环境的固定命令

yolov5 环境配置是翻车率最高的第一站。大多数问题出在 torch 和 CUDA 版本对不上,或者 pip 把包装进了 base 环境,跑训练时 import torch 直接报错。我习惯为每个项目单独建 conda 环境,Python 版本固定在 3.9 或 3.10,YOLOv5 官方源码在这两个版本上运行最顺畅。想先快速过一遍 yolov5 基础笔记再动手的,拉完源码看官方 README 就够了。

conda create -n yolov5-fire python=3.10 -y conda activate yolov5-fire git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt

requirements.txt 里有几个包需要单独说明。torch 和 torchvision 默认装的是 CPU 版还是 CUDA 版,取决于你机器上是否装好了对应驱动,这一条恰恰是新手最容易忽略的。如果训练机是 NVIDIA 显卡且驱动已装好,用pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118单独重装一次,再装 requirements.txt 里其余包,能少踩一半坑。如果只是做 CPU 训练或者后面要部署到树莓派5,默认的 CPU 版 torch 就够用,不用额外折腾。

注意:源码仓库会持续更新,不同 commit 对 Python 和 torch 版本要求有差异。跑训练前先执行python -c "import torch; print(torch.__version__)"确认版本号,再决定是否回头锁定 requirements.txt 里的版本。

装完环境跑一次官方检测做验证:

python detect.py --weights yolov5s.pt --source data/images/bus.jpg

能正常输出检测框,说明环境是通的。很多人卡在这一步,原因是模型的 pt 文件下载超时,多试几次即可,或者跳过这一句直接进入训练阶段,训练时如果设置了--weights yolov5s.pt,程序会按需下载。

2.2 火灾数据从哪来:公开数据集与自采标注

做火灾检测最忌讳一上来就自己标几千张图。公开的火焰烟雾数据集其实不少,常见的有火焰分割类、烟雾检测类,我个人会把它们统一成 fire 和 smoke 两类后合并使用。公开数据的问题在于场景单一,很多是实验室火焰或网上视频截帧,拿到工地现场容易出现“训练集里的火都长一个样”的过拟合。

所以我的组合拳是:先下公开数据集做预训练,再用自己现场摄像头抽帧补一批真实场景图。抽帧用 ffmpeg 从监控视频里按 1 秒 1 帧或 2 秒 1 帧抽取,然后人工筛选出真正包含火焰、烟雾或相似干扰物的帧。筛选后的图用 labelimg 标注,它可以直接导出 YOLO 格式的 txt,比手工改 xml 省事得多。

标注时类别设定有一个关键决策。很多首次做火灾检测的工程师会把类别拆成 fire_flame、fire_smoke、fire_ember,结果每个类样本都少,训练出来互相混淆。我在实际项目里只用两类:fire 和 smoke。火焰与烟雾分两类是因为它们的纹理特征差异极大,但不会把一个火焰场景里的火苗拆成多个细分类别,这样每类的样本量才够模型学习。负样本同样重要,红灯、橙红色车、落日、晚霞这类“颜色像火但不是火”的图,至少放几十张,放在 background 类别里。这个习惯后来帮我省掉了很多误报排查时间。

2.3 把 VOC 标注转成 YOLO txt:转换脚本与四个边界坑

如果拿到的公开数据集是 VOC 格式的 xml 标注,或者自己用 labelimg 选了 VOC 格式,需要转成 YOLO 需要的归一化 txt。转换脚本不复杂,但边界处理最容易出问题。

import os import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_dir, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): cls = obj.find("name").text if cls not in class_map: continue box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) # 边界裁剪:标注可能越出图片边缘 x1 = max(0.0, min(x1, img_w - 1)) y1 = max(0.0, min(y1, img_h - 1)) x2 = max(0.0, min(x2, img_w - 1)) y2 = max(0.0, min(y2, img_h - 1)) # 归一化:中心坐标和宽高都除以图片尺寸 cx = ((x1 + x2) / 2) / img_w cy = ((y1 + y2) / 2) / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h lines.append(f"{class_map[cls]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_name = Path(xml_path).stem + ".txt" with open(Path(out_dir) / out_name, "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": class_map = {"fire": 0, "smoke": 1} xml_dir = "voc_annotations" out_dir = "yolo_labels" os.makedirs(out_dir, exist_ok=True) for xml_file in Path(xml_dir).glob("*.xml"): voc_to_yolo(str(xml_file), out_dir, class_map)

逻辑上要注意四个边界坑。第一,除宽高时必须用浮点除法,Python 3 里单斜杠已经是浮点除法,但如果你从旧项目复制代码且头上丢了from __future__ import division,整数除法会把小于 1 的归一化坐标直接变成 0。第二,边界裁剪是因为有些标注框在标注工具里拖出了画布,不裁剪会造成后续 loss 计算出现越界框。第三,类别编号必须从 0 开始且与 data yaml 里的顺序一一对应,很多人 fire 写了 1、smoke 写了 0,训练时损失直接乱掉。第四,空标注文件要保留,一张图没有目标时对应的 txt 应该是 0 字节,训练时 YOLOv5 会跳过它,但不要因为“没目标”就不生成文件,否则数据加载逻辑会报错。前两个是代码问题,后两个是数据管理问题,写脚本时一次处理好,后面训练会省心很多。

3. 训练自己的火灾检测模型:超参数与 Loss 调优

3.1 预训练权重怎么选:yolov5s.pt 与火灾场景的匹配

YOLOv5 官方提供 s/m/l/x 四个规模,对应不同的深度和宽度系数。火灾检测因为部署目标通常是工控机或边缘设备,我的选择逻辑很直接:先用 yolov5s.pt 做预训练。原因不是 s 精度够高,而是 s 的参数量约 7M,训练迭代快,能在几小时内验证数据标注和超参数是否合理。数据集小、目标场景集中时,从 s 起步比直接上 l/x 更稳妥,后者参数量大但样本量不足时容易过拟合,训练时间也翻倍。

什么时候换更大的模型?如果训练数据到了两三千张以上,且你明确知道部署机的算力能接受 m 的推理时间,可以试 yolov5m.pt。m 在火焰大目标上提升并不明显,但对小目标的召回率会好一些,这个差异在后面的避坑章节细讲。值得注意的是预训练权重的作用是提供底层纹理特征,火灾的火焰纹理和 ImageNet 里的物体差别很大,但边缘、颜色渐变、纹理基元还是通用的。所以即便火灾和预训练类别差异再大,也不建议从随机权重开始训练,收敛速度和最终精度都会差一个量级。

3.2 train.py 的最小可跑命令与每项参数含义

训练自己数据集时,train.py 参数里最需要仔细设的是 data yaml、weights、img、batch 和 epochs。先放一个我常用的命令:

python train.py \ --data fire.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache ram \ --hyp hyp.scratch-low.yaml \ --project runs/fire_train \ --name exp_v1

fire.yaml 内容对应 2.3 节的类别约定:

train: ./datasets/fire/images/train val: ./datasets/fire/images/val nc: 2 names: ["fire", "smoke"]

逐个说参数。img 是训练输入分辨率,640 是性价比平衡点,火焰小目标多的时候我会提到 800 或 960,代价是显存和训练时间增加约 50%,这个后面细说。batch 表示单卡每轮的样本量,显存不够就减小,但不要小于 8,否则 BatchNorm 统计不稳定。epochs 对火灾这类小数据集 100 起步,如果 val loss 还在下降就继续训练到 300。cache ram 是把图像缓存进内存,能减少磁盘 I/O,数据集几百张时提升明显,几千张以上时注意内存容量。hyp 指定超参数文件,hyp.scratch-low.yaml 适合小数据集,hyp.scratch-high.yaml 的增强更强,适合大数据集但容易在小数据集上过拟合。

提示:把--project和--name固定下来,每次实验换 name,不要用默认的 exp 目录反复覆盖,否则后面想对比超参数效果时没有后悔药。

训练过程中最该盯的控制台输出是 val BoxLoss 和 val cls loss。火灾目标边缘不规则,box loss 下降慢是正常的,但如果 cls loss 在训练中期出现震荡,优先怀疑类别不平衡,接着去调整 3.3 的内容。

3.3 火灾样本不平衡:少火焰多烟雾时的超参数打法

火灾数据集几乎必然出现类别不平衡。最常见的是 fire 样本几百张、smoke 样本只有几十张,因为火焰场景好找,烟雾尤其是刚起火时的淡烟很难收集。这种情况下模型会把 smoke 类学成 fire 的附属品,推理时烟检测不出来。

有三个有效的调整方向。第一是调整 cls loss 的权重,在 train.py 命令里加--cls 1.5或更高。YOLOv5 的 cls loss 默认权重在 hyp 里是 0.5,把 fire 类权重调高会让模型更重视火焰,把 smoke 类权重调高则相反。但这个只能缓解,不能根治。

第二是关掉或减弱会让小目标更碎的增强。hyp.scratch-low.yaml 里的 mosaic 在火灾场景里要特别小心,mosaic 会把四张图拼成一张,火焰对象经常被裁掉一半。火焰这种“边缘模糊”的目标被切碎后,模型学到的是残缺火焰纹理,掉点明显。我的做法是把 mosaic 从默认值调到 0.3,同时保留 mixup 0.1。hsv_h 和 hsv_s 参数也不必按默认来,火灾数据集里火焰的颜色本身已经够丰富,过度的色相增强会让红色车漆和火焰更难区分,我习惯把 hsv_h 从 0.015 调低到 0.005,hsv_s 保留默认。

第三类是数据层面补样本,把烟雾视频里多抽几帧,哪怕标得粗糙也比缺类强。实话说,超参数调整的边际收益有上限,如果不平衡比例超过 10:1,补数据才是根本解法。训练后跑一次验证看每个类别的 mAP,哪个类明显低就重点补哪类:

python val.py --data fire.yaml --weights runs/fire_train/exp_v1/weights/best.pt

4. 部署到树莓派5:推理速度、模型导出与后处理取舍

4.1 导出 FP16/INT8 引擎的两种路径

树莓派5 上部署自己训练的 yolov5 模型,首先要认清一个边界:它是 ARM 处理器,没有 CUDA 和 TensorRT。常见的落地路径有两条:一条是导出 ONNX 后用 onnxruntime 推理,另一条是转成 NCNN 格式跑 ncnn 的 C++ 接口。两条我都走过,建议是只做原型验证就选 ONNX,要做长期运行的监控服务选 NCNN。原因很简单,NCNN 在树莓派5 上对 ARM CPU 的算子优化比 onnxruntime 更彻底,INT8 量化也更成熟。

导出 ONNX 用官方 export.py:

python export.py \ --weights runs/fire_train/exp_v1/weights/best.pt \ --include onnx \ --img 640

这里有一个关键取舍:不要加--half。FP16 半精度对 NVIDIA GPU 有效,因为 GPU 有专用 FP16 计算单元;树莓派5 的 CPU 虽然支持 FP16 指令,但 onnxruntime 在 ARM 上对 FP16 算子的支持并不完整,实测反而比 FP32 慢。NCNN 路径的 INT8 量化则值得做,火灾检测对精度损失有一定容忍度,INT8 能把推理时间再砍一半。INT8 量化需要准备校准图片,用训练集的验证集图片就行,校准图片一百张上下就能达到不错效果。

4.2 把检测结果接进视频流:NMS 后的置信度过滤

导出后的 ONNX 模型默认不带 NMS,需要在业务代码里自己做 yolov5 后处理。树莓派5 上性能有限,后处理的取舍直接影响帧率。

import cv2 import numpy as np import onnxruntime as ort class FireDetector: def __init__(self, onnx_path, conf_thres=0.4, iou_thres=0.5): self.session = ort.InferenceSession(onnx_path) self.conf_thres = conf_thres self.iou_thres = iou_thres self.strides = [8, 16, 32] self.nc = 2 # fire, smoke def letterbox(self, img, new_size=640): h, w = img.shape[:2] scale = min(new_size / h, new_size / w) new_h, new_w = int(h * scale), int(w * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((new_size, new_size, 3), 114, dtype=np.uint8) canvas[:new_h, :new_w] = resized return canvas, scale, (new_w, new_h)

这个类里,letterbox 必须和原版 YOLOv5 保持一致的填充方式,否则推理结果坐标会错位。推理时先 letterbox 到 640,送入 session 得到形状如[1, 25200, 7]的输出,其中 25200 = 8080 + 4040 + 20*20 个 anchor 位置,7 = 4 个框坐标 + 2 个类别置信度 + 1 个 objectness。因为是火灾场景,后处理里我通常直接把 conf_thres 拉到 0.4,比默认 0.25 高,能滤掉大量边缘误报。IoU 阈值 0.5 保持不变。如果树莓派5 上 CPU 占用率已经很高,可以把输入分辨率从 640 降到 480,火焰这类大目标受影响有限,速度提升却能接近一半,这个取舍在算力紧张时非常管用。

4.3 树莓派5 上的实测帧率与线程模型安排

树莓派5 上用 yolov5s,640 输入,FP32 的 ONNX,单帧推理时间大致在 150 到 250 毫秒之间,换算过来是 4 到 6 FPS。对于火灾监控这个场景,这个帧率是够用的。火灾发展是以秒甚至分钟为单位的,5 FPS 意味着 200 毫秒级别的检测延迟,完全满足报警需求。真正影响系统可靠性的是线程模型,而不是那百分之几十的帧率差。

我一般会把采集、推理、报警拆成三个线程。采集线程用 OpenCV 的 VideoCapture 持续拉流,把最新帧放入一个双缓冲队列;推理线程从队列取帧,做 letterbox、推理、后处理,把结果写到共享状态;报警线程只管读结果状态,满足连续 N 帧判定条件就触发报警。这套模型里,采集和推理解耦后,即使推理偶尔出现一次 100 毫秒的抖动,也不会导致视频流积压和延迟滚雪球。双缓冲队列的实现要点是换帧时加一个锁,避免推理线程读到半写入的帧。这块不做好的话,树莓派5 上跑不了多久就会出现画面卡顿和报警滞后的血泪经验。

5. 火灾检测的避坑笔记:漏检、误报与显存不足

5.1 火焰小目标漏检:原因在 PAN 层下采样太狠

现象:测试视频里远处的火苗,人眼能隐约看到,模型就是不报;把视频暂停放大后才报出来。原因:YOLOv5 的 PAN 结构把输入逐步下采样到 32 倍,小目标的特征在深层特征图里几乎消失,20x20 这一层对几个像素的小火焰响应极弱。解决路径有三条,按投入产出比排序:把 img 从 640 提到 800 或 960,小目标在特征图上的像素数直接翻倍,这是最有效的一步;其次用 yolov5s 而不是 m/x,小模型对小目标的拟合往往比大模型更稳;最后是针对小目标特别多的场景,把输入切成四块分别推理再合并结果,也就是 SAHI 思路,但树莓派5 上用这个方案帧率会掉到 1 FPS 以下,只适合离线分析。

注意:调高 img 后显存占用按平方增长,batch 需要同步调小。640 到 960 意味着每张图特征图面积变成 2.25 倍,原来 batch 16 能跑的卡,现在 batch 8 都未必够。

5.2 红灯、落日、橙色车漆误报:数据增强补不回来

现象:训练集 mAP 不错,一到真实监控画面里,红灯、黄昏的太阳、橙色的车都被标成 fire。原因:火灾检测本质上是颜色和纹理的联合判断,而这类干扰物在颜色上与火焰高度重合,模型在纹理特征不足时就会退回用颜色分类。解决的第一步是收集 hard negative,专门从监控视频里截取包含红色灯牌、夕阳、红色车辆的帧,作为 background 类加进训练集。这一步比调任何超参数都有效。第二步是克制 hsv 色相增强,前面 3.3 里把 hsv_h 调低就是为这个服务的。第三步是部署端加时间维判断,单帧里颜色像火不足以报警,连续多个帧都检测到且位置不漂移才触发,第六节会展开。这里有个黑匣子现象:同样的模型,加了 hard negative 之后 mAP 可能不升反降,因为背景类变多会稀释正类置信度,但实际误报大幅减少,看指标时要认清这个反直觉现象。

5.3 显存不足与 batch 掉到 1:梯度累积怎么保训练效果

现象:显存 8G 的卡设了 batch 16,训练开始就 OOM,被迫改成 batch 2,结果训练出来的模型 val loss 一直下不去。原因:batch 太小,BatchNorm 统计的均值和方差噪声太大,训练不稳定。解决:不要改 batch 数值,改用梯度累积。YOLOv5 的 train.py 会自动根据 batch 和显存计算梯度累积步数,如果你改小了 batch,它会自动把累积步数调大。但手动设置更可控,做法是保持--batch 16的命令不变,在代码里把单卡实际加载的 batch 调成 4,等效于每 16 张图更新一次权重。显存不够时宁可用 4 张图累积 4 步,也不要用 2 张图跑全程,这两种方式显存占用接近,但训练稳定性差很多。

5.4 标注框抖动导致 mAP 虚高:标注一致性的量纲

现象:验证集 mAP 0.82,兴致勃勃部署上去,实际视频里频繁漏检,人工查看每个检测框都觉得框得不准。原因:标注不一致。有人把火焰整个轮廓框住,有人只框中心亮部,同一个目标在不同图里的标注框差异巨大,模型学习到的边界就是混乱的。mAP 是按 IoU 阈值计算的,标注抖动会让评估结果虚高,因为只要预测框覆盖了标注中心就算对。解决:标注前定一份统一的标注口径。我现在的做法是火焰框整体外轮廓、烟雾框半透明可见部分的下沿,全项目统一。标注完抽样检查框的重心分布,如果同一物体多人标注的框中心偏差超过框宽度的 20%,就返工。这个检查放在训练前,能省掉后面无数调参时间。

6. 用帧差 + 多帧投票压低误报:一个可落地的后处理技巧

6.1 为什么火灾检测需要时间维信息

单帧静态检测在火灾场景里有一个天然缺陷:火焰是动态目标,而误报源(红灯、落日)是静态的。只凭单帧判断,模型很难区分“正在燃烧的火”和“颜色像火的物体”。引入时间维后,这个区分变得非常简单,火焰的闪烁和位移会导致检测框跳动,而静态干扰物的检测框位置稳定。多帧投票就是利用这个差异来压低误报。

6.2 五帧投票的两种实现:滑窗与指数平滑

from collections import deque class FireVoter: def __init__(self, window=5, min_votes=3): self.window = window self.min_votes = min_votes self.history = deque(maxlen=window) def update(self, detections): # detections: 每帧 [class_id, conf, x1, y1, x2, y2] 列表 fire_frames = [d for d in detections if d[0] == 0] conf = max([d[1] for d in fire_frames], default=0.0) fire = conf >= 0.4 self.history.append(fire) if len(self.history) < self.window: return False, 0.0 votes = sum(self.history) return votes >= self.min_votes, conf

这个滑窗实现里,窗口 5 帧、最少 3 帧判定为火灾报警,既能滤掉单帧偶发误报,又把报警延迟控制在两帧以内。滑窗的缺点是每帧都要存状态,指数平滑方案更轻量:score = 0.6 * current_conf + 0.4 * prev_score,超过阈值即报警。前者的好处是逻辑直观、参数好解释,后者响应更快。我推荐工程上用滑窗版本,因为它对“连续几帧”的语义可以直接用窗口参数说明,给现场验收人员解释起来容易。实际部署时我会按场景调 min_votes:室外场景火焰有风会闪烁,min_votes 设 2;室内场景设 3。

6.3 验证方法:在测试视频上统计误报/漏报比

后处理改完,要用数据说话。我习惯准备三段视频:一段正常焚烧场景、一段包含红色干扰物的场景、一段火焰从出现到蔓延的实拍。统计两种指标:单帧检测的误报帧数与投票后的误报次数,以及投票引入的报警延迟帧数。我过去的一个项目里,单帧误报率大概每百帧出现 5 次误报,加载 5 帧 3 票后,误报降低到 0,代价是报警比单帧模式晚了 3 帧约 0.6 秒,这个延迟对火灾场景完全可接受。需要强调的是,投票会略微降低对快速小火苗的敏感度,如果你监控的是实验室燃烧场景,建议把窗口缩到 3 帧。调参时把这两个指标记录下来,比凭感觉调阈值可靠得多。

这套后处理算是我在火灾检测部署里最常被同事问到的技巧。它不改变模型本身,只动后处理逻辑,却往往比重新训练一轮收益更大。我现在的习惯是任何火灾检测项目都先把单帧模型跑通,再无条件加上时间维过滤,然后再回头评估数据和超参数的调整方向,顺序反了容易在调参里空耗时间。希望帮到你。

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

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

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

立即咨询