Meet YOLO26: next-gen vision AI.

Link to this sectionSo trainierst du YOLO mit COCO JSON, ohne es konvertieren zu müssen#

Link to this sectionWarum du direkt mit COCO JSON trainieren solltest#

Annotationen im COCO JSON-Format können direkt für das Training mit Ultralytics YOLO verwendet werden, ohne sie vorher in .txt-Dateien zu konvertieren. Dies geschieht durch die Ableitung von YOLODataset, um COCO JSON im laufenden Betrieb zu parsen und es über einen benutzerdefinierten Trainer in die Trainings-Pipeline einzubinden.

Dieser Ansatz hält das COCO JSON als einzige Quelle der Wahrheit (Single Source of Truth) bei — kein convert_coco()-Aufruf, kein Umstrukturieren von Verzeichnissen, keine intermediären Label-Dateien. YOLO26 und alle anderen Ultralytics YOLO Detektionsmodelle werden unterstützt. Segmentierungs- und Pose-Modelle erfordern zusätzliche Label-Felder (siehe FAQ).

Suchst du stattdessen nach einer einmaligen Konvertierung?

Siehe den COCO-zu-YOLO-Konvertierungsleitfaden für den standardmäßigen convert_coco()-Workflow.

Link to this sectionArchitektur-Übersicht#

Es werden zwei Klassen benötigt:

  1. COCODataset — liest COCO JSON und konvertiert Bounding Boxes während des Trainings im Speicher in das YOLO-Format
  2. COCOTrainer — überschreibt build_dataset(), um COCODataset anstelle des standardmäßigen YOLODataset zu verwenden

Die Implementierung folgt dem gleichen Muster wie das integrierte GroundingDataset, das JSON-Annotationen ebenfalls direkt liest. Drei Methoden werden überschrieben: get_img_files(), cache_labels() und get_labels().

Link to this sectionErstellen der COCO JSON Dataset-Klasse#

Die COCODataset-Klasse erbt von YOLODataset und überschreibt die Logik zum Laden der Labels. Anstatt .txt-Dateien aus einem Labels-Verzeichnis zu lesen, öffnet sie die COCO JSON-Datei, iteriert über die nach Bildern gruppierten Annotationen und konvertiert jede Bounding Box vom COCO-Pixelformat [x_min, y_min, width, height] in das normalisierte YOLO-Zentrumsformat [x_center, y_center, width, height]. Crowd-Annotationen (iscrowd: 1) und Boxen mit einer Fläche von Null werden automatisch übersprungen.

Die Methode get_img_files() gibt eine leere Liste zurück, da die Bildpfade aus dem JSON-Feld file_name innerhalb von cache_labels() aufgelöst werden. Kategorie-IDs werden sortiert und auf nullbasierte Klassenindizes umgemappt, sodass sowohl 1-basierte (Standard-COCO) als auch nicht zusammenhängende ID-Schemata korrekt funktionieren.

import json
from collections import defaultdict
from pathlib import Path

import numpy as np

from ultralytics.data.dataset import DATASET_CACHE_VERSION, YOLODataset
from ultralytics.data.utils import get_hash, load_dataset_cache_file, save_dataset_cache_file
from ultralytics.utils import TQDM

class COCODataset(YOLODataset):
    """Dataset that reads COCO JSON annotations directly without conversion to .txt files."""

    def __init__(self, *args, json_file="", **kwargs):
        """Initialize the dataset with a COCO JSON annotation file."""
        self.json_file = json_file
        super().__init__(*args, data={"channels": 3}, **kwargs)

    def get_img_files(self, img_path):
        """Image paths are resolved from the JSON file, not from scanning a directory."""
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        """Parse COCO JSON and convert annotations to YOLO format. Results are saved to a .cache file."""
        x = {"labels": []}
        with open(self.json_file) as f:
            coco = json.load(f)

        images = {img["id"]: img for img in coco["images"]}

        # Sort categories by ID and map to 0-indexed classes
        categories = {cat["id"]: i for i, cat in enumerate(sorted(coco["categories"], key=lambda c: c["id"]))}

        img_to_anns = defaultdict(list)
        for ann in coco["annotations"]:
            img_to_anns[ann["image_id"]].append(ann)

        for img_info in TQDM(coco["images"], desc="reading annotations"):
            h, w = img_info["height"], img_info["width"]
            im_file = Path(self.img_path) / img_info["file_name"]
            if not im_file.exists():
                continue

            self.im_files.append(str(im_file))
            bboxes = []
            for ann in img_to_anns.get(img_info["id"], []):
                if ann.get("iscrowd", False):
                    continue
                # COCO: [x, y, w, h] top-left in pixels -> YOLO: [cx, cy, w, h] center normalized
                box = np.array(ann["bbox"], dtype=np.float32)
                box[:2] += box[2:] / 2  # top-left to center
                box[[0, 2]] /= w  # normalize x
                box[[1, 3]] /= h  # normalize y
                if box[2] <= 0 or box[3] <= 0:
                    continue
                cls = categories[ann["category_id"]]
                bboxes.append([cls, *box.tolist()])

            lb = np.array(bboxes, dtype=np.float32) if bboxes else np.zeros((0, 5), dtype=np.float32)
            x["labels"].append(
                {
                    "im_file": str(im_file),
                    "shape": (h, w),
                    "cls": lb[:, 0:1],
                    "bboxes": lb[:, 1:],
                    "segments": [],
                    "normalized": True,
                    "bbox_format": "xywh",
                }
            )
        x["hash"] = get_hash([self.json_file, str(self.img_path)])
        save_dataset_cache_file(self.prefix, path, x, DATASET_CACHE_VERSION)
        return x

    def get_labels(self):
        """Load labels from .cache file if available, otherwise parse JSON and create the cache."""
        cache_path = Path(self.json_file).with_suffix(".cache")
        try:
            cache = load_dataset_cache_file(cache_path)
            assert cache["version"] == DATASET_CACHE_VERSION
            assert cache["hash"] == get_hash([self.json_file, str(self.img_path)])
            self.im_files = [lb["im_file"] for lb in cache["labels"]]
        except (FileNotFoundError, AssertionError, AttributeError, KeyError, ModuleNotFoundError):
            cache = self.cache_labels(cache_path)
        cache.pop("hash", None)
        cache.pop("version", None)
        return cache["labels"]

Geparste Labels werden in einer .cache-Datei neben dem JSON gespeichert (z. B. instances_train.cache). Bei nachfolgenden Trainingsläufen wird der Cache direkt geladen, wodurch das Parsen des JSON entfällt. Wenn sich die JSON-Datei ändert, schlägt die Hash-Prüfung fehl und der Cache wird automatisch neu erstellt.

Link to this sectionVerbindung des Datasets mit der Trainings-Pipeline#

Die einzige notwendige Änderung im Trainer ist das Überschreiben von build_dataset(). Der standardmäßige DetectionTrainer erstellt ein YOLODataset, das nach .txt-Label-Dateien sucht. Durch das Ersetzen durch COCODataset liest der Trainer stattdessen aus dem COCO JSON.

Der Pfad zur JSON-Datei wird aus einem benutzerdefinierten train_json / val_json-Feld in der Datenkonfiguration gezogen (siehe Schritt 3). Während des Trainings wird mode="train" zu train_json aufgelöst; während der Validierung wird mode="val" zu val_json aufgelöst. Wenn val_json nicht gesetzt ist, wird auf train_json zurückgegriffen.

from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import colorstr

class COCOTrainer(DetectionTrainer):
    """Trainer that uses COCODataset for direct COCO JSON training."""

    def build_dataset(self, img_path, mode="train", batch=None):
        """Build a COCODataset for the given split using the JSON file from the data config."""
        json_file = self.data["train_json"] if mode == "train" else self.data.get("val_json", self.data["train_json"])
        return COCODataset(
            img_path=img_path,
            json_file=json_file,
            imgsz=self.args.imgsz,
            batch_size=batch,
            augment=mode == "train",
            hyp=self.args,
            rect=self.args.rect or mode == "val",
            cache=self.args.cache or None,
            single_cls=self.args.single_cls or False,
            stride=int(self.model.stride.max()) if hasattr(self, "model") and self.model else 32,
            pad=0.0 if mode == "train" else 0.5,
            prefix=colorstr(f"{mode}: "),
            task=self.args.task,
            classes=self.args.classes,
            fraction=self.args.fraction if mode == "train" else 1.0,
        )

Link to this sectionKonfiguration der dataset.yaml für COCO JSON#

Die dataset.yaml verwendet die Standardfelder path, train und val, um Bildverzeichnisse zu finden. Zwei zusätzliche Felder, train_json und val_json, geben die COCO-Annotationsdateien an, die COCOTrainer liest. Die Felder nc und names definieren die Anzahl der Klassen und deren Namen, passend zur sortierten Reihenfolge der categories im JSON.

path: /path/to/images # root directory with train/ and val/ subfolders
train: train
val: val

# COCO JSON annotation files
train_json: /path/to/annotations/instances_train.json
val_json: /path/to/annotations/instances_val.json

nc: 80
names:
    0: person
    1: bicycle
    # ... remaining class names

Erwartete Verzeichnisstruktur:

my_dataset/
  images/
    train/
      img_001.jpg
      ...
    val/
      img_100.jpg
      ...
  annotations/
    instances_train.json
    instances_val.json
  dataset.yaml

Link to this sectionTraining auf COCO JSON ausführen#

Mit der Dataset-Klasse, der Trainer-Klasse und der YAML-Konfiguration erfolgt das Training über den Standardaufruf model.train(). Der einzige Unterschied zu einem normalen Trainingslauf ist das Argument trainer=COCOTrainer, das Ultralytics anweist, den benutzerdefinierten Dataset-Loader anstelle des Standard-Loaders zu verwenden.

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)

Die vollständige Trainings-Pipeline läuft wie erwartet ab, einschließlich Validierung, Speichern von Checkpoints und Metrik-Protokollierung.

Link to this sectionVollständige Implementierung#

Der Einfachheit halber ist die vollständige Implementierung unten als kopierbares Skript bereitgestellt. Es enthält das benutzerdefinierte Dataset, den benutzerdefinierten Trainer und den Trainingsaufruf. Speichere dies zusammen mit deiner dataset.yaml und führe es direkt aus.

import json
from collections import defaultdict
from pathlib import Path

import numpy as np

from ultralytics import YOLO
from ultralytics.data.dataset import DATASET_CACHE_VERSION, YOLODataset
from ultralytics.data.utils import get_hash, load_dataset_cache_file, save_dataset_cache_file
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import TQDM, colorstr

class COCODataset(YOLODataset):
    """Dataset that reads COCO JSON annotations directly without conversion to .txt files."""

    def __init__(self, *args, json_file="", **kwargs):
        """Initialize the dataset with a COCO JSON annotation file."""
        self.json_file = json_file
        super().__init__(*args, data={"channels": 3}, **kwargs)

    def get_img_files(self, img_path):
        """Image paths are resolved from the JSON file, not from scanning a directory."""
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        """Parse COCO JSON and convert annotations to YOLO format, saving results to a .cache file."""
        x = {"labels": []}
        with open(self.json_file) as f:
            coco = json.load(f)

        images = {img["id"]: img for img in coco["images"]}
        categories = {cat["id"]: i for i, cat in enumerate(sorted(coco["categories"], key=lambda c: c["id"]))}

        img_to_anns = defaultdict(list)
        for ann in coco["annotations"]:
            img_to_anns[ann["image_id"]].append(ann)

        for img_info in TQDM(coco["images"], desc="reading annotations"):
            h, w = img_info["height"], img_info["width"]
            im_file = Path(self.img_path) / img_info["file_name"]
            if not im_file.exists():
                continue

            self.im_files.append(str(im_file))
            bboxes = []
            for ann in img_to_anns.get(img_info["id"], []):
                if ann.get("iscrowd", False):
                    continue
                box = np.array(ann["bbox"], dtype=np.float32)
                box[:2] += box[2:] / 2
                box[[0, 2]] /= w
                box[[1, 3]] /= h
                if box[2] <= 0 or box[3] <= 0:
                    continue
                cls = categories[ann["category_id"]]
                bboxes.append([cls, *box.tolist()])

            lb = np.array(bboxes, dtype=np.float32) if bboxes else np.zeros((0, 5), dtype=np.float32)
            x["labels"].append(
                {
                    "im_file": str(im_file),
                    "shape": (h, w),
                    "cls": lb[:, 0:1],
                    "bboxes": lb[:, 1:],
                    "segments": [],
                    "normalized": True,
                    "bbox_format": "xywh",
                }
            )
        x["hash"] = get_hash([self.json_file, str(self.img_path)])
        save_dataset_cache_file(self.prefix, path, x, DATASET_CACHE_VERSION)
        return x

    def get_labels(self):
        """Load labels from .cache file if available, otherwise parse JSON and create the cache."""
        cache_path = Path(self.json_file).with_suffix(".cache")
        try:
            cache = load_dataset_cache_file(cache_path)
            assert cache["version"] == DATASET_CACHE_VERSION
            assert cache["hash"] == get_hash([self.json_file, str(self.img_path)])
            self.im_files = [lb["im_file"] for lb in cache["labels"]]
        except (FileNotFoundError, AssertionError, AttributeError, KeyError, ModuleNotFoundError):
            cache = self.cache_labels(cache_path)
        cache.pop("hash", None)
        cache.pop("version", None)
        return cache["labels"]

class COCOTrainer(DetectionTrainer):
    """Trainer that uses COCODataset for direct COCO JSON training."""

    def build_dataset(self, img_path, mode="train", batch=None):
        """Build a COCODataset for the given split using the JSON file from the data config."""
        json_file = self.data["train_json"] if mode == "train" else self.data.get("val_json", self.data["train_json"])
        return COCODataset(
            img_path=img_path,
            json_file=json_file,
            imgsz=self.args.imgsz,
            batch_size=batch,
            augment=mode == "train",
            hyp=self.args,
            rect=self.args.rect or mode == "val",
            cache=self.args.cache or None,
            single_cls=self.args.single_cls or False,
            stride=int(self.model.stride.max()) if hasattr(self, "model") and self.model else 32,
            pad=0.0 if mode == "train" else 0.5,
            prefix=colorstr(f"{mode}: "),
            task=self.args.task,
            classes=self.args.classes,
            fraction=self.args.fraction if mode == "train" else 1.0,
        )

model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)

Empfehlungen zu Hyperparametern findest du im Leitfaden Tipps zum Modelltraining.

Link to this sectionFAQ#

Link to this sectionWas ist der Unterschied zwischen diesem Ansatz und convert_coco()?#

convert_coco() schreibt .txt-Label-Dateien als einmalige Konvertierung auf die Festplatte. Dieser Ansatz parst das JSON zu Beginn jedes Trainingslaufs und konvertiert Annotationen im Speicher. Verwende convert_coco(), wenn permanente Labels im YOLO-Format bevorzugt werden; verwende diesen Ansatz, wenn das COCO JSON als einzige Quelle der Wahrheit dienen soll, ohne zusätzliche Dateien zu generieren.

Link to this sectionKann YOLO ohne benutzerdefinierten Code auf COCO JSON trainieren?#

Nicht mit der aktuellen Ultralytics-Pipeline, die standardmäßig YOLO .txt-Labels erwartet. Dieser Leitfaden bietet den minimal erforderlichen benutzerdefinierten Code — eine Dataset-Klasse und eine Trainer-Klasse. Sobald diese definiert sind, erfordert das Training lediglich einen standardmäßigen model.train()-Aufruf.

Link to this sectionWird Segmentierung und Pose-Estimation unterstützt?#

This guide covers object detection. To add instance segmentation support, include the segmentation polygon data from COCO annotations in the segments field of each label dictionary. For pose estimation, include keypoints. The GroundingDataset source code provides a reference implementation for handling segments.

Link to this sectionFunktionieren Augmentierungen mit diesem benutzerdefinierten Dataset?#

Ja. COCODataset erweitert YOLODataset, daher laufen alle integrierten DatenaugmentierungenMosaic, Mixup, Copy-Paste und andere — ohne Modifikation.

Link to this sectionWie werden Kategorie-IDs auf Klassenindizes gemappt?#

Kategorien werden nach id sortiert und auf sequentielle Indizes beginnend bei 0 gemappt. Dies funktioniert bei 1-basierten IDs (Standard-COCO), 0-basierten IDs und nicht zusammenhängenden IDs. Das names-Dictionary in der dataset.yaml sollte derselben sortierten Reihenfolge wie das COCO-categories-Array folgen.

Link to this sectionGibt es einen Performance-Nachteil im Vergleich zu vorkonvertierten Labels?#

Das COCO JSON wird beim ersten Trainingslauf einmalig geparst. Die geparsten Labels werden in einer .cache-Datei gespeichert, sodass nachfolgende Läufe ohne erneutes Parsen sofort starten. Die Trainingsgeschwindigkeit ist identisch mit dem Standard-YOLO-Training, da die Annotationen im Speicher gehalten werden. Der Cache wird automatisch neu erstellt, falls sich die JSON-Datei ändert.

Kommentare