
简介本资源面向目标检测方向的工程技术人员、大学生课题与论文研究者以及希望快速查看火焰识别效果的技术入门者提供一套可直接运行的火焰检测完整方案。包内包含YOLO格式标注数据集、已训练完成的模型权重文件以及基于QT搭建的可视化交互界面代码可直接运行无需从零训练即可验证检测效果。资源共234个文件以jpg、jpeg、png等图像样本与yaml配置文件为主配合40个Python源码、9个pt模型权重、21个pyc缓存及csv训练日志、sh脚本、Dockerfile等工程文件压缩包约997.51MB覆盖数据、训练、推理与界面展示全流程。目前已有935人学习下载。读者可据此快速复现火焰检测流程理解数据集组织与模型调用方式并借助QT界面直观查看识别结果适合作为工程原型参考或课题论文的实践基础。1. 火焰检测算法 YOLO 格式数据集 模型文件 QT界面一条能跑通的落地链路厂区消防改造项目里最容易被低估的不是模型精度而是从「一堆标注图片」到「车间大屏上能实时框出火苗」之间的工程距离。火焰检测算法本身在开源社区已经相当成熟YOLO 系列从 v5 到 v8 都能做真正卡住人的是数据集怎么按 YOLO 格式组织、模型文件怎么导出成推理引擎能吃的形态、QT 界面怎么把摄像头帧喂给模型再把框画回去。这三件事任何一环脱节demo 就永远停在笔记本里。这套方案适合两类人一类是手里已经有火焰图片、想快速验证检测效果的算法同学另一类是负责上位机开发、需要把现成模型集成进 QT 视频监控界面的嵌入式或工控方向工程师。下面按「数据集准备 → 模型训练与导出 → QT 界面集成 → 踩坑排查 → 进阶技巧」的顺序把每个环节的可复现命令和参数讲清楚能直接抄作业。2. YOLO 格式火焰数据集的准备与校验2.1 火焰数据集为什么必须统一成 YOLO 格式火焰检测的数据来源通常很杂有的是自己用手机拍的视频抽帧有的是公开的火灾图像数据集还有的是从监控录像里截的图。这些数据如果直接丢给训练脚本第一关就过不去——YOLO 训练器只认一种标注格式每张图片对应一个同名.txt文件每行是类别编号 中心x 中心y 宽 高且四个坐标值都是相对于图片宽高的归一化值0 到 1 之间。常见做法是先用 LabelImg 打标导出时选 YOLO 格式。LabelImg 打标完 YOLO 格式的标之后会在图片同级目录生成classes.txt和每张图的.txt。但这里有个血泪经验LabelImg 默认的类别顺序是按你第一次输入标签的顺序定的如果中途改了标签名或者增删了类别classes.txt和实际.txt里的编号会对不上训练时模型学到的类别就是错的。所以打完标第一件事是校验类别映射。2.2 用脚本校验标注文件与图片的一一对应下面这段脚本做三件事检查每张图片是否有对应的.txt、检查.txt里类别编号是否超出classes.txt的范围、统计每个类别的框数量。import os from pathlib import Path from collections import Counter img_dir Path(datasets/fire/images/train) lbl_dir Path(datasets/fire/labels/train) classes_file Path(datasets/fire/classes.txt) # 读取类别列表 classes classes_file.read_text(encodingutf-8).strip().splitlines() num_classes len(classes) print(f类别数: {num_classes}, 类别: {classes}) img_exts {.jpg, .jpeg, .png, .bmp} imgs [p for p in img_dir.iterdir() if p.suffix.lower() in img_exts] missing_label [] bad_class [] counter Counter() for img in imgs: lbl lbl_dir / (img.stem .txt) if not lbl.exists(): missing_label.append(img.name) continue for line in lbl.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: bad_class.append((img.name, 字段数不对)) continue cid int(parts[0]) if cid 0 or cid num_classes: bad_class.append((img.name, f类别编号 {cid} 越界)) counter[cid] 1 print(f图片总数: {len(imgs)}) print(f缺失标注的图片: {len(missing_label)} 张) print(f异常标注: {len(bad_class)} 条) print(f各类别框数: {dict(counter)})逻辑说明先建立图片 stem 到标注文件的映射逐行解析标注。参数上img_dir和lbl_dir要按你实际的数据集目录改YOLO 官方推荐的结构是images/train和labels/train平行放置。如果missing_label不为零说明有图片没打标训练时这些图会被忽略但不会报错容易让人误以为数据都用了。bad_class里出现越界编号基本就是 LabelImg 中途改过标签需要手动修正或重新导出。2.3 数据集划分与 data.yaml 的写法YOLOv8 训练自己的数据集时靠一个data.yaml告诉训练器去哪里找图片和类别名。常见做法是训练集、验证集按 8:2 或 9:1 划分火焰检测这种场景如果正样本本身就不多验证集可以只留 10%。path: /home/user/datasets/fire train: images/train val: images/val nc: 2 names: 0: flame 1: smoke参数说明path是数据集根目录train和val是相对路径。nc是类别数必须和names的条目数一致。names的键从 0 开始连续不能跳号。如果只检测火焰不检测烟雾把nc改成 1names只留0: flame同时要确保所有标注文件里没有编号为 1 的行否则训练启动时会直接报类别越界。提示划分脚本不要用随机数直接切火焰视频抽帧得到的相邻帧高度相似随机切会导致验证集里出现和训练集几乎一样的图验证指标虚高。按视频来源分组切分更可靠。3. 火焰检测模型的训练、验证与模型文件导出3.1 环境配置与训练命令的最小闭环YOLOv8 的环境配置要求不算高Python 3.8 以上、PyTorch 1.8 以上就能跑。用 Anaconda 建环境是最省事的做法避免和系统里的其他包打架。conda create -n fire_yolo python3.10 -y conda activate fire_yolo pip install ultralytics opencv-python装完之后训练命令一行就能启动yolo detect train \ data/home/user/datasets/fire/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ projectruns/fire \ nameexp1参数说明modelyolov8n.pt用的是 nano 版本预训练权重火焰检测这种目标特征明显、背景相对固定的场景nano 或 small 版本通常够用推理速度还快。imgsz640是输入分辨率如果火焰在画面里占比很小可以提到 960 或 1280但显存占用会明显上升。batch16在 8G 显存上跑 640 分辨率基本安全显存不够就降到 8。device0指定第一块 GPU用 CPU 训练的话改成devicecpu但 100 个 epoch 可能要跑一整天。训练过程中重点看两个指标mAP50和mAP50-95。火焰检测一般mAP50能到 0.85 以上就算可用如果卡在 0.5 左右上不去优先检查标注框是不是把火焰区域框得太松或者太紧。3.2 模型文件导出从 .pt 到推理引擎训练完得到的是best.pt这是 PyTorch 权重文件。如果 QT 界面用 Python 写直接加载.pt就行但如果 QT 是 C 写的或者要部署到没有 PyTorch 环境的工控机上就需要导出成 ONNX 或 TensorRT 引擎。# 导出 ONNX yolo export modelruns/fire/exp1/weights/best.pt formatonnx imgsz640 opset12 # 导出 TensorRT需要 NVIDIA GPU 和 TensorRT 环境 yolo export modelruns/fire/exp1/weights/best.pt formatengine imgsz640 halfTrue逻辑说明ONNX 是通用中间格式C 端可以用 OpenCV 的dnn模块直接加载不需要额外装 TensorRT。opset12是算子集版本太低会缺算子太高有些推理框架不支持12 是比较稳的选择。TensorRT 引擎导出时加halfTrue开启 FP16 量化推理速度能提升接近一倍精度损失通常在 1% 以内火焰检测这种任务完全能接受。注意TensorRT 引擎和导出时的 GPU 型号、TensorRT 版本强绑定换一台机器基本要重新导出。如果 QT 界面要分发给多台设备优先用 ONNX。3.3 用 Python 验证导出模型是否可用导出后别急着往 QT 里塞先用脚本跑一张测试图确认输出格式和预期一致。import cv2 import numpy as np net cv2.dnn.readNetFromONNX(runs/fire/exp1/weights/best.onnx) img cv2.imread(test_fire.jpg) h, w img.shape[:2] # YOLO 输入是 640x640保持比例缩放后填充 scale 640 / max(h, w) nh, nw int(h * scale), int(w * scale) resized cv2.resize(img, (nw, nh)) canvas np.full((640, 640, 3), 114, dtypenp.uint8) canvas[:nh, :nw] resized blob cv2.dnn.blobFromImage(canvas, 1/255.0, (640, 640), swapRBTrue) net.setInput(blob) outputs net.forward() print(输出形状:, outputs.shape)逻辑说明YOLOv8 的 ONNX 输出形状通常是(1, 4nc, 8400)8400 是候选框数量4nc里前 4 个是框坐标后面是各类别置信度。swapRBTrue是因为 OpenCV 读进来是 BGR而模型训练时用的是 RGB。填充值 114 是 YOLO 官方用的灰度填充和训练时的预处理保持一致否则检测框会偏移。如果输出形状对不上大概率是导出时的imgsz和推理时不一致。4. QT 视频监控界面集成火焰检测的完整步骤4.1 QT 界面选型Python 端还是 C 端QT 视频监控界面的实现路线有两条PyQt5/PySide6 用 Python 写开发快、和 YOLO 的 Python 生态无缝衔接Qt C 性能好、部署干净但需要自己处理模型推理的 C 接口。如果团队里没有强制 C 要求我一般会选 PyQt5因为从摄像头取帧到模型推理再到界面绘制全在 Python 里闭环调试成本低很多。界面布局上核心控件就三个一个QLabel显示视频帧、一个QPushButton控制启停、一个QLabel或QTextEdit显示检测结果比如「检测到火焰置信度 0.92」。不要一上来就搞复杂布局先把视频流跑通。4.2 用 QThread 把推理和界面刷新分开QT 界面卡死的头号原因就是把模型推理放在主线程里。YOLO 推理一帧在 CPU 上可能要 100 毫秒以上主线程被占住界面就没法响应按钮点击和窗口拖动。正确做法是把取帧和推理放到QThread里通过信号槽把结果传回主线程绘制。from PyQt5.QtCore import QThread, pyqtSignal import cv2 import numpy as np class DetectThread(QThread): frame_ready pyqtSignal(np.ndarray, list) # 帧 检测框列表 def __init__(self, model_path, conf_thres0.4): super().__init__() self.net cv2.dnn.readNetFromONNX(model_path) self.conf_thres conf_thres self.running True def run(self): cap cv2.VideoCapture(0) while self.running: ret, frame cap.read() if not ret: continue boxes self.infer(frame) self.frame_ready.emit(frame, boxes) cap.release() def infer(self, frame): h, w frame.shape[:2] scale 640 / max(h, w) nh, nw int(h * scale), int(w * scale) resized cv2.resize(frame, (nw, nh)) canvas np.full((640, 640, 3), 114, dtypenp.uint8) canvas[:nh, :nw] resized blob cv2.dnn.blobFromImage(canvas, 1/255.0, (640, 640), swapRBTrue) self.net.setInput(blob) out self.net.forward()[0].T # (8400, 4nc) boxes [] for row in out: scores row[4:] cid int(np.argmax(scores)) conf float(scores[cid]) if conf self.conf_thres: continue cx, cy, bw, bh row[:4] x1 int((cx - bw/2) / scale) y1 int((cy - bh/2) / scale) x2 int((cx bw/2) / scale) y2 int((cy bh/2) / scale) boxes.append((x1, y1, x2, y2, cid, conf)) return boxes逻辑说明frame_ready信号携带原始帧和检测框列表主线程收到后只做绘制不做推理。conf_thres0.4是置信度门限火焰检测建议从 0.4 起步误报多就往上调到 0.5 或 0.6漏报多就降到 0.3。坐标还原时除以scale是因为推理前做了等比缩放不还原的话框会偏小且位置不对。4.3 主线程绘制与置信度门限的动态调整主线程里把检测框画到帧上再转成QImage显示。这里有个细节OpenCV 的帧是 BGRQImage需要 RGB转换时要用cv2.cvtColor或者直接指定QImage.Format_BGR888。from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap import cv2 import sys class MainWindow(QMainWindow): def __init__(self): super().__init__() self.label QLabel(等待视频流...) self.btn QPushButton(开始检测) self.btn.clicked.connect(self.toggle) layout QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.btn) container QWidget() container.setLayout(layout) self.setCentralWidget(container) self.thread None def toggle(self): if self.thread is None: self.thread DetectThread(best.onnx, conf_thres0.4) self.thread.frame_ready.connect(self.on_frame) self.thread.start() self.btn.setText(停止检测) else: self.thread.running False self.thread.wait() self.thread None self.btn.setText(开始检测) def on_frame(self, frame, boxes): for x1, y1, x2, y2, cid, conf in boxes: color (0, 0, 255) if cid 0 else (255, 0, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, f{conf:.2f}, (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch*w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg).scaled( self.label.width(), self.label.height()))逻辑说明toggle里用self.thread is None判断当前是否在检测避免重复启动线程。on_frame里火焰用红色框、烟雾用蓝色框置信度标在框上方。QImage构造时传入rgb.data要确保rgb是连续内存cv2.cvtColor返回的数组默认连续没问题。缩放显示用scaled保持比例否则画面会拉伸变形。提示如果界面刷新时画面撕裂或延迟累积可以在DetectThread里加一个帧队列只保留最新帧丢弃来不及处理的旧帧。火焰检测对实时性要求高宁可丢帧也不要延迟。5. 火焰检测落地中的避坑与排查记录5.1 训练 loss 正常但验证 mAP 极低现象训练日志里box_loss和cls_loss都在下降但mAP50一直在 0.1 以下。原因最常见的是标注文件里的坐标没有归一化。LabelImg 导出 YOLO 格式时会自动归一化但如果手动改过.txt或者用其他工具导出时选了绝对坐标格式坐标值就会是几百甚至上千训练器读到后框全在图片外面。解决打开任意一个.txt看四个数值是否都在 0 到 1 之间。如果大于 1用脚本批量除以图片宽高重新归一化。另外检查data.yaml里的nc和实际类别数是否一致不一致时训练器会把所有类别都当成背景。5.2 QT 界面启动后摄像头画面卡在第一帧现象点击「开始检测」后QLabel只显示第一帧之后不再更新但程序没崩溃。原因DetectThread.run里的while循环没有让出 GIL 或者信号发射太频繁导致主线程事件队列堵塞。PyQt 的信号槽跨线程默认是队列连接如果发射频率高于主线程处理速度队列会越积越长界面看起来就像卡住了。解决在run循环里加self.msleep(1)让出时间片或者在frame_ready信号连接时用Qt.DirectConnection不推荐会回到主线程执行。更稳的做法是限制推理帧率比如每处理一帧后time.sleep(0.03)把帧率控制在 30 帧以内。5.3 ONNX 模型在 C 端加载报维度错误现象Python 端 ONNX 推理正常但 C 用 OpenCVdnn加载时提示输入维度不匹配或输出为空。原因导出 ONNX 时imgsz和 C 端blobFromImage的尺寸不一致或者opset版本和 OpenCV 版本不兼容。OpenCV 4.5 以下对 opset 12 以上支持不好。解决统一imgsz为 640导出时显式指定opset12。C 端blobFromImage的size参数写(640, 640)swapRB设truecrop设false。如果还是不行把 OpenCV 升级到 4.8 以上。5.4 火焰检测误报把灯光和夕阳当成火焰现象模型在白天把暖色灯光、夕阳反射、橙色安全帽都框成火焰置信度还不低。原因训练集里负样本太少模型没学过「像火焰但不是火焰」的样本。火焰的颜色特征和某些暖色光源高度重叠只靠 RGB 很难区分。解决收集误报图片作为负样本加入训练集标注文件为空.txt重新训练。同时在 QT 界面里加一个置信度门限滑动条让现场人员根据实际误报情况动态调整。如果条件允许在推理前加一个 HSV 颜色预筛选把明显不是火焰颜色的区域先排除。5.5 模型文件换机器后推理结果全乱现象在开发机上检测正常的best.pt拷到工控机上后框的位置全部偏移或者类别全错。原因工控机上的 ultralytics 版本和开发机不一致不同版本对 YOLOv8 输出层的解析方式有差异。或者工控机上没有 GPUPyTorch 回退到 CPU 推理时某些算子行为不同。解决导出 ONNX 而不是直接拷.ptONNX 的算子行为是标准化的跨平台一致性更好。如果必须用.pt在两台机器上pip freeze对比 ultralytics 和 torch 版本保持一致。工控机没有 GPU 时导出 ONNX 后用 OpenCVdnn的 CPU 后端推理速度虽然慢一些但结果稳定。6. 火焰检测置信度门限的动态调整与误报抑制技巧置信度门限是火焰检测落地后调得最频繁的参数。固定门限在实验室里看着挺好一到现场就翻车白天阳光强的时候误报多晚上光线暗的时候漏报多。我后来养成的习惯是在 QT 界面里加一个滑动条把门限暴露给现场人员同时记录每次调整后的误报和漏报情况攒一周数据再回头定一个分时段门限。具体做法是在DetectThread里把conf_thres做成可写属性主线程滑动条变化时通过信号槽更新。下面是一个简化的实现from PyQt5.QtWidgets import QSlider from PyQt5.QtCore import Qt # 在主窗口里加滑动条 self.slider QSlider(Qt.Horizontal) self.slider.setRange(20, 80) # 对应 0.2 到 0.8 self.slider.setValue(40) self.slider.valueChanged.connect(self.on_thres_change) def on_thres_change(self, val): if self.thread: self.thread.conf_thres val / 100.0逻辑说明滑动条范围 20 到 80 映射到 0.2 到 0.8步进 1 对应 0.01。valueChanged信号触发时直接改线程对象的属性Python 里属性赋值是原子操作不需要加锁。现场调试时让操作员一边看画面一边拖滑动条找到当前光照条件下误报和漏报的平衡点。除了门限还有一个抑制误报的技巧是连续帧确认。单帧检测到火焰不报警连续 3 帧同一位置都检测到才触发报警。这个逻辑放在主线程里维护一个最近帧的检测框列表用 IOU 匹配。火焰在视频里是持续存在的而灯光反射、飞鸟这类干扰通常只出现一两帧连续帧确认能过滤掉大部分瞬时误报。def confirm_fire(self, boxes, iou_thres0.5, need_frames3): fire_boxes [b for b in boxes if b[4] 0] matched 0 for fb in fire_boxes: for pb in self.prev_fire_boxes: if self.iou(fb, pb) iou_thres: matched 1 break self.prev_fire_boxes fire_boxes return matched need_frames def iou(self, a, b): x1 max(a[0], b[0]); y1 max(a[1], b[1]) x2 min(a[2], b[2]); y2 min(a[3], b[3]) inter max(0, x2-x1) * max(0, y2-y1) area_a (a[2]-a[0]) * (a[3]-a[1]) area_b (b[2]-b[0]) * (b[3]-b[1]) return inter / (area_a area_b - inter 1e-6)参数说明iou_thres0.5表示前后帧的火焰框重叠超过一半才算同一目标火焰移动快的时候可以降到 0.3。need_frames3是连续确认帧数设太高会导致报警延迟设太低起不到过滤作用3 帧在 25 帧的视频流里大约是 120 毫秒延迟基本无感。最后说一个我踩过的坑不要用验证集上的最优门限直接上现场。验证集里的负样本和现场的真实干扰分布差很远实验室里 0.5 门限 mAP 最高到现场可能 0.6 才压得住误报。我的习惯是留一批现场采集的误报图片作为「现场测试集」每次调完门限在这批图上跑一遍确认误报率可接受再更新到设备上。这套流程跑顺之后火焰检测从训练到上线基本能控制在一周以内剩下的就是根据现场反馈微调参数。希望帮到你。本文还有配套的精品资源点击获取