简介:一份面向火焰烟雾检测的YOLO数据集资源,图片经过人工挑选与标注,场景覆盖广泛,可直接作为通用模板数据集用于模型训练,也可按需加入特定场景样本,适合深度学习与人工智能方向的开发者及研究人员。压缩包共332个文件,整体仅32.18MB,内容以yaml配置文件、Python脚本、C与CUDA源码、Darknet相关文件及模型权重(pt)为主,并附有少量示例图片和视频。其中yaml与cfg文件负责网络结构和训练参数的定义,py与sh脚本承担数据准备、训练及推理流程,c与cu源码便于二次修改暗网框架,pt权重支持加载即用,jpg与mp4则可直观查看标注效果,目录层次清晰,便于按模块检索。资源包当前已有2017人学习下载,轻量而完整。实际使用时,可省去收集、挑选、标注火焰烟雾图片的大量时间,直接基于模板数据集开展工程化训练;针对特定场景,只需补充少量场景数据即可完成精准检测,非常适合入门实践与项目原型验证。
1. 一个 zip 文件,装的是火焰烟雾检测模型的训练原料
做消防预警或工业安防的工程师,手头大概率收过这类文件:一个叫“火焰烟雾数据集YOLO.zip”的压缩包,几十个文件夹、几千张 JPEG 图片密密麻麻的 txt 标注文件。这不是给人看的相册,是一份能直接喂给 YOLO 系列模型的监督学习数据集。它的价值在于:火焰和烟雾的样本被统一整理成了 YOLO 格式的标签——每张图对应一个同名的 txt,里面记录每个目标的类别、中心点坐标和宽高。用它对 YOLOv5、YOLOv8 做训练或微调,可以做出一个能识别早期火情、在 1080P 视频流上实时告警的检测模型。合适的人群是正在做烟火识别项目、有训练环境但缺少干净数据的算法工程师,或者想用现成数据跑通 YOLO 训练流程的入门者。这个 zip 不是终点,只是原料——真正花时间的不是解压,而是把数据整理到能开训的状态。
2. 解开 zip 之后别急着训练:先核对目录结构与 YOLO 标注格式
2.1 为什么火焰烟雾检测不能照搬通用目标检测的思路
车辆、行人这类目标有相对稳定的几何结构,车窗、轮胎、四肢的位置关系可以用锚框大致描述。火焰和烟雾不一样,它们几乎算得上目标检测里最不守规矩的一类:火焰边缘时刻在抖动,形状从一个小火苗到铺满整面墙都可能,颜色也不是单纯的橙红色——高温核心偏白,外焰偏黄,环境光和曝光会让色相跑得更远。烟雾更是半透明、无固定轮廓,浓烟和薄雾在画面上呈现的纹理完全不同,背景里的树影、灯光、蒸汽都可能被误认成烟。
YOLO 的检测方式是把图像划分成网格,用预设锚框回归目标的中心点和宽高。对一个“没有固定形状”的目标,锚框的宽高比设定往往不生效,模型更多靠学习目标与背景的纹理差异来定位。所以训练烟火模型时,数据质量直接影响收敛速度,尤其是标注框边界是否紧贴目标。如果框大了一圈,烟雾的半透明区域会和背景混在一起,模型学到的特征是“一片模糊的区域”,而不是真正的烟。这个本质决定了后面所有数据整理动作的优先级:宁可样本量少一点,也要保证框紧贴目标、类别标签准确。
2.2 解压后第一件事:核对 images 与 labels 的对应关系
常见的数据集打包结构是images/和labels/两个顶层目录,里面按场景或者采集批次分子目录,图片和标注文件名一一对应。先别急着写训练脚本,我拿到 zip 后的第一个动作永远是解压并统计文件数量,先把目录结构摸清楚。
# 解压,注意保留目录结构 unzip 火焰烟雾数据集YOLO.zip -d ./fire_smoke_dataset # 进入数据集根目录 cd fire_smoke_dataset # 统计图片数量和标注数量 find images -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" | wc -l find labels -name "*.txt" | wc -l # 随机抽取一个样本,核对同名文件是否存在 ls images/2023_siteA/img_0042.jpg ls labels/2023_siteA/img_0042.txt这里值得留意的是find命令中的条件组合。-name "*.jpg" -o -name "*.jpeg"这一串不加括号的话,find按从左到右的顺序执行,-o会打破默认的-a优先级,可能导致统计漏掉某些格式。稳妥的写法是加括号:find images \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | wc -l。文件数量对得上只说明没有被截断,先记下来,后面还要抽样核对内容。
2.3 YOLO 格式的五个字段与容易看走眼的归一化问题
打开一个 txt 标注文件,每行是一个目标,格式是class_id cx cy w h,这五个字段都是空格分隔的浮点数。cx和cy是该目标中心点相对于图片宽高的比例坐标,w和h是目标框宽高相对于图片尺寸的比例。也就是说,即使图片是 1920x1080,所有数值也都在 0 到 1 之间。
# 读一个标注文件,检查数值范围是否合法 with open("labels/2023_siteA/img_0042.txt", "r") as f: lines = [line.strip().split() for line in f.readlines()] for i, line in enumerate(lines): cls_id, cx, cy, w, h = float(line[0]), float(line[1]), float(line[2]), float(line[3]), float(line[4]) # 计算左上右下角点,检查是否超出图片边界 x1, y1 = cx - w / 2.0, cy - h / 2.0 x2, y2 = cx + w / 2.0, cy + h / 2.0 if x1 < 0 or y1 < 0 or x2 > 1 or y2 > 1: print(f"第{i}行坐标越界: {line}")用这个脚本跑一遍数据,常见的不规范情况有两类:一类是w或h大于 1,这通常是从 VOC 格式转换时忘了除以图片宽高;另一类是中心点坐标超出 0 到 1 区间,比如标注工具导出的像素坐标被当成归一化坐标直接用。出现这类问题不能靠训练时裁剪蒙混过关,裁剪会把目标的真实尺度改变,导致框的位置和实际目标对不上。遇到这种标签,最稳妥的做法是找到原始标注重新生成,或者直接剔除这一条。我见过很多训练到一半 loss 突然跳动的情况,最后定位到就是个别越界标注在梯度里贡献了错误信号。
3. 把 zip 整理成能直接开训的样子:数据清洗与 train/val 划分
3.1 用脚本自动划分训练集和验证集,按场景切而不是随机切
很多人习惯用train_test_split随机打乱全部图片然后按 8:2 切分。这个做法在通用目标检测里能凑合用,但火焰烟雾数据集不行。烟火数据往往按场景采集:同一个摄像头、同一个光照条件、同一个角度的画面会产生大量极其相似的帧。这些帧随机分到训练集和验证集之后,验证集的画面和训练集太像,跑出来的 mAP 虚高。部署到新的现场,光线一变、视角一变,精度立刻掉下来。
正确做法是按“场景”切分,而不是按“文件”切分。比如数据集里是siteA、siteB、siteC这样的目录结构,那就按目录划分:
import os import random random.seed(42) IMAGES_DIR = "fire_smoke_dataset/images" LABELS_DIR = "fire_smoke_dataset/labels" # 每个场景目录作为切分单位 scenes = [d for d in os.listdir(IMAGES_DIR) if os.path.isdir(os.path.join(IMAGES_DIR, d))] random.shuffle(scenes) val_scenes = set(scenes[: max(1, len(scenes) // 5)]) # 20%的场景做验证 train_files, val_files = [], [] for scene in scenes: scene_imgs = os.listdir(os.path.join(IMAGES_DIR, scene)) for img_name in scene_imgs: if not img_name.endswith((".jpg", ".jpeg", ".png")): continue label_name = os.path.splitext(img_name)[0] + ".txt" # 只收留那些有对应标注文件的样本 if os.path.isfile(os.path.join(LABELS_DIR, scene, label_name)): path_line = f"images/{scene}/{img_name}" if scene in val_scenes: val_files.append(path_line) else: train_files.append(path_line) with open("train.txt", "w") as f: f.write("\n".join(train_files)) with open("val.txt", "w") as f: f.write("\n".join(val_files)) print(f"train: {len(train_files)}, val: {len(val_files)}")切分时只统计有对应标签的图片,因为后面会专门做一轮清洗。如果某个场景只有几幅图,切到验证集里会导致验证集样本太少,我的习惯是整个数据集样本不足 1000 张时,只切 10% 做验证;超过 5000 张再回到 20%。这个比例不是死规矩,核心是保证验证集场景种类和训练集不重叠。
3.2 清理坏图、空标注和超过边界的框
zip 包上传下载过程容易丢文件,数据集的来源也可能决定它的质量参差不齐:有的图片本身是损坏的 JPEG,有的标注文件是 0 字节,有的标签类别编号和约定文本对不上。这些脏数据不清理,训练过程会频繁报错,即使不报错,也会在算 loss 时给模型错误引导。
from PIL import Image import os BAD_IMAGES = [] BAD_LABELS = [] # 第一步:剔除损坏图片 for split_file in ["train.txt", "val.txt"]: with open(split_file, "r") as f: lines = [line.strip() for line in f.readlines()] for line in lines: img_path = f"fire_smoke_dataset/{line}" try: img = Image.open(img_path) img.load() except (IOError, OSError, SyntaxError): BAD_IMAGES.append(line) print(f"损坏图片: {len(BAD_IMAGES)}") # 第二步:剔除空标注和越界标注 for split_file in ["train.txt", "val.txt"]: with open(split_file, "r") as f: lines = [line.strip() for line in f.readlines()] for line in lines: label_path = line.replace("images/", "labels/").replace( os.path.splitext(line.split("/")[-1])[1], ".txt" ) if not os.path.isfile(f"fire_smoke_dataset/{label_path}"): BAD_LABELS.append(line) continue with open(f"fire_smoke_dataset/{label_path}", "r") as f: content = f.read().strip() if not content: BAD_LABELS.append(line)坏图直接删掉,空标注的图如果没有其他目标,也建议删除。注意一个细节:with open(...) as f: f.read()在 Windows 上如果文件编码是 GBK,读取时可能出问题,标注文件是纯数字不会有这个坑,但如果标签文件里混了中文注释就需要用encoding="utf-8"指定。
3.3 写好 data.yaml,设置类别顺序与预训练权重下载
YOLO 训练时需要一个 data.yaml 文件告诉框架数据在哪、有几个类别。这个文件写错是最常见的翻车点,尤其是类别列表顺序和标注文件里的class_id对不上。比如 txt 里写0是火焰、1是烟雾,data.yaml 里names列表的第一个元素就必须是 flame,第二个是 smoke。
# data.yaml,路径用相对路径,配合 data_root 参数使用 train: train.txt val: val.txt # 类别数量 nc: 2 # 类别名称,顺序必须和标注里的 class_id 一一对应 names: 0: flame 1: smoke写的时候注意第一行的train和val指向的 txt 文件里,每行都是相对数据集根的图片路径。做熟练之后,我会直接用yolo detect train data=data.yaml model=yolov8s.pt一行命令开训,框架会自动从官方地址下载预训练权重。但下载慢是常态,建议在命令行把model指定成已经下载好的本地权重文件路径,比如models/yolov8s.pt,省掉每次重复下载的时间。预训练权重对火焰烟雾这类小数据集特别重要,从零训练一个检测头很难收敛,用 COCO 上训好的骨架做初始化,一般 50 轮就能看到可用的结果。
4. 火焰烟雾数据集的训练参数:图像尺寸、batch 与损失函数怎么调
4.1 图像尺寸:火焰目标的尺度分布决定要不要上 1080
YOLOv8 默认用 640x640 的输入分辨率。这个尺寸对通用目标检测是性价比平衡点,但对火焰烟雾场景往往不够。原因很直白:远处摄像头画面里,一个初期火苗可能只有 20x30 像素,缩到 640 分辨率后只剩十几个像素,特征图上一个锚框都覆盖不住。反观近景画面,火焰可能铺满半屏。同一个数据集里目标尺度差异可以大到 100 倍,这是烟火识别特别容易漏检小目标的结构性原因。
我的常见做法是先做一次标注框尺寸统计:
import os import numpy as np sizes = [] for root, _, files in os.walk("fire_smoke_dataset/labels"): for f in files: with open(os.path.join(root, f), "r") as fh: for line in fh: parts = line.strip().split() if len(parts) == 5: w, h = float(parts[3]), float(parts[4]) sizes.append((w, h)) sizes = np.array(sizes) print("框宽中位数:", np.median(sizes[:, 0])) print("框高中位数:", np.median(sizes[:, 1])) print("小目标占比(w<0.05):", (sizes[:, 0] < 0.05).mean())如果小目标占比超过 20%,我的做法是训练时直接上imgsz=960。代价是显存占用变成原来的 2.25 倍左右,V100 这种 32G 显存的卡可以轻松撑住 batch 32;在 16G 的卡上就要把 batch 降到 8 甚至 4,配合梯度累积来补偿。反过来,如果数据集中火焰目标基本都是中近景大目标,640 就够用,硬上 960 只会拖慢训练速度,精度收益很有限。
4.2 batch、学习率与预热轮数的失配问题
batch size 调大时,学习率不跟着调会出现一个典型现象:loss 在前几个 epoch 快速下降的很漂亮,到了第 30 轮左右突然开始震荡,然后一发不可收拾。这是因为学习率过大,梯度更新跨过了 loss 曲面的凹谷。YOLOv8 默认学习率是 0.01,但这个值是针对 batch 16 设定的。用更大 batch 时,线性放缩学习率到 0.02 或 0.025 可以,但要注意同时把 warmup 轮数加长,让模型在开头几个 epoch 用小学习率先把骨架参数稳住。
# 在 V100 32G 上,用 960 分辨率训练火焰烟雾模型 yolo detect train \ data=data.yaml \ model=models/yolov8s.pt \ imgsz=960 \ batch=24 \ lr0=0.015 \ warmup_epochs=5 \ epochs=150 \ weight_decay=0.0005 \ device=0这里warmup_epochs=5的意思是前 5 轮学习率从零逐步升到设定值。火焰烟雾数据的标注噪声普遍比 COCO 大(因为目标边界本身就模糊,不同的标注员画出来的框可能差 5% 到 10%),给模型一个“慢热期”能显著降低后期震荡的概率。weight_decay保持默认 0.0005 就行,这个值主要防止过拟合,数据量越小时越重要。
4.3 损失函数曲线怎么读:不要迷信“loss 越低越好”
YOLOv8 的损失由三部分组成:box 回归损失、分类损失、特征图分布损失。训练日志里显示的box_loss、cls_loss、dfe_loss是这三分量的加权和。刚开训时box_loss下降最快,这是预训练骨架在发挥作用;cls_loss则往往先跌后涨再跌,因为分类边界在早期不稳定。看到这个别慌,它不是训练发散。
有一个经常被忽略的判断方法:单独看val/box_loss和val/cls_loss。如果训练集 loss 一直在降、验证集 loss 在 60 轮之后不再下降甚至回升,那就是过拟合的信号,而不是继续加轮数的理由。数据量少于 2000 张时,150 轮足够,再多就是在背训练集。火焰烟雾数据本身类间方差大、类内方差更大,train loss 降到 0.02 以下基本说明模型在死记图片而不是学习泛化特征,此时回归到 100 轮左右反而部署效果更好。
5. 火焰烟雾模型训练与部署的避坑清单:从空 txt 到推理漏报
5.1 解压后 txt 全部变成 0 字节,训练全程没学到任何目标
现象:labels目录下所有 txt 文件都只有 0 字节,训练日志里cls_loss从第 1 轮开始就是 0,说明模型把整张图都当成了背景,检测结果一直是空的。
原因:这类数据集 zip 是从网盘或者微信传输下载的,压缩包本身用了伪加密或者传输中断后被工具强制补全了文件头。用解压软件强制解压后,图片能打开,但部分 txt 文件实际内容是残缺的。更隐蔽的一种情况是,旧的解压工具对中文文件名编码处理错误,t以乱码文件名被解压出来,但内容在,只是find找错了位置。
解决:解压后用文件数量和平均文件大小做双重校验。正常标注 txt 文件至少有几十字节,如果一批文件全是 0 字节,说明数据流有问题。用 7-Zip 重新解压一次,如果解压过程提示“数据错误”,那就是压缩包本身损坏,只能联系源数据的提供方重新获取。平时在 zip 包里看到txt文件名带乱码时,先用ls -b看一眼真实文件名,再决定是写脚本批量重命名还是换解压参数。
5.2 火焰和烟雾被合并成同一个标注类别,验证集 mAP 虚高
现象:训练完成后验证集 mAP 达到 0.85,拿到现场一测,模型把灯光、红色卡车、傍晚的红霞都识别成火焰,把树影、蒸汽识别成烟雾,基本没法用。
原因:数据集里标注员为了省事,把所有火焰和烟雾都标成同一个类别fire。模型学到的不是一个稳定的语义概念,而是“颜色偏亮的区域”或“边缘模糊的团块”。在验证集上精度好,是因为验证集的图片和训练集来自同一个场景、同样的环境背景,模型记住了背景,没有学会目标。
解决:打开标注文件统计一下类别分布。发现所有标注的 class_id 都是 0 时,就要考虑重新将数据拆成flame和smoke两类。如果原始 zip 里已经有flame和smoke两个子目录,可以写脚本按目录重新分配 class_id。如果数据集本身就没区分,宁可少留一半数据,也不要强制合并:把明显是火焰的标注归为类别 0,明显是烟雾的归为类别 1,界限模糊的直接删掉。烟雾检测本来就是高误报场景,类别定义不干净,后续所有调参都是在错误地基上施工。
5.3 训练到一半 loss 变成 NaN,BN 层参数爆炸
现象:训练一切正常,到第 42 轮时 loss 突然变成nan,重启训练后到第 6 轮又复现,而且在同一轮附近。
原因:这是 YOLO 训练里经典的“BN 崩溃”。特征图进入批归一化层时,如果某个 batch 内的激活值方差过大,归一化输出的均值和方差参数会朝极端方向更新,随后整个网络被“带崩”。触发因素往往是学习率偏大、batch size 过小(比如只有 4 个样本时 BN 统计量噪声太大),或数据集里有极端像素值的图片——比如一张 99% 像素都是纯白色的烟雾过曝图。
解决:先看是不是特定图片触发的。把训练中断时正在处理的数据切片拿出来,检查像素值分布,发现过曝或纯黑图后直接删除。如果删除无效,把学习率从 0.01 降到 0.005,batch从 4 提到 8 或 16。还有一种治本的方法是冻结骨干网络的 BN 层参数,只对检测头做微调,这在数据集比较脏的时候效果显著,但训练速度会变慢。顺着日志找到第一个出现 NaN 的 epoch,对比该轮的数据增强配置,排查大概率在数据本身。
5.4 验证集 AP 很高,但用视频流测试时连续漏报
现象:验证集 mAP 0.88,火焰召回率 0.92,满怀信心接到 RTSP 视频流里跑,发现烟雾完全是断断续续的:有烟的那几秒不报警,画面里烟散去后反而报一次,延迟超过 20 秒。
原因:验证集里都是挑选过的清晰帧,而现场视频有运动模糊、低照度、压缩伪影。火焰烟雾检测对时序连续性有天然要求,单帧检测在烟刚出现的第 1 到 2 秒内目标特征弱,容易低于置信度阈值。另一个主要原因是验证集指标看的是mAP@0.5,它统计的是“框位置对的概率”,而不是“每一帧都报出来的概率”。
解决:部署时把置信度阈值从默认的 0.25 降到 0.1,同时在推理代码里做帧间平滑:连续 3 帧中至少 2 帧检测到火焰或烟雾才触发告警。这个策略能压掉大部分误报,同时保住早期火情的敏感度。另外,数据集的验证集要加一批视频抽帧数据,哪怕只有 200 帧,测出来的召回率才接近真实场景。测试视频里的漏报概率,比验证集 AP 更能说明模型能不能用。
5.5 zip 压缩包内文件带中文名或空格,训练时图片路径全部失效
现象:所有图片和标注文件路径在 Windows 下正常,传到 Linux 服务器后训练报错,指向FileNotFoundError,但文件确实存在。
原因:zip 打包在 Windows 上用默认的“发送到压缩文件夹”生成,中文文件名使用了 GBK 编码。Linux 的 unzip 默认按 UTF-8 解压,导致解压出来的文件名是一串乱码占位符,路径对不上;还有的文件名里带了空格,YAML 解析路径时把空格当成了分隔符。
解决:解压时指定编码,unzip在较新版本支持-O参数,但有兼容性问题,7-Zip 更稳,一条7z x 火焰烟雾数据集YOLO.zip就能按正确的编码解出中文名。更根治的做法是解压后统一把所有文件名改成英文加下划线命名,避免后续在 C++ 或 RKNN 部署时再遇到编码问题。动手前先file 图片名称.jpg看一眼文件编码,批量重命名的脚本比手工一个个改省太多时间。
6. 把模型搬到现场:导出 ONNX、在 RK3588 上跑实时推理与混淆矩阵分析
6.1 导出前先看混淆矩阵,确认火焰和烟雾有没有互相打架
训练结束后先看一眼runs/detect/train/confusion_matrix.png。这张图能直接反映两个类别的互检情况。火焰和烟雾在视觉上确实有重叠区域:火焰周围往往伴随烟气,标注员在边界区域可能给出不一致的标签。如果混淆矩阵显示flame被预测成smoke的比例超过 10%,说明这两个类在特征空间里靠得太近。此时要么增加细颗粒度的样本,要么考虑将分类损失权重调高,让模型更侧重区分两类的语义差异,而不是只做存在性检测。
我的习惯是在调参前先看这张图。很多工程师拿到模型就只盯着 mAP,但 mAP 是多个类别的平均,完全掩盖了类间混淆。确认完混淆矩阵再决定要不要导出部署,可以减少返工。
6.2 导出 ONNX 与部署端输入尺寸必须一致的“三不原则”
模型训练完,导出 ONNX 是部署到边缘设备的前提,尤其是 RK3588 这类带有 NPU 的国产板卡。核心原则是:训练时的imgsz参数必须和导出时的输入尺寸保持完全一致,否则模型在板卡上的输出会偏离预期。
# 导出固定尺寸的 ONNX 文件,opset 用 12 在 RKNN 里兼容性最好 yolo export model=runs/detect/train/weights/best.pt format=onnx opset=12 imgsz=960 # 用 onnxruntime 做一次纯 CPU 推理验证,确认输出不是 NaN python -c " import onnxruntime as ort import numpy as np sess = ort.InferenceSession('best.onnx') x = np.random.rand(1, 3, 960, 960).astype(np.float32) out = sess.run(None, {'images': x})[0] assert not np.isnan(out).any(), '输出包含 NaN,导出或训练阶段有异常' print('ONNX 输出正常,shape:', out.shape) "之后在 RK3588 上用 rknn-toolkit2 将 ONNX 转成 RKNN 格式。这个转换过程本身是黑匣子,我们能控制的就是输入尺寸、归一化通道顺序和数据布局。导出端用了imgsz=960,转换端就一定要设置 960,而不是为了跑分顺手调成 640——这个改动会让部署效果断崖式下降,不是线性损失,而是检测头感受野和训练时完全错位。
整套流程走下来,我现在拿到任何数据集 zip,第一件事永远是解压、抽查标注、看类别分布,而不是急着开训。数据集的坑分布在前 20% 的工作量里,却决定了后面 80% 的训练结果。希望帮到你。
本文还有配套的精品资源,点击获取