YOLO Vision 2026:

YOLO mit COCO JSON ohne Konvertierung trainieren#

Annotationen im COCO-JSON-Format können direkt für das Training von Ultralytics YOLO verwendet werden, ohne sie vorher in .txt-Dateien zu konvertieren. Dazu wird YOLODataset abgeleitet, um COCO JSON während der Laufzeit zu verarbeiten, und über einen benutzerdefinierten Trainer in die Trainingspipeline eingebunden.

Warum direkt mit COCO JSON trainieren#

Bei diesem Ansatz bleibt COCO JSON die einzige Quelle der Wahrheit — kein Aufruf von convert_coco(), keine Neuorganisation von Verzeichnissen und keine Zwischen-Labeldateien. YOLO26 und alle anderen Erkennungsmodelle von Ultralytics YOLO werden unterstützt. Für Segmentierungs- und Posenschätzungsmodelle sind zusätzliche Label-Felder erforderlich (siehe FAQ).

Suchst du stattdessen eine einmalige Konvertierung?

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

Architekturübersicht#

Es werden zwei Klassen benötigt:

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

Die Implementierung ist eine vereinfachte Version von GroundingDataset, das ebenfalls JSON-Annotationen direkt liest. Hier werden drei Methoden überschrieben — get_img_files(), cache_labels() und get_labels() —, während GroundingDataset mehr überschreibt, darunter eigene Prüfungen für Cache-Hash und Instanzanzahl.

Die COCO-JSON-Datasetklasse erstellen#

Die Klasse COCODataset erbt von YOLODataset und überschreibt die Logik zum Laden der Labels. Statt .txt-Dateien aus einem Labelverzeichnis zu lesen, öffnet sie die COCO-JSON-Datei, durchläuft 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 Bildpfade aus dem JSON-Feld file_name innerhalb von cache_labels() aufgelöst werden. Kategorie-IDs werden sortiert und auf bei null beginnende Klassenindizes abgebildet, sodass sowohl IDs ab 1 (Standard bei 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."""
        self.fraction = 1.0  # fraction is applied while scanning a directory, which this dataset skips
        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)

        # 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",
                }
            )
        if not x["labels"]:
            raise RuntimeError(f"No images listed in {self.json_file} were found in {self.img_path}")
        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"]

Die eingelesenen Labels werden in einer .cache-Datei neben der JSON-Datei gespeichert (z. B. instances_train.cache). Bei nachfolgenden Trainingsläufen wird der Cache direkt geladen, sodass das Parsen der JSON-Datei entfällt.

Der Cache-Schlüssel ist die Dateigröße der JSON-Datei, nicht ihr Inhalt

get_hash() hasht Dateigrößen und Pfade statt der Dateiinhalte, sodass ein erneuter Lauf die JSON-Datei nur dann erneut parst, wenn sich ihre Byteanzahl ändert. Das Hinzufügen oder Entfernen von Bildern kann auch die Größe des Bildverzeichnisses selbst verändern und eine Neuerstellung auslösen, aber verlasse dich nicht darauf — der Hash prüft keine einzelnen Bilddateien. Wenn ein Bild durch ein anderes mit derselben Größe ersetzt wird, kann die Größe unverändert bleiben. Eine Bearbeitung, bei der die Byteanzahl gleich bleibt — etwa das Verschieben einer Koordinate, das Ändern von iscrowd oder das Austauschen zweier Klassennamen gleicher Länge — lässt den veralteten Cache bestehen. Dadurch wird ohne Warnung mit den alten Annotationen trainiert. Auch das Ersetzen eines Bildes an Ort und Stelle bleibt aus demselben Grund unbemerkt. Lösche die .cache-Datei, nachdem du Annotationen oder Bilder an Ort und Stelle bearbeitet hast.

Das Dataset in die Trainingspipeline einbinden#

Im Trainer muss lediglich build_dataset() überschrieben werden. Das standardmäßige DetectionTrainer erstellt ein YOLODataset, das nach .txt-Labeldateien sucht. Durch den Ersatz mit COCODataset liest der Trainer stattdessen aus COCO JSON.

Der Pfad zur JSON-Datei wird aus einem benutzerdefinierten Feld train_json / val_json in der Datenkonfiguration übernommen (siehe dataset.yaml konfigurieren). Während des Trainings wird mode="train" zu train_json aufgelöst; während der Validierung wird mode="val" zu val_json aufgelöst. Beide Schlüssel sind erforderlich — die beiden Aufteilungen verwenden unterschiedliche Bildverzeichnisse, daher kann die Trainings-JSON nicht als Ersatz für ein fehlendes val_json dienen.

Das Dataset setzt außerdem fraction auf 1.0 zurück. BaseDataset wendet dieses Argument beim Durchsuchen eines Bildverzeichnisses an, ein Schritt, den COCODataset überspringt. Daher kann es eine Anfrage für ein Teildataset nicht berücksichtigen. Durch das Zurücksetzen wird verhindert, dass das Dataset den Eindruck erweckt, einen Wert zu akzeptieren, den es ignoriert. Das integrierte GroundingDataset geht aus demselben Grund denselben Kompromiss ein.

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["val_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,
        )

dataset.yaml für COCO JSON konfigurieren#

dataset.yaml verwendet die standardmäßigen Felder path, train und val, um Bildverzeichnisse zu finden. Beachte, dass path hier auf das Bildstammverzeichnis verweist. Daher sind train und val einfache Namen der Aufteilungen — anders als im Konvertierungsleitfaden, wo path das Dataset-Stammverzeichnis ist und die Aufteilungen ein Präfix images/ enthalten. Zwei zusätzliche Felder, train_json und val_json, geben die COCO-Annotationsdateien an, die COCOTrainer liest. Das Feld names enthält die Klassennamen in der sortierten Reihenfolge von categories in der JSON-Datei. Die Klassenanzahl wird daraus abgeleitet, sodass nc nicht gesetzt werden muss.

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

# COCO JSON annotation files (use absolute paths; these custom keys are not resolved against `path`)
train_json: /path/to/my_dataset/annotations/instances_train.json
val_json: /path/to/my_dataset/annotations/instances_val.json

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

Training mit COCO JSON ausführen#

Sobald Datasetklasse, Trainerklasse und YAML-Konfiguration eingerichtet sind, funktioniert das Training über den standardmäßigen Aufruf model.train(). Der einzige Unterschied zu einem normalen Trainingslauf ist das Argument trainer=COCOTrainer, das Ultralytics anweist, den benutzerdefinierten Dataset-Lader anstelle des Standardladers 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, einschließlich der Validierung während des Trainings, des Speicherns von Checkpoints und der Protokollierung von Metriken.

Eigenständiges `model.val()` benötigt eine eigene Überschreibung

Nur die Validierung während des Trainings läuft über COCOTrainer.build_dataset. Ein separater Aufruf von model.val() erstellt das standardmäßige YOLODataset, das neben den Bildern nach .txt-Labels sucht und keine findet. Es bricht nicht ab: Die Bilder werden als Hintergründe gezählt, sodass die Validierung vollständig ausgeführt wird und jede Metrik als 0 meldet, mit den Warnungen No labels found in ... und no labels found in detect set, cannot compute metrics without labels. Um außerhalb eines Trainingslaufs zu validieren, leite den Validator mit derselben Überschreibung build_dataset ab und übergib ihn an model.val(validator=...).

Vollständige Implementierung#

Der Einfachheit halber wird die vollständige Implementierung unten als einzelnes Skript zum Kopieren und Einfügen bereitgestellt. Sie enthält das benutzerdefinierte Dataset, den benutzerdefinierten Trainer und den Trainingsaufruf. Speichere sie zusammen mit deinem dataset.yaml und führe sie 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."""
        self.fraction = 1.0  # fraction is applied while scanning a directory, which this dataset skips
        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)

        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",
                }
            )
        if not x["labels"]:
            raise RuntimeError(f"No images listed in {self.json_file} were found in {self.img_path}")
        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["val_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)

Du verfügst nun über ein minimales Dataset und einen Trainer, die Ultralytics YOLO direkt mit COCO JSON trainieren. Dabei bleiben die Annotationen die einzige Quelle der Wahrheit, und es werden keine .txt-Zwischendateien erzeugt. Erweitere die Methode cache_labels() mit segments oder keypoints, um Segmentierung und Posenschätzung abzudecken. Empfehlungen zur Abstimmung der Hyperparameter findest du im Leitfaden Tipps zum Modelltraining.

FAQ#

  • convert_coco() schreibt .txt-Labeldateien als einmalige Konvertierung auf die Festplatte. Bei diesem Ansatz wird die JSON-Datei zu Beginn jedes Trainingslaufs geparst und die Annotationen werden im Arbeitsspeicher konvertiert. Verwende convert_coco(), wenn dauerhafte Labels im YOLO-Format bevorzugt werden. Verwende diesen Ansatz, um COCO JSON als einzige Quelle der Wahrheit beizubehalten, ohne zusätzliche Dateien zu erzeugen.

  • Nicht mit der aktuellen Ultralytics-Pipeline, die standardmäßig YOLO-.txt-Labels erwartet. Dieser Leitfaden stellt den erforderlichen minimalen benutzerdefinierten Code bereit — eine Datasetklasse und eine Trainerklasse. Sobald diese definiert sind, erfordert das Training nur einen standardmäßigen model.train()-Aufruf.

  • Dieser Leitfaden behandelt die Objekterkennung. Um die Instanzsegmentierung zu unterstützen, füge die segmentation-Polygoninformationen aus den COCO-Annotationen in das Feld segments jedes Label-Dictionaries ein. Für die Posenschätzung füge keypoints ein. Der GroundingDataset-Quellcode enthält eine Referenzimplementierung für die Verarbeitung von Segmenten.

  • Ja. COCODataset erweitert YOLODataset, daher funktionieren alle integrierten DatenaugmentierungenMosaik, Mixup, Copy-Paste und weitere — ohne Änderungen.

  • Kategorien werden nach id sortiert und auf fortlaufende Indizes ab null abgebildet. Damit werden IDs ab 1 (Standard bei COCO), IDs ab 0 und nicht zusammenhängende IDs unterstützt. Das Dictionary names in dataset.yaml sollte dieselbe sortierte Reihenfolge wie das COCO-Array categories verwenden.

  • COCO JSON wird beim ersten Trainingslauf einmal geparst. Die eingelesenen Labels werden in einer .cache-Datei gespeichert, sodass nachfolgende Läufe ohne erneutes Parsen sofort starten. Die Trainingsgeschwindigkeit entspricht der des standardmäßigen YOLO-Trainings, da die Annotationen im Arbeitsspeicher gehalten werden. Der Cache basiert auf der Dateigröße der JSON-Datei. Lösche daher die .cache-Datei nach jeder Bearbeitung, bei der die Dateilänge unverändert bleibt.

Kommentare