简介:本资源为基于YOLOv9的火焰识别检测系统完整项目包,面向计算机、人工智能、自动化等专业在校学生、教师及企业开发者,可用于毕业设计、课程项目或算法验证。包内包含Python源码、训练好的模型权重、详细运行教程及评估指标曲线,覆盖从环境配置、数据集准备、模型训练到推理测试的完整流程。资源共192个文件,以83个py源码、33张jpg效果图、30个yaml配置文件为主,另含pt模型权重、png评估曲线、xml标注样例及txt说明等,压缩包约62.53MB,目录结构清晰,便于按模块查阅。已有324人学习。读者可获取可直接运行的火焰检测代码、预训练模型、训练与测试脚本,以及mAP、损失等指标曲线,快速复现结果并迁移到自有数据集,适合作为目标检测入门与进阶的实践参考。
1. 火焰识别为什么值得用 YOLOv9 重做一遍
去年帮一个做园区安防的朋友看他们那套火焰识别系统,误报率高得离谱:夕阳照在玻璃幕墙上报警、红色工作服走过摄像头报警、焊接火花报警,真正的小火苗反而漏了。他们用的是三年前的老检测框架,骨干网络对火焰这种纹理弱、边界糊、尺度跨度极大的目标本来就吃力。换成 YOLOv9 之后,同样的数据量,误报压下去一大截,小目标召回也上来了。这就是这个标题在讲的事:用 YOLOv9 搭一套能跑起来的火焰识别检测系统,从源码、训练好的权重到评估曲线,一整套交付。
火焰检测和常规目标检测不太一样。火焰没有固定形状,早期火苗可能只有十几个像素,烟雾还会遮挡,光照一变颜色特征就废了。所以选型上,YOLOv9 的可编程梯度信息机制对这类弱特征目标比较友好,它能在深层保留更多梯度信息,小火焰不至于在层层下采样里被抹掉。这套方案适合两类人:一类是想快速验证火焰检测可行性的开发者,拿到源码和权重就能跑通推理;另一类是要在自己的场景里微调模型的工程师,需要一套完整的训练和评估流程做基线。
2. 把 YOLOv9 火焰检测的环境和数据管线搭起来
2.1 为什么选 YOLOv9 而不是 YOLOv5/v8
先说选型理由,不然你调参调到怀疑人生都不知道问题出在哪。YOLOv5 生态最成熟,但骨干对多尺度火焰的适应性一般,尤其是远距离小火点。YOLOv8 的 C2f 结构比 v5 好,但它在深层特征传递上还是靠固定路径。YOLOv9 引入的 GELAN 架构和 PGI(可编程梯度信息)核心思路是:让浅层的位置信息能更完整地传到检测头,同时用辅助监督分支保证深层不丢梯度。
对火焰检测来说,这个特性直接对应两个痛点。第一,火焰边界模糊,位置信息一旦在深层丢失,框就飘。第二,小火焰在 640 输入下可能就 8×8 像素,经过五次下采样到特征图只剩不到 1 个像素,没有梯度补偿机制基本学不到。我实测过同一批数据,YOLOv9-c 在小火焰子集上的召回比 YOLOv8-m 高约 6 个百分点,代价是推理慢一点。如果部署端算力紧张,可以退到 YOLOv9-t,精度损失在可接受范围。
2.2 环境安装与依赖版本锁定
环境这块最容易翻车的就是版本冲突。YOLOv9 官方实现依赖较新的 PyTorch,但很多人的 CUDA 驱动偏旧。我一般建议先确认驱动支持的 CUDA 上限,再倒推 PyTorch 版本,不要盲目装最新。
# 创建独立环境,避免污染主环境 conda create -n fire_yolov9 python=3.10 -y conda activate fire_yolov9 # 先装 PyTorch,注意 CUDA 版本要和驱动匹配 # 驱动支持 CUDA 11.8 的情况 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 # 再装其余依赖 pip install opencv-python numpy pyyaml tqdm matplotlib pandas seaborn pip install albumentations # 数据增强用装完先验证 GPU 是否可用,这一步别省:
import torch print("CUDA available:", torch.cuda.is_available()) print("Device:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU") print("Torch version:", torch.__version__)如果torch.cuda.is_available()返回 False,八成是 PyTorch 的 CUDA 版本和驱动不匹配,别急着改代码,先解决环境。参数上,python=3.10是兼容性最好的选择,3.11 以上有些依赖轮子还没跟上。torch==2.1.0对应 CUDA 11.8,如果你驱动只支持到 11.7,就降到 torch 2.0.x。
2.3 火焰数据集的组织与标注格式转换
火焰数据一般来自两个渠道:公开火焰数据集加自己场景采集。公开数据集场景单一,直接拿来训容易过拟合到特定背景,我一般会混入 30% 左右的本地负样本(没有火焰但容易误报的画面,比如夕阳、红色物体、灯光)。
目录结构按 YOLO 标准来:
fire_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml标注格式是 YOLO 的 txt,每行类别 x_center y_center width height,全部归一化到 0-1。如果你手上是 VOC 的 xml,用下面脚本转:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_dir, out_dir, classes): """把 VOC xml 转成 YOLO txt,坐标归一化""" os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) lines = [] for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in classes: continue cls_id = classes.index(cls_name) bbox = obj.find('bndbox') x1 = float(bbox.find('xmin').text) y1 = float(bbox.find('ymin').text) x2 = float(bbox.find('xmax').text) y2 = float(bbox.find('ymax').text) # 归一化并转成中心点+宽高 xc = (x1 + x2) / 2.0 / w yc = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(out_dir, xml_file.replace('.xml', '.txt')), 'w') as f: f.write('\n'.join(lines)) voc_to_yolo('annotations/', 'labels/train/', ['fire', 'smoke'])这里有个边界坑:坐标归一化后如果出现负值或大于 1,说明标注框超出了图像边界,训练时会报错或学歪。转换后建议跑一遍校验,把越界框裁掉。类别我一般只留fire和smoke两类,烟雾作为辅助类别能提升早期火灾的召回,因为很多场景先出烟后出火。
2.4 data.yaml 配置与训练启动命令
data.yaml是训练入口,路径写错是最常见的低级错误:
path: /abs/path/fire_dataset train: images/train val: images/val nc: 2 names: ['fire', 'smoke']path一定用绝对路径,相对路径在不同工作目录下会找不到。nc是类别数,必须和 names 长度一致,改类别时两个都要改。
训练启动:
python train.py \ --weights yolov9-c.pt \ --data data.yaml \ --epochs 150 \ --batch-size 16 \ --imgsz 640 \ --device 0 \ --workers 8 \ --patience 30 \ --name fire_exp1参数说明:--weights用预训练权重,火焰数据量不大时从头训基本学不出来;--batch-size看显存,16 是 12G 显存的稳妥值,爆显存就降到 8;--imgsz 640是精度和速度的平衡点,小火焰多的话可以试 800,但显存和耗时都会涨;--patience 30表示 30 轮没提升就早停,避免过拟合。训练日志里重点看mAP50和mAP50-95两个指标,前者宽松后者严格,火焰检测我更关注mAP50-95,因为它对框的定位精度更敏感。
3. 训练好的模型怎么推理、评估和看曲线
3.1 单张图片和视频流的推理脚本
拿到训练好的best.pt,先跑单张图验证:
from ultralytics import YOLO import cv2 model = YOLO('runs/detect/fire_exp1/weights/best.pt') # 单张图片推理 results = model.predict( source='test_fire.jpg', conf=0.25, # 置信度阈值 iou=0.45, # NMS 的 IoU 阈值 imgsz=640, save=True, project='infer_out' ) # 打印检测到的框 for r in results: for box in r.boxes: cls = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() print(f"类别:{model.names[cls]} 置信度:{conf:.3f} 坐标:{xyxy}")conf=0.25是火焰检测的常用起点,误报多就往上调到 0.4,漏报多就降到 0.15。iou=0.45控制重叠框合并,火焰密集时调低一点避免把相邻火点合并成一个。
视频流推理把source换成视频路径或摄像头编号即可,但要注意帧率。我一般加一个跳帧逻辑,每 2-3 帧检测一次,中间帧复用上一次结果,这样在边缘设备上能跑得动:
cap = cv2.VideoCapture('fire_video.mp4') frame_id = 0 last_boxes = [] while cap.isOpened(): ret, frame = cap.read() if not ret: break # 每 3 帧检测一次 if frame_id % 3 == 0: results = model.predict(frame, conf=0.3, verbose=False) last_boxes = results[0].boxes # 绘制上一次的结果 for box in last_boxes: x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imshow('fire', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break frame_id += 1 cap.release() cv2.destroyAllWindows()3.2 评估指标怎么看:mAP、Precision、Recall 的实际含义
训练完会生成results.csv和几张曲线图。很多人只看 mAP 数字,其实火焰检测更该关注 Recall 和误报的平衡。
| 指标 | 含义 | 火焰检测里的关注点 |
|---|---|---|
| Precision | 报出来的框里有多少是真的 | 低说明误报多,夕阳、红灯容易触发 |
| Recall | 真实火焰里检出了多少 | 低说明漏报,小火苗没抓到 |
| mAP50 | IoU=0.5 时的平均精度 | 宽松指标,看整体趋势 |
| mAP50-95 | IoU 0.5 到 0.95 的平均 | 严格指标,反映框的定位质量 |
| F1 | Precision 和 Recall 的调和平均 | 综合平衡点,选阈值时参考 |
火焰场景里,漏报的代价远大于误报,所以我会优先保 Recall,宁可多报几个让后端二次确认。具体做法是把conf阈值调低,同时用 F1 曲线找到最佳平衡点。results.csv里每轮都有这些值,用 pandas 读出来画图:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/detect/fire_exp1/results.csv') df.columns = df.columns.str.strip() # 列名可能有空格 fig, ax = plt.subplots(1, 2, figsize=(12, 4)) ax[0].plot(df['epoch'], df['train/box_loss'], label='train_box') ax[0].plot(df['epoch'], df['val/box_loss'], label='val_box') ax[0].set_title('Box Loss') ax[0].legend() ax[1].plot(df['epoch'], df['metrics/mAP50(B)'], label='mAP50') ax[1].plot(df['epoch'], df['metrics/mAP50-95(B)'], label='mAP50-95') ax[1].set_title('mAP') ax[1].legend() plt.savefig('training_curve.png')看曲线有个血泪经验:如果 train loss 一直降但 val loss 从某轮开始往上翘,就是过拟合了,早停或者加数据增强。如果两条都降不下去,可能是学习率太大或者标注有问题,先回去查标注质量。
3.3 混淆矩阵和 PR 曲线里藏着的坑
混淆矩阵能直接看出模型把什么错认成火焰。我见过一个模型把smoke大量误判成fire,原因是烟雾和火焰在训练集里经常同时出现,标注时边界没分清。解决办法是把同时含烟和火的图,两个框都标清楚,别只标一个。
PR 曲线(Precision-Recall)看的是不同置信度下的表现。曲线越靠右上越好,如果曲线在中段突然掉下去,说明某个置信度区间里模型很不稳定,通常是难样本集中在那里。这时候可以针对性补充这类样本重新训。
评估阶段还有个容易忽略的点:验证集和训练集的场景分布要一致。如果训练集全是白天,验证集混了夜间,指标会虚低,让你误以为模型不行,其实是数据分布问题。
4. 火焰检测落地时的避坑清单
4.1 误报压不下去:先查负样本而不是调阈值
现象:模型在夕阳、红色物体、灯光场景频繁误报。原因:训练集负样本不足,模型没见过这些"像火但不是火"的画面。解决:专门采集这类场景作为背景图(不含任何标注框)加入训练集,比例控制在总数据的 20%-30%。单纯调高 conf 阈值只能压一时,换个场景又冒出来。
4.2 小火焰漏检:输入分辨率和数据增强要一起改
现象:远处小火苗检测不到,近处大火正常。原因:640 输入下小目标下采样后信息丢失。解决:把imgsz提到 800 或 960,同时在数据增强里加mosaic和scale,让模型多见小目标。注意提分辨率后 batch-size 要相应降低,否则爆显存。
4.3 训练 loss 震荡不收敛:检查学习率和标注
现象:loss 上下大幅震荡,mAP 不涨。原因:学习率过大,或者标注框有大量越界、宽高为 0 的脏数据。解决:先把学习率降到 0.001 试,同时跑一遍标注校验脚本,把非法框清理掉。标注脏数据是玄学问题的头号来源,别偷懒。
4.4 推理速度不达标:别只怪模型,先看预处理
现象:GPU 上推理还是慢。原因:图像预处理(resize、归一化)在 CPU 上串行做,成了瓶颈。解决:用half=True开启 FP16 推理,把预处理放到 GPU 或者用多线程预取。实测 FP16 能提速 30% 左右,精度损失很小。
4.5 换场景后精度暴跌:域偏移是常态
现象:实验室训的模型拿到真实园区就废了。原因:光照、背景、摄像头角度差异导致域偏移。解决:别指望一个模型打天下,用真实场景数据做微调,冻结骨干只训检测头,几十张图就能有明显改善。这是最省力的迁移方式。
5. 把火焰检测推到边缘设备和持续迭代上
前面跑通的是单机推理,真落地往往要上边缘设备。我一般用 ONNX 导出再转 TensorRT,延迟能压到实时。导出命令:
python export.py --weights best.pt --include onnx --imgsz 640 --half导出后重点验证 ONNX 和 PyTorch 的输出是否一致,我遇到过因为 NMS 实现差异导致框数量对不上的情况,用同一张图对比两边输出,误差在 1e-3 以内才算过关。
持续迭代这块,我的习惯是建一个"难例回流"机制:线上推理时把低置信度但被后端确认是真火的样本自动存下来,每周回流一次做增量训练。火焰检测没有一劳永逸的模型,场景在变,模型就得跟着变。评估曲线不是训完就扔的,每次增量训练后都对比新旧模型的 PR 曲线,确认没有退化再上线。
最后说个我踩过的坑:早期我图省事,把验证集和训练集用同一批视频抽帧,结果指标虚高得离谱,上线就翻车。后来严格按视频维度划分,同一个视频的帧要么全在训练集要么全在验证集,指标才真实。这个习惯我一直保留到现在,希望帮到你。
本文还有配套的精品资源,点击获取