☰
烟雾检测比火焰检测更值得先做:YOLO11火灾检测数据集与训练模型实战
2026/10/11 15:59:53 网站建设 项目流程

简介:这份资源面向从事火灾检测与安全监控的算法工程师、安防开发者及计算机视觉学习者,提供基于ultralytics YOLO11的烟雾识别完整方案,用于在监控场景中及时发现火灾隐患。包内包含746张已标注图像,同时提供YOLO格式txt标签与VOC格式xml标签,并已划分train、val、test三个子集,附有data.yaml文件,可直接用于YOLOv5至YOLOv12等系列算法的训练与迁移。资源共2000个文件,以753个txt标注、746个xml标签、377个md说明文档、103个py脚本为主,另含少量cpp、js、yaml等工程文件,压缩包约222.28MB,兼顾数据、代码与文档。已有150人学习下载。读者可借此快速复现烟雾检测流程,理解数据组织与训练配置,并参考可视化思路完成从数据到推理的闭环实践。

1. 烟雾检测为什么比火焰检测更值得先做:从一次误报排查说起

凌晨两点,厂区值班室打电话过来说 3 号仓库的监控画面弹了火灾告警,人跑过去一看,是叉车倒车时扬起的粉尘被摄像头当成了烟雾。这件事让我彻底改变了对火灾检测项目的排序思路:火焰检测看起来更直观,但真正能在火灾隐患阶段救命的,是烟雾检测。火焰出现时往往已经进入燃烧阶段,留给处置的窗口可能只有几分钟;而阴燃、线缆过热、垃圾堆自燃这些场景,最早释放的信号是烟雾,可能提前十几分钟甚至更久。

ultralytics-yolo11 在火灾检测和安全监控中的定位,就是用目标检测模型对监控画面里的烟雾和火焰做实时识别,把「事后看录像」变成「事前弹告警」。它适合三类人:做园区/仓库/机房安防的集成商,想给现有摄像头加一层 AI 分析;做嵌入式边缘盒子的开发者,需要在有限算力上跑检测;以及想拿一个完整数据集加训练好的模型快速验证方案可行性的团队。标题里提到的数据集和训练好的模型,价值不在于「省事」,而在于让你能先跑通推理链路,再决定要不要针对自己的场景重新训练。

这一章先把方向立住:烟雾检测的核心难点不是模型结构,而是数据分布和误报控制。粉尘、水蒸气、逆光、夜间红外画面,都会让模型翻车。后面几章会从数据集组织、YOLO11 训练参数、推理部署到误报排查,一步步拆开讲。

2. 数据集怎么组织:烟雾火焰标注的四个关键决策

2.1 类别定义决定模型上限

很多人拿到火灾数据集第一反应是「烟雾、火焰两类就够了」,但实际落地时这个定义太粗。我一般会把类别拆成smoke、fire、smoke_like三类,第三类专门放粉尘、水蒸气、雾霾这些容易误报的负样本。如果数据集里没有smoke_like,模型就只能靠纹理和颜色硬分,夜间红外画面下几乎必翻车。

标注格式用 YOLO 标准的 txt,每行class_id x_center y_center width height,坐标归一化到 0-1。目录结构按 ultralytics 的约定来:

fire_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

data.yaml是训练入口,内容如下:

path: ./fire_dataset train: images/train val: images/val test: images/test nc: 3 names: 0: smoke 1: fire 2: smoke_like

这里nc必须和 names 数量一致,path用相对路径时要注意训练时的当前工作目录。我习惯用绝对路径,避免在服务器上跑训练时找不到数据。

2.2 训练集验证集划分的坑

按 8:1:1 随机划分是最常见的做法,但火灾场景不能纯随机。同一个视频片段抽出来的连续帧如果被分到训练集和验证集,验证指标会虚高,因为模型见过几乎一样的画面。正确做法是按视频源或时间段划分:比如 10 个摄像头,8 个的视频帧进训练,1 个进验证,1 个进测试。

另一个坑是负样本比例。如果数据集里全是烟雾火焰,模型会倾向于把任何灰色团块都判成烟雾。我一般让smoke_like类占训练集总量的 15% 到 25%,太少压不住误报,太多会拉低召回。这个比例没有理论最优,靠验证集上的误报率调。

2.3 数据增强参数怎么设

ultralytics 默认的增强对火灾场景有几个不合适的地方。hsv_h默认 0.015,但火焰颜色是重要特征,色相扰动太大会让红色火焰变成紫色,模型学不到真实分布。我一般把hsv_h降到 0.01,hsv_s和hsv_v保持默认或略高,因为监控画面亮度变化大。

mosaic增强默认开启,对烟雾检测有帮助,因为烟雾形状不规则,拼接能增加上下文多样性。但mixup我通常关掉,它会把两张图线性叠加,烟雾和火焰叠在一起后标注框语义混乱,小目标烟雾容易学坏。copy_paste对烟雾这种无固定形状的目标收益不稳定,建议先关,等基线跑通再试。

from ultralytics import YOLO model = YOLO("yolo11n.pt") model.train( data="fire_dataset/data.yaml", epochs=100, imgsz=640, batch=16, hsv_h=0.01, hsv_s=0.7, hsv_v=0.4, mosaic=1.0, mixup=0.0, copy_paste=0.0, degrees=5.0, translate=0.1, scale=0.5, fliplr=0.5, flipud=0.0, )

degrees控制旋转角度,监控摄像头一般水平安装,旋转超过 10 度会产生不真实的视角,设 5 度足够。flipud垂直翻转要关掉,因为火焰和烟雾在重力方向上有明确朝向,上下翻转不符合物理规律。scale设 0.5 是让目标在画面中大小变化,模拟远近不同的摄像头。

2.4 标注质量检查的脚本

标注框画歪、漏标、类别错是训练翻车的头号原因。我写了一个快速检查脚本,统计每个类别的框数量和尺寸分布:

import os from collections import Counter label_dir = "fire_dataset/labels/train" class_count = Counter() size_list = [] for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f"格式错误: {fname} -> {line}") continue cid, x, y, w, h = int(parts[0]), *map(float, parts[1:]) class_count[cid] += 1 size_list.append(w * h) print("类别分布:", class_count) if size_list: print(f"框面积均值: {sum(size_list)/len(size_list):.4f}") print(f"最小框面积: {min(size_list):.4f}")

如果某个类别数量不到总数的 5%,训练时会被其他类压制。如果最小框面积小于 0.001,说明有大量极小目标,YOLO11 在 640 输入下可能检测不到,需要考虑提高输入分辨率或单独处理。

3. YOLO11 训练参数怎么调:从基线到可用的三轮迭代

3.1 第一轮:用预训练权重跑通基线

不要一上来就改模型结构。先用yolo11n.pt或yolo11s.pt跑一个基线,确认数据管道和标注没问题。n 版本参数量小,训练快,适合验证流程;s 版本精度更高,适合最终部署。命令如下:

yolo detect train \ data=fire_dataset/data.yaml \ model=yolo11s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ project=runs/fire \ name=baseline

device=0指定第一块 GPU,多卡用device=0,1。project和name决定输出目录,权重、曲线、混淆矩阵都会存在runs/fire/baseline/下。第一轮重点看results.png里的mAP50-95曲线是否稳定上升,如果震荡剧烈,先把学习率降到 0.001 再试。

3.2 第二轮:针对烟雾小目标调输入和 anchor

烟雾在监控画面里往往只占几十个像素,640 输入下经过 32 倍下采样后只剩几个像素,特征几乎消失。两个办法:提高输入到 960 或 1280,或者用yolo11s配合更密集的检测头。提高输入分辨率最直接,但显存和推理耗时都会涨。我一般先试 960:

yolo detect train \ data=fire_dataset/data.yaml \ model=yolo11s.pt \ epochs=150 \ imgsz=960 \ batch=8 \ device=0 \ project=runs/fire \ name=img960

batch要相应降到 8 或 4,否则显存溢出。如果显存够,可以用batch=16加amp=True混合精度。YOLO11 的检测头对 anchor 的依赖比早期版本弱,但imgsz变化后最好重新跑一次yolo detect train让模型自适应,不要直接拿 640 的权重推理 960 的图。

3.3 第三轮:冻结 backbone 微调检测头

如果数据集只有几千张,从头训练容易过拟合。常见做法是冻结 backbone 前几层,只训练检测头。ultralytics 支持freeze参数:

yolo detect train \ data=fire_dataset/data.yaml \ model=yolo11s.pt \ epochs=80 \ imgsz=960 \ batch=8 \ freeze=10 \ lr0=0.0005 \ device=0 \ project=runs/fire \ name=freeze10

freeze=10表示冻结前 10 层,具体层数要看模型结构,YOLO11s 的 backbone 大约 10 到 12 层。lr0降到 0.0005,因为只微调头部,学习率太大会破坏预训练特征。这一轮重点看验证集上的误报率,如果smoke_like类被大量判成smoke,说明头部还没学好,可以增加smoke_like样本或提高该类损失权重。

3.4 关键参数对照表

参数基线值烟雾小目标建议说明
imgsz640960 或 1280提高小目标分辨率
batch168 或 4随 imgsz 增大而降低
lr00.010.001 到 0.0005微调时降低
freeze010小数据集防过拟合
mosaic1.01.0保持,增加上下文
mixup0.00.0火灾场景不建议开
hsv_h0.0150.01保护火焰颜色特征
fliplr0.50.5水平翻转合理
flipud0.00.0垂直翻转不合理

训练过程中如果val/box_loss持续上升而train/box_loss下降,是过拟合信号,优先加数据或开freeze,不要盲目加 epoch。

4. 推理部署与误报排查:模型上线后真正要盯的东西

4.1 推理脚本与置信度阈值

训练完的best.pt可以直接用 ultralytics 推理:

from ultralytics import YOLO model = YOLO("runs/fire/freeze10/weights/best.pt") results = model.predict( source="rtsp://camera_ip:554/stream", conf=0.35, iou=0.5, imgsz=960, stream=True, verbose=False, ) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) label = model.names[cls_id] if label in ("smoke", "fire") and conf > 0.5: print(f"告警: {label} 置信度 {conf:.2f}")

conf=0.35是推理时的低阈值,用于召回更多候选;业务告警再用conf > 0.5过滤。iou=0.5控制重叠框合并,烟雾框重叠多,可以适当降到 0.4。stream=True对视频流是必须的,否则会一次性加载所有帧导致内存爆掉。

4.2 误报排查清单

上线后误报比漏报更让人头疼,因为值班员会被折腾到不信任系统。按下面顺序排查:

第一,看误报画面的时间分布。如果集中在傍晚或夜间,大概率是红外切换或逆光导致颜色分布变化,需要补这类样本重新训练。

第二,看误报目标的类别。如果smoke_like被大量判成smoke,说明负样本不够或阈值太低,先把conf提到 0.6 观察。

第三,看检测框位置。如果框总是出现在画面边缘或固定区域,可能是摄像头脏污、雨滴或镜头光晕,这类问题模型解决不了,要清洁镜头或加遮挡掩膜。

第四,看连续帧。单帧误报可以靠多帧投票压下去:连续 5 帧里有 3 帧检测到烟雾才告警。这个逻辑在业务层做,不要塞进模型。

4.3 边缘设备部署的量化取舍

如果部署在 Jetson 或瑞芯微这类边缘盒子上,FP16 量化通常无损,INT8 量化需要校准集。校准集要从真实监控画面里抽,不能用训练集代替,否则量化后的分布偏移会让误报率上升。我一般先用 FP16 跑一周,统计推理耗时和误报率,再决定要不要上 INT8。

yolo export model=runs/fire/freeze10/weights/best.pt format=engine half=True device=0

format=engine导出 TensorRT,half=True开启 FP16。导出后要用同一批测试图对比 PyTorch 和 TensorRT 的输出,确认 mAP 下降不超过 1 个百分点。

5. 避坑与常见问题:五条血泪经验

现象:训练 loss 正常下降,但验证集 mAP 始终在 0.2 以下。原因:标注类别和data.yaml里的 names 顺序不一致,模型学的是错位标签。 解决:用脚本统计 labels 里出现的 class_id,和data.yaml的 names 逐一对齐,重点检查是否有 class_id 超出nc范围。

现象:模型在测试集上表现很好,上线后误报率极高。原因:测试集和训练集同源,没有覆盖真实摄像头的夜间、逆光、雨雾场景。 解决:从上线摄像头抽 200 张误报帧,人工标注后加入训练集,重新微调检测头。

现象:推理时显存溢出,报 CUDA out of memory。原因:imgsz提到 1280 后batch没降,或者stream=True没开导致视频帧全部加载。 解决:先降batch到 1 确认能跑,再逐步加;视频流必须开stream=True,图片批量推理用batch控制。

现象:烟雾检测框抖动严重,同一团烟雾在连续帧里时有时无。原因:单帧置信度在阈值附近波动,模型对烟雾边界不确定。 解决:业务层加多帧投票,连续 5 帧中 3 帧命中才告警;同时把iou降到 0.4 合并重叠框。

现象:导出 TensorRT 后精度掉了很多。原因:INT8 校准集用了训练集,分布和真实场景不一致;或者导出时imgsz和训练时不一致。 解决:校准集从真实监控画面抽,导出imgsz必须和训练一致,FP16 优先于 INT8。

6. 把误报压下去的一个具体技巧:负样本挖掘闭环

模型上线只是开始,真正让火灾检测可用的是负样本挖掘闭环。我的习惯是每周从告警记录里抽 50 张误报帧,人工确认后加入smoke_like类,用freeze=10微调 20 个 epoch,再推回线上。这个循环跑四周,误报率通常能降一个数量级。

具体操作上,先写一个从告警日志提取误报帧的脚本:

import cv2 import json with open("alerts.json") as f: alerts = json.load(f) for i, alert in enumerate(alerts): if alert["confirmed"] == "false_positive": cap = cv2.VideoCapture(alert["video_path"]) cap.set(cv2.CAP_PROP_POS_FRAMES, alert["frame_id"]) ret, frame = cap.read() if ret: cv2.imwrite(f"hard_neg/{i:04d}.jpg", frame) cap.release()

alerts.json里记录每次告警的视频路径、帧号和人工确认结果。抽出来的图放进hard_neg/,标注时全部标成smoke_like,然后合并进训练集。微调命令和第三轮一样,但epochs降到 20,lr0用 0.0003,避免破坏已经学好的烟雾特征。

验证闭环是否有效,看两个指标:一是每周误报总数是否下降,二是smoke_like类的召回是否上升。如果误报降了但烟雾漏报也涨了,说明负样本加太多,把smoke_like比例回调到 15% 左右。这个平衡点每个场景不一样,只能靠数据说话。

我自己的习惯是每次微调后保留旧权重,新权重先影子模式跑三天,对比两版模型的告警差异,确认没有引入新的系统性误报再切换。这套流程不复杂,但坚持下来比换任何模型结构都管用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询