
无人机视角下的目标检测一直是个很让人头疼的场景。地面拍摄的数据集里目标通常又大又清晰到了无人机俯拍图里一辆车在两千多像素宽的画面里可能只占二三十个像素这种小目标别说人工标注费劲模型训练的收敛难度也完全不是一个量级。这篇文章想聊的就是我拿 YOLOv8 在公开的 VisDrone 无人机视角数据集上做小目标检测的完整过程——从数据格式转换、基线训练到输入分辨率、切图、数据增强、检测头改进这一系列精度提升手段。无论你是刚接触检测任务的学生还是要在航拍、卫星、高位监控这类小目标场景里落地模型的工程人员文里的经验应该都能直接搬过去用。先说明一下标题里的 Visidron 其实就是 VisDrone 的常见笔误下文统一用 VisDrone。这个数据集的难点非常典型图像分辨率极高目标在上面显得极小同时目标密集、遮挡严重很适合用来验证小目标检测的提升思路。1. 项目背景与数据集认知1.1 VisDrone 到底难在哪VisDrone 是一个无人机视角下的公开目标检测数据集图像全部来自真实航拍涵盖城市、乡村、港口、球场等场景。标注类别一共有 10 个有效类别行人、人、自行车、轿车、面包车、卡车、三轮车、带篷三轮车、公交车、摩托车。注意它还有一类 ignore 区域是用来遮挡标注的转换时要过滤掉。这个数据集最核心的难点是目标尺度。按照通用定义小于 32×32 像素的目标就算小目标而 VisDrone 里大量目标连这个尺寸都达不到。我随机统计过一批训练图像里的标注框大小在 2000×1500 左右的原始分辨率下相当比例的目标宽高只有十几个像素。这类目标经过模型 backbone 的多次下采样之后在深层特征图上基本就变成一两个像素点信息量已经近乎丢失。同时无人机俯拍视角下目标非常密集。一个街区画面里可能有上百辆车和行人相互遮挡、堆叠导致检测框很容易被 NMS 误杀。再加上光照变化、阴影、视角倾斜这数据集想刷高 mAP 确实不容易。对做工程的人来说VisDrone 练出来的模型往监控、巡检、安防场景迁移泛化能力往往都还不错这也是我选它的原因。1.2 为什么选 YOLOv8 作为基线选 YOLOv8 不是因为它是当前精度最高的检测器而是因为它的训练成本、部署生态、迭代效率综合起来最适合做这种小目标专项优化。YOLOv8 相比之前的版本有几个重要改进backbone 改用 C2f 结构颈部沿用 PAN-FPN检测头换成 Anchor-Free 的 Decoupled Head分类和回归分支解耦损失函数用 CIoU 加 DFL。这些改动让它在训练时会更快收敛对密集场景的抗干扰能力也更强。但要说清楚YOLOv8 默认配置并不是为小目标设计的。它默认输出三个尺度的特征图最小 stride 是 8也就是说一个 8×8 像素的小目标在最小尺度的特征图上只占 1 个像素。所以用默认参数跑 VisDrone效果一般是中等偏下这就给了我们后面一系列调优动作的空间。1.3 整体技术路线我的完整路线分四个阶段每一步都有明确目的这样出了问题能快速定位。阶段核心操作预期效果数据准备格式转换、过滤 ignore、目录重整训练流程不卡壳基线训练用官方预训练权重跑 640 分辨率跑通流程暴露瓶颈精度提升分辨率提升、切图、数据增强、结构改动小目标召回率明显上涨后处理与部署NMS 调参、TTA、模型导出提升最终推理效果这个路线最好的地方在于每一步都可以独立验证效果。如果直接一上来就同时改一堆东西后面根本分不清是谁起的作用。这也是做算法优化最重要的习惯一次只动一个变量。2. 环境准备与数据转换先把地基打牢2.1 硬件与训练环境清单训练 YOLOv8 的门槛其实不高但 VisDrone 训练图像很大如果显卡显存不够分辨率提升这一步就会非常吃力。我的建议是至少 8GB 显存起步理想是 24GB 以上。显存小也不是不能做后面会提到梯度累积和切图训练这两个绕开显存限制的办法。软件环境方面我使用的是常规的 Python 3.8 及以上版本PyTorch 建议装对应 CUDA 的官方版本。模型部分直接安装 YOLOv8 官方 Python 包即可安装命令pip install ultralytics装完后在命令行敲yolo如果能弹出提示说明环境就绪。这里有个小坑如果你同时装了多个深度学习框架要留意依赖版本冲突建议用独立的虚拟环境别把系统环境搞乱。2.2 VisDrone 标注转 YOLO 格式VisDrone 官方给的是 txt 标注每一行格式为bbox_id, x, y, w, h, score, category, truncation, occlusion其中x, y是目标左上角坐标w, h是宽高都没有归一化。category 为 0 表示 ignore 区域1 到 11 对应具体类别。YOLO 格式需要的是归一化后的中心点坐标和宽高category x_center y_center w h所以转换脚本要做四件事读取原始标注、过滤掉 ignore 和目标面积过小或 score 为 0 的框、把 category 减 1 映射到 0-9、最后做归一化。下面是我实际用过的转换脚本可以直接参考import os import glob def visdrone2yolo(src_dir, dst_dir, img_width2000, img_height1500): os.makedirs(dst_dir, exist_okTrue) for txt_path in glob.glob(os.path.join(src_dir, *.txt)): base os.path.basename(txt_path).replace(.txt, ) out_path os.path.join(dst_dir, base .txt) with open(txt_path, r) as f, open(out_path, w) as out: for line in f: parts line.strip().split(,) if len(parts) 7: continue x, y, w, h, score, category map(float, parts[0:6]) # 过滤 ignore 区域和无效标注 if category 0 or score 0: continue cls_id int(category) - 1 if cls_id 0 or cls_id 10: continue # 过滤过小的框像素小于2的基本是噪声 if w 2 or h 2: continue x_center (x w / 2) / img_width y_center (y h / 2) / img_height norm_w w / img_width norm_h h / img_height out.write(f{cls_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}\n)需要注意VisDrone 不同划分的图像尺寸不完全一致不能像我上面这样写死宽高。更稳妥的做法是先从对应图像文件读取真实宽高再传进来甚至可以在脚本里用图像库自动获取。我写死 2000×1500 只是为了演示逻辑实际使用时一定要按每张图的真实尺寸归一化。2.3 数据集目录与配置文件转换完成后目录结构组织成 YOLO 训练的标准形式visdrone/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── visdrone.yaml对应写一个数据集配置path: /data/visdrone train: images/train val: images/val names: 0: pedestrian 1: people 2: bicycle 3: car 4: van 5: truck 6: tricycle 7: awning-tricycle 8: bus 9: motor这个 yaml 文件里的 names 顺序必须和转换脚本里的类别映射保持一致否则训练出来的类别标签会错位。另外建议训练前用 YOLOv8 的可视化接口抽查几张图确认标注框是否贴合目标轮廓。如果标注偏移严重后面所有的提升都无从谈起。3. 基线训练跑通流程找到瓶颈3.1 超参数设计第一次跑基线我的目标是快速验证数据管线正常同时拿到一组可靠的对比数据。模型我选择的是 YOLOv8s 而不是 n。原因很简单n 太小能力上限低后面调优时精度涨幅会受限而 s 的训练成本适中精度已经有不错的起点。如果你显存确实紧张用 n 也可以但基线数字会低一些。输入分辨率先用 640这是官方默认值训练速度快也能暴露小目标检测的底线问题。其他超参数尽量不动只把 epoch 加到 200因为 VisDrone 类别多、场景多200 轮比较稳妥。batch 大小按显存能承受的最大值来我的配置是 16配合默认的自动混合精度 AMP。这里要解释一个关键点为什么要用官方 COCO 预训练权重而不是从零训练。YOLOv8 模型在 COCO 上已经学到了丰富的通用特征尤其是纹理、边缘、形状这些底层信息。VisDrone 虽然场景特殊但底层视觉特征和 COCO 是通用的用预训练权重初始化训练会快得多精度也通常更高。3.2 训练命令与过程监控使用命令行或者 Python API 都可以我习惯用命令行参数一目了然yolo detect train datavisdrone.yaml modelyolov8s.pt epochs200 imgsz640 batch16 device0训练过程中要重点盯几个指标P是精确率R是召回率mAP50是 IoU 阈值为 0.5 时的平均精度mAP50-95是更严格的综合指标。对小目标场景来说R比P更值得关注因为小目标本来就容易被漏检召回率上去了才有后续调整空间。我自己跑完 200 轮基线结果大概是 mAP50 在 0.35 到 0.40 之间mAP50-95 在 0.20 左右。不同硬件、不同随机种子会有浮动这个数字不关键关键是看到趋势模型对行人、公交车这种相对大一点的目标检测尚可但对摩托车、自行车这类小目标召回非常低几乎没有超过 0.2。3.3 基线结果分析拿到基线后不要急着改参数先搞清楚问题到底出在哪。我通常做两件事。第一看训练结束生成的混淆矩阵和 P-R 曲线。混淆矩阵能直观显示哪些类别之间容易相互混淆比如行人和骑自行车的人在无人机俯拍视角下形态接近误检率就会很高。第二把验证集上的预测结果可视化出来看漏检目标在画面中的分布。你会发现漏检的大多是面积小于 20×20 像素的目标尤其是暗光处和密集人群里的目标。由此可以得出一个结论YOLOv8 默认 640 输入配合 stride 为 8 的最小检测层对小目标的特征表达严重不足。后面所有优化都应该围绕一件事来展开让模型在更大的特征图尺度上看到这些微小目标。这也是 4.2 节切图方案的出发点。4. 小目标精度提升的五个关键操作4.1 输入分辨率性价比最高的参数第一个动作把输入分辨率从 640 提高到 1280或者如果有显存直接上 1536。原理非常朴素输入分辨率越高小目标在图像中占据的像素就越多模型就有更多特征可以学习。在 stride 为 8 的最小检测层上一个原始的 16×16 目标在 640 输入下约等于 2×2 像素在 1280 输入下就变成约 4×4 像素特征表达能力强了不止一倍。具体训练命令yolo detect train datavisdrone.yaml modelyolov8s.pt epochs200 imgsz1280 batch8 device0这里 batch 从 16 降到了 8因为图像面积变大了 4 倍显存占用剧增。如果显存还是不够可以加ampTrue默认开启和accumulate4让梯度累积四步再更新一次等效于 batch 32。我在实测中发现仅这一点改动mAP50 就能提升大约 4 到 6 个点。这是整个调优过程中性价比最高的一步强烈建议优先尝试。但有一点要注意训练时用了 1280推理时也要用 1280 或至少接近这个尺度不要训练用高分辨率、推理用 640那样精度会明显回退。4.2 切图训练与切图推理提升分辨率的本质是让每个目标在输入图像里占更多像素。但直接加大分辨率有上限它受显存约束。切图的思路则是另一个方向把大图先切成若干小图每个小图单独训练和推理。这样在不改变模型输入尺寸的情况下让目标相对尺寸变大。切图有两种做法。第一种是离线切图训练把原始 2000×1500 的大图切成 4 到 6 块比如 1000×1000 带 20% 重叠的子图过滤掉没有有效目标的空白子图用这批子图重新训练。这样做训练集的数量会膨胀好几倍但每个目标在子图中的相对尺度都变大了模型会更容易收敛。第二种是推理阶段切图也就是 Slicing Aided Hyper Inference 的做法。推理时用滑动窗口在原始大图上遍历检测每个窗口独立推理最后把所有窗口的检测框映射回原图坐标做一次全局 NMS 合并。我建议两者结合训练阶段可以不做太复杂的切图直接使用高分辨率训练推理阶段用滑动窗口切图来弥补训练和推理的尺度差异。下面是一个简单的推理阶段切图伪代码import numpy as np def slice_inference(model, img, slice_size640, overlap0.2): h, w img.shape[:2] detections [] step int(slice_size * (1 - overlap)) for y in range(0, h - slice_size 1, step): for x in range(0, w - slice_size 1, step): patch img[y:y slice_size, x:x slice_size] results model(patch, imgszslice_size, conf0.25) for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() score box.conf[0].item() cls int(box.cls[0].item()) detections.append([x x1, y y1, x x2, y y2, score, cls]) # 最后用 torchvision.ops.nms 或 opencv 做合并 return detections这个思路对小目标的提升是非常明显的在保持模型不变的前提下mAP50 通常能再涨 3 到 5 个点。代价是推理速度会成倍下降一个 2000×1500 的大图如果切成 640×640要跑十几个 patch。实际工程中要按帧率要求做取舍不能无脑切。4.3 数据增强策略的取舍YOLOv8 默认开启了一堆数据增强其中效果最强的是 Mosaic。Mosaic 把四张图拼在一起训练能显著提升模型泛化能力但对小目标并不全是好事。因为四张图拼在一起后每张图都会被等比缩小原本就很小目标再次被缩小很多直接缩到不可见的程度。解决办法是在训练后期关闭 Mosaic让模型回到真实的尺度分布上微调。官方超参数里有一个close_mosaic字面意思是最后 N 轮关闭 Mosaic。比如训练 200 轮可以设置close_mosaic10让最后 10 轮用完整原图训练这往往能给 mAP 带来一个小幅提升。Copy-Paste 和 MixUp 这两个增强我在 VisDrone 上做了对比实验结果发现对小目标场景收益不大。原因我认为是Copy-Paste 会在一个位置插入另一个目标容易产生不自然的遮挡关系MixUp 则会让目标透明度变低已经很小的目标在叠加后更加模糊。所以如果时间有限建议只保留 HSV 颜色扰动、随机翻转和多尺度训练这类几何和颜色层面的增强对小目标更友好。多尺度训练也很值得加。YOLOv8 支持在多尺度图像上训练每轮迭代随机选择一个尺度这相当于一种免费的尺度抖动让模型对目标大小更不敏感。4.4 从模型结构上照顾小目标除了数据和输入层面模型结构本身也值得动一动。我做的第一个结构尝试是增加 P2 检测层。YOLOv8 默认从 stride 8 的 P3 层开始输出检测结果而 P2 层是 stride 4 的高分辨率特征图图上是原始图像四分之一大小小目标的响应会更完整。具体做法是修改 YOLOv8 的 yaml 配置文件在检测头里增加一个基于 P2 特征的分支。这个改动会增加计算量但收益很直接。实测在小目标类别上的召回率提升明显不过对显存和推理耗时都有影响需要权衡。第二个尝试是加注意力模块。我测试过在 backbone 末尾接一个 CBAM以及在颈部加 SimAM。效果比较稳定的是轻量级注意力参数增加不多但能帮助模型聚焦目标区域。要注意别加得太重小目标数据集本身量级不小太强的注意力模块可能过拟合反而掉点。第三类是从损失函数入手。小目标对 IoU 的变化非常敏感目标本身只有十几个像素预测框偏移两三个像素 IoU 就骤降梯度也容易不稳定。常见的改进方向是用 Wise-IoU 或 NWD 这类对小目标更友好的回归损失。NWD 的思路是把边界框建模成二维高斯分布用 Wasserstein 距离衡量两个框的相似度对微小偏移的度量更平滑。网上有现成的实现我接进去后定位精度有了一些改善尤其是对极小的目标。4.5 把后处理也当成精度提升的一部分训练调优做到位了后处理还能再抠一点分出来。YOLOv8 推理时的默认置信度阈值是 0.25NMS 的 IoU 阈值是 0.7。在 VisDrone 这种密集场景下我建议把 NMS 的 IoU 阈值调到 0.5减少重叠框的误保留同时把置信度阈值适当提高到 0.3 到 0.4过滤掉一堆低置信度的误检框。如果你的应用场景允许更高的召回率可以开启 TTA。YOLOv8 提供了测试时增强会对图像做多尺度、多方向变换后综合结果。TTA 对小目标检测效果的提升是稳的大概能涨 1 到 2 个点 mAP但推理耗时成倍增加我一般只在线下评测或离线批处理场景用。另外对密集目标场景可以设置max_det避免同一区域输出太多框。实际在无人机俯拍的密集人群画面里过高的 max_det 会导致大量低质量预测框涌出反而降低评估指标。5. 训练中常见问题与排查实录5.1 显存溢出这是做高分辨率和切图训练时最常碰到的报错典型错误是 CUDA out of memory。排查顺序先看 batch 是不是太大再看是否关闭了 AMP。解决办法优先级从低到高降低 batch、开启梯度累积、关闭 Mosaic、降低输入分辨率。注意梯度累积只是延迟了更新频率并不能减少单步显存占用它解决的是 batch 太小导致 BN 统计不稳定和收敛变差的问题。如果你真的需要大分辨率训练但显存只有 8GB我建议直接用切图方案把大图切成小块训练而不是硬撑着跑 1280 分辨率。后者大概率会频繁 OOM时间全浪费在重启上。5.2 训练不收敛或指标震荡训练前几十轮 loss 不降是正常现象但如果在 100 轮后 mAP 还在震荡上不去就要考虑学习率问题了。YOLOv8 默认 lr0 是 0.01对小数据集来说可能偏大。我一般在小目标场景会把初始学习率降到 0.001 到 0.002然后开启 warmup。另一个隐藏问题是预训练权重没加载成功如果发现 loss 从特别高的值开始说明模型可能随机初始化了检查一下权重路径。指标震荡还有一个原因是 batch 太小。如果只有 4 或 8BN 统计不稳定训练曲线会上下剧烈跳动。这时用 accumulate 增大等效 batch或者干脆缩小模型尺寸。5.3 小目标类别漏检严重如果你发现模型对行人和摩托车几乎不预测先不要急着改模型结构先统计一下数据集里每个类别的目标数量。VisDrone 的类别分布严重不均衡car 可能占比极高而 awning-tricycle 这种类别数量很少模型学不好很正常。这种情况下可以做简单的类别加权采样或者在损失函数里给少数类更高的权重。另外检查一下数据增强是否把小目标增强得过于模糊。我遇到过 Mosaic 开着导致小目标大量消失关闭后漏检率明显下降的情况。5.4 误检过多误检率和数据质量直接相关。VisDrone 的标注里有大量遮挡严重的框模型会学到把部分遮挡的人体也预测出来这在严格评估下算误检。我的处理方式是在转换脚本里过滤掉 truncation 或 occlusion 值过高的标注让模型只学干净样本。推理侧把置信度阈值调高是立竿见影的办法但会牺牲召回率。更推荐的做法是做类别层面的阈值区分比如对容易混淆的行人和骑车人单独给一个较高阈值。5.5 常见问题速查表问题可能原因快速解决方案CUDA OOMbatch 过大、分辨率过高降 batch、开 AMP、用梯度累积loss 降不动学习率过高或预训练没加载降低 lr0、检查权重路径训练震荡batch 太小调大 batch 或 accumulate小类别漏检类别不平衡、目标太小类别加权、切图、提高分辨率误检暴涨标注遮挡多、阈值偏低过滤坏标注、调高置信度阈值训练慢数据加载瓶颈、分辨率太高开多线程、缩小训练尺寸推理掉帧切图太碎、TTA 开启加大切片尺寸、关闭 TTA6. 消融对比与落地建议6.1 我的消融实验总览下面这组数据是我在某个实验环境下得到的相对提升环境不同数值会有出入但它能反映各手段的贡献大小。配置组合mAP50 相对提升默认 640 输入基线1280 输入约 51280 推理切图约 31280 切图 关闭后期 Mosaic约 11280 切图 P2 层 注意力约 2全部组合约 11结论很清楚分辨率提升和推理切图是两大主力结构改动是锦上添花。如果你的时间和算力有限优先做前两者。这个顺序对大多数小目标场景都是成立的。6.2 推理部署时的注意点小目标模型落地时精度和速度的矛盾会被放大。我通常会把模型导出为 ONNX再用 TensorRT 加速这样在相同分辨率下推理速度能快不少。量化时要特别谨慎INT8 量化对小目标的特征表达损失很大可能出现模型每张图都测得到目标、推理时一个都测不到的情况。如果一定要用 INT8建议用 1280 以上的分辨率和更大的输入图来补偿量化误差或者干脆用 FP16。如果业务方只给很紧的推理预算我建议放弃大分辨率推理改用切图加小模型方案。比如用 YOLOv8n 在 640 分辨率下切图速度和精度往往比用 YOLOv8l 全图推理更平衡因为它让每个目标的相对尺度更大。6.3 后续还能怎么继续提升走到这一步常规手段基本用完了还想继续提精度就得往更高阶的方向走。我个人经验里有几个方向值得研究。第一个是伪标签半监督。用当前最强模型在没有标注的测试集上生成预测挑高置信度的作为伪标签加入训练集重训。这个迭代在小目标场景里很有效相当于免费扩充了数据。第二个是结合检测与分割的强先验。无人机视角下的车辆、行人都有比较规整的轮廓如果先用分割模型提取前景区域再引导检测器聚焦前景区域误检率通常会明显下降。第三个是尝试新的检测器结构比如 RT-DETR 这类端到端检测器或者把 YOLOv8 和 DETR 的优势结合。但这类模型训练成本高调参也更复杂建议先在小规模数据上验证可行性再投入。说实话小目标检测没有一招鲜的方案每一次提升都来自对数据和模型的深入理解。VisDrone 这个数据集好在它难度足够大能逼着你把每一步都做扎实。我做完这一整套实践后的体会是很多同学一开始就想着改网络结构这是最不明智的。先检查和优化数据、输入、增强往往花最小的代价就能拿到最大的提升。只有在这些常规手段都不起作用时再去动网络结构你才会真正理解结构改动到底解决了什么问题。这套思路放到任何小目标检测项目里都是通用的。