
简介本资源面向计算机视觉研究者与深度学习开发者聚焦航拍图像场景下的小目标检测难题提供一套基于改进YOLOv8的完整算法实现与实战项目。针对小目标尺寸小、分辨率低、背景噪声干扰强等痛点项目在网络结构、损失函数与锚框策略上做了针对性优化可应用于安防监控、遥感分析、无人机巡检等场景。压缩包共87个文件约1.97MB以38个Python源码文件为核心辅以26个pyc编译文件、18个yaml配置、2张jpg与2张png检测效果图及1份md说明涵盖模型定义、损失与指标计算、数据配置及可视化模块目录结构清晰便于复现。已有138人学习下载。读者可获取完整项目源码、训练配置与检测效果展示快速搭建环境并复现改进算法理解小目标检测的优化思路与调参方法为学术研究与工程落地提供可参考的实战范例。1. 航拍小目标检测为什么 YOLOv8 直接跑会漏掉一半目标航拍图像里的小目标检测和常规自然场景目标检测完全是两回事。你拿 VisDrone 或者自建的无人机数据集跑一遍原版 YOLOv8大概率会发现一个反直觉的现象mAP 看着还行但实际推理图上密密麻麻的小车、行人、锥桶漏了一大片。原因不复杂——航拍视角下目标像素占比极低COCO 预训练权重里那些 32×32 以上的特征响应根本激活不起来P3 检测头感受野和分辨率之间的矛盾被放大到了极致。这个方向要解决的问题很具体在 1080P 甚至 4K 航拍图上把 8×8 到 32×32 像素的目标稳定检出来同时不把推理速度拖到没法用。适合谁做做遥感图像目标检测的、搞无人机巡检落地的、以及拿 YOLOv8 做毕业设计但发现原版效果不够写论文的。核心思路就三条线加高分辨率检测头、改特征融合方式、补小目标正样本。下面按我实际改过的顺序拆开讲。2. 改进 YOLOv8 的三个切入点从 P2 检测头到注意力机制2.1 为什么加 P2 检测头是航拍小目标的第一优先级YOLOv8 默认三个检测头分别对应 stride 8、16、32也就是 P3/P4/P5。输入 640×640 时P3 特征图是 80×80一个 16×16 像素的目标在 P3 上只占 2×2 个格子。航拍图里大量目标就在这个尺度甚至更小P3 已经到极限了。加 P2 检测头意味着 stride 4、特征图 160×160同样 16×16 的目标变成 4×4 格子回归空间一下子宽裕了。但加 P2 不是改一行配置就完事。它带来三个连锁反应计算量涨、显存涨、正样本匹配策略要跟着调。我一般会先把 backbone 最后阶段的通道数压一压再把 neck 里 P2 分支的融合权重单独初始化不然训练前期 loss 会炸。具体改法是在 YOLOv8 的 yaml 配置里增加 P2 层。以 ultralytics 的格式为例# yolov8-p2.yaml 关键改动部分 head: - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 6], 1, Concat, [1]] # cat backbone P2 - [-1, 3, C2f, [128]] # P2 分支通道数从 256 降到 128 - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 4], 1, Concat, [1]] # cat backbone P3 - [-1, 3, C2f, [256]] # P3 分支 - [-1, 1, Conv, [256, 3, 2]] - [[-1, 12], 1, Concat, [1]] # cat head P3 - [-1, 3, C2f, [256]] # P4 分支 - [-1, 1, Conv, [512, 3, 2]] - [[-1, 9], 1, Concat, [1]] # cat head P4 - [-1, 3, C2f, [512]] # P5 分支 - [[15, 18, 21, 24], 1, Detect, [nc]] # 四个检测头这段配置的逻辑是在原有 P3/P4/P5 基础上从 backbone 的 P2 层引出一条上采样路径和 neck 的 P2 特征做 Concat再经过 C2f 压缩通道。注意 P2 分支的 C2f 输出通道我设成了 128而不是默认的 256原因是 P2 特征图分辨率太高通道数不压显存直接爆。检测头那行从三个索引变成四个nc 是类别数按你的数据集改。参数上要盯住两个一是 P2 分支的 C2f 重复次数默认 3 次够了加到 6 次收益很小但速度掉得明显二是 Detect 头的 reg_max默认 16小目标多的话可以降到 8让回归分布更集中。2.2 注意力机制加在哪一层才不白加热搜里「yolov8 协调注意力机制」出现频率很高但很多人加错了位置。注意力不是万能药加在 backbone 浅层和加在 neck 融合后效果差很多。航拍小目标的痛点在于特征在向下采样过程中被稀释所以注意力应该加在「特征还没被降维之前」和「多尺度融合之后」这两个位置。我常用的组合是backbone 的 C2f 模块里嵌 Coordinate AttentionCAneck 的 Concat 之后加一个轻量 SE。CA 的好处是它同时编码了通道关系和空间坐标信息对航拍这种目标分布有方向性的场景比纯 SE 更合适。import torch import torch.nn as nn class CoordAtt(nn.Module): def __init__(self, inp, reduction32): super().__init__() self.pool_h nn.AdaptiveAvgPool2d((None, 1)) self.pool_w nn.AdaptiveAvgPool2d((1, None)) mip max(8, inp // reduction) self.conv1 nn.Conv2d(inp, mip, 1) self.bn1 nn.BatchNorm2d(mip) self.act nn.SiLU() self.conv_h nn.Conv2d(mip, inp, 1) self.conv_w nn.Conv2d(mip, inp, 1) def forward(self, x): identity x n, c, h, w x.size() x_h self.pool_h(x) # 沿宽度池化保留高度 x_w self.pool_w(x).permute(0, 1, 3, 2) # 沿高度池化保留宽度 y torch.cat([x_h, x_w], dim2) y self.act(self.bn1(self.conv1(y))) x_h, x_w torch.split(y, [h, w], dim2) x_w x_w.permute(0, 1, 3, 2) a_h torch.sigmoid(self.conv_h(x_h)) a_w torch.sigmoid(self.conv_w(x_w)) return identity * a_h * a_w这个模块的关键在 forward 里的两次池化pool_h 把宽度压成 1、保留高度pool_w 反过来。这样注意力图同时带了水平和垂直方向的位置信息对小目标定位帮助明显。reduction 默认 32通道数少于 64 的层建议改成 8不然 mip 算出来太小没意义。插入位置建议在 backbone 最后两个 C2f 的残差分支上以及 neck 每个 Concat 之后。2.3 正样本匹配策略小目标漏检的隐形杀手这一条最容易被忽略。YOLOv8 用的 TaskAlignedAssigner按分类和回归的联合分数选正样本。小目标本身回归难度大早期分数低很容易被大目标的样本挤掉导致小目标正样本严重不足。改法有两种一是调 assigner 的 topk 参数从默认 13 提到 20二是给不同尺度检测头设不同的正样本阈值。# 在 ultralytics 的 loss.py 里调整 TaskAlignedAssigner 初始化 self.assigner TaskAlignedAssigner( topk20, # 默认 13小目标多提到 20 num_classesself.nc, alpha0.5, # 分类权重小目标可降到 0.4 beta6.0 # 回归权重小目标可提到 8.0 )topk 提到 20 意味着每个 GT 多匹配 7 个候选 anchor小目标被选中的概率上升。alpha 和 beta 是分类和回归的平衡系数alpha 降低、beta 提高等于告诉 assigner「回归准比分类准更重要」这对小目标有利。但别调太狠beta 超过 10 训练会不稳定我踩过这个坑loss 震荡到没法收敛。3. 航拍数据集处理从标注到 YOLO 格式的完整链路3.1 航拍小目标数据集的标注工具选型和标注规范航拍数据标注和普通场景不一样目标小、数量多、密集排列。Labelme 适合做分割和复杂形状但纯检测任务用 LabelImg 或者 X-AnyLabeling 效率更高。X-AnyLabeling 支持自动标注可以先拿预训练模型跑一遍再人工修正航拍图上一张 4K 图几百个目标纯手工标一天标不了几张。标注规范上航拍小目标必须定死最小框尺寸。我的经验是小于 6×6 像素的目标直接标成 ignore 区域不参与训练。原因是你标了模型也学不动反而引入噪声。另外密集区域要放大到 200% 再标不然框会重叠得一塌糊涂。标注完导出成 COCO 格式或者直接 YOLO txt。YOLO txt 每行是class_id x_center y_center width height全部归一化到 0-1。这里有个坑不同标注工具导出的归一化基准可能不一样有的按图像实际尺寸有的按 resize 后的尺寸。转换前一定确认清楚。3.2 把 VisDrone 转成 YOLOv8 可训练格式的脚本VisDrone 是航拍小目标最常用的公开数据集之一但它的标注格式和 YOLO 不兼容。原始标注是bbox_left, bbox_top, bbox_width, bbox_height, score, object_category, truncation, occlusion需要转成归一化的中心点格式。import os import cv2 def visdrone2yolo(anno_path, img_dir, out_dir, class_map): anno_path: VisDrone 标注文件路径 img_dir: 对应图像目录 out_dir: 输出 YOLO 标签目录 class_map: VisDrone 类别 id 到 YOLO 0-based id 的映射 os.makedirs(out_dir, exist_okTrue) with open(anno_path, r) as f: lines f.readlines() yolo_lines [] for line in lines: parts line.strip().split(,) if len(parts) 8: continue x, y, w, h map(float, parts[:4]) score int(parts[4]) category int(parts[5]) # score0 表示忽略区域category0 表示其他都跳过 if score 0 or category 0: continue if category not in class_map: continue # 找到对应图像拿宽高 img_name os.path.basename(anno_path).replace(.txt, .jpg) img_path os.path.join(img_dir, img_name) img cv2.imread(img_path) if img is None: continue ih, iw img.shape[:2] # 转中心点归一化坐标 xc (x w / 2) / iw yc (y h / 2) / ih nw w / iw nh h / ih # 裁剪到 [0,1]防止越界 xc, yc min(max(xc, 0), 1), min(max(yc, 0), 1) nw, nh min(nw, 1), min(nh, 1) cls_id class_map[category] yolo_lines.append(f{cls_id} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) out_path os.path.join(out_dir, os.path.basename(anno_path)) with open(out_path, w) as f: f.write(\n.join(yolo_lines)) # VisDrone 10 类映射到 0-9 class_map {1:0, 2:1, 3:2, 4:3, 5:4, 6:5, 7:6, 8:7, 9:8, 10:9}脚本逻辑分四步读标注、过滤无效行、按图像尺寸归一化、写 YOLO txt。关键参数是 class_mapVisDrone 的类别 id 从 1 开始YOLO 要求从 0 开始所以映射表要减 1。score0 的行是「忽略区域」category0 是「其他」这两类必须跳过不然会污染训练集。归一化后我加了裁剪因为 VisDrone 有些框会超出图像边界不裁的话 YOLO 训练时坐标越界会报错。3.3 小目标数据增强mosaic 之外必须补的两个操作YOLOv8 默认的 mosaic 增强对小目标其实不够。mosaic 把四张图拼一起每张图被缩小到一半原本就小的目标变得更小网络更难学。所以航拍小目标训练时我一般会做两个额外操作一是 copy-paste 增强。把标注好的小目标裁剪出来随机粘贴到其他图像的空白区域同时更新标签。这个操作直接增加小目标的样本密度对召回率提升很明显。注意粘贴位置要避开已有目标不然标签会重叠。二是降低 mosaic 的使用概率。默认 mosaic1.0 表示每张图都做我一般降到 0.5让一半的图保持原始尺度。同时把 close_mosaic 从默认的 10 个 epoch 提前到 20 个 epoch让模型在训练后期有更多时间适应真实尺度分布。# 训练时的关键增强参数 augment_config { mosaic: 0.5, # 降低 mosaic 概率 close_mosaic: 20, # 提前关闭 mosaic copy_paste: 0.3, # 开启 copy-paste概率 0.3 scale: 0.3, # 缩放幅度降低保护小目标 translate: 0.1, # 平移幅度也降低 degrees: 0.0, # 航拍图一般不需要旋转 flipud: 0.5, # 上下翻转对航拍合理 fliplr: 0.5 # 左右翻转 }scale 和 translate 降低是因为大幅缩放平移会让小目标移出有效感受野。degrees 设 0 是因为航拍图有固定的方向语义旋转会破坏这个先验。flipud 和 fliplr 可以开航拍图上下左右翻转不改变目标性质。4. 训练参数配置与显存优化GTX 1660 Ti 也能跑起来4.1 从预训练权重到自定义数据集的完整训练命令训练航拍小目标预训练权重必须用但用哪个有讲究。COCO 预训练的 yolov8s 或 yolov8m 都可以n 太小容量不够l 以上显存吃不消。我一般从 yolov8s 起步如果 mAP 不够再换 m。# 单卡训练命令 yolo detect train \ datavisdrone_p2.yaml \ modelyolov8s-p2.yaml \ pretrainedyolov8s.pt \ epochs200 \ imgsz1024 \ batch8 \ device0 \ workers4 \ optimizerSGD \ lr00.01 \ lrf0.01 \ momentum0.937 \ weight_decay0.0005 \ warmup_epochs3 \ cos_lrTrue \ patience50 \ save_period20 \ projectruns/visdrone \ nameexp_p2_ca逐项说imgsz1024 是航拍小目标的关键640 输入下小目标像素太少1024 能保留更多细节但显存占用是 640 的 2.5 倍左右。batch8 是 1660 Ti 6G 显存下的极限再大就 OOM。optimizer 用 SGD 而不是 AdamW是因为小目标训练对学习率敏感SGD 的动量机制更稳。lr00.01 配合 cos_lr 余弦退火前期快速下降后期精细调整。patience50 表示 50 个 epoch 没提升就早停航拍数据集一般 150-200 epoch 收敛。4.2 显存不够时的四个降级方案1660 Ti 只有 6G 显存跑 1024 输入加 P2 检测头很容易爆。按优先级降级第一开 AMP 混合精度。YOLOv8 默认开启但有时候会被关掉确认ampTrue。这个能省 30% 左右显存几乎不掉精度。第二用 gradient accumulation。batch8 跑不动就设 batch4、accumulate2等效 batch 还是 8但显存峰值降一半。# 梯度累积配置 batch4 accumulate2 # 等效 batch size 4 * 2 8第三冻结 backbone 前几层。航拍小目标主要靠浅层特征深层特征可以复用预训练权重。冻结前 5 层显存能再省 15%。第四降 imgsz 到 800 或 768。这是最后手段因为输入分辨率直接决定小目标能不能被检出来。768 是底线再低 P2 检测头就没意义了。4.3 训练过程监控看什么指标判断有没有跑偏训练启动后重点盯三个指标box_loss、cls_loss、mAP50。正常情况 box_loss 从 2.0 左右开始降cls_loss 从 3.0 左右降。如果 box_loss 不降反升大概率是学习率太大或者正样本匹配出了问题。如果 cls_loss 降但 mAP 不涨说明分类学好了但定位不准要调 assigner 的 beta。验证集 mAP50 在航拍小目标上VisDrone 数据集原版 YOLOv8s 大概 35-38加了 P2 和 CA 之后能到 42-45。如果低于 35检查数据标注有没有问题如果高于 50检查验证集是不是和训练集有重叠。# 训练后画 loss 曲线和 mAP 曲线 import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/visdrone/exp_p2_ca/results.csv) df.columns df.columns.str.strip() fig, axes plt.subplots(1, 2, figsize(14, 5)) axes[0].plot(df[epoch], df[train/box_loss], labelbox_loss) axes[0].plot(df[epoch], df[train/cls_loss], labelcls_loss) axes[0].set_xlabel(Epoch) axes[0].set_ylabel(Loss) axes[0].legend() axes[0].set_title(Training Loss) axes[1].plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) axes[1].plot(df[epoch], df[metrics/mAP50-95(B)], labelmAP50-95) axes[1].set_xlabel(Epoch) axes[1].set_ylabel(mAP) axes[1].legend() axes[1].set_title(Validation mAP) plt.tight_layout() plt.savefig(training_curves.png, dpi150)results.csv 里列名可能带空格读进来先 strip 一下。box_loss 和 cls_loss 看收敛趋势mAP 曲线看有没有过拟合。如果 mAP50 在后期还在涨但 mAP50-95 平了说明模型定位精度到瓶颈了可以考虑加更多回归分支或者换更大的模型。5. 避坑与排查航拍小目标训练里最容易翻车的五件事5.1 加了 P2 检测头但 mAP 反而降了现象改完 yaml 加 P2训练 loss 正常降但验证 mAP 比原版还低 2-3 个点。原因P2 特征图分辨率太高正样本数量暴增但其中大量是低质量匹配。TaskAlignedAssigner 在 P2 上选出的正样本很多是背景噪声导致分类分支被带偏。解决给 P2 检测头单独设更严格的匹配阈值。在 assigner 里按特征层索引区分 topkP2 层用 topk10P3/P4/P5 保持 13。同时把 P2 分支的 C2f 通道数从 128 再降到 64减少参数量。5.2 训练到一半 loss 突然变 NaN现象前 50 个 epoch 正常第 51 个 epoch 开始 box_loss 变成 NaN之后所有指标全崩。原因航拍数据集里有个别标注框宽高为 0 或者极小归一化后接近 0计算 IoU 时除零。另外 AMP 混合精度下梯度溢出也会导致 NaN。解决训练前跑一遍数据清洗脚本过滤掉宽或高小于 2 像素的框。然后在训练配置里加ampFalse排除混合精度因素确认是数据问题后再开回来。如果开了 AMP 还 NaN把lr0从 0.01 降到 0.005。5.3 推理时小目标全漏但验证集 mAP 正常现象验证集指标看着不错但拿实际航拍图推理小目标一个都检不出来。原因验证集和实际推理图的尺度分布不一致。验证集可能做了 resize 到 1024但推理时用了默认 640。或者推理时的 conf 阈值设太高小目标的分类分数普遍偏低被阈值滤掉了。解决推理时 imgsz 必须和训练时一致训练用 1024 推理也用 1024。conf 阈值从默认 0.25 降到 0.1先看能不能检出再逐步往上调。另外检查推理代码有没有做 letterbox没做的话长宽比失真会严重影响小目标。# 推理命令注意 imgsz 和 conf yolo detect predict \ modelruns/visdrone/exp_p2_ca/weights/best.pt \ sourcetest_images/ \ imgsz1024 \ conf0.1 \ iou0.5 \ saveTrue \ save_txtTrue5.4 显存够但训练速度慢得离谱现象batch8、imgsz1024一个 epoch 要跑 40 分钟GPU 利用率只有 30%。原因数据加载是瓶颈。航拍图分辨率高解码和增强耗时大workers 设少了 CPU 喂不饱 GPU。另外如果数据集放在机械硬盘上IO 也是瓶颈。解决workers 从 4 提到 8 或 16具体看 CPU 核数。数据集移到 SSD。增强操作里 mosaic 和 copy-paste 比较耗时可以放到 GPU 上做YOLOv8 支持augmentTrue在 GPU 做部分增强。如果还慢把 imgsz 降到 896 试试速度能快 40%。5.5 换了自己的数据集后类别 ID 对不上现象训练不报错但推理时所有目标都标成同一个类别或者类别名全错。原因data.yaml 里的 names 列表顺序和标注文件里的 class_id 不一致。比如标注时 0 是 car、1 是 person但 data.yaml 里写反了。解决写个脚本统计标注文件里出现的所有 class_id和 data.yaml 的 names 逐一对齐。这个错误很隐蔽因为训练 loss 正常只有推理可视化才能发现。import os from collections import Counter def check_class_ids(label_dir, names): counter Counter() for f in os.listdir(label_dir): if not f.endswith(.txt): continue with open(os.path.join(label_dir, f)) as fh: for line in fh: parts line.strip().split() if parts: counter[int(parts[0])] 1 print(标注中出现的 class_id 及数量:, dict(counter)) print(data.yaml 中的 names:, names) missing set(counter.keys()) - set(range(len(names))) if missing: print(f警告以下 class_id 在 names 中无对应: {missing}) unused set(range(len(names))) - set(counter.keys()) if unused: print(f提示以下 names 索引在标注中未出现: {unused}) check_class_ids(datasets/labels/train, [car, person, bicycle])6. 进阶技巧用切片推理把 4K 航拍图的小目标召回率再提一截训练层面的改进做到位之后推理端还有一个几乎零成本的提升手段切片推理SAHISlicing Aided Hyper Inference。原理很简单——4K 图直接缩到 1024 输入小目标被缩没了但如果把 4K 图切成 640×640 的块每块单独推理小目标在块内就变成了「大目标」。切块之间有重叠区域最后用 NMS 把跨块的重复框合并。SAHI 和 YOLOv8 的集成很直接from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction # 加载 YOLOv8 模型 detection_model AutoDetectionModel.from_pretrained( model_typeyolov8, model_pathruns/visdrone/exp_p2_ca/weights/best.pt, confidence_threshold0.15, devicecuda:0 ) # 切片推理 result get_sliced_prediction( test_images/4k_aerial.jpg, detection_model, slice_height640, slice_width640, overlap_height_ratio0.2, # 块间重叠 20% overlap_width_ratio0.2, perform_standard_predTrue, # 同时做整图推理防止大目标漏检 postprocess_typeNMS, postprocess_match_threshold0.5 ) result.export_visuals(export_diroutput/)参数里最关键的是 slice_height/width 和 overlap_ratio。切片尺寸一般设成训练 imgsz 的 0.5-1 倍训练用 1024 的话切片设 640 或 800。overlap_ratio 设 0.2 是经验值太低边缘目标会被切断太高重复框多、NMS 压力大。perform_standard_predTrue 表示同时跑一次整图推理因为切片推理对大目标反而不友好——大目标被切碎了。最后 NMS 的 match_threshold 设 0.5比默认的 0.45 稍高因为航拍密集场景下相邻目标 IoU 本来就高阈值太低会误删。实测数据VisDrone 验证集上不加 SAHI 的 mAP50 是 44.2加了 SAHI 切片推理后能到 48.7提升 4.5 个点代价是推理时间从 25ms 涨到 180ms。如果做离线检测这个代价完全值得如果做实时无人机巡检可以把切片尺寸调大、overlap 调小在精度和速度之间找平衡。验证切片推理有没有生效最直接的方法是拿一张小目标密集的图分别跑整图推理和切片推理把两张可视化结果并排看。如果切片推理多检出的目标集中在图像边缘和密集区域说明参数设对了。如果多出来的全是误检把 confidence_threshold 从 0.15 提到 0.25 再试。我自己的习惯是训练阶段把 P2 和注意力机制调到位推理阶段默认开 SAHI但 confidence 阈值会根据实际场景动态调——白天光照好用 0.25傍晚或雾天降到 0.15。这套组合在几个航拍巡检项目里跑下来小目标召回率比原版 YOLOv8 高了将近 20 个点误检率控制在可接受范围内。希望帮到你。本文还有配套的精品资源点击获取