Como treinar YOLO com COCO JSON sem converter#
As anotações no formato COCO JSON podem ser usadas diretamente no treinamento de Ultralytics YOLO, sem convertê-las primeiro em arquivos .txt. Isso é feito criando uma subclasse de YOLODataset para analisar o COCO JSON em tempo real e conectá-la ao pipeline de treinamento por meio de um treinador personalizado.
Por que treinar diretamente com COCO JSON#
Esta abordagem mantém o JSON do COCO como a única fonte da verdade — sem chamada a convert_coco(), sem reorganização de diretórios e sem arquivos intermediários de rótulos. YOLO26 e todos os outros modelos de detecção YOLO da Ultralytics são compatíveis. Os modelos de segmentação e pose exigem campos de rótulo adicionais (consulte FAQ).
Consulte o guia de conversão de COCO para YOLO para ver o fluxo de trabalho padrão convert_coco().
Visão geral da arquitetura#
São necessárias duas classes:
COCODataset— lê o JSON do COCO e converte caixas delimitadoras para o formato YOLO na memória durante o treinamentoCOCOTrainer— substituibuild_dataset()para usarCOCODatasetem vez doYOLODatasetpadrão
A implementação é uma versão simplificada de GroundingDataset, que também lê as anotações JSON diretamente. Aqui, três métodos são substituídos — get_img_files(), cache_labels() e get_labels() — enquanto GroundingDataset substitui mais métodos, incluindo as próprias verificações de hash do cache e de contagem de instâncias.
Criando a classe de conjunto de dados COCO JSON#
A classe COCODataset herda de YOLODataset e substitui a lógica de carregamento dos rótulos. Em vez de ler arquivos .txt de um diretório de rótulos, ela abre o arquivo JSON do COCO, percorre as anotações agrupadas por imagem e converte cada caixa delimitadora do formato de pixels COCO [x_min, y_min, width, height] para o formato YOLO normalizado com centro [x_center, y_center, width, height]. As anotações de multidão (iscrowd: 1) e as caixas com á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 JSON file_name dentro de cache_labels(). Os IDs das categorias são ordenados e remapeados para índices de classe começando em zero, permitindo que esquemas de IDs iniciados em 1 (padrão COCO) e esquemas de IDs 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."""
self.fraction = 1.0 # fraction is applied while scanning a directory, which this dataset skips
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",
}
)
if not x["labels"]:
raise RuntimeError(f"No images listed in {self.json_file} were found in {self.img_path}")
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). Nas execuções de treinamento seguintes, o cache é carregado diretamente, sem analisar o JSON novamente.
get_hash() calcula o hash dos tamanhos e caminhos dos arquivos, não do conteúdo dos arquivos. Portanto, uma nova execução só analisa o JSON novamente quando a contagem de bytes do JSON muda. Adicionar ou remover imagens também pode alterar o tamanho do próprio diretório de imagens e acionar uma recriação, mas não conte com isso — o hash nunca inspeciona arquivos de imagem individuais, então trocar uma imagem por outra pode manter o tamanho inalterado. Uma edição que preserve a contagem de bytes — ajustar uma coordenada, inverter iscrowd ou trocar dois nomes de classe com o mesmo comprimento — mantém o cache desatualizado e treina com as anotações antigas, sem aviso. Pelo mesmo motivo, substituir uma imagem no mesmo local não é detectado. Exclua o arquivo .cache depois de editar anotações ou imagens no mesmo local.
Conectando o conjunto de dados ao pipeline de treinamento#
A única alteração necessária no treinador é substituir build_dataset(). O DetectionTrainer padrão cria um YOLODataset que procura arquivos de rótulos .txt. Ao substituí-lo por COCODataset, o treinador passa a ler do JSON do COCO.
O caminho do arquivo JSON vem de um campo personalizado train_json / val_json na configuração dos dados (consulte Configurando dataset.yaml). Durante o treinamento, mode="train" é resolvido como train_json; durante a validação, mode="val" é resolvido como val_json. As duas chaves são obrigatórias — as duas divisões leem diretórios de imagens diferentes, então o JSON de treinamento não pode substituir um val_json ausente.
O conjunto de dados também redefine fraction para 1.0. BaseDataset aplica esse argumento ao percorrer um diretório de imagens, uma etapa ignorada por COCODataset; por isso, não consegue respeitar uma solicitação de subconjunto do conjunto de dados. Redefini-lo impede que o conjunto de dados pareça aceitar um valor que ignora. O GroundingDataset integrado faz a mesma concessão pelo mesmo motivo.
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["val_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#
dataset.yaml usa os campos padrão path, train e val para localizar diretórios de imagens. Observe que path aponta para a raiz das imagens aqui, então train e val são nomes de divisão sem prefixo — ao contrário do guia de conversão, em que path é a raiz do conjunto de dados e as divisões têm o prefixo images/. Dois campos adicionais, train_json e val_json, especificam os arquivos de anotação COCO que COCOTrainer lê. O campo names lista os nomes das classes na ordem ordenada de categories no JSON, e a quantidade de classes é derivada dessa lista; portanto, não é necessário definir nc.
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
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 com COCO JSON#
Com a classe do conjunto de dados, a classe do treinador e a configuração YAML prontas, o treinamento funciona com a 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 a Ultralytics a usar o carregador personalizado do conjunto de dados 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 funciona como esperado, incluindo validação durante o treinamento, salvamento de pontos de verificação e registro de métricas.
Apenas a validação durante o treinamento passa por COCOTrainer.build_dataset. Uma chamada separada a model.val() cria o YOLODataset padrão, que procura rótulos .txt ao lado das imagens e não encontra nenhum. A chamada não gera um erro: as imagens são contabilizadas como fundos, então a validação é concluída e informa todas as métricas como 0, com os avisos No labels found in ... e no labels found in detect set, cannot compute metrics without labels. Para validar fora de uma execução de treinamento, crie uma subclasse do validador com a mesma substituição build_dataset e passe-a para model.val(validator=...).
Implementação completa#
Para facilitar, a implementação completa é apresentada abaixo em um único script pronto para copiar e colar. Ela inclui o conjunto de dados personalizado, o treinador personalizado e a chamada de treinamento. Salve o script 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):
"""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."""
self.fraction = 1.0 # fraction is applied while scanning a directory, which this dataset skips
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",
}
)
if not x["labels"]:
raise RuntimeError(f"No images listed in {self.json_file} were found in {self.img_path}")
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["val_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 conjunto de dados e um treinador mínimos para treinar a Ultralytics YOLO diretamente com COCO JSON, mantendo as anotações como única fonte da verdade e sem arquivos intermediários .txt. Amplie o método cache_labels() com segments ou keypoints para incluir segmentação e pose, e consulte o guia Dicas para treinamento de modelos para ver recomendações de ajuste de hiperparâmetros.
Perguntas frequentes#
convert_coco()grava arquivos de rótulos.txtno 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. Useconvert_coco()quando preferir rótulos permanentes no formato YOLO; use esta abordagem para manter o JSON do COCO como única fonte da verdade sem gerar arquivos adicionais.Não com o pipeline atual da Ultralytics, que espera rótulos YOLO
.txtpor padrão. Este guia fornece o código personalizado mínimo necessário — uma classe de conjunto de dados e uma classe de treinador. Depois de defini-las, o treinamento requer apenas uma chamada padrãomodel.train().Este guia aborda a detecção de objetos. Para adicionar suporte à segmentação de instâncias, inclua os dados de polígonos
segmentationdas anotações COCO no camposegmentsde cada dicionário de rótulos. Para a estimativa de pose, incluakeypoints. O código-fonte deGroundingDatasetfornece uma implementação de referência para lidar com segmentos.Sim.
COCODatasetestendeYOLODataset, então todas as aumentações de dados integradas — mosaico, mixup, CutMix e outras — funcionam sem modificações.As categorias são ordenadas por
ide mapeadas para índices sequenciais começando em 0. Isso funciona com IDs iniciados em 1 (padrão COCO), IDs iniciados em 0 e IDs não contíguos. O dicionárionamesemdataset.yamldeve seguir a mesma ordem da matriz COCOcategories.O JSON do COCO é analisado uma vez na primeira execução do treinamento. Os rótulos analisados são salvos em um arquivo
.cache, então as execuções seguintes carregam os dados instantaneamente, sem analisar o JSON novamente. A velocidade do treinamento é idêntica à do treinamento YOLO padrão, pois as anotações ficam na memória. O cache usa como chave o tamanho do arquivo JSON; por isso, exclua o arquivo.cacheapós qualquer edição que mantenha o mesmo tamanho do arquivo.