news 2026/10/8 7:31:48

航空图像野火检测数据集:4500份XML标注的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
航空图像野火检测数据集:4500份XML标注的实战指南

简介:这份资源是面向野火探测与火灾识别方向的航空图像数据集,适合从事深度学习目标检测的研究者、算法工程师及高校学生使用,可支撑火灾预警、灾害监测等场景下的模型训练与验证。压缩包共包含2000个文件,全部为XML格式的标注文件,整体约165.02MB,每份标注以手动方式记录图像中目标对象的精确位置及对应标签,并借助Roboflow完成,保证了标注质量与一致性。数据集专门为野火探测设计,配套4500个图像注释文件,覆盖fire系列样本,便于直接接入主流检测框架进行训练与评估。目前已有362人学习下载,读者可借此获得结构清晰的标注数据,用于构建火灾识别模型、验证检测算法效果,或作为迁移学习与数据增强的素材,快速推进野火监测相关实验。

1. 航空图像野火检测数据集:4500 份 XML 标注到底怎么用

去年帮一个做林火预警的团队看模型,他们 mAP 卡在 0.4 上不去,第一反应是换 backbone、加注意力、调学习率,折腾两周没动静。我让他们把训练集里随机抽 50 张图连标注一起打开看,问题当场就露了:一半以上的火点框只框了明火,把烟雾和过火区域全漏了,模型学到的“火”其实是“亮橙色像素块”。这就是野火检测里最典型的翻车方式——数据没吃透,模型再花哨也是白搭。这份专门为探测野火设计的综合性航空图像数据集,价值就在这儿:它把 4500 个图像注释文件用 XML 格式手动标好,指定了图像中对象及其对应标签的确切位置,注释统一走 Roboflow 流程,保证整批数据的一致性和质量。它适合三类人:刚入门目标检测想找一个真实场景练手的新手、做林火/环保监测需要快速搭 baseline 的工程师、以及想验证自己数据清洗流程的老手。下面我按“先看清数据长什么样,再动手转格式、切分、训练,最后讲坑”的顺序拆一遍。

2. 先读懂 XML 标注:VOC 结构与野火标签的对应关系

2.1 一份 XML 里到底存了什么

这批注释文件命名形如fire-1044-_png.rf.a911498c3329c5db1b99383926264140.xml,文件名里的fire-1044是图像编号,.rf.后面那串哈希是 Roboflow 导出时生成的唯一标识,用来防止不同批次文件重名覆盖。打开任意一份 XML,结构是标准的 PASCAL VOC 格式,核心节点就几个:folder、filename、size(宽高和通道数)、以及一个或多个object。每个object下面挂name(类别名)、pose、truncated、difficult,再往下是bndbox,里面xmin/ymin/xmax/ymax四个值就是目标框的左上角和右下角坐标。

野火场景里,name通常不会只有fire一个类。常见做法是把明火、烟雾、过火区域分开标,因为这三者在航空视角下的纹理和颜色差异很大,混成一类会让模型在“有烟无火”的图上疯狂误检。你拿到数据后第一件事不是写训练脚本,而是把所有 XML 的name值统计一遍,看看类别分布。我一般会跑这么一段:

import glob import xml.etree.ElementTree as ET from collections import Counter # 统计所有 XML 中的类别名分布 counter = Counter() for xml_path in glob.glob("annotations/*.xml"): tree = ET.parse(xml_path) root = tree.getroot() for obj in root.findall("object"): name = obj.find("name").text.strip() counter[name] += 1 for cls, cnt in counter.most_common(): print(f"{cls}: {cnt}")

这段代码遍历注释目录,用ElementTree解析每个 XML,把object/name取出来计数。glob.glob的路径按你实际存放位置改,strip()是防止导出时类别名前后带空格导致统计出fire和fire两个类。跑完你会得到一张类别频次表,如果某个类少于总框数的 5%,训练时要么给它加权,要么先合并,否则模型基本学不动。

2.2 坐标是绝对像素还是归一化值

VOC 格式的bndbox存的是绝对像素坐标,不是 YOLO 那种 0~1 的归一化值。这意味着你转格式时必须拿size里的width和height去做除法。很多人转完发现框全挤在左上角,就是因为忘了除,或者除反了(拿 x 除 height)。验证方法很简单:随便挑一张图,用 OpenCV 把框画出来看一眼。

import cv2 import xml.etree.ElementTree as ET xml_path = "annotations/fire-1044-_png.rf.a911498c3329c5db1b99383926264140.xml" img_path = "images/fire-1044-_png.rf.a911498c3329c5db1b99383926264140.jpg" tree = ET.parse(xml_path) root = tree.getroot() img = cv2.imread(img_path) for obj in root.findall("object"): bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) ymin = int(bbox.find("ymin").text) xmax = int(bbox.find("xmax").text) ymax = int(bbox.find("ymax").text) # 在图上画矩形,颜色用绿色,线宽 2 cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.imwrite("check_1044.jpg", img)

int()转换是因为 XML 里坐标可能是浮点字符串,画框需要整数。cv2.rectangle的坐标顺序是(x, y),别写成(ymin, xmin)。画完打开图,如果框正好套住火点或烟雾区域,说明坐标没问题;如果框偏移或大小不对,回去检查size节点读的是不是对的图。这一步花两分钟,能省掉后面几小时的排查。

2.3 为什么选 VOC 而不是直接上 COCO

这批数据给的是 XML,不是 COCO JSON。有人会问要不要先转 COCO 再转 YOLO,我的建议是别绕。VOC 结构简单,解析成本低,直接写脚本转成 YOLO 的 txt 或者 COCO 的 json 都行。COCO 的优势在于支持分割掩码和关键点,但野火检测目前主流还是矩形框,用不上那些字段。多转一道手,就多一个引入坐标错误的机会。常见做法是:如果训练框架用 YOLO 系列,直接 VOC→YOLO txt;如果用 Detectron2 或 MMDetection,再转 COCO。转换脚本我放在下一章。

3. 从 XML 到训练集:转换、切分与类别平衡的完整脚本

3.1 VOC 转 YOLO txt 的转换脚本

YOLO 格式每张图对应一个 txt,每行是class_id x_center y_center width height,全部归一化到 0~1。下面这个脚本把 XML 批量转成 YOLO 标签,同时生成classes.txt。

import glob import os import xml.etree.ElementTree as ET # 类别列表,顺序决定 class_id,必须和训练配置一致 classes = ["fire", "smoke", "burned"] # 建立类别到 id 的映射 cls_map = {name: idx for idx, name in enumerate(classes)} def convert(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) # 用文件名(不含扩展名)作为输出 txt 名 base = os.path.splitext(os.path.basename(xml_path))[0] lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in cls_map: 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) # 归一化并转为中心点+宽高 x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_map[name]} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") out_path = os.path.join(out_dir, base + ".txt") with open(out_path, "w") as f: f.write("\n".join(lines)) os.makedirs("labels", exist_ok=True) for xml_path in glob.glob("annotations/*.xml"): convert(xml_path, "labels")

classes列表的顺序就是class_id,训练时的names配置必须和它一字不差,否则模型会把烟雾当火。if name not in cls_map: continue这行是保险丝,防止 XML 里出现你没预料到的类别导致整个脚本崩掉。归一化时.6f保留六位小数,YOLO 官方实现能接受,精度也够。转完检查一下 txt 行数是否和 XML 里的object数量一致,不一致就说明有类别被跳过了。

3.2 训练集/验证集切分:别用随机切分

目标检测数据切分有个血泪经验:不能简单random.shuffle然后按比例切。因为航空图像往往是连续帧或者同一区域的多角度拍摄,随机切分会让同一场景的图同时出现在训练集和验证集里,验证指标虚高,上线就崩。正确做法是按图像编号或拍摄区域分组切分。文件名里的fire-1044这种编号,如果相邻编号是同一片火场,就按编号段切。

import glob import os import random # 按文件名前缀分组,这里假设 fire-XXXX 中 XXXX 连续的是同一区域 all_xml = sorted(glob.glob("annotations/*.xml")) # 提取编号并排序 def get_id(path): base = os.path.basename(path) return int(base.split("-")[1]) all_xml.sort(key=get_id) # 按 8:2 切分,前 80% 训练,后 20% 验证 split_idx = int(len(all_xml) * 0.8) train_xml = all_xml[:split_idx] val_xml = all_xml[split_idx:] with open("train.txt", "w") as f: f.write("\n".join(train_xml)) with open("val.txt", "w") as f: f.write("\n".join(val_xml))

get_id从文件名里抠出编号,sort保证顺序稳定。按编号排序后切分,能最大程度让同一区域的图落在同一侧。如果你的数据编号本身是打乱的,那就得靠图像内容做聚类分组,或者至少用hash按文件名分组。切完看一眼两个 txt 的行数比例,8:2 是常规起点,数据量少于 2000 张时建议 7:3,给验证集多留点。

3.3 类别不平衡怎么处理

野火数据天然不平衡:明火框多,烟雾框少,过火区域更少。统计完类别频次后,如果发现smoke只有fire的十分之一,有三个处理方向。一是数据层面,对少类图像做增强(旋转、亮度扰动、加噪声),但注意别把火点增强成不真实的颜色。二是损失层面,YOLO 系列可以在hyp配置里调cls_pw(类别权重),少类给大权重。三是评估层面,别只看 mAP,单独看少类的 recall,否则模型全预测fire也能有不错的整体指标。我一般先跑一版不加权的,看混淆矩阵,再决定要不要动权重。常见做法是cls_pw从 1.0 开始,少类 recall 低于 0.5 时逐步加到 2.0 左右,加太多会导致多类误检飙升。

4. 训练与验证:用 YOLOv8 跑通野火检测 baseline

4.1 数据配置文件怎么写

YOLOv8 需要一份 yaml 描述数据路径和类别。假设目录结构是images/train、images/val、labels/train、labels/val,yaml 这么写:

# fire_dataset.yaml path: /data/fire_dataset # 数据集根目录 train: images/train val: images/val names: 0: fire 1: smoke 2: burned

path用绝对路径最稳,相对路径在不同工作目录下容易找不到。names的键值必须和转换脚本里的classes顺序完全一致。写完 yaml 先别急着训,用yolo checks确认环境没问题,再跑一版 1 epoch 的冒烟测试,看 loss 有没有正常下降、有没有报“no labels found”。

4.2 启动训练与关键参数

yolo detect train \ data=fire_dataset.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/fire \ name=baseline

model=yolov8s.pt是速度和精度的折中,数据量 4500 张左右够用,显存小就换yolov8n.pt。imgsz=640是默认值,航空图像里火点可能很小,如果发现小目标漏检多,可以提到 1024,但显存和训练时间会涨。patience=20表示 20 个 epoch 验证指标不升就早停,防止过拟合。lr0=0.01是 SGD 的初始学习率,用 Adam 的话降到 0.001。训练过程中重点看mAP50-95和各类的recall,如果fire的 recall 高但smoke低,回去看 3.3 的类别平衡。

4.3 验证与可视化排查

训练完在验证集上跑:

yolo detect val \ model=runs/fire/baseline/weights/best.pt \ data=fire_dataset.yaml \ imgsz=640 \ conf=0.25 \ iou=0.5

conf=0.25是置信度阈值,低于它的框不输出。iou=0.5是 NMS 的 IoU 阈值,野火场景里烟雾区域可能重叠,IoU 设太高会保留大量重复框,设太低会漏掉相邻火点。跑完看混淆矩阵,如果background那一列(误检)特别高,说明模型把云、夕阳、红色屋顶当成了火,需要加负样本。可视化预测结果用yolo detect predict,把预测图导出来人工过一遍,比看数字直观得多。

5. 避坑与排查:野火数据集落地时最容易翻车的五件事

5.1 现象:训练 loss 正常下降但 mAP 一直是 0

原因通常是类别名不匹配。XML 里写的是Fire或FIRE,转换脚本里classes写的是fire,cls_map匹配不上,所有框被continue跳过,生成的 txt 全是空的。解决:转换后立刻检查 txt 文件大小,空文件就是信号;或者在转换脚本里加name = name.lower()统一大小写。

5.2 现象:验证集指标很高,实际部署误检爆炸

原因是随机切分导致训练集和验证集有同场景图像,模型记住了背景而不是学到了火的特征。解决:按 3.2 的分组方式重新切分,确保同一火场、同一拍摄批次的图只出现在一侧。切完再训一版,指标会降,但那个才是真实水平。

5.3 现象:小目标火点大量漏检

原因是imgsz太小或者 anchor 不匹配。航空图像里早期火点可能只有十几个像素,640 分辨率下经过多次下采样就没了。解决:把imgsz提到 1024 或 1280,同时在数据增强里关掉mosaic的最后几个 epoch(YOLO 有close_mosaic参数),让模型在真实尺度上收敛。

5.4 现象:烟雾和云被混为一类

原因是负样本不足。数据集里如果只有火和烟的正样本,模型没见过云,就会把白色团状物都判成烟。解决:从数据集中挑出含云但不含火的图,作为背景负样本加入训练,或者用burned类之外的图做 hard negative mining。常见做法是负样本占训练集 10%~20%。

5.5 现象:XML 解析报ParseError

原因是文件编码不是 UTF-8,或者 XML 里有非法字符。Roboflow 导出的文件偶尔会带 BOM 头。解决:用ET.parse前先以二进制读入并去掉 BOM,或者用lxml的recover=True模式容错解析。批量处理时加try/except跳过坏文件并记录,别让一个坏文件卡住整个流程。

6. 进阶技巧:用分层采样和 TTA 把少类召回再拉一截

baseline 跑通之后,如果smoke或burned的召回还是上不去,有两个成本低、见效快的技巧。第一个是分层采样:重写 DataLoader 的 sampler,让每个 batch 里少类样本的出现概率和fire接近。YOLO 官方没直接暴露这个接口,但可以在生成训练 txt 时对少类图像做重复采样(oversampling),把少类图的路径在train.txt里多写几遍。注意别复制太多,一般少类重复 2~3 倍就够,再多会过拟合。

第二个是测试时增强(TTA)。推理时把同一张图做水平翻转、多尺度缩放,分别预测后再做 NMS 融合。YOLOv8 的val和predict都支持augment=True:

yolo detect predict \ model=runs/fire/baseline/weights/best.pt \ source=test_images/ \ imgsz=1024 \ conf=0.2 \ augment=True \ save_txt=True

augment=True会启用 TTA,conf可以比验证时再低一点,因为 TTA 融合后误检会被 NMS 压掉一部分。代价是推理速度慢 2~3 倍,实时预警场景要权衡。我一般只在离线复核时开 TTA,线上还是用单尺度。

还有一个容易被忽略的点:验证时把save_txt=True打开,导出预测框的 txt,和真值 XML 转出来的 txt 做逐类对比,能精确算出每个类的漏检和误检数量,比看混淆矩阵更细。这个对比脚本我习惯每次训完都跑一遍,花不了几分钟,但能提前发现“整体 mAP 涨了但 smoke 反而降了”这种隐蔽退化。从那以后我每次动数据增强或损失权重,都强制走一遍逐类对比,再决定这版模型要不要留。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 7:30:58

零依赖手写UTF-8编解码:C语言字符处理实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:30:37

工业级电源路径保护:TPS259483与dsPIC33EP协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:29:57

基于TLS握手特征的加密恶意流量检测实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:28:12

前端工程规范落地周报:用机器可执行的门禁守住代码质量红线

几乎每一个前端团队都经历过这样尴尬的局面:技术专家花了几周时间精心编写了一份长达几十页的《前端架构与代码开发规范》,在团队周会上组织全员宣贯,大家一致鼓掌通过并庄重归档到团队 Wiki 知识库中。然而仅仅一个月后去看代码仓库&#xf…

作者头像 李华