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 mathematical formulas, a production-ready 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: $$\text{bbox} = [x_{\min}, y_{\min}, \text{width}, \text{height}]$$

YOLO Bounding Box Format

YOLO requires values normalized between $0.0$ and $1.0$, representing the center point of the box: $$\text{YOLO} = [\text{class_id}, x_{\text{center}}, y_{\text{center}}, w_{\text{norm}}, h_{\text{norm}}]$$

Conversion Formulas

Given an image with dimensions $W$ (width) and $H$ (height):

$$x_{\text{center}} = \frac{x_{\min} + \frac{\text{width}}{2}}{W}$$

$$y_{\text{center}} = \frac{y_{\min} + \frac{\text{height}}{2}}{H}$$

$$w_{\text{norm}} = \frac{\text{width}}{W}$$

$$h_{\text{norm}} = \frac{\text{height}}{H}$$


2. Production-Ready Python Conversion Script

Here is a robust Python script using only the standard library (json, pathlib, collections). It automatically handles category re-indexing (ensuring zero-based class IDs) and generates a ready-to-train data.yaml.

#!/usr/bin/env python3
"""
coco_to_yolo.py
Converts a COCO JSON annotation file into YOLO TXT format with data.yaml.
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,
    img_folder_name: str = "images"
):
    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

    # 5. Generate data.yaml for Ultralytics
    yaml_path = out_path / "data.yaml"
    with open(yaml_path, "w", encoding="utf-8") as yf:
        yf.write(f"path: {out_path.resolve()}\n")
        yf.write(f"train: {img_folder_name}\n")
        yf.write(f"val: {img_folder_name}\n\n")
        yf.write(f"nc: {len(class_names)}\n")
        yf.write("names:\n")
        for idx, name in enumerate(class_names):
            yf.write(f"  {idx}: {name}\n")

    print(f"Successfully converted {converted_count} images with {box_count} total boxes.")
    print(f"Labels saved to: {labels_dir}")
    print(f"YAML config saved to: {yaml_path}")


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])

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.

Let's talk about your project

Tell us what you need and we'll shape the right solution together.

Try Online Converter