第一次在 VisDrone 上跑通 YOLOv8 的时候我盯着验证集上的 mAP 数字愣了半天总觉得代码哪里写错了。后来才反应过来不是代码的问题而是无人机视角下的目标检测和你平时在 COCO 上玩的那些根本是两个世界。VisDrone 数据集里的目标又小又密一辆车可能只占几十个像素一个行人也就是一个小点常规的检测思路在这上面很容易失灵。这篇文章就是记录我怎么从零开始把 YOLOv8 在 VisDrone 上完整跑通一个 baseline 的全过程——包括数据集怎么准备、标注怎么转换、训练参数怎么调、哪些坑必须避开以及跑通之后真实性能大概是什么水平。1. 无人机视角下的检测难题为什么 VisDrone 值得当第一个 baseline1.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这表示一个目标的位置信息左上角坐标 x169y114宽 79高 251score 为 1表示确定存在的目标类别编号为 4对应 cartruncation 为 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, encodingutf-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, encodingutf-8) as f: f.write(\n.join(yolo_lines))这个脚本里有几个细节值得说类别编号减 1VisDrone 官方标注中类别是从 1 开始的0 是忽略类别而 YOLO 格式要求从 0 开始。如果忘了减一所有类别都会整体平移一位模型训练时虽然能收敛但验证结果会完全对不上。过滤 score 为 0 的目标VisDrone 标注里有些目标被标记为 score0表示严重模糊或不确定建议不要拿来训练。归一化不能漏除图像宽高这点看起来是常识但实际操作中很容易图省事直接硬编码一个默认值。VisDrone 的图像尺寸虽然大多数是 2000x1500 或者 960x540 附近但并不是完全统一必须用每张图的真实尺寸计算。2.3 分割训练/验证集以及转换后校验VisDrone 官方划分已经比较合理通常我们把官方 train 目录下的图像全部用于训练官方 val 目录用于验证。写转换脚本时建议把 train 和 val 分别跑一遍生成对应的 labels 目录。转换完成之后千万别直接开训。先做一次可视化校验随机挑几张图把 YOLO 格式的标注画回去确认框和类别都对得上。这个步骤看似多此一举却总能在第一时间暴露出脚本里的低级错误。比如我见过有人把所有类别的框都画重叠在一起原因就是归一化坐标写成了左上角坐标。3. 环境安装与训练配置的实战细节3.1 ultralytics 安装与版本验证训练 YOLOv8 最省事的方式就是直接安装 ultralytics 开源库。它把数据加载、模型构建、训练过程、验证输出全封装好了对大部分场景已经完全够用。conda create -n yolov8 python3.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 taskdetect modetrain modelyolov8s.pt datavisdrone.yaml \ epochs100 imgsz640 batch16 device0说一下几个超参数的实际选择逻辑imgsz640官方预训练权重是在 640 分辨率上训练的直接用 640 起步不会引入额外的适配成本。但 VisDrone 目标非常小640 分辨率会丢失大量细节。baseline 阶段先用 640 把整个流程跑通后面再逐步提升到 1280 甚至 1536。batch16取决于显存大小。如果显存是 24GB 级别batch16 通常没问题如果只有 12GB建议降到 8。显存不够时优先减小 batch而不是降低分辨率因为分辨率对 VisDrone 的影响远大于 batch 大小。epochs100VisDrone 训练集有 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 taskdetect modeval modelruns/detect/train/weights/best.pt datavisdrone.yaml你会看到一份完整的结果报告重点关注 mAP50 和 mAP50-95。按照我实测的经验yolov8s imgsz640 100 epochs 在 VisDrone 上的合理范围大约为 mAP50 在 28~34 之间mAP50-95 在 14~18 之间。如果你得到的是这个区间说明 baseline 就已经正常跑通。如果远低于此八成是数据路径或者类别映射的问题如果明显高于此反而要检查是不是验证集划分出了误差。不同类别之间的差异也很大。car 因为数量多、目标相对大mAP50 往往最高pedestrian 和 people 目标极小精度会差一个档次bicycle 和 motor 经常与行人混在一起互相遮挡严重也是偏低的重灾区。这种“偏科”现象在 VisDrone 上非常典型。4.3 可视化预测与常见翻车验证通过后用 best.pt 跑几张图看看yolo taskdetect modepredict modelruns/detect/train/weights/best.pt source/path/to/test/images/输出图片上会画出预测框、类别标签和置信度。这一步可以看到模型的实际表现也是一种最直观的 sanity check。这里最容易出现两类翻车情况预测框在图上坐标错位通常是验证时读取图片尺寸与实际不一致导致的。VisDrone 的 JPEG 有些带 EXIF 信息某些库读取时会自动旋转导致坐标偏移。解决方法是统一用同一种图片读取库并关闭自动旋转。所有目标都找不到框大概率是置信度阈值设得过高预测时可以降低 conf 参数比如 conf0.05。VisDrone 上小目标很多模型虽然检测到了但置信度不高默认的 0.25 阈值会过滤掉大量有效预测。5. 在 Baseline 基础上的调优方向参考跑通 baseline 只是第一步实际应用场景下 VisDrone 的检测精度往往还需要进一步提高。给大家几个我实测过的调优方向按性价比排序5.1 提高输入分辨率从 imgsz640 提升到 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 完整跑出来再逐步优化。我后来把数据转换流程稳定下来之后每次换模型、换参数都是半小时内就能出结果省下来的时间全用在了正经的模型改进上。