COCO数据集提取指南:下载、解析标注与按类生成子集的完整流程
做目标检测和实例分割的朋友基本都绕不开一个坎COCO数据集。很多人一开始都想着“直接上完整的COCO”结果一看硬盘——训练集图片18GB加上标注文件解压后又是几十GB直接劝退。更现实的问题是很多场景下你根本用不到80个类别比如只想做人、车、猫、狗这几个类那下载完整数据集纯属浪费带宽和磁盘空间。所以这篇东西就干一件事讲清楚COCO数据集怎么下、下哪些文件以及如何用最简单的方式从里面提取出你真正需要的类别生成一份精简可用的子数据集。这篇文章适合三类人看第一次接触COCO、被数据体积吓住的新手已经在用COCO但每次都要手工筛数据、想找个稳定方案的老手以及准备用自定义类别训练模型、需要快速构造数据集的开发者。我会把从下载到提取的完整流程、背后的原理和踩过的坑都写出来代码直接可用。1. 先弄明白COCO数据集到底是个什么结构1.1 COCO为什么这么流行COCOCommon Objects in Context能成为目标检测领域的默认基准不只是因为它图片多更关键的是它的标注质量和任务覆盖。2017版本是现在最常用的版本包含训练集train201711.8万张图、验证集val20175000张图、测试集test20174万张图覆盖80个物体类别。每张图的标注信息包含物体边界框bbox、分割掩码segmentation、关键点keypoints针对人、密集描述captions等。对于做检测任务的人来说最核心的就是bbox和segmentation这两类标注。这套数据的价值在于它定义了一个统一的评测标准不管你在哪个框架下训练Detectron2、MMDetection、YOLO系最终都要对齐COCO的类别定义和评估指标mAP、AP0.5:0.95等。所以做工程的人哪怕最终业务只有两三个类别也通常先用COCO的预训练权重做迁移学习再用自定义数据集微调。这时候你手上不需要完整的COCO但你手上一开始准备用来微调的数据经常就是从COCO里“切一刀”出来的。1.2 数据文件与标注文件的对应关系COCO 2017的下载页面把文件分得很清楚核心文件就几个文件大小用途train2017.zip18GB训练集原始图片val2017.zip1GB验证集原始图片test2017.zip6GB测试集图片无公开标注annotations_trainval2017.zip873MB训练集验证集的全部标注文件stuff_annotations_trainval2017.zip约1.2GBstuff类别物体背景标注全景分割用这几个文件的命名逻辑是图片按年份和用途分目录标注文件统一放在一个压缩包里。很多人第一次下会犯一个错误——把test2017.zip也下载了结果发现测试集的标注根本不会公开评估要提交到官方服务器本地训练完全用不上。如果你只是做训练和本地验证只需要下载train2017.zip、val2017.zip和annotations_trainval2017.zip。这样下来省掉至少6GB流量。标注文件解压后得到一个annotations文件夹里面最常用的四个文件是instances_train2017.json训练集的目标检测/实例分割标注instances_val2017.json验证集的目标检测/实例分割标注captions_train2017.json/captions_val2017.json图片文本描述标注person_keypoints_train2017.json/person_keypoints_val2017.json人体关键点标注我们做类别提取操作对象就是instances_*.json这两个文件。2. 下载的实操方法别再傻等浏览器了2.1 下载组合与命令行方法如果直接在浏览器下载18GB的压缩包中断一次就心态崩了。推荐的方式是用命令行工具做断点续传下载。Linux/macOS直接用wgetWindows下建议用aria2c或者IDM。先建一个工作目录比如D:\coco_data按下面的命令拉取# 训练集图片 wget -c http://images.cocodataset.org/zips/train2017.zip # 验证集图片 wget -c http://images.cocodataset.org/zips/val2017.zip # 标注文件 wget -c http://images.cocodataset.org/annotations/annotations_trainval2017.zip-c参数的作用是支持断点续传。如果下载一半网络断了重新执行同一条命令它会从断点继续而不是从头开始。提示如果你在国内网络环境下下载速度很慢可以使用支持多线程的工具比如aria2c -x 16 -s 16将文件分成16段并行下载实测速度会明显改善。不要用浏览器下载大文件浏览器断线之后的恢复机制不够稳定。2.2 下载后的校验与解压下载完成后不要急着解压先确认一下文件完整性。官方没有提供MD5校验值但zip文件本身自带CRC校验解压时如果文件损坏会自动报错。保险起见可以看下文件大小是否和官网页面对应ls -lh *.zip正常的体积应该是 train2017.zip 约18GBval2017.zip 约1GBannotations_trainval2017.zip 约873MB。如果差得太多说明下载不完整需要重新下。解压同样推荐用命令行Windows用户用tar -xfWin10 1803自带或者直接右键解压。Linux/macOSunzip train2017.zip -d . unzip val2017.zip -d . unzip annotations_trainval2017.zip -d .最终你会得到train2017/、val2017/、annotations/三个目录。如果你不打算用COCO原始图片而是只要标注做数据处理甚至可以不下载图片压缩包直接用标注文件做类别过滤和统计之后再按需复制图片。我个人的建议是第一次操作时先用val2017那一套小文件1GB图片注释把流程跑通再决定要不要下18GB的train2017。很多人在大文件上下载半天结果代码写出来跑不通排查起来特别痛苦。用小数据验证逻辑大文件只做一次拷贝这个顺序能省很多时间。3. 读懂instances标注文件的JSON结构这是提取的钥匙3.1 五段式结构instances_train2017.json是一个大约600多MB的大JSON文件不好直接打开看。它的顶层结构包含五个字段info数据集说明、版本信息licenses图片版权信息images所有图片的列表每个元素包含id、file_name、width、height、license等annotations所有标注的列表每个元素是一个目标物体的标注categories类别列表80个类真正需要建立联系的是后面三个字段。先看categories的结构{ id: 1, name: person, supercategory: person }每个类别有一个唯一的数字id1就是 person2是 bicycle一直到90。注意这里的id不是从0开始连续排的COCO的80个类的id范围是1到90中间跳过了很多数字。这一点非常常见很多新手写代码时想当然地认为“类别0是person”结果全错。再来看images{ id: 397133, license: 4, file_name: 000000397133.jpg, height: 427, width: 640, date_captured: 2013-11-14 17:02:52 }id是图片的唯一标识file_name用于在train2017/目录下找到实际图片文件。annotations是重头戏每个目标物体的标注长这样{ id: 100567, image_id: 397133, category_id: 1, bbox: [264.31, 95.82, 370.57, 273.69], area: 104742.24, segmentation: [...], iscrowd: 0 }image_id和categories里的id是两张关键的“外键表”通过image_id找到图片通过category_id找到类别。整个提取过程本质就是在做按category_id筛选annotations再反向找出唯一涉及的image_id集合。3.2 三个字段的join逻辑我用大白话描述一次这个关系annotations.category_id是categories.id的外键它告诉我们这个框框属于哪个类别。annotations.image_id是images.id的外键它告诉我们这个框框出现在了哪张图片里。一张图片可能包含多个类别比如一张街景同时有person、car、traffic light所以在做类别提取的时候一张图片如果同时包含目标类别和非目标类别那么只要图片中有一个框框属于你的目标类别这张图片就要保留但非目标类别的框框要不要保留取决于你的策略。这里就引出提取方案的两种策略策略A按图像保留image-level filtering。只要图片里出现了目标类别就保留整张图和它的全部标注。好处是保留了目标类别物体周围的上下文信息坏处是标注文件里会混入非目标类别的标注。策略B按标注保留annotation-level filtering。只保留目标类别的标注记录同一张图片中非目标类别的标注全部丢弃图片仍然保留。好处是训练时不会让模型被无关类别的标注干扰坏处是如果目标类别在图片里只占一个很小区域其他区域被当作背景可能带来训练噪声。大多数场景我用策略B尤其在用检测框架时你更希望模型专注于你定义的类别而不是让它在训练集中看到一些“没有标签的物体”因为非目标类别的物体在标注里被删了但在图像中依然可见模型会忽略它们。不过这里也会有一个副作用对于那些没被删的图片其上下文信息可能变少。所以如果你只是做预训练微调策略B是主流。4. 手写一个提取特定类别的脚本4.1 为什么不直接装pycocotools来处理一定有人问COCO官方提供了pycocotools工具库直接用loadRes或者getCatIds不就行了可以用但有几个问题第一pycocotools在某些环境中编译安装比较麻烦尤其是Windows下需要Cython和Visual C构建工具第二针对“提取子集并生成新标注文件”这个需求pycocotools并没有直接提供API你仍然要自己写过滤逻辑第三纯Python脚本很容易调试稳定性也高。所以我的做法是不依赖pycocotools只用Python标准库的json和shutil。这样在任何环境都能跑逻辑也是透明的。4.2 核心代码按类别过滤并生成新标注文件下面这个脚本做三件事读取原始标注JSON筛选目标类别对应的标注生成新的JSON文件并把涉及到的图片复制到新目录。以提取person人、car小汽车、motorcycle摩托车三个类为例import json import os import shutil from collections import defaultdict # 配置区域 COCO_JSON_PATH annotations/instances_val2017.json # 原始标注文件 IMAGE_SRC_DIR val2017 # 原始图片目录 OUTPUT_JSON annotations/instances_val2017_person_car_moto.json # 新标注文件 OUTPUT_IMAGE_DIR val2017_person_car_moto # 新图片目录 TARGET_CATEGORY_NAMES [person, car, motorcycle] # 目标类别名 # with open(COCO_JSON_PATH, r, encodingutf-8) as f: coco json.load(f) # 第一步建立类别名 - 类别ID映射 cat_id_to_name {cat[id]: cat[name] for cat in coco[categories]} name_to_cat_id {cat[name]: cat[id] for cat in coco[categories]} target_ids set() for name in TARGET_CATEGORY_NAMES: if name not in name_to_cat_id: print(f警告类别 {name} 不存在忽略) continue target_ids.add(name_to_cat_id[name]) print(目标类别ID, target_ids) # 第二步筛选标注 filtered_annotations [] annotated_image_ids set() for ann in coco[annotations]: if ann[category_id] in target_ids: filtered_annotations.append(ann) annotated_image_ids.add(ann[image_id]) # 第三步筛选图片信息 filtered_images [img for img in coco[images] if img[id] in annotated_image_ids] # 第四步只保留目标类别的categories定义 filtered_categories [cat for cat in coco[categories] if cat[id] in target_ids] # 构建新的JSON new_coco { info: coco.get(info, {}), licenses: coco.get(licenses, []), images: filtered_images, annotations: filtered_annotations, categories: filtered_categories } # 写文件 os.makedirs(os.path.dirname(OUTPUT_JSON), exist_okTrue) with open(OUTPUT_JSON, w, encodingutf-8) as f: json.dump(new_coco, f, ensure_asciiFalse) # 第五步复制图片到新目录可选 os.makedirs(OUTPUT_IMAGE_DIR, exist_okTrue) image_id_to_info {img[id]: img for img in coco[images]} for img_id in annotated_image_ids: img_info image_id_to_info[img_id] src_path os.path.join(IMAGE_SRC_DIR, img_info[file_name]) dst_path os.path.join(OUTPUT_IMAGE_DIR, img_info[file_name]) if os.path.exists(src_path): shutil.copy(src_path, dst_path) else: print(f警告源图片不存在 {src_path}) # 第六步输出统计信息 print(f原始标注总数{len(coco[annotations])}) print(f筛选后标注总数{len(filtered_annotations)}) print(f筛选后图片总数{len(filtered_images)}) print(f涉及类别{[cat[name] for cat in filtered_categories]})脚本流程不复杂但有几处细节刻意处理过值得说明。关于cat_id_to_name和name_to_cat_id这里先把categories列表转成一个映射字典是为了后面大量查询时不用反复遍历列表。COCO的annotations数量在25万条以上遍历一次就好不要在循环里嵌套遍历categories。关于annotated_image_ids用Set去重同样的image_id可能在多个目标类别标注里出现多次用Set可以天然去重后续处理图片列表时也不会重复。关于新JSON中的categories有些人做提取时不会改categories直接把80个类全部保留只过滤了图片和标注。这在少数框架里也能用但多数框架会要求标注文件里的category定义与实际标注记录一致否则会出现训练时类别索引错乱的问题。4.3 如何验证提取结果是正确的跑完脚本之后不能只看“完成”两个字就收工。我通常做三个检查第一检查统计数字是否合理。比如从instances_val2017.json提取person类别一张图里可能出现多个人所以标注数一定大于等于图片数。如果发现标注数等于图片数先别高兴说明大概率过滤条件太严格把很多标注漏掉了。第二抽查图片和标注是否对得上。可以写一个快速脚本随机挑三张图对应的标注打印bbox值再到原图上看框的位置是否大致合理import random import json with open(OUTPUT_JSON, r, encodingutf-8) as f: new_coco json.load(f) # 建索引 img_id_to_info {img[id]: img for img in new_coco[images]} cat_id_to_name {cat[id]: cat[name] for cat in new_coco[categories]} sample_img_ids random.sample(list(img_id_to_info.keys()), 3) for img_id in sample_img_ids: img img_id_to_info[img_id] print(图片, img[file_name], 尺寸, img[width], x, img[height]) anns_for_img [ann for ann in new_coco[annotations] if ann[image_id] img_id] for ann in anns_for_img[:5]: print( 类别, cat_id_to_name[ann[category_id]], bbox, ann[bbox])第三用框架自带的工具做一次解析验证。比如MMDetection有load_json_annotations或Detectron2的load_coco_json如果读不进去说明JSON格式有问题。这一步不是必须的但训练前做一次能避免启动到一半报错。4.4 如果你还需要把提取结果转成YOLO格式COCO提取只是第一步很多朋友最终要的是YOLO格式的txt标签。这里顺便给一段转换代码的要点核心就是根据图片的宽高把bbox从[x, y, w, h]归一化成[cx, cy, w, h]并且类别ID要重新映射成0开始连续编号# 注意YOLO格式的类别序号必须是0,1,2... id_mapping {cat_id: new_id for new_id, cat_id in enumerate(sorted(target_ids))} with open(labels.txt, w) as label_f: for img in filtered_images: img_w, img_h img[width], img[height] txt_name img[file_name].replace(.jpg, .txt) for ann in filtered_annotations: if ann[image_id] ! img[id]: continue x, y, w, h ann[bbox] cx, cy x w / 2, y h / 2 cx / img_w cy / img_h w / img_w h / img_h label_f.write(f{id_mapping[ann[category_id]]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n)这一段的要点是id_mapping因为在COCO里person的id是1car的id是3如果你直接拿原始id写进YOLO txt那么你的类别只有person、car、motorcycle三个但标签里却可能出现1、3、4这些空洞编号导致训练时类别数对不上。做任何数据集处理时只要换了数据子集就要重新做一套连续编号。5. 常见问题与排查实录5.1 类别ID对不上你以为的“car2”其实是错的有次我帮一个朋友排查他用网上流传的某个代码里面写死了car 2一跑模型训练后识别的全是自行车。原因很简单COCO的80个类别的id分布是稀疏的car实际对应id3bicycle对应id2person对应id1。除非你自己用coco.names那类文件重新映射过否则不要凭记忆写死ID。任何时候都以JSON里categories字段为准。5.2 提取后图片目录和标注文件脱节最常见的情况是你复制了图片但忘了改标注JSON里的file_name对应的路径前缀或者你改了标注JSON但图片还在原目录里因为新目录没被创建成功。调试时检查两点新JSON里的images[0].file_name是否能在新图片目录下找到。新图片目录里的文件数量是否等于新JSON里的images数量。如果两者对不上多半是图片复制阶段有文件缺失或者某些image_id对应的原图压根不在train2017/目录里。注意COCO的图片不是全部在同一个目录下的annotations里虽然偶有图片ID存在于train2017的JSON却对应不到文件的情况多半是你把train和val的标注文件混着用了。比如用instances_val2017.json去匹配train2017/目录下的图片必然一堆找不到。下载时按官方组合来别交叉。5.3 数据量不均衡怎么办如果你只提取一个冷门类别比如“雪犁车”这种可能翻遍整个train集也只有一两百张图。这种情况下直接用提取结果训练效果不会好。通常会配合数据增强翻转、裁剪、颜色抖动或者干脆换用其他数据源补充。另外有一个容易被忽略的点保留iscrowd字段的标注。有些时候你从COCO中提取目标类别时会把iscrowd1的标注也保留然后在训练时很多框架默认忽略iscrowd标注。如果提取出来的某个类大多数标注都是crowd样本实际可用训练样本数会大幅缩水。建议在筛选时明确一个策略要不要保iscrowd然后提前打印统计信息。5.4 文件读取速度太慢处理600MB的JSONPython普通json.load大约需要十几秒之后遍历annotations几十万条又需要一两秒总的处理时间并不长但内存占用会比较明显可能吃掉3GB内存。如果你的机器内存小于8GB建议把脚本拆成两步先用命令行工具把annotations按image_id映射成多个小文件再做筛选。不过大部分情况下只要确认machine上内存够用直接跑就完了速度完全可接受。5.5 提取后想合并多个类别注意标注数量翻倍的问题当你同时提取多个类别时annotations的条数是所有目标类别标注的总和。但图片数量不是所有类别图片数量之和因为有交集。比如person有2500张图car有1800张图两者同时出现的图片有600张那么最终提取结果图片数量是 25001800-6003700张而标注数量是两者的总标注数。统计时不要把“各类图片数相加”当成“最终图片数”否则会误判。6. 经验总结与扩展方向我自己的习惯是第一次处理COCO子集时不要贪多先用person一个类做通整个流程。原因是person在COCO中覆盖面最广、图片最多、各种场景都有数据量足够大调试起来最方便。等流程完全跑通后再替换成自己真正需要的类别列表。代码层面可以把上面这个脚本封装成一个函数参数是target_category_names、input_json、output_dir做成命令行工具后面每次处理不同数据集直接复用。如果有进一步需求比如从多个COCO版本2014/2017合并数据或者把COCO子集和自有数据合并也可以基于同样的JSON操作逻辑往上叠。在实际项目里我还会用提取后的子集先做一次小的过拟合测试——只拿三五十张图训练十几个epoch看看loss能不能降下去bbox预测是否合理。这比直接全量训练要快得多能提前暴露数据标注问题。这个经验分享出来希望少走点弯路。