航拍森林火灾检测数据集:VOC/YOLO双格式对齐与训练避坑

发布时间:2026/10/25 14:35:47
航拍森林火灾检测数据集:VOC/YOLO双格式对齐与训练避坑

简介:面向森林火灾智能监测与航拍目标检测研究者的数据集说明文档,围绕俯拍、航拍场景下的火焰与烟雾识别任务,帮助读者快速了解一份VOC与YOLO双格式标注数据的构成与使用边界。文档系统梳理了6116张图像及对应标注:VOC提供jpg图像与xml文件,YOLO提供txt标签,类别为fire与smoke,标注框分别达15380和12613个,总计27993个,并交代labelImg矩形框标注流程、类别定义与标注示例预览,便于在训练前核对标注规则和数据质量。压缩包共1个docx文件,约5.06MB,以文字说明、统计信息和预览节选为主,适合作为数据检索、方案选型与实验记录的依据。当前已有62人学习。读者可从中获取数据规模、格式差异、类别分布及精度免责声明等关键信息,为森林火灾早期发现模型的预处理、训练配置与评估验证提供参考。

1. 俯拍航拍森林火灾检测数据集:6116张双格式标注先看什么

拿到一份俯拍航拍森林火灾检测数据集,很多人的第一反应是把6116张图直接塞进训练脚本,真正容易翻车的却是VOC与YOLO两套标注是否逐张对齐。这个数据集面向俯拍航拍视角的森林火灾检测,2类别为 fire 和 smoke,提供jpg、xml、txt三类文件,标注框合计27993个,适合做2类别目标检测训练、格式转换校验和烟雾小目标增强实验。它更适合已经能跑通检测流程、需要补足火烟场景样本的从业者;不适合把它当成精度承诺,标注只解决“标签在哪”,模型效果仍由划分、增强和调参决定。

2. 拆开VOC与YOLO双格式:27993个框的坐标对齐与统计口径

双格式不是简单“多给一份标注”。VOC的xml是绝对像素坐标,YOLO的txt是归一化中心点坐标,两者差一个换算层;如果文件名、类别顺序或边界裁剪没对齐,训练日志里的loss会看起来正常,验证可视化却满屏错框。先把6116组三件套的对应关系锁死,再谈统计和转换,后面训练才有意义。

2.1 先核对目录与文件命名:6116组三件套缺一不可

这份数据集没有附带划分路径的txt文件,只有图片和对应的VOC xml、YOLO txt,所以目录结构要自己整理。我一般会把图片、xml、txt分开放,用文件stem做唯一键,先检查jpg集合、xml集合、txt集合是否一一相等。不要用“文件数量都是6116”代替逐张核对,数量相等但文件名错位的情况并不少见,尤其是从压缩包解压后混入系统生成的隐藏文件时。

from pathlib import Path import xml.etree.ElementTree as ET from collections import Counter, defaultdict IMG_DIR = Path("images") # jpg 所在目录 XML_DIR = Path("annotations/voc") # VOC xml 目录 TXT_DIR = Path("labels/yolo") # YOLO txt 目录 CLASSES = ["fire", "smoke"] jpg = {p.stem for p in IMG_DIR.glob("*.jpg")} xml = {p.stem for p in XML_DIR.glob("*.xml")} txt = {p.stem for p in TXT_DIR.glob("*.txt")} print("jpg", len(jpg), "xml", len(xml), "txt", len(txt)) print("缺xml", sorted(jpg - xml)[:5]) print("缺txt", sorted(jpg - txt)[:5]) print("多余xml", sorted(xml - jpg)[:5]) print("多余txt", sorted(txt - jpg)[:5]) box_counter = Counter() per_image = defaultdict(int) for p in XML_DIR.glob("*.xml"): root = ET.parse(p).getroot() for obj in root.findall("object"): name = obj.find("name").text.strip() box_counter[name] += 1 per_image[p.stem] += 1 print("类别框数", dict(box_counter)) print("总框数", sum(box_counter.values())) print("每图平均框数", round(sum(per_image.values()) / max(len(per_image), 1), 3))

这段脚本先做三集合差集,逻辑很直接:jpg - xml是有图没VOC标注,jpg - txt是有图没YOLO标注,反向差集则是标注多余。后面解析xml只统计object/name和每图框数,不做坐标转换,用来快速判断类别分布和图片拥挤程度。参数上只需要改三个目录和CLASSES,如果类别名里出现空格或大小写不一致,name.strip()之后还要统一小写再比对,否则统计会把同一类拆成两个键。

对比项VOC xmlYOLO txt
坐标形式xmin、ymin、xmax、ymax,绝对像素class、cx、cy、w、h,归一化到0到1
类别表达name文本,如 fire类别索引,如 0 或 1
主要用途人工核对、转换源、可视化回画直接喂给YOLO训练脚本
常见错位文件名stem不一致类别索引与data.yaml不一致

提示:先跑一遍集合核对,再跑坐标统计。文件名错位时谈框数没有意义,统计结果会把你带偏。

2.2 VOC xml 转 YOLO txt 的坐标换算与边界裁剪

VOC转YOLO的核心公式不复杂:cx = ((xmin + xmax) / 2) / width,cy = ((ymin + ymax) / 2) / height,bw = (xmax - xmin) / width,bh = (ymax - ymin) / height。容易出错的是边界裁剪和无效框过滤。labelImg画框时偶尔会留下越界坐标,或者xmax小于xmin的拖拽残影,直接归一化会得到负数或大于1的值,训练脚本不一定报错,但框会飞到图像外面。转换时统一裁到[0, width]和[0, height],再判断宽高是否大于0,能挡掉大部分脏框。

from pathlib import Path import xml.etree.ElementTree as ET SRC_XML = Path("annotations/voc") DST_TXT = Path("labels/yolo_converted") DST_TXT.mkdir(parents=True, exist_ok=True) CLASSES = ["fire", "smoke"] CLASS_TO_ID = {name: i for i, name in enumerate(CLASSES)} def clamp(v, lo, hi): return max(lo, min(hi, v)) for xml_path in SRC_XML.glob("*.xml"): root = ET.parse(xml_path).getroot() size = root.find("size") w = float(size.find("width").text) h = float(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASS_TO_ID: continue bnd = obj.find("bndbox") xmin = clamp(float(bnd.find("xmin").text), 0, w) ymin = clamp(float(bnd.find("ymin").text), 0, h) xmax = clamp(float(bnd.find("xmax").text), 0, w) ymax = clamp(float(bnd.find("ymax").text), 0, h) if xmax <= xmin or ymax <= ymin: continue cx = ((xmin + xmax) / 2.0) / w cy = ((ymin + ymax) / 2.0) / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{CLASS_TO_ID[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") (DST_TXT / f"{xml_path.stem}.txt").write_text("\n".join(lines), encoding="utf-8")

这段脚本把xml逐张转成同名txt,CLASS_TO_ID决定fire是0还是smoke是0,必须和后面训练配置完全一致。clamp负责把越界坐标压回图像范围,xmax <= xmin或ymax <= ymin的框直接丢弃,避免写出宽高为负的标签。保留6位小数是为了减少归一化误差,对6116张图、27993个框这种量级足够用。转换完不要立刻开训,抽10张把txt画回原图,看框是否贴合火点或烟雾边缘,这一步比盯着loss曲线有用。

2.3 用统计结果判断数据分布:fire 15380 与 smoke 12613 怎么读

按给定标注统计,fire有15380个框,smoke有12613个框,总框数27993。按6116张图摊开,平均每张图约4.58个框,说明不少图片里同时存在多个火点或烟雾区域,训练时不能假设“一图一目标”。fire框数占比约54.9%,smoke约45.1%,两类并不悬殊,但烟雾的视觉形态更麻烦:颜色接近背景、边缘扩散、对比度低,框往往偏大且重叠多。框数均衡不代表难度均衡,smoke的召回通常更值得盯。

类别框数占全部框按6116张图平均
fire15380约54.9%约2.51
smoke12613约45.1%约2.06
合计27993100%约4.58

统计时还要看每张图的框数分布和空标注数量。如果某些图片的txt是0字节,那是纯背景负样本,不要当脏数据删掉;如果一张图里有几十个框,先确认是不是把大片烟雾拆成了多个小框,或者把同一火点重复标了多次。对俯拍航拍场景,框的宽高比也能说明问题:火点通常偏小、偏圆,烟雾容易拉长成不规则大片。后面做增强时,scale和mosaic可以多一点,但mixup要克制,低对比度烟雾被混合后更容易糊成背景。

3. 把数据集接进YOLO训练:2类别配置、640输入与小目标增强

格式对齐之后,下一步是让训练配置和标注索引完全一致。这里最常见的误用是“拿到txt就直接训”,不管类别顺序、不划分验证集、不检查空标注。YOLO训练脚本对格式很宽容,错标签也能跑出下降的loss,所以配置层要自己卡死:nc=2、names顺序、train/val路径、图片和标签的stem对应,四项都对上再开训。

3.1 data.yaml 与类别映射:fire/smoke 顺序不能靠猜

训练配置里最重要的是类别索引。转换脚本里CLASSES = ["fire", "smoke"],那么fire就是0,smoke就是1;如果原始YOLO txt的类别索引不是这个顺序,要么重转,要么改names,不能只改nc。我一般会在data.yaml里显式写出0和1,避免不同框架对names列表的解析差异。目录上让images/train和labels/train保持同名stem,验证集同理,这样训练脚本按路径推断标签时不会串。

# configs/fire_smoke.yaml path: /data/fire_smoke_6116 train: images/train val: images/val test: images/test nc: 2 names: 0: fire 1: smoke

path是数据集根目录,train、val、test写相对路径,根目录下要有对应的images和labels结构。nc必须是2,names的键值要和txt里的类别索引对齐。如果暂时没有test集,可以先把test留空或复用val,但不要把它当成调参集反复看。没有官方划分文件时,常见做法是按8:1:1或7:2:1切分,但切分要按场景分组,不能简单随机抽图,原因在避坑章展开。

3.2 训练命令与关键超参:imgsz、batch、lr0 怎么落

训练命令以常见YOLO命令行接口为例,模型权重路径换成自己手里的预训练权重。输入尺寸先定640,如果烟雾在图中占比很小,可以试960,但显存和速度会明显变化。batch从8或16起步,显存不够就降batch,不要硬撑导致最后几个batch的统计量抖动。学习率lr0=0.01是常见起点,小数据集或微调时可以降到0.005;lrf=0.01控制末端学习率衰减,patience=30用于早停,避免验证指标长时间不升还继续烧卡。

# 以常见 YOLO 训练命令行接口为例,权重路径换成自己的 yolo detect train \ model=weights/pretrained.pt \ data=configs/fire_smoke.yaml \ imgsz=640 \ epochs=120 \ batch=16 \ workers=4 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ project=runs/fire_smoke \ name=baseline_640

imgsz=640是速度和精度的折中,俯拍图里烟雾如果经常只占几十个像素,640下经过下采样会更小,所以第一轮不要只看mAP,先把推理可视化打开。epochs=120不是硬指标,早停触发就停;workers=4按机器CPU核数调,太大反而拖慢数据加载。project和name决定输出目录,建议把baseline、增强版、换尺寸版分开命名,后面比较结果时不会翻错文件夹。

参数建议起点作用调整信号
imgsz640,烟雾偏小试960输入分辨率小框漏检多就加,速度不够就降
batch8或16批统计稳定性显存不足先降batch
lr00.01,微调0.005初始学习率loss震荡大就降
patience30早停耐心轮数验证指标长期不升可减
mosaic0.8左右多图拼接增强小目标多可保留,纯背景多要下调

注意:不要用验证集反复调conf和iou再报告mAP。验证集参与调参后,指标就不再是干净评估,后面换场景会掉得很难看。

3.3 小目标烟雾的增强策略:mosaic、mixup 与尺度抖动怎么配

烟雾的难点是低对比度和边缘模糊,增强不能只堆强度。mosaic能增加小目标和多目标共存,适合俯拍图里多个火点、多个烟羽的情况;scale和translate让目标出现在不同位置,减少模型依赖固定构图;hsv_s和hsv_v可以模拟烟羽颜色变化和光照差异。mixup要谨慎,烟雾本来就和背景接近,混合过度会让标签区域更模糊,训练集看起来更“难”,验证集反而学不到稳定特征。俯拍场景可以保留水平翻转,垂直翻转按数据来源决定,如果航线方向固定,垂直翻转比例不要太高。

# configs/aug_smoke.yaml hsv_h: 0.015 hsv_s: 0.6 hsv_v: 0.4 degrees: 5.0 translate: 0.1 scale: 0.5 shear: 2.0 perspective: 0.0 flipud: 0.2 fliplr: 0.5 mosaic: 0.8 mixup: 0.1 copy_paste: 0.0

这份增强配置里,scale: 0.5让目标有一定尺度抖动,mosaic: 0.8保留较高多图拼接概率,mixup: 0.1只做轻微混合,避免烟雾被过度糊化。flipud: 0.2和fliplr: 0.5针对俯拍视角,如果数据来自固定航线且方向信息有意义,可以把flipud降到0。copy_paste对烟雾这种不规则目标不一定合适,先关掉,等baseline稳定后再试。增强参数不是越多越好,训练集mAP上升但验证集不动的增强,基本可以撤掉。

4. 训练后别只看mAP:混淆矩阵、PR曲线与可视化抽查

训练日志里的mAP是汇总值,2类别火烟检测更该看每类召回和混淆方向。fire和smoke在远景里可能同时出现,烟雾又容易扩散到火点周围,混淆矩阵能告诉你模型是把烟判成火,还是把背景判成烟。评估阶段先把验证集固定,再谈阈值;阈值扫得太随意,最后得到的指标没法复现。

4.1 划分验证集与评估命令:先固定随机种子再谈指标

验证集要和训练集同分布但不能同场景,划分时固定随机种子,记录哪些文件进了val。评估命令里conf=0.001是为了算完整PR曲线,不要用0.25这种部署阈值去评估mAP;iou=0.6是NMS的IoU阈值,和训练时的框匹配阈值不是一回事。如果验证集里烟雾样本很少,mAP会被fire主导,最好再单独看smoke的recall。

yolo detect val \ model=runs/fire_smoke/baseline_640/weights/best.pt \ data=configs/fire_smoke.yaml \ imgsz=640 \ batch=16 \ conf=0.001 \ iou=0.6 \ project=runs/fire_smoke_val \ name=eval_640

model指向训练产出的best权重,data必须和训练时同一份yaml,imgsz也要一致,否则小目标评估会偏差。conf=0.001保留低置信度预测,让PR曲线完整;iou=0.6用于NMS合并重叠框,烟雾大片重叠时这个值会影响召回。评估输出目录里重点看混淆矩阵、PR曲线和每类指标表,不要只抄一个mAP50就结束。

4.2 看混淆矩阵定位 fire/smoke 混淆:背景框和烟雾漏检怎么读

混淆矩阵的横纵轴分别是真实类别和预测类别,背景也会作为一类出现。fire被预测成smoke,说明颜色和纹理特征没分开,常见于小火点被大片烟雾包住;smoke被预测成背景,说明低对比度烟羽没被激活,常见于远景薄烟。背景列偏高则是误检,背景行偏高是漏检,两者处理方式不同:误检多先回捞负样本和调conf,漏检多先查标注覆盖和增强强度。

观察点可能原因优先动作
fire与smoke互相混淆类别索引错位或视觉特征重叠先核对names顺序,再看可视化
smoke大量进背景薄烟对比度低、框太小提高输入尺寸,补烟雾样本,调低conf
背景大量进fire/smoke负样本不足或conf过低回捞纯背景图,保留空txt
fire召回高、smoke召回低类别难度不均衡单独看smoke的PR曲线和框尺寸分布

每类指标要分开看:precision高、recall低,说明模型保守,阈值可以降一点;precision低、recall高,说明误检多,先查背景样本和标注质量。mAP50到mAP50-95掉得厉害,通常是框定位不准,烟雾边缘模糊时更明显,不一定是分类问题。

4.3 推理可视化与阈值选择:conf 0.25 只是起点

部署阈值和评估阈值要分开。评估用低conf看全貌,部署时从0.25起步,再按业务接受度扫0.1、0.25、0.4。烟雾密集时NMS的iou可以降到0.4到0.5,避免一片烟只留一个框;火点分散时iou可以高一点。可视化不要只抽成功案例,专门挑漏检、误检、重叠框各10张,才能看出阈值和标注的边界。

yolo detect predict \ model=runs/fire_smoke/baseline_640/weights/best.pt \ source=images/val \ imgsz=640 \ conf=0.25 \ iou=0.5 \ save_txt=True \ save_conf=True \ project=runs/fire_smoke_pred \ name=val_conf025_iou050

conf=0.25是部署起点,iou=0.5控制重叠框合并,save_txt=True把预测结果落盘,方便和真值对比。save_conf=True保留置信度,后面做阈值扫描时不用重新推理。如果烟雾大片只出一个框,先把iou降到0.4看是否改善;如果背景误检多,先把conf升到0.4看误检是否来自低置信度。阈值没有全局最优,只有当前场景的折中。

5. 避坑与排查:双格式坐标、空标注与烟雾低对比度的5个坑

双格式数据集的问题往往不在训练脚本,而在“看起来能跑”的细节。下面5个坑按出现频率排,前两个是格式层,后三个是训练和评估层。每个都按现象、原因、解决写,照着排查能省掉反复重训。

5.1 坑一:VOC与YOLO坐标顺序搞反:框飞到左上角

现象:验证可视化里框集中在左上角,或者框宽高很小、几乎为0;训练loss能降,但mAP极低。

原因:把xmin, ymin, xmax, ymax直接写进txt,或者把归一化的cx, cy, w, h当成绝对坐标。YOLO txt的顺序是class cx cy w h,不是class xmin ymin xmax ymax,两者混用后坐标范围完全不对。

解决:用2.2的转换脚本重转,随机抽10张把txt画回原图。再跑一遍范围检查,cx和cy必须在0到1之间,w和h必须大于0且不超过1。发现越界先查转换公式,不要先去改模型。

from pathlib import Path bad = [] for p in Path("labels/yolo").glob("*.txt"): for line in p.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue cls, cx, cy, w, h = line.split() vals = list(map(float, [cx, cy, w, h])) if not (0 <= vals[0] <= 1 and 0 <= vals[1] <= 1 and 0 < vals[2] <= 1 and 0 < vals[3] <= 1): bad.append((p.name, line)) break print("异常文件数", len(bad)) print(bad[:5])

这段检查只做范围校验,不判断框是否贴合目标,所以还要配合可视化。bad里出现文件名就先看对应图片,确认是转换问题还是原始xml越界。范围检查通过但框仍然偏移,重点查width和height取的是不是原图尺寸,有些xml的size节点和实际jpg分辨率不一致,转换前要以实际图片尺寸为准。

5.2 坑二:空txt与纯背景图被误删:smoke 召回莫名下降

现象:清洗脚本把0字节txt删掉后,训练集只剩有目标的图,验证集烟雾召回下降,背景误检反而变多。

原因:YOLO格式里空txt表示纯背景,是负样本。删掉空txt等于告诉模型“所有图里都有火或烟”,模型没见过无目标背景,遇到云层、阴影、反光就容易误检。

解决:保留空txt和对应图片。先统计空标注比例,如果空标注很少,保留即可;如果空标注很多,按比例抽样,不要让背景图淹没正样本。清洗脚本里把“txt为空”和“txt缺失”分开处理,缺失是错位,为空是负样本,两者不能一起删。

5.3 坑三:烟雾框重叠与NMS阈值过高:一片烟只留一个框

现象:可视化里大片烟雾只出一个框,相邻火点被吞掉,或者同一片烟被重复框出很多小框。

原因:烟雾标注常有大框重叠,NMS的iou阈值过高会合并过度;conf阈值过高会漏掉低对比度烟雾。评估阶段用0.001看全貌,部署阶段直接套0.5以上的iou,容易把重叠目标压成一个。

解决:评估用conf=0.001、iou=0.6看PR曲线;部署时扫conf从0.1到0.4、iou从0.4到0.6,按漏检和误检的接受度选点。烟雾密集场景优先降iou,火点分散场景再考虑升conf。不要用验证集mAP最高的阈值直接上线,业务误检成本不同,阈值要单独定。

5.4 坑四:按图片随机划分导致同场景泄漏:验证集虚高

现象:验证集mAP很高,换一段航拍视频或另一个架次就崩,烟雾召回掉得特别明显。

原因:同一段航线连续帧的纹理、光照、烟羽形态几乎一样,随机分到train和val后,验证集对模型来说近似“见过的图”。这种泄漏会让指标虚高,实际跨场景泛化差。

解决:按架次、场景或时间窗口分组划分,同一组只进train或只进val。文件名有场景前缀时按前缀分组;没有前缀时按拍摄时间分桶,或者按文件夹分。分组后固定随机种子,把train/val文件列表落盘,后续复现实验直接用同一份列表。

from pathlib import Path import random from collections import defaultdict random.seed(42) groups = defaultdict(list) for p in Path("images").glob("*.jpg"): # 假设文件名形如 sceneA_0001.jpg,按前缀分组 group = p.stem.split("_")[0] groups[group].append(p.stem) keys = list(groups) random.shuffle(keys) n_val = max(1, int(len(keys) * 0.2)) val_groups = set(keys[:n_val]) train, val = [], [] for g in keys: (val if g in val_groups else train).extend(groups[g]) Path("splits").mkdir(exist_ok=True) Path("splits/train.txt").write_text("\n".join(train), encoding="utf-8") Path("splits/val.txt").write_text("\n".join(val), encoding="utf-8") print(len(train), len(val), "val groups", sorted(val_groups))

这段脚本按文件名前缀分组,n_val控制验证组比例,random.seed(42)保证划分可复现。文件名没有场景前缀时不能硬套,改成按文件夹或时间字段分组。划分完检查val组列表,确认没有同一场景同时出现在train和val,否则mAP再高也不能信。

5.5 坑五:换模型只改nc不改names:类别索引错位

现象:推理时fire显示成smoke,或者两个类别概率互换,混淆矩阵里两类对调。

原因:data.yaml里nc=2改了,但names顺序没改;或者转换脚本的CLASSES和训练配置的names不一致。模型只认索引,不认文本,索引0被解释成什么完全由配置决定。

解决:固定一份类别映射,转换、训练、评估、推理都用同一份。data.yaml里显式写0: fire、1: smoke,训练前抽一张图打印txt类别并叠框核对。换模型或换框架时,先检查names,再检查nc,最后才看权重加载。

6. 进阶技巧:用分层抽样和负样本回捞把烟雾召回拉上来

烟雾召回上不去,很多时候不是模型容量不够,而是验证集里烟雾组合太少、负样本又被清掉了。我一般先按每张图的fire框数、smoke框数分桶,再按桶比例抽train/val,保证验证集里同时有“只有fire”“只有smoke”“两者都有”“纯背景”四类样本。这样调conf和iou时,看到的是烟雾真实边界,不会被fire样本掩盖。

分桶判定方式抽样注意
bothfire框数>0且smoke框数>0验证集必须保留,观察共存场景
fire_only只有fire框防止fire主导指标
smoke_only只有smoke框烟雾召回的关键桶
bg空txt保留一部分做负样本

训练后把验证集里误检最高的背景图回捞进训练集,空txt继续保留,不要因为“没有目标”就删。部署阈值也别照搬评估阈值:conf从0.25起步,iou从0.5起步,烟雾密集时把iou降到0.4看重叠框是否回来。从那以后我每次拿到双格式数据集,都先跑统计、抽10张叠框、再按场景分组划分,最后才

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