
简介一套基于YOLOv8的火灾检测系统部署资源面向计算机视觉、智慧安防及火灾预警方向的开发者和课程学员可帮助快速掌握从模型推理到视频流检测的完整流程。压缩包共9个文件含三个Python脚本应用入口、配置与工具函数、预训练权重、依赖清单、环境说明及README文档整体约19.82MB轻量且边界清晰。已有247人学习下载。资源核心是一套可直接调用的YOLOv8模型与配套脚本读者能参照README完成环境配置、运行实时检测并根据config调节参数同时附带的工具函数与requirements也降低了复现门槛适合基于真实场景做二次训练或功能扩展。1. 火灾检测部署真正的门槛在数据与链路不在模型厂区摄像头二十四小时挂在天花板上真正着火前十几秒往往只是一缕灰烟或者角落里的一个亮斑人眼盯三块屏最多撑二十分钟就会漏。把已训练好的 yolov8 火焰检测权重放进真实监控链路让它连续读 RTSP 流、输出带类别的目标框、按规则触发报警这样的整合才是火灾检测部署的全部内容。它不要求你发明新算法难的是让模型在特定摄像头和光照条件下少误报、不漏报并保证推理服务持续在线。这篇笔记写给要在服务器或者 Jetson、RK3588 这类边缘盒子上把 yolov8 用起来的工程人员下面按数据、训练、导出、部署、排错一条线走完。2. 数据这关过不去yolov8火灾检测后面全是白搭数据集选择、清洗与标注边界部署效果的上限在数据不在模型。yolov8 本身只是一个把你的标注规律“背”下来的容器火灾检测这种目标外观高度依赖场景的视觉任务尤其明显同一团火焰在室内白墙上、在户外逆光里、在夜视黑白画面中特征完全不同。你喂进去的数据是什么样子部署现场就是什么样子。2.1 按真实监控视角选数据集而不是按“好看”选公开图像库里高质量火焰照片很多但它们大多来自摄影作品或者新闻配图火焰充满画面、侧光、背景干净。这类图统计上很好放到真实摄像头下几乎派不上用场因为监控画面里火焰往往只占几个像素或者几十个像素环境里还有大量反光、灯光、红色物体干扰。我一般按三条标准筛数据。第一条真实监控视角的样本要占主导公开图像只用来撑起始数量宁可画面暗一点、糊一点也不要全是影楼级清晰度。第二条小目标样本要单独数一遍火焰面积小于整图 2% 的帧至少要占三成否则训练出来的模型只会对“满屏都是火”的图像敏感部署时真正早期的火苗就漏了。第三条必须包含纯烟帧火灾早期多半先起烟烟雾和火焰要作为两类分别建模不能混成一个标签。有一个容易被忽略的点负样本。也就是完全没有火、但场景里存在大量红色物体的帧。没有这类数据模型会把红色工服、红色警示带、刹车灯当成火灾后面推理端的阈值怎么调都难救。2.2 把 VOC 标注转成 YOLO 格式一个能直接用的 Python 脚本大多数标注工具导出的是 VOC XML 格式而 yolov8 训练需要的是每个图像对应一个同名 txt每行表示一个目标框类别 id、中心点 x、中心点 y、框宽、框高后四个值都归一化到 0 到 1。这一步最简单也最容易出错我提供一个可以直接改路径运行的转换脚本。import os import xml.etree.ElementTree as ET # class_names 的顺序必须和后续训练用的 data.yaml 完全一致 class_names [fire, smoke] def voc2yolo(xml_path, image_width, image_height): 将单个 VOC XML 转为 YOLO txt 内容返回字符串列表 tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 归一化中心点除以图片宽高防止宽高都用了 width x_center ((xmin xmax) / 2) / image_width y_center ((ymin ymax) / 2) / image_height box_width (xmax - xmin) / image_width box_height (ymax - ymin) / image_height # 过滤掉 xmax 小于 xmin 的坏标注 if box_width 0 or box_height 0: continue lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) return lines def convert_folder(xml_dir, output_dir, width, height): os.makedirs(output_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue lines voc2yolo(os.path.join(xml_dir, xml_file), width, height) if not lines: continue txt_name os.path.splitext(xml_file)[0] .txt with open(os.path.join(output_dir, txt_name), w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: # 示例把某批次图片统一按宽度 1920、高度 1080 转换 convert_folder(xml_dir, labels_dir, 1920, 1080)脚本里有三个容易踩的地方。第一宽和高必须从原始图片实际尺寸读取不能想当然统一填一个值不同镜头裁切过比例不一样宽高都写错框会横竖全部偏移。第二类别顺序是全局约束脚本里的 class_names、训练用的 data.yaml、推理时的类别映射三处必须一致中途重新排序等于把所有标签重排了一遍。第三火焰和烟雾的标注框建议“保守”把火焰主体包住就好宁可框小一点也不要沿烟雾边缘画得太大框进太多环境背景会让模型学到周围的颜色而不是火焰本身。2.3 按镜头切数据集不做随机划分随机划分训练集和验证集是火灾检测里最常见的翻车操作。一条监控视频连续两小时的帧随机分成两半一半进了训练集一半进了验证集这两部分画面高度相似验证分数会漂亮得吓人但一到陌生摄像头就露馅。应该按镜头或者按视频片段来切每个镜头的帧要么全在训练集要么全在验证集。做完划分后我习惯跑一个统计脚本快速找出“孤图”和目标密度异常的样本。孤图指的是有图像文件但没有对应标签 txt或者反过来有 txt 没有图像的样本这类文件在训练时会被自动跳过看似不影响损失实际上会干扰你对样本量的判断。from pathlib import Path def check_dataset(image_dir, label_dir): images sorted(Path(image_dir).glob(*.jpg)) sorted(Path(image_dir).glob(*.png)) orphans [] object_counts {} for img in images: txt Path(label_dir) / (img.stem .txt) if not txt.exists(): orphans.append(img.name) continue count len(txt.read_text(encodingutf-8).splitlines()) object_counts[img.name] count print(f图片总数: {len(images)}) print(f缺标签孤图: {len(orphans)}) print(f单图最多目标数: {max(object_counts.values()) if object_counts else 0}) if __name__ __main__: check_dataset(images/train, labels/train)注意单图目标数如果一张 1080p 的监控画面里标了五六十个框多半是烟雾区域被切碎了模型去学碎块特征没有意义要回到标注环节把碎框合并成大框。这里顺手说一下增广yolov8 自带的翻转、尺度扰动已经够用不要为了“模拟更多光照”而把 HSV 色相扰动调得太狠否则模型会把红色灯箱和红色警示牌也当成火焰这个教训在后端阈值怎么调都拦不住。3. 用yolov8训练火灾检测模型从环境配置到损失函数曲线图的完整命令数据整理完训练本身反而是相对省心的一步。这里讲的不是研究型调参而是把数据集跑熟、看懂曲线、拿到一个可以进部署的权重。3.1 环境配置GTX1660Ti这种6G卡也能跑先把依赖配对训练环境的常见做法是用 conda 建独立环境避免把系统 Python 搅乱。GTX1660Ti 这类 6G 显存的卡完全可以跑关键是模型尺寸选 n 或 s不要一上来就选 l 或 x。显存不够时优先降 batch不要优先降 imgsz因为图片分辨率对火焰小目标的检测影响远大于 batch。conda create -n yolo python3.10 -y conda activate yolo pip install ultralytics onnxruntimeultralytics 会帮你装好配套的 PyTorch。如果机器是旧 NVIDIA 卡注意它默认装的可能是新版 CUDA 编译的 torch跑不起来时先查nvidia-smi驱动版本再按对应 CUDA 版本重装 torch这一步能省下后面半天排错时间。CPU 机器也能训练但一个 epoch 可能要跑很久建议先用小模型和少量 epoch 验证流程通不通再决定是否换 GPU。3.2 数据集 yaml 一次写对路径、类别顺序和名称训练前要给 ultralytics 一个数据集描述文件常见命名是fire.yaml。它只做三件事告诉训练器训练集和验证集在哪、类别有几类、每类叫什么。写错路径是最常见的报错来源路径尽量用绝对路径省得在 cwd 上折腾。# fire.yaml path: /data/fire_dataset # 数据集根目录绝对路径最可靠 train: images/train # 相对于 path 的训练集目录 val: images/val # 相对于 path 的验证集目录 names: 0: fire 1: smoke这里的names顺序就是第 2 章转换脚本里class_names的顺序必须完全一致。常见翻车是有人把0: smoke, 1: fire定义了标签文件里 0 却是 fire训练出来的类别就反了。yolov8 允许你写类别名但底层存的永远是class_id模型自己不管名字叫什么。3.3 训练命令与损失函数曲线图第一次训练怎么判断收敛第一次训练建议用最保守的一组参数跑通全流程同时打开 plots 让 ultralytics 自动绘制损失曲线和验证结果图。命令如下yolo detect train \ datafire.yaml \ modelyolov8n.pt \ epochs60 \ imgsz640 \ batch8 \ patience10 \ device0 \ projectrun_fire \ nameexp001 \ plotsTrue逐项说明参数modelyolov8n.pt是从 COCO 预训练权重迁移火灾检测属于小数据量场景迁移比从零训练明显收敛快imgsz640是保守值如果监控画面里火焰目标很小训练完看效果再往上加到 768 或 896代价是显存和推理耗时增加patience10表示连续 10 个 epoch 验证集没有提升就早停防止浪费时间plotsTrue会在run_fire/exp001/下生成results.png这就是经常被搜的损失函数曲线图里面包含train/box_loss、val/box_loss、val/cls_loss等子图。看曲线时注意三点。第一不要追求 loss 趋近于零那是过拟合信号看 val 曲线是否在下降后走平patience帮你自动停在最佳点附近。第二如果train/box_loss还在降、val/box_loss已经开始反弹说明过拟合了此时最优权重不是最后的best.pt而是之前某个 epoch训练产生的last.pt就是干这个用的可以做“后悔药”。第三第一次训练前 20 个 epoch 损失曲线剧烈抖动通常是lr或batch偏大6G 显存跑yolov8s时batch4更稳。3.4 训练完先看阈值的业务后果再谈模型结构很多人一上来就搜 yolov8 head 改进、网络结构图想靠改模型解决精度问题。在火灾检测部署里模型结构是最后一根救命稻草不是第一选择。正确顺序是先查数据 - 再调置信度阈值 - 再调输入分辨率 - 最后才考虑换更大模型或者改结构。这里有个部署前必须做的事给两类目标分别定阈值。训练时默认用 conf0.25但火灾报警的业务逻辑里漏报一次可能比误报十次更严重。常见做法是 fire 类保留 conf0.4smoke 类用 conf0.25因为烟雾边界模糊置信度天然偏低。如果你用 Netron 打开导出的 onnx 看网络结构会发现 yolov8 的输出是一个很大的矩阵选阈值本质是在这个矩阵里做取舍和模型改几层结构没有直接关系。类别不平衡可以靠 yaml 里的cls参数缓解比如cls: 2.0会让模型对少数类产生更大损失。但小样本下强行拉大会让整体损失被少数类带偏我一般只在样本比超过 5:1 时才加并且会盯验证集的曲线防止过拟合。4. 从模型到服务火灾检测部署的模型导出、推理API与报警联动训练完成只是第一步真正让 yolov8 跑在监控链路上需要过三关把权重转换成部署格式、包一个能对外提供推理能力的接口、把单路或多路视频流变成报警信号。4.1 导出前先做精度对齐把 ONNX 当成另一个黑匣子部署现场最常见的需求是导出 ONNX方便用 onnxruntime 或 TensorRT 推理。先执行导出命令yolo export modelrun_fire/exp001/weights/best.pt formatonnx dynamicTruedynamicTrue让 batch 和宽高维度动态变化方便同一份模型服务不同分辨率的摄像头画面。导出完成后不要急着写推理代码先用同一张图、同一个阈值把best.pt和best.onnx的输出对比一遍from ultralytics import YOLO pt_model YOLO(run_fire/exp001/weights/best.pt) onnx_model YOLO(run_fire/exp001/weights/best.onnx) img check.jpg result_pt pt_model(img, conf0.25, verboseFalse)[0] result_onnx onnx_model(img, conf0.25, verboseFalse)[0] print(pt 检出框数:, len(result_pt.boxes)) print(onnx 检出框数:, len(result_onnx.boxes))两者框数量不一致时先查预处理差异。YOLO 的 letterbox 在长宽比不常见的图上会做填充如果两个后端输入尺寸一致输出应该基本一致差一点点坐标可以接受差出几个框就要把输入的归一化方式拉齐。ONNX 对你来说是个黑匣子你不需要理解里面每个张量但输入输出的对齐规则必须搞清楚后面调后端格式才有参考。如果目标设备是 Jetson Orin常见做法是把 ONNX 再转成 TensorRT 引擎获取更大的吞吐量提升如果是 RK3588则需要走 RKNN 工具链并量化到 INT8。量化会掉一点精度我习惯用验证集分别跑 FP16 和 INT8对比 mAP 和每帧耗时再决定要不要量化。4.2 Flask 包一个最小推理服务先打通接口再谈并发部署形态上我一开始不直接写复杂的推流服务而是先用 Flask 包一个最小 HTTP 接口。它接收一张 base64 图片返回检测框列表。这个做法有两个目的先把模型推理服务质量钉死再让报警逻辑只管调用接口不耦合具体模型后端。from flask import Flask, request, jsonify import base64 import cv2 import numpy as np from ultralytics import YOLO app Flask(__name__) model YOLO(run_fire/exp001/weights/best.onnx) app.route(/detect, methods[POST]) def detect(): data request.get_json() if image not in data: return jsonify({ok: False, error: missing image}), 400 raw base64.b64decode(data[image]) img cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) results model.predict(img, conf0.4, imgsz640, verboseFalse) boxes [] for r in results: for box in r.boxes: boxes.append({ class: int(box.cls[0]), conf: round(float(box.conf[0]), 4), xyxy: [int(v) for v in box.xyxy[0].tolist()], }) return jsonify({ok: True, boxes: boxes}) if __name__ __main__: app.run(host0.0.0.0, port8000)model.predict内部包含缩放、letterbox、NMS 全部逻辑先用它跑通链路最省事。注意两点第一这个接口要部署在生产环境时不要用 Flask 自带的开发服务器常见做法是用 gunicorn 或多个 worker 顶着但每个 worker 都会复制一份模型权重进内存模型一大多个 worker 内存就爆实际部署我一般单进程内做多路 frame 队列第二imgsz640要和导出 ONNX 时的尺寸一致否则推理时发生隐式缩放坐标会整体偏。4.3 RTSP 拉流与报警联动去抖、冷却、断流重连三件事缺一不可接口就绪后接视频流。真实监控链路不是拿来一张张静态图推理而是连续拉流、逐帧判断并触发报警。这段代码几乎是火灾检测部署里最不能出错的逻辑。import cv2 import time import requests URL rtsp://user:passcamera_ip:554/stream def reconnect(): cap cv2.VideoCapture(URL) time.sleep(2) return cap cap cv2.VideoCapture(URL) fire_streak 0 cooldown_until 0 while True: ok, frame cap.read() if not ok: # 摄像头断流必须释放旧连接再重建不能原地重试 cap.release() cap reconnect() continue results model.predict(frame, conf0.4, imgsz640, verboseFalse) has_fire any(int(box.cls[0]) 0 for box in results[0].boxes) if has_fire: fire_streak 1 else: fire_streak 0 # 连续 3 帧检出才报警避免单帧抖动误报 if fire_streak 3 and time.time() cooldown_until: cooldown_until time.time() 60 # 冷却 60 秒防止重复轰炸 requests.post( http://your-alert-server/webhook, json{camera: cam-01, ts: time.time(), confidence: float(max(results[0].boxes.conf).item())}, timeout5 ) # 跳帧25fps 的流不要逐帧推理3 秒取一帧足够 time.sleep(0.1)代码里有三个经验点。第一fire_streak去抖单帧误报不计入连续三帧都有 fire 才确认报警这个机制能砍掉大半偶发误检。第二冷却时间必须加否则一个火源在画面里持续存在报警会以秒级频率狂发接入方一天就被打爆。冷却 60 秒意味着同一路摄像机一分钟最多告警一次人看起来也合理。第三断流重连不能只在read返回失败时重试有些摄像头在夜间切码流参数时连接不报错但画面永远卡住处理办法是在报警线程外再加一个看门狗超过 N 秒没读到新帧就强制重建连接。4.4 目标设备选型CPU服务器、Jetson Orin、RK3588 各自适合哪类现场部署时先想清楚要在哪里算。下面是按照我自己的现场经验整理的选型参考不是官方跑分具体能跑多少路必须以你的分辨率和帧率为准。设备形态适用规模常用导出格式部署建议CPU 服务器单路、低帧率验证ONNX / OpenVINO适合先跑通业务链路不适合多路并发GPU 服务器多路并发、高分辨率ONNX - TensorRT用队列合并多路请求注意显存占用Jetson Orin边缘盒、单点位多路TensorRT FP16适合现场就近推理减少视频上行带宽RK3588边缘盒、低功耗RKNN INT8/FP16量化前必跑验证集INT8 掉点要买账这里说一个常见误判RK3588 部署 yolov8 时有人直接把 ONNX 喂进 RKNN 工具链量化后 mAP 掉了五六个点还硬着头皮上线。正确的顺序是先量化少量验证集对比 INT8 和 FP16 的 mAP再决定。Jetson Orin 带 TensorRT 反而对模型结构适配度高默认 FP16 就能满足多数火灾检测场景。5. 部署避坑误报、掉帧、显存泄漏与夜间失效的4个排查记录部署阶段的坑大多不是模型本身的而是环境、习惯和调度带来的。下面四条都是实际踩过的记录按现象、原因、解决三个步骤展开。5.1 红色工服与警示带白天误报率最高的来源现象推理服务上线第一天还好第二天下午开始密集报警点开抓拍一看全是穿红色工服走过画面的工人和贴在柱子上的红色警示带。原因火焰的特征和红色物体高度重合。训练集里如果负样本不够模型学到的其实是“红色局部高亮”的纹理组合而不是火焰特有的边缘抖动和中心发白。置信度阈值调高可以缓解但会把真正的小火焰也压没了两难。解决最有效的是补负样本。从现场摄像头截取一整天的无火画面挑出里面有人、有红色物体的帧放到数据集里label 文件留空重新训练。yolov8 支持空标签图片参与训练模型会在这些图上把损失全部压在“没有目标”一侧。训练后如果还想压误报可以在推理端加一个区域过滤只在画面预设的火灾高风险区做报警监控画面上方天空区域直接忽略。5.2 显存只涨不降跑了三天就“假死”的推理服务现象服务刚启动时显存占用 1.2G跑了两天后占到 5.6G第三天推理变慢最终进程报 CUDA out of memory 挂掉。原因绝大多数情况是代码里有没释放的列表比如把每帧检测结果 append 到全局变量里做统计统计逻辑没写上限另一种是 onnxruntime 会话被反复创建每次创建都会持有一份显存上下文还有 gunicorn 多个 worker 同时跑每个 worker 各占一份模型显存叠加起来直接爆掉。解决先查全局列表检测结果只留最近 N 帧用固定长度的队列结构覆盖写。再查会话创建模型初始化放在进程启动时执行一次不要在请求循环里创建 session。最后加一层进程看门狗每天凌晨低峰期自动重启推理进程。显存泄漏这类问题允许你用重启兜底但不能只靠重启。5.3 一到夜间就断流昼夜切换带来的 RTSP 坑现象白天摄像头一切正常晚上六点半左右开始频繁重连某几个摄像头甚至完全拉不到流。原因很多监控摄像头在光线变化时会切换昼夜模式画面从彩色切黑白码流参数会变化部分相机在这个过程中会短暂断开 RTSP 连接。此时 OpenCV 的cap.read()返回 False如果代码没有正确的重连逻辑整个推理线程就死在断流上。解决断流重连要处理得比简单循环更细一点。连接失败时不立刻重连先等 2 到 5 秒避免在相机切换期间反复握手把设备搞得更不稳定。重连成功后跳过前 5 帧黑屏或花屏画面等图像亮度恢复正常再恢复推理。另外尽量用子码流做检测主码流留给录像子码流码率低断流概率小很多。5.4 多路视频 CPU 飙高解码与逐帧推理的成本陷阱现象从单路扩到四路视频后CPU 占用直接到 400%画面检测延迟到了 5 秒以上明显掉帧。原因OpenCV 默认是软解码四路 1080p 解出来已经吃掉了大量 CPU再加上每帧都做推理CPU 当然扛不住。实时监控里往往是解码成本大于推理成本这也是最容易被忽略的。解决按流水线拆开处理。拉流解码用一个线程池推理用一个独立进程帧数据通过队列交接不要在一个循环里边拉流边推理。压缩推理频率火灾蔓延速度不是毫秒级的每 3 秒推理一帧足够抓到早期火情。如果设备支持硬件解码用 GStreamer 后端重新编译 OpenCV或者干脆在边缘盒子上做推理视频流不进服务器。掉帧不是丢帧宁可少算几帧也不要把服务拖死。6. 验证与进阶用一段现场视频量化火灾检测部署效果部署上线不等于交付完成得想办法证明这套系统在现场真的可用。我会在报警接口前面加一个轻量录制模块把所有触发报警的帧连同推理结果存成 JSON 日志然后用来做两类统计误报率和延迟是否可接受。import json import glob total_alerts 0 true_alerts 0 conf_sum 0.0 for path in glob.glob(alerts/*.json): record json.load(open(path, encodingutf-8)) total_alerts 1 if record.get(confirmed) is True: # 人工回访后打的标记 true_alerts 1 conf_sum record.get(confidence, 0) print(f总告警数: {total_alerts}) print(f确认命中: {true_alerts}) print(f误报率: {(total_alerts - true_alerts) / total_alerts * 100:.1f}%) print(f命中平均置信度: {conf_sum / max(true_alerts, 1):.3f})把这个脚本跑一个周期你会看到比任何验证集都真实的数字。我习惯的做法是部署第一周不做告警即处置所有报警只记录每天人工回放一遍抓拍图给“确认命中”打标记。第一周结束后根据误报集中发生的时间段和场景再回头补数据或调阈值。这个步骤能直接把很多隐藏问题逼出来比如某个机位下午逆光时误报率高某个机位夜间纯烟帧全部漏检。进阶方向有两个可以顺手做。一是把告警加上区域坐标映射火焰框的中心点如果落在预设的风险区域内才推送紧急告警否则只标记不入库二是当模型误报集中在颜色特征时不要靠猜用热力图工具看看模型到底聚焦在图像的哪个区域多数时候你会发现它在看红色警示标而不是火焰本身。这个方法比反复调阈值更接近根因。说实话火灾检测部署里模型训练只占不到三成的工作量剩下的是数据和链路工程。我最早做第一版时也犯过“训练完直接上线”的错结果现场误报率高到被值班同事直接断电后来花了两周补负样本和重连逻辑才把系统救回来。从那以后我每次部署都会先跑一天现场日志再接入真报警。这套节奏你照着试能少走不少弯路。希望帮到你。本文还有配套的精品资源点击获取