资讯详情

火焰检测算法落地:从YOLO数据集到QT界面的完整链路

📅 2026/10/7 1:33:11 | 华诺云谱 👁 阅读
火焰检测算法落地:从YOLO数据集到QT界面的完整链路
简介本资源面向火焰检测方向的工程开发人员与高校学生提供一套可直接运行的完整方案涵盖YOLO格式数据集、已训练模型文件与QT可视化界面既能支撑实际工程项目落地也适合作为大学生课题或论文的实验基础。压缩包共234个文件约997.51MB包含jpg、jpeg、png等图像样本yaml与cfg配置py与pyc源码pt权重文件以及csv训练日志、sh脚本、Dockerfile、md说明和xml标注等覆盖数据、训练、推理与部署各环节。目前已有935人学习下载说明该方案在火焰识别场景中具备一定参考价值。读者可借助现成模型快速查看检测效果结合训练曲线与结果文件复盘调参过程并基于QT界面完成演示与二次开发省去从零搭建数据与模型的时间成本。1. 火焰检测算法落地从 yolo 数据集到 QT 界面的完整链路厂区仓库的消防改造项目里最容易被低估的不是模型精度而是从「一堆标注图片」到「值班员能点开就用的桌面程序」之间那条链路。火焰检测算法本身在开源社区已经相当成熟yolo 系列把检测门槛压得很低但真正交付时你会发现数据集格式不对、模型文件加载报错、QT 界面卡在主线程、误报把烟雾当成火焰——这些才是让项目翻车的地方。这篇笔记面向的是手里已经有一批火焰/烟雾图片、想把它做成一个能跑在工控机上的桌面检测工具的工程师。我会按「数据集怎么整理成 yolo 格式 → 模型怎么训练和导出 → QT 界面怎么接推理 → 怎么避坑」的顺序讲每一步都给可抄的命令和参数新手能跟着跑通熟手能直接看到边界条件。2. 火焰数据集整理成 yolo 格式标注、划分与两个易错点2.1 火焰检测的数据特点决定了标注策略火焰和烟雾这类目标有几个和常规 COCO 目标不一样的地方直接照搬通用检测经验会吃亏。第一火焰边界模糊火苗外焰和背景之间是渐变过渡标注框画大画小全凭标注员手感同一张图两个人标出来的框可能差 15% 以上。第二火焰形态高度依赖场景蜡烛火、酒精灯、木材堆火、电缆短路电弧视觉差异极大如果数据集里只有一种火模型换场景就废。第三负样本极其关键夕阳、车灯、焊接火花、红色反光板这些「像火但不是火」的样本如果不进训练集现场误报率会高到没法用。我一般建议火焰检测数据集按「场景 火源类型」两个维度去凑每个维度至少覆盖 3 到 5 类正样本和困难负样本的比例控制在 1:1 到 1:2 之间。困难负样本就是那些模型容易误判的图比如黄昏天空、暖色灯光、金属反光。这部分数据不用标框但必须进训练集否则模型学不会「什么不是火」。2.2 从 VOC/COCO 转成 yolo txt 的脚本大多数人手头的数据集是 VOC 的 XML 或者 LabelImg 直接导出的yolo 要的是每张图对应一个 txt每行类别 中心x 中心y 宽 高且全部归一化到 0~1。下面这个脚本处理 VOC 转 yolo同时做训练/验证集划分。import os import random import xml.etree.ElementTree as ET import shutil # 类别映射火焰检测一般就 fire / smoke 两类多类按需扩展 CLASSES [fire, smoke] # 输入VOC 格式的图片和 xml 目录输出yolo 数据集根目录 VOC_IMG_DIR ./voc/JPEGImages VOC_XML_DIR ./voc/Annotations OUT_ROOT ./fire_yolo VAL_RATIO 0.2 # 验证集比例火焰场景样本少时别超过 0.2 def convert_box(size, box): # size: (w, h)box: (xmin, ymin, xmax, ymax) dw, dh 1.0 / size[0], 1.0 / size[1] x (box[0] box[2]) / 2.0 * dw y (box[1] box[3]) / 2.0 * dh w (box[2] - box[0]) * dw h (box[3] - box[1]) * dh return x, y, w, h def convert_annotation(xml_path, txt_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in CLASSES: continue cls_id CLASSES.index(cls) bnd obj.find(bndbox) box (float(bnd.find(xmin).text), float(bnd.find(ymin).text), float(bnd.find(xmax).text), float(bnd.find(ymax).text)) x, y, bw, bh convert_box((w, h), box) # 过滤掉宽高为 0 的脏标注这类框会让训练 loss 直接 NaN if bw 0 or bh 0: continue lines.append(f{cls_id} {x:.6f} {y:.6f} {bw:.6f} {bh:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) # 建目录 for sub in [images/train, images/val, labels/train, labels/val]: os.makedirs(os.path.join(OUT_ROOT, sub), exist_okTrue) xml_files [f for f in os.listdir(VOC_XML_DIR) if f.endswith(.xml)] random.shuffle(xml_files) val_count int(len(xml_files) * VAL_RATIO) val_set set(xml_files[:val_count]) for xml_file in xml_files: name os.path.splitext(xml_file)[0] split val if xml_file in val_set else train txt_path os.path.join(OUT_ROOT, labels, split, name .txt) convert_annotation(os.path.join(VOC_XML_DIR, xml_file), txt_path) img_src os.path.join(VOC_IMG_DIR, name .jpg) if os.path.exists(img_src): shutil.copy(img_src, os.path.join(OUT_ROOT, images, split, name .jpg))这段脚本的逻辑是先按比例随机划分文件再逐张解析 XML把绝对坐标转成归一化中心点格式。几个参数要留意VAL_RATIO在火焰样本少于 2000 张时建议设 0.15 到 0.2样本多了可以降到 0.1CLASSES的顺序必须和后面训练时 data.yaml 里的names完全一致顺序错了模型会把火识别成烟。转换完一定要抽查几张 txt确认坐标没越界、没有空文件。2.3 data.yaml 与目录结构yolo 训练靠一个 yaml 描述数据位置和类别火焰检测的典型配置如下path: ./fire_yolo train: images/train val: images/val nc: 2 names: 0: fire 1: smokepath用相对路径时训练命令要在同一级目录执行否则会找不到图。nc是类别数必须和 names 条数一致。常见翻车点是图片和标签文件名对不上——yolo 靠同名匹配abc.jpg必须配abc.txt差一个字符这张图就被静默跳过训练日志里看不出来只能靠train前打印数据集统计去核对。3. 火焰检测模型训练与导出参数怎么设、模型文件怎么来3.1 训练命令与关键超参数据集就绪后用 yolo 官方 CLI 或 Python API 都能训。火焰检测我一般从预训练权重起步而不是从头训因为火焰样本通常不够多。命令如下yolo detect train \ data./fire_yolo/data.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience30 \ device0 \ project./runs_fire \ nameexp1参数逐个说model选 n 还是 s 取决于部署硬件工控机 CPU 推理就选 n有 GPU 可以上 sepochs火焰检测一般 100 到 200 够用再多容易过拟合小数据集imgsz640是速度和精度的平衡点火焰目标通常占画面比例不小640 够用如果检测远处小火苗可以提到 960batch按显存调8G 显存跑 640 用 16 比较稳patience30是早停验证集 30 轮不涨就停省时间lr0初始学习率 0.01 是默认值小数据集可以降到 0.005 更稳。训练过程中重点看三个指标mAP50看整体精度火焰检测能到 0.85 以上基本可用precision看误报这个对消防场景比 recall 还重要误报多了值班员会直接关掉系统recall看漏检漏检意味着真着火没报同样致命。如果 precision 低加困难负样本如果 recall 低检查标注框是不是画太小。3.2 模型文件导出与格式选择训练完在runs_fire/exp1/weights/下会有best.pt和last.pt部署用best.pt。但.pt是 PyTorch 格式QT 里如果直接用 Python 推理没问题如果要嵌 C 或者上 TensorRT 就得转格式。# 导出 ONNX通用性最好QT OpenCV DNN 能直接读 yolo export model./runs_fire/exp1/weights/best.pt formatonnx imgsz640 opset12 # 有 NVIDIA 显卡且追求帧率导出 TensorRT engine yolo export model./runs_fire/exp1/weights/best.pt formatengine imgsz640 halfTrue device0ONNX 的opset12兼容性最好QT 里用 OpenCV 的readNetFromONNX直接加载。TensorRT 的halfTrue开 FP16速度能翻倍但精度掉一点点火焰检测这种二分类任务基本无感。注意 TensorRT engine 和显卡型号、驱动版本绑定换机器要重新导出这是很多人部署时踩的坑——在开发机上导出的 engine 拷到现场工控机上直接报错。3.3 推理后处理置信度和 NMS 怎么调模型输出的是原始框要经过置信度过滤和 NMS 才能用。火焰检测的阈值和通用检测不一样参数通用检测常用值火焰检测建议值原因conf 置信度阈值0.250.4~0.5火焰误报代价高宁可漏一点iou NMS 阈值0.450.5~0.6火焰框重叠多阈值太低会误删max_det 最大检测数30020画面里火焰目标不会太多conf 提到 0.4 以上能砍掉大部分「疑似火焰」的误报代价是远处小火苗可能漏掉。如果场景对漏检零容忍就保持 0.3 但必须加困难负样本重训。NMS 阈值调高是因为火焰区域经常被模型拆成多个框阈值低了会把相邻的真框也删掉。4. QT 界面接火焰检测线程、绘制与实时性4.1 为什么不能把推理放在主线程QT 的主线程负责界面刷新和事件循环推理一帧在 CPU 上可能几十到几百毫秒放主线程界面直接卡死按钮点不动、窗口拖不动。血泪经验是只要推理耗时超过 30ms就必须开独立线程。QT 里标准做法是QThread加信号槽工作线程跑推理结果通过信号发回主线程绘制。from PyQt5.QtCore import QThread, pyqtSignal import cv2 import numpy as np class InferThread(QThread): # 信号传回绘制好的帧和检测结果列表 frame_ready pyqtSignal(np.ndarray, list) def __init__(self, model_path, conf0.45, iou0.55): super().__init__() self.net cv2.dnn.readNetFromONNX(model_path) self.conf conf self.iou iou self.running True def run(self): cap cv2.VideoCapture(0) # 现场换成 RTSP 地址或视频文件 while self.running: ret, frame cap.read() if not ret: continue blob cv2.dnn.blobFromImage(frame, 1/255.0, (640, 640), swapRBTrue, cropFalse) self.net.setInput(blob) outputs self.net.forward() detections self.postprocess(outputs, frame.shape) for det in detections: x1, y1, x2, y2, score, cls_id det color (0, 0, 255) if cls_id 0 else (255, 0, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, f{score:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) self.frame_ready.emit(frame, detections) cap.release() def postprocess(self, outputs, shape): # yolo 输出形状 (1, 4nc, 8400)转置后逐行处理 h, w shape[:2] outputs np.squeeze(outputs).T boxes, scores, class_ids [], [], [] for row in outputs: cls_scores row[4:] cls_id int(np.argmax(cls_scores)) score float(cls_scores[cls_id]) if score self.conf: continue cx, cy, bw, bh row[:4] x1 int((cx - bw / 2) * w / 640) y1 int((cy - bh / 2) * h / 640) x2 int((cx bw / 2) * w / 640) y2 int((cy bh / 2) * h / 640) boxes.append([x1, y1, x2 - x1, y2 - y1]) scores.append(score) class_ids.append(cls_id) indices cv2.dnn.NMSBoxes(boxes, scores, self.conf, self.iou) results [] if len(indices) 0: for i in indices.flatten(): x, y, bw, bh boxes[i] results.append((x, y, x bw, y bh, scores[i], class_ids[i])) return results def stop(self): self.running False self.wait()这段代码的关键点blobFromImage的swapRBTrue是因为 OpenCV 读图是 BGR模型训练用 RGB不换通道颜色会反火焰可能识别不出来1/255.0是归一化必须和训练时一致后处理里坐标要从 640 缩放回原图尺寸缩放比例算错框会画偏。frame_ready信号把帧和结果一起发出去主线程只负责setPixmap显示不做任何计算。4.2 主线程绘制与界面布局主线程收到信号后更新 QLabel 显示画面同时把检测结果写进一个列表控件方便值班员回溯。from PyQt5.QtWidgets import (QMainWindow, QLabel, QVBoxLayout, QWidget, QListWidget, QPushButton, QHBoxLayout) from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt class FireDetectWindow(QMainWindow): def __init__(self, model_path): super().__init__() self.setWindowTitle(火焰检测系统) self.video_label QLabel(等待视频流...) self.video_label.setAlignment(Qt.AlignCenter) self.alert_list QListWidget() self.btn_start QPushButton(开始检测) self.btn_stop QPushButton(停止) right QVBoxLayout() right.addWidget(self.alert_list) right.addWidget(self.btn_start) right.addWidget(self.btn_stop) main QHBoxLayout() main.addWidget(self.video_label, stretch3) main.addLayout(right, stretch1) container QWidget() container.setLayout(main) self.setCentralWidget(container) self.thread InferThread(model_path) self.thread.frame_ready.connect(self.on_frame) self.btn_start.clicked.connect(self.thread.start) self.btn_stop.clicked.connect(self.thread.stop) def on_frame(self, frame, detections): # BGR 转 RGB再转 QImage注意 stride 参数 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg)) for det in detections: x1, y1, x2, y2, score, cls_id det label 火焰 if cls_id 0 else 烟雾 self.alert_list.addItem(f{label} 置信度{score:.2f} 位置({x1},{y1}))QImage构造时的ch * w是每行字节数漏了这个参数图像会斜。alert_list每帧都加会爆实际项目里要加去重逻辑比如同一位置 2 秒内只记一次。按钮直接连thread.start和thread.stop注意QThread不能重复 start停止后要重建线程对象才能再启动这是 QT 多线程的经典坑。4.3 实时性优化从 15 帧到 30 帧的几个手段工控机上跑 1080p 火焰检测默认配置可能只有 10 到 15 帧。几个立竿见影的优化第一推理分辨率从 640 降到 416速度提升接近一倍火焰目标大时精度掉得不多第二用 TensorRT engine 替代 ONNXN 卡上能到 2 到 3 倍第三跳帧推理每 2 帧推一次中间帧复用上次结果画面看起来仍然流畅第四把cv2.dnn换成onnxruntime的 GPU providerCPU 场景提升有限但 GPU 场景明显。这几招组合下来1080p 25 帧在带独显的工控机上是可以做到的。5. 火焰检测落地避坑五条现场踩出来的经验5.1 误报夕阳和暖色灯光被识别成火焰现象是每天傍晚系统疯狂报警白天正常。原因是训练集里缺少黄昏、暖色照明这类困难负样本模型只学会了「橙红色 火」。解决办法是专门采集一批夕阳、路灯、红色广告牌的图不标框直接进训练集同时把 conf 阈值从 0.25 提到 0.45。重训后误报能降一个数量级。5.2 模型文件加载失败ONNX 和 engine 的兼容性现象是开发机上跑得好好的模型拷到现场工控机上报readNetFromONNX失败或者输出全零。原因是 ONNX 的 opset 版本和现场 OpenCV 版本不匹配或者 TensorRT engine 和显卡架构不匹配。解决办法是 ONNX 导出时固定opset12现场 OpenCV 至少 4.5.4engine 必须在目标机器上重新导出不能跨机器拷贝。5.3 界面卡顿推理和绘制抢主线程现象是视频画面一顿一顿点按钮要等好几秒才响应。原因是推理写在了主线程或者信号槽用了Qt.DirectConnection导致跨线程直接调用。解决办法是确认推理在QThread.run里信号槽用默认的Qt.AutoConnection让 QT 自动排队到主线程。另外绘制时不要每帧都addItem列表长了会拖慢界面。5.4 漏检小火苗和远距离火焰检测不到现象是近处大火能报远处小火苗一直不报。原因是训练集里小目标样本太少或者推理分辨率太低导致小目标特征丢失。解决办法是训练时开mosaic增强yolo 默认开把imgsz提到 960同时在数据里补充远距离火焰样本。如果还不行考虑用切片推理把大图切成小块分别检测。5.5 颜色通道错误火焰框位置对但类别反了现象是框的位置准确但火被标成烟、烟被标成火。原因是训练时用了 RGB推理时blobFromImage没设swapRBTrue通道反了导致颜色特征错乱。解决办法是检查推理预处理确保swapRBTrue并且归一化系数和训练一致。这个坑很隐蔽因为框的位置不受颜色影响只有类别会错。6. 把火焰检测做成可交付系统的一个进阶技巧前面讲的都是单模型单场景但真实项目里经常遇到「一个模型覆盖不了所有场景」的情况室内蜡烛火和室外木材堆火用同一个模型精度都一般。我一般会用一个轻量的场景路由先跑一个极小的分类模型判断当前画面属于哪类场景再路由到对应的专用检测模型。分类模型可以用 MobileNet 这种级别的推理耗时几乎可以忽略但检测精度能提升 10 个点以上。具体做法是训练 2 到 3 个专用火焰检测模型每个模型只负责一类场景然后在 QT 推理线程里先跑分类再选模型。代码结构上就是把InferThread里的self.net换成一个字典根据分类结果动态选。# 场景路由分类结果 - 检测模型 self.det_models { indoor: cv2.dnn.readNetFromONNX(fire_indoor.onnx), outdoor: cv2.dnn.readNetFromONNX(fire_outdoor.onnx), } self.cls_net cv2.dnn.readNetFromONNX(scene_cls.onnx) def pick_model(self, frame): blob cv2.dnn.blobFromImage(frame, 1/255.0, (224, 224), swapRBTrue) self.cls_net.setInput(blob) cls_out self.cls_net.forward() scene indoor if np.argmax(cls_out) 0 else outdoor return self.det_models[scene]这个方案的代价是要维护多个模型文件训练和标注工作量翻倍所以只适合场景差异确实很大、单模型精度死活上不去的项目。如果场景比较统一老老实实把单模型的数据集做扎实比堆模型更划算。验证这套系统是否真的可用我的习惯是拿一段没进过训练集的现场录像跑一遍人工数漏检和误报算一个「每千帧误报数」和「漏检率」。这两个数比 mAP 更能说明问题因为 mAP 是平均出来的现场关心的是最差情况。火焰检测这行模型精度只是及格线真正决定能不能交付的是误报控制和界面稳定性。我踩过最深的坑就是模型指标很漂亮结果现场因为一个夕阳误报被值班员投诉到项目差点黄掉。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑