Meet YOLO26: next-gen vision AI.

Link to this sectionCome addestrare YOLO su COCO JSON senza conversione#

Link to this sectionPerché addestrare direttamente su COCO JSON#

Le annotazioni in formato COCO JSON possono essere utilizzate direttamente per l'addestramento di Ultralytics YOLO senza convertirle prima in file .txt. Questo si ottiene creando una sottoclasse di YOLODataset per analizzare il COCO JSON al volo e integrarla nella pipeline di addestramento tramite un trainer personalizzato.

Questo approccio mantiene il COCO JSON come unica fonte di verità: nessuna chiamata a convert_coco(), nessuna riorganizzazione delle directory, nessun file di etichetta intermedio. YOLO26 e tutti gli altri modelli di rilevamento Ultralytics YOLO sono supportati. I modelli di segmentazione e posa richiedono campi di etichetta aggiuntivi (vedi FAQ).

Cerchi invece una conversione una tantum?

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

Link to this sectionPanoramica dell'architettura#

Sono necessarie due classi:

  1. COCODataset: legge il file COCO JSON e converte i bounding box nel formato YOLO in memoria durante l'addestramento
  2. COCOTrainer: sovrascrive build_dataset() per utilizzare COCODataset invece del YOLODataset predefinito

L'implementazione segue lo stesso pattern di GroundingDataset integrato, che legge anch'esso le annotazioni JSON direttamente. Vengono sovrascritti tre metodi: get_img_files(), cache_labels() e get_labels().

Link to this sectionCreazione della classe dataset COCO JSON#

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

Il metodo get_img_files() restituisce un elenco vuoto perché i percorsi delle immagini vengono risolti dal campo file_name del JSON all'interno di cache_labels(). Gli ID delle categorie vengono ordinati e rimappati in indici di classe con base zero, quindi funzionano correttamente sia gli schemi ID basati su 1 (COCO standard) che quelli 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."""
        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"]

Le etichette analizzate vengono salvate in un file .cache accanto al JSON (es. instances_train.cache). Nelle esecuzioni di addestramento successive, la cache viene caricata direttamente, saltando l'analisi del JSON. Se il file JSON cambia, il controllo dell'hash fallisce e la cache viene ricostruita automaticamente.

Link to this sectionCollegamento del dataset alla pipeline di addestramento#

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

Il percorso del file JSON viene estratto da un campo personalizzato train_json / val_json nella configurazione dei dati (vedi Passaggio 3). Durante l'addestramento, mode="train" risolve in train_json; durante la validazione, mode="val" risolve in val_json. Se val_json non è impostato, si ricorre a 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):
        """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 sectionConfigurazione di dataset.yaml per COCO JSON#

Il file dataset.yaml utilizza i campi standard path, train e val per individuare le directory delle immagini. Due campi aggiuntivi, train_json e val_json, specificano i file di annotazione COCO letti da COCOTrainer. I campi nc e names definiscono il numero di classi e i relativi nomi, corrispondenti all'ordine ordinato delle categories nel 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

Struttura di directory prevista:

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

Link to this sectionEsecuzione dell'addestramento su COCO JSON#

Con la classe dataset, la classe trainer e la configurazione YAML pronte, l'addestramento avviene tramite la chiamata standard model.train(). L'unica differenza rispetto a un'esecuzione di addestramento normale è l'argomento trainer=COCOTrainer, che indica a Ultralytics di utilizzare 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 viene eseguita come previsto, inclusa la validazione, il salvataggio dei checkpoint e il logging delle metriche.

Link to this sectionImplementazione completa#

Per comodità, l'implementazione completa è fornita di seguito come script copia-incolla unico. Include il dataset personalizzato, il trainer personalizzato e la chiamata all'addestramento. Salvalo insieme al tuo dataset.yaml ed eseguilo 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."""
        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)

Per suggerimenti sugli iperparametri, consulta la guida ai Consigli per l'addestramento dei modelli.

Link to this sectionFAQ#

Link to this sectionQual è la differenza tra questo metodo e convert_coco()?#

convert_coco() scrive i file di etichette .txt su disco 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() quando preferisci etichette permanenti in formato YOLO; usa questo approccio per mantenere il COCO JSON come unica fonte di verità senza generare file aggiuntivi.

Link to this sectionYOLO può addestrarsi su COCO JSON senza codice personalizzato?#

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

Link to this sectionQuesto supporta la segmentazione e la stima della posa?#

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 sectionLe aumentazioni funzionano con questo dataset personalizzato?#

Sì. COCODataset estende YOLODataset, quindi tutte le aumentazioni di dati integrate — mosaic, mixup, copy-paste e altre — vengono eseguite senza modifiche.

Link to this sectionCome vengono mappati gli ID delle categorie agli indici di classe?#

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

Link to this sectionC'è un sovraccarico di prestazioni rispetto alle etichette pre-convertite?#

Il COCO JSON viene analizzato una volta alla prima esecuzione di addestramento. Le etichette analizzate vengono salvate in un file .cache, quindi le esecuzioni successive vengono caricate istantaneamente senza ri-analisi. La velocità di addestramento è identica all'addestramento YOLO standard poiché le annotazioni sono mantenute in memoria. La cache viene ricostruita automaticamente se il file JSON cambia.

Commenti