Ultralytics YOLO27:
Get Started

Come addestrare YOLO su COCO JSON senza conversione#

Le annotazioni in formato COCO JSON possono essere usate direttamente per l'addestramento di Ultralytics YOLO senza convertirle prima in file .txt. A tale scopo, si crea una sottoclasse di YOLODataset per analizzare COCO JSON al volo e integrarla nella pipeline di addestramento tramite un trainer personalizzato.

Perché addestrare direttamente su COCO JSON#

Questo approccio mantiene il JSON COCO come unica fonte attendibile: niente chiamate convert_coco(), nessuna riorganizzazione delle directory e nessun file di etichette intermedio. YOLO26 e tutti gli altri modelli di rilevamento Ultralytics YOLO sono supportati. I modelli di segmentazione e posa richiedono campi di etichetta aggiuntivi (consulta le FAQ).

Cerchi invece una conversione una tantum?

Consulta la guida alla conversione da COCO a YOLO per il workflow standard convert_coco().

Panoramica dell'architettura#

Servono due classi:

  1. COCODataset — legge il JSON COCO e converte le bounding box nel formato YOLO in memoria durante l'addestramento
  2. COCOTrainer — esegue l'override di build_dataset() per usare COCODataset invece di YOLODataset, il valore predefinito

L'implementazione è una versione semplificata di GroundingDataset, integrato, che legge anch'esso direttamente le annotazioni JSON. Qui vengono sovrascritti tre metodi: get_img_files(), cache_labels() e get_labels(); GroundingDataset ne sovrascrive altri, inclusi i controlli personalizzati dell'hash della cache e del conteggio delle istanze.

Creazione della classe del dataset JSON COCO#

La classe COCODataset eredita da YOLODataset e sovrascrive la logica di caricamento delle etichette. Invece di leggere i file .txt da una directory delle etichette, apre il file JSON COCO, scorre le annotazioni raggruppate per immagine e converte ogni bounding box dal formato pixel COCO [x_min, y_min, width, height] al formato YOLO con centro normalizzato [x_center, y_center, width, height]. Le annotazioni di tipo crowd (iscrowd: 1) e le box con area zero vengono ignorate automaticamente.

Il metodo get_img_files() restituisce una lista vuota perché i percorsi delle immagini vengono risolti dal campo JSON file_name all'interno di cache_labels(). Gli ID delle categorie vengono ordinati e rimappati su indici di classe a partire da zero, quindi funzionano correttamente sia gli schemi con ID a partire da 1 (COCO standard) sia quelli con ID non contigui.

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

Le etichette analizzate vengono salvate in un file .cache accanto al JSON (ad es. instances_train.cache). Nelle esecuzioni successive dell'addestramento, la cache viene caricata direttamente e l'analisi del JSON viene saltata.

La chiave della cache è la dimensione del file JSON, non il suo contenuto

get_hash() calcola l'hash delle dimensioni e dei percorsi dei file, non del loro contenuto: una nuova esecuzione analizza nuovamente il JSON solo se cambia il numero di byte del file JSON. L'aggiunta o la rimozione di immagini può anche modificare la dimensione della directory delle immagini e attivare una ricostruzione, ma non farci affidamento: l'hash non esamina mai i singoli file immagine, quindi la sostituzione di un'immagine con un'altra può lasciare invariata la dimensione. Una modifica che mantiene invariato il numero di byte — spostare una coordinata, invertire iscrowd, sostituire due nomi di classe della stessa lunghezza — lascia in uso la cache obsoleta e addestra il modello con le vecchie annotazioni senza alcun avviso; anche la sostituzione di un'immagine sul posto è invisibile per lo stesso motivo. Dopo aver modificato le annotazioni o le immagini sul posto, elimina il file .cache.

Collegamento del dataset alla pipeline di addestramento#

L'unica modifica necessaria nel trainer è sovrascrivere build_dataset(). Il valore predefinito DetectionTrainer crea un YOLODataset che cerca i file di etichette .txt. Sostituendolo con COCODataset, il trainer legge invece dal JSON COCO.

Il percorso del file JSON viene recuperato da un campo personalizzato train_json / val_json nella configurazione dei dati (consulta Configurazione di dataset.yaml). Durante l'addestramento, mode="train" viene risolto in train_json; durante la convalida, mode="val" viene risolto in val_json. Sono necessarie entrambe le chiavi: le due suddivisioni leggono directory di immagini diverse, quindi il JSON di addestramento non può sostituire un val_json mancante.

Il dataset reimposta anche fraction su 1.0. BaseDataset applica questo argomento durante la scansione di una directory di immagini, passaggio che COCODataset salta, quindi non può rispettare una richiesta di dataset parziale; reimpostarlo evita che il dataset sembri accettare un valore che ignora. Per lo stesso motivo, il valore integrato GroundingDataset adotta la stessa soluzione.

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

Configurazione di dataset.yaml per il JSON COCO#

dataset.yaml usa i campi standard path, train e val per individuare le directory delle immagini. Nota che qui path punta alla directory radice delle immagini, quindi train e val sono nomi semplici delle suddivisioni, a differenza della guida alla conversione, in cui path è la directory radice del dataset e le suddivisioni hanno il prefisso images/. Due campi aggiuntivi, train_json e val_json, specificano i file di annotazione COCO letti da COCOTrainer. Il campo names elenca i nomi delle classi nell'ordine ordinato di categories nel JSON; il numero di classi viene ricavato da questo campo, quindi non è necessario impostare nc.

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

Struttura delle directory prevista:

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

Esecuzione dell'addestramento sul JSON COCO#

Una volta predisposte la classe del dataset, la classe del trainer e la configurazione YAML, l'addestramento funziona con la chiamata standard model.train(). L'unica differenza rispetto a una normale esecuzione di addestramento è l'argomento trainer=COCOTrainer, che indica a Ultralytics di usare il caricatore di dataset personalizzato invece di quello predefinito.

from ultralytics import YOLO

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

L'intera pipeline di addestramento funziona come previsto, inclusi la convalida durante l'addestramento, il salvataggio dei checkpoint e la registrazione delle metriche.

La chiamata `model.val()` autonoma richiede un override dedicato

Solo la convalida durante l'addestramento passa per COCOTrainer.build_dataset. Una chiamata separata a model.val() crea il YOLODataset standard, che cerca le etichette .txt accanto alle immagini e non ne trova. Non genera errori: le immagini vengono conteggiate come sfondi, quindi la convalida termina e riporta tutte le metriche come 0, con gli avvisi No labels found in ... e no labels found in detect set, cannot compute metrics without labels. Per convalidare al di fuori di un'esecuzione di addestramento, crea una sottoclasse del validator con lo stesso override build_dataset e passala a model.val(validator=...).

Implementazione completa#

Per comodità, di seguito trovi l'implementazione completa in un unico script da copiare e incollare. Include il dataset personalizzato, il trainer personalizzato e la chiamata di addestramento. Salvala accanto al tuo dataset.yaml ed eseguila direttamente.

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)

Ora hai un dataset e un trainer essenziali che addestrano Ultralytics YOLO direttamente sul JSON COCO, mantenendo le annotazioni come unica fonte attendibile e senza file .txt intermedi. Estendi il metodo cache_labels() con segments o keypoints per gestire segmentazione e posa, e consulta la guida Consigli per l'addestramento dei modelli per suggerimenti sulla regolazione degli iperparametri.

Domande frequenti#

  • convert_coco() scrive su disco i file di etichette .txt come conversione una tantum. Questo approccio analizza il JSON all'inizio di ogni esecuzione di addestramento e converte le annotazioni in memoria. Usa convert_coco() se preferisci mantenere in modo permanente le etichette in formato YOLO; usa questo approccio per mantenere il JSON COCO come unica fonte attendibile senza generare file aggiuntivi.

  • Non con la pipeline Ultralytics attuale, che per impostazione predefinita si aspetta etichette YOLO .txt. Questa guida fornisce il codice personalizzato minimo necessario: una classe del dataset e una classe del trainer. Una volta definite, basta una chiamata standard model.train() per avviare l'addestramento.

  • Questa guida tratta il rilevamento degli oggetti. Per aggiungere il supporto alla segmentazione delle istanze, includi i dati dei poligoni segmentation delle annotazioni COCO nel campo segments di ogni dizionario delle etichette. Per la stima della posa, includi keypoints. Il codice sorgente di GroundingDataset fornisce un'implementazione di riferimento per la gestione dei segmenti.

  • Sì. COCODataset estende YOLODataset, quindi tutte le augmentations dei dati integrate — mosaic, mixup, CutMix e altre — funzionano senza modifiche.

  • Le categorie vengono ordinate in base a id e mappate su indici sequenziali a partire da 0. Questo gestisce gli ID a partire da 1 (COCO standard), gli ID a partire da 0 e gli ID non contigui. Il dizionario names in dataset.yaml deve seguire lo stesso ordine ordinato dell'array COCO categories.

  • Il JSON COCO viene analizzato una volta alla prima esecuzione dell'addestramento. Le etichette analizzate vengono salvate in un file .cache, quindi le esecuzioni successive lo caricano subito senza ripetere l'analisi. La velocità di addestramento è identica a quella dell'addestramento YOLO standard, poiché le annotazioni sono mantenute in memoria. La cache è indicizzata in base alla dimensione del file JSON, quindi elimina il file .cache dopo qualsiasi modifica che lasci invariata la lunghezza del file.

Commenti