简介:大型无人机视角下的森林桩燃烧图像识别数据集,聚焦燃烧与未燃烧二分类任务,覆盖无人机遥感、森林火灾监测等应用场景,适合需要现成视觉分类数据的开发者、研究人员与院校学生使用。资源包共2000个文件,核心为1998张jpg图像,另含1个类别字典json文件与1个show.py可视化脚本,以7z格式打包,整体约807.61MB。数据已在data目录下划分训练集与测试集,训练集约2万张、测试集约8600张,训练集和测试集内按类别组织图片;类别映射可通过json直接读取,show.py可快速预览样本,省去手动整理标签和查看数据分布的工作。该数据集既可直接接入YOLOv5分类分支或CNN分类网络,也可作为无人机视角图像分类的基准数据,用于算法对比、迁移学习与教学演示。目前已有86人学习下载,适合作为分类任务入门或遥感场景实验的起步数据。
1. 图像分类数据集里的一类硬需求:无人机视角下的森林桩燃烧识别
做森林防火巡检的人都有这种经历:无人机一个架次拍回几千张航拍图,真正要判读的往往是林间采伐剩余物堆或者树桩阴燃这种小目标。这套大型无人机视角下的森林桩燃烧图像识别数据集,就是为这个场景准备的图像分类数据集。它把图片按 train、val、test 三个文件夹划分好,类别以目录名体现,另附一份类别字典文件,拿到手可以直接灌进分类模型,省掉从视频抽帧、清洗、打标、切分的脏活。适合做森林防火预警、无人机巡检算法的工程师,也适合拿真实航拍数据练图像分类的新手。
2. 森林图像分类任务拆解:类别、目录与类别字典文件
2.1 无人机视角下的森林桩燃烧,拆开是几个分类维度
这是一个标准的单标签图像分类任务,不是目标检测。输入是一张无人机航拍图,输出是这张图里树桩或者采伐剩余物堆当前的燃烧状态。和检测相比,它不需要框出火焰的具体位置,只要把整张图归到正确的类别。如果你的业务最终是“弹窗报警并派单”,分类已经够用;只有要控制云台或水炮自动瞄准时才需要检测框。
无人机视角带来的变化集中在三处。第一是俯拍角度,火焰和烟雾在画面里呈现的是顶视形态,和地面近景拍摄完全不同。第二是目标尺度,大型无人机飞行高度高,树桩堆在画面里可能只占几十个像素,整片区域的亮度、颜色、纹理成了更重要的判别线索。第三是背景干扰,林间的裸土、树荫、水洼反光,在俯拍视角下都可能被模型误认成燃烧区域。
类别设计上,这类数据集的类别字典一般会覆盖正常、冒烟、明火和过火余烬几种状态,具体类别名以数据集自带的类别字典文件为准。设计思路是统一的:用“燃烧状态”而不是“物体类型”做标签,因为同一堆树桩在半小时内可能从无火变成冒烟,再变成明火。从“大型”这个词看,数据集规模通常不是千八百张,而是几万张起步。规模大不代表能直接躺赢,要注意图源的时空分布,同一个下午、同一片林的图占比过高时,模型学到的可能就是光照和背景,而不是燃烧特征。
2.2 类别字典文件:为什么不能只靠文件夹名
数据集作者把类别做成了文件夹,文件夹名本身看起来就是标签,那还要类别字典文件干什么?问题出在 PyTorch 的 ImageFolder 读取机制上。
ImageFolder 默认会扫描目录下所有子文件夹,把文件夹名按字典序排序,再生成 class_to_idx 映射。这个排序是自动的,看起来省事,坑在于:如果类别名是中文、带编号,或者不小心带了个空格,排序结果和你在标注表里记的标签可能完全不同。训练时用这个自动映射还能跑,因为标签和输出一一对应;等部署时如果重新读一遍目录,顺序变了,模型输出的类别序号对应的名字就全错了。
类别字典文件通常是一个 JSON,把类别名和整数标签固定下来。常见结构是这样:
{ "no_fire": 0, "smoke_only": 1, "open_flame": 2, "afterfire": 3 }这是我按常见习惯假设的结构,你拿到的数据可能反过来用数字做 key。两种情况都合理,关键是训练脚本和推理脚本必须读同一份文件,不要再自己 sort 一遍。我一般会把这份文件复制到代码仓库里,而不是只留在数据集目录下,否则模型训练完数据集目录被清理,推理端的映射就对不上了。
2.3 文件夹划分方式:拿起来就能训,但要先查三件事
带划分的数据集目录结构一般是下面这个样子:
forest_stump_burn/ ├── train/ │ ├── no_fire/ │ │ ├── 00001.jpg │ │ └── ... │ ├── smoke_only/ │ └── open_flame/ ├── val/ ├── test/ └── classes.json这种划分的意义是:每个集合独立成目录,ImageFolder 不需要额外的 txt 标注文件,train、val、test 直接作为三个 ImageFolder 实例读入。你一定见过另一类数据集,只给一大包原始图片加 train.txt、val.txt,每行写图片路径和标签。那种方式训练前还得写脚本读取 txt、拼接路径、对齐标签,这套数据集把这一步省了。
不过拿到手第一件事不是急着训练,而是先查三件事。第一,train、val、test 三个集合的类别目录是否完全一致,漏一个类会让训练脚本直接报错。第二,每个类别的样本量大概多少,是否出现某个集合里某个类只有几张图的情况。第三,类别字典文件和实际目录名是否对得上。我习惯用一条命令先看目录结构:
find forest_stump_burn/train -maxdepth 1 -type d | sort | nl这条命令把 train 目录下一层所有文件夹按名字排序并编号,结果应该和 classes.json 里的顺序能对上。注意一定要先sort再nl,因为 ImageFolder 内部也是先排序再编号,两边的排序规则必须一致,否则你觉得错了它可能其实没错,或者它错了你看不出来。
文件夹划分确实方便,但也把“划分是否随机”这个坑藏起来了。如果作者按视频抽帧切数据集,相邻帧之间高度相似,同一个场景的连续帧被拆进 train 和 val,测试结果就会虚高。后文专门讲这个坑。
3. 用 PyTorch 接住划分好的文件夹:加载、变换与检查
3.1 一致性检查:让 ImageFolder 的映射对齐类别字典文件
Python 图像识别最常见的落地方式,就是把目录结构交给 ImageFolder。这套数据集最方便的点是 train、val、test 三个目录可以直接用torchvision.datasets.ImageFolder读。但我的习惯是训练脚本第一行不训练,先检查 ImageFolder 自动生成的映射和数据集自带的 classes.json 是否完全一致。
import json from pathlib import Path from torchvision import datasets data_root = Path("forest_stump_burn") with open(data_root / "classes.json", "r", encoding="utf-8") as f: class_dict = json.load(f) # 数据集作者给的类别字典 train_set = datasets.ImageFolder(str(data_root / "train")) print("ImageFolder 映射:", train_set.class_to_idx) print("类别字典文件:", class_dict) assert train_set.class_to_idx == class_dict, "目录名与类别字典不一致,先查命名再训练"逻辑说明:ImageFolder 的 class_to_idx 是按目录名排序后生成的,如果和类别字典文件不一致,训练时每个样本的标签就会整体错位,而且训练过程不会报错,只会让模型学一个“错位但自洽”的映射。等部署时推理端直接读 classes.json,类别对应关系又和训练时不一样,整个模型就废了。所以这个 assert 不是形式主义的,它能挡住最隐蔽的标签错位问题。
3.2 变换设置:无人机图不要直接 Resize 了事
决定用 ImageFolder 之后,下一步是 transform。这里要区分训练集和验证集,训练集可以加随机增强,验证集只做缩放和归一化,否则验证指标会忽高忽低。训练集的一段典型配置:
from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(30), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])逻辑说明:Resize 先把无人机大图压到 256 见方,训练时再从里面随机裁剪到 224 见方,相当于做了随机缩放和平移。RandomRotation 的角度用得比较大,因为无人机航向角不稳定,俯拍图旋转 30 度语义基本不变。ColorJitter 是针对航拍场景加的,同一个树桩在不同光照、阴影下颜色差异明显,不做颜色增强模型会过度依赖原始光照条件。
参数说明:mean 和 std 用的是 ImageNet-1K 预训练统计量,不是这张数据集自己算的。你可能会觉得“我自己的数据集应该用自己的 mean/std”,但如果你用 ImageNet 预训练模型做迁移学习,输入归一化必须跟预训练时一致,否则预训练权重里学到的特征分布被你人为破坏,效果反而更差。等模型收敛稳定后,再换回自己的统计量微调,那是另一种玩法。
3.3 加载检查:第一个 batch 就能暴露大部分问题
在正式训练前,我会先拉一个 batch 出来看形状和标签分布。这一步能挡住大量低级错误:
from torch.utils.data import DataLoader train_loader = DataLoader( train_set, batch_size=32, shuffle=True, num_workers=4, drop_last=True, ) images, labels = next(iter(train_loader)) print(images.shape, labels.shape, labels.unique()) # 期望输出类似: torch.Size([32, 3, 256, 256]) torch.Size([32]) tensor([...])说明:这里的目的不是看性能,而是确认图像解码没问题、通道数是 3、尺寸和 transform 一致、标签范围不超过类别数。num_workers 在 Windows 上如果开多线程报错,可以先改成 0,确认数据没问题再往上调。如果 labels.unique() 里缺了某些类别,说明随机采样没覆盖到,这是正常现象,不用慌。
3.4 如果类别字典文件丢失,用目录重建一份
数据集在拷贝、解压过程中偶尔会丢文件,尤其是那种不起眼的 json。如果 classes.json 丢了,但目录还在,可以自己重建一份:
from pathlib import Path import json train_dir = Path("dataset_root/train") class_names = sorted(p.name for p in train_dir.iterdir() if p.is_dir()) class_dict_rebuilt = {name: idx for idx, name in enumerate(class_names)} with open("classes_rebuilt.json", "w", encoding="utf-8") as f: json.dump(class_dict_rebuilt, f, indent=2, ensure_ascii=False) print("重建完成:", class_dict_rebuilt)说明:重建的前提是目录名本身正确。注意sorted不能丢,否则和 ImageFolder 的默认排序不一致,重建的字典等于没建。目录名带空格是个隐形坑,比如open_flame,打印出来看不出,但 ImageFolder 会把它当成独立类别,导致类别数多一个。检查这类问题时用repr看原始字符串最稳。
3.5 顺手查一遍损坏图片
无人机抽帧数据里偶尔会出现截断的半张图,解码到一半报错。训练时遇到一张坏图,整个 DataLoader 可能直接崩掉。先全量扫一遍:
from PIL import Image from pathlib import Path for img_path in sorted(Path("dataset_root/train/no_fire").glob("*.jpg")): with Image.open(img_path) as im: im.verify() # verify 不加载完整像素,速度快 # 如果某张图损坏,这里会抛 OSError,直接看报错路径说明:verify()只校验文件头和解码信息,不会把整张图读进内存,几千张图也能很快跑完。不要在循环里用im.load()或者np.array(im)逐张检查,速度慢几个数量级。
4. 从算法选型跑通森林桩燃烧基线:参数与验证
4.1 最新的图像分类模型多了,我还是先选 ResNet
从图像分类算法的角度看,ResNet 不是最新,但最适合当第一版基线。最新的图像分类模型比如 EfficientNet-V2、ConvNeXt、Vision Transformer 都在 ImageNet 上刷了更高分,但对无人机拍摄的森林燃烧场景,数据规模通常只有几万张,ViT 这种少归纳偏置的模型需要更多数据和更精细的训练策略,起步就跑 ViT 容易把时间花在调超参上。CNN 的局部纹理和颜色归纳偏置与燃烧识别天然匹配:明火就是局部高亮橙色,烟雾就是局部灰白纹理,不需要太长的全局依赖。
模型规模上,ResNet18 在数据量不大时是稳妥起点,ResNet50 在数据量足够时表现更稳。无论如何,先加载 ImageNet 预训练权重。直觉上燃烧区域的颜色分布和普通物体差异很大,但浅层卷积学到的边缘、纹理对航拍图仍然有效。输入尺寸优先 224,显存允许就上 256,小火苗在低分辨率下容易被直接抹掉。
4.2 训练参数速查表
第一版基线不必追求花哨,下面这套参数是我的常用起点:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 输入尺寸 | 224×224 | 显存够用就换 256,对小目标更友好 |
| batch size | 32 | 16G 显存左右,显存大可到 64 |
| 优化器 | AdamW | lr=1e-4,weight_decay=1e-4 |
| 损失函数 | CrossEntropyLoss | 多分类标准,可以加类别权重 |
| 学习率调度 | CosineAnnealing | 比 StepLR 平缓,收敛更稳 |
| epoch | 30~60 | 以验证集曲线 plateau 为准 |
| 冻结策略 | 先冻结 backbone 训 5 epoch,再解冻全量微调 | 避免预训练权重被冲坏 |
几个关键点不是玄学。先冻结 backbone 只训练分类头,是为了让新分类头先适配 ImageNet 特征空间;解冻后学习率要回落到 1e-4 甚至 5e-5,不然预训练知识在第一个 epoch 就被打乱。损失函数如果直接加权,注意权重别调太大,否则会从漏报变成误报。
4.3 一个可复现的最小训练循环
下面这个循环只展示了单 epoch 的核心逻辑,去掉了验证和保存,目的是先确认训练链路能跑通:
import torch import torch.nn as nn from torchvision import models num_classes = len(train_set.classes) model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) model.fc = nn.Linear(model.fc.in_features, num_classes) model = model.to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) criterion = nn.CrossEntropyLoss() model.train() for batch_idx, (images, labels) in enumerate(train_loader): images, labels = images.to(device), labels.to(device) outputs = model(images) # 输出形状 [batch, num_classes] loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() if batch_idx % 50 == 0: print(f"step {batch_idx} loss: {loss.item():.4f}")说明:这个循环省略了验证和断点保存,只跑前向、损失、反向和更新。第一个 epoch 的作用是确认 loss 能下降、显存不爆、DataLoader 吞吐够。如果 loss 在第一个 epoch 就卡住不动,先查学习率是不是太高或者归一化设置错误,而不是急着换模型。ResNet50 显存不够就换 ResNet18,fc 层替换方式一样。
4.4 验证指标:对燃烧识别来说准确率是骗人的
很多人在验证阶段只看整体准确率,这是森林桩燃烧识别最容易踩的坑。正常帧往往远多于燃烧帧,模型把全部图片判成 no_fire,准确率照样能到 90% 以上,但报警功能等于没有。要按类别看 precision、recall 和 f1:
from sklearn.metrics import confusion_matrix, classification_report y_true, y_pred = [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: outputs = model(images.to(device)) y_true.extend(labels.tolist()) y_pred.extend(outputs.argmax(dim=1).cpu().tolist()) print(classification_report(y_true, y_pred, target_names=train_set.classes, digits=3))说明:classification_report 一行一个类别,能同时看每个类的 precision、recall、f1。重点看“明火”和“冒烟”的 recall,这个数值低意味着火已经烧起来但算法没报。我见过不少模型整体 accuracy 95% 以上,明火类 recall 只有 40%,这种模型拿到值班现场就是摆设。验证集的 transform 不要带随机增强,否则指标不稳定,复现性差。
4.5 要不要换 YOLOv8 检测模型:先看清标注形式
经常有人问,YOLOv8 训练自己的数据集那么火,是不是拿目标检测来做更好?关键在标注形式。这个数据集是分类标注,每张图只有一个全局标签,没有边界框。YOLOv8 需要每个目标的 bbox,这套数据没法直接转过去;如果要重新标注,成本接近重做数据集。不是分类方案更好,而是换检测取决于业务是否需要定位。单纯巡检告警,分类够用;要联动云台自动瞄准,才应该重新采集和标注检测数据集。
5. 森林桩燃烧识别数据集的五个避坑记录
5.1 迁移学习不生效:验证准确率卡在随机水平
现象:换用 ImageNet 预训练 ResNet 后,训练 loss 不降,或者验证准确率始终停留在类别比例附近。
原因:不是预训练没用,而是输入分布差异过大。无人机航拍俯拍视角、高亮度天空、叶片纹理和 ImageNet 里的自然图片差异明显,燃烧区域又是小目标,预训练模型的特征对这些区域响应弱。另一个常见诱因是训练时直接用了 3e-4 以上的学习率更新整个网络,预训练知识在第一个 epoch 就被冲掉。
解决:先把 backbone 冻结,只训练分类头,用 1e-3 跑几个 epoch,让新分类头先收敛到现有特征空间;再解冻骨干,用 1e-4 微调。同时检查 transform 里 Normalize 的 mean/std 是否用了 ImageNet 统计量,如果随手填了自己算的均值,模型看到的输入分布完全不对,迁移学习不生效是必然的。
5.2 类别不均衡:准确率 95%,燃烧样本召回率是 0
现象:正常帧占绝对多数时,模型把所有图都判成 no_fire,整体准确率很高,但报警功能完全失灵。
原因:CrossEntropyLoss 对多数类的梯度贡献大,少数类的错误被整体 loss 淹没。这在巡检类数据里尤其严重,因为正常天气下的巡飞画面天然远多于起火画面。
解决:第一种做法是给损失函数加类权重,第二种是用加权采样器。类权重的方式更简单,不容易影响 batch 多样性:
from collections import Counter counts = Counter(label for _, label in train_set.samples) # 每个类别的样本数 total = sum(counts.values()) class_weights = torch.tensor( [total / (len(counts) * counts[i]) for i in range(len(counts))], dtype=torch.float, ).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)逻辑说明:类别样本数越少,权重越大,反向传播时少数类的梯度不会被淹没。如果加权后模型开始大量误报,说明权重太大,可以把权重整体乘 0.5 再试。安全场景下宁可误报不可漏报,权重方向稍微偏向燃烧类是合理的。
5.3 验证集高分、实飞崩盘:数据泄漏藏在连续帧里
现象:训练集、验证集准确率都很好看,但换一个新飞行架次的数据,效果骤降。
原因:数据集按文件夹划分,如果切分时不留意,同一个视频片段或同一架次的相邻抽帧被分到了 train 和 val。模型在验证时其实见过同一场景的邻近帧,等于考试时看到了原题。
解决:拿到数据先看文件名是否带序列编号或时间戳。如果带,按架次分组划分,而不是按帧划分。对已经划分好的数据集,核查方式是看 val 和 test 的结果差距,如果 val 远高于 test,大概率是 val 泄漏了。这种情况下优先相信 test 指标,并以 test 作为调参依据。
注意:有些数据集只给了 train 和 val,没有独立 test。这时一定要自己按架次再留一份 hold-out,否则模型上线前的评估都是虚的。
5.4 类别字典文件在 Windows 与 Linux 之间翻车
现象:在 Windows 本地跑得好好的,部署到 Linux 服务器时加载 classes.json 报编码错误,或者类别数对不上。
原因:Windows 上某些编辑器会把 JSON 存成带 BOM 的 UTF-8,json.load遇到开头的\ufeff会把第一个 key 解析错。如果数据集给的是 txt 格式的类别文件,在 Linux 上默认按 UTF-8 读 GBK 内容会直接乱码。
解决:读文件时统一加encoding="utf-8-sig",这个编码会自动忽略 BOM。txt 文件则一行一个类名,读取后先 strip 再比对:
with open("classes.txt", "r", encoding="utf-8-sig") as f: lines = [line.strip() for line in f if line.strip()]说明:读出来之后,和 ImageFolder 的 class_to_idx 做逐项比对,别只打印看一眼就放过。这类问题在换服务器、换容器环境时最容易冒出来,属于典型的“环境变了才炸”的坑。
5.5 高空大视角与低空特写混装,模型尺度适应崩掉
现象:训练时图里树桩占画面很大,测试时高空广角图里树桩只有几十个像素,模型把远处阴影都当成了火。
原因:大型无人机和小型多旋翼的飞行高度完全不同,同一个“燃烧”类别在不同尺度下视觉特征差别很大。如果数据集里混装了多个架次、多个高度,模型默认学会了某种尺度的特征,尺度一变就失效。
解决:训练时做多尺度增强,Resize 随机到 200~320 之间再裁剪;或者按图源目录里的架次、高度信息分层抽样,确保每个尺度都进训练集。如果数据集的目录层级里本身带了架次或高度信息,不要删掉,这是排查尺度问题时的第一手线索。
6. 部署前最后一道验证:看分布、看误报、看阈值
6.1 用置信度分布而不是准确率决定是否上线
在推到现场前,把验证集的置信度拉出来,对正常类和燃烧类分别看分布。如果两类置信度大量重叠,说明模型不是有把握地分类,而是蒙对的,实际数据稍微偏移就会崩。代码很简单:
val_loader = DataLoader(val_set, batch_size=64, shuffle=False) all_probs, all_labels = [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: probs = torch.softmax(model(images.to(device)), dim=1) all_probs.append(probs.cpu()) all_labels.append(labels) all_probs = torch.cat(all_probs) all_labels = torch.cat(all_labels) # 假设类别字典里 open_flame 对应的 index 是 2 fire_probs = all_probs[all_labels != 0, 2] print(fire_probs.mean().item())说明:具体取哪一列以类别字典文件为准。低置信度的燃烧样本单独抽出来人工看一遍,你会发现大量“模型其实没看到火,只是看到深色树桩就当火”的错误。这类错误靠数值指标很难暴露,但人眼一看就明白。
6.2 端侧设备上最容易忽略的输入一致性
训练在服务器,推理在机载电脑或端侧设备,最容易出问题的不是模型结构,而是预处理不一致。esp32s3cam 这类摄像头采集的 JPEG 经过强压缩,纹理细节比训练集模糊;机载电脑如果换了相机,白平衡和色彩响应也不同。我习惯上线前写一小段脚本统计现场图像的平均像素值和标准差,和训练集对比,差太多先查相机参数和图像压缩质量,而不是急着加数据。训练脚本里的 Resize、Normalize 必须原样搬到推理端,少一个 Resize 中间步骤,模型看到的分布就变了。
我自己用这类数据集踩过最典型的坑,是拿到手没查拍摄条件和序列划分就开始调参,结果换一个相机验证时被打回原形。现在不管做什么项目,第一周一定把类别、序列、尺度、相机参数四件事查完再动手训练。希望帮到你。
本文还有配套的精品资源,点击获取