☰
烟雾明火火灾目标检测数据集:VOC/YOLO格式与YOLOv8训练实战
2026/10/2 14:05:44 网站建设 项目流程

简介:这是一套适合目标检测入门与实战的烟雾明火数据集,包含3007张真实场景图片,覆盖消防监控、森林防火、室内外明火等典型烟火识别场景,可用于构建烟火预警系统。数据集采用VOC与YOLO双格式标注,每张图片均配套xml和txt文件,标注类别为fire、smoke,合计6849个矩形框,其中fire框5198个、smoke框1651个,使用labelImg按统一规则框选,标注准确合理,可直接供YOLO、SSD、Faster R-CNN等常见检测框架训练使用,也能用于验证算法在烟火目标上的识别精度与泛化能力,尤其适合迁移学习与模型微调场景。压缩包内共有2000个文件,以xml标注文件为主体,附带说明txt,整体大小约299.85MB,便于按文件索引、快速开展实验。已有656人浏览学习,适合需要真实烟火样本的目标检测开发者、学生及消防智能化项目相关人员,可显著节省数据筛选与格式转换的时间,更专注于模型训练、调参与场景落地。

1. 烟雾明火烟火火灾目标检测数据集:3000张图拆给你看

做火灾识别的目标检测模型,最头疼的往往不是网络结构,而是没数据可喂。公开的火焰数据集不是数量太少,就是标注不规范,跑出来的模型放到真实摄像头前面框全在飘。这套烟雾明火烟火火灾目标检测数据集总共3000多张JPEG图片,配齐了VOC和YOLO两种标注格式,共标注6849个目标框,其中fire类5198个、smoke类1651个,类别是[“fire”, “smoke”],用labelImg画矩形框完成。如果你是正在训练烟雾明火检测模型、需要一套能直接跑的标注数据的从业者,这套资源能省下你两三周的数据清洗时间。我把它解码开,把文件结构、格式互转、训练参数和踩坑点逐一讲透。

2. VOC和YOLO双格式:文件结构拆解与格式互转逻辑

2.1 一张图对应三个文件的目录组织

压缩包解压后,你首先会看到一个说明.txt,里面写清楚了格式约定。整套数据是典型的jpg + xml + txt三件套结构:每一张jpg原图,配套一个VOC格式的xml标注文件,再配套一个YOLO格式的txt标签文件。文件名完全同名,只是后缀不同,打开目录后(不管你的解压工具是平铺还是按文件夹分好),实际看到的内容是长这样的:

ESE_fire_557.jpg ESE_fire_557.xml ESE_fire_557.txt

清单里没有给出images、labels之类的分类子目录,说明原数据大概率是一张图片配两个标签文件平铺存放。拿到手之后你不需要反向构造标注,直接用同名匹配就能完成数据划分。

这里有一个别的数据集不太一样的点:摘要里写明了txt文件不包含分割路径,只有类别和归一化坐标,没有一条多余通路信息。也就是说,这个YOLO标签是拿来做目标检测的,不是拿来做实例分割的,别到时候拿着它去跑分割任务,那肯定会翻车。

2.2 VOC的xml字段:每个框的信息源头

labelImg画框后保存的VOC格式,信息结构是固定的。以ESE_fire_1498.xml为例,解压后看到的内容骨架是这样的:

<annotation> <folder>.</folder> <filename>ESE_fire_1498.jpg</filename> <size> <width>实际宽度</width> <height>实际高度</height> <depth>3</depth> </size> <object> <name>fire</name> <bndbox> <xmin>100</xmin> <ymin>80</ymin> <xmax>420</xmax> <ymax>360</ymax> </bndbox> </object> </annotation>

不要迷信我写的具体坐标值,不同图片差异极大,重要的是字段含义。<name>就是类别名,本数据集里只有fire和smoke两种;<bndbox>给出矩形框的左上角xmin/ymin和右下角xmax/ymax,单位是像素,基于<size>里的图片宽高。如果你的框想转换成YOLO格式,像素坐标是唯一的原始依据。另外,VOC格式允许一张图片多个<object>节点,比如一张图里同时有明火和浓烟,那就有两个object,一个name是fire,另一个是smoke。

一张图多个框,直接导致每个类别框数与图片数不对等。3007张图、6849个框,平均每图约2.3个目标,说明这套数据不是“一图一框”的玩具集,图像中多目标情况非常普遍,训练时不光要能检出目标,还得扛得住密集场景。

2.3 YOLO的txt:归一化之后的四元组

YOLO格式把每个目标压缩成一行,五个数字空格隔开,秘密全在归一化:

0 0.482910 0.366406 0.620313 0.355469 1 0.721875 0.531250 0.181250 0.497656

第一个数字是类别索引:如果按摘要里给出的顺序["fire","smoke"]转出来,0对应fire,1对应smoke。但这个顺序是“转换脚本按什么顺序写classes”决定的,拿到数据的第一件事不是信任这个约定,而是自己验证。

后面四个数字依次是归一化中心横坐标、中心纵坐标、框宽、框高。计算规则是把VOC的像素坐标除以图片宽高:

x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height width = (xmax - xmin) / width height = (ymax - ymin) / height

这个数据集的txt全部按这个规则生成,你拿上面那个ESE_fire_1498.xml手算一下就能对上。暴力验证的办法是把tx输出类别0和类别1的框个数,分别应该接近5198和1651;如果是反过来的,说明我推断的索引顺序和你的假设不一致。具体怎么验,第四章写避坑时会给脚本。

2.4 从VOC到YOLO的转换脚本:兼作格式核验工具

下载的资源里没有带转换工具,这是正常的,标准做法是自己写一个。我一般在拿到这类双格式数据时,会先写个小脚本做两件事:一是把xml转成txt做交叉核验,二是打印每个类别的框数分布。脚本如下:

import xml.etree.ElementTree as ET import os from collections import Counter CLASSES = ["fire", "smoke"] def convert_xml_to_yolo(xml_path, out_path): 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.findall("object"): name = obj.find("name").text if name not in CLASSES: continue cls_id = CLASSES.index(name) box = obj.find("bndbox") xmin = int(box.find("xmin").text) ymin = int(box.find("ymin").text) xmax = int(box.find("xmax").text) ymax = int(box.find("ymax").text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines)) # 对单个文件做转换,并统计整个数据集的框数 counter = Counter() for f in os.listdir("."): if not f.endswith(".xml"): continue base = f[:-4] convert_xml_to_yolo(f, base + ".yolo.txt") for line in open(base + ".yolo.txt"): cls_id = line.split()[0] counter[cls_id] += 1 print(counter)

这段代码里CLASSES的先后顺序,就是YOLO类别索引的先后顺序,必须和你运行时一致。size/width和size/height是相对路径查找方式,ElementTree用斜杠表示多层路径。当你把2500多个xml全部转换完,控制台打印的Counter应该稳定在{'0': 5198, '1': 1651}上下;如果出现明显偏差,说明要么转换脚本顺序理解错了,要么某个xml里的name不是标准值,得单独抽出来看热。这也是我用这个脚本复用为验证工具的原因。

3. 用YOLOv8训练这套火灾数据集:完整参数与流程

3.1 数据划分:train/val/test的比例

你这套数据没有自带划分目录,压缩包里平铺的jpg直接拿来开训是不行的,得先划分。我的做法是固定随机种子,保证每次重跑结果可复现。参考代码如下:

import os import random from shutil import copyfile random.seed(2024) # 平铺目录下所有jpg文件 images = [f for f in os.listdir(".") if f.lower().endswith(".jpg")] random.shuffle(images) train_cnt = int(len(images) * 0.8) val_cnt = int(len(images) * 0.9) train = images[:train_cnt] val = images[train_cnt:val_cnt] test = images[val_cnt:] for split_name, split_files in [("train", train), ("val", val), ("test", test)]: os.makedirs(f"images/{split_name}", exist_ok=True) os.makedirs(f"labels/{split_name}", exist_ok=True) for fname in split_files: base = fname.rsplit(".", 1)[0] copyfile(fname, f"images/{split_name}/{fname}") copyfile(base + ".txt", f"labels/{split_name}/{base}.txt")

划分比例按80/10/10走,训练集2400多张、验证集300张、测试集300张,对3000张出头的小数据集是合理选择。random.seed(2024)固定洗牌顺序,同一份数据反复跑不会因为划分不同导致指标波动,这是对比实验结果前必须做的一件事。txt与jpg同名匹配,复制时只拷贝标签不拷贝xml,因为YOLO训练只消费txt。

3.2 data.yaml配置:路径与类别名的对应

接下来建立data.yaml。注意YOLOv8的路径基准是自动识别你填的绝对路径,不要写相对路径,训练脚本启动目录一变就全废:

path: F:/datasets/fire_smoke # 改成你解压后的实际根目录 train: images/train val: images/val test: images/test nc: 2 names: 0: fire 1: smoke

nc必须等于2,这个不写对后面所有环节都会崩。names的索引顺序,就是上面转换脚本里CLASSES列表的顺序。如果我转换时CLASSES = ["fire", "smoke"],这里0就是fire、1就是smoke,两个文件必须达成一致,否则训练出来的模型,预测结果和可视化标签会对不上。

3.3 训练命令与超参:从yolov8s起步的基线方案

先用yolov8s.pt做预训练权重打底。火灾烟火目标不算极端小目标,s模型复杂度适中,跑出来的效果有参考价值,又不至于像nano小模型那样精度被压太多:

yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ patience=20 \ workers=4

几个关键参数拆开说。epochs=100对3000张图片偏保守,配合patience=20可以让它在验证集指标20轮不涨时自动早停,不必死跑完100轮。imgsz=640是默认推荐,和YOLOv8的预训练尺寸一致,不需要刻意加大到1280,火焰和烟雾主体区域占比较大,640足够。batch=16以单卡12GB显存为前提,显存不够降到8,如果batch太小导致BN统计不稳定,精度会断崖式下跌,别硬撑。

跑完之后一个重要动作是看runs/detect/train下的results.png,里面有train/loss和val/loss的曲线。正常情况val loss随epoch下降,如果val loss前20轮降完就开始回升,那是过拟合信号,把epochs裁到40再试。

3.4 类别不平衡:fire 5198 vs smoke 1651的处理策略

这是全套数据里最显眼的数字冲突:fire框数是smoke的三倍多。很多人的第一反应是给smoke加loss权重,但我建议不要刚上手就这么干。YOLOv8默认就带cls损失,盲目调class_weights反而会让fire精度暴跌。

优先做法是数据增强里给smoke倾斜,并对smoke类的样本做过采样。写训练配置时可以通过超参平衡,但更通用的做法是在数据层面复制smoke的txt对应图片,让两类框数接近。当然,也可以直接改YOLOv8训练脚本里的损失权重,只是这样做会让你的实验结果很难和别人的对比,因为大家都在同一个默认配置下比mAP。

实操中我建议先用默认配置跑一版,验证mAP50能否到70%以上。如果smoke的AP值远低于fire,再针对smoke小幅上调。训练集里每次迭代自动做了mosaic等增强,真实暴露出来的不平衡会比框数显示的小一些,不用紧张到一上来就重写损失函数。

4. 避坑:这套火灾数据集最容易翻车的五个点

4.1 现象:Loss降得很低,但可视化预测全是乱框,fire和smoke画反了

原因:txt里的类别索引和你训练配置里的names顺序不一致。我见过不止一次,转换脚本里classes是["fire", "smoke"],而data.yaml里names恰好填反了,模型训练全程都在学反标签,自然收敛到“识别正常但语义颠倒”的诡异状态。

解决:动手训练前先跑一次标签统计:

import os from collections import Counter counter = Counter() total = 0 for f in os.listdir("labels/train"): if not f.endswith(".txt"): continue for line in open(f"labels/train/{f}"): cls_id = line.split()[0] counter[cls_id] += 1 total += 1 print(counter, total)

输出应该是{'0': 约4158, '1': 约1321}这一量级,0多1少。如果你看到的相反,说明txt里0对应smoke,把data.yaml的names顺序调转。

4.2 现象:图片能训练,但验证集mAP50-95差值大,smoke的AP比fire低十几个点

原因:smoke本身只有1651个框,又叠加烟雾半透明、边界模糊难标注,学不到清晰纹理是物理规律决定的,不是模型问题。

解决:先不要动模型结构。把imgsz提到960,让烟雾区域有更多有效像素;数据增强里把hsv_h调低一点,因为烟雾颜色对色调敏感,过度色偏增强会让模型把背景雾霾当烟雾。我一般用albumentations针对烟雾样本单独做一次裁剪增强,而不是全数据集统一洗色。

4.3 现象:训练到一半报Image ... is corrupted or has EXIF issues,进程直接终止

原因:3000张原图并非全由同一部相机产出,部分网络爬取的图片EXIF信息异常或文件尾部截断。YOLO训练时读图采用OpenCV后端,遇到断图就会抛异常。

解决:先跑一轮全量体检:

from PIL import Image import os bad = [] for root, _, files in os.walk("images"): for fname in files: path = os.path.join(root, fname) try: img = Image.open(path) img.load() except Exception as e: bad.append((path, str(e))) print(bad)

把打印出来的坏图连同对应xml/txt一并剔除,再跑划分,问题消失。

4.4 现象:模型框出的火焰区域比实际明火大一圈,IoU怎么都提不上去

原因:标注规则是画矩形框,但火焰往往是不规则扩散形态,标框时必然包含部分无火焰区域,这是矩形标注的固有限制,不算标注错误。同样的问题在smoke上更严重,烟雾边缘软,标框时不同标注员的手抖程度完全不同。

解决:评价这套数据时别死磕IoU=0.5,结合AP50和AP75一起看。部署时用置信度阈值过滤而不是IoU阈值过滤,宁可框略大也不漏检,这在消防场景里是能接受的工程权衡。如果要做框回归精度竞赛级调优,唯一出路是自己二次标注或改成旋转框。

4.5 现象:Windows解压后出现乱码文件名,或者jpg和xml对不上

原因:压缩包内文件用UTF-8编码存储,Windows自带的zip工具按GBK解出来,中文目录没影响,但部分带有特殊字符的英文文件名也偶尔出现冲突。这类平铺数据一旦文件名错位,三个文件就拆家。

解决:不用系统自带解压,用Bandizip或7-Zip,右键以“自动检测编码”方式解压。解压后不要急着开训,先跑一段校验脚本,统计同名的jpg/xml/txt数量差值,三方数量都等于3007才能进训练管线。

提示:下载后先验证三个文件数是否一致,再谈训练。这一步花5分钟,省下后面几十个小时的返工。

5. 数据增强与边界:3000张图能覆盖哪些真实火灾场景

5.1 能跑的增强:HSV、mosaic、仿射的组合

3000张图片数量不大,直接训练容易过拟合,增强是必选项。YOLOv8自带的mosaic、fliplr默认开启,但火焰场景有自己的特殊性,我习惯在外部管线里再加一层albumentations增强,针对烟雾半透明特征做强化:

import albumentations as A fire_transform = A.Compose([ A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=0.5), A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.5), A.ShiftScaleRotate(shift_limit=0.02, scale_limit=0.05, rotate_limit=5, border_mode=0, p=0.3), A.OneOf([ A.MotionBlur(blur_limit=3, p=0.5), A.GaussNoise(var_limit=(5.0, 20.0), p=0.5), ], p=0.2), ])

这套组合里的参数说直白点:hue_shift_limit=10只做小幅色调偏移,避免把黄色火焰调成绿色假样本;rotate_limit=5控制旋转角度在5度以内,因为火灾检测场景里摄像头基本固定,大幅度旋转不符合真实部署视角。MotionBlur模拟摄像头抖动,对室内固定机位来说不是最优选择,但对于手持巡检设备却是刚需,是否启用要看你的实际目标场景。

5.2 不建议的增强:无脑翻转与暴力Cutout

很多人拿到新数据集先一键开启全部增强,结果在火灾场景里翻车。垂直翻转尤其要少用,摄像头画面里的火焰和烟雾具有明显的重力方向,烟永远是往上飘的,你把图片竖直翻转,模型就会学到“烟往下沉”这种物理上不存在的模式,推理时对真实视频很难适应。

Cutout随机遮挡也要谨慎使用。火焰区域如果被遮住一半,残留的烟和火的纹理依然有判别力,但如果你遮挡比例过大,负样本语义被破坏,模型会学到不合理的补全,验证集上AP奇高,到了真实环境立刻现形。控制遮挡比例在0.2以下,且只能作为辅助增强,不能当主力。

5.3 这套数据的真实边界:白天厂房与自然光场景

从标注的框数和内容名称看,这套数据偏向可见光下的室内厂房、户外堆场和开阔地带的火焰与烟雾目标,适合做早期火灾预警模型的预训练基底。它的短板也明显:缺乏夜间红外或低照度下的样本;缺乏燃气管道、电气柜等小目标密集场景;没有俯视视角的无人机画面。

检测边界必须心里有数。如果你拿这套数据集训练完直接部署到夜晚的化工园区,漏检率会比你想象的高得多,因为夜间火焰和灯光的视觉特征重叠严重、烟雾在暗光下几乎不可见。一个合格的做法是用这套数据做预训练,再采集目标场景数据做微调,而不是把它当成终态数据用。

6. 验证与下钻:不只看mAP,用混淆矩阵和置信度阈值做工程取舍

6.1 用验证命令产出混淆矩阵,看fire和smoke怎么互相干扰

YOLOv8在验证阶段可以直接产出可视化图表,跑一次带plots=True的验证:

yolo detect val \ model=runs/detect/train/weights/best.pt \ data=data.yaml \ plots=True

执行完成后,runs/detect/val目录下会生成confusion_matrix.png。你要重点看两个格子:真实fire被预测成smoke的比例,以及真实smoke被预测成fire的比例。一般规律是smoke漏检多于fire漏检,因为烟气边缘像素与背景混合严重。如果矩阵里两类交叉项超过15%,说明模型对“烟包火”的复合目标区分度不足,优先做法是增加两类同时出现的训练样本,而不是调模型宽度。

6.2 置信度阈值:消防场景的漏检与误报权衡

学术评测里conf=0.25是通用默认值,但工程上属于“裸奔参数”。火灾检测的代价函数极度不对称:漏一次警是重大事故,误报一次只是让值班员去确认一下。我会把置信度阈值压到0.05-0.10,宁多框不漏框:

yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=test_images/ \ conf=0.05 \ iou=0.45

iou=0.45是NMS阈值,控制同一个目标保留几个框。压得越低,重复框越少,但对密集目标的召回率也会下降,0.45是一个不会翻车的起始点。实践时先跑一遍,看输出的预测结果里有多少低置信度框是真实火焰,再微调决定。

6.3 小目标框的处理:AP50-95不理想时先查框面积分布

smoke的平均框面积天然小于fire,因为烟雾在早期阶段就是一小缕。验证时如果smoke的AP50和AP50-95差距巨大,往往说明小目标检测能力不足。这时先别上注意力机制,先做一件事:统计训练集里所有框的归一化宽度和高度,看有多少框面积低于0.05。

占总框20%以上的小目标,imgsz=640可能不够,把它提至960再训练一个版本做对比。如果提分辨率后smoke的AP50-95涨了但速度明显下降,部署时就做双路推理:一路640快速筛查,一路960只对视频画面中的ROI区域精细复核。自从那以后,我每次拿到新数据集都要先统计框的面积分布,再见小目标就不焦虑了,希望帮到你。

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

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

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

立即咨询