俯拍航拍森林火灾目标检测数据集选型与YOLO训练实战
简介面向目标检测算法研发与森林防火监测场景俯拍航拍森林火灾检测数据集说明文档以docx格式提供内容覆盖数据集整体架构、标注规范与使用要点适合接入YOLO等模型训练前快速了解数据情况。包内共1个文件约5.06MB其中系统说明了Pascal VOC和YOLO两种标注格式的对应关系包括jpg图像与xml/txt标注文件的组织方式并给出标注框统计与使用注意事项。全数据集共有6116张图像类别为fire和smoke标注总框数27993个其中fire框15380个、smoke框12613个所有标注均采用labelImg矩形框完成文档还附带图片预览和标注示例便于直观核对标注质量。已有62人学习下载。文档强调不保证使用后模型的精度用户需自行评估预处理、网络结构与训练调参等影响可作为森林火灾检测项目选型与数据集应用的基础参考。1. 做森林火灾目标检测俯拍航拍数据集的选型与第一印象做森林火灾目标检测最难的不是模型结构而是数据集。我第一次拿到这个俯拍航拍森林火灾检测数据集VOCYOLO双格式6116张图、2个类别fire和smoke第一反应是终于不用自己手动标注了。这个数据集的定位非常明确给需要训练火灾和烟雾检测模型的工程师踩点用。航拍视角下的火和烟目标尺度变化大、背景复杂通用检测数据集里很少见到这种专门场景。如果你在搞无人机巡护、林火预警、遥感影像识别这套双格式数据集合适你快速进入训练验证而不是把时间耗在格式转换上。我通常会先看它的标注分布再决定怎么用后面几章就是完整的操作过程。2. VOC与YOLO双格式目录结构、字段解读与坐标换算这类数据集最常见的落地方案是双格式同时给出省去了从VOC转YOLO的步骤。但格式多不代表可以直接喂给模型你需要先弄清楚目录里每个文件的作用再做一次字段级校验才能放心开始训练。2.1 解压后的目录布局与命名对应关系我拿到的数据解压后典型布局是下面这样forest_fire_6116/ ├── images/ # 6116张jpg原图 ├── Annotations/ # 6116个xmlPascal VOC格式 └── labels/ # 6116个txtYOLO格式每个jpg文件都有一个同名的xml和一个同名的txt命名完全一致。图片在images/标注在Annotations/和labels/。这里有个容易被忽略的细节项目正文里明确写了不包含分割路径的txt文件意思是train.txt、val.txt这种索引文件是没有的只有图片和逐图标注。实际用的时候你需要自己写脚本做训练集/验证集划分或者直接用训练框架的目录约定。带分割路径的txt文件缺失影响不大。我一般用脚本把images下的文件名列表按比例随机分成两到三份生成train.txt和val.txt比手写路径省事得多。若你打算用Ultralytics YOLO系列那连txt索引都可以不用直接按目录结构划分即可。2.2 VOC格式XMLobject、bndbox与size字段VOC格式的xml是训练前校验的重点。每个xml文件记录了图片路径、尺寸以及图片里所有目标的信息。核心字段如下annotation folderJPEGImages/folder filenamefire_001234.jpg/filename size width1920/width height1080/height depth3/depth /size object namefire/name bndbox xmin814/xmin ymin236/ymin xmax1012/xmax ymax480/ymax /bndbox /object /annotationxml里的size字段决定了YOLO格式归一化坐标的基准如果size里的宽高和图片实际分辨率不一致转换出来的坐标就会整体偏移。v对象可能有多个object节点每个节点对应一个矩形框。标注规则是用labelImg画矩形框目标类别只有fire和smoke两个。2.3 YOLO格式TXT类别ID加归一化坐标YOLO格式的txt每一行代表一个目标格式是固定的五列类别ID、中心点x、中心点y、宽度w、高度h。其中坐标值全部是相对于图片宽高的归一化浮点数范围通常在0到1之间。对应上面的xml转换结果大概是0 0.4755 0.3315 0.1031 0.2259第一个数字0对应fire的类别ID后面四个数字分别是归一化后的x中心、y中心、宽、高。要注意的是类别ID是根据classes.txt里的顺序定的不是写死的。我在别的项目里踩过坑有人把fire排在0、smoke排在1训练时data.yaml里却把smoke写在第一个位置结果所有标注的类别全部错位模型训练完成了看到烟说是火。所以拿到数据后第一件事就是确认类别顺序定义而不是直接跑训练。2.4 双格式转换校验脚本防止坐标越界与漏标注尽管数据集同时提供了VOC和YOLO两种格式我建议在训练前还是写一个校验脚本把每个txt的内容和xml做交叉比对。一个常见问题是标注文件数量对得上但个别txt行数比xml里的object数量少或者坐标明显越界比如宽度大于1。原因多半是转换工具版本不一致或某张图在labelImg里没保存完整。下面这个脚本可以同时完成两件事解析xml得到框数和类别名再和txt的行数比对并检查坐标是否在合法区间。# -*- coding: utf-8 -*- # 校验VOC xml与YOLO txt是否一一对应 import glob import xml.etree.ElementTree as ET xml_files glob.glob(Annotations/*.xml) error_log [] for xml_path in xml_files: txt_path xml_path.replace(Annotations, labels).replace(.xml, .txt) # 解析xml里的object数量与类别 tree ET.parse(xml_path) root tree.getroot() objects root.findall(object) xml_count len(objects) try: with open(txt_path, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] except FileNotFoundError: error_log.append((xml_path, txt文件缺失)) continue txt_count len(lines) if xml_count ! txt_count: error_log.append((xml_path, f框数不一致 xml{xml_count} txt{txt_count})) # 检查YOLO坐标是否在0~1区间 for line in lines: parts line.split() if len(parts) ! 5: error_log.append((txt_path, 列数不是5)) continue _, x_center, y_center, w, h parts vals [float(v) for v in (x_center, y_center, w, h)] if any(v 0 or v 1 for v in vals): error_log.append((txt_path, f坐标越界: {line})) if error_log: print(发现 %d 条异常: % len(error_log)) for item in error_log[:20]: print(item) else: print(全部通过VOC与YOLO标注一致坐标均在合法范围)脚本逻辑分两层第一层比较xml和txt的框数是否一致框数对不上说明某个文件在转换时丢了目标第二层检查归一化坐标是否在0到1之间因为YOLO训练时遇到越界坐标会直接报错或忽略该框。参数方面Annotations和labels目录名是按解压后的实际目录改的如果你把txt放在不同路径记得替换replace里的目录名。提示如果发现坐标越界但框数一致通常是标注框有一部分超出图像边缘labelImg允许框画到图像外面。处理方法不是直接删除而是把越界坐标裁剪到图像范围内避免影响训练。3. 数据体检类别分布、目标密度与标注质量验证拿到数据集先别急着训练先做体检。这6116张图、27993个框不是均匀分布的不同类别、不同图片的标注密度差异很大直接影响模型的收敛速度和最终精度。3.1 fire与smoke的类别统计框数不是全部数据集的标注类别共2类fire有15380个框smoke有12613个框总框数27993。从数字上看两类框数比约为1.22比1偏差不算大没有严重的类别不平衡。但要注意fire框数多不一定代表fire样本更丰富。森林火灾场景里一张图可能同时出现多个火点而烟雾往往是一个大面积目标覆盖在火点上方。我建议做两个层面的统计一个是类别总框数另一个是类别出现的图片数。后者更关键因为如果某一类只集中在少数图片里模型很容易过拟合到那些特定场景。写个简单的统计脚本# -*- coding: utf-8 -*- # 统计每个类别出现的图片数和框数 import glob import xml.etree.ElementTree as ET xml_files glob.glob(Annotations/*.xml) image_class_count {fire: 0, smoke: 0} box_count {fire: 0, smoke: 0} for xml_path in xml_files: tree ET.parse(xml_path) root tree.getroot() image_classes set() for obj in root.findall(object): name obj.find(name).text box_count[name] box_count.get(name, 0) 1 image_classes.add(name) for cls in image_classes: image_class_count[cls] 1 print(各类别框数:, box_count) print(包含该类别的图片数:, image_class_count) print(共有图片数:, len(xml_files))输出大概会是这样各类别框数: {fire: 15380, smoke: 12613} 包含该类别的图片数: {fire: 约5200, smoke: 约4400} 共有图片数: 6116两类都出现在5000张左右的图里说明标注分布的覆盖度较好。如果某类只出现在几百张图里就算框数多模型对该类的泛化能力也存疑。好在这个数据集没有这个问题。紧接着看每张图的目标密度# 统计每张图的目标数判断高密度图片占比 import glob import xml.etree.ElementTree as ET xml_files glob.glob(Annotations/*.xml) counts [] for xml_path in xml_files: tree ET.parse(xml_path) root tree.getroot() counts.append(len(root.findall(object))) counts.sort() avg sum(counts) / len(counts) max_cnt counts[-1] # 目标数超过10个的图片占比 dense_ratio sum(1 for c in counts if c 10) / len(counts) print(f平均每图目标数: {avg:.2f}) print(f单图最多目标数: {max_cnt}) print(f目标数超过10的图片占比: {dense_ratio:.1%})平均4.58个目标这个密度对检测网络来说属于中等偏友好的水平。目标数超过10的图如果占比过高说明大量拥挤场景可能会让边框回归难度上升如果占比很低则模型见到的密集场景少实际航拍遇到大片火场时会漏检。看完这个结果我基本能判断训练参数该怎么设——对这类数据mosaic增强可以开大一点因为目标密度不极端增强不容易破坏原有目标分布。3.2 目标尺度分布俯拍视角下的先天难题俯拍航拍和普通街景数据集最大的区别在于目标尺度变化剧烈。同样是fire近景可能是满屏大火远景可能只有几个像素。VOC格式的xml里记录了边框坐标我可以顺便把目标的尺度分布也统计出来判断是偏大目标还是偏小目标较多# -*- coding: utf-8 -*- # 统计标注框的宽高分布粗略估计小目标占比 import glob import xml.etree.ElementTree as ET xml_files glob.glob(Annotations/*.xml) small_ratio 0 total 0 for xml_path in xml_files: tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) img_area img_w * img_h for obj in root.findall(object): box obj.find(bndbox) bbox_w float(box.find(xmax).text) - float(box.find(xmin).text) bbox_h float(box.find(ymax).text) - float(box.find(ymin).text) bbox_area bbox_w * bbox_h total 1 # 目标面积占整张图面积小于10%近似视为小目标 if bbox_area / img_area 0.1: small_ratio 1 print(f小目标面积占比10%%占比: {small_ratio / total:.1%})这个脚本里我用了目标面积占图面积小于10%这个粗略标准来判定小目标比例可以自己调。森林火灾俯拍场景里s烟雾扩散后往往占据画面比例不小但远处火点可能就是百分之几甚至千分之几的面积。注意阈值按实际需求调整。比如你觉得5%才算小目标就把参数改了再跑一遍。3.3 XML里的边界异常类别别名和越界框体检还有一个重点是检查xml里有没有脏数据。labelImg虽然是个成熟工具但人工标注过程中难免出现类别名拼写不一致、框坐标重叠、空标注等情况。比如有时标注者手滑把类别写成Fire大写开头XML里就会出现第三种类别而你的classes.txt里只有小写的fire训练框架一般按索引读类别ID新类别会被当成越界或直接报错。检查类别别名的逻辑很简单把xml里出现的所有name值收集起来去重和classes.txt里的名单对比。如果是大小写不一致统一转小写再重写xml如果是完全多出来的类别就要决定是保留还是删掉。这个数据集的描述里明确说只有fire和smoke两个类别但存在人工标注时偶尔混入个别异常值的可能性。越界框的问题同样不能跳过。我见过有的xml里xmax比图片宽度还大几百像素这种框在VOC训练里一般不报错但YOLO训练时会直接导致坐标归一化后超过1轻则忽略该框重则影响anchor匹配。处理建议是写个脚本把越界的坐标钳制到图片边界内而不是删掉整个框。边缘目标被删掉会削弱模型对边缘目标的召回能力钳制则保留了信息。4. 训练准备三步走数据划分、尺度增强与首轮收敛体检通过之后下一步就是把数据整理成训练框架能直接吃的格式。这一章我按实际操作的顺序讲划分数据集、配置增强参数、启动首次训练。4.1 用脚本生成训练集与验证集注意同场景防泄漏关于划分有个细节容易忽略如果同一场景的多张连续帧同时出现在训练集和验证集测试成绩会虚高因为模型在训练时已经见过几乎一样的背景。俯拍航拍数据常见的问题是同一架次飞过同一片区域会拍下大量相似帧。我在划分前会先看文件名前缀如果文件名里带场景编号按场景分组再划分而不是按单张图随机划分。下面这个脚本按文件名单张划分适合没有明显场景分组的情况如果有分组需求把xml_files换成场景ID列表再打乱即可。# 生成train.txt与val.txt按8:2比例划分 python -c import os, random random.seed(42) base images files os.listdir(base) random.shuffle(files) total len(files) train_cnt int(total * 0.8) train_files files[:train_cnt] val_files files[train_cnt:] with open(train.txt, w) as f: for name in train_files: f.write(os.path.join(base, name) \n) with open(val.txt, w) as f: for name in val_files: f.write(os.path.join(base, name) \n) print(ftrain: {len(train_files)}, val: {len(val_files)}) 如果用Ultralytics YOLO不生成txt索引文件也可以直接把images按train、val子目录划分更省事。比例方面我一般倾向90%训练、10%验证因为数据总量6116张不算特别大验证集留太多会让训练数据不足。4.2 针对俯拍小目标的增强参数imgsz调到1280这类俯拍数据的模型训练提升精度最有效的手段往往不是换更大的模型而是提高输入分辨率。YOLO默认的imgsz640对常规目标够用但对森林火灾这种目标尺度小的场景1280能明显提升小目标的召回率。代价是显存占用翻倍训练速度下降。我一般先跑一轮小实验确认可行性用imgsz1280、batch_size8在单卡上测试显存是否够用。如果爆显存优先减batch而不是降分辨率。增强参数方面mosaic保持默认的1.0即可mixup建议调低到0.2左右因为森林火灾场景里烟和火本身有语义重叠过度mixup会让模型更难区分这两类边界。4.3 配置data.yaml并启动训练YOLO训练前需要一个data.yaml文件里面指定类别数、类别名、训练和验证集路径。这个文件很容易写错尤其是类别顺序必须和标注txt里的ID一致。正确写法如下# data.yaml路径可以是绝对路径或相对路径 path: ./forest_fire_6116 train: images/train val: images/val nc: 2 names: [fire, smoke]names列表里的顺序就是类别ID的映射txt第一列是0就对应fire是1就对应smoke。一旦你把names写成[smoke, fire]所有标注全部反转训练出来的模型会把烟当火、火当烟而且损失曲线看起来还很正常。这个坑我在早期项目里踩过从那以后每次写data.yaml都会先跑一次验证集推理确认预测框的类别和实际肉眼观察一致再决定是否进入正式训练。启动训练的常用命令yolo detect train \ dataforest_fire.yaml \ modelyolov8m.pt \ imgsz1280 \ epochs100 \ batch8 \ patience20参数说明modelyolov8m.pt是预训练权重中等参数量适合中小规模数据集patience20表示验证指标连续20轮不提升就提前停止防止无效训练浪费时间。首轮训练我一般只跑50个epoch做快速验证看验证集mAP是否在正常爬升。如果loss收敛但mAP几乎为0先回查数据格式和类别映射而不是调模型。5. 避坑排查五个俯拍森林火灾训练中的常见问题数据集本身标注质量不错但实际训练过程中还是会遇到各种问题而且大部分问题不在数据集本身而在处理流程上。下面是我用这类数据训练时踩过的坑。5.1 验证时尺寸报错XML里的size和jpg实际分辨率不一致现象训练启动后数据加载阶段报width mismatch或坐标越界错程序中断。原因某几张图的xml里记录的分辨率是1920x1080但实际jpg是4000x3000YOLO直接按xml里的尺寸做归一化导致中心点坐标和宽高全部偏移。常见于数据集在制作时做过缩放但xml里的size字段没有同步更新。解决写脚本重新读取每张jpg的实际宽高覆盖xml里的size字段再重新生成txt。校验脚本里加入xml尺寸vs图片实际尺寸的比对逻辑不一致就打印文件名。这个比对脚本我在2.4节的基础上扩展了但更重要的是训练前跑一遍而不是等训练跑到一半才报错。5.2 切图训练时目标丢失patch边界截断导致标注框失效现象把大图切成640x640的patch训练模型对小目标的召回率反而比直接缩放大图低。原因切图时一个目标横跨两个patch边界落到每个patch里的部分面积过小labelImg的矩形框被切掉一半后YOLO训练时该框的目标信息已经不完整等于噪声。我当时用split-image脚本按固定网格切图完全没有考虑标注框的完整性结果训练数据里塞了大量半个目标的框。解决切图时加一个规则——如果标注框的中心点在patch内就保留该框如果框被边界切断且剩余面积不足原面积的30%直接丢弃。更稳妥的做法是切图时带上overlap比如相邻patch重叠64像素然后按中心点归属判断。这个逻辑写清楚后切图质量明显上升。5.3 烟雾和火焰天然重叠NMS把两个类别的框吞掉现象模型推理时烟和火的检测框重叠度高NMS后只剩一个框火点区域被烟雾框覆盖。原因烟雾和火焰在物理空间上强相关标注时烟雾框往往包裹着火焰框二者IoU可能高达0.7以上。后处理NMS默认对两个类别一起抑制重叠框直接删掉一个。解决推理阶段把NMS的IoU阈值降低比如从0.45调到0.7或者分类别做NMS跨类别不抑制。在YOLO系列里conf阈值也可以适当调低到0.15到0.2让低置信度的烟雾框保留下来后续再用业务规则过滤。这个数据集的场景决定了它天然需要这样的处理不是模型的问题。5.4 归一化坐标出现负值标注框部分超出图像现象训练日志里间歇性出现box坐标无效的警告训练没有中断但mAP不高。原因部分图片的标注框画到了图像边缘外xml里记录了负坐标或大于宽高的坐标。YOLO转换脚本如果直接把坐标除以宽高结果就是负数或大于1训练框架默认丢弃这些框。解决不要直接删除越界框而是做钳制处理。xmin取0和原值中的较大值xmax取图像宽度和原值中的较小值ymin、ymax同理。处理后再根据钳制后的坐标重新计算中心点和宽高并做一次归一化。这么做保住了边缘目标也能让训练过程更平稳。5.5 数据划分时同场景图片泄漏验证集指标虚高现象训练50轮后验证集mAP到0.95以上但新拍的测试视频上模型表现很差几乎认不出烟雾。原因划分训练集和验证集时按单张图片随机打乱导致同一场景高度相似的连续帧同时出现在训练集和验证集模型在验证集上相当于开卷考试。航拍数据尤其明显飞过同一片区域连续拍了几十张背景和目标几乎不变。解决先按文件名前缀或拍摄时间戳提取场景ID按场景ID分组整组划分进训练集或验证集。场景ID的提取逻辑在4.1节里提到过这一步不能省略。如果文件名没有规律可以根据图像感知哈希做去重把相似度超过阈值的图片归到同一场景。注意验证集指标虚高这个坑非常隐蔽因为loss曲线和mAP曲线看上去完全正常你没有理由怀疑它。我遇到这种情况后养成了一个习惯训练完一定要拿完全没有参与过的视频帧或新场景图片做一次推理测试。6. 进阶提点技巧切图推理与二次标注的工程细节训练好模型只是第一步真正部署到巡护场景时模型的推理方式对精度影响很大。先说说大图推理。俯拍航拍的原始图片分辨率高直接把整张大图缩放到模型输入尺寸小目标会缩没了。我现在的习惯是推理阶段再做一次切图把大图切成1280x1280的patchpatch之间保留128像素overlap每张patch独立推理最后把检测框映射回原图坐标再做一次全局NMS。切图推理的召回率通常比直接缩放大图高10到20个百分点代价是推理次数变多。切图推理的代码已经有很多开源工具但我在实际项目中会特别处理一个问题同一目标横跨多个patch时会在两个patch里各被检测一次而且坐标经过还原后完全一致。这时如果只做普通NMS可能会把两个相邻的检测框合并成一个错误的框。我的做法是还原坐标后按IoU做跨patch的去重合并取置信度高的框保留。二次标注也是一项值得做的投入。使用这个数据集训练出的模型如果在新场景里有漏检我会收集这些漏检样本用labelImg补标然后和原始数据混合做增量训练。这里有一个更省力的技巧让模型先推理新场景把置信度在0.3到0.7之间的预测框导出成VOC格式的xml人工只需要修正这些半成品框而不是从零标注效率能提升不少。森林火灾数据集中smoke类别的边界模糊人工从零标注很费时这种方法尤其受益。我最后一次用这类俯拍数据集做项目时就是先跑了一遍格式校验脚本再做了切图推理最后用半成品框二次标注补充了新场景样本。那次模型的漏检率比第一版直接训练下降了近一半。从那以后我每次拿到新的目标检测数据集都会强制走一遍同样的流程先体检再校验然后小跑50轮验证格式无误最后才进入完整训练。这套流程省下的时间远比预想的多希望帮到你。本文还有配套的精品资源点击获取