简介:面向需要将 VisDrone 数据集用于目标检测训练的研究者与开发者,资源直接提供由官方 VisDrone 标注转换出的 COCO 格式 JSON 文件,省去自行编写格式转换脚本的时间,也避免不同标注体系对接时的常见错误。压缩包共 4 个文件、约 10.18MB,其中 2 个 JSON 分别为训练集与验证集标注,1 个 TXT 记录类别标签,1 张 JPG 说明图片下载与目录放置方法。整理时发现 10 张训练集图片标注有误,已删除这些错误标注,保留可用图片;使用者只需从 VisDrone 官网下载原始图片并放入对应目录,即可直接用于 YOLOX 等模型训练。该数据集格式经过 YOLOX 完整训练与测试验证,也支持导入百度 EasyDL 进行二次检查。目前已有 765 人学习下载,适合需要直接获得干净、可用且标注已被验证的 COCO 格式 VisDrone 数据,用于检测实验、算法对比或项目预训练的中初级开发者。
1. 为什么 visdrone 转 COCO 格式是道绕不过去的坎
做过无人机目标检测的应该都有同感:VisDRONE 数据集本身质量不错,但它的标注格式和主流的 COCO 格式差别很大。原始标注是 txt 文本,每行代表一个目标,字段顺序是target_id, bbox_x, bbox_y, bbox_w, bbox_h, score, category_id, truncation, occlusion,而 COCO 格式要求的是 json 文件,包含images、annotations、categories三个顶层数组,bbox 的坐标含义也略有不同。最直接的影响就是:你拿到的预训练模型、开源训练代码,绝大多数默认读 COCO 格式的 json,你没法直接把 visdrone 的 txt 塞进去训练。
我最早接触这个转换是因为复现一个基于 Faster R-CNN 的无人机视角检测实验,数据是 VisDrone2019,代码里 hardcode 了 COCO 格式的数据加载。当时我手动写脚本转,第一版跑完发现检测框全偏了,后来一步步排查才发现是坐标精度和类别映射的问题。这篇文章就把我后来沉淀下来的转换脚本、参数设计和踩坑经验讲清楚,保证你照着做能一次转对。适合正在做无人机视角目标检测、用 COCO API 或 mmdetection 训练,但手里只有 VisDRONE 原始标注的从业者。
2. 认识 visdrone 原始标注和 COCO 格式:先把两个黑匣子拆开
2.1 visdrone 的 txt 标注里每个字段到底是什么意思
VisDRONE 的标注文件放在VisDrone2019-DET-train/annotations目录下,每个图像对应一个同名 txt 文件。打开看,每一行是 10 个数字,用逗号分隔。格式是:
<bbox_left>, <bbox_top>, <bbox_width>, <bbox_height>, <score>, <category>, <truncation>, <occlusion>这里有个关键点:前四个数字是目标框的左上角坐标和宽高,单位是像素。score在检测任务的标注里通常是 -1 或者 1,表示这个标注是否有效。category是类别编号,VisDRONE 里1是pedestrian,2是people,3是bicycle,4是car,5是van,6是truck,7是tricycle,8是awning-tricycle,9是bus,10是motor,11是others。truncation是截断程度,0表示无截断,1表示部分截断,2表示严重截断。occlusion是遮挡程度,同样0/1/2。
注意最后一个字段ignore有的版本会写成 10 个数字,有的版本是 9 个。官方发布的数据里有的文件末尾多余一个逗号,有的没有,这种脏数据很常见。转换的第一步是把这些字段拆对,并且区分哪些目标是要参与训练的、哪些应该被过滤掉。
2.2 COCO json 的顶层结构:images、annotations、categories 三件套
COCO 格式的 json 文件本质是一个嵌套字典,顶层有三个 key:
"images":一个列表,每个元素代表一张图片,包含id、file_name、width、height。"annotations":一个列表,每个元素代表一个目标实例,包含id、image_id、category_id、bbox(四个浮点数:[x, y, width, height])、area、iscrowd。"categories":一个列表,定义类别 ID 到类别名称的映射关系。
这里的bbox同样是[x, y, width, height],从表面上看和 visdrone 标注一致,但坑在于category_id的编号体系。visdrone 的类别编号从1到11,但中间有跳号,而 COCO 类别 ID 要求从1开始连续递增。如果你直接把 visdrone 的category字段传给 COCO json,就会出现类别对应错乱的问题。
另外 COCO 的area字段是width * height,这个值在评估mAP时会用到,不能省。iscrowd在 visdrone 里没有对应概念,统一写0就行。
2.3 为什么不能直接拿现成脚本改改就用
网上确实能找到一些 visdrone 转 COCO 的脚本,但大多数存在几个通病:一是硬编码了路径,换台机器就得改一堆;二是没处理图片尺寸字段,导致 COCO API 在加载时拿不到width和height,跑评估直接报错;三是把ignore目标也写进 annotations,导致训练时把大量遮挡目标当成正样本,mAP 虚高或者训练不稳定。我见过一个项目用这样的脚本转换后,训练 loss 一直在 0.8 上下波动,后来发现是负样本数据被污染了。所以别迷信现成代码,自己把转换逻辑吃透,按需求调整过滤规则,才是可靠的做法。
3. 编写 visdrone 转 COCO 格式的转换脚本:从零开始可复现
3.1 准备数据目录和标注文件清单
在写转换脚本之前,先把数据目录结构理顺。我一般这样组织:
data/ VisDrone2019-DET-train/ images/ 0000001_00000_d_0000010.jpg 0000001_00000_d_0000011.jpg annotations/ 0000001_00000_d_0000010.txt 0000001_00000_d_0000011.txt output/ train_coco.json脚本需要遍历images目录,找到所有 jpg 文件,然后去annotations目录找同名 txt。如果某个图片没有对应的 txt 文件,说明这张图片没有标注,要不要保留看你的训练需求。如果做检测,通常建议过滤掉,因为空白图片不提供正样本信息。如果做鲁棒性测试,可以保留。
import os import json from glob import glob def build_image_list(image_dir, annotation_dir): image_paths = sorted(glob(os.path.join(image_dir, "*.jpg"))) image_list = [] skipped = [] for img_path in image_paths: base_name = os.path.splitext(os.path.basename(img_path))[0] anno_path = os.path.join(annotation_dir, base_name + ".txt") if not os.path.exists(anno_path): skipped.append(img_path) continue image_list.append((img_path, anno_path)) print(f"Total images: {len(image_paths)}, with annotation: {len(image_list)}, skipped: {len(skipped)}") return image_list这段代码的作用是建立图片和标注文件的一一对应关系。glob排序保证了后续生成的image_id是稳定递增的,不会因为文件系统顺序不同而错乱。打印统计信息能帮你快速判断数据是否完整——如果跳过的数量巨大,说明路径配错了。
3.2 定义 visdrone 类别映射表和过滤规则
类别映射是整个转换里最容易出错的地方,visdrone 的类别编号和 COCO 类别 ID 不是一回事。我建议在脚本里显式定义一个映射字典:
VISDRONE_CATEGORIES = { 1: "pedestrian", 2: "people", 3: "bicycle", 4: "car", 5: "van", 6: "truck", 7: "tricycle", 8: "awning-tricycle", 9: "bus", 10: "motor", 11: "others", } def build_coco_categories(): categories = [] for idx, (vis_id, name) in enumerate(VISDRONE_CATEGORIES.items(), start=1): categories.append({ "id": idx, "name": name, "supercategory": "object", }) return categories这里我们把 visdrone 的原始类别编号作为字典的 key,转换后的 COCOcategory_id从1到11连续编号。为什么不用 visdrone 原始编号作为 COCO 的category_id?因为 COCO API 内部会按category_id建索引,跳号会引发评估时类别错位,尤其是当你只训练其中几个类别时,逻辑会乱套。
过滤规则方面,我一般会过滤掉score == 0的标注。VisDRONE 的注释里有特殊标记:score为0表示该目标属于“忽略”区域,不应该参与训练。另外,truncation为2且occlusion为2的目标,通常被认为是极度困难样本,除非你要做难度递增的实验,否则建议过滤。这个规则要写成一个独立的函数,方便你按实验需求调整。
def should_ignore(annotation_values): score = float(annotation_values[4]) truncation = float(annotation_values[6]) occlusion = float(annotation_values[7]) if score == 0: return True if truncation == 2 and occlusion == 2: return True return False把过滤规则独立出来是值得的,因为你可能会需要做消融实验,比如保留全部目标看看在困难样本上的表现。这时候只需要改这个函数,其他逻辑不用动。
3.3 解析 txt 并生成 COCO 格式的 json 文件
核心的转换函数需要逐行解析 txt,把 visdrone 的字段映射成 COCO 的annotation字典:
def convert_visdrone_to_coco(image_list, output_path): coco = { "images": [], "annotations": [], "categories": build_coco_categories(), } annotation_id = 1 for image_id, (img_path, anno_path) in enumerate(image_list, start=1): from PIL import Image im = Image.open(img_path) width, height = im.size coco["images"].append({ "id": image_id, "file_name": os.path.basename(img_path), "width": width, "height": height, }) with open(anno_path, "r") as f: for line in f: line = line.strip() if not line: continue parts = line.split(",") if len(parts) < 8: continue parts = [p.strip() for p in parts] if should_ignore(parts): continue x = float(parts[0]) y = float(parts[1]) w = float(parts[2]) h = float(parts[3]) vis_cat = int(parts[5]) coco_cat = list(VISDRONE_CATEGORIES.keys()).index(vis_cat) + 1 if w <= 0 or h <= 0: continue area = w * h coco["annotations"].append({ "id": annotation_id, "image_id": image_id, "category_id": coco_cat, "bbox": [x, y, w, h], "area": area, "iscrowd": 0, }) annotation_id += 1 with open(output_path, "w", encoding="utf-8") as f: json.dump(coco, f, indent=2) print(f"Converted {len(image_list)} images, {annotation_id - 1} annotations -> {output_path}")这段代码有几个关键参数需要说明:
image_id从1开始,和文件名排序对应,不要用0,因为部分框架将0视为无效 ID。category_id通过list(VISDRONE_CATEGORIES.keys()).index(vis_cat) + 1计算,这里利用了 Python 字典保持插入顺序的特性,确保映射稳定。area直接算w * h,没有乘任何缩放系数。如果你做的是切图后的数据集,比如把大图切成 512x512 的小图,area也要相应地基于小图坐标计算,不能直接用原值。- 过滤掉
w <= 0 or h <= 0的边界框,这类脏标注虽然少,但一旦存在会导致训练时 loss 变成 NaN。
3.4 执行转换:单卡跑通全流程
运行转换脚本时,建议先处理一个子集验证正确性,再跑全量。我习惯加一个--max-images参数,方便快速调试:
python visdrone2coco.py \ --image-dir data/VisDrone2019-DET-train/images \ --anno-dir data/VisDrone2019-DET-train/annotations \ --output data/output/train_coco.json \ --max-images 100上面这个命令只处理前 100 张图片,大约几秒就跑完。打开生成的 json,肉眼检查几个关键字段:file_name是否带.jpg后缀、anno的坐标是否在图像宽高范围内、category_id是否落在1~11区间。确认无误后去掉--max-images参数跑全量。
全量转换 VisDrone2019-DET-train 大约有 6500 张图片,如果PIL逐张读图取尺寸会比较慢,瓶颈在磁盘 IO 和图片解码。想提速的话,可以预先用缓存图片尺寸表,或者直接用os.path.getsize之外的方法从文件名提取——但代价是你要保证数据源没有修改过。我一般直接读图,虽然慢一点但最可靠。
生成 json 之后,下一步通常是用官方 COCO API 做加载验证,确认格式没问题再进训练管线。
4. 验证转换出的 json 是否可用:COCO API 和可视化双保险
4.1 用 pycocotools 加载 json,确定没有结构性错误
转换完成并不代表 json 一定符合 COCO 规范,最容易出问题的是缺失字段或类型不对。用pycocotools做一次权威校验是最快的做法:
from pycocotools.coco import COCO coco = COCO("data/output/train_coco.json") print(f"Loaded {len(coco.imgs)} images") print(f"Loaded {len(coco.anns)} annotations") print(f"Categories: {[coco.cats[i]['name'] for i in sorted(coco.cats)]}")COCO类的构造函数会检查images、annotations、categories三个基础结构,如果 json 缺字段或类型不匹配,它会直接抛异常。这里能通过的 json 在结构上基本没问题。
但COCO加载通过不代表坐标数据合理。一个常见情况是 visdrone 的标注是左上角坐标加宽高,而某些到手的脚本把它当成中心点坐标加宽高,导致全部框位置错误。这种错误COCOAPI 不会报,因为坐标值本身合法。所以必须做第二层验证:可视化。
4.2 随机抽几张图画出检测框,直观验证坐标对齐
可视化虽然是土办法,但却是最有效的验证手段。用matplotlib把图片画出来,然后把 bbox 画上去,肉眼就能看出坐标是否对齐:
import matplotlib.pyplot as plt import matplotlib.patches as patches from PIL import Image import random def visualize_random(coco, num_samples=5): image_ids = list(coco.imgs.keys()) sample_ids = random.sample(image_ids, min(num_samples, len(image_ids))) fig, axes = plt.subplots(1, num_samples, figsize=(15, 5)) for ax, image_id in zip(axes, sample_ids): img_info = coco.imgs[image_id] img_path = f"data/VisDrone2019-DET-train/images/{img_info['file_name']}" im = Image.open(img_path) ax.imshow(im) ann_ids = coco.getAnnIds(imgIds=image_id) anns = coco.loadAnns(ann_ids) for ann in anns: bbox = ann["bbox"] rect = patches.Rectangle( (bbox[0], bbox[1]), bbox[2], bbox[3], linewidth=1, edgecolor="red", facecolor="none", ) ax.add_patch(rect) ax.text( bbox[0], bbox[1] - 5, coco.cats[ann["category_id"]]["name"], fontsize=8, color="yellow", ) plt.show() visualize_random(coco)可视化的时候注意两点:一是确认框的边界是否和目标贴合,如果框明显偏到一边,大概率是坐标定义不对;二是看类别标签是否和框内目标匹配,特别是pedestrian和people这两个类别容易混淆,visdrone 里pedestrian是站立行人,people是群体或非站立的人,转换以后不要试图改变类别定义。
4.3 在 mmdetection 里直接验证训练数据流是否畅通
如果你用的是 mmdetection,转换完的 json 还可以直接用它自带的browse_dataset.py脚本验证。这个脚本会加载数据并可视化样本,它内部走的是和训练一样的数据管线,如果它能正常跑通,说明你的 json 能被训练框架接受。
python tools/misc/browse_dataset.py \ configs/faster_rcnn/faster-rcnn_r50_fpn_1x_coco.py \ --output-dir browse_output \ --show-dir browse_output注意这里需要把你的数据集配置先改好:data_root指向你的数据目录,train_ann_file指向生成的 json,train_img_prefix指向图片目录。如果 browse 出来的图片上标注位置正确,说明数据管线没问题,接下来就能直接开训了。
5. visdrone 转 COCO 挺身而出的避坑指南:这些坑我替你踩过了
5.1 坑一:图片尺寸读取错误,导致 area 计算和归一化全部错乱
现象:转换后的 json 里width和height字段和实际图片尺寸不一致,有的甚至直接写成[0, 0],训练时数据加载直接报错。
原因:脚本地读取的图片尺寸是从 label 文件里猜的,或者干脆写死了固定值1920x1080。VisDRONE 数据集的图片分辨率确实多为1360x765或类似的宽高比,但不是每张都完全一致,尤其是在裁剪后的子集里更不统一。
解决:不要信任何先验尺寸,用PIL.Image.open(img_path).size实时读取每一张图的实际尺寸。前面 3.3 节里的代码已经采用这个做法,如果你接手的是别人的转换脚本,第一时间检查这部分逻辑。
5.2 坑二:类别 ID 映射错位,训练时类别标签全乱了
现象:训练时 loss 能收敛,但验证集的 mAP 断崖式下跌,查看预测结果的类别名发现car被预测成了bus,所有类别整体错位。
原因:visdrone 原始category字段是1~11,但中间没有编号0。有些转换脚本直接把这个编号赋给 COCOcategory_id,而 COCO 要求类别 ID 从1开始连续。更隐蔽的问题是,如果你只选择 visdrone 的car、bus、truck三类做训练,但是映射表写成了按原编号取值,最终categories列表里的顺序和annotation的category_id对不上。
解决:按 3.2 节的做法,用字典做显式映射,先定义 visdrone 原始编号到 COCO 新编号的对应关系,再生成categories列表。转换后写一个小测试脚本,统计每个category_id的样本数量,和源数据对应类别数量做对比,能快速发现错位。
5.3 坑三:过滤了 score=0 的目标,但忘了同步过滤图片
现象:转换后的 json 里存在某些图片完全没有任何 annotation,COCO API 在评估时会报错或者跳过这些图,导致 mAP 计算图片数量不一致。
原因:有的 txt 文件里所有目标都是score=0,过滤后该图片的annotations列表为空,但images列表里仍然保留了这条记录。COCO 的评估代码通常假设每张图片至少有一个目标,遇到零目标图片会直接崩溃。
解决:在生成images条目时做一个后检查——如果一张图片过滤后没有剩余任何 annotation,就把它的images条目也删掉。或者反过来,先解析所有 annotation,再统一构建没有重叠的images列表。同时打印跳过图片的数量,确保这个比例在合理范围内。
5.4 坑四:坐标值没有保留浮点精度,检测框在降采样后偏移
现象:训练时数据增强里的随机裁剪、缩放一切正常,但最终检测时小目标的框位置有系统性偏移。
原因:转换脚本里用int()而不是float()处理 visdrone 的坐标和宽高。visdrone 原始标注是小数坐标精度,转成整数后,在图像缩放到更小分辨率时误差被放大,尤其是小目标本来就只有十几个像素宽,损失几像素就可能导致 IoU 骤降。
解决:bbox字段保留浮点类型,不要在转换时取整。pycocotools 本身支持浮点 bbox,很多检测框架在数据增强时也会把坐标转成 float 处理,所以完全没有取整的必要。这个坑在验证阶段很难发现,因为可视化时框差一两个像素肉眼分辨不出来,只有看定量 mAP 才会暴露。
5.5 坑五:json 文件编码问题,COCO API 在 Windows 下读不了
现象:json 文件在 Linux 下转换正常,但拷贝到 Windows 机器上用 COCO API 加载就报UnicodeDecodeError或 keyError。
原因:Linux 下默认utf-8,Windows 默认gbk。json.dump如果不指定ensure_ascii=False,所有中文字符串都会转成\uXXXX的 ASCII 编码,虽然在 Linux 下能正常读,但 Windows 下如果文件头没有 UTF-8 BOM,就可能出问题。
解决:json.dump(coco, f, ensure_ascii=False, indent=2),并在打开文件时指定encoding="utf-8"写入。这样生成的 json 是纯 UTF-8 编码,跨平台不会出乱码。此外,看 json 文件里类别名称是否可读,也是判断编码正确与否的最直观方法。
6. 进阶:把 COCO json 用起来,训练前再做一次数据体检
6.1 按类别统计样本数和 bbox 面积分布,排查长尾问题
数据集转换完之后,别急着开训。先跑一个数据统计脚本,看看每个类别的实例数量、bbox 面积分布、以及长尾效应:
from pycocotools.coco import COCO import numpy as np import matplotlib.pyplot as plt coco = COCO("data/output/train_coco.json") cat_ids = list(coco.cats.keys()) instances_per_cat = {} area_ranges = {"small": 0, "medium": 0, "large": 0} for cat_id in cat_ids: ann_ids = coco.getAnnIds(catIds=[cat_id]) anns = coco.loadAnns(ann_ids) instances_per_cat[coco.cats[cat_id]["name"]] = len(anns) for ann in anns: area = ann["area"] if area < 32 * 32: area_ranges["small"] += 1 elif area < 96 * 96: area_ranges["medium"] += 1 else: area_ranges["large"] += 1 print("Instances per category:", instances_per_cat) print("Area distribution:", area_ranges)这一步能让你看到 visdrone 里pedestrian和car的样本量远大于awning-tricycle,训练时就需要考虑类别重采样或损失函数加权。另外,如果 small 目标占比过高(在无人机视角下这是常态),你的模型结构里针对小目标的检测头设计就得多花心思。
6.2 把 json 转成 mmdetection 能直接用的数据集配置模板
如果你最终要在 mmdetection 里训练,可以用下面的配置模板替换 COCO 数据集路径:
dataset_type = "CocoDataset" data_root = "data/visdrone/" metainfo = { "classes": [ "pedestrian", "people", "bicycle", "car", "van", "truck", "tricycle", "awning-tricycle", "bus", "motor", "others", ], } train_dataloader = dict( batch_size=4, num_workers=4, dataset=dict( type=dataset_type, data_root=data_root, ann_file="annotations/train_coco.json", data_prefix=dict(img="images/"), metainfo=metainfo, ), )注意metainfo里的类别名称顺序要和 json 的categories列表顺序严格一致,否则 mmdetection 在显示类别名时会错乱。这里classes名称本身并不影响训练,因为训练只认category_id,但影响日志和可视化结果的解读。
6.3 最后一步:检查完整训练集、验证集划分是否合理
VisDRONE 官方数据集本身提供了train、val、test划分,每个部分都有独立的 images 和 annotations 目录。转换时要分开转换,生成三个独立的 json 文件。不要用一个 json 既做训练又做验证,因为目标检测领域交叉验证的常规做法是保持域一致,不让同分布的图片出现在训练和验证两个集合里。
我建议转换后记录一组基线数据:训练集图片数、实例总数、每张图平均实例数。VisDrone2019-DET-train 这些指标大概是 6500+ 张图、约 30 万个实例、平均每张图 40 多个目标。如果你的统计结果和这个量级差距过大,先查转换逻辑哪里有问题,别直接开训。
我每次转换完都会随机抽 20 张图过一遍可视化,确认坐标、类别没问题才进训练。这个习惯帮我挡过不少数据质量的暗坑。希望这篇能帮你少走一段弯路,祝转换顺利。
本文还有配套的精品资源,点击获取