Como treinar YOLO em COCO JSON sem converter
Por que treinar diretamente no COCO JSON
mais limpa em COCO JSON O formato pode ser usado diretamente para Ultralytics YOLO treinamento sem converter para .txt arquivos primeiro. Isso é feito criando uma subclasse de YOLODataset para processar o COCO JSON em tempo de execução e conectando-o ao pipeline de treinamento por meio de um trainer personalizado.
Essa abordagem mantém o COCO JSON como a única fonte da verdade — sem convert_coco() chamadas, sem reorganização de diretórios, sem arquivos de label intermediários. YOLO26 e todos os outros modelos de detecção Ultralytics YOLO são suportados. Modelos de segmentação e pose exigem campos de label adicionais (veja FAQ).
Veja a Guia de conversão de COCO para YOLO para o convert_coco() fluxo de trabalho.
Visão Geral da Arquitetura
Duas classes são necessárias:
COCODataset— lê o COCO JSON e converte bounding boxes 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 do GroundingDataset integrado, que também lê anotações JSON diretamente. Três métodos são sobrescritos: get_img_files(), cache_labels(), e get_labels().
Construindo a classe do Dataset COCO JSON
O argumento COCODataset classe herda de YOLODataset e sobrescreve a lógica de carregamento de labels. Em vez de ler .txt arquivos de um diretório de labels, ele abre o arquivo COCO JSON, itera sobre as anotações agrupadas por imagem e converte cada BBox do formato de pixel COCO [x_min, y_min, width, height] para o formato de centro normalizado YOLO [x_center, y_center, width, height]. Anotações de grupo (iscrowd: 1) e caixas de área zero são ignoradas automaticamente.
O argumento get_img_files() método retorna uma lista vazia porque os caminhos das imagens são resolvidos a partir do campo file_name dentro de cache_labels(). Os IDs das categorias são classificados e remapeados para índices de classe de base zero, portanto, esquemas de ID baseados em 1 (COCO padrão) e 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 labels processados são salvos em um .cache arquivo próximo ao JSON (por exemplo, instances_train.cache). Em execuções de treinamento subsequentes, o cache é carregado diretamente, ignorando o processamento 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 mudança necessária no trainer é sobrescrever build_dataset(). O padrão DetectionTrainer constrói um YOLODataset que verifica arquivos de label .txt. Ao substituí-lo por COCODataset, o trainer lê do COCO JSON em vez disso.
O caminho do arquivo JSON é extraído de um campo personalizado train_json / val_json na configuração de dados (veja o Passo 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 argumento 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 COCOTrainer lê. Os campos nc e names definem o número de classes e seus nomes, correspondendo à ordem classificada de 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órios esperada:
my_dataset/
images/
train/
img_001.jpg
...
val/
img_100.jpg
...
annotations/
instances_train.json
instances_val.json
dataset.yamlExecutando o treinamento no COCO JSON
Com a classe do dataset, classe do trainer e a configuração YAML prontas, 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 training é executado conforme o esperado, incluindo validação, salvamento de checkpoints e registro de métricas.
Implementação completa
Para conveniência, a implementação completa é fornecida abaixo como um script único de copiar e colar. Inclui o dataset personalizado, trainer personalizado e a chamada de treinamento. Salve isso ao lado do 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 hiperparâmetro recomendações, veja as Dicas de treinamento de modelos.
FAQ
Qual é a diferença entre isso e convert_coco()?
convert_coco() escreve arquivos de label .txt no disco como uma conversão única. Esta abordagem processa o JSON no início de cada execução de treinamento e converte as anotações na memória. Use convert_coco() quando labels 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 da Ultralytics, que espera labels YOLO .txt por padrão. Este guia fornece o código personalizado mínimo necessário — uma classe de dataset e uma classe de trainer. Uma vez definido, o treinamento requer apenas uma chamada padrão model.train().
Isso suporta segmentação e estimativa de pose?
Este guia cobre object detection. Para adicionar suporte a segmentação de instâncias, inclua os dados de polígono segmentation das anotações COCO no campo segments de cada dicionário de label. Para estimativa de pose, inclua keypoints. O GroundingDataset código-fonte fornece uma implementação de referência para lidar com segmentos.
As aumentações funcionam com este dataset personalizado?
Sim. COCODataset estende YOLODataset, então todas as data augmentations — mosaic, mixup, copy-paste integradas e outras — funcionam sem modificação.
Como os IDs das categorias são mapeados para índices de classe?
As categorias são classificadas por id e mapeadas para índices sequenciais começando 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 do array COCO categories.
Existe uma sobrecarga de desempenho em comparação com labels pré-convertidos?
O COCO JSON é processado uma vez na primeira execução de treinamento. Os labels processados são salvos em um .cache arquivo, então as execuções subsequentes carregam instantaneamente sem reprocessar. A velocidade de treinamento é idêntica ao treinamento padrão YOLO, já que as anotações são mantidas na memória. O cache é reconstruído automaticamente se o arquivo JSON for alterado.