YOLO Vision 2026:

Como treinar YOLO em COCO JSON sem converter#

As anotações no formato COCO JSON podem ser usadas diretamente para o treinamento do Ultralytics YOLO sem a necessidade de converter para arquivos .txt primeiro. Isso funciona criando uma subclasse de YOLODataset para analisar o COCO JSON em tempo de execução e integrando-a ao pipeline de treinamento por meio de um personalizador de treinamento (trainer).

Por que treinar diretamente em COCO JSON#

Essa abordagem mantém o COCO JSON como a única fonte de verdade — sem chamadas a convert_coco(), sem reorganização de diretórios e sem arquivos de rótulo intermediários. O YOLO26 e todos os outros modelos de detecção do Ultralytics YOLO são suportados. Modelos de segmentação e pose exigem campos de rótulo adicionais (consulte o FAQ).

Procurando uma conversão única em vez disso?

Consulte o guia de conversão de COCO para YOLO para o fluxo de trabalho padrão com convert_coco().

Visão geral da arquitetura#

Duas classes são necessárias:

  1. COCODataset — lê o COCO JSON e converte as caixas delimitadoras (bounding boxes) para o formato YOLO na memória durante o treinamento.
  2. COCOTrainer — substitui build_dataset() para usar COCODataset em vez do padrão YOLODataset.

A implementação segue o mesmo padrão do componente integrado GroundingDataset, que também lê anotações JSON diretamente. Três métodos são sobrescritos: get_img_files(), cache_labels() e get_labels().

Criando a classe de conjunto de dados COCO JSON#

A classe COCODataset herda de YOLODataset e sobrescreve a lógica de carregamento de rótulos. Em vez de ler arquivos .txt de um diretório de rótulos, ela abre o arquivo COCO JSON, percorre as anotações agrupadas por imagem e converte cada caixa delimitadora do formato de pixel COCO [x_min, y_min, width, height] para o formato de centro normalizado do YOLO [x_center, y_center, width, height]. Anotações de multidão (iscrowd: 1) e caixas de área zero são ignoradas automaticamente.

O método get_img_files() retorna uma lista vazia porque os caminhos das imagens são resolvidos a partir do campo file_name do JSON dentro de cache_labels(). Os IDs de categoria são ordenados e remapeados para índices de classe iniciados em zero, de modo que tanto os esquemas de ID baseados em 1 (COCO padrão) quanto os não contíguos funcionem corretamente.

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

Os rótulos analisados são salvos em um arquivo .cache ao lado do JSON (por exemplo, instances_train.cache). Em execuções de treinamento subsequentes, o cache é carregado diretamente, pulando a análise do JSON. Se o arquivo JSON for alterado, a verificação de hash falhará e o cache será reconstruído automaticamente.

Conectando o conjunto de dados ao pipeline de treinamento#

A única alteração necessária no treinador é sobrescrever build_dataset(). O DetectionTrainer padrão cria um YOLODataset que busca arquivos de rótulo .txt. Ao substituí-lo por COCODataset, o treinador passa a ler a partir do COCO JSON.

O caminho do arquivo JSON é obtido de um campo personalizado train_json / val_json na configuração de dados (consulte Configurando o dataset.yaml). Durante o treinamento, mode="train" é resolvido para train_json; durante a validação, mode="val" é resolvido para val_json. Se val_json não estiver definido, ele recorre 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,
        )

Configurando dataset.yaml para COCO JSON#

O dataset.yaml usa os campos padrão path, train e val para localizar diretórios de imagens. Dois campos adicionais, train_json e val_json, especificam os arquivos de anotação COCO que o COCOTrainer lê. Os campos nc e names definem o número de classes e seus nomes, correspondendo à ordem ordenada de categories no 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

Estrutura de diretório esperada:

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

Executando o treinamento no COCO JSON#

Com a classe do dataset, a classe do treinador e a configuração YAML definidas, o treinamento funciona por meio da chamada padrão model.train(). A única diferença em relação a uma execução de treinamento normal é o argumento trainer=COCOTrainer, que instrui o Ultralytics a usar o carregador de dataset personalizado em vez do padrão.

from ultralytics import YOLO

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

O pipeline de treinamento completo é executado conforme o esperado, incluindo validação, salvamento de pontos de verificação (checkpoints) e registro de métricas.

Implementação completa#

Para sua conveniência, a implementação completa é fornecida abaixo como um único script pronto para copiar e colar. Ele inclui o dataset personalizado, o treinador personalizado e a chamada de treinamento. Salve isso junto com o seu dataset.yaml e execute-o diretamente.

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)

Agora você tem um dataset e um treinador mínimos que treinam o Ultralytics YOLO diretamente no COCO JSON, com as anotações permanecendo como a única fonte de verdade e sem arquivos intermediários .txt. Estenda o método cache_labels() com segments ou keypoints para cobrir segmentação e pose, e consulte o guia Dicas de Treinamento de Modelos para recomendações de ajuste de hiperparâmetros.

FAQ#

  • convert_coco() grava arquivos de rótulo .txt no disco como uma conversão única. Esta abordagem analisa o JSON no início de cada execução de treinamento e converte as anotações na memória. Use convert_coco() quando preferir rótulos permanentes no formato YOLO; use esta abordagem para manter o COCO JSON como a única fonte de verdade sem gerar arquivos adicionais.

  • Isso não é possível com o pipeline atual do Ultralytics, que espera rótulos YOLO .txt por padrão. Este guia fornece o código personalizado mínimo necessário — uma classe de dataset e uma classe de treinador. Uma vez definido, o treinamento requer apenas uma chamada padrão de model.train().

  • Este guia abrange a detecção de objetos. Para adicionar suporte a segmentação de instâncias, inclui os dados de polígono segmentation das anotações COCO no campo segments de cada dicionário de rótulos. Para estimativa de pose, inclui keypoints. O código-fonte GroundingDataset fornece uma implementação de referência para lidar com segmentos.

  • Sim. O COCODataset estende o YOLODataset, portanto, todas as aumentações de dados integradas — mosaic, mixup, copy-paste e outras — são executadas sem modificações.

  • As categorias são ordenadas por id e mapeadas para índices sequenciais a partir de 0. Isso lida com IDs baseados em 1 (COCO padrão), IDs baseados em 0 e IDs não contíguos. O dicionário names em dataset.yaml deve seguir a mesma ordem classificada da matriz COCO categories.

  • O COCO JSON é analisado uma vez na primeira execução de treinamento. Os rótulos analisados são salvos em um arquivo .cache, de modo que as execuções subsequentes são carregadas instantaneamente sem nova análise. A velocidade de treinamento é idêntica à do treinamento YOLO padrão, pois as anotações são mantidas na memória. O cache é reconstruído automaticamente se o arquivo JSON for alterado.

Comentários