YOLO Vision 2026:

So trainierst du YOLO mit COCO JSON, ohne es konvertieren zu müssen#

Annotations im COCO JSON-Format können direkt für das Ultralytics YOLO-Training verwendet werden, ohne vorher in .txt-Dateien konvertiert zu werden. Dies funktioniert, indem YOLODataset untergeortnet wird, um COCO JSON zur Laufzeit zu parsen und es über einen benutzerdefinierten Trainer in die Trainings-Pipeline einzubinden.

Warum du direkt mit COCO JSON trainieren solltest#

Dieser Ansatz behält das COCO JSON als einzige zentrale Wahrheitsquelle (Single Source of Truth) bei — kein convert_coco()-Aufruf, keine Verzeichnisneugliederung, keine temporären Label-Dateien. YOLO26 und alle anderen Ultralytics YOLO-Erkennungsmodelle werden unterstützt. Segmentierungs- und Pose-Modelle erfordern zusätzliche Label-Felder (siehe FAQ).

Suchst du stattdessen nach einer einmaligen Konvertierung?

Siehe den Leitfaden zur 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 COCODataset anstelle des Standardwerts YOLODataset zu verwenden

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

Erstellen der COCO JSON Dataset-Klasse#

Die Klasse COCODataset erbt von YOLODataset und überschreibt die Logik zum Laden der Labels. Anstatt .txt-Dateien aus einem Label-Verzeichnis zu lesen, öffnet sie die COCO JSON-Datei, iteriert über nach Bildern gruppierte Anmerkungen und konvertiert jede Bounding Box vom COCO-Pixelformat [x_min, y_min, width, height] in das YOLO-normierte Mittenformat [x_center, y_center, width, height]. Crowd-Anmerkungen (iscrowd: 1) und Boxen mit Null-Fläche 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 nullbasierte Klassenindizes abgebildet, sodass sowohl 1-basierte (Standard-COCO) als auch nicht-kontinuierliche 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)

        # 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 neben dem JSON in einer .cache-Datei gespeichert (z. B. instances_train.cache). Bei nachfolgenden Trainingsläufen wird der Cache direkt geladen, wodurch das Parsen von JSON entfällt. Wenn sich die JSON-Datei ändert, schlägt die Hash-Prüfung fehl und der Cache wird automatisch neu erstellt.

Verbindung des Datasets mit der Trainings-Pipeline#

Die einzige Änderung, die im Trainer erforderlich ist, ist das Überschreiben von build_dataset(). Der Standardwert DetectionTrainer erstellt ein YOLODataset, das nach .txt-Label-Dateien sucht. Durch den Austausch mit COCODataset liest der Trainer stattdessen aus dem COCO JSON.

Der Pfad zur JSON-Datei wird aus einem benutzerdefinierten Feld train_json / val_json in der Datenkonfiguration bezogen (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. Wenn val_json nicht festgelegt 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,
        )

Konfiguration der dataset.yaml für COCO JSON#

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

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

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

Training auf COCO JSON ausführen#

Wenn die Dataset-Klasse, die Trainer-Klasse und die YAML-Konfiguration vorhanden sind, erfolgt das Training über den Standardaufruf model.train(). Der einzige Unterschied zu einem normalen Trainingslauf ist das Argument trainer=COCOTrainer, welches 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 gewohnt ab, einschließlich Validierung, Checkpoint-Speicherung und Metrik-Protokollierung.

Vollständige Implementierung#

Der Einfachheit halber wird die vollständige Implementierung unten als einzelnes Copy-Paste-Skript bereitgestellt. Es enthält das benutzerdefinierte Dataset, den benutzerdefinierten Trainer und den Trainingsaufruf. Speichern Sie dies zusammen mit Ihrem dataset.yaml und führen Sie 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. 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",
                }
            )
        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)

Sie verfügen nun über ein minimales Dataset und einen Trainer, die Ultralytics YOLO direkt auf COCO JSON trainieren, wobei Anmerkungen die einzige Wahrheitsquelle bleiben und keine zusätzlichen .txt-Dateien erzeugt werden. Erweitern Sie die Methode cache_labels() um segments oder keypoints, um Segmentierung und Pose abzudecken, und lesen Sie den Leitfaden Tipps zum Modelltraining für Empfehlungen zur Hyperparameter- Abstimmung.

FAQ#

  • convert_coco() schreibt .txt-Label-Dateien als einmalige Konvertierung auf die Festplatte. Dieser Ansatz parst das JSON zu Beginn jedes Trainingslaufs und konvertiert Anmerkungen im Arbeitsspeicher. Verwenden Sie convert_coco(), wenn dauerhafte Labels im YOLO-Format bevorzugt werden; nutzen Sie diesen Ansatz, um das COCO JSON als einzige Wahrheitsquelle ohne Erstellung zusätzlicher Dateien beizubehalten.

  • Nicht mit der aktuellen Ultralytics-Pipeline, die standardmäßig YOLO .txt-Labels erwartet. Dieser Leitfaden bietet den minimalen benutzerdefinierten Code, der erforderlich ist — eine Dataset-Klasse und eine Trainer-Klasse. Nach der Definition erfordert das Training nur einen Standardaufruf von model.train().

  • Dieser Leitfaden behandelt die Objekterkennung. Um Unterstützung für Instanzsegmentierung hinzuzufügen, binden Sie die segmentation-Polygondaten aus COCO-Anmerkungen in das Feld segments jedes Label-Wörterbuchs ein. Schließen Sie für die Posen-Schätzung keypoints ein. Der Quellcode von GroundingDataset stellt eine Referenzimplementierung für die Handhabung von Segmenten bereit.

  • Ja. COCODataset erweitert YOLODataset, sodass alle integrierten DatenerweiterungenMosaic, Mixup, Copy-Paste und andere — unverändert laufen.

  • Kategorien werden nach id sortiert und auf sequentielle Indizes beginnend bei 0 abgebildet. Dies verarbeitet 1-basierte IDs (Standard-COCO), 0-basierte IDs und nicht-kontinuierliche IDs. Das Wörterbuch names in dataset.yaml sollte derselben sortierten Reihenfolge wie das COCO categories-Array folgen.

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

Kommentare