Como treinar YOLO com COCO JSON sem converter
Por que treinar diretamente em COCO JSON
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 é feito criando uma subclasse de YOLODataset para analisar o COCO JSON dinamicamente e conectá-lo ao pipeline de treinamento por meio de um treinador personalizado.
Esta abordagem mantém o COCO JSON como a única fonte da verdade — sem chamadas convert_coco(), sem reorganização de diretórios e sem arquivos de rótulos 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 (veja o FAQ).
Consulte o guia de conversão de COCO para YOLO para o fluxo de trabalho padrão convert_coco().
Visão Geral da Arquitetura
Duas classes são necessárias:
COCODataset— lê o COCO JSON e converte caixas delimitadoras para o formato YOLO na memória durante o treinamentoCOCOTrainer— substituibuild_dataset()para usarCOCODatasetem vez do padrãoYOLODataset
A implementação segue o mesmo padrão da GroundingDataset integrada, 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 Dataset para 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, itera sobre as anotações agrupadas por imagem e converte cada caixa delimitadora do formato de pixel do COCO [x_min, y_min, width, height] para o formato central 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 das categorias são classificados e remapeados para índices de classe de base zero, portanto, tanto os esquemas de ID baseados em 1 (padrão COCO) quanto os não contíguos funcionam 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):
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"]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, ignorando a análise do JSON. Se o arquivo JSON for alterado, a verificação de hash falha e o cache é reconstruído automaticamente.
Conectando o Dataset ao pipeline de treinamento
A única alteração necessária no treinador é sobrescrever build_dataset(). O DetectionTrainer padrão constrói um YOLODataset que procura arquivos de rótulo .txt. Ao substituí-lo por COCODataset, o treinador lê o COCO JSON.
O caminho do arquivo JSON é extraído de um campo personalizado train_json / val_json na configuração de dados (veja a Etapa 3). Durante o treinamento, mode="train" resolve para train_json; durante a validação, mode="val" resolve para val_json. Se val_json não estiver definido, ele volta para 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,
)Configurando dataset.yaml para COCO JSON
O dataset.yaml usa os campos padrão path, train e val para localizar diretórios de imagem. 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 classificada das categories no 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 namesEstrutura de diretório esperada:
my_dataset/
images/
train/
img_001.jpg
...
val/
img_100.jpg
...
annotations/
instances_train.json
instances_val.json
dataset.yamlExecutando o treinamento com COCO JSON
Com a classe de dataset, classe de treinador e configuração YAML no lugar, o treinamento funciona através da chamada padrão model.train(). A única diferença de uma execução de treinamento normal é o argumento trainer=COCOTrainer, que diz ao Ultralytics para 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 completo de treinamento é executado conforme o esperado, incluindo validação, salvamento de checkpoint e registro de métricas.
Implementação completa
Para conveniência, a implementação completa é fornecida abaixo como um script simples de copiar e colar. Ele inclui o dataset personalizado, o treinador personalizado e a chamada de treinamento. Salve isso junto com 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):
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)Para recomendações de hiperparâmetros, consulte o guia Dicas de treinamento de modelos.
FAQ
Qual é a diferença entre isso e o convert_coco()?
convert_coco() grava arquivos de rótulo .txt no disco como uma conversão única. Essa 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 rótulos permanentes no formato YOLO forem preferidos; use esta abordagem para manter o COCO JSON como a única fonte da verdade sem gerar arquivos adicionais.
O YOLO pode treinar em COCO JSON sem código personalizado?
Não 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 definidas, o treinamento requer apenas uma chamada padrão model.train().
Isso suporta segmentação e estimativa de pose?
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.
As ampliações (augmentations) funcionam com este dataset personalizado?
Sim. COCODataset estende YOLODataset, então todas as ampliações de dados integradas — mosaic, mixup, copy-paste e outras — são executadas sem modificação.
Como os IDs de categoria são mapeados para índices de classe?
As categorias são classificadas por id e mapeadas para índices sequenciais começando em 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 no dataset.yaml deve seguir a mesma ordem classificada do array categories do COCO.
Existe uma sobrecarga de desempenho em comparação com rótulos pré-convertidos?
O COCO JSON é analisado uma vez na primeira execução de treinamento. Os rótulos analisados são salvos em um arquivo .cache, portanto, execuções subsequentes carregam instantaneamente sem reanálise. A velocidade de treinamento é idêntica ao treinamento YOLO padrão, já que as anotações são mantidas na memória. O cache é reconstruído automaticamente se o arquivo JSON for alterado.