ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VisDrone转COCO格式JSON:目标检测数据集标注转换实践与避坑指南

VisDrone转COCO格式JSON:目标检测数据集标注转换实践与避坑指南 简介面向目标检测与计算机视觉开发者这份资源包提供VisDrone数据集转出的COCO格式标注文件省去自行编写转换脚本的麻烦。转换过程基于官方VisDrone数据并剔除10个存在错误标注的训练集图片保障标注质量同时已用YOLOX训练并测试通过也可导入百度EasyDL进一步复核适合用于YOLO系列、MMDetection等框架的模型训练。资源共4个文件包含两个JSON标注文件、一个txt标签说明和一个jpg图片放置示意图压缩包整体仅10.18MB下载后可快速接入训练流程。目前已有765人学习下载。JSON文件遵循官方COCO标准结构分别对应训练集与验证集标注txt文件列出类别标签jpg示意图则直观展示图片目录摆放方式便于用户从官网下载原图后直接对应使用大幅降低数据准备门槛。1. 从VisDrone的txt到COCO格式JSON转换前先想清楚的一件事跑无人机视角目标检测第一道坎往往不是模型选型而是数据格式。VisDrone这个数据集虽然覆盖场景多、标注量大但标注是它自家的一套txt格式而你想直接喂给Detectron2、MMDetection或者现成的COCO预训练权重时这些框架默认只认COCO那种JSON标注文件。于是“visdrone-coco格式json文件”就成了绕不开的一步把VisDrone的txt逐行解析成COCO的images、annotations、categories三层JSON结构。这个过程不需要改坐标算法却要在图片尺寸、类别id、空标注文件这些细节上反复踩坑。这篇笔记就是把我做过的转换方案和排错记录摊开来说适合刚把VisDrone下下来、卡在标注文件上的同学。2. 一行VisDrone txt如何变成COCO JSON两张格式表的映射关系转换前最忌直接写脚本因为两个格式的字段名差一个字后面训练时对不上号就麻烦了。先把两边都拆开看映射关系清楚了代码只是体力活。2.1 VisDrone的txt标注字段含义与类别编号VisDrone DET任务的每个txt文件与一张jpg图同名里面每行是一个目标整行就是8个逗号分隔的数字。我第一次打开时以为是预处理没做完后来查了一遍官方说明才确认这8个字段是固定的字段含义bbox_left目标框左上角x坐标bbox_top目标框左上角y坐标bbox_width框宽bbox_height框高score置信度GT文件里基本恒为1object_category目标类别id范围1~11truncation截断程度0/1/2occlusion遮挡程度0/1/2/3类别编号从1开始没有01是pedestrian2是people3是bicycle4是car5是van6是truck7是tricycle8是awning-tricycle9是bus10是motor11是others。这里注意people和pedestrian的区别前者是站着的一群人后者是单个行人很多转换教程直接把2类丢掉做行人检测没事做通用目标检测就会少一截数据。还有一点容易混淆网上不少地方说VisDrone的标注是10个字段。早期版本或经过预处理的txt确实可能在一行末尾多出两个数字比如ignore标记。我一般统一按前8个字段取多出来的直接忽略这样无论哪种版本都能兼容。score字段在GT里几乎全是1如果看到小于1的值那往往是该目标被标记为不参与训练转换时可以直接过滤掉。2.2 COCO JSON的固定结构images、annotations、categories三件套COCO格式虽然也是JSON但它不是把每张图的信息拍平存而是分成三个数组images存图片基本信息annotations存所有框和类别categories存类别id到类名的映射。一个最小的检测用JSON长这样{ images: [ {id: 1, file_name: 0000001_00001_d_0000001.jpg, width: 2000, height: 1500} ], annotations: [ {id: 1, image_id: 1, category_id: 4, bbox: [100, 200, 50, 80], area: 4000, iscrowd: 0} ], categories: [ {id: 4, name: car} ] }images里最关键的是id、file_name、width、heightannotations里最关键的是image_id、category_id、bbox、area、iscrowdcategories里只要id和name就能跑起来加一个supercategory字段会更保险因为个别校验工具会检查它是否存在。注意两个细节。一是annotations里的id是全数据集唯一的递增整数不是每张图从1重新计数二是segmentation字段在纯检测场景下可以省略或置空列表但很多人的习惯是保留一个空数组占位这样以后想从检测扩展到实例分割只需要回填多边形坐标。area字段官方定义是分割区域的像素面积做纯检测时大家都用w * h替代大多数框架并不严格校验这个值。2.3 八字段到JSON的映射哪些直接搬哪些必须处理现在把两个结构放一起比转换规则就一目了然了VisDrone字段COCO字段处理方式bbox_left/top/width/heightbbox直接搬浮点转intscore无丢弃GT里恒为1object_categorycategory_id保留VisDrone的1~11编号truncation无丢弃或作为过滤条件occlusion无丢弃或作为过滤条件无image_id转换时自己生成从1递增无area计算 w * h无iscrowd固定写0坐标本身不需要任何变换VisDrone的bbox和COCO的bbox都是[x, y, width, height]以左上角为原点这比VOC那种[xmin, ymin, xmax, ymax]省事多了。真正要处理的只有三件事图片宽高要从图片文件里读、类别编号不能动、空标注文件不能丢。3. 把VisDrone批量转成COCO格式JSON完整Python脚本与参数调法结构理清后转换脚本本身不复杂但有几个参数值得单独拿出来说因为改错一个后面训练全是黑匣子报错。3.1 转换前准备目录结构、依赖库和最小环境VisDrone2019-DET下载解压后是分任务的检测任务里train和val的目录结构一模一样VisDrone2019-DET-val/ ├── annotations/ │ ├── 0000001_00001_d_0000001.txt │ ├── 0000002_00001_d_0000002.txt │ └── ... └── images/ ├── 0000001_00001_d_0000001.jpg ├── 0000002_00001_d_0000002.jpg └── ...图片和txt是一一对应的文件名相同只是后缀不同。脚本只需要三个Python库json标准库、pathlib标准库、cv2读图片尺寸。cv2没有的话pip install opencv-python即可也可以用PIL但要注意cv2读取返回的shape顺序是(height, width)PIL返回的是(width, height)写错就废了。3.2 转换脚本从txt解析到JSON落盘下面这个脚本我一直在用包含路径遍历、txt解析、图片尺寸读取、空标注保留、类别编号保持这五个关键点。import json import cv2 from pathlib import Path # VisDrone-DET 标注类别: id - name注意id从1开始 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 parse_visdrone_txt(txt_path): 解析VisDrone的txt标注返回bbox列表和类别列表 boxes, cats [], [] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split(,) if len(parts) 8: continue # VisDrone DET每行是8个字段少数版本多出字段只取前8个 x, y, w, h (float(parts[i]) for i in range(4)) score float(parts[4]) cat int(parts[5]) if score 1.0: # GT里score恒为1小于1一般是被忽略的框 continue if w 0 or h 0: # 宽高非正的退化框直接跳过 continue boxes.append([int(x), int(y), int(w), int(h)]) cats.append(cat) return boxes, cats def visdrone_to_coco(img_dir, ann_dir, output_json): img_dir Path(img_dir) ann_dir Path(ann_dir) images, annotations [], [] ann_id 1 # 以txt为准遍历保证图片和标注一一对应 for txt_path in sorted(ann_dir.glob(*.txt)): img_name txt_path.stem .jpg img_path img_dir / img_name if not img_path.exists(): print(f[warn] 缺少图片: {img_path}) continue img cv2.imread(str(img_path)) if img is None: print(f[warn] 图片读取失败: {img_path}) continue h_img, w_img img.shape[:2] # cv2返回的是 height, width boxes, cats parse_visdrone_txt(txt_path) image_id len(images) 1 images.append({ id: image_id, file_name: img_name, # 只存文件名不存绝对路径 width: w_img, height: h_img, }) # 空标注也要保留图片记录annotations留空 for bbox, cat in zip(boxes, cats): bw, bh bbox[2], bbox[3] annotations.append({ id: ann_id, image_id: image_id, category_id: cat, # 保持VisDrone的1~11编号 bbox: bbox, # COCO格式: [x, y, width, height] area: bw * bh, iscrowd: 0, segmentation: [], }) ann_id 1 categories [ {id: cid, name: name, supercategory: none} for cid, name in VISDRONE_CATEGORIES.items() ] coco_json { info: {description: VisDrone converted to COCO format}, licenses: [], images: images, annotations: annotations, categories: categories, } with open(output_json, w, encodingutf-8) as f: json.dump(coco_json, f, ensure_asciiFalse, indent1) print(fimages: {len(images)}, annotations: {len(annotations)}) if __name__ __main__: visdrone_to_coco( VisDrone2019-DET-val/images, VisDrone2019-DET-val/annotations, visdrone_val_coco.json, )3.3 代码逻辑与参数说明解析txt时用parts[i]逐个取值而不是直接解包是为了兼容10字段的版本多出来的部分自然忽略。score小于1的框过滤掉这个阈值可以按需调但在GT里几乎用不到。图片尺寸必须从图片文件读VisDrone的txt里没有宽高信息。这里用cv2.imread读一遍是比较慢的val集296张图、train集几千张图总体也就几分钟可以接受。如果嫌慢有一个取巧方案是先读前10张图确认尺寸一致然后统一用同一对宽高但我不建议这么干VisDrone里确实存在不同分辨率的图片省这几分钟后面训练时框全歪掉更亏。file_name只存文件名不带目录是刻意为之。这样换机器、换数据集根目录都不用改JSON训练框架里通常有img_prefix之类的参数用来拼完整路径。如果你喜欢把路径存成images/0000001_00001_d_0000001.jpg这种带一级目录的相对路径也可以关键是不要存绝对路径。3.4 只想要部分类别过滤逻辑怎么改很多项目不需要全部11类比如只做车辆检测时通常只留car、van、bus、truck。在调用处加一段过滤KEEP_CATEGORIES {4, 5, 6, 9} # car, van, truck, bus boxes, cats parse_visdrone_txt(txt_path) keep_boxes, keep_cats [], [] for bbox, cat in zip(boxes, cats): if cat in KEEP_CATEGORIES: keep_boxes.append(bbox) keep_cats.append(cat)这里有两种类别编号策略。一种是保留原始编号JSON里category_id还是4、5、6、9categories数组只留这四个好处是不用改后续模型配置另一种是重新编号为1、2、3、4好处是类别索引连续适合作为新数据集从头训练。我一般用第一种因为不需要额外维护映射关系框架里classes列表写成[car, van, truck, bus]category_id4正好是索引3不会错位。注意重新编号时一定要在过滤之后再编否则会出现category_id断档某些框架不报错但训练会出负样本不匹配的问题。过滤阈值和保留类别的定义放在脚本顶部方便每次改。4. 转换后的JSON能不能直接用三类自检脚本先验证再训练转换脚本能跑通不代表JSON能用。我见过太多直接拿去训练等loss曲线或mAP异常了才回来查标注文件的情况。训练一次成本不小花五分钟做三层校验能把大部分格式问题挡在训练门外。4.1 基本合法性校验image_id、category_id、bbox越界第一个校验完全不依赖第三方库纯Python就能做检查的是数据间的引用关系是否自洽。import json from pathlib import Path def check_coco(json_path, image_base_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) images {img[id]: img for img in data[images]} cats {c[id]: c[name] for c in data[categories]} for ann in data[annotations]: assert ann[image_id] in images, fannotation {ann[id]} 指向不存在的图片 assert ann[category_id] in cats, fannotation {ann[id]} 类别不存在 x, y, w, h ann[bbox] assert w 0 and h 0, fannotation {ann[id]} bbox非正: {ann[bbox]} img_w images[ann[image_id]][width] img_h images[ann[image_id]][height] # 允许轻微越界但严重越界说明坐标解析有问题 if x w img_w * 1.5 or y h img_h * 1.5: print(f[warn] 框严重越界: image_id{ann[image_id]}, bbox{ann[bbox]}) for img in data[images]: p Path(image_base_dir) / img[file_name] if not p.exists(): print(f[warn] 图片不存在: {p}) print(f检查通过: {len(data[images])} 张图片, {len(data[annotations])} 个标注)这个脚本把annotation里的image_id去images字典里查查不到就说明转换时出现了孤儿标注bbox宽高非正说明txt解析出了问题常见原因是以逗号分割时混入了空字符串严重越界的框说明坐标单位或者解析顺序错了。图片路径检查直接传转换时用的数据集根目录确保file_name拼出来的路径真实存在。4.2 用pycocotools重新加载一遍统计类别和样本数纯脚本检查的是结构pycocotools则是用COCO官方的加载器再做一遍校验。这一步能发现一些结构检查看不到的问题比如id重复、annotations顺序异常。from pycocotools.coco import COCO coco COCO(visdrone_val_coco.json) print(类别id:, coco.getCatIds()) print(图片数:, len(coco.getImgIds())) for cat_id in coco.getCatIds(): ann_ids coco.getAnnIds(catIdscat_id) print(f类别 {cat_id}: {len(ann_ids)} 个标注)pycocotools加载时会自动检查image_id和category_id的引用关系如果JSON里有指向不存在的类别或图片会直接在COCO()构造时报错而不会等到训练。另一个有用的检查是按类别统计标注数量可以一眼看出某一类是不是因为过滤条件写错变成0个了。Windows下如果pip install pycocotools编译失败可以换用4.1里的纯Python校验效果接近。4.3 画框抽查随机抽几张图肉眼比对标注结构校验通过只能说明JSON内部自洽不能说明标注数据是对的。最直观的验证是把框画到原图上随机抽几张肉眼看一遍。import cv2 import json import random from pathlib import Path BASE_DIR Path(VisDrone2019-DET-val/images) with open(visdrone_val_coco.json, r, encodingutf-8) as f: data json.load(f) anns_by_img {} for ann in data[annotations]: anns_by_img.setdefault(ann[image_id], []).append(ann) for img in random.sample(data[images], 5): im cv2.imread(str(BASE_DIR / img[file_name])) for ann in anns_by_img.get(img[id], []): x, y, w, h ann[bbox] cv2.rectangle(im, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(im, str(ann[category_id]), (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(fcheck_{img[id]}.jpg, im)画框时顺便在框左上角把category_id打出来这样类别编号和颜色区分都能一次验证。这一步能发现两类问题一是框整体偏移比如横坐标和纵坐标写反了这在无人机俯视图的小目标上特别常见二是画出来的框超出图片边界很多说明图片尺寸字段写错了训练框架会把图片裁掉或拉伸标注跟着错位。5. VisDrone转COCO格式JSON的避坑记录五个现场翻车与修复这些坑不是从文档里看来的是我自己转完几轮后一条条记下来的。每条按现象、原因、解决三段写遇到类似报错可以直接对照排查。5.1 图片没读出来整个转换安静地少了一批图现象转换脚本跑完打印的images数量比annotations目录下的jpg数量少了几百张但没有任何报错。原因cv2.imread对某些图片返回了None常见原因是图片路径中包含中文或者图片文件本身损坏。脚本里直接img.shape[:2]取尺寸None类型会抛AttributeError但如果你用的PIL或者先检查存在性再读取可能就直接跳过了。解决读取后立即判断if img is None: continue并打印警告。千万不要用Path.exists()替代读取验证因为文件存在不等于能解码。脚本里加上这个判断后会明确输出哪些图读失败再单独处理这些文件。5.2 json.dump报错Object of type int64 is not JSON serializable现象转换脚本跑到最后一步json.dump时抛TypeError提示int64或int32不是JSON可序列化类型。原因txt解析出来的数字是Python原生int和float本身没有问题但如果你后续用numpy处理了bbox数组比如np.array(boxes).astype(int)数组里的元素就成了numpy的int64标准json库不认。解决在构造annotations时对每个字段显式转类型int(x)、int(w)这样包一层。或者懒一点在json.dump里加defaultstr兜底但这个方案会破坏数字类型训练时还得再做一次解析。我一般两个都不省解析时转int最后序列化保持默认。5.3 类别对不上mAP低得离谱混淆矩阵全乱现象模型训练正常收敛但验证时每个类别的精度都低混淆矩阵的主对角线全是乱的。原因转换时把category_id做了减1处理想模仿COCO那种从0开始的编号习惯。VisDrone里类别1是pedestrian类别11是others减1之后others变成10类别编号和classes配置列表的索引发生错位框架里的第11个类是配置里的第11个名字实际却读到了others的标注。解决COCO JSON里的category_id保持VisDrone原始编号1~11不要在转换阶段重编号。后续框架的classes列表按这个顺序配置如果你确实需要从0开始的编号在模型配置的dataset_info里做映射而不是改JSON文件。5.4 换台机器就报FileNotFoundErrorfile_name里藏着绝对路径现象转换机器上训练正常代码和数据都拷到另一台服务器后训练一开始就报找不到图片。原因转换时file_name写成了类似/home/user/data/VisDrone2019-DET-val/images/xxx.jpg的绝对路径。切到新机器后路径前缀变了JSON里的绝对路径指向了不存在的目录而其他图片是正常的所以报错只出现在特定图中。解决JSON里只存相对路径我一般只存文件名本身由训练配置里的img_prefix参数拼完整路径。这样数据和代码可以整体移动不受机器目录结构影响。检查方法很简单打开JSON看file_name如果以/开头或包含盘符就已经踩坑了。5.5 空标注图片被丢掉评估时图像数量对不上现象训练集数量看起来比原始数据集少验证集的mAP和官方baseline差距巨大或者eval阶段报图片数量不一致。原因转换脚本遇到空txt或只有几行且全被score过滤掉的txt时直接continue跳过了这张图。VisDrone里确实存在没有目标的图片尤其是那些纯地面或纯天空的负样本。解决空标注也要写入imagesannotations留空数组保证每张图都有对应的image记录。另外如果整张图的标注全被score小于1过滤掉效果等同空标注也要注意保留。这直接影响后面做结果评估时的图片数量对齐。顺带一提转换完想打开JSON看内容几十MB的文件用记事本打开会卡死或乱码用VS Code或命令行python -m json.tool visdrone_val_coco.json做格式化查看都比记事本靠谱。6. 把它做成一个能反复用的转换小工具命令行参数与可视化抽查转换脚本写好后下一步是把它封装成命令行工具不然每换一个数据集目录就要改一次代码。加一个argparse入口是最快的做法import argparse if __name__ __main__: parser argparse.ArgumentParser(descriptionVisDrone to COCO JSON) parser.add_argument(--img-dir, requiredTrue, helpimages目录) parser.add_argument(--ann-dir, requiredTrue, helpannotations目录) parser.add_argument(--output, defaultcoco_annotations.json) parser.add_argument(--filter, default, help要保留的类别id逗号分隔如 4,5,6) args parser.parse_args() if args.filter: KEEP_CATEGORIES {int(x) for x in args.filter.split(,) if x.strip()} visdrone_to_coco(args.img_dir, args.ann_dir, args.output)调用时就可以直接传之前那套目录结构输出文件名也支持自定义。filter参数不传就保留全部11类传了就在解析时做过滤不用每次改代码。我不会因为转换脚本跑通就立刻把数据丢给训练。固定的流程是先跑一遍第4章的合法性校验确认没有任何warn输出再随机抽10张图画框用肉眼扫一遍框是不是紧贴目标边缘、类别编号是不是对应确认无误才启动训练。这一步看起来土但能挡掉很多训练时说不清的玄学问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表