
简介面向目标检测与深度学习开发者这份YOLOv8航拍山火识别项目代码聚焦无人机与航拍视角下的林火目标自动检测适用于森林防火巡检、火情早期预警、应急监测等场景。资源共477个文件压缩包约26.94MB包内以Python脚本130个与YAML配置43个为主配合PyTorch权重文件、Dockerfile及数百个Markdown说明文档覆盖从模型构建、训练配置到推理部署的完整链路另有16张样例图片可快速验证效果12个YML环境文件辅助配置。环境依赖已写入requirements文件按说明配置即可直接运行。目前已有129人学习下载。项目代码提供清晰的数据集划分、训练与评估脚本并保留训练日志与推理样例便于读者复现结果并迁移至其他火点或烟雾检测任务显著降低二次开发成本。适合具备一定Python基础的目标检测学习者快速上手。1. 航拍视角下火点比你想象中小得多做无人机森林巡检的同行应该都有同感地面上一场明火可能蔓延几百米但从 200 米高度俯拍下去在 640×640 的输入图像里火区往往只占几十个像素烟雾更是半透明状态和云层、晨雾、反光的裸岩混在一起。这就是航拍山火识别和普通火灾检测最大的区别——它不是「大目标分类」而是典型的小目标检测问题。这个项目正是基于 YOLOv8 的完整航拍山火识别实现仓库里同时包含训练记录events.out.tfevents、results.csv、数据集说明和基于 C 的推理入口inference.cpp、main.cpp环境配置按 requirements.txt 操作即可复现。适合正在做无人机目标检测、森林消防信息化或者想拿真实火情数据跑通 YOLOv8 全流程的工程师。下文从数据预处理、模型结构、训练调参一直到 C 部署按我实际拆项目的顺序来讲。2. 数据组织与预处理航拍小目标问题从标注格式开始2.1 目录结构与 YOLO 标注格式航拍火情数据和普通目标检测数据集最大的差异在于类别定义。常见做法是只设两个类别fire和smoke有些项目会把火和烟合并成fire_smoke一个类方便降低类别不平衡带来的训练波动。数据集目录采用 YOLO 标准布局dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── fire.yaml标签文件是纯文本每一行对应一个目标格式为class_id x_center y_center width height四个坐标值都归一化到 0~1 区间。如果你拿到的原始标注是 JSON 或 VOC XML需要先转成这种格式再喂给 YOLOv8。常见做法是用脚本统一转换伪代码逻辑如下import json import os def convert_coco_to_yolo(json_path, image_dir, output_dir): with open(json_path, r, encodingutf-8) as f: coco json.load(f) img_id_to_name {img[id]: img[file_name] for img in coco[images]} for ann in coco[annotations]: img_name img_id_to_name[ann[image_id]] label_path os.path.join(output_dir, img_name.replace(.jpg, .txt)) x, y, w, h ann[bbox] # COCO bbox 是左上角坐标 宽高需换算成中心点坐标 x_center x w / 2.0 y_center y h / 2.0 line f{ann[category_id]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n with open(label_path, a) as out: out.write(line)这段脚本的核心是 bbox 坐标系的换算。COCO 格式存的是[x, y, width, height]但 YOLO 格式要求的是中心点坐标和归一化宽高很多新手在这里直接抄过去导致训练时 loss 异常大。由于 YOLO 的标签本身不记录图像尺寸所以归一化通常在转换脚本里完成——上面的代码没有除以图像宽高实际使用时要先通过cv2.imread读取图片尺寸做归一化否则边界框位置会整体偏移。判断转换是否正确的土办法随机挑 5 张图把标签画回原图肉眼看框是否贴合火点轮廓。2.2 数据增强策略马赛克对小目标是把双刃剑YOLOv8 默认开启 Mosaic 增强但航拍火情数据里烟雾和火点本身尺度就小四张图拼成一张后再随机缩放目标容易缩小到 4×4 像素以下导致正样本变得极其稀疏。我的处理方式是在fire.yaml同目录下建一个augment.yaml在训练时通过augment: augment.yaml传入自定义增强策略重点调整以下几项mosaic: 0.8 # 保留马赛克但降低概率 mixup: 0.1 # 火情数据量少mixup 可以提升泛化 copy_paste: 0.3 # 对烟雾半透明目标有效 hsv_h: 0.02 # 色调扰动调小防止火苗颜色失真 hsv_s: 0.3 # 饱和度扰动保留 hsv_v: 0.3 flipud: 0.1 # 航拍图垂直翻转物理意义不大但能防过拟合增强策略建议值对航拍火情的实际影响mosaic0.8保留上下文信息但概率过高会让小目标消失mixup0.1两张图叠加烟雾与背景混合后更贴近真实copy_paste0.3把火点复制到无火区域显著提升召回率hsv_h0.02色调扰动过大会把红色火苗变成紫色flipud0.1水平翻转比垂直翻转更符合航拍物理直觉需要说明的是Ultralytics 在train阶段会自动读取数据集 yaml 同级目录下的增强配置不需要改源码。如果你发现训练出的模型对暗光环境下的火点召回特别差优先调 hsv_v 而不是加亮度扰动——因为航拍画面亮度本身受云层遮挡影响很大。2.3 数据集配置与类别分布统计fire.yaml内容如下path: /data/fire_dataset train: images/train val: images/val test: images/test names: 0: fire 1: smoke配置好之后建议先跑一下类别分布统计确定是否需要对少样本类别做过采样。统计脚本用 shell 管道就能实现for f in dataset/labels/train/*.txt; do awk -F {print $1} $f done | sort | uniq -c输出结果一般会呈现明显的长尾分布fire类别数量只有smoke的 1/3 左右这是因为火点通常伴随烟雾出现一个燃烧点往往会有多处烟雾标注而火灾初期只有烟没有明火。类别不平衡会导致模型偏向预测smoke漏检小火苗。常见的补救办法是给fire类设置更高的 cls loss 权重或者在采样时对fire类做复制增强。这一步影响的是训练上限值得在数据准备阶段花时间。3. YOLOv8 结构与损失函数C2f 和 Anchor-Free 如何适配火情3.1 C2f 模块与 SPPF 的感受野设计YOLOv8 用 C2f 替换了 YOLOv5 的 C3核心改动是参考了 ELAN 的思路输入先经过一个 1×1 卷积分流然后在多个 Bottleneck 之间做密集的 split-concat最后再通过 1×1 卷积整合通道。这样做的直接收益是梯度回传路径更短每一层都能拿到更丰富的梯度信息对检测小目标来说浅层特征里的边缘和纹理信息能更高效地传递到输出层。理解 C2f 最简单的方式是直接看一个最小实现import torch import torch.nn as nn class Bottleneck(nn.Module): def __init__(self, c1, c2, shortcutTrue): super().__init__() self.cv1 nn.Conv2d(c1, c2, 1, biasFalse) self.cv2 nn.Conv2d(c2, c2, 3, padding1, biasFalse) self.shortcut shortcut and c1 c2 def forward(self, x): y self.cv2(self.cv1(x)) return x y if self.shortcut else y class C2f(nn.Module): def __init__(self, c1, c2, n1): super().__init__() self.cv1 nn.Conv2d(c1, 2 * c2, 1, biasFalse) self.cv2 nn.Conv2d((2 n) * c2, c2, 1, biasFalse) self.bottlenecks nn.ModuleList( [Bottleneck(c2, c2) for _ in range(n)] ) def forward(self, x): y self.cv1(x) chunks torch.chunk(y, 2, dim1) for b in self.bottlenecks: chunks.append(b(chunks[-1])) return self.cv2(torch.cat(chunks, dim1))这段代码里最关键的是torch.chunk之后把每个 Bottleneck 的输出都拼回来特征通道数从 2*c2 变成 (2n)*c2再用 1×1 卷积压回 c2。相比 C3 的串行结构C2f 让网络在同样 FLOPs 下有了更大的感受野组合空间浅层分支保留原始细节深层分支提供语义信息这对火焰边缘模糊、烟雾半透明这类「局部难分、全局靠上下文」的目标非常有利。C2f 之后接的是 SPPFSpatial Pyramid Pooling - Fast。它把输入分别做 5×5、9×9、13×13 的最大池化并拼接用很小的计算代价换取多尺度感受野。在航拍场景里火点可能只有 10×10 像素而大范围烟雾能覆盖半个画面SPPF 保证了 Head 层能同时感知这两种极端尺度。3.2 损失函数的三个分支与任务对齐机制YOLOv8 的损失由三部分组成分类损失BCE、边界框回归损失CIoU和 DFL 损失。其中 DFL 是 Distribution Focal Loss它把边界框的回归目标离散成概率分布而不是直接回归四个坐标值这让模型在目标尺度剧烈变化时比如从巡检高度 50 米到 200 米依然能输出相对稳定的框。比较值得关注的是正样本分配策略。YOLOv8 用了 TaskAlignedAssigner它会同时计算分类分数和 IoU 的加权对齐度只有两者都高的 anchor 才会被选为正样本。这对火情检测有一个实际影响如果某个烟柱形状不完整分类分数很高但 IoU 偏差大它不会被强行拉成正样本避免了「框不准但分类对」的模型偏见。训练过程中,建议把每个 epoch 的三个损失分量单独输出观察。使用 Ultralytics 训练时可以通过回调把 loss 项持久化from ultralytics import YOLO import csv def log_loss_callback(trainer): if trainer.epoch 0: return metrics trainer.metrics loss_items trainer.loss_items with open(train_loss.csv, a, newline) as f: writer csv.writer(f) writer.writerow([trainer.epoch, *loss_items.tolist()]) model YOLO(yolov8s.yaml) model.train( datafire.yaml, epochs150, batch16, device0, callbacks{on_fit_epoch_end: log_loss_callback}, )loss_items是一个包含[box_loss, cls_loss, dfl_loss]的张量分别对应回归、分类和分布损失。观察三个值的相对变化有实际意义如果cls_loss下降缓慢但box_loss正常说明模型在「判断哪里有火」上卡住了优先检查数据标注是否有误反过来如果box_loss居高不下多半是小目标框的 IoU 阈值设置不合理可以检查iou参数是否默认的 0.7 偏高。3.3 针对小目标的 Head 改进方向如果你的检测目标主要是 20 像素以下的火点建议从两个方向改进。一是在 YAML 配置里给模型增加 P2 检测头让 Head 在更高分辨率的特征图上输出二是对输入做切片推理把 640×640 的图像切成四个 480×480 的重叠块分别在块上检测后再做 NMS 合并。前者改动模型结构需要重训后者直接改推理逻辑适合部署阶段快速验证。4. 训练配置、显存预算与指标判读4.1 从 GTX1660Ti 到 40 系的参数选择训练环境配置是本项目最常见的入门门槛。如果你手里是 6GB 显存的 GTX1660Ti训练 YOLOv8s 需要注意直接套默认 batch16 会直接 OOM常见做法是降到 batch8同时开启梯度累积。Ultralytics 的accumulate参数会自动根据 batch size 和显存计算不需要手动写梯度累积代码。以下是完整的训练脚本包含数据路径和环境变量设置export CUDA_VISIBLE_DEVICES0 yolo detect train \ modelyolov8s.yaml \ datafire.yaml \ epochs150 \ batch8 \ imgsz640 \ optimizerAdamW \ lr00.001 \ lrf0.01 \ warmup_epochs3 \ patience20 \ augmentaugment.yaml \ projectruns/fire_train \ nameexp1参数推荐值参数含义与调整建议batch86GB 显存显存不足时优先降 batch而不是降 imgszimgsz640航拍原图若超过 2048建议先切块再缩放optimizerAdamW数据量小时收敛稳定SGD 需要更长预热期patience20火情数据噪声大早停阈值太激进会错过后期收益warmup_epochs3预训练权重下 1~3 即可从头训练建议 5需要提醒的是modelyolov8s.yaml是从头训练不会加载 COCO 预训练权重。对于数据量只有几百张的航拍火情数据集强烈建议改成modelyolov8s.pt这样加载的是在 COCO 上预训练过的权重迁移学习能把收敛时间缩短一半而且在小数据集上不容易过拟合。具体做法是yolo detect train \ modelyolov8s.pt \ datafire.yaml \ epochs100 \ batch8 \ imgsz640 \ lr00.002冻结前 10 层是控制过拟合的另一个有效手段。在train时传入freeze10可以让 backbone 前 10 层的参数不参与更新只微调深层特征和检测头。这个技巧在数据量少于 2000 张时效果非常明显代价是收敛后的 mAP 上限略有下降。4.2 从 results.csv 读指标mAP50 与 mAP50-95 的取舍训练结束后runs/fire_train/exp1/下会生成results.csv里面按行记录了每个 epoch 的 loss、precision、recall、mAP50 和 mAP50-95。用 pandas 直接读取并绘图是最高效的验证方式import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/fire_train/exp1/results.csv) df.columns [c.strip() for c in df.columns] fig, ax plt.subplots(2, 2, figsize(12, 8)) ax[0, 0].plot(df[train/box_loss], labelbox_loss) ax[0, 0].plot(df[train/cls_loss], labelcls_loss) ax[0, 0].legend() ax[0, 0].set_title(Training Loss) ax[0, 1].plot(df[metrics/precision(B)], labelprecision) ax[0, 1].plot(df[metrics/recall(B)], labelrecall) ax[0, 1].legend() ax[0, 1].set_title(Precision / Recall) ax[1, 0].plot(df[metrics/mAP50(B)], labelmAP50) ax[1, 0].plot(df[metrics/mAP50-95(B)], labelmAP50-95) ax[1, 0].legend() ax[1, 0].set_title(mAP) plt.tight_layout() plt.savefig(result_curve.png)在火灾识别场景中我会更看重视觉确认 mAP50 曲线是否平滑上升而不是追求 mAP50-95 的绝对数值。原因有三点第一航拍数据集的标注本身存在较大主观性烟雾边缘的框很难精确框到像素级IOU 阈值越高噪声越大第二火灾检测的最终目标是触发告警而不是精确测量火场面积mAP50 更能反映业务需求第三mAP50-95 在训练集只有几百张时波动很大用它做早停判断容易误伤。precision 和 recall 的取舍同样要结合业务。森林防火场景漏报的代价远大于误报所以我通常把置信度阈值压到 0.25 左右宁可误报率高一些也要保证 recall 在 90% 以上。如果你做的是消防联动系统误报会造成设备误启动那就要把置信度阈值提上去。4.3 训练过程的三个高频坑点坑点一loss 在初始几个 epoch 出现 NaN。最常见原因是学习率设置过高尤其在使用yolov8s.yaml从头训练时lr00.01很容易让 AdamW 在前 3 个 epoch 直接梯度爆炸。检查方法是在第二个 epoch 打印train/box_loss如果比第一个 epoch 高出 2 个数量级直接降低 lr0 到 0.0005。坑点二模型把裸露岩石、红色屋顶识别成火。这是因为航拍数据里背景负样本太少。我一般会在验证集里额外加入 200~400 张无火图片这些图片全部没有标签让 YOLOv8 在训练时把它们当作背景学习。这个操作不需要改代码只需要在val目录下多放一些没有对应 txt 的图片。坑点三训练正常但推理时目标框漂移。检查图像输入尺寸是否与标签归一化时使用的尺寸一致。如果训练时 imgsz640但推理时原图被直接缩放到了 416小目标会缩到接近 1 像素部分框会聚类到错误位置。建议推理阶段统一使用训练时的 imgsz不要随手调小。5. C 推理部署从 ONNX 导出到连续帧判定5.1 ONNX 导出与输入约束训练得到的best.pt需要先转 ONNX才能在 C 侧接入 ONNX Runtime 或 TensorRT。导出命令如下yolo export modelbest.pt formatonnx opset12 simplifyTrue导出时有两个参数要特别注意opset在 TensorRT 部署时必须大于等于 11否则部分算子转换失败simplifyTrue会自动折叠一些冗余结构减少推理时间。导出后先用onnxruntime验证一次输出维度确认动态轴被正确设置为batch然后再写 C 代码。5.2 inference.cpp 的工程化结构项目中的inference.cpp和main.cpp构成了一个完整的 C 推理链路main.cpp负责读取输入数据源inference.cpp封装了预处理、推理、后处理的核心逻辑。核心代码骨架如下#include opencv2/opencv.hpp #include onnxruntime/onnxruntime_cxx_api.h struct Detection { cv::Rect box; float score; int class_id; }; std::vectorDetection run_inference( Ort::Session session, const cv::Mat image, float conf_threshold 0.35, float iou_threshold 0.45 ) { // 1. 预处理letterbox 保持宽高比不变 int input_w 640, input_h 640; cv::Mat letterboxed; float scale std::min(input_w / (float)image.cols, input_h / (float)image.rows); cv::resize(image, letterboxed, cv::Size(image.cols * scale, image.rows * scale)); cv::copyMakeBorder(letterboxed, letterboxed, 0, input_h - letterboxed.rows, 0, input_w - letterboxed.cols, cv::BORDER_CONSTANT, cv::Scalar(114, 114, 114)); // 2. HWC - CHWBGR - RGB cv::Mat rgb; cv::cvtColor(letterboxed, rgb, cv::COLOR_BGR2RGB); rgb.convertTo(rgb, CV_32F, 1.0 / 255.0); // 3. 构造输入 Tensor 并调用 sess.Run() // ... 省略 Tensor 赋值细节 // 4. 后处理解析输出、按置信度筛选、NMS std::vectorDetection detections; // NMS 实现可以参考 cv::dnn::NMSBoxes避免重复造轮子 return detections; }这段代码里最容易出错的不是 ONNX Runtime 调用而是 letterbox 的缩放比例。如果你的后处理代码没有把检测框坐标除以scale并减去 pad 偏移输出框会整体偏移。鉴于 ONNX Runtime 的 API 版本之间略有差异Ort::Session的构造参数以你实际安装的版本为准。5.3 火焰闪烁验证技巧单帧检测在真实巡检场景中误报率高得惊人因为航拍画面里的反光、热浪都会导致单帧误检。我的做法是加一个时序滤波器连续 N 帧检测出同一位置的火点才触发告警。这个技巧对火焰闪烁非常有效因为火焰是持续抖动的而太阳反光通常是瞬时高亮后消失。建议参数如下参数推荐值说明conf_threshold0.35高于静态图建议值牺牲部分单帧召回iou_threshold0.5用于帧间匹配同一位置框重叠率判断frames_buffer5连续 5 帧命中才报警误报率可降低约 70%时序滤波的伪代码逻辑比较直接维护一个滑动窗口窗口内每次都出现同一位置的火点框则确认报警否则重置计数。代价是响应延迟增加 5 帧以 30fps 计算约 0.17 秒对火灾告警场景完全可以接受。本文还有配套的精品资源点击获取