如何将 COCO 标注转换为 YOLO 格式#
训练 Ultralytics YOLO 模型需要使用 YOLO 格式的标注,但许多热门的标注工具导出的是 COCO JSON 格式。本指南将介绍如何将 COCO 标注转换为 YOLO 格式,并开始训练目标检测、实例分割和姿态估计模型。
如需直接使用 COCO JSON 进行训练而不生成 .txt 文件,请参阅无需转换,直接在 COCO JSON 上训练 YOLO。
为什么要从 COCO 转换为 YOLO?#
COCO JSON 格式将所有标注存储在一个文件中,而 YOLO 为每张图像使用一个包含归一化坐标的文本文件。必须进行转换,原因如下:
- YOLO 模型需要
.txt标签文件,每张图像对应一个文件,其中包含采用归一化坐标表示的class x_center y_center width height。 - COCO JSON 使用像素坐标,采用
[x_min, y_min, width, height]格式,并为所有图像使用一个 JSON 文件。 - 类别 ID 不同——COCO 使用任意的
category_id值,而 YOLO 要求类别 ID 从零开始索引。
| 特性 | COCO JSON | YOLO TXT |
|---|---|---|
| 结构 | 所有图像共用一个 JSON 文件 | 每张图像对应一个 .txt 文件 |
| BBox 格式 | 以像素为单位的 [x_min, y_min, width, height] | 归一化的 class x_center y_center width height(0-1) |
| 类别 ID | category_id(可以从任意数字开始) | 从零开始索引(从 0 开始) |
| 分割 | segmentation 字段中的多边形数组 | 类别 ID 后的多边形坐标 |
| 关键点 | 以像素为单位的 [x, y, visibility, ...] | 归一化的 [x, y, visibility, ...] |
快速开始#
转换 COCO 标注并开始训练的最快方式:
from ultralytics.data.converter import convert_coco
convert_coco(
labels_dir="my_dataset/annotations/", # directory containing your JSON files
save_dir="my_dataset/converted/", # where to save converted labels
cls91to80=False, # set False for custom datasets (see warning below)
)转换后,整理目录结构、创建 dataset.yaml,然后开始训练。请参阅下面的完整分步指南。
cls91to80=True 默认值仅针对包含 80 个目标类别的标准 COCO 数据集设计,它会将 91 个不连续的类别 ID 映射到 80 个连续的类别 ID。对于任何自定义数据集,你必须设置 cls91to80=False,否则类别 ID 会被静默错误映射,导致模型学习到错误的类别。
分步转换指南#
1. 准备 COCO 数据集#
从标注工具导出的典型 COCO 格式数据集具有以下结构:
my_dataset/
├── images/
│ ├── train/
│ │ ├── img_001.jpg
│ │ ├── img_002.jpg
│ │ └── ...
│ └── val/
│ ├── img_100.jpg
│ └── ...
└── annotations/
├── instances_train.json
└── instances_val.json每个 JSON 文件都遵循 COCO 数据格式规范,并包含三个必需字段——images、annotations 和 categories:
{
"images": [{ "id": 1, "file_name": "img_001.jpg", "width": 640, "height": 480 }],
"annotations": [
{
"id": 1,
"image_id": 1,
"category_id": 1,
"bbox": [100, 50, 200, 150],
"area": 30000,
"iscrowd": 0
}
],
"categories": [
{ "id": 1, "name": "helmet" },
{ "id": 2, "name": "vest" }
]
}2. 转换标注#
使用 convert_coco() 函数,将 COCO JSON 标注转换为 YOLO .txt 格式:
from ultralytics.data.converter import convert_coco
convert_coco(
labels_dir="my_dataset/annotations/",
save_dir="my_dataset/converted/",
cls91to80=False,
)convert_coco() 会将每个已标注图像对应的一个 .txt 文件写入以各 JSON 文件命名的 labels/ 子目录,并移除 instances_ 前缀(因此 instances_train.json 会生成 labels/train/)。没有标注的图像会被跳过,也不会生成标签文件,因此 labels/ 目录树可能不会与所有图像完全对应:
my_dataset/converted/
├── images/ # created but left empty
└── labels/
├── train/ # from instances_train.json
│ ├── img_001.txt
│ └── ...
└── val/ # from instances_val.json
└── ...convert_coco() 不会覆盖现有的 save_dir:如果 my_dataset/converted/ 已存在,重新运行时会改为写入 my_dataset/converted-2/。重新运行前请删除之前的输出(或更改 save_dir),否则后续步骤会读取过时的标签。
3. 整理目录结构#
转换后,需要将标签文件放置在图像旁边。YOLO 要求存在一个与 images/ 目录结构对应的 labels/ 目录:
import shutil
from pathlib import Path
converted_dir = Path("my_dataset/converted/labels")
dataset_dir = Path("my_dataset")
# convert_coco names each subdirectory after its JSON file (minus the "instances_" prefix),
# so iterate the actual subdirectories instead of assuming "train"/"val".
for src in converted_dir.iterdir():
if not src.is_dir():
continue
dst = dataset_dir / "labels" / src.name
dst.mkdir(parents=True, exist_ok=True)
for f in src.glob("*.txt"):
shutil.move(str(f), str(dst / f.name))最终的数据集结构应如下所示:
my_dataset/
├── images/
│ ├── train/
│ │ ├── img_001.jpg
│ │ └── ...
│ └── val/
│ └── ...
├── labels/
│ ├── train/
│ │ ├── img_001.txt
│ │ └── ...
│ └── val/
│ └── ...
└── dataset.yaml4. 创建 dataset.yaml#
创建 dataset.yaml 配置文件,将 COCO 类别映射到 YOLO 类别名称。该文件会告诉 YOLO 数据的位置以及要检测的类别:
import json
from pathlib import Path
import yaml
# Read categories from your COCO JSON
with open("my_dataset/annotations/instances_train.json") as f:
coco = json.load(f)
# Build class names matching convert_coco output (category_id - 1)
categories = sorted(coco["categories"], key=lambda x: x["id"])
names = {cat["id"] - 1: cat["name"] for cat in categories}
# NOTE: convert_coco maps class IDs as category_id - 1, so category_id must
# start from 1. If your categories start from 0, add 1 to each ID first.
# Create dataset.yaml
dataset = {
"path": str(Path("my_dataset").resolve()),
"train": "images/train",
"val": "images/val",
"names": names,
}
with open("my_dataset/dataset.yaml", "w") as f:
yaml.dump(dataset, f, default_flow_style=False)生成的 YAML 文件:
path: /absolute/path/to/my_dataset
train: images/train
val: images/val
names:
0: helmet
1: vest有关数据集 YAML 格式的更多详情,请参阅数据集配置指南。
5. 训练 YOLO 模型#
准备好转换后的数据集后,训练 YOLO 模型:
from ultralytics import YOLO
model = YOLO("yolo26n.pt") # load a pretrained model
results = model.train(data="my_dataset/dataset.yaml", epochs=100, imgsz=640)有关训练技巧和最佳实践,请参阅模型训练指南。
6. 验证转换结果#
训练前,抽查几个标签文件,确认类别 ID 和坐标正确:
from pathlib import Path
label_file = Path("my_dataset/labels/train/img_001.txt")
for line in label_file.read_text().strip().splitlines():
parts = line.split()
cls_id = int(parts[0])
coords = [float(v) for v in parts[1:5]]
assert cls_id >= 0, f"Negative class ID {cls_id} — category_id in your JSON may start from 0"
assert all(0 <= v <= 1 for v in coords), f"Coordinates out of [0, 1] range: {coords}"如果看到负数类别 ID,说明你的 COCO JSON 可能使用了从 0 开始的 category_id。在运行 convert_coco() 前,将 JSON 中所有 category_id 值加 1,因为它会将类别 ID 映射为 category_id - 1。
常见问题排查#
转换后类别 ID 错误#
如果模型能够训练但检测出的目标类别错误,你可能是在自定义数据集上使用了 cls91to80=True(默认值)。该设置会通过 COCO 91-to-80 查找表映射你的 category_id 值,而这仅适用于标准的 COCO 数据集。没有对应 COCO-80 类别的 category_id 会被映射为空,并在转换期间引发 TypeError: must be real number, not NoneType,而不是生成错误标签。
解决方案:对于自定义数据集,始终使用 cls91to80=False。
训练期间未找到标签#
如果标签扫描报告 0 images, N backgrounds,随后训练因 ValueError: train: No labels found in .../labels/train.cache 中止,说明你的标签文件不在预期目录中。convert_coco() 会将标签保存到单独的输出目录(例如 save_dir/labels/train/),但 YOLO 需要在数据集目录中将 labels/ 与 images/ 并列放置。
解决方案:移动标签文件,使其符合预期的目录结构。确保 labels/train/ 与 images/train/ 位于同一级目录。
转换期间出现 KeyError#
如果运行 convert_coco() 时出现 KeyError: 'bbox' 或类似错误,你的 labels_dir 可能包含非实例 JSON 文件(例如 captions_train2017.json),这些文件具有不同的标注结构。
解决方案:只将实例标注 JSON 文件(例如 instances_train2017.json)放入 labels_dir。
转换后标签文件为空#
如果转换完成后 .txt 文件为空或缺失,可能是所有标注都具有 iscrowd: 1(这在 SAM 生成的掩码中很常见),或者边界框的宽度或高度为零。对仅包含检测标注的导出结果运行 use_keypoints=True 也会产生相同结果,因为不含 keypoints 字段的标注会被完全跳过。
解决方案:检查 JSON 标注中的 iscrowd 值。如果使用 SAM 掩码,请预处理 JSON 以设置 iscrowd: 0。如果传入了 use_keypoints=True,请确认标注确实包含 keypoints。
掩码标注生成盒状多边形#
如果 use_segments=True 记录了 annotations without a usable polygon,说明某些标注没有 segmentation 值,或者该值不是至少包含三个坐标对的列表。通常原因包括仅检测导出结果(该字段缺失或为空)以及 COCO 游程编码({"counts": ..., "size": ...}),后者由 SAM 等位掩码导出器写入;没有外层多边形列表的扁平坐标列表、只有一两个点的轮廓以及其他格式错误的值也会以相同方式处理。标注会保留仍然有效的多边形;如果没有有效多边形,则回退为形状类似其边界框的分割行,因此标签仍然有效,但这些行不包含掩码细节。
解决方案:重新导出带有多边形分割的标注,在运行 convert_coco() 前将 RLE 掩码解码为多边形,或修正格式错误的 segmentation 值。
转换后标签中的类别 ID 不连续#
如果标签文件中的类别 ID 不连续(例如为 0、4、9,而不是 0、1、2),说明你的标注工具使用了不连续的 category_id 值。
解决方案:确认 .txt 文件中的类别 ID 与 dataset.yaml 中的 names 字典一致。如有需要,将 ID 重新映射为连续值。
有关完整的 API 详情和参数说明,请参阅 convert_coco API 参考。
常见问题#
使用 Ultralytics 提供的
convert_coco()函数,将 COCO JSON 标注转换为 YOLO.txt格式。对于自定义数据集,请设置cls91to80=False:from ultralytics.data.converter import convert_coco convert_coco(labels_dir="path/to/annotations/", save_dir="output/", cls91to80=False)转换后,重新整理标签文件,使
labels/与images/目录对应,然后创建dataset.yaml文件。有关完整工作流程,请参阅分步指南。这是因为
convert_coco()会将标签保存到save_dir/labels/内的子目录(例如save_dir/labels/train/),而不是直接保存到数据集的labels/train/中,与images/train/并列。YOLO 要求标签与图像并列放置——例如,images/train/img.jpg需要labels/train/img.txt。移动转换后的标签,使其符合此结构。请参阅修复目录结构。cls91to80参数控制 COCOcategory_id值如何映射到 YOLO 类别 ID。当True(默认值)时,它会应用为标准 COCO 数据集设计的coco91_to_coco80_class()查找表。该数据集包含 80 个类别,其 ID 不连续(1-90)。对于自定义数据集,始终设置cls91to80=False——它只需从每个category_id中减去 1,即可创建从零开始索引的类别 ID。不能直接使用默认流程,除非编写自定义代码。默认训练流程需要 YOLO
.txt标签,并且每张图像对应一个文件。因此,你可以运行convert_coco()并遵循此分步指南,或者继承数据集类以即时解析 COCO JSON——请参阅无需转换,直接在 COCO JSON 上训练 YOLO。有关支持格式的更多信息,请参阅数据集格式。可以,在调用
convert_coco()时使用use_segments=True,即可在转换后的 YOLO 标签中包含多边形分割掩码。这样会生成兼容 YOLO 分割模型的标签文件:from ultralytics.data.converter import convert_coco convert_coco(labels_dir="annotations/", save_dir="output/", use_segments=True, cls91to80=False)使用
use_keypoints=True转换 COCO 关键点标注,以用于姿态估计训练:from ultralytics.data.converter import convert_coco convert_coco(labels_dir="annotations/", save_dir="output/", use_keypoints=True, cls91to80=False)请注意,如果
use_segments和use_keypoints都设置为True,标签文件中只会写入关键点,分割片段会被静默忽略。