☰
烟雾火焰数据集从XML到YOLOv8:完整训练落地指南
2026/10/2 9:04:13 网站建设 项目流程

简介:一份用于烟雾与火焰检测的标注数据集,面向计算机视觉初学者与目标检测研究者,旨在解决火灾预警场景中标注数据稀缺的问题。资源共包含4118个文件,由2059个jpg原始图像与2059个xml标注文件一一配对组成,压缩包整体约168MB,目录结构清晰,解压后即可直接用于YOLO、SSD、Faster R-CNN等主流检测框架的训练与验证。图像覆盖室内外多种场景,标注信息中准确标出了烟雾、火两类目标的类别与位置边框,便于开展模型训练、性能评估与算法对比。目前已有2326人学习下载,适合需要快速获取带标注烟火数据、减少人工标注成本的开发者与研究人员,也可用于教学演示和毕业设计中的目标检测环节。

1. 烟雾火焰数据集怎么用:2056张图像、两类目标与XML标签的完整落地路径

很多人拿到烟雾火焰数据集的第一反应是直接拖进YOLOv8开训,结果第一步就被卡住——这个数据集提供的是2056张图像和XML标签,而YOLO系列训练工具期待的是YOLO格式的txt标注。XML标签需要先解析、再转换、还要做质量校验,之后才能喂给训练流程。这篇笔记按“拆标签 → 转格式 → 配目录 → 训模型 → 排坑 → 验证”的顺序,把一套能直接照抄的落地路径完整讲完。适合手里有XML标注图像、想转成YOLO训练格式的从业者,也适合刚接触数据集处理、想跑通目标检测闭环的新手。

2. 拆解XML标签:先摸清这张烟雾火焰数据集的底细再动手

拿到任何数据集,第一步不是写训练代码,而是先清点家底。2056张图、两类目标,这只是简介,不代表实际标注就是如此。类别数是不是恰好两个?会不会把“smoke”和“fire”写错成其他拼写?图像尺寸是统一还是多分辨率混入?框坐标有没有越界甚至宽高为负?这些如果不提前查一遍,后面转换脚本跑完,你就会进入“发现一个坑补一个坑”的连续翻车状态——你以为XML解析出来的数据是对的,结果某些XML的filename字段对不上图像名,或者标注框的像素值超出了图像边界。所以,我坚持把统计流程放在任何数据集处理工作的第一步,这个习惯能省掉后面一整天的排错时间。

2.1 烟雾和火的视觉差异,决定了标注框的“画法”不同

烟雾是半透明目标,边界模糊,颜色偏灰白或青灰,跟随气流扩散,没有锐利轮廓。标注员画烟雾框时只能按“模糊可见的大致范围”框选,所以烟雾的框普遍偏大,甚至会把一部分背景包进来。火焰恰恰相反,中心是明亮的橙黄色,边缘虽然也有外焰过渡,但至少有一个清晰的视觉重心,标注框可以收得很紧。同样一张火灾现场图,一个熟练标注员会画一个紧密的fire框,再叠一个范围更大的smoke框,因为烟里裹着火,两者天然重叠。

这种差异直接决定训练策略。YOLO类模型对边界框做回归,标注松,模型学到的预测框就松。烟雾框大一些在消防场景里不算坏事,宁肯框住整团烟也别漏掉半团;但火焰框如果也松,模型会把火周围的背景学进目标特征,误检率明显上升。更关键的是目标尺度差异:烟雾通常是大面积目标,横向展开且边缘碎;火焰则是从十几像素的小火苗到占半屏的大火都有。在imgsz=640的训练设置下,烟雾能提取到丰富纹理,而小火焰在缩放后可能只剩几个像素,特征图里几乎没有可用的响应。这就是为什么同一份数据集,有人训练完mAP很高,有人训练完实际场景漏得厉害,差别往往不在网络结构,而在对这两类目标视觉特性的理解深度。

2.2 逐字段拆一个烟雾火焰标注XML:不只是读bndbox

Pascal VOC格式的XML是很多老数据集的标配。下面这段就是该数据集中最常见的一类标注文件,字段顺序我稍作整理,内容保持一致:

<annotation> <folder>smoke_fire_2056</folder> <filename>img_0042.jpg</filename> <path>/datasets/smoke_fire_2056/images/img_0042.jpg</path> <source> <database>smoke_fire</database> </source> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>fire</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>526</xmin> <ymin>210</ymin> <xmax>814</xmax> <ymax>690</ymax> </bndbox> </object> <object> <name>smoke</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>180</xmin> <ymin>120</ymin> <xmax>1050</xmax> <ymax>704</ymax> </bndbox> </object> </annotation>

逐字段往下看。<folder>和<filename>用于定位图像文件,但<filename>不一定带路径,转换脚本里最安全的做法是拿XML文件名做基准,而不是靠<filename>去找图——很多事故就发生在xml叫img_0042.xml、里面写的却是另一个文件名。<size>的三个值必须读,因为后续转YOLO格式时,所有坐标都要除以width和height做归一化;如果XML里记录的是640×640,实际图像是1920×1080,那转出来的框全部错位。<segmented>只在分割任务里用,检测流程可以直接忽略。

<object>是单个目标,一张图里多个目标就多个<object>。<name>是类别名,要严格区分大小写和拼写,smoke、fire如果某份XML里被写成Smoke或fire_smoke,类别映射就崩了。<truncated>表示目标是否被图像边界截断,<difficult>表示这个样本是不是难例,两者在很多转换脚本里会被静默跳过。<bndbox>里四个值就是标注框左上角和右下角的像素坐标,xmin必须小于xmax,ymin必须小于ymax,且不能超出图像尺寸。这个数据集的2056份XML里,绝大多数框都是合法规范的,但偶尔也会出现坐标反了、框宽为0这类脏样本,统计脚本要做的事就是把它们暴露出来。

2.3 用Python脚本统计类别、框宽高和图像分辨率

import xml.etree.ElementTree as ET import glob import os from collections import Counter xml_dir = "./annotations" cls_counter = Counter() box_records = [] img_sizes = [] for xml_path in glob.glob(os.path.join(xml_dir, "*.xml")): try: tree = ET.parse(xml_path) except ET.ParseError as e: print(f"XML解析失败: {os.path.basename(xml_path)}, {e}") continue root = tree.getroot() size_el = root.find("size") if size_el is None: print(f"缺少size节点: {os.path.basename(xml_path)}") continue img_w = int(size_el.find("width").text) img_h = int(size_el.find("height").text) img_sizes.append((img_w, img_h)) objs = root.findall("object") if not objs: print(f"没有object节点: {os.path.basename(xml_path)}") for obj in objs: name = obj.find("name").text.strip() cls_counter[name] += 1 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) if xmax <= xmin or ymax <= ymin: print(f"非法框: {os.path.basename(xml_path)}, {name}, {xmin},{ymin},{xmax},{ymax}") if xmin < 0 or ymin < 0 or xmax > img_w or ymax > img_h: print(f"越界框: {os.path.basename(xml_path)}, {name}") box_records.append((xmax - xmin, ymax - ymin, img_w, img_h)) print("类别统计:", dict(cls_counter)) print("图像分辨率种类:", len(set(img_sizes)), "种") print("总标注框数:", len(box_records))

这段脚本会输出五类信息:XML解析失败的样本、缺<size>的样本、没有<object>的样本、框宽高非法或坐标越界的样本,以及类别与分辨率分布。我实际处理这类数据时,最常见的异常反倒不是框越界,而是“疑似重复图”——同一场景换了文件名,标注完全一致。如果不做去重,训练时验证集里会混进训练图的近亲,mAP虚高。脚本不会自动去重,但把框宽高和图像分辨率列出来后,看到大量完全相同的尺寸组合,就该做一次图像哈希去重。框记录里最后一项是图像宽高,后续转换脚本会再次用到。到这里,数据集的底细已经摸清,下一步就是把XML翻译成YOLO格式。

3. 把XML标签转成YOLO格式:转换脚本与三个必做的检查

Pascal VOC的XML标注虽然标准,但当前主流的YOLO系训练工具吃的是txt格式标注。在给YOLOv8喂数据之前,你需要把2056个XML文件逐一转成对应的txt文件,同时保证类别顺序和YOLO的类别索引一致。这个环节比想象中更容易翻车,尤其是当XML里的filename字段不可信时,转换脚本的基准选择就变得很重要。

3.1 为什么YOLOv8不直接吃XML:归一化坐标与单图单txt

XML标注用的是绝对像素坐标,xmin、ymin、xmax、ymax都是具体数值;而YOLO系列的标注文件要求每一行是“类别索引 中心点x 中心点y 宽 高”,全部归一化到[0,1]区间。转换公式是:

x_center = (xmin + xmax) / 2 / img_w
y_center = (ymin + ymax) / 2 / img_h
w = (xmax - xmin) / img_w
h = (ymax - ymin) / img_h

格式差异能用公式解决,真正的坑在于组织方式。XML一个文件可以包含多个<object>,同一张图的多个目标在YOLO格式里必须写到同一个txt文件,一行一个目标;类别名也变成了整数索引。如果XML里是smoke和fire,映射表一旦定错,两个类别的语义就互换了,训练结果直接失真。还有一点:XML里的<filename>字段经常不可信。你可能遇到一个目录里叫smoke_0001.jpg的图,XML里<filename>写的却是另一个名字。转换脚本里更稳妥的做法是:完全忽略<filename>字段,直接用XML文件的文件名(去掉.xml后缀)作为输出txt的文件名,确保一一对应。

3.2 完整转换脚本:代码、逻辑说明与class_map参数

import xml.etree.ElementTree as ET import glob import os # 类别映射:顺序决定YOLO类别索引,不要随意变换 class_map = { "smoke": 0, "fire": 1, } xml_dir = "./annotations" out_dir = "./labels_yolo" os.makedirs(out_dir, exist_ok=True) for xml_path in glob.glob(os.path.join(xml_dir, "*.xml")): try: tree = ET.parse(xml_path) except ET.ParseError: print(f"跳过损坏XML: {xml_path}") continue root = tree.getroot() # 用XML文件名作为基准,不信任filename字段,避免图和标注对不上 base_name = os.path.splitext(os.path.basename(xml_path))[0] size_el = root.find("size") if size_el is None: print(f"跳过缺少size的XML: {xml_path}") continue img_w = int(size_el.find("width").text) img_h = int(size_el.find("height").text) write_lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in class_map: print(f"未知类别 {name} 在 {xml_path}") continue difficult = int(obj.find("difficult").text) if difficult == 1: # 常见策略是跳过difficult难例,避免干扰训练 continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(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 # 浮点误差导致的越界裁剪回[0,1] x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) write_lines.append( f"{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}" ) out_path = os.path.join(out_dir, base_name + ".txt") with open(out_path, "w") as f: f.write("\n".join(write_lines)) print("转换完成,输出目录:", out_dir)

这段代码有两个关键设计。第一,用os.path.splitext(os.path.basename(xml_path))[0]取XML文件名做输出基准,而不是信任<filename>字段,这能堵住“图像改名导致txt和jpg对不上”的坑。第二,difficult=1的样本默认跳过;如果你希望保留难例,把continue改成pass即可,但要知道difficult样本大概率是极端光照或严重遮挡的图,保留它们会拉低整体mAP。

class_map的顺序就是YOLO训练时的类别索引顺序,训练配置文件里的names必须和它完全一致,例如['smoke', 'fire']。一旦顺序反了,模型学出来的类别解释也是反的。:.6f是保留6位小数,足够YOLO做边界框回归;如果只保留3位,小目标的宽高误差可到千分之一,虽然不是致命伤,但没必要省那点字节。

3.3 转换后必做三样检查:目录数量、坐标边界、类别索引

转换脚本跑完并不等于结束,至少要做三样检查。第一,txt数量要和XML数量一致,除非有损坏文件被跳过。用两条命令快速比对:

ls ./annotations/*.xml | wc -l ls ./labels_yolo/*.txt | wc -l

第二,抽查txt里的坐标是否都在[0,1]之间。如果出现1.2或-0.1,说明XML的<size>或bndbox里有脏数据,训练时会导致loss异常。用这条命令全局检查:

awk '{for(i=2;i<=5;i++){if($i<0||$i>1) print $0}}' ./labels_yolo/*.txt | head -20

第三,确认类别索引没有超出nc范围。这里只有smoke(0)和fire(1)两类,如果txt第一列出现2或更大的数字,说明XML里混了第三个类别,先回头看class_map和原XML的<name>,而不是直接开训。这三个检查做完,数据阶段才算真正闭环,后面训练就不会被低级问题拖住。

4. 用YOLOv8训练自己的烟雾火焰数据集:yaml配置、命令行与指标解读

数据转换和目录整理都完成后,就进入了真正动手训练的环节。很多人在这里再卡一次:data.yaml写在项目里就直接点训练,结果路径找不到、类别顺序写错、batch直接把显存打爆。所以还是把配置和命令逐行讲清楚,照着能跑通才是硬道理。

4.1 数据集目录结构和data.yaml怎么写

YOLOv8对数据集目录有一套清晰的预期。推荐使用下面这个标准布局,兼容性最好:

smoke_fire/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/

images/train里放jpg,labels/train里放对应的同名txt,val目录同构。图像和标注文件的文件名必须完全一致,包括后缀;可别把img_0042.jpg的标注写成img_0042.txt之外的名字。把之前转换生成的txt按比例拆到train和val,可以用下面这段脚本快速完成:

find . -name "*.txt" | head -20 # 先看看转换出来的txt数量 python3 - <<'EOF' import os, random, shutil random.seed(42) txts = [f for f in os.listdir("./labels_yolo") if f.endswith(".txt")] random.shuffle(txts) for i, t in enumerate(txts): img = t.replace(".txt", ".jpg") sub = "train" if i < len(txts) * 0.8 else "val" shutil.move(f"./images/{img}", f"./images/{sub}/{img}") shutil.move(f"./labels_yolo/{t}", f"./labels/{sub}/{t}") EOF

data.yaml是这个烟雾火焰数据集在YOLOv8训练里的唯一配置入口,最简写法如下:

path: /data/smoke_fire train: images/train val: images/val nc: 2 names: 0: smoke 1: fire

path是绝对路径,train和val是相对path的相对路径。nc=2和前面class_map的顺序必须对应,names的顺序千万不能换。如果你把names写成fire在0、smoke在1,那转换出来的txt里0就代表fire,能训但会留下灾难级的后患。

4.2 yolo detect train命令逐参数拆解

yolo detect train \ data=smoke_fire.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ patience=20 \ lr0=0.01 \ seed=42

对2056张图的二分类检测来说,最需要关注的是epochs、imgsz、batch和lr0这四项。

参数推荐值调整逻辑
modelyolov8n.pt先选n或s小模型跑通流程,再迭代大模型
epochs100配合patience,20轮不提升会自动早停
imgsz640显存不足降到480,小目标多时提到960或1280
batch16以显存不爆为准,越小梯度噪声越大
lr00.01数据噪声大时降到0.005或0.001
patience20太大浪费时间,太小容易过早停

model=yolov8n.pt会从官方预训练权重初始化参数,这对烟雾火焰这类自然图像数据集很有帮助——虽然官方数据没有烟雾和火,但底层的纹理和轮廓特征可以复用。如果不想用预训练权重,改成model=yolov8n.yaml会从头训练,收敛更慢,效果也差一截。imgsz=640是训练时的目标分辨率而非原图大小,YOLO会自动缩放;这个参数在验证和推理时要保持一致,否则模型看到的尺度分布会乱。

4.3 用mAP和PR曲线判断标注质量:哪些指标在说实话

训练结束后终端输出的mAP50和mAP50-95是最直接的反馈。对烟雾火焰数据集,期望的特征是:mAP50在0.75到0.85区间,因为烟雾框偏松、重叠多;mAP50-95可能在0.4到0.6之间,框得不够精确就会掉分。如果mAP50连0.5都不到,大概率是数据问题而不是模型问题——先检查类别索引是否配错,或者train目录里有没有大量烟雾没被标注。

PR曲线能告诉你低置信度下召回能拉到多高。烟雾这种宽框目标,PR曲线通常有一条较长的“尾巴”,也就是为了提高召回,精度掉得很快;火焰的PR曲线则相对坚挺。再去看混淆矩阵:如果smoke被大量判成fire,说明标签里重叠区域的比例偏高,应该参考第5.5条调整标注或数据划分。最后我会做一个独立测试——挑5张没进过训练集的火灾实拍图跑推理,看置信度和框位置是否符合直觉。这些指标和人工抽查才是对“标注好”这件事的最终验证。

5. 烟雾火焰数据集避坑指南:五条踩坑记录与排查路径

下面五条是我在处理类似烟雾火焰项目时真实遇到过的坑,按“现象 → 原因 → 解决”的记录方式拆解,方便你直接对照排查。这份数据集虽然整体标注质量不错,但标注流程中的典型问题它一个不少。

5.1 loss在训练中期开始震荡不收敛

现象:训练前30个epoch,box_loss正常下降;到第40个epoch后loss曲线开始上下起伏,train和val的mAP在0.7到0.8之间来回跳动,final权重时好时坏。

原因:首先是学习率过大,训练后期无法在损失曲面里安安稳稳收敛;更常见的是数据里部分烟雾框质量太差。烟雾框有的画得很紧、有的画得很松,模型对同一类目标的回归目标不一致,梯度方向互相冲突,loss自然震荡。

解决:先把lr0从0.01降到0.001,观察三四十个epoch;如果还震荡,把imgsz从640调到960,让大烟雾目标少受resize压缩;最后把标注跨度差异大的框挑出来重新标注,或者暂时从训练集中剔除。这个排查顺序基本能定位到根因。

5.2 mAP50很高但实际推理漏检

现象:验证集mAP50达到0.87,但把训练好的模型拿到实拍视频上推理,中远距离的小火苗全部漏掉,烟雾还能检出几个。

原因:mAP50只看预测框和真实框IoU大于0.5就算检出,对框得准不准极不敏感。验证集里如果大多是近景大目标,mAP50会被拉得很高,小火焰本来就容易被漏掉。再加上验证集和训练集来自同一批场景,评估结果水涨船高。

解决:用五折交叉验证保证场景不重叠,推理阶段对大图做切片输入而不是整图直接resize,也可以把agnostic_nms设为True,避免烟和火的预测框互相抑制。小目标漏检问题很多时候不是网络不行,而是评估方式和推理方式绕开了小目标。

5.3 XML里混入difficult=1,训练直接把样本丢了

现象:转换脚本跑完后,labels目录里的txt总数比XML总数少了几百个,有些图片完全没有对应的txt文件。

原因:这些XML里至少有一个<object>的<difficult>为1,转换脚本默认跳过难例;如果一张图的所有目标都被标记difficult=1,那这个图的txt是空的或者根本没生成。YOLOv8训练时遇到没有txt的图像,会默认没有标注,直接跳过。

解决:选择保留或删除,但必须全程一致。要么把转换脚本里的continue改成pass,把难例当普通样本一起训练;要么统一丢弃。不要出现同一张图在train里没标注、在val里又有标注的情况——这会让验证指标变成黑匣子,你怎么解释都说不通。

5.4 大图被缩到640后小火焰消失

现象:训练和验证都用imgsz=640,模型在验证集mAP不错,但用原始1280×720图像推理时,小火焰完全检测不到;把推理imgsz调回640又能检到。

原因:训练时把所有图像统一缩放到640,原图里只有10像素高的小火焰,在训练图上只剩约5像素,特征图里几乎没有可用响应。即使推理时用了原图,模型也没在那种尺度分布下见过正样本。

解决:把训练和推理的imgsz统一提高到1280,同时调小batch;或者训练时开启multi_scale选项,让模型在每个epoch随机缩放输入尺寸,增加对目标尺度的适应能力。显存实在不够,训练保持640,但推理时用tile切片把大图切块分别检测,效果也能救回大半。

5.5 烟雾和火重叠区域的标签互相干扰

现象:部分图像火被烟雾包裹,两个标注框重叠严重。训练中类别相关的loss反复横跳,推理时重叠区域里烟和火的置信度都偏低。

原因:同一块像素区域同时存在两个不同类别的目标,模型在特征空间中对重叠区域的语义产生混淆。如果重叠区域的框边界还不精确,标注的不确定性会被进一步放大。

解决:两条路。第一,在数据层面控制“烟包火”极端样本的比例,不要整份数据集都是重叠场景;第二,在标注策略上给两类目标划清边界——fire框收到火焰亮区,smoke框扩展到烟雾可视范围,两者重叠没关系,但边界必须可解释。真实场景就该是重叠的,模型学会输出低置信度的重叠区域,反而是正常的消防告警行为。

6. 想真正验证模型:用五折交叉验证和增强参数把这2056张图榨干

一次性train/val切分对2056张图来说信息量太薄,模型很可能背住了训练集的场景,而不是真正学会泛化。我建议在正式交付前,用五折交叉验证再确认一次模型的真实水平。

6.1 五折交叉验证:跑5次验证比跑1次更有说服力

把2056张图均匀切成5份,每份约411张。每次训练用其中4份做train,留1份做val,循环5次,得到5组mAP50和mAP50-95。取平均和标准差,如果标准差超过0.05,说明不同场景之间的难度差异较大,值得继续补数据或重新划分。五折训练5次对这个小数据集来说成本完全可控,但得到的泛化结论比单次划分可靠得多,这是判断模型能不能上线最扎实的一步。

6.2 固定随机种子与增强参数:保证你的实验能复现

训练命令里的seed=42控制数据shuffle、增强和初始化。固定种子后,同一台机器跑第二遍结果严格一致。增强方面,ultralytics默认的mosaic=1.0和mixup=0.0在这个任务上算是平衡配置。我习惯在调参前先关闭mosaic跑一次看基础指标,再开启mosaic看能提升多少;如果mosaic导致loss不收敛,先关掉再逐步打开。

五折交叉验证、固定随机种子、增强开关对照,这三件事拼在一起就是一套可复现的评估流程。我现在拿到任何新的数据集,都会先跑一遍这个流程确认基线,再决定是否投入调参。这份烟雾火焰数据集虽然自称已经标注好,但自己亲测过数据统计和验证闭环,才敢把模型交付到现场去用。希望帮到你。

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

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

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

立即咨询