资讯动态

VisDrone2019转COCO格式:目标检测数据集转换与标注清洗实战

发布时间:2026/10/17 7:06:38 来源:尧图企业网站定制
1. 数据集转换这件事到底卡在哪做目标检测的朋友应该都有过这种经历手里攥着一个很合适的公开数据集论文里写得清清楚楚、效果榜上明明白白结果下载下来一看标注格式当场就想把压缩包删了。VisDrone2019就是典型代表它是无人机视角目标检测领域的标杆数据集覆盖车辆、行人、自行车、三轮车等类别场景从城市道路到乡村田野都有拍摄高度和角度也很多样拿来当训练集非常合适。但问题在于它的标注文件是纯文本格式每一行长这样bbox_left, bbox_top, bbox_width, bbox_height, score, category, truncation, occlusion而主流检测框架比如Detectron2、MMDetection、YOLOv5的某些变体普遍依赖COCO格式的JSON标注。所谓COCO格式就是把所有信息——图片路径、宽高、标注框、类别ID——统一塞进一个结构化的JSON文件里用images、annotations、categories三个大数组来描述整个数据集。VisDrone的文本格式和COCO的JSON结构完全是两套语言训练脚本读不了迁移学习更无从谈起。我之前帮某导师做无人机视角的车辆检测实验第一步就卡在这个转换上。当时网上能找到的转换脚本要么是半成品要么依赖特定版本库跑起来各种报错。后来我把完整流程走了一遍整理出一套自己用了很顺的转换方案这次就把整个思路、细节、采坑过程全部写出来希望能帮你少走弯路。适合谁看两种情况一是你准备在VisDrone2019上做实验或者做预训练二是你手里有其他类似的TXT标注数据集想统一转成COCO格式。不管是哪种这篇都能给你一个可以直接落地的操作路径。2. 转换前的准备工作理清结构和关键参数2.1 先搞懂VisDrone2019的文件结构VisDrone2019数据集下载下来之后目录结构大概是这样的VisDrone2019-DET-train/ ├── annotations/ │ ├── 0000001_00005_d_0000005.txt │ ├── 0000001_00005_d_0000010.txt │ └── ... ├── images/ │ ├── 0000001_00005_d_0000005.jpg │ ├── 0000001_00005_d_0000010.jpg │ └── ... └── sequences/关键就是annotations和images这两个文件夹。名称里的同一串编号就是图片和标注文件的对应关系。标注文件里每一行各字段的含义和取值下面这个表格说得很清楚字段含义取值范围bbox_left框左上角x坐标0~图片宽度bbox_top框左上角y坐标0~图片高度bbox_width框宽度0bbox_height框高度0score检测置信度训练集里通常是0或10或1category类别ID0~11truncation截断程度0~2occlusion遮挡程度0~3训练集里score这一列一般是0或11表示该框质量可靠0表示这个框可能有标注质量问题。实际转换时大家通常会直接用score 0作为过滤条件把不可靠的框扔掉这是一个很常见的处理方式。2.2 VisDrone的类别ID和COCO类别ID怎么对应VisDrone的类别编号是0~11一共12类但实际使用中很多类别不参与检测。它的类别映射如下0: ignored 1: pedestrian 2: people 3: bicycle 4: car 5: van 6: truck 7: tricycle 8: awning-tricycle 9: bus 10: motor 11: others其中0ignored和11others通常会被过滤掉。ignored代表标注人员明确标记为不参与训练的干扰目标比如极小目标、模糊目标others则是什么都算的兜底类别训练时加入这两个类别只会增加噪声。所以转换时我一般只保留1~10类并在生成的categories字段里建立新的从1开始的连续ID。这里有一个新手特别容易踩的坑不能直接把VisDrone的category值当作COCO的category_id填进去。COCO格式要求category_id是categories列表中类别的索引一般是1开始连续编号。如果你直接拿VisDrone原始的类别号当category_id最后训练脚本读出来的类别就全乱了。转换时要么新建一个映射字典要么动态构建categories列表总之不能偷懒。2.3 过滤逻辑和数据清洗的取舍VisDrone的标注质量虽然整体不错但确实存在一些边界情况需要过滤。我常用的过滤规则有这几条score 0的框全部丢弃category 0或category 11的框丢弃宽或高小于某个阈值的框丢弃具体阈值看你的任务一般设2像素或4像素坐标越界的框裁剪到图片边界内。有些人会问宽高阈值设太小会不会把有效目标丢掉我的经验是无人机视角下的小目标很多如果阈值设得太大小目标检测的实验结果会失真。设2像素是一个比较保守又安全的做法既能过滤掉明显的标注噪声又不会误伤真实目标。2.4 需要的软件环境整个转换过程我用的都是Python不需要额外下载重型框架只需要以下几个库pip install opencv-python pillow tqdmPillow用来读取图片尺寸tqdm用来显示进度条OpenCV在这件事里其实不是必需的但后面如果你要顺便做可视化验证用它画框很方便。Python版本建议3.7以上太低的话字典合并和类型标注的写法可能不顺手。3. 转换脚本的核心设计思路3.1 为什么选择逐图处理而不是全局构建COCO格式的JSON结构其实不复杂核心就是三个列表{ images: [ {file_name: xxx.jpg, height: 1080, width: 1920, id: 1} ], annotations: [ {id: 1, image_id: 1, category_id: 4, bbox: [x, y, w, h], area: 10000, iscrowd: 0} ], categories: [ {id: 1, name: pedestrian} ] }注意三个列表之间的关联关系annotations里的image_id对应images里的idcategory_id对应categories里的id。转换脚本的核心任务就是按顺序遍历每张图片把txt标注解析成bbox然后填充到三个列表中。这里有一个容易忽略的地方area字段必须计算并填充。虽然很多简单脚本会直接写成-1或者漏掉但严谨的COCO格式要求面积存在有些训练框架会用它做小目标/中目标/大目标的统计分桶如果缺失或错误会影响后续评估。另外iscrowd字段在VisDrone数据集里没有对应的标注信息统一填0即可。3.2 图片ID和标注ID的生成策略图片ID我用的是从1开始的递增整数顺序就是遍历图片文件夹的顺序。这样虽然简单但有个隐含问题如果训练集和验证集分开转换两边都会从1开始编号到时候如果把多个数据集的JSON合并训练ID就会冲突。解决方案有两个在转换时给每个数据集加一个偏移量比如训练集从1开始验证集从100000开始转换完成后用脚本统一重新编号。我更推荐第一种方案因为一次到位后面省心。具体做法就是在脚本里加一个image_offset参数转换不同数据集时传入不同的基准值。标注ID同理从1开始递增即可因为标注ID只需要全局唯一不需要与图片ID有任何关联。3.3 坐标精度的处理方案VisDrone的原始坐标是整数但COCO格式中bbox的坐标可以是浮点数。某些检测框架会用浮点坐标做精细的边界对齐但这里只有一个原则原样保留不要自己加偏移。我在实际转换中遇到过一个问题有人会在转换时顺手把x轴坐标加1理由是COCO的bbox是左上角坐标而VisDrone的也是左上角坐标没毛病。但有些人可能把txt里的坐标误当成中心点坐标处理转换出来框全部偏了半个身位。转换前先搞清楚每个数据集的坐标定义别想当然。4. 完整可复用的转换脚本4.1 主脚本代码下面这个脚本是我实际在用的版本可以直接复制修改使用import os import json import glob from PIL import Image from tqdm import tqdm # 类别映射只保留 1~10忽略 0 和 11 VISDRONE_CATEGORIES { 1: pedestrian, 2: people, 3: bicycle, 4: car, 5: van, 6: truck, 7: tricycle, 8: awning-tricycle, 9: bus, 10: motor } def convert_visdrone_to_coco( images_dir, annotations_dir, output_json, image_offset0, ann_offset0, min_side2 ): # 按文件名排序保证顺序稳定 img_files sorted(glob.glob(os.path.join(images_dir, *.jpg))) coco_images [] coco_annotations [] coco_categories [ {id: idx, name: name, supercategory: object} for idx, name in enumerate(VISDRONE_CATEGORIES.values(), start1) ] ann_id ann_offset 1 for img_idx, img_path in enumerate(tqdm(img_files, descProcessing images)): img_id img_idx 1 image_offset # 读取图片尺寸 with Image.open(img_path) as img: width, height img.size # 构建 image 条目 coco_images.append({ file_name: os.path.basename(img_path), height: height, width: width, id: img_id }) # 找到对应的标注文件 base_name os.path.splitext(os.path.basename(img_path))[0] ann_path os.path.join(annotations_dir, base_name .txt) if not os.path.exists(ann_path): continue with open(ann_path, r, encodingutf-8) as f: lines f.readlines() for line in lines: parts line.strip().split(,) if len(parts) 8: continue bbox_left float(parts[0]) bbox_top float(parts[1]) bbox_width float(parts[2]) bbox_height float(parts[3]) score float(parts[4]) category int(parts[5]) truncation int(parts[6]) occlusion int(parts[7]) # 过滤条件 if score 0: continue if category not in VISDRONE_CATEGORIES: continue if bbox_width min_side or bbox_height min_side: continue # 裁剪越界坐标 x1 max(0, bbox_left) y1 max(0, bbox_top) x2 min(width, bbox_left bbox_width) y2 min(height, bbox_top bbox_height) if x2 x1 or y2 y1: continue final_w x2 - x1 final_h y2 - y1 coco_annotations.append({ id: ann_id, image_id: img_id, category_id: category, bbox: [x1, y1, final_w, final_h], area: final_w * final_h, iscrowd: 0, truncation: truncation, occlusion: occlusion }) ann_id 1 coco_data { images: coco_images, annotations: coco_annotations, categories: coco_categories } with open(output_json, w, encodingutf-8) as f: json.dump(coco_data, f, ensure_asciiFalse) print(fDone! Images: {len(coco_images)}, Annotations: {len(coco_annotations)}) return coco_data if __name__ __main__: convert_visdrone_to_coco( images_dirVisDrone2019-DET-train/images, annotations_dirVisDrone2019-DET-train/annotations, output_jsonvisdrone_train_coco.json )4.2 代码中的几个设计细节说明脚本里有两个自定义字段truncation和occlusion这是我特意保留的。标准COCO格式里没有这两个字段但检测框架读JSON时只会按需要的键取值多余的键会被忽略所以加上不会出错。保留它们的好处是可以做后续分析比如统计遮挡程度对检测精度的影响。实测在MMDetection里这个JSON可以直接跑通多余的键没有任何副作用。类别映射这里要重点说一句我保留了原始的category值作为COCO的category_id比如car的类别ID是4那么在生成的COCO JSON中category_id也是4。这样能保证转换前后类别ID语义一致方便后续结果分析和映射回溯。如果你希望类别ID从1开始连续编号可以在VISDRONE_CATEGORIES构建时重新用enumerate生成一套新的对应关系然后对标注里的category_id做一次查表替换。4.3 可视化验证脚本转换完成之后一定要做可视化验证这一步千万别省略。光靠看数字看不出对齐问题画出来一眼就能发现坐标偏移、框大小异常这些毛病。验证脚本也很简单import cv2 import json import os import random with open(visdrone_train_coco.json, r, encodingutf-8) as f: coco_data json.load(f) # 建立 image_id 到 file_name 的映射 img_id_to_file {img[id]: img[file_name] for img in coco_data[images]} # 随机抽样10张图 sample_ids random.sample(list(img_id_to_file.keys()), 10) for img_id in sample_ids: img_path os.path.join(VisDrone2019-DET-train/images, img_id_to_file[img_id]) img cv2.imread(img_path) for ann in coco_data[annotations]: if ann[image_id] ! img_id: continue x, y, w, h [int(v) for v in ann[bbox]] cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(img, str(ann[category_id]), (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) # 保存结果 cv2.imwrite(fvis_{img_id}.jpg, img)跑完之后逐张检查框有没有明显偏移类别编号有没有张冠李戴小目标有没有被误删这一步花不了几分钟但能帮你避免训练到一半才发现数据有问题的大坑。5. 实操中的坑与解决方案5.1 文件名匹配问题VisDrone的图片文件名和标注文件名完全一致没有后缀差异但要注意某些压缩包解压后图片可能是.png格式标注还是.txt这时glob匹配*.jpg就会漏掉文件。解决方案是把图片格式匹配改成*.jpg和*.png都扫一遍。还有就是文件名排序问题建议用sorted()强制排序保证多次转换结果一致。5.2 标注文件为空或缺失VisDrone里存在部分图片没有对应标注文件或者标注文件是空的。脚本里已经用os.path.exists判断跳过但如果一张图有标注文件但内容是空的也需要跳过。上面的代码用continue处理了空文件的情况但严格来说应该对空文件做一个计数统计方便后续排查。5.3 内存占用失控VisDrone训练集图片接近6500张标注框总数接近40万个如果每张图片的像素数组都加载到内存里记忆体直接爆掉。解决办法很简单就是像我的脚本那样只读取图片的宽高不加载像素数据。Pillow的Image.open()配合img.size访问宽高时不会把整张图读入内存而是只解析文件头部信息所以效率很高。5.4 类别分布统计转换完之后我还习惯做一次类别分布统计看看每类有多少个框。这个步骤能帮你快速发现数据是否有严重的不平衡问题比如car类别可能有20万个框bicycle可能只有1万个那么训练时就需要考虑样本均衡策略。统计脚本就是在上面代码基础上加一个Counter而已非常简单。6. 验证与后续使用建议6.1 用一个最小样例跑通全流程正式转换整个数据集之前我强烈建议先做一个迷你测试随便选3~5张图生成一个小JSON跑一遍可视化脚本确认没问题了再玩大的。这一步看着多此一举实际能帮你省下大把排查时间。我之前有一次直接全量转换跑了十分钟结果发现所有框上下颠倒了最后排查下来是坐标原点定义理解错了浪费了整整一下午。迷你测试30秒就能发现这个问题。6.2 在检测框架里试跑一次训练JSON生成之后拿一个成熟检测框架比如MMDetection或者Detectron2把数据集配置指到这个JSON上随便跑几个iteration看看能否正常读取、loss能否正常计算。这一步是检验COCO格式正确性的最终标准。很多看起来格式正确的JSON真到了框架里就会因为某个字段缺失或者类型不对报错。反正我在MMDetection里跑通后才敢真正用它开始完整训练。6.3 小目标检测场景的额外建议VisDrone数据集最典型的特点是目标小、密度高、无人机视角。转成COCO格式之后如果打算做小目标检测建议看几个额外的配置参数。area字段要算对因为小目标检测的评估协议会按面积阈值分桶比如area 32*32算small32*32 ~ 96*96算medium 96*96算large。如果你的area算错了评估结果完全没有参考意义。许多开源框架内部也会用这个字段做过滤比如训练时忽略面积小于某个阈值的GT框省显存的同时避免模型被极小目标带偏。6.4 坐标系统的注意点这里再强调一次坐标系统的定义。VisDrone的bbox_left,bbox_top是框左上角坐标width和height是框的宽度和高度这跟COCO的bbox定义一致都是[x, y, width, height]格式。转换时不需要任何坐标变换。但如果你拿到的数据源是VOC格式那就要注意VOC里存的是[xmin, ymin, xmax, ymax]转换成COCO时需要用xmax-xmin计算宽高。6.5 数据划分策略VisDrone2019官方本身就分好了训练集、验证集和测试集但很多人会自己重新划分比如把训练集的10%抽出来当验证集调参用。转成COCO后不要直接改JSON而是用现成的划分工具或者自己写一个基于文件名的随机采样脚本。关键是划分完之后train和val两个JSON里不能有同一张图片ID不然训练时会出现诡异的过拟合现象。7. 脚本扩展思路7.1 增加多数据集合并能力如果你的实验需要把VisDrone和其他数据集混合训练比如加入CARPK或者UAV123的部分数据那就要考虑多数据集合并的问题。最简单的方法是分别转换各自成COCO格式然后写一个合并脚本把所有annotations拼接然后把images里的ID加上偏移重新映射。合并后的categories是一个大混合列表需要处理好不同数据集之间的类别ID冲突。7.2 增加类别筛选和重映射能力有时候你只关心car和pedestrian两类其他类别全部弃用。可以在转换时增加一个keep_categories参数传入想要保留的类别集合然后对保留的类别重新编号从1开始。这个能力在做专项任务时非常实用因为类别太多会稀释模型对特定类别的学习能力。我在转车辆检测数据时就是这么干的只留car、van、truck、bus四类。7.3 生成COCO的segmentation字段COCO格式的标注可以包含多边形精度的segmentation字段但VisDrone原始标注是矩形框如果你要做的实验涉及实例分割可以用矩形框近似构造一个四边形的segmentationsegmentation [[x, y, x w, y, x w, y h, x, y h]]这种方式虽然不能代表精确的实例轮廓但是对于很多基准实验来说是足够的。不过要说明矩形segmentation的精度有限做分割任务时还是建议标注数据本身是多边形格式。7.4 结合多尺度策略的思考VisDrone的图片尺寸统一是1920×1080部分子集略有差异但检测框架通常会把训练图片缩放到固定尺寸比如1333×800或者640×640。转换JSON时不需要做任何缩放操作因为COCO格式存的是原始像素坐标缩放逻辑交给训练框架在数据加载时处理。千万不能在转换时就缩放坐标否则后续使用就会两头出错框架按COCO坐标做增强而你给的已经是缩放后的坐标双重缩放直接爆框。8. 一些经验心得转换数据集这件事看起来只是一个格式翻译问题其实里面藏着不少细节。我在做某个无人机视角检测项目时光是这个转换环节就反复折腾了好几次。第一次是没过滤ignored类别训练出来的模型在真实场景里把一些杂背景物体识别成目标误检率很高。第二次是坐标裁剪逻辑写错了导致部分框出错可视化验证时一眼就看出来好在发现得早。第三次是类别ID映射搞混了训练时loss曲线看起来正常但评估mAP时结果完全对不上折腾了半天才发现是category_id错位。所以我的建议是转换脚本别急着写得多花哨先把最基础的流程跑通再用可视化验证一遍然后再逐步增加过滤规则和自定义功能。数据格式转换是最容易出看起来没问题但实际全错的环节宁可慢一点也要每一步都验证到位。这套流程我现在已经固化成标准操作了新拿到一个数据集先看标注格式文档再写转换脚本然后可视化抽查最后丢进框架跑一小段训练一套下来基本不会翻车。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑