How to Convert COCO Annotations to YOLO Format#
Training Ultralytics YOLO models requires annotations in YOLO format, but many popular annotation tools export in COCO JSON format instead. This guide shows you how to convert your COCO annotations to YOLO format and start training object detection, instance segmentation, and pose estimation models.
To train directly on COCO JSON without generating .txt files, see Train YOLO on COCO JSON Without Conversion.
Why Convert from COCO to YOLO?#
The COCO JSON format stores all annotations in a single file, while YOLO uses one text file per image with normalized coordinates. Converting is necessary because:
- YOLO models require
.txtlabel files with one file per image, containingclass x_center y_center width heightin normalized coordinates. - COCO JSON uses pixel coordinates in
[x_min, y_min, width, height]format with a single JSON file for all images. - Class IDs differ — COCO uses arbitrary
category_idvalues, while YOLO requires zero-indexed class IDs.
| Feature | COCO JSON | YOLO TXT |
|---|---|---|
| Structure | Single JSON file for all images | One .txt file per image |
| Bbox format | [x_min, y_min, width, height] in pixels | class x_center y_center width height normalized (0-1) |
| Class IDs | category_id (can start from any number) | Zero-indexed (starts from 0) |
| Segmentation | Polygon arrays in segmentation field | Polygon coordinates after class ID |
| Keypoints | [x, y, visibility, ...] in pixels | [x, y, visibility, ...] normalized |
Quick Start#
The fastest way to convert COCO annotations and start training:
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)
)After conversion, organize your directory structure, create a dataset.yaml, and start training. See the full step-by-step guide below.
The cls91to80=True default is designed only for the standard COCO dataset with 80 object classes, which maps 91 non-contiguous category IDs to 80 contiguous class IDs. For any custom dataset, you must set cls91to80=False — otherwise your class IDs will be silently mapped incorrectly and your model will learn wrong classes.
Step-by-Step Conversion Guide#
1. Prepare Your COCO Dataset#
A typical COCO-format dataset exported from annotation tools has the following structure:
my_dataset/
├── images/
│ ├── train/
│ │ ├── img_001.jpg
│ │ ├── img_002.jpg
│ │ └── ...
│ └── val/
│ ├── img_100.jpg
│ └── ...
└── annotations/
├── instances_train.json
└── instances_val.jsonEach JSON file follows the COCO data format specification with three required fields — images, annotations, and 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 Annotations#
Use the convert_coco() function to convert your COCO JSON annotations to YOLO .txt format:
from ultralytics.data.converter import convert_coco
convert_coco(
labels_dir="my_dataset/annotations/",
save_dir="my_dataset/converted/",
cls91to80=False,
)convert_coco() writes one .txt file per annotated image into a labels/ subdirectory named after each JSON file, with the instances_ prefix removed (so instances_train.json produces labels/train/). Images with no annotations are skipped and get no label file, so the labels/ tree may not mirror every image:
my_dataset/converted/
└── labels/
├── train/ # from instances_train.json
│ ├── img_001.txt
│ └── ...
└── val/ # from instances_val.json
└── ...convert_coco() never overwrites an existing save_dir: if my_dataset/converted/ already exists, a re-run writes to my_dataset/converted-2/ instead. Delete the previous output (or change save_dir) before re-running, or the next steps will read stale labels.
3. Organize Directory Structure#
After conversion, label files need to be placed alongside your images. YOLO expects a labels/ directory that mirrors the images/ directory:
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))Your final dataset structure should look like:
my_dataset/
├── images/
│ ├── train/
│ │ ├── img_001.jpg
│ │ └── ...
│ └── val/
│ └── ...
├── labels/
│ ├── train/
│ │ ├── img_001.txt
│ │ └── ...
│ └── val/
│ └── ...
└── dataset.yaml4. Create dataset.yaml#
Create a dataset.yaml configuration file that maps your COCO categories to YOLO class names. This file tells YOLO where your data is and what classes to detect:
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)The resulting YAML file:
path: /absolute/path/to/my_dataset
train: images/train
val: images/val
names:
0: helmet
1: vestFor more details on the dataset YAML format, see the dataset configuration guide.
5. Train Your YOLO Model#
With your converted dataset ready, train a YOLO model:
from ultralytics import YOLO
model = YOLO("yolo26n.pt") # load a pretrained model
results = model.train(data="my_dataset/dataset.yaml", epochs=100, imgsz=640)For training tips and best practices, see the model training guide.
6. Verify Your Conversion#
Before training, spot-check a few label files to confirm class IDs and coordinates are correct:
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}"If you see negative class IDs, your COCO JSON likely uses category_id starting from 0. Add 1 to all category_id values in your JSON before running convert_coco(), since it maps class IDs as category_id - 1.
Troubleshooting Common Issues#
Wrong Class IDs After Conversion#
If your model trains but detects wrong object classes, you're likely using cls91to80=True (default) on a custom dataset. This maps your category_id values through the COCO 91-to-80 lookup table, which is only correct for the standard COCO dataset.
Solution: Always use cls91to80=False for custom datasets.
No Labels Found During Training#
If training shows WARNING: No labels found or 0 images, N backgrounds, your label files are not in the expected directory. convert_coco() saves labels to a separate output directory (e.g., save_dir/labels/train/), but YOLO expects labels/ parallel to images/ inside your dataset directory.
Solution: Move label files to match the expected directory structure. Make sure labels/train/ is a sibling of images/train/.
KeyError During Conversion#
If you get KeyError: 'bbox' or similar errors when running convert_coco(), your labels_dir likely contains non-instance JSON files (e.g., captions_train2017.json) that have a different annotation structure.
Solution: Only place instance annotation JSON files (e.g., instances_train2017.json) in the labels_dir.
Empty Label Files After Conversion#
If conversion completes but .txt files are empty or missing, all annotations may have iscrowd: 1 (common with SAM-generated masks), or bounding boxes have zero width or height.
Solution: Inspect your JSON annotations for iscrowd values. If using SAM masks, preprocess the JSON to set iscrowd: 0.
Box-Shaped Polygons From Mask Annotations#
If use_segments=True logs annotations without a usable polygon, some annotations carry no segmentation value, or one that is not a list of at least three coordinate pairs. The usual causes are detection-only exports, which leave the field missing or empty, and COCO run-length encoding ({"counts": ..., "size": ...}), which bitmask exporters such as SAM write; a flat coordinate list with no enclosing polygon list, one- and two-point outlines, and other malformed values are handled the same way. An annotation keeps whichever polygons remain, and falls back to a segment row shaped like its bounding box when none do, so the labels stay valid but those rows carry no mask detail.
Solution: Re-export the annotations with polygon segmentations, decode the RLE masks to polygons before running convert_coco(), or correct any malformed segmentation values.
Class ID Gaps in Converted Labels#
If class IDs in label files are non-contiguous (e.g., 0, 4, 9 instead of 0, 1, 2), your annotation tool uses non-contiguous category_id values.
Solution: Verify the class IDs in your .txt files match the names dictionary in dataset.yaml. Remap IDs to contiguous values if needed.
For full API details and parameter descriptions, see the convert_coco API reference.
FAQ#
Use the
convert_coco()function from Ultralytics to convert COCO JSON annotations to YOLO.txtformat. Setcls91to80=Falsefor custom datasets:from ultralytics.data.converter import convert_coco convert_coco(labels_dir="path/to/annotations/", save_dir="output/", cls91to80=False)After conversion, reorganize your label files so
labels/mirrors theimages/directory, then create adataset.yamlfile. See the step-by-step guide for the complete workflow.This happens because
convert_coco()saves labels to a subdirectory insidesave_dir/labels/(e.g.,save_dir/labels/train/) rather than directly into your dataset'slabels/train/alongsideimages/train/. YOLO expects labels to sit parallel to images — for example,images/train/img.jpgneedslabels/train/img.txt. Move your converted labels to match this structure. See fixing the directory structure.The
cls91to80parameter controls how COCOcategory_idvalues are mapped to YOLO class IDs. WhenTrue(default), it applies thecoco91_to_coco80_class()lookup table designed for the standard COCO dataset, which has 80 classes with non-contiguous IDs (1-90). For custom datasets, always setcls91to80=False— this simply subtracts 1 from eachcategory_idto create zero-indexed class IDs.Not with the current YOLO training pipeline — annotations must be in YOLO
.txtformat with one file per image. Useconvert_coco()to convert your COCO JSON first, then follow this guide to organize and train. For more on supported formats, see dataset formats.Yes, use
use_segments=Truewhen callingconvert_coco()to include polygon segmentation masks in the converted YOLO labels. This produces label files compatible with YOLO segmentation models:from ultralytics.data.converter import convert_coco convert_coco(labels_dir="annotations/", save_dir="output/", use_segments=True, cls91to80=False)Use
use_keypoints=Trueto convert COCO keypoint annotations for pose estimation training:from ultralytics.data.converter import convert_coco convert_coco(labels_dir="annotations/", save_dir="output/", use_keypoints=True, cls91to80=False)Note that if both
use_segmentsanduse_keypointsare set toTrue, only keypoints will be written to the label files — segments are silently ignored.