简介:面向计算机视觉开发者和火灾预警系统研究者的YOLO火焰与烟雾检测数据集,专注解决不同光照、遮挡和复杂背景条件下火焰与烟雾目标识别难题。资源包共1444个文件,包含482个txt标注文件、481张jpg原始图像和481个xml标注文件,整体压缩包大小约3.93MB。txt与xml分别对应YOLO和PASCAL VOC两种主流标注格式,每张图片中的火焰和烟雾位置均经过精确边界框标注,可直接用于YOLOv3、YOLOv4等模型训练,也便于迁移到TensorFlow、PyTorch或Darknet等框架。已有2780人学习下载,适合需要数据增强、迁移学习或基准测试的火焰烟雾检测任务。开发者可对照图片与边界框快速完成训练验证,借助双格式标注灵活切换预处理流程;针对小样本还可通过旋转、缩放、翻转等增强手段提升模型泛化能力,为火灾预警与安防监控提供可靠的数据支撑。
1. YOLO 火焰和烟雾检测数据集:一个小样本、双标注的目标检测包
我拆过不少目标检测数据集,看到fire_smoke-dataset.zip的第一反应是:这包比那些动辄几万张的 COCO 子集“寒酸”很多,但它恰好是火灾预警场景最缺的那种素材。全量接近五百张图片,每张都带火焰和烟雾的标注框,而且同时给了 PASCAL VOC 和 YOLO 两种格式——一个适合人看、方便画框调试,一个适合直接喂给训练脚本。如果你正在做安全监控、厂区防火这类项目,或者踩着 yolov8 训练自己的数据集的路子跑毕设,这包能省掉你三天以上的标注时间。
但它不是解压就能直接训练的。小样本、火焰烟雾形态多变、双格式标注带来的坐标换算问题,都会在训练和验证环节冒出来。这篇就按真实拆包的顺序,把文件结构、格式转换、训练参数和最容易翻车的几个坑过一遍。
2. 拆开 fire_smoke-dataset.zip:目录结构、XML 与 txt 怎么对应
2.1 先看清三件套:图片、XML、txt 各自管什么
解压后第一件事不是急着训练,而是把目录角色分清楚。绝大多数公开检测数据集都沿用“图片目录 + 标注目录”的组织方式,这个包也不意外,常见结构是images/放 jpg,Annotations/放 VOC 格式的 XML,labels/放 YOLO 格式的 txt。
我习惯先把结构固定好,再写检查脚本。原因很直接:后续不管是转格式还是做数据增强,都依赖文件名的强一致性。图片 299.jpg,对应的标注就必须是 299.xml 和 299.txt,名字错一个字符,训练时就会静默跳过这张图,最后发现 mAP 缺了一截还找不到原因。
| 目录/文件 | 格式 | 承担的角色 |
|---|---|---|
| images/ | .jpg | 原始图像,数字命名 |
| Annotations/ | .xml | PASCAL VOC 标注,含 object 标签、边界框像素坐标 |
| labels/ | .txt | YOLO 标注,每行一个目标:类别ID + 归一化中心点 + 归一化宽高 |
图片命名是纯数字序号,没有额外前缀,这点对我这种要批量处理的人很友好。后面做数据集合并或者按比例划分训练/验证集时,直接按数字取模就行,不用处理乱七八糟的字符串。
2.2 两种标注格式,读起来思路完全不同
VOC 的 XML 是给人看的。它把图片尺寸、每个目标的类别和边界框绝对坐标全部用标签包起来,我贴一个典型片段:
<annotation> <filename>299.jpg</filename> <size> <width>1280</width> <height>720</height> </size> <object> <name>fire</name> <bndbox> <xmin>400</xmin> <ymin>150</ymin> <xmax>520</xmax> <ymax>260</ymax> </bndbox> </object> </annotation>参数含义很直白:name是类别名,bndbox里四个值分别是左上角 x、左上角 y、右下角 x、右下角 y,单位是像素。这种格式的优势是直观,画框调试、人工审核都方便,OpenCV 直接拿着坐标往上画就行,不需要任何换算。
YOLO 的 txt 则完全不同。每行一个目标,五个值全是浮点数:类别 ID、归一化后的中心点 x、中心点 y、归一化宽度、归一化高度。归一化的意思是除以图片宽高,所以火焰在图片左上角的话,cx、cy 都会比较小。好处是训练时不管输入图怎么缩放,标注都跟着比例走,不会错位;代价是拿眼睛看 txt 根本看不出框在哪。
0 0.359375 0.284722 0.093750 0.152778有经验的开发者看到这种文件会先确认一件事:类别 ID 0 到底是 fire 还是 smoke?这个包的 XML 里写的是fire,txt 里第一列是 0,那么 0 就代表 fire。这个对应关系必须写进训练配置,不能凭感觉猜。
2.3 先跑一遍完整性检查,确认图片和标注没有“缺对”
我拆数据集的第一步永远是写完整性检查脚本。目标有三:确认每张图片都有同名 XML 和 txt;确认 txt 文件不为空;确认 XML 里的类别都在预期集合里。
import os import glob images = glob.glob('images/*.jpg') expected_classes = {'fire', 'smoke'} for img_path in images: stem = os.path.splitext(os.path.basename(img_path))[0] voc_path = f'Annotations/{stem}.xml' yolo_path = f'labels/{stem}.txt' if not os.path.exists(voc_path): print(f'[MISS-VOC] {img_path}') if not os.path.exists(yolo_path): print(f'[MISS-YOLO] {img_path}') if os.path.exists(yolo_path) and os.path.getsize(yolo_path) == 0: print(f'[EMPTY-YOLO] {yolo_path}')这段脚本的逻辑是:先拿到所有 jpg 的文件名主干,再拼出预期的 XML 和 txt 路径,逐个检查存在性和非空性,最后把问题行打印出来。参数上唯一要动的地方是expected_classes,如果这个数据集未来加了第三类,比如“灯”,就得扩展这个集合。实际使用中我会把这套检查做成所有数据集通用的脚本,换目录名就能跑,因为踩过一次“图片多、标注少”的坑之后,就再也不敢跳过这步了。
3. 把 VOC 标注转成 YOLO 格式:坐标换算与三条校验规则
3.1 为什么建议统一成 YOLO 格式
如果只想用 YOLO 训练,理论上可以只读 txt,忽略 XML。但实际拆包时你会发现,标注格式转换是躲不掉的:一是有些脚本、可视化工具只认 VOC;二是如果后续要扩展成 yolo 实例分割,还得把边界框转成多边形标签。而训练端是绝对的 YOLO 格式主导,因为模型训练时读的就是 txt,省去解析 XML 的开销。
转换的核心是坐标映射。VOC 里的绝对像素坐标除以图片尺寸,就得到归一化的 YOLO 坐标。公式很简单,但坑藏在细节里:XML 里的<size>不一定和实际图片一致。如果有人在标注后压缩了图片,XML 里还留着旧尺寸,换算出的框就会整体偏移。所以转换脚本里必须用 PIL 重新读一次图片真实宽高,而不是信 XML 里的数字。
3.2 VOC 转 YOLO:写一个不会丢框的转换脚本
下面这个脚本我做过很多次,换成哪个数据集都能用。核心思路是:遍历所有 XML,解析每个 object 的类别和边界框,按公式换算,写入同名 txt。
import os import glob import xml.etree.ElementTree as ET from PIL import Image CLASS_MAP = {'fire': 0, 'smoke': 1} # 类别名 -> ID def convert_voc_to_yolo(xml_path, img_path, out_dir): img = Image.open(img_path) img_w, img_h = img.size tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in CLASS_MAP: print(f'[SKIP-UNKNOWN] {xml_path} -> {name}') continue bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) center_x = ((xmin + xmax) / 2) / img_w center_y = ((ymin + ymax) / 2) / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h lines.append( f'{CLASS_MAP[name]} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}' ) stem = os.path.splitext(os.path.basename(xml_path))[0] out_path = os.path.join(out_dir, stem + '.txt') with open(out_path, 'w') as f: f.write('\n'.join(lines) + '\n') # 用法示例 # convert_voc_to_yolo('Annotations/299.xml', 'images/299.jpg', 'labels/')逻辑说明:CLASS_MAP在代码里写死了类别映射,fire 是 0,smoke 是 1,这必须和训练时data.yaml里的类别顺序一致。坐标换算部分用了两次除法,第一次算中心点和宽高的像素值,第二次除图片宽高得到归一化值。.6f保留六位小数,足够表达亚像素精度,也不会让文件膨胀。
参数需要注意两个点。第一,img_w、img_h是从 PIL 读的真实尺寸,不是 XML 里的<size>,这个细节决定转换后框准不准。第二,xmin到xmax如果出现负数或者反序,说明标注本身有问题,最好在脚本里加个判断,打印哪张图哪条框违规,而不是静默写出一个负数坐标。
3.3 转换后必须做的三个校验
转换脚本跑完不等于数据能训练。我每次都要再跑一段校验脚本,检查三点:
第一,坐标值必须在 0 到 1 之间。YOLO 格式是归一化的,如果出现大于 1 的 cx 或 w,直接说明换算除错了数。第二,中心点必须落在图片范围内——某种意义上这是冗余检查,因为归一化后不可能超界,但真出现过由脏数据导致的坐标 1.000001。第三,类别 ID 不能超过总类别数减一,也就是不能出现 ID 大于 1 的行。
def validate_yolo_file(txt_path, num_classes=2): with open(txt_path) as f: for line_idx, line in enumerate(f, 1): parts = line.strip().split() cls_id = int(parts[0]) vals = [float(v) for v in parts[1:]] if cls_id >= num_classes: print(f'[BAD-CLASS] {txt_path}:{line_idx}') if any(v < 0 or v > 1 for v in vals): print(f'[OUT-OF-RANGE] {txt_path}:{line_idx}')这个脚本的num_classes=2对应 fire 和 smoke 两类。如果后续加类,别忘记同步改这里。校验跑完没有输出,才说明这批标注从数字上看是正常的。
4. 用 YOLOv8 训练自己的火焰烟雾模型:data.yaml、增强与关键参数
4.1 写好 data.yaml:路径、类别顺序、验证集划分
训练前第一步是把数据描述文件写好。YOLOv8 的data.yaml直接决定训练集、验证集路径和类别列表,写错一个路径,训练直接报错;写错类别顺序,模型训练时把 0 当 fire、1 当 smoke,看起来能跑,实际预测全乱。
path: ./fire_smoke_dataset train: images/train val: images/val nc: 2 names: 0: fire 1: smoke参数说明:path是数据集根目录的绝对或相对路径,train和val是相对于path的子目录。nc是类别总数,必须和names里的数量一致。类别顺序尤其关键,0 永远是 fire,1 永远是 smoke,和上一章转换脚本里的CLASS_MAP保持一致。
数据集小,我一般不会手动建 train/val 子目录,而是用脚本按 8:2 或 9:1 比例随机划分。火焰烟雾场景里漏检代价高,验证集稍微多留一点反而踏实。
mkdir -p fire_smoke_dataset/images/train fire_smoke_dataset/images/val mkdir -p fire_smoke_dataset/labels/train fire_smoke_dataset/labels/val ls images/ | awk 'NR % 5 != 0' | xargs -I {} mv images/{} images/train/这段命令的逻辑是:先建四个目录,然后用awk每隔 5 个文件名保留一个到 train,其余靠mv移走。百分比可以自己调,NR % 5 != 0是每 5 个取 4 个到 train,也就是 80% 训练、20% 验证。要注意的是 labels 也要做一模一样的划分,否则图片在 train 里、标注在 val 里,训练时一张图也加载不出来。
4.2 训练参数:小数据集原本就过拟合,先看这几项
近五百张图放在目标检测里属于小样本,训练参数和那些几万张的数据集完全不同。我习惯用 YOLOv8 的 n 模型起步,把显存留给更大的输入尺寸。
yolo train \ data=fire_smoke.yaml \ model=yolov8n.pt \ epochs=200 \ imgsz=1024 \ batch=16 \ patience=50 \ augment=True \ mosaic=0.5 \ degrees=5 \ hsv_h=0.02参数说明:epochs=200对于小数据集来说不多,因为模型要反复看这些图到熟;imgsz=1024是这一串参数里最关键的一个——火焰往往是画面里的小目标,输入太小会直接抹掉小火焰的细节;mosaic=0.5让模型每轮有一半概率看到马赛克拼接图,能显著缓解小样本的过拟合;degrees=5允许图片旋转正负 5 度,模拟摄像头安装角度偏差;hsv_h=0.02是色调扰动。
这些增强参数在火焰烟雾场景里要讲究。烟雾的颜色从白到灰到黑都有,hsv_h稍微加大一点,模型更容易学到“颜色变化但形状不变”的烟雾本质。而degrees不能给太大,火灾检测场景里倒置或横滚的图片不符合实际监控视角,转多了反而制造无效样本。
4.3 用迁移学习,但要把预训练类别“忘记”到合理程度
数据量小,从头训练几乎必挂,所以要用预训练权重yolov8n.pt做初始化。这一步是在 COCO 上预训练过的模型基础上微调,模型已经会提取通用特征,火焰的边缘、烟雾的纹理这些低层信息可以复用,只需要在高层学习“什么是火、什么是烟”。
参数上唯一推荐动的是检测头的学习率。YOLOv8 里可以通过lr0和lrf控制初始学习率和最终学习率,小数据集我一般把lr0降到 0.005 左右,微调时步子小一点,避免把预训练权重冲坏。如果你的 NVIDIA 显卡小于 8GB,batch降到 8,imgsz降到 640,否则还没跑到第一个 epoch 就 OOM 了。
4.4 评估指标:火焰烟雾更要盯召回率
训练完看验证结果,mAP50、mAP50-95、precision、recall 四个值要一起看。其中最重要的其实是召回率。火焰检测漏报的代价远远大于误报——漏掉一个真实火源可能酿成大祸,而误报最多就是触发一次假警报。
YOLOv8 的验证输出默认会打印每个类别的 AP,这时候重点看 smoke 的 AP。烟雾的标注天然比火焰困难,因为它边界模糊、半透明,同一张图不同标注员画出的框差距很大。如果 smoke 的 AP 明显低于 fire,别急着调网络结构,先回去看烟雾的标注框是不是偏大或者漏标。
模型预测时还可以用conf参数调报警灵敏度。默认阈值 0.25,如果希望在监控场景里少漏报,降到 0.1;如果误报太多,提到 0.4。这个数字直接影响误报率和召回率,后面接预警系统时就是靠它来平衡。
yolo predict model=runs/detect/train/weights/best.pt \ source=test_video.mp4 \ conf=0.15 \ imgsz=1024conf=0.15的意思是置信度低于 0.15 的框直接丢弃。这个值越小越敏感,但也会带来更多误检,实际部署时最好拉出一段真实监控视频,跑一遍后数一下误报数量再调。
5. 火焰烟雾数据集避坑清单:五类翻车现场与处理办法
5.1 损失函数降了但精度上不去
现象:训练时 loss 一路下降,降到 0.4 以下,打开验证集图片一看,框的位置基本对,但 mAP50 只有二十几。
原因:数据集小,模型在训练集上“背答案”了。火焰和烟雾样本数少,模型记住的是这几百张图里的固定背景和光照条件,换一张没见过的监控画面就歇菜。迁移学习也只能缓解,不能根治。
解决:先从数据层面补,做更强的增广,尤其是 mosaic 和 copy-paste;再从模型层面补,换更小的模型容量,比如从 s 降到 n,抑制过拟合。最后实在不行就人工补拍或者从公开视频里截帧,凑到千张级别,效果会有明显提升。
5.2 VOC 转 YOLO 后批量框偏移
现象:转换完的 txt 里,火焰的框全部偏到右下方,或者宽高明显比实际大一圈。用可视化脚本把 YOLO 框画回图片,每个框都飘。
原因:XML 里的<size>和实际图片分辨率不一致。最常见是标注后在修图软件里缩放过图片,XML 没跟着更新,转换脚本却用 XML 里的旧尺寸做除法,导致归一化坐标集体偏移。
解决:回到转换脚本,用 PIL 重新读图片真实宽高,完全忽略<size>节点的值。这个改动就两行代码,却能救回整批标注。从那以后我所有转换脚本都默认先Image.open再取img.size,绝不信任 XML 里写死的尺寸。
5.3 把路边红尾灯认成火,把白墙认成烟
现象:训练完成后跑测试视频,晴天傍晚的路口,尾灯和落日被大量标成 fire;白色雾气、蒸汽、水面反光也被标成 smoke。
原因:火焰和烟雾的外观本质上是“颜色 + 纹理 + 运动”的组合,但静态图片模型只能看到颜色和纹理。红色圆形物体像火焰,白色半透明的东西像烟雾,这种混淆在小数据集下尤其严重,因为见过的反例太少。
解决:最有效的是收集负样本。从监控视频里截取“像但又不是”的帧,比如红色车尾灯、晚霞、排气蒸汽,标注成背景类,加入训练集重训。另一个思路是后续接视频流时用帧间差异做二次确认,真实火焰有闪烁和飘动特征,静态的红色尾灯没有。
5.4 增强参数拉满,显存当场爆炸
现象:训练时把mosaic=1.0、degrees=30、hsv_h=0.5全开,又把imgsz设到 1280,启动三秒后 CUDA out of memory。
原因:增强和输入尺寸都在烧显存。mosaic会把四张图拼在一起送进网络,输入分辨率加倍,显存占用直接翻倍;imgsz=1280在小显存卡上本来就很极限。
解决:8GB 显存老老实实imgsz=1024、mosaic=0.5、batch=8。火焰是小目标,但 1024 已经能保留足够细节,没必要硬上 1280。如果一定要高分辨率训练,先看nvidia-smi确认剩余显存,再决定参数。
5.5 patience 太小,模型还没学会就早停了
现象:训练到第 30 轮,验证集 mAP 还在爬升,但训练直接停止,因为连续 20 轮没有明显改善,patience 触发。
原因:火焰烟雾的难点分布不均匀。有些类别前 10 轮就学得差不多,有些难样本要到 80 轮以后才逐渐学会。小数据集训练波动本来就大,早停条件太苛刻,会在模型还没到最佳点的时候掐断。
解决:小数据集patience设到 80 甚至 100,让它“多熬一会儿”。或者干脆设 200,用 epoch 数控制训练时长,反正小数据集一个 epoch 就几十秒,多跑几十轮代价很小。
6. 把模型接进视频预警:跳帧检测、阈值与防误报技巧
6.1 跳帧检测与置信度阈值
模型训练完,最终要落到视频流里才能预警。监控摄像头常见 25 帧每秒,逐个帧全量推理对 GPU 是浪费,因为火焰和烟雾都是慢变化目标,写个跳帧检测既能降低延迟又不会漏掉主要事件。
import cv2 from ultralytics import YOLO model = YOLO('best.pt') cap = cv2.VideoCapture('camera_01.mp4') frame_id = 0 consecutive_hits = 0 ALERT_CONF = 0.4 while cap.isOpened(): ret, frame = cap.read() if not ret: break if frame_id % 3 != 0: frame_id += 1 continue results = model(frame, conf=0.15, imgsz=1024, verbose=False) fire_detected = any( r.boxes.cls == 0 and r.boxes.conf >= ALERT_CONF for r in results ) if fire_detected: consecutive_hits += 1 else: consecutive_hits = 0 if consecutive_hits >= 2: print(f'[ALERT] 第 {frame_id} 帧连续检测到火焰') consecutive_hits = 0 frame_id += 1逻辑说明:frame_id % 3 != 0表示每 3 帧取 1 帧做推理,25 帧的摄像头相当于每秒检测 8 次,对火灾这种演变缓慢的目标完全够用。ALERT_CONF = 0.4是报警阈值,低于它只记录不报警,减少误报。consecutive_hits >= 2要求连续两次检测到火焰才报警,这是最有效的防误报手段——单帧的偶发误检被直接过滤掉。
参数要按场景调:摄像头离监控区域远,火焰在画面里变成小目标,imgsz保持 1024,ALERT_CONF可以降到 0.3;如果场景里红色物体多,比如厂房里的红色设备多,ALERT_CONF就得升高,同时依赖连续帧确认来压误报。
6.2 夜间与逆光场景的验证习惯
火焰烟雾模型最怕的是场景光照分布变化。同一个模型,白天 mAP 六十多,晚上因为光线弱、颜色失真直接掉到三十。这个数据集里的图片偏白天场景,部署到夜间场景前必须单独验证。
我的习惯是训练完不直接收工,而是拿一段夜间视频和一个黄昏视频跑一遍predict,看看烟雾在暗背景下能不能分出来。如果夜间漏检严重,优先调hsv_h和hsv_s增强,模拟不同色温和饱和度,重训。从那以后我每次训练完都强制走一遍“白天、黄昏、夜间”三段验证,不是等部署到现场才发现模型在晚上是半瞎的。地铁站、仓库、隧道这些光照复杂的场景尤其要跑这一步,希望这次的坑记录能帮你少走两趟弯路。
本文还有配套的精品资源,点击获取