无人机视角目标检测数据集drone-data-1与YOLOv8训练实战指南
简介目标检测是计算机视觉中的核心任务其关键在于数据与场景的匹配。在无人机航拍场景下俯拍视角导致目标尺度小、密度高、光照复杂传统平视数据集训练出的模型难以直接应用。本文将围绕一份经过清洗的无人机目标检测数据集drone-data-1从数据解压结构、VOC格式转YOLO格式的方法到YOLOv8训练参数调优、小目标切片推理、类别不平衡重采样以及标签清洗等全流程进行梳理为从事无人机视觉的开发者提供可直接复用的实践参考。 做了这么多年计算机视觉我越来越觉得“目标检测”这件事真正磨人的不是算法本身而是数据和场景之间的错位。最近我在归档自己手头的一批无人机数据按批次整理成了系列文件drone-data-1.zip 就是这个系列的第一份。这份数据集不是我随手从哪个网站拉下来就完事的而是把无人机视角下真正会遇到的目标类型、拍摄高度、光照条件、遮挡情况都过了一遍最后筛出来的一个可以直接拿去训练目标检测模型的干净子集。很多朋友拿到数据集的第一反应是赶紧丢进 YOLO 里跑一跑但无人机视角的数据和普通监控摄像头拍出来的东西完全不是一回事。这份 drone-data-1 我前后验证了不少轮中间踩过小目标漏检、类别不平衡、标注边界不干净这些坑也有了一些自己的处理套路。这篇文章就把这份数据集从结构分析、格式转换到 YOLOv8 训练调优的完整过程记录下来给正准备做无人机目标检测的朋友一份能直接照着操作的参考。1. 无人机视角为什么需要专属数据集1.1 俯拍和平视是两种完全不同的视觉任务常规的目标检测数据集比如 COCO 或者某些监控场景数据集绝大多数图片是平视视角拍的目标以侧面、正面为主行人站立、车辆侧面停靠或行驶外观轮廓相对固定。你拿这些预训练权重去推理无人机画面效果往往非常差原因很简单无人机绝大多数时候是俯拍或者大角度斜拍。俯拍视角下一个人从“站立的长条形”变成了“头顶的一小团”一辆车从“侧面带轮子的矩形”变成了“车顶的色块加阴影”这种外观变化直接改变了模型需要学习的特征分布。我在 drone-data-1 里专门统计过图片里绝大多数目标在画面中的占比小于 5%有的甚至只有十几像素宽。这种尺度特征决定了你不能拿训练普通目标检测模型的思路来套无人机场景必须有针对性的数据来源和训练策略。1.2 无人机数据的典型难点这份数据里全都有我整理 drone-data-1 的时候刻意让它覆盖了无人机目标检测最难的那几种情况高密度目标停车场车辆、密集人群这类画面单张图里可能有上百个目标互相遮挡严重。多尺度变化同一组数据里既有一两百像素的大目标也有仅有 7-8 像素的小目标模型要同时兼顾两者。视角旋转无人机转向时同一个目标在画面中的朝向会变化车辆可能从任意角度出现。光照差异正午强光、傍晚逆光、树荫下的低照度目标都会影响检测稳定性。这些不是“锦上添花”的难点而是无人机实际作业中每天都要面对的客观情况。如果你只拿几张街景图训出来的模型去飞到现场一测漏检率会非常感人。1.3 和公开数据集相比这份数据更适合当“第一份训练素材”公开的无人机数据集里VisDrone、UAVDT、AeroScapes 都是经典选择但我自己的体验是它们或者规模太大下载处理成本高或者标注格式偏科研向对刚入门的人来说并不友好。drone-data-1 这个系列的目标是做成“干净、可快速上手、别让数据预处理劝退人”的子集。这份数据总体上保持了中等规模——单批次图片数量和标注目标数足够训练出一个能跑的模型又不至于让你在准备阶段花掉大量时间去清洗数据。我用下来最大的感受是它很适合做以下几件事验证无人机目标检测的基础流程做迁移学习看看预训练模型在俯拍场景下掉多少点测试小目标增强策略切图、放大输入分辨率、SAHI 切片推理等对结果的影响。2. 解压之后别急着训练先看清楚这份数据集的内容和标注2.1 drone-data-1.zip 的目录结构解压之后第一件事不是开训练脚本而是把数据集的目录结构完整看一遍。我处理的这份 zip 解压后大概是这样的drone-data-1/ ├── images/ │ ├── train/ # 训练集图片 │ ├── val/ # 验证集图片 │ └── test/ # 测试集图片 ├── annotations/ │ ├── train/ # VOC 格式的 XML 标注 │ ├── val/ │ └── test/ ├── classes.txt # 类别列表 └── README.txt # 数据和统计说明我第一次拿到类似数据集的时候踩过坑直接去翻 images 文件夹没注意 annotations 目录是分开的结果还以为数据集没有标注。所以先看结构、读 README比直接写代码更重要。2.2 类别构成和标注数量这份数据集是面向常见地面目标的我这次统计到的类别一共 6 类类别实例数量占比常见出现场景person约 2800053%人行道、广场、施工现场car约 1450028%道路、停车场、小区bicycle约 42008%非机动车道、小区motorcycle约 26005%路口、非机动车道bus约 18003%主干道、公交站truck约 15003%主干道、工地周边看到这个分布你应该已经猜到后面会遇到什么问题——类别极其不平衡。person 占了超过一半而 bus、truck 加起来只有 6% 左右。后面训练出来的模型几乎必然对 person 和 car 非常敏感对 bus、truck 等少数类则会出现漏检。这个情况我在第 5 节会细讲这里先留个印象。2.3 图片特性与标签质量检查这一批数据里的图片分辨率跨度比较大以 1920×1080 和 3840×2160 为主少量老旧的航拍抽帧图只有 1280×720。图片基本覆盖了城市道路、住宅小区、公园、工地、停车场这几类典型场景光照条件有晴天、阴天、黄昏也有少量逆光和树荫遮挡的图。标签方面我写了个小脚本检查常见问题主要是三类坐标越界、类别名写错、XML 里出现没有 object 的空标注。检查下来发现小部分 XML 存在坐标超出图片宽高的情况这在后面转成 YOLO 格式时必须处理否则会直接把训练搞崩。提醒一下不要默认网上拿到/别人整理的数据集就是完全干净的。跑一个快速体检脚本只需要几十行代码但能省下后面调试时几个小时的时间。3. 把 VOC 格式标注统一成 YOLO 格式转换脚本和划分逻辑3.1 为什么非转成 YOLO 的 txt 不可drone-data-1 的标注是 VOC 风格 XML存的是 xmin、ymin、xmax、ymax 绝对坐标。YOLO 系模型训练时用的是归一化后的中心点坐标和宽高每一行代表一个目标格式是类别id x_center y_center width height所有值都是相对于图片宽高的比例范围在 0 到 1。这样做的目的是让模型不依赖图片的绝对尺寸不管输入是 1920 还是 640归一化后都在同一个坐标系下表达。转换这个动作本身不难但从 XML 转 txt 的过程中有一个非常隐蔽的坑有些 XML 里的 xmax、ymax 写的是右下角像素坐标有些版本会写成右上角加宽高的方式。你必须先抽几张图验证一下而不是直接批量跑。3.2 完整的 VOC 转 YOLO 脚本下面这个脚本是我实际处理 drone-data-1 时用的已经加上了坐标越界检查和过滤:import os import xml.etree.ElementTree as ET from pathlib import Path # 根据数据集的 classes.txt 修改 CLASSES [person, car, bicycle, motorcycle, bus, truck] def convert_voc_to_yolo(xml_path, out_txt_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) if size is None: print(f缺少 size 节点: {xml_path}) return False img_w int(size.find(width).text) img_h int(size.find(height).text) if img_w 0 or img_h 0: print(f非法图片尺寸: {xml_path}) return False lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in CLASSES: print(f未知类别 {name}跳过: {xml_path}) continue cls_id CLASSES.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 边界裁剪防止越界坐标 xmin max(0, min(xmin, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) xmax max(0, min(xmax, img_w - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: print(f退化框目标面积为零: {xml_path}) continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h x_center max(0, min(x_center, 1.0)) y_center max(0, min(y_center, 1.0)) width max(0, min(width, 1.0)) height max(0, min(height, 1.0)) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) if lines: Path(out_txt_path).parent.mkdir(parentsTrue, exist_okTrue) Path(out_txt_path).write_text(\n.join(lines), encodingutf-8) return len(lines) 0 # 批量转换 xml_root Path(drone-data-1/annotations) txt_root Path(drone-data-1/labels) for xml_file in xml_root.rglob(*.xml): rel_path xml_file.relative_to(xml_root) out_txt txt_root / rel_path.with_suffix(.txt) ok convert_voc_to_yolo(xml_file, out_txt) if not ok: print(f没有有效目标: {xml_file})转换完以后还要再写一个反向抽查脚本把 txt 里的坐标渲染回图片上看一眼确认框的位置和原标注一致。这一步很多人偷懒不做结果训练出来的模型框的位置总是偏的却找不到原因。3.3 训练集/验证集/测试集的划分逻辑drone-data-1 原始划分如果觉得不够合理可以重新划分。我习惯按 80/10/10 的比例划分但要注意一个原则尽量让同一场景、同一段航线上的图片只出现在一个集合里而不是随机切分。这是因为相邻帧画面高度相似如果训练集和验证集里出现了同一场景的相邻帧验证分数会虚高到真实场景里泛化效果立刻现原形。我在处理这份数据时是按照视频片段或拍摄路线进行分组后划分的虽然麻烦一点但评估结果更可信。划分后的目录结构我整理成 YOLO 训练直接能读的样子drone-data-1-yolo/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── drone_data1.yaml对应的drone_data1.yaml配置path: /data/drone-data-1-yolo train: images/train val: images/val test: images/test names: 0: person 1: car 2: bicycle 3: motorcycle 4: bus 5: truck注意YOLOv8 的names索引必须和 txt 文件里的类别 id 一一对应。这也是新手特别容易搞错的地方类别文件是字母序还是自定义序会直接影响训练语义。4. 用 YOLOv8 在 drone-data-1 上训练从环境到参数一步步来4.1 环境准备和安装我这次用的是 YOLOv8安装非常简单直接通过 pippip install ultralytics但有几个小坑要提醒机器上有多个 Python 环境时建议为项目单独建虚拟环境避免 ultralytics 和 torch 版本冲突。CUDA 版本和 PyTorch 版本必须匹配。我之前在一台机器上装了 CUDA 11.8 的 PyTorch结果机器实际驱动只支持 11.7训练时直接报CUDA error: no kernel image is available。用nvidia-smi看驱动支持的 CUDA 版本再决定装哪个版本的 torch。显存不够时batch size 要调小但不要小到 4 以下否则 BatchNorm 统计不稳定收敛效果差。4.2 训练参数的选择逻辑先看核心训练命令yolo detect train \ datadrone_data1.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ patience15 \ optimizerAdamW \ lr00.001每个参数都不是随便填的我按实际踩坑经验解释一下modelyolov8s.pt工业场景我从 s 起步不是 n。n 虽然快但对小目标的特征提取能力太弱无人机场景里小目标本来就多用小模型会漏得厉害。如果有条件直接上 m精度会再高一点代价是训练和推理更慢。imgsz640这是基础值不是最优值。drone-data-1 里有大量 10 像素以下的目标640 输入会在下采样后把目标特征直接抹掉。我后面测试过 960mAP 提升非常明显但显存占用也会显著增加。后面有一节专门讲这个。epochs100 patience15100 轮足够模型在这个规模的数据集上收敛到平台期。patience 设 15意思是连续 15 轮验证集 loss 不降就提前结束避免无效训练浪费时间。optimizerAdamW默认的 SGD 调起来更麻烦AdamW 对学习率没那么敏感更适合快速迭代。等后面要冲更高精度再换回 SGD 配合理的学习率策略。4.3 训练过程的观察点训练过程中我主要盯三个指标曲线train loss 和 val loss 的差值是否过大判断过拟合程度验证集的 mAP50 是否在持续上升还是已经进入平台期recall 和 precision 的走向是否同步。实际操作下来drone-data-1 上训练到第 40 轮左右 mAP50 就能到 0.6 左右后面进入缓慢提升阶段。如果发现 val loss 开始回升而 train loss 还在下降说明过拟合了这时候应该停止训练而不是继续硬扛。训练完成后用验证集做一次评估yolo detect val \ modelruns/detect/train/weights/best.pt \ datadrone_data1.yaml我第一轮训练出来的结果大概是这样指标数值mAP500.64mAP50-950.37precision0.71recall0.58看起来 mAP50 还行但如果按类别拆开看person 的 recall 很高bus、truck 的 mAP 就惨不忍睹。这就是开头说的类别不平衡问题开始“显灵”了。5. 训练中遇到的三类典型问题小目标、类别失衡、边界框噪声5.1 小目标为什么这么难搞以及我验证过的几种解法无人机目标检测绕不开小目标。YOLOv8 在 640 输入下经过 5 次下采样特征图最小分辨率只有输入的 1/32也就是 20×20。一个在 1920×1080 原图中占 30×30 像素的目标缩放 640 后只剩 10×10 像素再经过下采样到检测头时可能只有 3×3 的激活区域特征基本丢失。我在 drone-data-1 上的实测结果是把 imgsz 从 640 提高到 960mAP50 能提升 3-5 个点主要贡献来自小目标召回率的提高。代价是显存占用明显上升如果你的显卡只有 8G 显存需要配合更小的 batch。另外真正好用的还有切片推理我推荐 SAHI 这个思路推理时把原图切成若干有重叠的 patch每个 patch 单独检测再把结果合并。# 思路示例切图——检测——合并具体实现可以用 SAHI 库 from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model AutoDetectionModel.from_pretrained( model_typeyolov8, model_pathruns/detect/train/weights/best.pt, confidence_threshold0.3, devicecuda ) result get_sliced_prediction( imagetest_images/000001.jpg, detection_modeldetection_model, slice_height512, slice_width512, overlap_height_ratio0.2, overlap_width_ratio0.2 )切片后原本在整图里只有 8 像素的小目标在 patch 里可能达到 30-40 像素检测难度大幅降低。我跑了一批测试图小目标的 recall 从 0.34 提升到了 0.55 左右。当然切图推理的时间成本也翻倍了具体用不用要看你的实时性要求。5.2 类别不平衡一种很实用的重采样策略drone-data-1 里 person 占了一半以上而 bus、truck 加起来才 6%。偏斜的类别分布会让模型倾向于把一切疑似目标预测为 person导致少数类 recall 极低。处理类别不平衡最直接的办法是给少数类更高的采样权重。具体做法是在每个 epoch 构建批次时优先从包含少数类别的图片里抽图。YOLOv8 提供了数据增强和采样相关配置但更通用的做法是自己实现一个采样器。我实际用的策略是这样按图片内包含的类别给每张图打分包含 bus 或 truck 的图权重设为 3包含 motorcycle 或 bicycle 的设为 2只有 person 和 car 的设为 1。每个 epoch 按照权重抽样出固定数量的图片参与训练。这样既保证了总体数据量不变又让少数类在每个 epoch 中都得到足够多的“出场机会”。这个操作在 drone-data-1 上直接带来的改变是bus 类别的 mAP50 从 0.42 提升到了 0.61truck 从 0.35 提升到了 0.55。损失函数层面还可以给少数类加更高的 box loss 权重但采样重排的效果通常更立竿见影。5.3 边界框噪声不得不做的标签清洗前面提到数据里有坐标越界的框如果直接拿去训练会导致模型学到错误的边界回归目标尤其是靠近图片边缘的目标。YOLO 格式中如果归一化后的坐标超出 0-1 范围很多版本的训练代码会直接报错或警告。我的清洗经验分两步第一步用脚本自动清理所有超出边界的框要么裁剪回边界内要么直接删除如果目标大部分已经超出画面。裁剪保留少量信息可能训练时反而有帮助但如果是数值错误比如 xmax xmin直接删掉。第二步人工抽查。我把所有清洗过的标签画回原图随机抽了 10% 的图片人工过目重点检查类别对不对、框的贴合度高不高。发现有人把自行车框标成了摩托车这种错误脚本无法自动识别只能人工看。经验是标注质量的优先级高于标注数量。一批干净的 3000 张图训练效果往往好过混入噪声的 5000 张图。做目标检测项目第一优先级永远是确认数据是“干净可用”的再谈模型和参数。6. 用测试集验证真实效果以及下一步的迭代方向6.1 从指标分数到失败样例指标只能告诉你“好不好”不能告诉你“哪里不好”。我会把训练好的模型在 test 集上跑完整推理然后把预测结果可视化保存下来再人工翻看失败案例。在 drone-data-1 上我重点看了三类失败场景密集小目标广场上的人群距离远、互相遮挡模型只能检出其中 30%-40%漏检集中在小尺寸个体上。逆光目标黄昏时段车辆处于强逆光中轮廓和背景融为一体置信度普遍低于 0.3被阈值过滤掉了。部分遮挡停在树下的车辆上半部分被树冠遮挡模型有时会检测成 bus 或 truck因为露出来的轮廓更接近这两个类别。看失败样例不是“自我批评”而是为了确定下一步该往哪个方向调。如果你发现漏检主要是小目标那就去做切片推理或提高输入分辨率如果你发现误检集中在特定类别上那就要回看标注和类别定义是否合理。6.2 下一步我会做的四件事这一轮在 drone-data-1 上跑通基础流程之后我给自己列了下一步迭代方向实验 imgsz960 和 imgsz1280 的精度/速度权衡。在无人机离线分析场景下推理速度要求相对宽松可以把 imgsz 拉高来换取小目标检测率。引入 SAHI 切片推理并量化收益。已经验证过切片能显著提升小目标 recall下一步会把它整合到完整的检测流程里做一批更大的测试。尝试 yolov8m 和 yolov8l。更大的模型容量对复杂背景下的目标特征提取会有帮助但要评估显存消耗和训练时长。给数据集补充逆光、夜间红外样本。从失败样例看光照是比目标尺度更影响泛化性能的因素后续批次的数据我会刻意加入更多低照度场景。这份 drone-data-1 是我这个系列的第一份数据后续我还会继续整理其他场景和类别组合。每次整理完我都会把结构分析、踩坑记录和处理脚本沉淀成文章方便自己和别人后续直接复用。如果你在转换标注或者训练调参时遇到问题可以对照这篇文章里的流程排查一遍尤其是标记清洗和类别采样这两块大概率能省掉不少试错的时间。本文还有配套的精品资源点击获取
