
火灾火焰检测这几年在工业安全、消防预警、森林防护、社区安防里需求量一直很大但真正落地时很多开发者会发现一个尴尬问题模型训练出来是一回事能交给非技术用户使用是另一回事。很多人已经有了 YOLOv8 或 YOLOv5 的基础检测能力结果卡在界面封装上最后用命令行输出坐标和置信度给客户看体验很差。对于一个需要实际交付的火灾火焰检测识别系统最关键的不是单张图片测出 0.95 的置信度而是能不能形成一个从模型训练到可视化报警的完整闭环。这篇文章不会只讲训练一个 YOLO 模型就结束而是从技术选型、数据集准备、YOLOv5 与 YOLOv8 训练对比、PySide6 桌面应用构建到推理串接把“检测算法 GUI 程序”这个完整路径梳理清楚。我会直接给出可以参照的代码结构、关键实现思路和排错方法帮助你至少减少两个星期的弯路。1. 先判断一下这个系统的技术组合合理吗很多读者看到“YOLOv8 PySide6”这个组合第一反应是为什么不用 YOLOv8 的 Web 界面或者直接用 Flask 部署一个 HTTP 服务不是更简单吗这个疑问很合理但也正好反映了不同交付场景的差异。如果在服务器上做推理任务Flask 或 FastAPI 确实更工程化但如果你的使用对象是消防控制室值班人员、园区安保人员、或者一个只提供机房里一台 Windows 电脑的客户桌面客户端往往是更符合现场习惯的方案。PySide6 的价值在于可以在本地直接打开摄像头、读取视频文件、加载本地模型文件不依赖浏览器和网络数据隐私更好界面交互更接近传统安防软件用户学习成本低打包成 exe 后可以部署到没有 Python 环境的机器上。而同时使用 YOLOv5 和 YOLOv8本质上不是重复造轮子而是项目里常见的“双版本兼容”需求。很多时候你手上已有的数据集是用 YOLOv5 格式标注的早期模型也是 v5 训练出来的但你希望尝试 v8 的新结构、更强特征提取能力。如果系统只绑定一个版本升级成本会很高。因此一个合理的架构是数据层同一份火焰/烟雾标注数据集分别导出 v5 与 v8 格式 模型层YOLOv5 与 YOLOv8 两套权重可切换 推理层统一封装检测接口返回坐标、置信度、类别 界面层PySide6 负责视频/摄像头/图片展示、报警信息、参数设置这样做还有一个额外的好处算法改进与界面开发解耦。负责模型训练的成员可以单独迭代权重负责界面的成员只需要按照约定接口接入不需要关心模型内部结构。2. 核心概念YOLOv5、YOLOv8 与 PySide6 分别扮演什么角色2.1 YOLOv5稳定、成熟、生态完整YOLOv5 虽然不是官方 Ultralytics 系列中最新的版本但它在工业界的存量项目非常大。原因有三稳定、训练生态成熟、部署资料多。从网络结构上看YOLOv5 使用 CSPDarknet 作为骨干通过 PANet 结构做多尺度特征融合。对于火焰检测这种目标尺度变化很大的任务——火焰可能很小也可能占据整个画面——多尺度检测能力非常重要。YOLOv5 提供了 n/s/m/l/x 多个尺寸的模型你完全可以根据算力选择。对于火焰目标YOLOv5s 在中等算力设备上往往已经能达到不错的实时性。如果现场用的是 GTX 1660 Ti 级别的显卡训练 YOLOv5s 或者 YOLOv5m 是性价比比较高的选择。2.2 YOLOv8新结构、更强特征提取YOLOv8 是 Ultralytics 推出的系列模型相比 YOLOv5 有几个明显变化采用了 Anchor-Free 的检测头不再依赖预设 Anchor训练时对目标尺度变化更灵活C2f 模块替换了原来的 C3 模块梯度流动更丰富检测头使用 Decoupled Head分类与回归分支相互独立模型自带更完善的训练和导出生态ultralytics 包可以直接完成训练、验证、导出。从火焰检测任务来看YOLOv8 对小目标和中距离烟雾场景通常会有更好的表现但具体效果依然取决于数据集质量。不要盲目相信“新版本一定更好”实际项目里要做的是在同一个数据集上做对比实验谁在验证集上 mAP 更高、漏检更少就用谁。2.3 PySide6桌面应用的界面框架PySide6 是 Qt for Python 的官方绑定库基于 Qt 6。它和 PyQt6 功能几乎一致但许可证更友好在部分商业项目中更稳妥。在你的系统里PySide6 负责以下工作主窗口布局加载视频、显示检测画面、显示报警日志控件交互选择模型文件、调整置信度阈值、切换检测源多线程管理视频流读取和模型推理不能阻塞主界面摄像头调用通过 QCamera 或 OpenCV 读取摄像头帧。这里有一个常见误区很多人把 PySide6 当成“绘图工具”只用来画几个按钮和显示框然后把所有逻辑都写在同一线程里结果界面一卡一卡的。正确做法是在 PySide6 程序里通过 QThread 或 QTimer 配合工作线程处理推理任务。3. 环境准备与前置条件在开始写代码之前先把环境说清楚。这个系统的核心环境包括操作系统Windows 10/11 或 Ubuntu 20.04/22.04 Python建议 3.9 到 3.11 深度学习框架PyTorch版本以你的显卡驱动和 CUDA 版本为准 YOLOv8 依赖ultralytics YOLOv5 仓库独立的 git 仓库依赖 requirements.txt GUI 框架PySide6 其他依赖opencv-python、numpy、pillow3.1 创建独立的虚拟环境无论是训练还是做界面程序都强烈建议使用虚拟环境避免依赖污染。conda create -n fire_detect python3.10 -y conda activate fire_detect3.2 安装 PyTorch安装 PyTorch 时先确认你的 CUDA 版本。可以用nvidia-smi查看显卡驱动支持的 CUDA 版本。如果你不确认优先采用官方安装命令自动匹配。CPU 环境可以使用pip install torch torchvision --index-url https://download.pytorch.org/whl/cpuGPU 环境以 CUDA 11.8 为例pip install torch torchvision --index-url https://download.pytorch.org/whl/cu1183.3 安装 ultralytics 和 PySide6pip install ultralytics pip install pyside6 pip install opencv-pythonYOLOv5 是通过独立仓库方式使用的。建议单独克隆到你自己的项目目录下git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt这里要注意YOLOv5 项目和你的 PySide6 主程序不一定放在同一个目录。为了方便管理推荐目录结构如下fire_detection_system/ ├── yolov5/ # YOLOv5 官方仓库或精简版 ├── weights/ │ ├── fire_v5.pt │ └── fire_v8.pt ├── datasets/ │ └── FireDataset/ ├── app/ │ ├── main.py │ ├── detector.py │ ├── ui_main.py │ └── utils.py4. 数据集准备火焰检测系统成败的起点很多人做火灾检测系统上来就动模型这是最大的误区。火灾火焰检测不是标准目标检测数据集的典型场景公开数据集中火焰、烟雾样本与实际现场差异很大。建议你在准备数据时注意以下几点4.1 数据来源公开火灾数据集如一些论文公开的火焰/烟雾数据集自己拍摄或采集的火焰视频抽帧互联网搜索的火焰、烟雾图片现场安防摄像头的历史录像抽帧。4.2 数据标注标注格式取决于你最终采用哪个训练框架。YOLOv5 和 YOLOv8 在官方实现中都支持同一种 txt 标注格式即每行一个目标class_id x_center y_center width height这里的 x_center、y_center、width、height 都是归一化到 0 到 1 的坐标。你可以先用 LabelImg 或 LabelStudio 完成标注。标注时注意火焰区域有明显边界时框要贴合火焰主体烟雾比较分散时不要用一个大框覆盖整个烟雾区域这会引入大量背景噪音类别建议根据场景设计比如只分fire或者分fire和smoke两类同一个目标不要重复标注。4.3 数据集目录结构以 YOLOv5/v8 通用格式为例datasets/FireDataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml 示例# 文件路径datasets/FireDataset/data.yaml train: datasets/FireDataset/images/train val: datasets/FireDataset/images/val test: datasets/FireDataset/images/test nc: 2 names: [fire, smoke]注意yaml 文件里的路径取决于你执行训练命令时所在的目录。如果出现Dataset not found错误优先检查这里的相对路径或绝对路径是否正确。5. 模型训练YOLOv5 与 YOLOv8 的训练差异5.1 训练 YOLOv5进入 yolov5 目录后执行训练命令。cd yolov5 python train.py --img 640 --batch 16 --epochs 100 --data ../datasets/FireDataset/data.yaml --weights yolov5s.pt --device 0参数含义--img 640输入图片尺寸为 640x640--batch 16批大小根据显存调整--epochs 100训练轮数--weights yolov5s.pt预训练权重--device 0使用第一张 GPU。训练完成后权重保存在runs/train/exp/weights/best.pt。5.2 训练 YOLOv8YOLOv8 使用 ultralytics 包训练命令更简洁。yolo train modelyolov8s.pt data../datasets/FireDataset/data.yaml epochs100 batch16 imgsz640 device0训练完成后权重保存在runs/detect/train/weights/best.pt。5.3 两个版本训练的对比建议对比项YOLOv5YOLOv8配置复杂度相对高需要手动管理 repo低ultralytics 统一封装Anchor 机制Anchor-BasedAnchor-Free训练日志较多且分散日志简洁输出更友好老项目兼容性高视代码版本而定部署灵活性导出 ONNX/TensorRT 熟练同样支持工具链更新从实际工程角度说如果你要快速跑通一个监控项目并且希望后续长期维护YOLOv8 更方便如果现有项目已经基于 YOLOv5 开发或者你有大量旧版 v5 训练脚本继续用 v5 也没有问题。这里还要提一个常见问题火焰检测训练时类别不平衡。如果只分为fire和smoke而smoke样本远多于fire模型容易偏向预测 smoke。建议在训练时统计一下每个类别的数量如果差距过大可以先用一部分 smoke 样本做下采样或者给 loss 中不同类别设置权重。6. 模型推理与检测接口封装在你把模型接入 PySide6 之前先封装一个 detector.py这个文件的价值是让 GUI 逻辑只关心“调用 detect 方法得到结果”而不关心具体用的是 v5 还是 v8。6.1 统一检测接口设计# 文件路径app/detector.py import cv2 import numpy as np class BaseDetector: def __init__(self, weights_path: str, conf_thres: float 0.25): self.weights_path weights_path self.conf_thres conf_thres self.model None def load_model(self): raise NotImplementedError def detect(self, frame: np.ndarray): 输入: BGR 图像 返回: detections, 每项为 [x1, y1, x2, y2, score, class_id] raise NotImplementedError class YOLOv8Detector(BaseDetector): def load_model(self): from ultralytics import YOLO self.model YOLO(self.weights_path) def detect(self, frame: np.ndarray): results self.model.predict(frame, confself.conf_thres, verboseFalse) detections [] for r in results: boxes r.boxes if boxes is None: continue for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() score float(box.conf[0]) cls_id int(box.cls[0]) detections.append([x1, y1, x2, y2, score, cls_id]) return detections6.2 YOLOv5 推理封装# 文件路径app/detector_yolov5_wrapper.py import torch import cv2 class YOLOv5Detector: 假设你使用 yolov5 仓库的 models 模块。 这种方式适合将 v5 模型嵌入 PySide6 程序。 def __init__(self, weights_path: str, device: str cpu): self.weights_path weights_path self.device device self.model None def load_model(self): # 需要将 yolov5 仓库路径加入 sys.path import sys sys.path.append(yolov5) from models.experimental import attempt_load self.model attempt_load(self.weights_path, map_locationself.device) self.model.eval() def detect(self, frame): img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 这里简化预处理实际项目中注意 letterbox 操作 import torch from utils.augmentations import letterbox img, ratio, (dw, dh) letterbox(img, new_shape640, autoFalse) img img.transpose((2, 0, 1))[::-1] img np.ascontiguousarray(img) img_tensor torch.from_numpy(img).float() / 255.0 img_tensor img_tensor.unsqueeze(0).to(self.device) with torch.no_grad(): pred self.model(img_tensor)[0] # NMS 处理省略可使用 utils.general.non_max_suppression return pred注意YOLOv5 的预处理比 v8 复杂一些letterbox、BGR/RGB 转换、归一化都必须和训练时一致。如果你直接把原始 frame 喂进去结果通常会变差。6.3 为什么要封装一层封装的好处切换权重时只需要修改 detector_type 配置GUI 代码不必 import ultralytics 或 torch降低耦合方便后面加入 TensorRT、ONNX Runtime 等部署方式测试时可以直接对单个图像调用 detector不需要启动界面。7. PySide6 主程序与界面实现到了这一步你已经有了可用的推理模块。下面写一个基础但完整的 PySide6 主窗口包含按钮选择图片、选择视频、打开摄像头显示区QLabel 显示图像帧参数区置信度阈值 QDoubleSpinBox日志区QPlainTextEdit 显示检测时间和报警信息。7.1 主窗口代码# 文件路径app/main.py import sys import cv2 from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog, QDoubleSpinBox, QPlainTextEdit ) from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QImage, QPixmap from detector import YOLOv8Detector class DetectThread(QThread): frame_signal Signal(QImage) log_signal Signal(str) def __init__(self, detector, source, conf_thres0.25, parentNone): super().__init__(parent) self.detector detector self.source source self.conf_thres conf_thres self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running: ret, frame cap.read() if not ret: break # 更新置信度 self.detector.conf_thres self.conf_thres detections self.detector.detect(frame) # 绘制检测框 for det in detections: x1, y1, x2, y2, score, cls_id det cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) label f{self.detector.model.names[cls_id]} {score:.2f} cv2.putText(frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) if detections: self.log_signal.emit(f检测到 {len(detections)} 个目标, f最大置信度 {max(d[4] for d in detections):.2f}) # 转换为 QImage rgb_image cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w q_img QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.frame_signal.emit(q_img) cap.release() def stop(self): self.running False class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(火灾火焰检测识别系统) self.setMinimumSize(1000, 700) self.detector YOLOv8Detector(weights_path../weights/fire_v8.pt) self.detector.load_model() self.thread None self._init_ui() def _init_ui(self): central QWidget() self.setCentralWidget(central) main_layout QHBoxLayout(central) # 左侧显示区域 left_layout QVBoxLayout() self.video_label QLabel(请选择检测源) self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setStyleSheet(background-color: #1e1e1e; color: white;) self.video_label.setMinimumSize(640, 480) left_layout.addWidget(self.video_label) # 右侧控制区域 right_layout QVBoxLayout() right_layout.addWidget(QLabel(置信度阈值)) self.conf_spin QDoubleSpinBox() self.conf_spin.setRange(0.05, 0.95) self.conf_spin.setSingleStep(0.05) self.conf_spin.setValue(0.25) right_layout.addWidget(self.conf_spin) self.btn_image QPushButton(检测图片) self.btn_video QPushButton(检测视频) self.btn_camera QPushButton(打开摄像头) self.btn_stop QPushButton(停止) right_layout.addWidget(self.btn_image) right_layout.addWidget(self.btn_video) right_layout.addWidget(self.btn_camera) right_layout.addWidget(self.btn_stop) self.log_text QPlainTextEdit() self.log_text.setReadOnly(True) right_layout.addWidget(QLabel(检测日志)) right_layout.addWidget(self.log_text) main_layout.addLayout(left_layout, stretch3) main_layout.addLayout(right_layout, stretch1) # 信号连接 self.btn_image.clicked.connect(self.detect_image) self.btn_video.clicked.connect(self.detect_video) self.btn_camera.clicked.connect(self.detect_camera) self.btn_stop.clicked.connect(self.stop_detect) def detect_image(self): file_path, _ QFileDialog.getOpenFileName( self, 选择图片, , 图片文件 (*.jpg *.png *.bmp) ) if not file_path: return frame cv2.imread(file_path) detections self.detector.detect(frame) for det in detections: x1, y1, x2, y2, score, cls_id det cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) label f{self.detector.model.names[cls_id]} {score:.2f} cv2.putText(frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) self.show_frame(frame) self.log_text.appendPlainText(f图片检测完成共 {len(detections)} 个目标) def detect_video(self): file_path, _ QFileDialog.getOpenFileName( self, 选择视频, , 视频文件 (*.mp4 *.avi *.mov) ) if not file_path: return self.start_thread(file_path) def detect_camera(self): self.start_thread(0) def start_thread(self, source): if self.thread and self.thread.isRunning(): self.thread.stop() self.thread.wait() self.thread DetectThread( detectorself.detector, sourcesource, conf_thresself.conf_spin.value() ) self.thread.frame_signal.connect(self.show_frame) self.thread.log_signal.connect(self.log_text.appendPlainText) self.thread.start() def stop_detect(self): if self.thread and self.thread.isRunning(): self.thread.stop() self.thread.wait() self.log_text.appendPlainText(检测已停止) def show_frame(self, q_img): self.video_label.setPixmap(QPixmap.fromImage(q_img).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation )) if __name__ __main__: app QApplication(sys.argv) window MainWindow() window.show() sys.exit(app.exec())这个示例已经可以完成基本的图片、视频、摄像头检测。注意几个关键点DetectThread 继承 QThread视频流读取和模型推理都在子线程完成不会卡 UI每次循环都读取self.conf_thres所以界面调整阈值后能立即生效图片检测相对简单可以直接在主线程调用因为单帧推理耗时有限摄像头使用cv2.VideoCapture(0)如果电脑有多个摄像头可能要改成 1 或 2。7.2 摄像头打开失败排查PySide6 程序打开摄像头失败常见原因不是 PySide6 的问题而是 OpenCV 后端和摄像头驱动冲突。排查顺序单独运行一个 OpenCV 脚本测试cv2.VideoCapture(0)能否正常读取如果 OpenCV 正常再检查是否在 PySide6 程序之前打开了其他摄像头软件如果cv2.VideoCapture(0)返回 False尝试更换后端Windows 上可以用cv2.VideoCapture(0, cv2.CAP_DSHOW)Linux 上可以尝试cv2.VideoCapture(0, cv2.CAP_V4L2)。8. 火灾检测系统的常见问题与排查方法问题现象可能原因排查方式解决方案模型加载报错权重文件路径不对检查路径是否存在、是否有读写权限使用绝对路径或通过配置文件管理权重路径YOLOv8 推理没有检测框置信度阈值过高将 conf 降到 0.1 测试根据验证集结果调整阈值YOLOv5 推理结果异常预处理与训练不一致对比 letterbox 参数使用与训练相同的预处理函数摄像头画面黑屏摄像头被占用或驱动冲突重启摄像头、关闭其他软件改用 CAP_DSHOW/CAP_V4L2 后端PySide6 界面卡死推理放在主线程检查代码中是否在槽函数里执行耗时循环使用 QThread 处理视频和模型推理训练时显存不足batch 过大或图片尺寸过大观察显存占用降低 batch、降低 imgsz、使用梯度累积train/val 数据集找不到yaml 路径错误打印 yaml 内容检查使用项目根目录下的相对路径火焰和烟雾容易漏检数据集样本不足或标注不准统计类别数量、检查标注框质量补充数据、重新标注、增加负样本打包 exe 后模型加载失败路径被 PyInstaller 改变打印 sys._MEIPASS使用资源路径工具函数处理模型文件9. 工程化建议与生产环境注意事项9.1 训练与代码解耦把训练过程产生的权重文件单独放在 weights 目录并且不在 PySide6 代码中硬编码权重路径。推荐通过配置文件管理# 文件路径configs/detector_config.yaml model_type: yolov8 # 可选 yolov5 / yolov8 weights_path: weights/fire_v8.pt conf_thres: 0.25 iou_thres: 0.45 device: cpu这样切换 v5 和 v8 权重时不需要改代码。9.2 模型验证指标不要只盯着 mAP火灾检测更关心“漏检率”和“误报率”。漏检一次火焰后果很严重误报太多用户会关闭系统最终等于没有检测。因此在模型训练完成之后一定要在独立的测试视频上跑一遍记录火焰视频是否每一帧都检测到了普通生活场景中是否出现误报夜间、逆光、遮挡情况下检测效果如何。9.3 报警策略PySide6 程序检测到火焰后不要只画框建议加入声音报警弹窗提示截图保存调用第三方接口推送消息。报警逻辑不要放在模型推理循环里建议通过信号槽把报警事件发到单独线程处理。9.4 打包发布如果你需要交付给客户推荐使用 PyInstaller 打包pip install pyinstaller pyinstaller -w -F app/main.py但要注意-F生成单文件 exe启动慢但便于分发模型文件和配置文件不要打包进 exe放在外部目录方便替换如果打包后提示ultralytics相关模块缺失检查 PyInstaller 的 hidden-import。9.5 性能优化视频推理时使用cap.read()后先缩放到较小尺寸如果对延迟要求很高可以考虑把模型导出为 TensorRT 或 ONNXPySide6 界面中不要在检测线程里再次绘制大量系统字体减少 OpenCV 绘制开销多路摄像头场景使用多个 DetectThread 时注意模型线程安全问题。YOLOv8 的模型对象在多线程中调用 predict 不一定安全建议每个线程持有独立模型实例或者加锁。10. 总结与下一步实践建议这个火灾火焰检测识别系统核心不是某一行代码而是把“数据—训练—推理—界面”这条链路打通。YOLOv5 和 YOLOv8 各有适用场景PySide6 解决的是把模型能力变成用户可以操作的桌面工具而封装合理的检测接口让后续替换模型、增加摄像头、接入报警系统都变得容易很多。下一步你可以按这个顺序实践准备一份火焰/烟雾数据集先做小规模训练验证代码通路分别训练 YOLOv5s 与 YOLOv8s比较验证集指标运行上面给的 PySide6 主程序替换为自己的权重加入报警、截图、配置管理模块在真实视频或摄像头场景中做漏检率和误报率测试。做火灾检测项目与其追求浮夸的高精度指标不如把注意力放在一个稳定的检测流程和一套清晰可维护的代码结构上。数据质量、界面交互、报警准确性这三样才是最终交付时的核心竞争力。希望这篇文章能帮你减少前期排查问题的时间也欢迎在实践后根据你的场景改进检测逻辑和界面布局。