YOLO Vision 2026:

Cómo entrenar YOLO con COCO JSON sin conversiones#

Las Annotations en formato COCO JSON se pueden utilizar directamente para el entrenamiento de Ultralytics YOLO sin necesidad de convertir primero a archivos .txt. Esto funciona mediante la creación de una subclase de YOLODataset para analizar el COCO JSON al vuelo e integrarlo en el flujo de entrenamiento a través de un entrenador personalizado.

¿Por qué entrenar directamente con COCO JSON?#

Este enfoque mantiene el COCO JSON como la única fuente de verdad: sin llamadas a convert_coco(), sin reorganización de directorios y sin archivos de etiquetas intermedios. YOLO26 y todos los demás modelos de detección de Ultralytics YOLO son compatibles. Los modelos de segmentación y pose requieren campos de etiquetas adicionales (consulta las FAQ).

¿Buscas una conversión de una sola vez?

Consulta la guía de conversión de COCO a YOLO para conocer el flujo de trabajo estándar de convert_coco().

Descripción general de la arquitectura#

Se necesitan dos clases:

  1. COCODataset: lee el COCO JSON y convierte las bounding boxes al formato YOLO en la memoria durante el entrenamiento.
  2. COCOTrainer: sobrescribe build_dataset() para usar COCODataset en lugar del valor predeterminado YOLODataset.

La implementación sigue el mismo patrón que el elemento integrado GroundingDataset, el cual también lee anotaciones JSON directamente. Se sobrescriben tres métodos: get_img_files(), cache_labels() y get_labels().

Cómo crear la clase de conjunto de datos COCO JSON#

La clase COCODataset hereda de YOLODataset y sobrescribe la lógica de carga de etiquetas. En lugar de leer archivos .txt desde un directorio de etiquetas, abre el archivo COCO JSON, itera sobre las anotaciones agrupadas por imagen y convierte cada cuadro delimitador desde el formato de píxeles de COCO [x_min, y_min, width, height] al formato de centro normalizado de YOLO [x_center, y_center, width, height]. Las anotaciones de multitud (iscrowd: 1) y las cajas con área cero se omiten automáticamente.

El método get_img_files() devuelve una lista vacía porque las rutas de las imágenes se resuelven a partir del campo file_name del JSON dentro de cache_labels(). Los ID de las categorías se ordenan y se reasignan a índices de clase indexados desde cero, por lo que funcionan correctamente tanto los esquemas de ID basados en 1 (COCO estándar) como los no contiguos.

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

Las etiquetas analizadas se guardan en un archivo .cache junto al JSON (por ejemplo, instances_train.cache). En las siguientes ejecuciones de entrenamiento, la caché se carga directamente, omitiendo el análisis del JSON. Si el archivo JSON cambia, la comprobación del hash falla y la caché se reconstruye automáticamente.

Cómo conectar el conjunto de datos al flujo de trabajo de entrenamiento#

El único cambio necesario en el entrenador es sobrescribir build_dataset(). El elemento predeterminado DetectionTrainer construye un YOLODataset que busca archivos de etiquetas .txt. Al reemplazarlo por COCODataset, el entrenador lee directamente desde el COCO JSON.

La ruta del archivo JSON se obtiene de un campo personalizado train_json / val_json en la configuración de datos (consulta Configuring dataset.yaml). Durante el entrenamiento, mode="train" se resuelve en train_json; durante la validación, mode="val" se resuelve en val_json. Si val_json no está configurado, recurre por defecto 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,
        )

Cómo configurar dataset.yaml para COCO JSON#

El elemento dataset.yaml utiliza los campos estándar path, train y val para localizar los directorios de imágenes. Dos campos adicionales, train_json y val_json, especifican los archivos de anotación de COCO que lee COCOTrainer. Los campos nc y names definen el número de clases y sus nombres, coincidiendo con el orden ordenado de categories en el 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

Estructura de directorios esperada:

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

Ejecución del entrenamiento en COCO JSON#

Con la clase de conjunto de datos, la clase de entrenador y la configuración YAML listas, el entrenamiento funciona mediante la llamada estándar a model.train(). La única diferencia con una ejecución de entrenamiento normal es el argumento trainer=COCOTrainer, que le indica a Ultralytics que utilice el cargador de datos personalizado en lugar del predeterminado.

from ultralytics import YOLO

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

El flujo de entrenamiento completo se ejecuta según lo previsto, incluyendo la validación, el guardado de puntos de control y el registro de métricas.

Implementación completa#

Para mayor comodidad, la implementación completa se proporciona a continuación como un único script para copiar y pegar. Incluye el conjunto de datos personalizado, el entrenador personalizado y la llamada de entrenamiento. Guarda esto junto con tu dataset.yaml y ejecútalo directamente.

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)

Ahora dispones de un conjunto de datos y un entrenador mínimos para entrenar Ultralytics YOLO directamente en COCO JSON, manteniendo las anotaciones como la única fuente de verdad y sin archivos intermedios .txt. Amplía el método cache_labels() con segments o keypoints para cubrir la segmentación y la pose, y consulta la guía Model Training Tips para obtener recomendaciones sobre el ajuste de hiperparámetros.

FAQ#

  • convert_coco() escribe archivos de etiquetas .txt en el disco como una conversión única. Este enfoque analiza el JSON al inicio de cada ejecución de entrenamiento y convierte las anotaciones en la memoria. Usa convert_coco() cuando prefieras etiquetas permanentes en formato YOLO; utiliza este enfoque para mantener el COCO JSON como la única fuente de verdad sin generar archivos adicionales.

  • No con el flujo actual de Ultralytics, que espera etiquetas de YOLO .txt por defecto. Esta guía proporciona el código personalizado mínimo necesario: una clase de conjunto de datos y una clase de entrenador. Una vez definidas, el entrenamiento solo requiere una llamada estándar a model.train().

  • Esta guía cubre la detección de objetos. Para añadir soporte de segmentación de instancias, incluye los datos de polígonos de segmentation de las anotaciones de COCO en el campo segments de cada diccionario de etiquetas. Para la estimación de poses, incluye keypoints. El código fuente de GroundingDataset proporciona una implementación de referencia para gestionar segmentos.

  • Sí. COCODataset amplía YOLODataset, por lo que todas las aumentaciones de datos integradas —como mosaic, mixup, copy-paste y otras— se ejecutan sin modificaciones.

  • Las categorías se ordenan por id y se asignan a índices secuenciales que comienzan en 0. Esto gestiona ID basados en 1 (COCO estándar), ID basados en 0 e ID no contiguos. El diccionario names en dataset.yaml debe seguir el mismo orden ordenado que la matriz categories de COCO.

  • El COCO JSON se analiza una vez en la primera ejecución del entrenamiento. Las etiquetas analizadas se guardan en un archivo .cache, por lo que las ejecuciones posteriores se cargan al instante sin necesidad de volver a analizarlas. La velocidad de entrenamiento es idéntica a la del entrenamiento estándar de YOLO, ya que las anotaciones se mantienen en la memoria. La caché se reconstruye automáticamente si el archivo JSON cambia.

Comentarios