ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

VisDrone2019转COCO格式:目标检测数据集转换实战指南

2026/9/2 1:47:00 拓冰建站 浏览量
VisDrone2019转COCO格式:目标检测数据集转换实战指南 简介面向计算机视觉开发者与Visdrone数据集使用者这份工具包解决了将Visdrone2019的DET标注与VID跟踪标注转换为通用COCO格式的痛点适用于目标检测与视频目标检测/跟踪任务的前期数据准备。资源共3个文件、整体仅3KB其中2个Python脚本分别承担DET与VID数据的转换逻辑1个Markdown说明文档详细列出了所有需要修改的路径位置用户只需按提示调整路径即可运行对新手非常友好。目前已有2207人学习使用验证了其实际参考价值。通过该资源可以快速获得可直接对接主流检测框架的COCO格式数据集省去自行编写解析与映射代码的时间同时readme中还包含路径设置与常见注意事项有助于避免格式转换中的典型错误提升数据预处理效率。 最近在折腾计算机视觉检测模型的训练第一步就卡在了数据格式上。项目里用的基准数据集是VisDrone2019里面同时含DET单帧图像目标检测和VID视频目标检测两个子任务但这两个任务的标注都是VisDrone自己的txt格式而我现在要用的训练框架只认COCO JSON格式。网上翻了一圈要么脚本太老跑不通要么只支持DET不支持VID索性自己写了一套转换工具。这篇文章把整个转换过程、思路、代码和踩过的坑都整理出来给同样要做VisDrone2019转COCO的朋友做个参考。1. 转换前的数据梳理VisDrone2019的DET和VID到底长什么样1.1 VisDrone2019的数据目录与标注文件格式先说数据本身。VisDrone2019的DET和VID是两个独立的文件夹目录结构不太一样。DET部分比较简单长这样VisDrone2019-DET/ ├── annotations/ │ ├── 0000001_00000_d_0000001.txt │ ├── 0000001_00000_d_0000002.txt │ └── ... └── images/ ├── 0000001_00000_d_0000001.jpg ├── 0000001_00000_d_0000002.jpg └── ...VID部分多了一层序列目录每个序列是一个小视频片段VisDrone2019-VID/ ├── annotations/ │ ├── seq_000001/ │ │ ├── seq_000001_000001.txt │ │ └── ... │ ├── seq_000002/ │ │ └── ... └── sequences/ ├── seq_000001/ │ ├── seq_000001_000001.jpg │ └── ... ├── seq_000002/ │ └── ...标注文件不管在DET还是VID内容格式是一样的每一行代表一个目标框bbox_left, bbox_top, bbox_width, bbox_height, score, category_id, truncation, occlusion前四个是像素坐标值注意是左上角坐标加宽高不是中心点坐标也不是右下角坐标。第五个值一般是检测置信度在GT标注里通常为1但也可能出现0或-1。第六个是类别ID第七第八分别是截断程度和遮挡程度范围0到2或者0到1具体看官方说明。我建议保留这两个值以后做遮挡分析或者数据清洗能用上。VisDrone的类别ID从0开始0代表忽略区域后面1到10才是我们真正要用的目标类别pedestrian、people、bicycle、car、van、truck、tricycle、awning-tricycle、bus、motor。这里很容易踩坑如果直接把源文件里的category_id原样塞进COCO会把忽略区域也当成一类模型训练直接乱掉。1.2 COCO格式的核心字段和这次转换的基本思路COCO数据集的核心是一个JSON文件里面有images、annotations、categories三块images每张图的宽高、文件名、图片IDannotations每个标注框的图片ID、bbox坐标、面积、类别ID等categories类别ID和类别名的映射bbox的格式正好也是[x, y, width, height]和VisDrone一样这一点省了不少事。但这个“一样”是个坑后文细说。对于VIDCOCO标准格式里没有视频序列的概念。但好在检测模型训练时只要每张图能独立加载、每个框能正确归属到图片就行不需要跨帧信息。所以我在转换VID时在images里额外加了video_id和frame_id方便以后做视频检测或跟踪任务时复用不影响检测训练。转换思路也很直接一个Python脚本分别处理DET和VID两个模式解析txt - 组装dict - dump成JSON。不折腾xml、不搞数据库轻量直接。2. 关键问题拆解坐标、类别ID和为什么这么设计2.1 bbox坐标和类别ID的映射规则VisDrone的bbox是[x, y, w, h]COCO也是[x, y, w, h]粗看可以直接复制。但有一个细节坐标值和宽高在源码里可能是浮点数也可能是整数如果直接转JSON精度会有问题。我建议保留浮点数不要自己取整让训练框架自己去处理。因为目标检测的bbox精度对最终mAP影响不算大但取整会损失边界定位尤其在VisDrone这种大量小目标的场景里。类别ID的处理才是真正的重点。VisDrone的category_id从0开始0是忽略区域1~10是有效类别。COCO没有“忽略区域”这种说法虽然可以用iscrowd表示但与VisDrone的忽略语义不完全相同。稳妥的做法是过滤掉category_id为0的框保留1~10作为COCO的category_idcategories列表里的id也设为1~10name直接用VisDrone官方名字这样在mmdetection或detectron2里把classes列表写成[pedestrian, people, ...]就能正常对应。不要画蛇添足地把源类别减一变成0开始因为COCO的category_id和模型class索引经常是分离的很多框架读COCO时会自动用categories id来对齐你转换时保持和源数据一致反而最不容易出错。2.2 DET和VID统一转换的架构设计为什么不写两个脚本因为DET和VID的标注格式完全一样只是目录结构不同。写一个脚本加一个--mode参数共用同一个txt解析函数就能避免重复维护逻辑也减少出错概率。在代码组织上我拆成三个主要部分解析函数负责读取一个txt返回annotation列表遍历函数负责遍历图片目录和标注目录逐张图调用解析函数维护image_id和annotation_id主函数负责组装COCO字典写JSON文件这样后续如果要扩展到其他数据集格式转换只需要替换解析函数主流程完全不用动。另外还要考虑一点VisDrone的图片命名不是连续的DET里有些序号是跳的VID里序列目录和帧号也有自己的规律。如果脚本里硬编码解析文件名里的数字转换到一半可能就崩。我干脆用顺序计数器生成image_id文件名只作为file_name存进JSON。这个设计在后面排查问题时帮了很大忙。3. 实操过程完整脚本代码与使用说明3.1 脚本功能模块速览我先把最终脚本按功能模块拆一下让你知道每一段是干嘛的。整个脚本依赖Python标准库加上Pillow不需要额外装cv2如果处理超大图片集再考虑用cv2加速宽高读取。模块清单parse_visdrone_txt(txt_path, image_id)解析单个txtconvert_det(images_dir, anns_dir, output_json)处理DETconvert_vid(sequences_dir, anns_dir, output_json)处理VIDbuild_coco(anns_list, images_list, categories)组装COCO JSONif __name__ __main__命令行入口支持--mode det/vid3.2 核心代码实现通用解析函数import os import json from PIL import Image VISDRONE_CLASSES { 1: pedestrian, 2: people, 3: bicycle, 4: car, 5: van, 6: truck, 7: tricycle, 8: awning-tricycle, 9: bus, 10: motor } def parse_visdrone_txt(txt_path, image_id): annotations [] with open(txt_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue parts line.split(,) parts [float(p) for p in parts] if len(parts) 6: continue x, y, w, h, score, cat parts[0], parts[1], parts[2], parts[3], parts[4], int(parts[5]) trunc parts[6] if len(parts) 6 else 0 occ parts[7] if len(parts) 7 else 0 # 过滤忽略区域和无效框 if cat 0: continue if w 0 or h 0: continue annotation { image_id: image_id, bbox: [x, y, w, h], area: w * h, category_id: cat, iscrowd: 0, score: score, truncation: trunc, occlusion: occ } annotations.append(annotation) return annotations这里有几个关键点类别0过滤掉因为VisDrone的ignore区域在COCO评估里没有对应机制留在训练集里只会干扰模型score、truncation、occlusion都保留在annotation里COCO API读取时会自动忽略多出字段不影响兼容性如果txt里格式不完整缺了truncation和occlusion用len(parts)防御3.3 DET转换主流程def convert_det(images_dir, anns_dir, output_json): categories [{id: k, name: v} for k, v in VISDRONE_CLASSES.items()] images_list [] annotations_list [] image_id 1 annotation_id 1 for img_name in sorted(os.listdir(images_dir)): if not img_name.lower().endswith(.jpg): continue img_path os.path.join(images_dir, img_name) txt_path os.path.join(anns_dir, os.path.splitext(img_name)[0] .txt) if not os.path.exists(txt_path): print(f[warning] missing annotation: {txt_path}) continue with Image.open(img_path) as img: width, height img.size images_list.append({ id: image_id, file_name: img_name, width: width, height: height }) anns parse_visdrone_txt(txt_path, image_id) for ann in anns: ann[id] annotation_id annotation_id 1 annotations_list.append(ann) image_id 1 coco_data { images: images_list, annotations: annotations_list, categories: categories } with open(output_json, w, encodingutf-8) as f: json.dump(coco_data, f, ensure_asciiFalse) print(f[DET] images: {len(images_list)}, annotations: {len(annotations_list)}, output: {output_json})这个逻辑非常直白遍历图片找到同名txt读取宽高解析标注往列表里塞数据。注意这里用的是PIL.Image.open读取宽高相比用OpenCV读取少依赖一个库处理jpg也够用。3.4 VID序列结构怎么扩展VID比DET多一层序列目录转换逻辑只在遍历方式上不同核心解析函数完全复用。处理思路是外层遍历sequences下的每个子目录内层遍历序列目录里的所有jpg。def convert_vid(sequences_dir, anns_dir, output_json): categories [{id: k, name: v} for k, v in VISDRONE_CLASSES.items()] images_list [] annotations_list [] image_id 1 annotation_id 1 video_id 1 for video_name in sorted(os.listdir(sequences_dir)): video_path os.path.join(sequences_dir, video_name) if not os.path.isdir(video_path): continue frame_id 1 for img_name in sorted(os.listdir(video_path)): if not img_name.lower().endswith(.jpg): continue img_path os.path.join(video_path, img_name) txt_path os.path.join(anns_dir, video_name, os.path.splitext(img_name)[0] .txt) if not os.path.exists(txt_path): print(f[warning] missing annotation: {txt_path}) continue with Image.open(img_path) as img: width, height img.size images_list.append({ id: image_id, file_name: os.path.join(video_name, img_name), width: width, height: height, video_id: video_id, frame_id: frame_id }) anns parse_visdrone_txt(txt_path, image_id) for ann in anns: ann[id] annotation_id annotation_id 1 annotations_list.append(ann) image_id 1 frame_id 1 video_id 1 coco_data { images: images_list, annotations: annotations_list, categories: categories } with open(output_json, w, encodingutf-8) as f: json.dump(coco_data, f, ensure_asciiFalse) print(f[VID] videos: {video_id - 1}, images: {len(images_list)}, annotations: {len(annotations_list)}, output: {output_json})注意到file_name这里用video_name/img_name保留了相对路径。如果你后续用mmdetection可以在dataset里直接把data_root指向sequences根目录然后img_prefixsequences或者直接用这个相对路径非常方便。3.5 运行示例和验证结果假设你的数据目录长这样/path/to/VisDrone2019-DET/ /path/to/VisDrone2019-VID/运行命令python convert_visdrone.py --mode det \ --images /path/to/VisDrone2019-DET/images \ --anns /path/to/VisDrone2019-DET/annotations \ --output /path/to/visdrone_det_coco.json python convert_visdrone.py --mode vid \ --images /path/to/VisDrone2019-VID/sequences \ --anns /path/to/VisDrone2019-VID/annotations \ --output /path/to/visdrone_vid_coco.json转换完之后别急着拿去训练先验证一下JSONimport json with open(/path/to/visdrone_det_coco.json, r) as f: data json.load(f) print(images:, len(data[images])) print(annotations:, len(data[annotations])) print(categories:, data[categories])正常情况下DET转换完应该有几千张图片、几万个标注框VID转换完的图片数会更多因为视频帧是连续的。我实际转换后的DET图片数和官方标注一致VID部分由于有些帧缺失标注脚本会打印warning核对后数量也对得上。4. 常见问题与排查技巧实录4.1 官方数据集下载后目录结构不对解析总是失败这个问题我一开始也遇到过。下载VisDrone2019的压缩包时DET和VID是分开的解压后第一层目录有时候带有多余的文件夹名比如VisDrone2019-DET/VisDrone2019-DET/...。如果脚本找不到图片或txt先检查目录层级对不对不要只盯着代码报错。还有一个更隐蔽的问题VID的标注目录里有些序列文件夹是空的或者没有对应的图片目录。遍历时一定要先判断os.path.isdir(video_path)否则会试图打开一个不存在的文件直接抛异常。4.2 坐标宽高直接复制为什么训练后mAP偏低VisDrone的bbox宽高理论上和COCO一致都是[x, y, width, height]我用COCO API可视化检查时也没发现框偏移。但训练后mAP比预期低后来排查发现是数据清洗时把宽或高为0的框过滤掉了但没过滤面积过小的框。VisDrone里有很多小目标宽度小于2像素甚至1像素的标注不在少数。COCO官方的annToMask这类函数对面积太小或者宽高为0的框会出问题而mmdetection的部分数据增强也会因为bbox太小导致采样异常。建议在转换时加一个最小尺寸阈值比如宽高小于2的框直接过滤或者保留但设置一个最小面积。我这里因为要尽量保持原始标注完整只过滤了w 0 or h 0如果你在训练时报bbox相关错误可以回来检查这一步。4.3 category_id到底该从0开始还是从1开始这是在转换时最容易纠结的问题。VisDrone源文件的类别ID是1~10忽略区域是0。我转换时把所有0过滤掉然后保留1~10作为COCO的category_id。这样做之后在mmdetection的配置里classes (pedestrian, people, bicycle, car, van, truck, tricycle, awning-tricycle, bus, motor)mmdetection读取COCO时会按照categories里的id和name一一对应不会强制你从0开始。detectron2也类似。所以最稳妥的方式就是保持源数据ID不动只在代码里定义好类别列表顺序。但如果你用的某些框架内部默认类别ID从0开始比如自己写DataLoader时用0 ~ num_classes-1那就要在转换时把category_id - 1。这个没有标准答案取决于你的下游代码。我的建议是转换脚本里加一个--offset参数默认为0表示保持原ID不变需要时用--offset -1。别在脚本里硬编码后续换框架能少改很多。4.4 图片读取用PIL还是OpenCV路径带中文怎么办我代码里用的是Pillow因为纯Python环境下更容易安装并且读取图片宽高足够用。但实测发现如果图片路径里带中文部分Linux环境下Pillow能正常读OpenCV的imread会返回None。所以如果你的数据集放在带中文路径的目录下一定要先统一路径字符编码或者干脆用相对路径。另外转换完成后如果训练时提示图片文件不存在八成不是文件缺失而是COCO JSON里的file_name和你在dataset里配置的data_root拼不出正确路径。我在VID部分用的是seq_000001/seq_000001_000001.jpg这种相对路径然后在训练配置里把data_root指向sequences目录这样最不容易出错。如果你用的是DETfile_name可以只写图片文件名data_root指向images目录简单直接。5. 转换完成后怎么验证和扩展5.1 用COCO API和可视化快速检查JSON生成完我习惯用第一步可视化检查。如果你装了pycocotools可以把GT加载进去随便挑几张图画框看看from pycocotools.coco import COCO from PIL import Image, ImageDraw coco COCO(/path/to/visdrone_det_coco.json) img_info coco.loadImgs(coco.getImgIds()[0])[0] ann_ids coco.getAnnIds(imgIdsimg_info[id]) anns coco.loadAnns(ann_ids) img Image.open(/path/to/images/ img_info[file_name]) draw ImageDraw.Draw(img) for ann in anns: x, y, w, h ann[bbox] draw.rectangle([x, y, x w, y h], outlinered, width2) img.save(check_visdrone.png)这一步能直接发现坐标偏移、类别ID混乱、路径错误等问题。我建议至少抽20张图看不同场景、不同密度的都要看别只看前几张。5.2 给VID数据增加track_id扩展VisDrone的VID标注本身不提供跨帧目标ID所以转出来的COCO也无法直接用于训练多目标跟踪。如果后续要做跟踪需要自己根据帧间重叠做关联或者换用带track ID的数据集。这里先不展开但我在images里预留了video_id和frame_id后面做序列模型时这些字段可以直接用不需要重新转换一遍。5.3 关于性能转换慢怎么办VisDrone DET图片数量在几千到一万左右VID会更多可能达到几万甚至十万帧。如果遍历过程中每张图都调用Image.open读宽高速度其实还可以接受但如果机器不够快或者图片太大会明显变慢。我自己实测DET全量转换不到一分钟VID全量转换大概几分钟瓶颈主要在网络IO和磁盘不是CPU。如果确实要提速可以用OpenCV的cv2.imread配合IMREAD_UNCHANGED读取宽高速度差别不大真正影响性能的是写JSON时的ensure_asciiFalse和缩进参数。我这里为了代码清晰没有加缩进实际生产用的话可以关闭缩进json.dump(coco_data, f, ensure_asciiFalse, separators(,, :))这样文件更小写入更快。写在最后的一点经验这套转换脚本我后来反复跑了很多次也应用到其他数据集上最深的体会是格式转换本身不复杂难的是把源数据里的“脏东西”处理干净。VisDrone的忽略区域、缺失标注、空目录、非标准类别ID这些才是真正需要花时间的地方。如果你只是临时用一次可以直接拿上面的代码改路径如果打算长期维护建议把过滤规则、类别映射全部做成配置项别写死在函数里。数据转换是训练pipeline的第一步这一步不稳后面所有实验都得跟着返工。本文还有配套的精品资源点击获取