简介:面向无人机俯视视角下的车辆与行人检测任务,这份数据集已完成YOLO格式标注与目录划分,可直接衔接yolov5、yolov7、yolov8等主流检测框架的训练流程,适合航拍目标识别、智能交通等场景的算法学习与模型调优。压缩包共2000个文件,体量约960MB,以1887个txt标签文件为主,同时包含112张jpg航拍图像和1个data.yaml配置;data.yaml中已定义car、person两个类别,并指定train、valid、test三个子目录的图片路径,省去数据整理与格式转换的环节。已有1325人浏览学习,配套作者博客还提供了检测效果参考,便于使用者对比不同模型在无人机视角下的表现。就绪的目录结构与标签配置可让学习者直接进入训练环节,适合作为目标检测实战项目的数据支撑,也方便在此基础上进行数据增强、模型微调等进阶实验。
1. VisDrone 俯视视角数据集到底是什么:为什么做 YOLOv5 检测绕不开它
做无人机视角下的车辆和行人检测,第一件事往往不是调模型,而是先找一套能真实反映“俯视、小目标、密集遮挡”的数据集。VisDrone 是目前公认的航拍目标检测基准数据之一,而这个 vis-drone-yolov5-dataset-2.zip 的作用,就是把 VisDrone 的原始图片和框标注整理成 YOLOv5 可以直接开始训练的目录形态。适合的人群很直接:需要做无人机车流统计、安防巡检、遥感图像目标检测的工程师和学生,想快速走通 YOLOv5 训练自己的数据集流程,又没精力人工标注几万张图的,都可以从这套数据起步。它解决的核心问题是:让模型在俯视视角下把车辆、行人从密集的小目标中框出来,而不是只在常规视角的公开数据上表现好看。
2. 解包后的目录结构与 YOLOv5 能用的标注格式转换:转换脚本与三个边界坑
2.1 压缩包里有什么:目录划分与文件命名规则
拿到 zip 解压后,常见做法是看到一个主目录,里面按 images 和 labels 分开放,train、val 各自再建子目录,结构大致像下面这样:
visdrone-yolo/ ├── images/ │ ├── train/ │ │ ├── 0000156.jpg │ │ └── ... │ └── val/ ├── labels/ │ ├── train/ │ │ ├── 0000156.txt │ │ └── ... │ └── val/ └── visdrone.yaml图片名和标注 txt 文件名一一对应,这个命名规则是 YOLOv5 的基本要求。如果 zip 里只有一个合并目录,你需要自己按 9:1 拆成 train 和 val。注意 VisDrone 的官方标注文件是 CSV 风格的 txt,里面每行一个目标,前 6 个字段分别是bbox_left, bbox_top, bbox_width, bbox_height, score, category,后续还有截断和遮挡标记。这类原始标注不能直接喂给 YOLOv5,因为 YOLOv5 要求每行是class x_center y_center width height,后 4 个值全部归一化到 0~1。第一个边界坑就在这里:格式不转换,训练会直接报错或者把框画到九霄云外。
第二个边界坑是图片分辨率。VisDrone 不同序列的图尺寸并不统一,有 2000x1500,也有 1360x765,甚至更小。很多转换脚本为了省事把宽高写死,结果一部分图片的标注全部偏移。正确做法是逐张读取图片尺寸再归一化,下面这个脚本就是按这个思路写的。
2.2 把 VisDrone 的框标注转成 YOLOv5 的 txt:逐张读取尺寸再归一化
import cv2 from pathlib import Path src_dir = Path("raw_labels/train") # 原始 VisDrone 标注目录 img_dir = Path("images/train") # 对应图片目录 dst_dir = Path("yolo_labels/train") # 输出 YOLOv5 标注目录 dst_dir.mkdir(parents=True, exist_ok=True) # 只保留俯视场景里高频出现的 7 类,others 直接丢弃 category_map = { "pedestrian": 0, "car": 1, "van": 2, "truck": 3, "bus": 4, "motor": 5, "bicycle": 6, } for label_path in src_dir.glob("*.txt"): image_path = img_dir / (label_path.stem + ".jpg") img = cv2.imread(str(image_path)) if img is None: print(f"missing image: {image_path}") continue h, w = img.shape[:2] # 逐张取宽高,禁止写死 lines = label_path.read_text().strip().splitlines() out_lines = [] for line in lines: parts = line.split(",") if len(parts) < 7: continue x, y, bw, bh, score, cat = parts[:6] if float(score) == 0: # 忽略标注为 ignore 的实例 continue if cat not in category_map: continue cx = (float(x) + float(bw) / 2) / w cy = (float(y) + float(bh) / 2) / h nw = float(bw) / w nh = float(bh) / h # 防止四舍五入后越界 cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) out_lines.append(f"{category_map[cat]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") if out_lines: (dst_dir / (label_path.stem + ".txt")).write_text("\n".join(out_lines))这段脚本解决的两个问题:一是逐张读图取宽高,不写死分辨率,从根本上规避了多分辨率数据集的偏移;二是过滤score == 0的框。VisDrone 原始标注里有不少低置信度或需要忽略的区域,训练数据里混入这些噪声框,轻则让 loss 波动,重则让模型学到错误的边界。参数说明上,category_map的写入顺序就是 YOLOv5 类别索引,后面 data.yaml 的names列表必须和这个顺序保持一致;if cat not in category_map把 others 这类语义模糊的类别直接丢掉,因为俯视场景里它们对车辆和行人检测没有帮助,反而会增加后处理阶段的假阳性。
转换完成后,建议抽查 5~10 张图,把 txt 和原图叠在一起可视化。可以用任何目标检测常用标注工具打开,也可以用 OpenCV 画框,这一步能发现坐标偏移、类别错位和归一化方向错误。别跳过,因为这种黑匣子问题在训练跑到一半时才暴露,浪费时间。
2.3 类别映射与数据集划分:按视频序列拆而不是按帧拆
VisDrone 的数据来自无人机连续拍摄的视频帧,同一辆车在相邻帧里会反复出现。如果你用随机 shuffled 方式做 9:1 划分,很容易出现同一辆车在 train 和 val 里各出现一次,mAP 虚高,部署时立刻缩水。正确做法是先按视频序列分组,再以序列为单位划分。VisDrone 原始文件名通常带序列信息,比如0000156这种编号,你可以解析文件名前缀来做序列 ID。
import random from pathlib import Path label_files = sorted(Path("yolo_labels/train").glob("*.txt")) seq_groups = {} for f in label_files: seq_id = f.stem.rsplit("_", 1)[0] # 按文件名中的序列前缀分组 seq_groups.setdefault(seq_id, []).append(f) seq_ids = list(seq_groups.keys()) random.Random(42).shuffle(seq_ids) train_seqs = seq_ids[: int(len(seq_ids) * 0.9)] val_seqs = seq_ids[int(len(seq_ids) * 0.9):] train_files = [] for s in train_seqs: train_files.extend(seq_groups[s]) # 把 train_files 移动到 images/train 和 labels/train这里random.Random(42)固定随机种子,让数据集划分可复现,后续调整超参数时不会因为数据变了而误判效果。划分后还要检查一下两个子集的类别数量分布,如果 val 里恰好没有 bus,那么 bus 类在验证指标上会显示为 0,不代表模型没学好。
2.4 data.yaml 怎么写:路径、类别名和 nc 三处最容易错
YOLOv5 的 data.yaml 是训练入口,写错路径或类别名通常不会报错,只在结果里表现出诡异现象。下面是一个经过压缩包整理后的典型配置:
# visdrone.yaml path: ./datasets/visdrone-yolo train: images/train val: images/val test: images/test # 可选 nc: 7 names: 0: pedestrian 1: car 2: van 3: truck 4: bus 5: motor 6: bicyclepath最好用相对路径,并且整个项目目录不要带中文或空格,否则在 Windows 和服务器之间迁移时容易出问题。names的索引必须和转换脚本里的category_map完全对应:如果脚本里car是 1,data.yaml 里1的位置也必须写car。这个错位很隐蔽,训练时 loss 正常,但画框或解析类别时会张冠李戴。nc的值也必须和 names 数量一致,多写或少写都会在模型输出层留下隐患。
3. 用 YOLOv5 训练 VisDrone 数据集:训练命令与必调参数
3.1 从 conda 环境到训练命令:一套能跑通的最小配置
YOLOv5 的环境配置本身不算复杂,Python 3.8 到 3.10 都行,PyTorch 按显卡版本装对应 CUDA 版。装完后用下面这条命令就能开始训练:
conda activate yolo cd yolov5 pip install -r requirements.txt python train.py --data visdrone.yaml --weights yolov5s.pt \ --batch-size 16 --epochs 100 --img 640 \ --device 0 --name visdrone_s--weights yolov5s.pt加载的是 COCO 预训练权重,迁移到 VisDrone 航拍域时能明显加快收敛速度,不要用随机初始化的--weights ''从头训,除非你想在绝大多数任务上浪费大量时间。--img 640是最小的训练输入,跑通流程用它可以,但航拍小目标在这种尺寸下几乎会被压没,后面会专门讲为什么建议提到 1280。
参数说明:--name visdrone_s是训练结果保存目录名,所有输出会放在runs/train/visdrone_s下;--batch-size和--img相互制约,显存不够时先降 batch,不要降 img,因为对 VisDrone 来说输入尺寸比 batch 更影响最终精度。如果你的 GPU 只有 8G 显存,想跑 1280 输入,可以试试开启梯度累积:--batch-size 4 --accumulate 4,等效 batch 为 16。
3.2 YOLOv5 超参数调整:航拍场景优先动这四个
YOLOv5 的超参数文件在data/hyps/hyp.scratch-low.yaml,很多人直接默认配置跑,结果在 VisDrone 上效果一般。针对俯视视角的密集小目标,我一般会改四个地方:
lr0: 0.01 # 初始学习率 lrf: 0.1 # 最终学习率,训练到后期衰减到 lr0 * lrf mosaic: 1.0 # 保留 Mosaic 数据增强 scale: 0.5 # 随机缩放比例,默认 0.9,航拍场景下调小 hsv_h: 0.015 # 色调扰动,保留作为光照变化模拟scale是我改动最多的项。默认值 0.9 会让目标在训练中被随机缩放到 0.1~1.9 倍,对 COCO 这种目标尺度丰富的数据集是好事,但 VisDrone 里同一张图既有 300 像素的卡车又有 30 像素的行人,缩放幅度过大会破坏小目标的空间比例,让模型更关注大目标。调到0.5后,小目标的召回率普遍能提升几个点。mosaic: 1.0要保持开启,它把四张图拼成一张,相当于强行增加了训练样本密度,对密集目标场景帮助很大。如果训练后期 loss 波动剧烈,可以把mosaic在最后 20 个 epoch 关闭,方法是训练结束后用关闭 mosaic 的配置继续微调。
另外还有一个容易被忽略的类别损失权重问题。在hyp.scratch-low.yaml里没有直接按类别设权重的参数,如果想提高行人这一类别的权重,需要用--classification之外的定制手段,或者直接保证类别映射里行人样本数足够。VisDrone 的原始数据里行人标注其实不少,但很多是小目标,如果你切了图再训练,行人的小框会更容易被模型注意到。
3.3 处理小目标的关键增强:切图、多尺度与推理参数联动
无人机俯视视角下,行人可能只有 15x15 像素。YOLOv5 默认检测头里,小目标依赖的浅层特征容易被丢弃。常见的可靠手段是切图训练:把 2000x1500 的图切成 4 张 1000x750 的小块,切图后的标注跟着算一遍偏移量。每张小图的目标尺度变大,模型更容易学到特征。切图后可以用--img 640训练,显存占用反而更小,效果通常优于直接喂大图。
第二种手段是多尺度训练,命令里加一个--multi-scale:
python train.py --data visdrone.yaml --weights yolov5s.pt \ --batch-size 8 --epochs 100 --img 1280 \ --multi-scale --device 0 --name visdrone_ms--multi-scale会让每轮迭代随机采样输入尺寸,从 0.5 倍到 1.5 倍之间波动,模型被迫适应不同尺度的目标,对 VisDrone 这种尺度跨度大的数据集很合适。缺点是训练时间变长,约增加 30% 到 50%。
推理时也要配合调整。很多人在训练时用 1280,推理时却为了省时间用 640,效果大幅下降。YOLOv5 对输入尺寸很敏感,尤其是小目标场景。部署时如果算力允许,推理图尺寸尽量保持和训练一致,至少不要低于训练的 0.8 倍。先大图后小图的策略也可以:用 1280 训练前 30 个 epoch,然后切回到 640 再训练 30 个 epoch,模型既学到了大图上的细节,又能在实际部署时保持可接受的推理速度。这个技巧在 yolo 系列项目里称为多尺度迁移,是航拍检测里性价比很高的做法。
4. 无人机俯视视角 YOLOv5 的常见问题排查:5 个经典翻车现场
4.1 训练 loss 一直在掉,val mAP 却像心电图一样乱跳
现象:训练日志里 box loss 和 cls loss 持续下降,看起来一切正常,但 val 集的 mAP 忽高忽低,最终模型效果也很差。
原因:最常见的是训练集和验证集之间存在“数据泄漏”。VisDrone 的同一视频序列里,同一辆车和同一个人会连续出现在几十帧中。如果划分方式用了随机打散而不是按序列分组,train 里出现的车在 val 里又出现,模型记住的是这辆车而不是车的类别特征,val mAP 自然忽上忽下。
解决:回到第 2.3 节,按序列前缀分组后重新划分。如果你已经在跑训练,那就重新做数据集划分,清空runs/train下的旧结果再训,不要带着虚高的 mAP 去做任何参数判断。这一步是航拍数据集和老老实实的单帧数据集最不一样的地方。
4.2 转换脚本看着没问题,画出来的框全是歪的
现象:把 VisDrone 标注转成 YOLOv5 格式后,用可视化脚本叠框,部分图的框偏移严重,有的框甚至落到画面外。
原因:写死了图片宽高。我见过很多人把img_w, img_h = 2000, 1500写在脚本开头,结果遇到 1360x765 的图就出错。VisDrone 的图来自不同飞行平台和相机,分辨率不一致,不是所有图都是 2000x1500。
解决:用cv2.imread逐张取img.shape[:2],再算归一化坐标。另外注意 VisDrone 标注里可能存在越界坐标,比如框的左边界超出图像宽度,直接除法归一化后可能得到大于 1 的值,训练时会被 YOLOv5 内部裁剪,但也会带来隐性损失。解决方法是把归一化后的中心点和宽高都min(max(...))夹到 0~1,这条我写进了第 2.2 节脚本里。
4.3 模型训练完,检测结果里疯狂输出“others”假阳性
现象:val 的时候总体 mAP 还行,但可视化结果里经常冒出一些框在树丛、屋顶、阴影上,这些位置在 VisDrone 原始标注里被标成 others 或 ignore。
原因:原始标注里score=0的 ignore 框和 others 类别都会向模型传递大量低质量监督信号。ignore 框如果不过滤,模型会尝试去拟合标注者自己也说不清的目标;others 类别语义太杂,包含各种背景杂物,类别内部差异比车辆和行人之间的差异还要大。
解决:在转换脚本里按score == 0过滤,同时把 others 从类别表里移除。如果你已经训练完了,重新生成训练标签,把类别数从 11 压缩到 7,再从头训一次。类别少了,每一类的正样本数量相对变多,小目标的 AP 曲线也会更平滑。
4.4 输入尺寸调到 1280,显存直接爆掉
现象:--img 1280 --batch-size 16启动后直接 OOM,报CUDA out of memory。
原因:分辨率增加一倍,特征图面积增加四倍,显存开销不是线性涨的。很多人在 640 下用 batch 16 跑得动,就认为 1280 也可以,实际上 1280 在 batch 16 下需要的显存可能是 640 的 4 倍以上。
解决:降 batch。--img 1280 --batch-size 4在大多数 16G 显存的显卡上可以跑。如果还是爆,用--batch-size 2 --accumulate 8做梯度累积。另一种办法是先切图,切成 1000x750 后--img 640也能获得较好效果。不要硬扛 1280,YOLOv5 的切图逻辑对航拍数据很友好,用切图替代大分辨率是更省算力的路线。
4.5 推理画框时大量目标互相重叠,一个行人被截断成两个框
现象:检测结果里,同一辆车被输出多个框,行人区域出现断裂,框与框之间交错得一塌糊涂。
原因:YOLOv5 的 NMS 参数设置偏严格。默认--iou-thres 0.45在常规目标上够用,但 VisDrone 俯视图片中目标密集且彼此靠近,当两个真实目标的 IoU 超过 0.45 时,NMS 会把其中一个当成重复框直接干掉。框被干掉后,剩余框的置信度分布也跟着乱。
解决:推理时把--iou-thres提到 0.6 到 0.7,减少被误杀的框。同时把--conf-thres从默认 0.25 适当调到 0.3 到 0.4,抑制掉一批低置信度的假阳性。在人群和车流密集的视频流里,这两个阈值基本是必调项。训练时不需要改,后处理阶段单独验证就行。
5. 航拍检测的落地验证:mAP 之外还要看这四项
5.1 用 val.py 回归验证:小目标指标怎么看
训练结束后,val.py 输出的 mAP50 容易让人高兴,但对无人机俯视场景,我更看重四个数:小目标的 mAP50-95、中目标的 mAP50-95、每张图的平均漏检数、NMS 之后的假阳性数。可以这样跑:
python val.py --data visdrone.yaml --weights runs/train/visdrone_s/weights/best.pt \ --img 1280 --conf-thres 0.25 --iou-thres 0.6 --save-json--img 1280要与训练输入保持一致或更高,否则精度数据没有可比性;--iou-thres 0.6对应密集场景的 NMS 阈值,用来模拟真实部署时的表现。看结果时别只盯总 mAP,重点看输出的 per-class 表格。如果 pedestrian 的 recall 低于 0.5,说明俯视行人的漏检仍然严重,部署到视频流里会看到框断断续续。此时优先考虑切图训练,而不是盲目加大模型规模。
5.2 用少量自采数据微调:少样本条件下的定位
每次拿到新的航拍场景,我不会直接重新训练,而是在 VisDrone 模型基础上,用自己拍到的 30 到 50 张标注图做微调。命令如下:
python train.py --data my_drone.yaml --weights runs/train/visdrone_s/weights/best.pt \ --batch-size 8 --epochs 60 --img 1280 --freeze 10 --name visdrone_finetune--freeze 10让前 10 层骨干权重冻结,只更新后面的特征层和检测头,能避免少量样本把预训练特征冲掉。新场景的 data.yaml 也要按照第 2.4 节的格式重写,尤其要检查新场景里类别是否覆盖 VisDrone 的全部类别,如果只做车辆识别,可以把nc减小。微调后我习惯用一段没有标注的飞行视频做目测回归,看框的位置是否稳定、是否把树影和车顶搞混。如果发现某些特定角度漏检,就把那几帧挑出来手动加标,继续迭代。这个流程比反复调整超参数更管用,也是我做航拍检测项目时的一个固定习惯。希望帮到你。
本文还有配套的精品资源,点击获取