Skip to main content

Comment entraîner YOLO sur du JSON COCO sans conversion

Pourquoi entraîner directement sur du JSON COCO

plus propre. dans COCO JSON le format peut être utilisé directement pour Ultralytics YOLO l'entraînement sans conversion préalable en fichiers .txt. Cela se fait en créant une sous-classe de YOLODataset pour analyser le JSON COCO à la volée et en l'intégrant dans le pipeline d'entraînement via un entraîneur personnalisé.

Cette approche maintient le JSON COCO comme source unique de vérité — pas d'appel convert_coco() , pas de réorganisation de répertoire, pas de fichiers d'étiquettes intermédiaires. YOLO26 et tous les autres modèles de détection Ultralytics YOLO sont pris en charge. Les modèles de segmentation et de pose nécessitent des champs d'étiquettes supplémentaires (voir FAQ).

Tu cherches plutôt une conversion unique ?

Consulte la Guide de conversion COCO vers YOLO pour le convert_coco() workflow.

Aperçu de l'architecture

Deux classes sont nécessaires :

  1. COCODataset — lit le JSON COCO et convertit bounding boxes au format YOLO en mémoire pendant l'entraînement
  2. COCOTrainer — remplace build_dataset() pour utiliser COCODataset au lieu du YOLODataset

par défaut. L'implémentation suit le même modèle que le GroundingDataset intégré, qui lit aussi directement les annotations JSON. Trois méthodes sont remplacées : get_img_files(), cache_labels(), et la get_labels().

Construction de la classe de jeu de données JSON COCO

La méthode COCODataset la classe hérite de YOLODataset et remplace la logique de chargement des étiquettes. Au lieu de lire les fichiers .txt depuis un répertoire d'étiquettes, elle ouvre le fichier JSON COCO, itère sur les annotations regroupées par image et convertit chaque boîte englobante du format pixel COCO [x_min, y_min, width, height] au format centre normalisé YOLO [x_center, y_center, width, height]. Les annotations de foule (iscrowd: 1) et les boîtes de zone nulle sont sautées automatiquement.

La méthode get_img_files() la méthode renvoie une liste vide car les chemins des images sont résolus à partir du champ file_name dans cache_labels(). Les identifiants de catégorie sont triés et remappés vers des indices de classe commençant à zéro, donc les schémas d'identifiants basés sur 1 (COCO standard) ou non contigus fonctionnent correctement.

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

Les étiquettes analysées sont enregistrées dans un fichier .cache à côté du JSON (par ex. instances_train.cache). Lors des entraînements suivants, le cache est chargé directement, sautant l'analyse JSON. Si le fichier JSON change, la vérification du hash échoue et le cache est reconstruit automatiquement.

Connexion du jeu de données au pipeline d'entraînement

Le seul changement nécessaire dans l'entraîneur est de remplacer build_dataset(). Le DetectionTrainer par défaut construit un YOLODataset qui scanne les fichiers d'étiquettes .txt. En le remplaçant par COCODataset, l'entraîneur lit le JSON COCO à la place.

Le chemin du fichier JSON est extrait d'un champ personnalisé train_json / val_json dans la configuration des données (voir étape 3). Pendant l'entraînement, mode="train" résout vers train_json; pendant la validation, mode="val" résout vers val_json. Si val_json n'est pas défini, il revient à train_json.

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):
        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,
        )

Configuration de dataset.yaml pour JSON COCO

La méthode dataset.yaml utilise les champs standard path, train, et la val pour localiser les répertoires d'images. Deux champs supplémentaires, train_json et en val_json, spécifient les fichiers d'annotations COCO que COCOTrainer lit. Les champs nc et en names définissent le nombre de classes et leurs noms, correspondant à l'ordre trié de categories dans le 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

Structure de répertoire attendue :

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

Exécution de l'entraînement sur JSON COCO

Avec la classe de jeu de données, la classe d'entraîneur et la configuration YAML en place, l'entraînement fonctionne via l'appel standard model.train(). La seule différence avec un entraînement normal est l'argument trainer=COCOTrainer qui dit à Ultralytics d'utiliser le chargeur de données personnalisé plutôt que celui par défaut.

from ultralytics import YOLO

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

Le pipeline complet entraînement s'exécute comme prévu, incluant validation, la sauvegarde des points de contrôle et la journalisation des métriques.

Implémentation complète

Par souci de commodité, l'implémentation complète est fournie ci-dessous sous forme d'un script unique à copier-coller. Il inclut le jeu de données personnalisé, l'entraîneur personnalisé et l'appel d'entraînement. Enregistre ceci à côté de ton dataset.yaml et exécute-le directement.

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):
        self.json_file = json_file
        super().__init__(*args, data={"channels": 3}, **kwargs)

    def get_img_files(self, img_path):
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        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):
        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):
        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)

Pour les hyperparamètre pour des recommandations, voir le Conseils d'entraînement de modèle.

FAQ

Quelle est la différence entre ceci et convert_coco() ?

convert_coco() écrit des fichiers d'étiquettes .txt sur le disque comme une conversion unique. Cette approche analyse le JSON au début de chaque entraînement et convertit les annotations en mémoire. Utilise convert_coco() quand des étiquettes au format YOLO permanent sont préférées ; utilise cette approche pour garder le JSON COCO comme source unique de vérité sans générer de fichiers supplémentaires.

YOLO peut-il s'entraîner sur JSON COCO sans code personnalisé ?

Pas avec le pipeline Ultralytics actuel, qui attend des étiquettes .txt YOLO par défaut. Ce guide fournit le code personnalisé minimal nécessaire — une classe de jeu de données et une classe d'entraîneur. Une fois défini, l'entraînement ne nécessite qu'un appel standard model.train().

Cela prend-il en charge la segmentation et l'estimation de pose ?

Ce guide couvre object detection. Pour ajouter la prise en charge de la segmentation d'instances, inclut les données de polygone segmentation des annotations COCO dans le champ segments de chaque dictionnaire d'étiquettes. Pour , l'estimation de pose, le , inclut keypoints. Le GroundingDataset code source fournit une implémentation de référence pour gérer les segments.

Les augmentations fonctionnent-elles avec ce jeu de données personnalisé ?

Oui. COCODataset étend YOLODataset, donc toutes les fonctions intégrées augmentations de donnéesmosaic, mixup, copy-paste, et d'autres — s'exécutent sans modification.

Comment les identifiants de catégorie sont-ils mappés aux indices de classe ?

Les catégories sont triées par id et mappées vers des indices séquentiels commençant à 0. Cela gère les identifiants basés sur 1 (COCO standard), basés sur 0 et les identifiants non contigus. Le dictionnaire names dans dataset.yaml doit suivre le même ordre trié que le tableau categories de COCO.

Y a-t-il une surcharge de performance par rapport aux étiquettes pré-converties ?

Le JSON COCO est analysé une fois lors du premier entraînement. Les étiquettes analysées sont enregistrées dans un fichier .cache, donc les exécutions suivantes chargent instantanément sans ré-analyser. La vitesse d'entraînement est identique à l'entraînement YOLO standard puisque les annotations sont conservées en mémoire. Le cache est reconstruit automatiquement si le fichier JSON change.

Commentaires