Skip to main content
Blog
Tutorial
Aug 19, 20263 min

How to Convert COCO JSON to YOLO Format (Step-by-Step + Python Script)

Learn how to convert COCO JSON annotations to normalized YOLO TXT files. Includes the coordinate formulas, a Python conversion script, and an instant online converter.

COCO (Common Objects in Context) JSON and YOLO TXT are the two most prevalent annotation formats in modern computer vision.

However, training modern detectors like Ultralytics YOLOv8, YOLOv11, or YOLOv12 requires individual .txt files per image with normalized center coordinates, whereas COCO aggregates all annotations into a single JSON with absolute pixel coordinates.

This guide explains the coordinate mathematics, provides a zero-dependency Python conversion script, and demonstrates how to verify the resulting bounding boxes.

Need an instant conversion without running scripts? Use the free in-browser COCO to YOLO Converter Tool.


1. Coordinate Mathematics: COCO vs. YOLO

COCO Bounding Box Format

COCO stores boxes in absolute pixels measuring from the top-left corner: bbox = [xmin, ymin, box_width, box_height]

YOLO Bounding Box Format

YOLO requires values normalized between 0.0 and 1.0, representing the center point of the box: class_id x_center y_center width height

Conversion Formulas

Given an image with width W and height H:

x_center   = (xmin + box_width / 2) / W
y_center   = (ymin + box_height / 2) / H
norm_width  = box_width / W
norm_height = box_height / H

2. Python Conversion Script

This script uses only the Python standard library (json, pathlib, collections). It re-indexes categories to zero-based class IDs and writes one YOLO label file per COCO image. It does not split images into training and validation sets.

#!/usr/bin/env python3
"""
coco_to_yolo.py
Converts a COCO JSON annotation file into YOLO TXT labels.
Zero external dependencies required.
"""

import json
from pathlib import Path
from collections import defaultdict


def convert_coco_to_yolo(
    coco_json_path: str,
    output_dir: str
):
    json_path = Path(coco_json_path)
    out_path = Path(output_dir)
    labels_dir = out_path / "labels"
    labels_dir.mkdir(parents=True, exist_ok=True)

    print(f"Loading {json_path}...")
    with open(json_path, "r", encoding="utf-8") as f:
        coco_data = json.load(f)

    # 1. Build Category Mapping (Ensure 0-indexed contiguous IDs)
    categories = coco_data.get("categories", [])
    categories.sort(key=lambda c: c["id"])
    
    cat_id_to_yolo_id = {}
    class_names = []
    
    for yolo_idx, cat in enumerate(categories):
        cat_id_to_yolo_id[cat["id"]] = yolo_idx
        class_names.append(cat["name"])

    print(f"Mapped {len(categories)} categories: {class_names}")

    # 2. Map Images by ID
    images = {img["id"]: img for img in coco_data.get("images", [])}

    # 3. Group Annotations by Image ID
    image_annotations = defaultdict(list)
    for ann in coco_data.get("annotations", []):
        # Ignore crowd annotations if needed
        if ann.get("iscrowd", 0) == 1:
            continue
        image_annotations[ann["image_id"]].append(ann)

    # 4. Generate YOLO TXT Files
    converted_count = 0
    box_count = 0

    for img_id, img_info in images.items():
        img_w = float(img_info["width"])
        img_h = float(img_info["height"])
        file_name = Path(img_info["file_name"]).stem
        txt_file_path = labels_dir / f"{file_name}.txt"

        anns = image_annotations.get(img_id, [])
        yolo_lines = []

        for ann in anns:
            cat_id = ann["category_id"]
            if cat_id not in cat_id_to_yolo_id:
                continue

            yolo_class_id = cat_id_to_yolo_id[cat_id]
            x_min, y_min, w, h = ann["bbox"]

            # Guard against negative dimensions or zero sizes
            if w <= 0 or h <= 0:
                continue

            # Convert to YOLO normalized coordinates
            x_center = (x_min + (w / 2.0)) / img_w
            y_center = (y_min + (h / 2.0)) / img_h
            norm_w = w / img_w
            norm_h = h / img_h

            # Clamp coordinates to [0.0, 1.0]
            x_center = max(0.0, min(1.0, x_center))
            y_center = max(0.0, min(1.0, y_center))
            norm_w = max(0.0, min(1.0, norm_w))
            norm_h = max(0.0, min(1.0, norm_h))

            yolo_lines.append(
                f"{yolo_class_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}"
            )
            box_count += 1

        # Write txt file (empty file created if image has no boxes, important for negative samples)
        with open(txt_file_path, "w", encoding="utf-8") as out_f:
            out_f.write("\n".join(yolo_lines) + ("\n" if yolo_lines else ""))

        converted_count += 1

    print(f"Successfully converted {converted_count} images with {box_count} total boxes.")
    print(f"Labels saved to: {labels_dir}")
    print(f"Class names in YOLO ID order: {class_names}")


if __name__ == "__main__":
    import sys
    if len(sys.argv) < 3:
        print("Usage: python coco_to_yolo.py <annotations.json> <output_directory>")
    else:
        convert_coco_to_yolo(sys.argv[1], sys.argv[2])

The script writes labels only; keep the original images with matching filenames and unique image basenames. If you start with one COCO file, use the Dataset Splitter to create separate subsets before conversion. Convert each subset, then place its images and matching labels in separate train and val directories. Do not use the same images for both sets. Create a data.yaml with distinct paths and the class names printed by the script:

path: /path/to/dataset
train: images/train
val: images/val
names:
  0: first_class
  1: second_class

Put the corresponding labels in labels/train and labels/val. Replace the example paths and class names with your own.


3. Visualizing & Verifying Converted YOLO Bounding Boxes

To verify that coordinates did not drift, run this simple OpenCV script to draw the converted boxes back onto your images:

# verify_yolo_boxes.py
import cv2
from pathlib import Path

def draw_yolo_boxes(image_path: str, label_txt_path: str, class_names: list):
    img = cv2.imread(image_path)
    h, w, _ = img.shape

    with open(label_txt_path, "r") as f:
        lines = [line.strip().split() for line in f if line.strip()]

    for line in lines:
        class_id, xc, yc, bw, bh = map(float, line)
        class_id = int(class_id)

        # Convert back to absolute pixels
        x1 = int((xc - bw / 2) * w)
        y1 = int((yc - bh / 2) * h)
        x2 = int((xc + bw / 2) * w)
        y2 = int((yc + bh / 2) * h)

        label = class_names[class_id] if class_id < len(class_names) else str(class_id)
        
        cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
        cv2.putText(img, label, (x1, max(y1 - 10, 15)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

    cv2.imshow("YOLO Box Verification", img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

4. Common Pitfalls to Avoid

  1. Missing 0-index categories: COCO datasets from the web (e.g. COCO 2017) often have category IDs ranging from 1 to 90 with gaps (e.g. ID 12 missing). YOLO expects continuous indices from 0 to num_classes - 1.
  2. Top-Left vs. Center Coordinates: Forgetting to add half the width/height is the #1 bug in custom converters, causing boxes to shift towards the top-left.
  3. Empty Background Images: Do not skip images without annotations; write an empty .txt file so YOLO trains on true negative background samples.

Summary

Converting COCO to YOLO requires careful category mapping and normalized center coordinates.

For automated dataset conversion, multi-format exports, and dataset health checks without writing code, try LabelOp Free Tools.

Take the next step with your dataset.

Use local AI, managed cloud labeling, and team review in LabelOp. Export your work or train with your own Vast.ai account.

Try Online Converter