
简介无人机俯视视角下的车辆和行人目标检测数据集即Yolov5无人机俯视数据集dataset2主要面向需要训练无人机航拍检测模型的开发者与研究者提供一千余张已标注的俯拍图像省去自行采集和标注的繁琐流程并针对小目标遮挡、密集场景等无人机视角常见问题给出可直接用于训练的标注格式。压缩包内共两千个文件包含一百一十二张JPG图片、一千八百八十七个TXT标注文件以及一份data.yaml配置文件整体大小约九百六十MB。数据集已经划分好训练、验证、测试三个子集目录结构清晰配置文件中明确指定car与person两类目标及其路径因此使用YOLOv5、YOLOv7或YOLOv8时可直接读取配置启动训练无需额外调整。目前已有1325人学习下载适合智慧交通、安防巡检、农业植保等无人机应用场景中的目标检测模型快速迭代与评估。1. 先说结论VisDrone转YOLOv5卡住人的从来不是训练本身下载好这份VisDrone无人机俯视视角的车辆和行人目标检测数据集把它交给YOLOv5训练十个人里有八个会在第一步就懵住标注不是YOLO要的格式类别编号不是从0开始图片尺寸还大到不能直接扔进训练循环。这个数据集解决的是「无人机往下看时车辆和行人又小又密、相互遮挡」这一整类问题目前做航拍目标检测、智慧城市、低空安防方向的从业者基本都绕不开它。这篇笔记把VisDrone从解压、格式转换、数据配置到训练踩坑一次性讲完适合准备拿无人机视角数据训练YOLOv5、但不想在预处理上反复翻车的人。2. VisDrone是什么为什么无人机俯视小目标检测都用它当试金石2.1 数据规模与类别分布VisDrone是天津大学等团队开源的无人机视觉数据集常见的VisDrone2019-DET版本覆盖了不同城市、不同天气、不同光照条件下的无人机俯拍影像。整份数据集包含大量视频序列和静态图像训练集六千余张、验证集五百余张另外还有一千多张不带公开标注的测试图——那部分要提交到官方评测服务器才能拿到分数本地做验证时基本用不到。类别设计上它做得很细一共十类目标加一个忽略区域YOLO索引类别名常见出现场景0pedestrian站立或行走的行人1people坐姿、蹲姿或成团难以区分的人2bicycle骑行中的自行车3car小型轿车4van面包车、厢式货车5truck卡车6tricycle三轮车7awning-tricycle带篷三轮车8bus公交车9motor摩托车、电动车注意它的pedestrian和people是分开算的中国人习惯里都叫行人但VisDrone把「站着的人」和「坐着/蹲着/密集人群」拆成两类。转换标签时如果强行合并成一类mAP数字会好看一点但和官方评测口径就对不上了如果要提交VisDrone评测必须保留原始类别映射。从图像特征上看它介乎遥感目标检测和街景目标检测之间视角是高空俯视但目标尺度比纯遥感图大比车载相机看到的又小一个量级。官方公布的标注框数量在百万级以上因为一张图里可能同时出现几十甚至上百个目标密集程度远超常规自动驾驶数据集。这也正是它被当成衡量无人机检测算法能力的标准考卷的原因——类别齐全、场景真实、遮挡和尺度变化都极端。2.2 俯视视角带来的三个标注特征第一个特征是小目标占比极高。在常见的1920×1080或接近2000×1500的高空图像里一辆car往往只占几十个像素宽人群里的pedestrian更是可能只有十几个像素。这个特点直接决定了后面训练时的输入尺寸选择用YOLOv5默认的640输入去训练小目标在降采样过程中被压缩成几个像素网络根本学不到有效纹理。第二个特征是目标排列密集且方向任意。车载数据集里车辆基本是侧视或前后视角VisDrone里车是从正上方看的车头朝向五花八门加上彼此紧挨着停检测框之间大量重叠。这就导致NMS阶段很容易把两个挨着的目标并成一个或者把一辆长车拆成两截。第三个特征是标注里带着大量「脏数据」标记。VisDrone的标注txt里除了十类目标还有专门的ignore区域、被截断目标、被遮挡目标。这些不是给模型学的是给评测时兜底用的。训练前如果不把它们过滤掉模型会去拟合那些标注得含糊不清的目标验证时的AP数值也会被严重干扰。2.3 为什么是YOLOv5而不是别的检测框架当前目标检测领域可选项不少YOLOv8、YOLOv11、MMDetection、mmrotate这些都有人用但vis-drone这类老牌数据集在社区里流传最广的配套方案还是YOLOv5。原因很实际YOLOv5训练资源要求低、文档多、踩坑答案好搜而且它对自定义数据集的目录结构和标注格式要求非常明确适合快速跑通基线。我用YOLOv5跑VisDrone还有一个理由它的autoanchor机制对VisDrone这种目标尺度分布极端的数据集特别友好。训练时YOLOv5会根据标签里的真实框尺寸重新聚类anchor不需要手动去量数据里的目标大小。相比之下部分更新框架虽然网络结构更好但要在部署阶段额外处理anchor配置问题。所以这篇笔记后面的所有配置和命令都以YOLOv5为主线把VisDrone先跑通再考虑换更新框架不迟。3. VisDrone转YOLOv5格式目录结构、txt字段与转换脚本3.1 解压后的目录结构与文件映射这份数据集解压后训练和验证的图片、标注是分开的目录结构大致如下VisDrone2019-DET/ ├── VisDrone2019-DET-train/ │ ├── images/ │ │ ├── 0000001_00000_d_0000001.jpg │ │ ├── 0000001_00000_d_0000002.jpg │ │ └── ... │ └── annotations/ │ ├── 0000001_00000_d_0000001.txt │ ├── 0000001_00000_d_0000002.txt │ └── ... ├── VisDrone2019-DET-val/ │ ├── images/ │ └── annotations/ └── VisDrone2019-DET-test-dev/ └── images/train和val目录下各有一份images和annotations同名图片对应同名txt标注文件。test-dev只有images不提供标注因为它本来就是官方评测集。如果你在别处下载到的是只有图片没有标注的版本那基本是test部分训练用不了下载前先确认清楚。图片文件名统一是类似0000001_00000_d_0000001.jpg这种由视频序列号加帧号组成的编号标注文件名和图片名完全一致。转换脚本的核心工作就是遍历annotations目录把每个txt从VisDrone格式转成YOLO格式并写到另一个labels目录里。YOLOv5要求的标注目录和图片目录未必同级只要yaml文件里分别指定路径即可具体映射写在3.3节脚本里。3.2 标注txt的逐字段说明VisDrone的每行标注是一段逗号分隔的纯文本一共8个字段对应一个目标框bbox_left,bbox_top,bbox_width,bbox_height,score,category,truncation,occlusion和VOC的xml或者Labelme的json完全不同这里没有目标类别名只有一个category数字。每个字段的含义如下字段说明对训练的影响bbox_left目标框左上角x坐标像素值需要除以图像宽度做归一化bbox_top目标框左上角y坐标像素值需要除以图像高度做归一化bbox_width目标框宽度像素值可能是0要丢弃bbox_height目标框高度像素值可能是0要丢弃score置信度分数评测用训练时建议过滤掉非正值category目标类别编号从1开始必须重映射到0-9truncation目标被图像边界截断程度截断严重的目标建议扔掉occlusion目标被遮挡程度全遮挡目标建议扔掉两个容易踩的点一是category不是从0开始的直接拿它当YOLO类别索引会多出一个空类二是坐标是绝对像素值YOLOv5的标签要求是归一化后的中心点坐标cx, cy, w, h比例值在0到1之间。我最早转换时犯过把左上加宽高当YOLO坐标直接用的错训练出来的loss直接不收敛排查了好几个小时。3.3 转换脚本绝对坐标转YOLO归一化坐标下面这个脚本是我一直在用的转换方案把VisDrone的annotations批量转为YOLOv5能直接读取的labels目录import os from pathlib import Path # VisDrone官方类别编号 - YOLO索引 CLASS_MAP { 2: 0, # pedestrian 3: 1, # people 4: 2, # bicycle 5: 3, # car 6: 4, # van 7: 5, # truck 8: 6, # tricycle 9: 7, # awning-tricycle 10: 8, # bus 11: 9, # motor } def convert_one(src_txt: Path, dst_txt: Path, img_w: int, img_h: int): out_lines [] with open(src_txt, r, encodingutf-8) as f: for line in f: parts line.strip().split(,) if len(parts) 8: continue x, y, w, h float(parts[0]), float(parts[1]), \ float(parts[2]), float(parts[3]) score, cat float(parts[4]), int(parts[5]) if cat not in CLASS_MAP: continue # 跳过ignore区域和未知类别 if score 1: continue # 跳过评估时被忽略的目标 if w 0 or h 0: continue # 无效框 # 归一化到0-1 cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h # 防止边框超出图像范围 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) nw min(max(nw, 0.0), 1.0) nh min(max(nh, 0.0), 1.0) out_lines.append( f{CLASS_MAP[cat]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n ) with open(dst_txt, w, encodingutf-8) as f: f.writelines(out_lines) def convert_dataset(ann_dir: Path, img_dir: Path, out_dir: Path): out_dir.mkdir(parentsTrue, exist_okTrue) for ann_path in sorted(ann_dir.glob(*.txt)): img_path img_dir / (ann_path.stem .jpg) if not img_path.exists(): print(f跳过: {ann_path.name} 无对应图片) continue # 读取图片尺寸 from PIL import Image with Image.open(img_path) as img: w, h img.size convert_one(ann_path, out_dir / ann_path.name, w, h) if __name__ __main__: convert_dataset( ann_dirPath(VisDrone2019-DET-train/annotations), img_dirPath(VisDrone2019-DET-train/images), out_dirPath(VisDrone2019-DET-train/labels), )逻辑上分三段第一段定义类别映射把VisDrone的官方编号落到0-9的连续索引第二段对每行做字段解析和过滤只保留有效的、非忽略的、尺寸合理的标注第三段遍历整个目录逐张读取图片宽高用于归一化。参数说明img_w和img_h必须是原图尺寸不是resize之后的尺寸因为标注坐标是相对原图的score 1的过滤条件可以按需调整如果你发现转换后某类目标数量明显比官方统计少很多先检查这一条输出txt每行格式是「类别 cx cy w h」空格分隔这是YOLOv5在data.yaml里的labels目录中直接读取的标准格式。3.4 转换时最容易把mAP做假的几个细节第一个细节是ignore区域必须过滤。VisDrone里标注为ignore的区域包含大量模糊目标、极小目标和背景干扰物它们的category编号不属于十类目标直接跳过即可。如果不过滤这些区域会被当成背景负样本或者错误正样本送进训练导致模型在真实场景里把树影和房顶角当成车。第二个细节是同一个视频序列的连续帧高度相似。VisDrone的图片来自视频抽帧train里相邻帧几乎一样。直接划分train/val时如果验证集和训练集来自同一段视频验证分数会虚高——模型可能只是记住了场景而不是学会了检测。我习惯在转换后按照文件名里的视频序列号单独抽出一段视频的帧做验证而不是直接拿官方val的结果当真。第三个细节是图片尺寸读取不能出错。VisDrone整套图片尺寸并不完全统一虽然多数在2000×1500上下但也有1920×1080的。转换脚本里逐张读取宽高是对的千万别图省事写死一个统一尺寸。第四个细节是转换后的labels目录要放在合适的位置。YOLOv5官方默认在coco128的目录结构里把labels和images放在同一级但你的数据集可以随意放。只要data.yaml里train、val、nc、names写对YOLOv5不关心目录具体叫什么名字。4. 跑通YOLOv5训练visdrone.yaml配置、anchors重算与超参数取舍4.1 写visdrone.yaml图片目录、txt标签目录、十个目标类别转换完成后在YOLOv5目录下新建一个data/visdrone.yaml用来告诉训练脚本数据集在哪、有几个类别、类别名是什么# VisDrone无人机视角车辆与行人检测 path: /your/abs/path/VisDrone2019-DET # 数据集根目录 train: VisDrone2019-DET-train/images # 训练图片目录 val: VisDrone2019-DET-val/images # 验证图片目录 nc: 10 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字段填的是图片目录YOLOv5会自动把路径里的images替换为labels去找对应的标注文件所以labels目录必须和images目录同级并且命名是labels。我在3.3节脚本里输出的就是这样的结构。写完后先别急着训练跑一条命令验证数据配置是否正确python train.py --data data/visdrone.yaml --weights yolov5s.pt --img 640 --epochs 1如果配置有问题这条命令会在几分钟内报错而不是让你等几个小时后才发现标签路径错了。命令里的--epochs 1只是做冒烟测试确认能正常加载数据后就停掉。4.2 anchors必须重算小目标场景不要自信用coco预置框YOLOv5默认带的锚框是为COCO数据集设计的COCO里的目标在整幅图中占比通常比较大而VisDrone里大量目标只有十几个像素宽。默认anchors对VisDrone来说普遍偏大会导致小目标的定位精度明显下降。YOLOv5的autoanchor机制会在训练开始时重新评估数据集的标签框尺寸如果和当前anchor差异超过阈值就会自动用k-means聚类新anchor。要确认这一点真的生效了看训练前几行日志Analyzing anchors... anchors/target 4.32, Best Possible Recall (BPR) 0.9877如果BPR接近1.0说明当前anchor组合已经匹配数据分布如果BPR很低YOLOv5会花几分钟重新聚类。我在VisDrone上遇到过autoanchor因为图片尺寸太大导致聚类耗时很长的情况这时候耐心等它跑完别按CtrlC。聚类完成后锚框会明显小于COCO默认值这是正常现象说明它正在适应小目标分布。有同行会手动把anchor写进yaml但我觉得这个操作在YOLOv5里不是必须的。autoanchor基于真实标签框聚类结果比任何人拍脑袋写的都可靠所以我的建议是不要用--noautoanchor参数关闭它也不要在yaml里手写anchor值让autoanchor自己跑完。4.3 训练命令、imgsz选择与多尺度训练数据配置验证通过后正式训练我一般这样启动python train.py \ --data data/visdrone.yaml \ --weights yolov5s.pt \ --img 1280 \ --batch 8 \ --epochs 120 \ --workers 8 \ --cache ram \ --multi-scale这里的--img 1280是关键。VisDrone原图接近2000×1500用640训练等于把所有目标缩小三倍再学小目标信息基本丢光了。用1280训练模型能看到更多细节但显存消耗也成倍增加batch需要降到个位数。如果你的显卡是8G显存建议--img 960起步如果是24G显存1280可以配batch 16。--multi-scale会在训练过程中随机把输入图片缩放到0.5到1.5倍之间等于免费做数据增强对VisDrone这种尺度变化大的数据集几乎必开。副作用是每个batch的图片尺寸都不一样训练速度会慢一些。--cache ram把图片预加载进内存能明显加快数据读取。VisDrone单张图体积大如果内存不到32G建议去掉这个参数否则容易吃满内存导致训练进程被系统杀掉。损失函数和优化器方面我基本沿用YOLOv5默认超参数只把epochs加到120左右因为VisDrone类别多、小目标多收敛比COCO慢100轮以下常常还没稳定就停了。5. VisDrone训练避坑4个常见翻车现场与排查方法5.1 训练mAP虚高一上无人机视频就漏检现象是训练日志里mAP0.5涨到0.6以上但拿一段没见过的无人机视频去测车辆密集区域漏检严重连car这种大目标都频繁漏掉。原因大概率出在数据划分上。VisDrone训练集和验证集来自相同视频序列的相邻帧画面高度相似模型靠记忆周边像素就能在验证集上拿高分遇到真正没见过的场景就现出原形。解决方法是按照文件名里的视频序列号重新切分数据。我一般把train目录里的图片按序列号分组随机抽两个完整序列作为新的验证集其余序列作为训练集。这样验证集和训练集完全没有同一场景的帧评估出来的数字才有参考价值。操作上不需要改代码只需在visdrone.yaml的val字段指向一个新的图片目录。5.2 loss先降后升训练到一半出现震荡现象是前30个epoch的box_loss持续下降训练到40轮左右突然反弹之后呈锯齿状震荡最终收敛效果反而不如早停时的权重。原因通常有两个一是batch太小加上--multi-scale导致梯度不稳定VisDrone的图片在0.5倍缩放时目标只有几个像素模型在这些极端尺度上产生了冲突梯度二是学习率调度不对YOLOv5默认cos LR在epoch数设置不当时会在后半段重新拉高学习率。解决方法是先固定--img关闭multi-scale把batch调大到显存能承受的上限确认loss曲线平稳后再开multi-scale。学习率方面我把--cos-lr参数去掉改用默认的线性下降epochs从120减到80震荡基本消失。如果你发现损失从0.06左右一路涨到0.1以上先看是不是数据本身有问题——检查一下labels里有没有归一化后大于1的坐标值转换脚本里的边界裁剪逻辑能挡住一部分但如果你改过脚本这是第一个要排查的点。5.3 bus、awning-tricycle这类小样本类别基本学不会现象是mAP总数值还行但看每个类别的APpedestrian和car都很高bus、awning-tricycle的AP不到0.2检测结果里几乎见不到这些类别。原因是VisDrone本身类别极不均衡。城市道路上car、pedestrian数量以万计bus和带篷三轮车可能只有几百个样本模型在训练时天然偏向多数类少数类学不到有效特征。解决的常规思路是给少数类加权在YOLOv5里可以通过修改data.yaml暂时做不到类别加权我一般先做简单的过采样把含有bus、awning-tricycle这类少数类的图片在训练目录里重复几份相当于提高它们被采样的概率。更省事的方法是直接用--class-weights参数YOLOv5会根据训练集各类别数量自动计算权重让少数类在loss里占更高比重。另外如果最终目标是部署到实际项目里建议优先确认项目需求里到底需不需要少数类如果不需要直接在转换脚本里把这类目标过滤掉既减小训练压力又能提升整体mAP。5.4 验证集来自同一批视频评估结果不可信现象是训练日志里验证集mAP一路走高感觉已经可以上线了但用VisDrone官方test-dev或者其他无人机视频测试效果明显更差。原因前面提过官方val与train的序列重叠是VisDrone数据集的结构性特点评测协议默认就是同一个数据分布下的表现。这个问题本质上不是bug而是数据集的评测约定但如果你做的是落地项目而不是学术benchmark就必须按真实场景重新评估。解决办法是用一段完全没参与训练的无人机视频逐帧跑模型做定性评估再手工统计漏检和误检。更进一步的做法是把原始视频抽帧后用标注工具标一小段作为独立测试集。虽然标起来费时间但这部分标注能让你明确知道模型在真实场景里到底能打成什么样比看训练日志里的mAP有意义得多。6. 用大图切块推理验证VisDrone模型别让mAP替你背锅6.1 先做一个能切块推理的验证脚本在VisDrone这种接近2000像素的大图上训练时用1280输入推理时如果用原图直接预测目标太小会漏检。常规做法是切块推理把大图切成若干有重叠的小块分别预测再把框映射回原图坐标用NMS合并重叠结果。def slide_infer(model, img, patch_size640, overlap0.2): img: BGR ndarray, shape (H, W, 3) 返回: 原图坐标系下的检测结果 [x1, y1, x2, y2, conf, cls] h, w img.shape[:2] stride int(patch_size * (1 - overlap)) boxes [] for y in range(0, h, stride): for x in range(0, w, stride): y2 min(y patch_size, h) x2 min(x patch_size, w) # 保证小块和训练输入尺寸一致不足处补灰边 patch img[y:y2, x:x2] pad_h, pad_w patch_size - patch.shape[0], patch_size - patch.shape[1] if pad_h 0 or pad_w 0: patch cv2.copyMakeBorder( patch, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value(114, 114, 114) ) pred model(patch)[0] for det in pred: x1, y1, x2, y2, conf, cls det[:6] # 把patch坐标还原到原图注意去掉灰边偏移 x1 min(x1 x, w) y1 min(y1 y, h) x2 min(x2 x, w) y2 min(y2 y, h) boxes.append([x1, y1, x2, y2, conf, cls]) # 合并重叠框阈值按项目需求调整 keep non_max_suppression(np.array(boxes), conf_thres0.25, iou_thres0.5) return keep这段代码的核心思路是把大图切成固定patch_size的小块保证每个小块落在模型最擅长处理的尺度范围内。overlap取0.2到0.3之间比较合适太大会让同一个目标被预测几十次太小则目标正好被切在两块边界上导致框不完整。6.2 用PR曲线和confusion matrix确认真的学会了切块推理脚本跑通后真正验证模型可用性的是PR曲线和confusion matrix不是mAP总数字。训练日志里的mAP0.5是所有类别平均值一个小类别拉胯可能把整体数字拖低也可能被多数类的高AP掩盖单看它看不出问题。我现在的习惯是每次训练结束后先跑一遍验证集并输出PR曲线和confusion matrixPR曲线里某个类别的曲线如果在召回率0.6附近急剧掉头说明该类别大量目标被漏检confusion matrix里如果pedestrian和people互相混淆严重说明这两个类别在俯视视角下特征确实太接近需要增加样本或合并类别。确认这两张图都符合预期后再对一段没见过的无人机视频做定性测试肉眼确认车辆和行人的框稳定、不抖动才算真正验收完一个模型。这套流程走下来VisDrone训练翻车的概率就低很多了希望帮到你。本文还有配套的精品资源点击获取