简介:俯拍航拍森林火灾检测数据集配套文档,面向目标检测算法开发者与森林火灾监测系统设计者,系统介绍一套可用于模型训练和测试的VOC+YOLO双格式数据集,解决俯拍场景下火与烟雾的检测定位问题。文档共1个docx文件,压缩包大小约5.06MB,包含数据格式约定、图像与标注规模、类别分布、标注工具及规则等说明,并附图片预览和标注示例。数据部分包括6116张jpg图像,对应6116个VOC格式xml标注文件和6116个YOLO格式txt标注文件;标注类别为fire和smoke,fire框数为15380,smoke框数为12613,总框数27993,均由labelImg以画矩形框方式完成。文档明确说明数据集仅保证标注准确合理,不对训练所得模型精度作任何承诺,提醒使用者结合预处理、模型结构和调参自行验证。目前已有62人学习,适合需要快速掌握数据集结构、评估标注质量并据此训练和评估森林火灾检测模型的科研人员与工程师。
1. 森林火灾检测为什么绕不开航拍俯拍数据
无人机巡护已经是很多重点林区防火巡查的标配,一趟飞下来几百张照片,靠人盯屏幕找烟点和火点,眼睛很快就花了,漏检几乎无法避免。做目标检测的团队拿到这些画面后,最缺的不是模型结构,而是能反映俯拍视角真实特征的数据。这套「俯拍航拍森林火灾检测数据集」就是冲着这个需求来的:6116张图片、2个类别(火焰与烟雾),同时给出VOC和YOLO两种标注格式,省去了从地面视角数据集迁移过来再重新标注的时间。它能解决的具体问题很明确:训练一个能在俯拍画面上识别火焰和烟雾的检测模型,适合林区巡检、应急监测、无人机图像识别方向的算法工程师和学生。
6116张的数量谈不上大,这个数据集真正值得注意的地方在类别设计和视角设计。烟雾框和云朵混淆、小火苗在缩略图上只剩几个像素、VOC转YOLO时类别编号错位,这些都是我实际踩过的坑。这篇文章从数据特点、格式转换、训练配置、避坑到切片推理,把一条可复现的落地路径完整走一遍。
2. 俯拍、烟雾、小火苗:森林火灾检测数据集的设计逻辑
2.1 俯拍视角改变了什么:尺度、视角、背景三大变量
很多人第一次拿这类数据集时,会惯性套用通用火灾检测的思路,结果发现效果很差。通用数据集里火焰经常占据画面主体的三分之一甚至更多,而俯拍航拍画面里,一个初起火点往往只有几十到上百像素。无人机在100到400米高度拍摄时,火苗和烟柱在画面中的占比会急剧下降。目标检测模型天然偏向学习训练集中占比较高的目标尺度,训练数据里大目标多,小目标就会被系统性漏检。这套数据集的6116张图如果来自多段无人机视频片段,同一火情会在画面不同位置以不同大小出现,这些尺度变化正是训练时最需要的,它让模型在不同飞行高度下性能不会骤降。
视角是第二个变量。垂直下视和带倾角的斜视,火焰与烟雾的形状变化很大。斜视时火苗高度被压缩,烟柱被拉长而且相对地面有位移,如果训练集里完全没有斜视角样本,模型上线后遇到云台倾斜拍摄的镜头就很容易漏。部署前最好搞清楚数据集中正俯拍和斜拍各占多少比例,大部分公开航拍火灾数据以斜拍为主,正俯拍反而少,这一点直接决定了设备安装角度和飞行航线的设计。
背景复杂度是第三个变量。地面视角的背景复杂在目标种类多,航拍视角的背景复杂在纹理模式多。山区林地的树冠纹理、水面反光、农田色块、道路标线,都可能被模型当成异常区域。模型真正需要学的是烟和火本身的颜色与形状特征,而不是「这片林区和别处不一样」。数据集中如果覆盖了不同地形和季节的画面,换一个林区后的误报率会明显更低。此外,树冠遮挡也是俯拍图特有的问题,火焰被树梢挡住一半时,标注框只能框住可见部分,这类样本的占比直接决定了遮挡情况下的检出能力。
2.2 2个类别到底怎么标:火焰与烟雾的边界问题
2个类别最常见的定义是fire和smoke,但真正训练时你会发现标注边界不是非黑即白。火焰类直观,颜色亮、边缘锐利、形态不规则;烟雾类麻烦,半透明、随风扩散、颜色随背景光变化。数据集中烟雾框如果是人工标注的,不同标注者框出来的范围差异会很大,同一个烟柱两个人框的交并比可能只有0.6左右。这类标注噪声不会让训练崩溃,但会把烟雾类别的平均精度拉低。看到烟雾类的AP比火焰类低不少的时候,不用急着调模型结构,这是标注特性决定的正常现象。
烟和云的混淆是航拍火灾数据里最头疼的问题。低空云、晨雾、烟柱在俯拍画面里颜色高度接近。规范的标注会把云排除掉,只保留从地面升起的、与热源位置相关的烟柱区域;如果采集或标注环节没把关,把云误标成烟,模型就会学到「画面里出现的高亮白色区域就是烟」,上线后的误报基本压不住。排查这类问题有个笨办法但很有效:把验证集里烟雾类别的所有误报框导出成图,看它们是不是集中在高亮白色区域,如果是,就直接去清洗训练标签,而不是改模型。
工程上的处理建议是把两个类别区别对待。火焰可以用严格的矩形框甚至多边形标注,烟雾允许一定程度的宽松框,但标签要保守:拿不准是烟还是云的,宁可不标。这套数据集用的是2类别结构,如果后续自己补充数据,我习惯把「烟」再拆成「浓烟」和「薄烟」两个子类,训练后再合并输出,因为薄烟的定位本来就模糊,单独建模更容易收敛,这个技巧对烟雾类AP的提升通常能到3到5个点。
2.3 数据适用边界:白天、季节、地形
拿到数据第一件事不是急着训练,而是确认采集条件。可见光拍摄的航拍火灾检测数据集只覆盖白天的光学特征,夜间或低照度环境下火焰在可见光画面里亮度发散、色彩失真,烟雾几乎无法辨认,这种情况需要红外或热成像数据配合。如果项目需求里有夜间巡检,这套数据集只能作为日间模型的基础,夜间部分必须另行采集热成像样本。
季节和地形的影响同样明显。冬季林区地表有积雪,烟雾在雪地上的对比度和夏季完全不同;春秋农田秸秆焚烧产生的烟和森林火灾的烟形态接近,但周围环境差异很大。如果数据集以山地林区画面为主,模型到平原农田场景后误报率可能明显上升。我一般会用手头现场采集的小批量数据先做一次测试评估,看指标掉多少,再决定要不要做增量训练,而不是直接默认模型能跨区域复用。
6116张对目标检测来说属于中等偏小的量级。按每张图平均1到2个目标估算,总目标数在一万出头,对两分类检测器刚够起步。训练时我会再补充一部分自己场地的图片,尤其是完全没有火灾的负样本图。很多团队只盯着正样本数量,忽略了负样本对降低误报率的作用。另外,如果数据集中有连续帧抽帧的痕迹,相邻帧高度相似,直接随机划分训练集和验证集会高估指标,应该按拍摄片段分组划分,这个细节在第四章里会展开。
2.4 双格式标注给工作流带来的实际优势
VOC和YOLO双格式是这个数据集比较实用的设计。VOC格式以XML存储标注信息,可读性好,适合人工复核和二次编辑,用常见标注工具可以直接打开查看;YOLO格式是纯文本,每行一条记录,读取速度快,主流检测框架原生支持。对工程师来说,双格式意味着不用自己写一整套从标注平台到训练框架的转换逻辑,省下的是最容易被低估的时间成本。
实际项目里,我一般把VOC当作中间存档,XML里保留了每个目标的类别名称,后续如果要增加类别,在XML基础上改就行;把YOLO当作训练输入,直接喂给检测框架。转换脚本只需要维护一次,以后更新标注后重跑一遍即可。第三章给出我常用的转换脚本和转换后必做的检查项,这三项检查能挡住绝大多数格式暗坑。
3. VOC转YOLO:目录结构、坐标归一化与转换脚本
3.1 VOC格式的三个目录与XML字段
VOC格式的物理结构很固定,三个目录各有分工。
VOCdevkit/ ├── JPEGImages/ # 图片文件 ├── Annotations/ # XML标注文件 └── ImageSets/ └── Main/ # train.txt / val.txt,每行一个图片文件名(不含扩展名)JPEGImages放原始图片,Annotations放同名XML,ImageSets/Main下的txt只写图片文件名stem,不写路径也不写扩展名。XML内部的核心字段是这些:
<annotation> <filename>001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>fire</name> <bndbox> <xmin>120</xmin> <ymin>340</ymin> <xmax>190</xmax> <ymax>410</ymax> </bndbox> </object> </annotation>VOC坐标是像素绝对坐标,xmin、ymin、xmax、ymax都是整数,单位是像素,没有经过归一化。类别信息写在name节点里,是字符串而不是编号。转换的核心逻辑就是把像素绝对坐标变成0到1之间的相对坐标,再把类别字符串映射成从0开始的编号。
3.2 YOLO格式的txt与class_id
YOLO格式每个图片对应一个同名txt,放同一目录下。每行一个目标,格式是:
class_id x_center y_center width height五列都是空格分隔,其中后四列是归一化坐标。x_center和y_center是目标中心点相对图片宽高的比例,width和height是目标宽高相对图片宽高的比例,取值范围正常是0到1。class_id从0开始,与classes.txt的行号一一对应。转换时必须保证类别映射顺序和训练时配置文件的names顺序完全一致,否则训练出的模型会类别错位,这个错位在训练日志里很难直接发现。
3.3 VOC转YOLO脚本:核心转换逻辑
下面这个脚本是我每次都先跑一遍的版本,只处理bndbox矩形框,适合这套数据集的常规情况。
import xml.etree.ElementTree as ET from pathlib import Path # 类别映射:列表顺序决定 class_id,训练时 classes.txt 必须与之一致 CLASSES = ["fire", "smoke"] def voc_to_yolo(xml_path: Path, out_dir: Path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.findtext("size/width")) img_h = int(root.findtext("size/height")) lines = [] for obj in root.findall("object"): name = obj.findtext("name") if name not in CLASSES: # 不在类别列表中的目标直接跳过,避免未知类别混入 continue class_id = CLASSES.index(name) bbox = obj.find("bndbox") xmin = float(bbox.findtext("xmin")) ymin = float(bbox.findtext("ymin")) xmax = float(bbox.findtext("xmax")) ymax = float(bbox.findtext("ymax")) # 像素绝对坐标转归一化相对坐标 x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_dir.mkdir(parents=True, exist_ok=True) out_path = out_dir / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines)) voc_annotations_dir = Path("VOCdevkit/Annotations") yolo_labels_dir = Path("yolo_labels") for xml_file in voc_annotations_dir.glob("*.xml"): voc_to_yolo(xml_file, yolo_labels_dir)这段脚本的输入是VOC的Annotations目录,输出是yolo_labels目录下的同名txt。转换的关键在于用图片真实宽高去除坐标,归一化之后不管训练时把图片resize成640还是1280,标签都不需要重新计算,这是YOLO格式最大的优势。
参数说明:CLASSES列表的顺序就是class_id的编号顺序,这里fire是0,smoke是1。xml_path指单个XML文件,out_dir是txt输出目录。用findtext逐级取size/width、bndbox/xmin这些嵌套标签,比手动遍历子节点更短。name不在CLASSES里的目标会被静默跳过,这意味着可能丢框,所以转换完成后必须做下一节的空txt检查。
提示:XML里如果存在rle掩码或多边形标注,上面的脚本会报错。遇到这种情况先打开一个XML看object节点下有没有polygon字段,再做外接矩形转换。
3.4 转完之后必做的三类检查
转换完成不代表格式对了,三类检查我每次都会跑一遍,能挡住绝大多数翻车现场。
第一类是空txt检查。XML里原本有object,但txt为空,最常见的跳帧原因就是类别名不在CLASSES里,比如标注成了fire_forest。用一行命令就能查:
find yolo_labels -name "*.txt" -empty | wc -l如果输出不为0,去对应的XML里看name字段到底是什么字符,八成是类别名不统一。
第二类是越界坐标检查。归一化坐标理论上必须在0到1之间,一旦出现x_center加w/2大于1的情况,说明原始XML里xmax超过了图片实际宽度。这类框训练时会被框架裁切,但会引入无效信息,检查脚本如下:
from pathlib import Path bad = 0 for txt in Path("yolo_labels").glob("*.txt"): for line in txt.read_text().splitlines(): parts = line.split() if len(parts) != 5: bad += 1 continue _, xc, yc, w, h = parts xc, yc, w, h = map(float, (xc, yc, w, h)) if xc < 0 or xc > 1 or yc < 0 or yc > 1: print(f"中心点越界: {txt} -> {line}") bad += 1 if w <= 0 or h <= 0 or w > 1 or h > 1: print(f"宽高异常: {txt} -> {line}") bad += 1 print("异常行数:", bad)第三类是类别错位检查。YOLO训练时读取的是编号,不是字符串,fire是0还是1完全由转格式脚本的CLASSES顺序决定。检查方法很简单:把classes.txt打开看一眼,再随机抽20个txt,确认第一个框的编号对应的是fire而不是smoke。这个检查花两分钟,能避免训练完才发现类别标签全反了的尴尬。
4. 用YOLO把这套数据集跑起来:训练配置与日志解读
4.1 数据划分:先按场景分组再随机切片
数据划分是最容易被忽视的一步。航拍视频抽帧得到的图,相邻两帧内容高度相似,如果直接全体随机划分,训练集和验证集里会出现同一段火情的相邻帧,验证指标虚高得离谱。划分前先看文件名规律,很多数据集用视频编号加帧号命名,比如v01_00123.jpg,v01就是视频片段标识。
我常用的划分逻辑是先按片段分组,再把整个片段划进训练或验证,而不是按单张图划分:
import random from pathlib import Path random.seed(42) image_dir = Path("JPEGImages") images = list(image_dir.glob("*.jpg")) # 按文件名前缀分组,前缀通常对应视频片段编号 groups = {} for img in images: prefix = img.stem.split("_")[0] groups.setdefault(prefix, []).append(img) group_names = list(groups.keys()) random.shuffle(group_names) train_ratio = 0.85 split_idx = int(len(group_names) * train_ratio) train_groups = set(group_names[:split_idx]) val_groups = set(group_names[split_idx:]) train_files = [img for g in train_groups for img in groups[g]] val_files = [img for g in val_groups for img in groups[g]] # 写入 ImageSets/Main,供后续训练读取 with open("ImageSets/Main/train.txt", "w") as f: f.write("\n".join(p.stem for p in train_files)) with open("ImageSets/Main/val.txt", "w") as f: f.write("\n".join(p.stem for p in val_files)) print(f"训练集 {len(train_files)} 张,验证集 {len(val_files)} 张")这里random.seed(42)的作用是固定随机顺序,保证多次运行划分结果一致。按文件名下划线前缀分组是我处理这类数据集的默认逻辑,如果你的数据集文件名没有规律,就用拍摄时间戳的分钟段来分组。85比15的划分对6116张的量级比较合适,验证集大约900张,足够统计出可靠的mAP。
注意:划分后一定要检查验证集中两个类别都出现了。如果验证集里只有fire没有smoke,训练日志里的烟雾类AP会直接显示为0,这不是模型的问题,是划分不均衡。
4.2 YOLO训练的最小配置:从nano开始
6116张的数据量搭配检测框架,我建议从轻量模型开始跑基线。YOLO系列官方发布的预训练权重在COCO上训练过,直接用它的权重做迁移学习,比随机初始化收敛快很多,也更容易判断数据集本身的质量。先跑通流程,再换大模型追求指标,这是避免一上来就陷入调参泥潭的常见做法。
训练入口的data.yaml是训练配置的地基:
path: /data/forest_fire train: images/train val: images/val names: 0: fire 1: smokepath写数据集根目录的绝对路径,train和val写图片子目录的相对路径。names的顺序必须和第3章转换脚本里的CLASSES顺序完全一致,fire对应0,smoke对应1。
训练命令:
yolo detect train \ model=yolov8n.pt \ data=data.yaml \ imgsz=640 \ epochs=100 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/forest_fire \ name=exp1参数说明:model指定预训练权重文件,yolov8n是nano版本,参数量最小,适合数据量中等偏小的情况;如果你的显卡显存足够,第二步换成yolov8s通常能涨1到2个点mAP。imgsz是训练输入尺寸,这里用640,但航拍原图往往是1920乘1080甚至更高,小目标缩小到640后信息丢失严重,显存允许时优先用960或1280。batch按显存调整,16是常见的起步值,显存不足降到8。patience是早停参数,训练损失连续20轮不下降就自动停止,防止无效训练耗时间。
4.3 训练日志怎么读:先看这三条曲线
训练结束后不要只盯着最后一行mAP数字,日志目录下的results.csv里藏着所有过程曲线。我会先拉三组数据看,第一组是train/box_loss,它反映定位收敛情况,正常应该在前20轮快速下降,之后缓慢走平。第二组是train/cls_loss,分类损失,如果它在训练后期不降反升,通常意味着模型开始过拟合,去记训练集里的噪声了。第三组是metrics/mAP50(B),火灾检测的实际部署阈值一般以mAP50为主,mAP50-95作为严格参考,但航拍火灾场景里烟雾框边缘本身模糊,mAP50-95会明显低于mAP50,这是预期内的。
还有一个容易被忽略的现象:训练损失持续下降,但验证损失在第40轮左右开始回升,这是典型的过拟合信号。3116张图的训练集不算大,模型从40轮开始记住训练集细节很正常。处理办法不是提前停训练,而是给数据增强加强度,尤其对烟雾这类边缘模糊的目标,轻度mosaic和尺度扰动能有效抑制过拟合。如果你用的是yolov8n,100轮训练大概在普通消费级显卡上跑半小时到一小时,跑完看一眼曲线再决定要不要加轮次,这个时间花得值。
5. 航拍火灾检测的避坑记录:5个典型问题与排查
5.1 小目标漏检严重,mAP没崩但是召回率低
现象:验证集上mAP50有0.7左右,但按目标尺寸分层统计后发现,像素面积小于32乘32的火焰目标召回率只有0.3,漏掉的几乎全是小火点。
原因:训练时把高分辨率航拍图缩到640,小目标在特征图上只剩下几个像素,特征信息基本丢光;另一个原因是数据集里小目标和大目标的数量不均衡,模型学习时天然偏向数量多的尺度。
解决:优先提高输入分辨率,imgsz从640提到960或1280,这是性价比最高的手段;如果显存不允许,可以把训练图按原分辨率切块后训练,让每个小块里的小目标相对变大。部署阶段再叠加切片推理,这个技巧在第6章展开。
5.2 烟与云错检,晴天误报压不住
现象:在无火情的测试视频里,模型把高亮白云和远处薄雾检成smoke,置信度还不低,阈值从0.25提到0.4才勉强压住,但压住之后真烟也漏了。
原因:训练标签里混入了云被标成烟的样本,或者烟本身的颜色纹理和局部云层过于接近,模型学到的是「高亮白色区域」而不是「从地面升起的烟柱」。
解决:先导出所有烟雾类误报框,按置信度排序看图,确认误报集中在高亮白色区域后,回到标注层面清洗训练标签,把明显是云的框删掉。更彻底的做法是补充纯负样本图,也就是完全没有烟的林区航拍图,让模型见过更多「没有烟但很白」的物体,这个操作对压误报的效果比调阈值明显。
5.3 转换后类别错位,Loss直接发散
现象:训练刚开始的class_loss异常高,预测结果里类别编号和实际物体完全对不上,比如把火苗框标成了烟雾的编号。
原因:VOC转YOLO时CLASSES列表顺序是fire在前,但data.yaml里names写成了smoke在前,编号0对应的物体完全错位,训练在错误的监督信号下自然发散。
解决:训练前把classes.txt和data.yaml打开对比,确认编号一致;再随机抽3张图的txt,逐行用可视化工具画框验证。这个检查成本很低,但我见过太多次因为类别顺序写反导致整轮训练白跑的翻车现场。
5.4 标注里有多边形,脚本只认bndbox直接报错
现象:格式转换脚本跑到一半报KeyError,用浏览器看不到具体原因,打开XML才发现object节点下是polygon而不是bndbox。
原因:部分标注工具导出VOC时默认多边形点序列,尤其对不规则火焰,标注者可能用多边形精细勾勒,导致转换脚本崩溃。
解决:先抽一个报错XML看结构,如果确实是polygon,把多边形点坐标求外接矩形再转换;也可以改用支持多边形的开源转换工具。这类情况本质是标注工具的导出配置问题,在标注阶段就统一用矩形框可以绕开。
5.5 数据增强让烟雾框漂移,训练集Loss异常大
现象:开启默认mosaic和随机旋转增强后,训练集里个别图的loss比其他图高一截,把预测框画出来发现框和雾气内容明显错位。
原因:航拍大图在mosaic拼贴和旋转增强中,紧贴图片边缘的目标可能被变换到图外,或者旋转角度过大导致烟雾框和实际烟柱错开。烟雾本身边缘模糊,增强后的标签错位在视觉上更难察觉。
解决:调低增强参数,把degrees从默认值降到5以内,scale限制在0.3左右,关闭过强的上下翻转。小目标数据增强里mosaic仍然有效,但参数要保守,先跑通再逐步加强,不要直接上全套默认配置。
6. 小目标切片推理与测试集复盘:把数据集价值榨干的技巧
6.1 切片推理救回小目标
训练完模型直接在整张大图上推理,小目标漏检是一个绕不开的坎。切片推理的做法是把大图切成多个有重叠的小块,每个小块独立推理,再把结果合并回原图坐标。切成512乘512的块之后,原来只有20像素的小火苗在切片里占的比例大了十几倍,模型对它的感知能力完全不同。我习惯用SAHI配合YOLO权重,切片尺寸设在512,重叠率0.2,这个配置对小目标召回率的提升通常在10个点以上,而且不用重新训练,只改推理侧逻辑。代价是推理时间变长,一秒钟的视频帧可能要处理几十次模型推理,部署时要评估算力预算。
6.2 测试集复盘:漏检和误报的终点
切片推理跑完测试集后,把漏检框和误报框全部导出成图片,按置信度排序人工看一遍,这是整个流程里最脏但最有价值的环节。漏检集中在低亮度、远距离、树冠遮挡这些场景时,就往对应方向补充现场数据;误报集中在某种地形纹理时,就说阴负样本图。对6116张的底座数据集,增量补一两百张针对性图片再训练一轮,效果往往远好于盲目把训练轮数往上加。现场部署时我会把数据集验证指标和现场实测指标分开记录,数据集指标只能说明模型在这套数据分布上成立,现场指标才决定能不能上线,两套数字不能混着讲故事。
个人习惯是每拿到一套新数据集,先花半天做格式检查、场景划分和一次nano基线训练,再用切片推理做一次失败样本复盘。这半天看着慢,实际上省掉了后续调参和排错的大量时间。数据里的坑不会因为模型结构先进就自动消失,格式、场景、类别顺序这些基础动作才是最值得反复确认的地方。如果你也打算拿这套森林火灾检测数据集做航拍检测的底座,建议先按第3章走一遍格式转换,再按第4章跑个nano基线,最后用切片推理评估小目标效果,希望帮到你。
本文还有配套的精品资源,点击获取