简介:烟火检测数据集面向目标检测与YOLO系列模型训练,包含一千张真实场景图像及对应XML标注,适用于烟火识别、安全监控等视觉任务,也适合作为目标检测入门学习的练习数据。压缩包整体约89.87MB,共两千个文件,目录分为JPEGImages与Annotations两部分,前者存放一千张JPG原始图片,后者提供对应XML标注文件,可直接配合YOLOv3、YOLOv5等主流框架使用。已有三千余人学习下载,数据在社区中具有一定参考价值,也可作为入门或项目验证的样本集。标注以边界框形式标出烟火位置,并覆盖不同光照、背景和烟火类型,帮助模型学习形状、颜色、亮度等关键特征。配合数据增强与多尺度预测,可训练出在复杂环境下准确率较高的检测模型,为火灾预防提供可靠视觉支撑。
1. 烟火检测数据集:为什么1000张已标注图像是刚需
真正在产线上跑过烟火检测的人都有体会:模型结构再新,缺了高质量训练数据,效果也是空中楼阁。这个"烟火检测数据集1+1000IMG+已标注.zip"解决的问题很直接——你不需要自己扛着摄像头去化工园区、林场、仓库里蹲几个月采集画面,也不需要为了一张图里那个只有十几个像素的火苗拉几天标注框。它把1000张图像和对应的目标框打包在一起,类别覆盖火焰与烟雾两个核心对象,解压之后就能直接进入训练流程。对于算法工程师、消防物联网集成商,以及需要快速验证YOLO系模型在烟火场景表现的团队来说,这是把项目从零推进到第一版原型的最短路径。
2. 拿到ZIP先别急着训练:目录结构与标注格式解读
2.1 解压后的第一件事:确认images与labels的对应关系
我习惯把数据集解压到一个固定目录,比如/data/smoke_fire/,再用tree命令扫一眼整体结构。烟火检测数据集的打包方式多种多样,但绝大多数遵循图像目录与标注目录分离的原则。最常见的布局是images/与labels/同级,也有部分数据集沿用VOC的JPEGImages/加Annotations/结构。先别管是哪种,第一步一定是验证"每张图都有对应标注"这个基本事实。
unzip 烟火检测数据集1+1000IMG+已标注.zip -d /data/smoke_fire cd /data/smoke_fire find images -type f | wc -l find labels -type f | wc -l这段代码先解压,再分别统计图像和标注文件的数量。如果两边数字对不上,说明数据集里存在无标注的废图,这类图在训练时轻则浪费算力,重则让模型学到"无目标"的错误映射。我一般还会加一步,交叉验证文件名是否一一对应,把没有labels对应项的images挑出来直接删掉或移入unlabeled/目录,避免后续训练脚本报错。这一步看着简单,却是最容易埋雷的地方。
2.2 一眼识别标注格式:VOC、COCO与YOLO三种外观
烟火数据集的标注格式五花八门,常见的无外乎三种:VOC的XML、COCO的JSON,以及YOLO系的TXT。我拿到标注目录后,先看扩展名就能排除一半疑惑。.xml是VOC系,用<object><name>fire</name><bndbox>嵌套结构描述目标框;.json可能是COCO也有可能是LabelMe导出;.txt则大概率是YOLO格式,每行五个数字,依次是类别ID、中心点x、中心点y、框宽高,全部是相对图像的归一化坐标。表里列一下三种格式的关键差异。
| 格式 | 扩展名 | 坐标形式 | 类别表达 | 典型配套 |
|---|---|---|---|---|
| VOC | .xml | 绝对值(xmin, ymin, xmax, ymax) | 字符串类别名 | Pascal VOC |
| COCO | .json | 绝对值(x, y, w, h) | 数字类别ID+映射表 | COCO2017 |
| YOLO | .txt | 归一化(cx, cy, w, h) | 数字类别ID | Darknet/Ultralytics |
识别的核心逻辑是看坐标值是否落在0到1之间。YOLO的txt文件里所有数值必然在[0,1]区间,而VOC和COCO的绝对值坐标一般会大于1(取决于图像宽高)。还有一个细节:COCO的标注框宽高可能出现负值或包含分割多边形(segmentation字段),烟火检测数据集很少附带分割标注,遇到带多边形的大概率是COCO格式且做了实例分割扩展。
2.3 用一段脚本摸清类别分布与图像质量
拿到1000张图,我从不直接开训。先统计类别分布是烟火检测的必修课,因为火焰和烟雾在现实中出现的频率天然不对等——很多标注员习惯把图像里所有烟火都打框,但烟雾往往比火焰多几倍。如果smoke占80%而fire只有20%,训练出来的模型会天然偏向烟雾,对小型火灾初期的火苗反应迟钝。下面这段脚本可以快速统计YOLO格式标注的类别数量。
import os from collections import Counter label_dir = "/data/smoke_fire/labels" class_names = ["fire", "smoke"] # 按训练脚本里的类别顺序 total = Counter() for name in os.listdir(label_dir): if not name.endswith(".txt"): continue with open(os.path.join(label_dir, name)) as f: for line in f: cls_id = int(line.strip().split()[0]) total[class_names[cls_id]] += 1 print(f"共统计 {len(os.listdir(label_dir))} 个标注文件") for cls in class_names: print(f"{cls}: {total[cls]} 个目标框")逻辑很简单:遍历labels目录下所有txt文件,按空格切分每一行,取第一个字段作为类别ID,累加到计数器里。这里有个注意点,class_names列表的顺序必须和你后续训练配置里的names完全一致,否则统计结果和实际类别会对不上。跑完看到分布之后,如果一方占比超过70%,我会考虑在训练时给少样本类别加权重,或者用下文讲到的增强手段补样本,而不是直接硬训。
3. 把数据集喂进YOLO:格式转换、数据集划分与最小训练配置
3.1 从VOC或COCO转到YOLO格式:转换脚本与三个参数坑
绝大多数公开的烟火检测数据集都提供VOC或COCO格式,而YOLO系列训练要求的却是归一化txt。转换这一步绕不开,我直接给出一个从VOC XML转YOLO txt的Python脚本,这是烟火检测项目里最常复用的工具。
import xml.etree.ElementTree as ET import os def convert_voc_to_yolo(xml_path, out_path, classes): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in classes: continue # 跳过不在类别列表里的目标 cls_id = classes.index(name) bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # 把绝对值坐标转成YOLO的归一化中心点+宽高 cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) classes = ["fire", "smoke"] # 这个顺序就是训练时的类别ID xml_dir = "/data/smoke_fire/Annotations" out_dir = "/data/smoke_fire/labels" os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if xml_file.endswith(".xml"): convert_voc_to_yolo( os.path.join(xml_dir, xml_file), os.path.join(out_dir, xml_file.replace(".xml", ".txt")), classes )逻辑说明:解析XML拿到图像宽高,遍历每个object节点,读取bndbox下的四个坐标值,再转换成归一化的中心点、宽高组合。这里的三个坑必须提醒:第一,classes.index(name)会抛异常,如果XML里有不在列表里的类别名,必须先做if name not in classes过滤;第二,img_w和img_h必须从XML的size节点读取,不能自己猜固定值,否则宽高比不同的图转换后全部错位;第三,w和h不能加绝对值,虽然VOC坐标一般保证xmax > xmin,但COCO数据集偶尔有反框,转换前最好判断一下。
如果拿到的是COCO的JSON格式,转换逻辑也类似,只不过从annotations数组里遍历每个目标的bbox字段,注意COCO的bbox是[x, y, width, height]且坐标绝对值,除以图像宽高即可归一化。
3.2 按比例切分train/val并生成dataset.yaml
划分数据集的比例,我习惯用8:1:1,即800张训练、100张验证、100张测试。烟火检测场景下测试集尽量保留,因为你最终要做的是"没见过的火情"泛化验证,而不是在训练集上自嗨。切分脚本不建议直接用随机数,而是先random.shuffle再切片,这样保证每个子集里都有白天、夜晚、近景、远景的样本。
python -c " import os, random from shutil import copy2 imgs = os.listdir('/data/smoke_fire/images') random.seed(42) # 固定种子保证可复现 random.shuffle(imgs) n = len(imgs) train, val, test = imgs[:int(n*0.8)], imgs[int(n*0.8):int(n*0.9)], imgs[int(n*0.9):] for split, files in [('train', train), ('val', val), ('test', test)]: os.makedirs(f'/data/smoke_fire/{split}/images', exist_ok=True) os.makedirs(f'/data/smoke_fire/{split}/labels', exist_ok=True) for f in files: copy2(f'/data/smoke_fire/images/{f}', f'/data/smoke_fire/{split}/images/{f}') copy2(f'/data/smoke_fire/labels/{f.replace(\".jpg\", \".txt\")}', f'/data/smoke_fire/{split}/labels/{f.replace(\".jpg\", \".txt\")}') print(f'train={len(train)}, val={len(val)}, test={len(test)}') "这个脚本把图像和对应的txt标注按同一套随机顺序同步复制到新的划分目录。关键参数是seed=42,别小看这一行,没有固定随机种子,每次划分结果都不同,你复现实验时连baseline都对不上。另外还要确认图片后缀,烟火数据集里jpg、png、bmp混用很常见,上面脚本默认jpg,如果遇到png或webp,要么统一改后缀,要么在代码里用os.path.splitext处理扩展名。
划分完目录后,在/data/smoke_fire/下新建一个dataset.yaml,内容是训练和验证路径加类别名列表。
path: /data/smoke_fire train: train/images val: val/images test: test/images names: 0: fire 1: smoke nc: 2names的顺序必须和转换脚本里的classes顺序一致,这是YOLO训练最容易出的隐蔽错误——两个地方顺序不一致,模型不会报错,但loss会诡异地不降,你检查代码也看不出问题。nc是类别数,这里直接写2。
3.3 最小训练命令与三个必须改的超参
数据就绪之后,训练命令以YOLOv8为例,极简版本是下面这条。
yolo detect train data=/data/smoke_fire/dataset.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ device=0 \ patience=30逻辑说明:model=yolov8s.pt表示从COCO预训练权重开始微调,不是从头训练,这对只有800张训练图的烟火项目至关重要,迁移学习能让模型在冻结点上迅速收敛。patience=30是早停参数,如果验证集mAP连续30个epoch不涨就自动停止,防止过拟合也省时间。
三个必调参数里,第一个是imgsz,烟火目标往往小而远,640的输入尺寸对10像素以下的火苗基本无效,我建议先跑一版640看基线,再上960对比。第二个是batch,暴力设大没有意义,根据显存来,一般12G显存配batch=8到16,batch太小导致batchnorm统计不稳定。第三个是epochs,1000张规模的数据集160个epoch足够模型收敛,往上加只会过拟合。
4. 只有1000张图怎么够用:烟火场景的增强策略与扩充套路
4.1 火焰和烟雾的视觉特性决定了增强方向
通用目标检测在COCO上做增强那套方案,搬到烟火场景上效果会打折扣。火焰不是刚性物体,它没有固定边缘,颜色从内焰的蓝色到外焰的橙红色连续过渡,形状每秒都在变。烟雾更极端,半透明、低对比度、边缘几乎不可定义,经常与背景融为一体。这决定了烟火检测模型学习的核心不是"形状",而是"颜色分布 + 纹理模式 + 局部亮度突变"。增强策略也要围绕这三者展开,而不是像普通物体那样做大量几何变换。
明亮的场景下,模型容易把火焰理解成"红色高温物体",一旦夜晚光线变暗、火光颜色被环境光中和,同一个火焰在图像上的特征完全变了。所以在增强时,我格外看重亮度扰动和色彩抖动这两类,让模型见过尽量多的光照条件下的烟火。
4.2 在YOLOv8的增强参数里做文章:推荐配置表
YOLOv8原生支持一批数据增强超参,不需要额外写脚本就能直接调整。表里是我在烟火检测上反复试过的一组低配参数配置,直接写进训练命令或对应的yaml即可。
| 参数名 | 推荐值 | 作用 | 烟火场景的解释 |
|---|---|---|---|
| hsv_h | 0.015 | 色相扰动 | 让火焰颜色在橙红范围内小幅变化,模拟不同燃料燃烧的色差 |
| hsv_s | 0.7 | 饱和度扰动 | 压火苗的季节性差异,比如夏天阳光强导致颜色发白 |
| hsv_v | 0.5 | 明度扰动 | 模拟阴天、黄昏、夜晚的光照变化,对烟雾可见度影响巨大 |
| fliplr | 0.5 | 水平翻转 | 烟火目标没有左右语义,翻转等于白嫖一倍样本 |
| scale | 0.5 | 随机缩放 | 模拟摄像头在不同焦距下的目标大小,提升小目标鲁棒性 |
| mosaic | 1.0 | 四图拼接 | 强制模型在小目标的上下文中学习,解决远距离火情漏检 |
4.3 用albumentations做离线扩充:脚本与复检要点
在线增强有一个问题:它只作用于训练时的输入,验证集和测试集还是原始分布。如果火焰样本实在太少,我还是建议做一轮离线扩充。下面这段用albumentations实现亮度、对比度和随机裁剪的扩充脚本,可以把重点类别补到够用的程度。
import albumentations as A import cv2, os # 定义针对烟火场景的增强管线 transform = A.Compose([ A.RandomBrightnessContrast(brightness_limit=(-0.3, 0.3), contrast_limit=(-0.2, 0.2), p=0.9), A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=30, val_shift_limit=40, p=0.8), A.GaussNoise(var_limit=(10.0, 40.0), p=0.3), A.RandomScale(scale_limit=(-0.3, 0.3), p=0.5), ]) src_img_dir = "/data/smoke_fire/train/images" src_lbl_dir = "/data/smoke_fire/train/labels" out_img_dir = "/data/smoke_fire_aug/train/images" out_lbl_dir = "/data/smoke_fire_aug/train/labels" os.makedirs(out_img_dir, exist_ok=True) os.makedirs(out_lbl_dir, exist_ok=True) def read_yolo_txt(txt_path, img_w, img_h): boxes = [] with open(txt_path) as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) cx, cy, w, h = map(float, parts[1:]) # 归一化转像素坐标 boxes.append([cls_id, int((cx - w/2) * img_w), int((cy - h/2) * img_h), int((cx + w/2) * img_w), int((cy + h/2) * img_h)]) return boxes def write_yolo_txt(boxes, txt_path, img_w, img_h): with open(txt_path, "w") as f: for b in boxes: cls_id, xmin, ymin, xmax, ymax = b # 框可能被裁出图像外,先裁回来 xmin = max(0, min(xmin, img_w-1)) xmax = max(0, min(xmax, img_w-1)) ymin = max(0, min(ymin, img_h-1)) ymax = max(0, min(ymax, img_h-1)) if xmax <= xmin or ymax <= ymin: continue # 像素坐标再转回归一化 cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h f.write(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n") for fname in sorted(os.listdir(src_img_dir))[:]: img = cv2.imread(os.path.join(src_img_dir, fname)) if img is None: continue h, w = img.shape[:2] txt_path = os.path.join(src_lbl_dir, fname.replace(".jpg", ".txt")) boxes = read_yolo_txt(txt_path, w, h) # 对同一张图做3次不同增强 for i in range(3): transformed = transform(image=img, bboxes=boxes, class_labels=[b[0] for b in boxes]) t_img, t_boxes = transformed["image"], transformed["bboxes"] t_h, t_w = t_img.shape[:2] out_base = fname.replace(".jpg", f"_aug{i}.jpg") cv2.imwrite(os.path.join(out_img_dir, out_base), t_img) write_yolo_txt(t_boxes, os.path.join(out_lbl_dir, out_base.replace(".jpg", ".txt")), t_w, t_h) print("增强完成,输出目录:", out_img_dir)代码说明:先定义albumentations增强管线,RandomBrightnessContrast模拟不同光照强度下的烟火可见度,GaussNoise模拟监控摄像头在弱光环境下的噪点,这对夜间可燃气体火焰识别尤其关键。中间的辅助函数read_yolo_txt把归一化坐标转成像素坐标,交给albumentations处理后再由write_yolo_txt转回归一化格式。最后对每张原始图生成3张增强图,把总样本量从800训练图扩到3200张左右。
这里有个必须强调的参数细节:albumentations要求bboxes参数传入的是像素坐标的[xmin, ymin, xmax, ymax]列表,而且class_labels要和bboxes一一对应,少传一个就会报格式错误。输出时还要防止增强把目标框整个裁出画面,代码里做了xmax <= xmin的判断,遇到这种框要直接丢弃,不丢就会写出一条负宽高的非法标注,训练时损失函数直接给你算出NaN。
5. 烟火检测训练避坑指南:5个常见的翻车现场与排查方法
5.1 现象:mAP看着不错,小火苗和远烟全漏掉
训练完看验证集mAP能到0.8以上,以为万事大吉,真正拿一段园区实时画面去测,发现30米外的火苗和远景轻烟一个都检不出来。这类烟火目标只有20到30个像素,对下采样32倍的YOLO模型来说,经过多层卷积池化后特征几乎被抹平。
原因很直接:模型本身在下采样过程中丢失了小目标信息,与训练是否充分无关。解决上,我建议先把输入分辨率从640提到960,小目标的有效特征至少增加一倍。其次,在推理阶段用SAHI(Slicing Aided Hyper Inference)做切片推理,把大图切成512x512的重叠块分别检测再合并。最后,训练时单独把imgsz调大往往比换大模型更划算,因为烟火检测吃显存,换大模型容易OOM。
5.2 现象:一到夜间场景模型就变瞎子
白天测试一切正常,晚上监控画面一推流,FPS掉不下去但检测框全部消失。翻看训练集分布,发现800张train图里只有不到50张是夜间或弱光样本。烟火检测的数据集里,白天的占比天然偏高,这是采集成本决定的。
解决分两条路:一是用第4章的增强策略,把亮度下限调低,对原图做RandomBrightnessContrast之后扩充夜间样本占比;二是用图像处理手段把部分正常样本的亮度通道压低、叠加噪声,模拟红外夜视相机的均匀亮度画面。还有一种常见做法是引入红外光下的烟火图像作为额外训练数据,如果数据集里没这类图,可以用红外相机补拍几十张做微调。注意夜间增强不能把图像压成纯黑,否则模型学到的是"黑色背景=没有目标",不是真的学会识别暗光里的火苗。
5.3 现象:红色车尾灯、晚霞、红色霓虹灯被当成火焰
这个坑基本上是烟火检测的招牌问题。火焰在RGB空间里具有"高红、中绿、低蓝"的特征,而红色车灯和晚霞在颜色特征上和火焰高度相似。模型如果只在原图上学习颜色分布,而不关注纹理、闪烁频率和形状动态,误报几乎无法避免。
解决的关键是给模型提供足够多的难负样本(hard negative)。可以把背景图中出现的红色物体打上"背景"类别的标签,或者直接引入一批完全没有烟火但含有大量红色光源的负样本图像参与训练。另一个有效思路是使用background类别,让模型显式学习"这是红色但不是火"。如果数据集本身没有负样本目录,我建议从监控视频里抽帧,专门选晴天夕阳、车流密集的场景,手动标注为背景类。
5.4 现象:训练loss不降,验证集mAP来回震荡
训练跑了一百多个epoch,loss曲线像心电图,验证mAP在高位和低位之间反复横跳。检查模型结构没问题,超参也调过,问题大概率出在标注数据上。烟火数据集的标注质量参差不齐,标注员对半透明烟雾的边界判断标准不一致,同一个烟雾有的人框住整体,有的人只框了浓烟中心区域。
另外一个高频原因是标注坐标越界。有的烟火发生在图像边缘,标注框跑到图像之外,负值或超过宽高的坐标会在loss计算时产生异常梯度。解决方法是写一段脚本扫描所有txt标注文件,检查归一化坐标是否都在0~1之间,把越界的框直接裁回边界。如果裁回后宽高小于两三个像素,这个框就没有保留意义了,直接删除。这种脏数据清理比调任何超参都管用。
5.5 现象:模型FPS跑不满实时要求,监控流掉帧卡顿
一个烟火检测模型在GPU上推理速度能到200FPS,一上到8路监控视频流,服务器CPU立刻打满,画面开始掉帧。问题不出在模型推理本身,而在于多路视频流的解码、缩放、预处理没有管线化。烟火检测部署大多在边缘端或推理服务器上,运算瓶颈往往在图像缩放和归一化上。
常见做法是把图像的letterbox缩放、颜色通道转换、归一化操作全部放在GPU上做,用cv2.cuda或torchvision.transforms的张量操作替代CPU循环。其次,堆叠式检测场景建议用batch=4以上的批量推理,把多路画面拼成一个大张量一次过模型,吞吐量能提升三到五倍。真跑不满的时候,把模型从YOLOv8m降到YOLOv8n,或者导出TensorRT的FP16引擎,一般能拉回实时性,代价是mAP掉1到3个百分点,对烟火这种粗粒度检测来说通常可以接受。
6. 验证与进阶:用mAP、FPS和误报率判断模型能不能上场
6.1 混淆矩阵和PR曲线是烟火检测的照妖镜
YOLO训练完会在runs/detect/train/下生成一堆验证文件,我最先打开的不是results.png,而是confusion_matrix.png。在烟火检测里,这张图能直接告诉你模型哪里在"蒙"。看火焰类别那一行,如果大量真实框被预测成背景,说明召回率不足,小或者暗的火焰没学到;如果预测到火焰的框里有大量来自背景区域的点,说明误报严重。PR曲线的面积同样关键,烟火检测的P和R是此消彼长的关系,作为消防预警产品,我宁可牺牲一点precision换取recall,漏报的代价远高于误报。
6.2 用热力图看模型到底盯住了什么
跑通一版模型后,我会拿几张典型的火焰图像做梯度加权类激活图(Grad-CAM),观察模型关注的区域。如果热力图集中在火焰中心,而不是覆盖整个火焰区域甚至边缘的烟雾,说明模型学的是"局部亮点",这会导致火势蔓延后反而检测不到——因为大范围火焰的画面里,单点亮点的特征被稀释了。这时需要回去增强纹理信息,或者把标注框调整到包含完整的火焰边缘过渡区。
6.3 我压箱底的一个习惯
最后分享一个让我少被坑的习惯:每次训练前都把dataset.yaml里的names顺序、转换脚本里的classes顺序、验证脚本里的类别读取顺序,三个地方用同一份常量文件统一管理。烟火检测数据集本身不复杂,但项目一旦接手多人协作,最频繁翻车的永远是这类低级不一致,而不是模型结构。先跑10个epoch看看loss合理下降,再用真实视频流做一次端到端验证,确认检测延迟和误报率满足实际需求后,再回去调参和扩充数据。这套流程下来,基本不会出现训练一周才发现数据格式错了的惨剧。希望帮到你。
本文还有配套的精品资源,点击获取