☰
YOLOv8实战:VisDrone2019无人机小目标检测从训练到部署
2026/9/25 21:37:47 网站建设 项目流程

最近把无人机视角的目标检测任务重新捡起来,发现很多人在拿到 VisDrone2019 数据集之后的第一反应,都是直接拿 YOLOv8 默认参数跑一遍,然后 mAP50 上不去,就开始怀疑模型不行。其实问题往往不在模型,而在数据本身。VisDrone2019 是无人机俯拍场景下的大规模目标检测基准,目标尺寸小、分布密集、遮挡严重,和平时做 COCO 那种水平视角完全是两种玩法。这篇文章我完整记录一次基于 YOLOv8 训练 VisDrone2019-DET 子集的流程,重点说透环境怎么搭、数据格式怎么转、训练参数怎么调,最后再给到导出部署的建议,适合刚接触无人机视觉、正在为小目标检测发愁的玩家。

1. VisDrone2019到底难在哪:先用数据视角把“无人机场景”看透

1.1 官方数据集结构里,我们到底该用哪一部分

VisDrone2019 不是一个单一文件包,它本身包含多个子任务,常见的有 DET(目标检测)、VID(视频目标检测)、SOT(单目标跟踪)、MOT(多目标跟踪)。大家平时说“训练 Visdrone2019”,默认指的是 VisDrone2019-DET,也就是全帧图像上的目标检测任务。

整个 DET 数据集分成了 train、val、test-dev 三部分,训练集图像在 6400 张左右,验证集在 500 多张,测试集大约 1600 张。注意这个量级:训练集不算大,之前我们在 COCO 上动辄十几万张图,到了这里整个数据集可能还不如 COCO 一个类别多。所以训练 VisDrone 时,过拟合风险比 COCO 高不少,后面很多参数设置都要围绕“数据量有限”这一点来考虑。

另外官方包的目录结构一般是这样:

VisDrone2019-DET/ ├── Annotations/ │ ├── train/ │ │ ├── 0000001_00001_d_0000001.txt │ │ ├── ... │ ├── val/ │ └── test-dev/ ├── Sequences/ │ ├── train/ │ ├── val/ │ └── test-dev/

图像文件在 Sequences 目录下,每个图像对应的标注是 Annotations 目录下同名 txt 文件。很多第一次接触的人容易在目录结构上出问题,下完数据后不知道把路径指到哪里,后面转换格式也就跟着出错。

1.2 10个类别与原始标注的“怪习惯”

VisDrone2019-DET 标注了 10 个目标类,分别是 pedestrian(行人)、people(人群)、bicycle(自行车)、car(小汽车)、van(面包车)、truck(卡车)、tricycle(三轮车)、awning-tricycle(遮阳三轮车)、bus(公交车)、motor(摩托车)。

原始标注文件和 YOLO 系列用的格式差别很大,它一行一条记录,用逗号分隔,一共 8 列:

<bbox_left>,<bbox_top>,<bbox_width>,<bbox_height>,<score>,<category>,<occlusion>,<truncation>

比如一行真实的标注可能是:

493,142,302,159,1,4,1,0

这里 bbox_left、bbox_top 是目标框左上角坐标,bbox_width、bbox_height 是宽高,后面四个字段很多人会忽略掉,但里面有个隐藏的关键点:score 那一列。VisDrone 的 score 字段不是检测置信度,而是“这个框是否参与训练”。score 为 1 表示是真实目标框,score 为 0 表示这个区域是标注方认为的复杂区域或背景干扰区,建议训练时当作忽略区处理。麻烦的是,同一张图里一个目标被多个框框住的情况也存在,转换时如果不过滤 score,训练数据会混入大量负样本信息,导致模型被搞糊涂。

occlusion 表示遮挡程度,truncation 表示截断程度,这两个在普通检测任务里一般不直接用,但它们能辅助分析模型漏检原因。VisDrone 里类别编号从 1 开始,1 到 10 对应上面说的 10 个类,11 是 others,12 是 ignore。转 YOLO 格式时通常只保留 1 到 10,把 11、12 丢掉。

1.3 三个绕不开的难点:小目标、高俯拍、密集遮挡

第一是目标尺寸普遍极小。无人机飞行高度决定了目标在画面里只占一小块区域,很多目标在原始分辨率下也就 20×20 像素左右,放到 640 分辨率训练后可能只剩几个像素。这一点直接决定了训练时的 imgsz 不能无脑用默认值。

第二是高俯拍导致的视角变化。水平视角下汽车、行人有一个相对统一的“侧面”外观,无人机俯拍时人只剩一个头顶,汽车变成车顶俯视图,同一个类在不同高度、不同角度下外观差异非常大。这会让模型学习“类内一致性”更困难,单纯堆数据量还不够。

第三是密集遮挡与类别混淆。停车场里一排车紧密排列,路边行人和骑电动车的人距离极近,还有遮阳三轮车这种外观上可能和小汽车混淆的类别。VisDrone 的标注里“pedestrian”和“people”定义还很微妙,人群聚集时边界框互相重叠,转换格式后如果不注意负样本,训练过程会非常不稳定。

这也是为什么我建议训练前先把数据翻来覆去多看几遍。很多人直接跑训练,直到结果不好才开始怀疑数据,其实问题在数据准备阶段就已经埋下了。

2. YOLOv8训练环境:Ubuntu 20.04从CPU到GPU的一次到位配置

2.1 版本搭配:Python、PyTorch、Ultralytics和CUDA先对齐

YOLOv8 的 Python 实现来自 Ultralytics 团队,安装时最怕的是 torch、torchvision、ultralytics 三方版本互相打架。我这次在 Ubuntu 20.04 上搭环境,推荐直接用 conda 做一个独立环境,避免把系统 Python 弄乱。

基础搭配参考:

组件推荐版本
Ubuntu20.04 / 22.04
Python3.10
PyTorch2.0.1 或 2.1.x
torchvision与 torch 对应
CUDA11.8 或 12.1
Ultralytics8.1.x
OpenCV4.8+

之所以推荐 Python 3.10,是因为它对 torch 的 wheel 支持最全,3.11、3.12 在部分版本上也能用,但没必要给自己增加不确定性。

2.2 纯CPU环境:先把完整训练流程跑通

如果你手头没有 N 卡,比如只有一台普通笔记本或者云上的 CPU 服务器,也能把训练流程完整跑起来,只是速度慢。VisDrone 全量训练在 CPU 上非常痛苦,但你可以缩小数据量验证流程。配置方法:

conda create -n yolov8 python=3.10 -y conda activate yolov8 pip install ultralytics

这一步会把 ultralytics 以及它依赖的 torch、torchvision 一起装上。不过默认安装的 torch 可能带 CUDA 版本,体积比较大。想装纯 CPU 版本可以这样:

pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics

装完后验证:

python -c "import torch; print(torch.__version__)"

如果输出正常,就可以直接用 CPU 跑一个小轮次的训练,目的不是出精度,而是确认数据转换、代码链路没有问题。我自己测试过,在纯 CPU 上用 imgsz=640、batch=8、训练 3 个 epoch、只取 200 张图,大概也要几十分钟到一两个小时。所以 CPU 环境更适合做“流程验证”,不适合完整训练。

2.3 GPU环境:CUDA安装和PyTorch的隐性坑

有显卡的话,先把驱动确认好。Ubuntu 20.04 上输入:

nvidia-smi

能看到显卡列表和显存信息,说明驱动正常。注意 nvidia-smi 右上角显示的 CUDA Version 是“驱动支持的最高 CUDA 版本”,不代表你已经装了 CUDA Toolkit。PyTorch 是自带 CUDA runtime 的,你只需要保证驱动版本够新,然后安装对应版本的 PyTorch 即可。

如果驱动没装好,先装驱动:

sudo apt update sudo apt install ubuntu-drivers-common sudo ubuntu-drivers autoinstall sudo reboot

驱动装完后创建环境并安装 ultralytics:

conda create -n yolov8 python=3.10 -y conda activate yolov8 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics

常见报错之一是安装完 torch 后,用torch.cuda.is_available()返回 False。原因一般是驱动太旧,或者 PyTorch 版本和驱动支持的 CUDA 版本不匹配。遇到这种情况先看 nvidia-smi 里的驱动版本,再看你装的 PyTorch 对应的是 cu118 还是 cu121,换一个匹配的 wheel 重新装即可。

还有一个容易忽略的点:conda 环境里nvidia-smi可能不可用,但不影响 torch 调用 GPU。验证用 Python 命令更靠谱:

python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"

输出类似:

True NVIDIA GeForce RTX 3090

这就说明环境跑通了。

3. 数据准备是重头戏:VisDrone标注转YOLO格式与数据集划分

3.1 原始标注转YOLO格式前,先想清楚哪些框要留

YOLOv8 训练期望的标注是每个图像对应一个同名的 txt 文件,文件里每一行是:

class_id x_center y_center width height

注意所有坐标都归一化到 0 到 1 之间。VisDrone 原始标注是像素坐标和原始类别编号,所以必须做转换。

转换时最影响结果的决策是:如何处理 score 为 0 的框、类别 11 和 12。我在实验里采用的做法是:

  • score 为 0 的框直接丢弃。理论上它们可以转为“忽略区域”供模型训练时参考,但 YOLOv8 的标准训练流程不支持直接传入忽略框,强行保留只会让模型把负样本当正样本学。
  • 类别 11(others)、12(ignore)直接丢掉。
  • 类别 1 到 10 保留,转换时 class_id 减 1,变成 0 到 9。

这样做的代价是忽略了部分难例,比如一个目标周围恰好有 ignore 区域时,模型会在这个区域附近出现误检。但实测下来,对初次训练来说这个方案最稳,后续想提升再考虑用割草机式的工具去处理 ignore 区域。

3.2 一个可直接用的批量转换脚本

我写了一个比较保守的转换脚本,按上面说的策略批量处理整个数据集:

from pathlib import Path import cv2 def visdrone_to_yolo(ann_file: Path, image_dir: Path, output_dir: Path): image_file = image_dir / (ann_file.stem + ".jpg") if not image_file.exists(): print(f"missing image: {image_file}") return img = cv2.imread(str(image_file)) if img is None: print(f"cannot read: {image_file}") return h, w = img.shape[:2] out_lines = [] with ann_file.open("r", encoding="utf-8") as f: for line in f: parts = line.strip().split(",") if len(parts) < 6: continue left, top, bw, bh = map(float, parts[:4]) score = float(parts[4]) category = int(parts[5]) if score < 1: continue if category < 1 or category > 10: continue cx = (left + bw / 2.0) / w cy = (top + bh / 2.0) / h nw = bw / w nh = bh / h out_lines.append( f"{category - 1} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n" ) out_path = output_dir / (ann_file.stem + ".txt") out_path.write_text("".join(out_lines), encoding="utf-8") if __name__ == "__main__": base = Path("/path/to/VisDrone2019-DET") for split in ["train", "val"]: ann_dir = base / "Annotations" / split img_dir = base / "Sequences" / split out_dir = base / "labels" / split out_dir.mkdir(parents=True, exist_ok=True) for ann_file in sorted(ann_dir.glob("*.txt")): visdrone_to_yolo(ann_file, img_dir, out_dir)

脚本有几个细节需要注意。第一,图像的宽高不是写死的,而是用 cv2 读图后动态获取,因为 VisDrone 图片尺寸虽然大部分统一,但也有个别尺寸不同的情况。第二,坐标归一化时用(left + bw / 2.0) / w,不是left / w。第三,输出目录建议放在数据集根目录下的 labels/train、labels/val,这样 data.yaml 写起来清爽。

跑完脚本后,随便打开一个输出 txt 看一眼,比如原图分辨率是 2000×1500,一个目标框的像素坐标是 (493, 142, 302, 159),转出来大概是:

3 0.322000 0.221000 0.151000 0.106000

这中间任何一步输出异常,都要回到上一级查原始 txt。

3.3 数据划分:原子图方式,但注意验证集别用训练集图像

VisDrone 官方已经划分好了 train 和 val,所以不需要自己重新随机划分。但很多人喜欢把所有图混在一起再重新划分,这其实有风险:VisDrone 的序列图存在很强的时域相关性,同一个航拍片段里前后帧非常相似,如果 train 和 val 里混入了同一个片段的图像,验证集指标会虚高,部署到新场景后立刻现原形。所以最好直接用官方划分,除非你有非常明确的自定义场景。

转换完目录后,在数据集根目录写一个 data.yaml:

path: /path/to/VisDrone2019-DET train: images/train val: images/val nc: 10 names: 0: pedestrian 1: people 2: bicycle 3: car 4: van 5: truck 6: tricycle 7: awning-tricycle 8: bus 9: motor

这里 images/train 是相对 path 的路径,对应官方的 Sequences/train。如果你把图像目录名字改了,这里要同步改。我第一次训练时图省事,只把 labels 建好,忘了把 images 目录复制到预期路径,YOLOv8 报了一个“dataset not found”就退出了。这类问题主要是路径语义没对清楚。

4. 训练实战:从能跑通到效果提升的参数调整全记录

4.1 一条可复现的基础训练命令

环境装好、数据转好之后,训练其实只有一条命令:

yolo detect train data=/path/to/VisDrone2019-DET/data.yaml \ model=yolov8m.pt \ epochs=100 \ imgsz=1280 \ batch=16 \ name=visdrone_yolov8m_1280

如果想在 Python 脚本里控制训练流程,也可以:

from ultralytics import YOLO model = YOLO("yolov8m.pt") model.train( data="/path/to/VisDrone2019-DET/data.yaml", epochs=100, imgsz=1280, batch=16, name="visdrone_yolov8m_1280", )

这里model=yolov8m.pt非常重要,它表示从 COCO 预训练权重初始化,而不是从零开始。很多人会把 model 参数写成yolov8m.yaml,那样就是随机初始化,在 VisDrone 这么小的数据集上效果会差很多。预训练权重提供了基础的图像特征提取能力,后续迁移到无人机视角会快得多。

4.2 imgsz怎么选:640、960还是1280

VisDrone 原始图像普遍是 2000×1500 左右,目标框又小,如果训练分辨率设为默认的 640,相当于把原始图缩小到三分之一再输入网络,很多小目标会在下采样过程中直接消失。我建议至少从 1280 起步。

但分辨率不是越大越好,imgsz 每翻一倍,计算量大约是四倍。1280 尺寸下,batch=16 对显存的压力很大,24GB 显存都未必能稳定跑起来。如果你的卡是 8GB 或 12GB,可以考虑两个方案:一个是把 batch 降到 8 或 4,另一个是先用 960 跑通,再用 1280 做精调。

这里有个常见的操作误区:想提升精度,不一定是全程用 1280,可以先用 960 训练前期,最后二三十个 epoch 用 1280 继续训练。Ultralytics 也支持训练过程中切换 imgsz,但只建议对同一份数据和 labels 操作。我自己跑下来,纯 1280 从头训练比“960 加 1280 段训”更稳,因为前者整体收敛轨迹更一致。

4.3 batch和learning rate要一起调

YOLOv8 默认的 batch=16,但在 VisDrone 大分辨率下经常跑不动。很多人直接把 batch 调到 4 或者 8,却忘了学习率,结果模型收敛变慢。YOLOv8 的优化器是自适应学习率策略,batch 变小后,每个 step 看到的样本变少,梯度更不稳定,应适当降低学习率。

一个比较实用的搭配:

显卡显存imgszbatchlr0备注
8GB640160.01快速验证流程
12GB96080.005中等精度训练
24GB128080.003高精度训练
24GB+1280160.002完整训练

epochs 我建议至少 100。VisDrone 训练图才 6000 多张,100 epoch 相当于模型把每张图学了 100 遍,基本够用。如果你看到 val 指标还在稳步上升,可以加长到 150 或 200,但要注意过拟合。

4.4 预训练权重选型:s、m、l到底该用哪个

YOLOv8 系列有 n、s、m、l、x 五个规模,我在 VisDrone 上推荐从 m 起步。原因是 s 和 n 的参数量有限,特征提取能力较弱,面对小目标和密集场景容易欠拟合;l 和 x 精度更高,但训练时间和显存要求也高得多。如果你的卡是 8GB,又想跑 1280,建议先试 m 加 batch=4,而不是直接上 l。

如果时间允许,用同一份数据分别跑 s、m、l 各一个短训对比,你会发现 m 到 l 的提升是实打实的,但这个提升值不值得多花两三倍训练时间,得看你的部署平台。无人机机载设备往往算力有限,最后训练阶段选 m 在精度和部署成本之间更平衡。

4.5 数据增强设置:旋转和mosaic是无人机场景的宝藏

YOLOv8 默认开了 mosaic、随机平移、缩放、翻转等增强,但默认不一定适合无人机视角。我最想调的是两个参数:

  • degrees:默认是 0,意为不随机旋转。无人机飞行姿态变化大,俯拍画面里目标方向是很随意的,把 degrees 开到 30 或 45 能显著提升模型对任意方向目标的适应能力。
  • mosaic:默认 1.0。mosaic 把四张图拼接成一张训练图,很大程度上缓解了 VisDrone 小目标占比过高的问题。但 mosaic 增强会在最后 10 个 epoch 自动关闭,因为这阶段拼图会干扰收敛。

我自己用的 train 参数里会加:

degrees=45, mosaic=1.0, fliplr=0.5, scale=0.5,

注意 scale 不要开太大,无人机俯拍的尺度变化确实很大,但过度缩放会让原有分辨率优势消失。实测比较稳的是 scale=0.5 左右,再大对小目标反而不友善。

5. 训练结果怎么看:loss曲线、mAP指标和失败案例分析

5.1 正常收敛的loss曲线长什么样

训练结果默认保存在项目目录下的runs/detect/visdrone_yolov8m_1280/,里面有一个 results.csv,记录了每个 epoch 的 train_loss、val_loss、mAP 等指标。用 pandas 读出来画个图,几行代码搞定:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/visdrone_yolov8m_1280/results.csv") plt.figure(figsize=(12, 6)) plt.plot(df["epoch"], df["train/box_loss"], label="train box_loss") plt.plot(df["epoch"], df["val/box_loss"], label="val box_loss") plt.legend() plt.show()

正常收敛一般有两个特征:一是 train 和 val 的 box_loss 同时下降,且尾部趋缓;二是 val 的 mAP50 曲线在后期缓慢上升,而不是大起大落。如果 val loss 在某个 epoch 后开始反弹而 train loss 还在降,基本可以判断过拟合,下一轮就考虑减小模型规模、增大数据增强或提前停止。

VisDrone 场景里最常见的异常是 val mAP50 一直卡在 0.2 以下,这种时候先别急着调模型,回到数据层面检查 labels 数量和类别分布。我把 labels 可视化打开后,经常发现某些类别的标注框数量极少,比如 bus 只有几百个框,模型很难学出来。

5.2 按类别看指标,而不是只看总mAP

训练结束后 YOLOv8 会输出每类的 P、R、mAP50,我习惯于把 Pedestrian、People、Car 这类高频类别和 Tricycle、Awning-tricycle 这类低频类别分开看。高频类别 AP 高、低频类别 AP 低,这是正常现象;但如果 Car 的 AP 也掉到 0.3 以下,那多半是数据问题,比如忽略框没处理好,或者类别间混淆严重。

VisDrone 类别里最容易混淆的是 pedestrian 和 people,二者的标注标准本身就模糊;另外 awning-tricycle(遮阳三轮车)在俯拍视角下辨识度不高,经常和三轮车、小汽车混在一起。面对这种情况,一个可操作的思路是合并相近类别,把 pedestrian+people 合成 person,或者把 tricycle+awning-tricycle 合成一个 tricycle 类,类别数少了,每个类样本量变大,mAP 往往能拉上来。当然这是业务取舍,不是标准方案。

5.3 从混淆矩阵和val_batch里找问题

YOLOv8 默认会生成一张confusion_matrix.png,这张图非常有价值。它展示真实类别和预测类别的对应关系,对角线越亮说明分类越准,对角线外的亮点说明存在误检。我在 VisDrone 上看到最多的误检来源是背景(background)被预测成 car 或 pedestrian,原因是无人机俯拍场景里地面纹理、屋顶边缘、阴影区域与真实目标非常像,尤其低分辨率下更难区分。

建议训练完不要直接关程序,打开 val 批次的预测图多看几轮。肉眼扫一遍就能发现大量重复检测、框尺寸不合理、把密集人群拆成多个小框等问题。很多时候改进方向不是神经网络结构,而是后处理参数,比如 NMS 的 iou 阈值。YOLOv8 默认iou=0.7,对密集场景可以试试把conf=0.25调高到0.3或0.4,能明显减少误检。

我还发现一个很实操的技巧:把 results.csv 里每个 epoch 的metrics/precision(B)和metrics/recall(B)画在同一个图上,观察二者交叉位置。如果 recall 一直很高但 precision 上不去,说明模型把太多背景判成目标;反过来,precision 高但 recall 低,说明目标漏检严重,需要提高输入分辨率或增加正样本增强。

6. 部署到无人机或边缘设备:导出ONNX/TensorRT与后续优化方向

6.1 训练出best.pt之后,导出要看部署端格式

训练产生的best.pt是 PyTorch 权重,不能直接跑到大多数边缘设备上。把模型转成 ONNX 是做中间格式迁移的第一站:

yolo export model=runs/detect/visdrone_yolov8m_1280/weights/best.pt \ format=onnx \ imgsz=1280 \ dynamic=True

加dynamic=True的好处是允许推理时输入不同尺寸图像,但会牺牲一定性能。如果你确定部署时的输入尺寸固定,建议不开启,这样 ONNX 文件更小、推理更快。

如果目标设备是 NVIDIA Jetson 系列或者带 TensorRT 的板卡,可以一步到位导出 engine 格式:

yolo export model=runs/detect/visdrone_yolov8m_1280/weights/best.pt \ format=engine \ imgsz=1280 \ half=True

half=True使用 FP16 精度,显存减半、速度提升,精度损失在 VisDrone 这种小目标场景下会有一些,但多数情况可以接受。如果板卡是 RK3588 这类 SoC,则直接用 ONNX 转 RKNN 即可。注意不同部署端对 NMS 的支持不同,ONNX 导出时如果带了内置 NMS,板端处理更方便,但也更依赖算子兼容性。

6.2 小目标场景下,再往前走的两个方向

训练完不等于结束。从实测来看,VisDrone 上的精度瓶颈大部分来自小目标,两个常见优化方向值得尝试。

第一个是测试时增强(TTA)。YOLOv8 内置了 TTA,推理时对图像做多尺度翻转预测再融合结果,能稳定提升小目标 recall,代价是推理时间成倍增加。无人机实时场景下未必可用,但做离线分析时很香:

yolo detect predict model=best.pt source=xxx.jpg imgsz=1280 augment=True

第二个是切片推理,也叫裁剪推理。把大图切分成若干小块,对每一块分别做检测,再把结果合并回原图坐标。它的本质等效于把 imgsz 继续放大,小目标在切片里会变得“更大”。我测试过 640 分辨率原图推理和 1280 分辨率切片推理,后者在car和person类别上 recall 提升非常明显。缺点是推理耗时增加,密集目标多时还可能重复检测同一个物体,需要在合并阶段做二次 NMS。

6.3 个人习惯:训练记录、配置管理、分类别实验

最后分享一个我自己的习惯做法。VisDrone 训练跑一次成本不低,所以每次实验我会在数据集根目录创建一个 experiment 配置文件,把 data.yaml、训练命令、参数、备注都存成一份文本。这样回头复盘时,不会出现“这个模型当时是怎么训的”这种让所有人头疼的问题。train 和 val 目录做好版本管理,最好原始镜像打包后不再乱动;labels 转换脚本也固定下来,一旦重新生成,必须保证同一份图像同一份标注只生成一次结果。

现在这个流程跑通一次之后,换其他数据集基本就是改 data.yaml 的事,你会发现自己对“小目标”“密集场景”“俯拍视角”这些词的理解,比只看论文时要深得多。这也是我把这套流程完整记下来的原因:数据整理花的时间永远不算浪费,模型是最后一步,而不是第一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询