第一次在 VisDrone 上跑通 YOLOv8 的时候,我盯着验证集上的 mAP 数字愣了半天,总觉得代码哪里写错了。后来才反应过来,不是代码的问题,而是无人机视角下的目标检测,和你平时在 COCO 上玩的那些根本是两个世界。VisDrone 数据集里的目标又小又密,一辆车可能只占几十个像素,一个行人也就是一个小点,常规的检测思路在这上面很容易失灵。这篇文章就是记录我怎么从零开始,把 YOLOv8 在 VisDrone 上完整跑通一个 baseline 的全过程——包括数据集怎么准备、标注怎么转换、训练参数怎么调、哪些坑必须避开,以及跑通之后真实性能大概是什么水平。
1. 无人机视角下的检测难题:为什么 VisDrone 值得当第一个 baseline
1.1 小目标与极端分布
VisDrone 是一个专门面向无人机视觉理解任务的公开数据集,里面的图像全部来自无人机在不同高度、不同场景下拍摄的俯视画面。场景覆盖了城市街区、十字路口、高速公路、村庄等,目标类别一共 10 类:pedestrian(行人)、people(人群)、bicycle(自行车)、car(汽车)、van(面包车)、truck(卡车)、tricycle(三轮车)、awning-tricycle(带篷三轮车)、bus(公交车)、motor(摩托车)。
这个数据集给我最直观的感受就是:目标太小、太密、太多遮挡。同一张图里,一辆小汽车可能只占图像的 1% 都不到,而且大量目标互相重叠,车辆排成一排、行人挤成一团。传统检测模型在 COCO 上能到 40 多 mAP,但换到 VisDrone 上,能把 mAP50 做到 30 已经相当不错了。如果你刚接触这个数据集,建议先调整好心理预期,别拿 COCO 的标准去衡量。
1.2 标注格式的“坑”在哪里
VisDrone 的标注格式和 YOLOv8 需要的是两套完全不同的东西。VisDrone 官方提供的是 UOM 格式的标注文件,每个 txt 文件对应一张图,每一行代表一个目标:
<bbox_left>,<bbox_top>,<bbox_width>,<bbox_height>,<score>,<object_category>,<truncation>,<occlusion>举个例子,一行标注可能是:
169,114,79,251,1,4,0,2这表示一个目标的位置信息:左上角坐标 x=169,y=114,宽 79,高 251;score 为 1(表示确定存在的目标);类别编号为 4(对应 car);truncation 为 0(未截断);occlusion 为 2(遮挡程度中等)。
而 YOLOv8 需要的 YOLO 格式则是:
<object_class_id> <center_x> <center_y> <width> <height>所有坐标必须是归一化的,也就是要除以图像的宽和高,且是相对图像宽高的比例值,bounding box 用中心点 x、中心点 y、宽度、高度表示。这里就是第一个大坑:如果不做转换直接训练,模型根本学不到任何东西,因为 YOLOv8 会把 VisDrone 的绝对像素坐标当成归一化坐标用,损失直接乱套。
2. 数据集转换的正确姿势:从 VIA 标注到 YOLO 格式
2.1 VisDrone 目录结构先搞清楚
下载 VisDrone2019-DET 数据集之后,你会看到类似这样的目录结构:
VisDrone2019-DET ├── annotations │ ├── 0000001_00195_d_0000001.xml │ ├── 0000001_00195_d_0000002.xml │ └── ... ├── images │ ├── 0000001_00195_d_0000001.jpg │ ├── 0000001_00195_d_0000002.jpg │ └── ... ├── test │ └── images ├── train │ ├── annotations │ └── images └── val ├── annotations └── images不同版本的包目录可能略有差异,但核心就是 images 和 annotations 两个文件夹。官方标注文件是以 .xml 形式提供的,如果你下载到的是 txt 格式的 UOM 标注,那就在转换时直接处理 txt;如果是 xml,那就是基于 VIA 工具生成的标签,需要先解析 XML 再转 YOLO。建议先解压后花两分钟看一眼标注文件的实际内容,再决定脚本怎么写。
2.2 编写转换脚本:从 UOM 到 YOLO 的一行归一化
我实际写过一个最常见的转换脚本,核心逻辑并不复杂,但细节非常多。一定要记住:YOLOv8 的标注是归一化的 center_x、center_y、width、height,不是左上角坐标,这是一个最容易出错的点:
import os from pathlib import Path def visdrone_to_yolo(txt_path, out_path, img_w, img_h): with open(txt_path, "r", encoding="utf-8") as f: lines = f.readlines() yolo_lines = [] for line in lines: parts = line.strip().split(",") if len(parts) < 6: continue x1, y1 = int(parts[0]), int(parts[1]) w, h = int(parts[2]), int(parts[3]) score = int(parts[4]) cls_id = int(parts[5]) - 1 # VisDrone 类别从 1 开始,YOLO 从 0 开始 if score == 0 or cls_id < 0: continue # 归一化计算 center_x = (x1 + w / 2) / img_w center_y = (y1 + h / 2) / img_h norm_w = w / img_w norm_h = h / img_h # 过滤掉超出边界的框 if norm_w <= 0 or norm_h <= 0: continue yolo_lines.append(f"{cls_id} {center_x:.6f} {center_y:.6f} {norm_w:.6f} {norm_h:.6f}") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(yolo_lines))这个脚本里有几个细节值得说:
- 类别编号减 1:VisDrone 官方标注中类别是从 1 开始的(0 是忽略类别),而 YOLO 格式要求从 0 开始。如果忘了减一,所有类别都会整体平移一位,模型训练时虽然能收敛,但验证结果会完全对不上。
- 过滤 score 为 0 的目标:VisDrone 标注里有些目标被标记为 score=0,表示严重模糊或不确定,建议不要拿来训练。
- 归一化不能漏除图像宽高:这点看起来是常识,但实际操作中很容易图省事直接硬编码一个默认值。VisDrone 的图像尺寸虽然大多数是 2000x1500 或者 960x540 附近,但并不是完全统一,必须用每张图的真实尺寸计算。
2.3 分割训练/验证集以及转换后校验
VisDrone 官方划分已经比较合理,通常我们把官方 train 目录下的图像全部用于训练,官方 val 目录用于验证。写转换脚本时,建议把 train 和 val 分别跑一遍,生成对应的 labels 目录。
转换完成之后,千万别直接开训。先做一次可视化校验,随机挑几张图把 YOLO 格式的标注画回去,确认框和类别都对得上。这个步骤看似多此一举,却总能在第一时间暴露出脚本里的低级错误。比如我见过有人把所有类别的框都画重叠在一起,原因就是归一化坐标写成了左上角坐标。
3. 环境安装与训练配置的实战细节
3.1 ultralytics 安装与版本验证
训练 YOLOv8 最省事的方式就是直接安装 ultralytics 开源库。它把数据加载、模型构建、训练过程、验证输出全封装好了,对大部分场景已经完全够用。
conda create -n yolov8 python=3.10 conda activate yolov8 pip install ultralytics有几个环境问题建议提前留意:
- PyTorch 版本要和 CUDA 版本匹配:如果你有 GPU,建议直接安装官方源提供的 torch 版本,而不是让 pip 自动拉一个 CPU 版本。实际测试中很多刚接触的人卡在这一步,训练时发现 device 一直是 CPU。
- 装完先跑一个小 Demo:用 ultralytics 自带的预训练权重随便预测一张图,确保环境没问题,再上 VisDrone。很多环境问题在 Demo 阶段就会暴露,没必要等到训练才排查。
3.2 数据配置文件编写
YOLOv8 训练前需要准备一个 data.yaml,告诉框架数据集在哪里、类别有哪些。一个典型配置如下:
path: /home/yourname/VisDrone2019-DET 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这里最关键的一点是:path 路径建议写绝对路径,或者确保相对路径能正确对应到你的工作目录。我踩过太多次相对路径错误导致数据集加载为空的坑。另外,train 和 val 字段填写的是相对于 path 的路径,不需要写上 images 前缀之外的额外目录。
3.3 模型选择与超参数设置
跑 baseline 时,模型选择建议在 s 和 n 之间做出权衡。n 虽然速度快,但在小目标密集场景下严重缺乏容量;m 在 VisDrone 上能明显提升精度,但训练时间几乎翻倍。我用下来觉得yolov8s 是最适合作为第一个 baseline 的型号——精度和速度的平衡点,跑一晚上就能看到完整结果。
yolo task=detect mode=train model=yolov8s.pt data=visdrone.yaml \ epochs=100 imgsz=640 batch=16 device=0说一下几个超参数的实际选择逻辑:
- imgsz=640:官方预训练权重是在 640 分辨率上训练的,直接用 640 起步不会引入额外的适配成本。但 VisDrone 目标非常小,640 分辨率会丢失大量细节。baseline 阶段先用 640 把整个流程跑通,后面再逐步提升到 1280 甚至 1536。
- batch=16:取决于显存大小。如果显存是 24GB 级别,batch=16 通常没问题;如果只有 12GB,建议降到 8。显存不够时优先减小 batch,而不是降低分辨率,因为分辨率对 VisDrone 的影响远大于 batch 大小。
- epochs=100:VisDrone 训练集有 6471 张图,100 轮已经能跑出一个可参考的 baseline。如果你的时间宽裕,加到 150 也合理。断点续训机制可以让你随时中断再继续,不用一口气跑完。
4. 训练、验证与预测:跑通 Baseline 的全过程
4.1 训练日志怎么读
训练一开始,终端会滚动输出每一轮的 box_loss、cls_loss、dfl_loss 和各类指标。很多人看着日志不知所措,其实核心只需要关注三件事:
- box_loss 和 cls_loss 是否整体下降:如果前 20 轮两个 loss 都不降,大概率是数据转换出了问题,应该立即停训排查。
- 验证集 mAP 是否在缓慢上升:VisDrone 上 mAP 的提升非常缓慢,不像 COCO 那样有大幅跃升。你可能会看到前 10 轮 mAP50 一直徘徊在 5~8,这是正常的,到 50 轮以后才会慢慢爬升到 20+。
- 模型是否过拟合:每轮都会打印训练集和验证集指标,如果训练集指标持续走高而验证集长期停滞甚至下降,说明开始过拟合了。
训练结束后,runs/detect/train 目录下会生成 best.pt 和 last.pt。best.pt 是验证集上表现最好的权重,last.pt 是最后一轮的权重。实际应使用 best.pt,这是模型整体性能最好的快照。
4.2 验证结果的关键指标
跑完训练后运行验证命令:
yolo task=detect mode=val model=runs/detect/train/weights/best.pt data=visdrone.yaml你会看到一份完整的结果报告,重点关注 mAP50 和 mAP50-95。按照我实测的经验,yolov8s + imgsz=640 + 100 epochs 在 VisDrone 上的合理范围大约为 mAP50 在 28~34 之间,mAP50-95 在 14~18 之间。如果你得到的是这个区间,说明 baseline 就已经正常跑通。如果远低于此,八成是数据路径或者类别映射的问题;如果明显高于此,反而要检查是不是验证集划分出了误差。
不同类别之间的差异也很大。car 因为数量多、目标相对大,mAP50 往往最高;pedestrian 和 people 目标极小,精度会差一个档次;bicycle 和 motor 经常与行人混在一起,互相遮挡严重,也是偏低的重灾区。这种“偏科”现象在 VisDrone 上非常典型。
4.3 可视化预测与常见翻车
验证通过后,用 best.pt 跑几张图看看:
yolo task=detect mode=predict model=runs/detect/train/weights/best.pt source=/path/to/test/images/输出图片上会画出预测框、类别标签和置信度。这一步可以看到模型的实际表现,也是一种最直观的 sanity check。
这里最容易出现两类翻车情况:
- 预测框在图上坐标错位:通常是验证时读取图片尺寸与实际不一致导致的。VisDrone 的 JPEG 有些带 EXIF 信息,某些库读取时会自动旋转,导致坐标偏移。解决方法是统一用同一种图片读取库,并关闭自动旋转。
- 所有目标都找不到框:大概率是置信度阈值设得过高,预测时可以降低 conf 参数,比如 conf=0.05。VisDrone 上小目标很多,模型虽然检测到了但置信度不高,默认的 0.25 阈值会过滤掉大量有效预测。
5. 在 Baseline 基础上的调优方向参考
跑通 baseline 只是第一步,实际应用场景下 VisDrone 的检测精度往往还需要进一步提高。给大家几个我实测过的调优方向,按性价比排序:
5.1 提高输入分辨率
从 imgsz=640 提升到 1280 是对 VisDrone 最直接的提升手段。分辨率翻倍意味着小目标由几个像素变成十几个像素,特征更明显。代价是训练时间成倍增长,显存占用也大幅上升。建议在硬件允许的情况下,优先做这一步。
5.2 使用拼图推理
切图推理的思路是把大图切成若干有重叠的 patch 分别检测,再把结果拼接起来。VisDrone 很多原图是 2000x1500 以上,直接缩放会丢失大量小目标细节。切图后每个 patch 里的目标相对变大,检测精度会明显提升。具体实现方式可以参考一些常见的目标检测 patching 方案,处理好重叠区域的 NMS 合并即可。
5.3 利用遮挡标注做数据筛选
VisDrone 标注中的第 7 和第 8 个字段分别表示目标的截断程度和遮挡程度。训练时把严重遮挡的目标过滤掉,往往能提升整体模型在清晰目标上的表现;保留它们则会让模型学得更鲁棒但单类精度略低。实际策略取决于你的下游任务:如果是做车流统计,宁可丢掉部分遮挡目标也要保证精度;如果是做全场景感知,建议保留。
5.4 更大模型与多尺度训练
yolov8m 在 VisDrone 上的 mAP50 通常比 s 高 3~5 个点,代价是训练时间约增加 60%。多尺度训练指的是在训练时随机把输入图像缩放到不同尺寸,增强模型对不同目标尺度的适应能力。ultralytics 里这其实是默认开启的特性,但如果你自己改了数据加载器,就要确认一下。
5.5 小目标专用改进
在 baseline 跑通之后,你可以关注一些针对小目标检测的思路:增加 P2 检测头、在 feature pyramid 中融合更高分辨率层、自定义针对小目标的损失权重等。但这些都属于进阶改动,会明显增加代码复杂度,建议在确认普通调参已经榨不出多少性能之后再做。
跑这个项目时我个人最大的体会是:VisDrone 这类航拍数据集,真正的瓶颈往往不在模型结构,而在数据预处理和训练配置的细节上。很多人第一次跑,花掉大半个晚上在调环境、搞数据转换,最后模型本身反而没留多少调试时间。如果你正在做这个数据集,建议先把转换脚本和验证可视化做好,确保训练前数据就是对的,然后用一套保守的参数把 baseline 完整跑出来,再逐步优化。我后来把数据转换流程稳定下来之后,每次换模型、换参数,都是半小时内就能出结果,省下来的时间全用在了正经的模型改进上。