☰
烟雾火焰数据集xml标签解析与YOLO训练实战
2026/10/11 8:11:25 网站建设 项目流程

简介:这份烟雾火焰数据集面向计算机视觉方向的学习者与算法开发者,尤其适合从事目标检测、火灾预警、安防监控等场景研究的人员使用。数据集包含烟雾与火焰两类目标,共2056张图像,均已人工完成标注,可直接用于YOLO、Faster R-CNN等检测模型的训练与验证。压缩包内共4118个文件,由2059个jpg图像与2059个xml标注文件一一对应组成,xml文件记录每张图中烟雾或火焰的类别与边界框坐标,整体包大小约168.41MB,目录结构规整,便于按类别或文件名快速检索与划分训练集、验证集。目前已有2327人学习下载,说明其在烟火检测任务中具有较高的实用参考价值。对于需要快速搭建烟火识别基线、验证标注质量或扩充自有数据集的读者,这份资源可省去大量采集与标注成本,直接投入模型训练与效果对比。

1. 烟雾火焰数据集:2056 张已标注 xml 标签,到底能跑什么任务

如果你正在做烟火检测,大概率经历过这样的阶段:网上找的公开数据集要么只有图片没有标注,要么标注格式五花八门,拿到手先花两天写解析脚本,模型还没开始训,人已经先累了。这份烟雾火焰数据集的核心价值就在这儿——2056 张图像,烟雾和火两类,全部配好了 xml 标签,省掉的是从零标注和格式对齐的时间。它适合谁?做安防监控烟火识别的、跑 YOLO 或 Faster R-CNN 想快速验证 pipeline 的、以及需要一个小规模干净数据集做课程设计或原型验证的从业者。xml 标签意味着它天然贴合 PASCAL VOC 体系,转 YOLO、COCO 都有成熟路径,不用自己从像素级重新标。下面我按「先看清结构、再动手转换、最后避坑」的顺序拆一遍,能直接抄的脚本我都放出来。

2. 先看清 xml 标签结构:VOC 格式里到底存了什么

2.1 一份 xml 文件拆开看,字段对应什么

拿到数据集先别急着写 dataloader,随便打开一个 xml 文件,你会看到类似这样的结构:

<annotation> <folder>smoke_fire</folder> <filename>0001.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>smoke</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>112</xmin> <ymin>56</ymin> <xmax>430</xmax> <ymax>390</ymax> </bndbox> </object> </annotation>

这里每个字段都有实际用途。filename是图片文件名,训练时靠它去关联图像路径;size里的宽高是原图尺寸,做坐标归一化时必须用它,不能想当然按 640×640 算;object可以出现多次,一张图里同时有烟和火就会有两个 object 块;name是类别名,这份数据集里就是 smoke 和 fire 两种;bndbox是左上角和右下角坐标,注意是绝对像素值,不是归一化值。difficult和truncated这两个字段很多人直接忽略,但在评估阶段,把 difficult=1 的样本排除掉能让指标更真实,尤其是烟雾这种边界模糊的目标。

2.2 类别分布和标注质量,决定你怎么切分

2056 张图、两个类别,听起来不多,但烟火检测本身类别少、目标大,这个量级做迁移学习是够用的。真正影响效果的是类别是否均衡。我一般会先跑一个统计脚本,把每类的框数量和图片数量拉出来:

import os import xml.etree.ElementTree as ET from collections import Counter xml_dir = "annotations" cls_counter = Counter() img_with_cls = Counter() for f in os.listdir(xml_dir): if not f.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, f)) root = tree.getroot() names_in_img = set() for obj in root.findall("object"): name = obj.find("name").text.strip() cls_counter[name] += 1 names_in_img.add(name) for n in names_in_img: img_with_cls[n] += 1 print("框数量统计:", cls_counter) print("含该类别的图片数:", img_with_cls)

这段脚本的逻辑很直白:遍历所有 xml,用 ElementTree 解析,对每个 object 取 name 计数,同时用 set 记录一张图里出现过哪些类别,避免同一张图多个同类框被重复计图片数。跑完你会得到类似 smoke 框数、fire 框数、以及各自覆盖多少张图。如果某一类图片数不到总数的 20%,切分时就要做分层抽样,否则验证集里可能一个 fire 样本都没有,mAP 直接失真。

提示:统计完先别删任何样本,哪怕发现个别框坐标越界(xmax 大于 width),也先记下来,后面统一处理,避免边统计边改导致数据对不上。

3. 把 xml 转成 YOLO 格式:转换脚本与四个边界坑

3.1 转换的核心公式和脚本

YOLO 格式要求每张图对应一个 txt,每行是class_id x_center y_center width height,全部归一化到 0~1。转换公式不复杂,但坑都在细节里:

import os import xml.etree.ElementTree as ET # 类别映射,顺序决定 class_id,必须和训练时的 names 一致 classes = ["smoke", "fire"] def convert(xml_path, out_dir, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in classes: continue cls_id = classes.index(name) bnd = obj.find("bndbox") xmin = float(bnd.find("xmin").text) ymin = float(bnd.find("ymin").text) xmax = float(bnd.find("xmax").text) ymax = float(bnd.find("ymax").text) # 坐标裁剪,防止越界 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) # 归一化中心点和宽高 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}") base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base + ".txt"), "w") as f: f.write("\n".join(lines))

逻辑说明:先按类别映射拿到 class_id,再把 bndbox 的四个坐标读出来,做一次裁剪保证不越界,然后按公式算中心点和宽高并归一化,最后写成同名 txt。参数上,classes列表的顺序就是最终标签里的数字,训练配置里的 names 必须和它一字不差,否则模型学出来的类别是错位的。img_w和img_h建议直接从 xml 的 size 字段读,而不是硬编码,因为这份数据集里图片尺寸未必统一。

3.2 四个容易翻车的边界情况

第一个坑是坐标越界。有些标注框的 xmax 会等于甚至略大于图片宽度,归一化后 w 可能大于 1,YOLO 训练时直接报错或产生异常梯度。上面脚本里的裁剪就是干这个的,宁可裁掉一两个像素,也别让非法值进训练。

第二个坑是空 txt 文件。如果一张图里所有 object 的类别都不在 classes 里,转换后会生成一个空 txt。YOLO 对空标签文件的处理是当作负样本,但如果你本意是漏标,就会引入噪声。建议转换后统计一下空文件数量,超过总数 5% 就回去查原因。

第三个坑是文件名大小写和扩展名不一致。xml 里写的 filename 可能是0001.JPG,而实际图片是0001.jpg,在 Linux 下直接找不到文件。稳妥做法是转换时以 xml 文件名(不含扩展名)为准去匹配图片,而不是信 filename 字段。

第四个坑是类别名有空格或大小写混用。比如Smoke和smoke会被当成两个类。转换前先统一 strip 和 lower,再映射。

注意:转换完别只看脚本没报错就完事,随机抽 5 张图把框画出来肉眼过一遍,这一步能拦住 80% 的坐标错位问题。

4. 训练前的数据切分与配置文件怎么写

4.1 分层切分,别让验证集缺类

2056 张图按 8:1:1 切,训练集约 1645 张,验证和测试各 205 张左右。但直接随机切有个风险:fire 样本如果本来就少,可能验证集里一个都没有。稳妥做法是按「图片包含的类别组合」分层。比如把图片分成「只有 smoke」「只有 fire」「两者都有」三组,每组按比例切,再合并。这样每类在三个集合里的占比都接近原始分布。

import random from collections import defaultdict # img_to_classes: {文件名: {类别集合}} groups = defaultdict(list) for img, cls_set in img_to_classes.items(): key = tuple(sorted(cls_set)) groups[key].append(img) train, val, test = [], [], [] for key, imgs in groups.items(): random.shuffle(imgs) n = len(imgs) n_train = int(n * 0.8) n_val = int(n * 0.1) train += imgs[:n_train] val += imgs[n_train:n_train + n_val] test += imgs[n_train + n_val:] print(len(train), len(val), len(test))

这段的关键是groups按类别组合分组,保证「只有 fire」这种小群体也能按比例进入三个集合。参数上 0.8/0.1/0.1 可以按需调,如果某组样本少于 10 张,建议整组进训练集,别硬切。

4.2 data.yaml 和训练命令

YOLO 系列的配置文件长这样:

path: ./smoke_fire train: images/train val: images/val test: images/test nc: 2 names: ["smoke", "fire"]

path是数据集根目录,train/val/test是相对路径下的图片文件夹,标签文件夹默认与图片同级、同名但换成 labels。nc是类别数,names顺序必须和转换脚本里的 classes 完全一致。训练命令常见的是:

yolo detect train data=smoke_fire.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16

imgsz=640是输入尺寸,如果原图普遍偏大且目标小,可以提到 960,但显存要跟上。batch=16在 8G 显存上跑 yolov8n 基本稳,跑更大的模型就往下调。epochs 100 对这个量级够用,配合早停更省事。

提示:第一次训练先把 epochs 设成 10 跑通全流程,确认数据路径、类别数、标签格式都没问题,再开长训练,能省下大量排查时间。

5. 避坑与排查:xml 标签训练时最常见的五个问题

5.1 现象:训练 loss 一直不降,mAP 接近 0

原因通常是类别映射错位。转换脚本里 classes 是["smoke", "fire"],但 data.yaml 里 names 写成了["fire", "smoke"],模型学到的 0 类其实是 fire,评估时对不上。解决方法是把两个文件里的类别顺序逐字比对,或者干脆用同一份配置生成两处。

5.2 现象:报错「No labels found」或大量图片被跳过

原因多半是标签文件夹路径不对。YOLO 默认图片在images/train,标签就在labels/train,文件名除扩展名外必须一致。如果你把 txt 和 xml 混在一个文件夹,或者图片是 jpg 而标签名带了_result后缀,就会被跳过。解决方法是转换后写个校验脚本,检查每张图是否有同名 txt。

5.3 现象:训练中途 loss 变 NaN

原因可能是坐标越界导致 w 或 h 为负,或者某张图尺寸读成了 0。回到转换脚本,确认裁剪逻辑生效,并且 img_w、img_h 是从 xml 的 size 字段读的而不是硬编码。如果个别 xml 的 size 缺失,直接跳过该样本并记录。

5.4 现象:验证集 mAP 波动极大

原因通常是验证集太小或类别分布不均。205 张验证图里如果 fire 只有十几张,一个样本的检测结果就能让 mAP 跳十几个点。解决办法是增大验证集比例到 15%,或者用交叉验证取平均。另一个可能是标注框本身抖动大,烟雾边界模糊,不同标注者画的框差异明显,这种只能靠数据量摊平。

5.5 现象:推理时框位置整体偏移

原因多半是训练和推理的预处理不一致。训练时用了 letterbox 填充,推理时如果直接 resize,坐标就会偏。检查推理脚本是否和训练用同一套预处理,尤其是 imgsz 和 padding 策略。这个坑很隐蔽,指标看着还行,实际部署就翻车。

6. 进阶技巧:用 xml 里的 difficult 字段做难例挖掘

xml 标签里那个容易被忽略的difficult字段,其实是个现成的难例标记。标注者认为难以辨认的目标会被标成 1,通常是烟雾稀薄、火焰被遮挡、或者目标极小的样本。常规训练会把这些样本一视同仁,但你可以用它做两件事。

第一件是在评估时排除 difficult=1 的框,得到「干净指标」,用来判断模型在明确目标上的真实水平。第二件更实用:训练完一轮后,把 difficult=1 的样本单独抽出来做一次推理,看哪些被漏检,这些就是下一轮该重点补的数据。我一般会写个脚本把 difficult 样本的图片路径和对应框导出来:

import os import xml.etree.ElementTree as ET hard_samples = [] for f in os.listdir("annotations"): if not f.endswith(".xml"): continue root = ET.parse(os.path.join("annotations", f)).getroot() for obj in root.findall("object"): diff = obj.find("difficult") if diff is not None and diff.text == "1": hard_samples.append(f) break print("难例数量:", len(hard_samples)) with open("hard_list.txt", "w") as fp: fp.write("\n".join(hard_samples))

拿到 hard_list.txt 后,可以在下一轮训练时对这些样本做过采样,或者单独微调。参数上,过采样倍数别超过 3,否则模型会过拟合这批难例,反而拉低整体表现。另一个技巧是把 difficult 样本按类别再分一次,如果 fire 的难例远多于 smoke,说明火焰的小目标和遮挡问题更严重,可以考虑针对性增强,比如 mosaic 里提高小目标拼接概率。

验证方法上,我习惯在每轮训练后固定跑一次难例子集,记录召回率变化。如果整体 mAP 涨了但难例召回没动,说明模型只是在简单样本上刷分,实际部署遇到遮挡场景照样漏。从那以后我每次拿到带 difficult 字段的 xml 数据集,都强制先跑一遍难例统计再开训,这个习惯帮我省下了不少「指标好看、上线翻车」的后悔药。希望帮到你。

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

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

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

立即咨询