Como treinar YOLO em COCO JSON sem converter#
As anotações no formato COCO JSON podem ser usadas diretamente para treinar o Ultralytics YOLO, sem primeiro converter para ficheiros .txt. Isto funciona através da criação de uma subclasse de YOLODataset para analisar COCO JSON em tempo real e integrá-la no pipeline de treino através de um trainer personalizado.
Porquê treinar diretamente em COCO JSON#
Esta abordagem mantém o COCO JSON como fonte única da verdade — sem uma chamada convert_coco(), sem reorganização de diretórios e sem ficheiros de etiquetas intermédios. O YOLO26 e todos os outros modelos de deteção Ultralytics YOLO são compatíveis. Os modelos de segmentação e pose requerem campos de etiquetas adicionais (consulta as FAQ).
Consulta o guia de conversão de COCO para YOLO para conhecer o fluxo de trabalho padrão convert_coco().
Visão geral da arquitetura#
São necessárias duas classes:
COCODataset— lê COCO JSON e converte caixas delimitadoras para o formato YOLO em memória durante o treinoCOCOTrainer— substituibuild_dataset()para usarCOCODatasetem vez deYOLODatasetpor predefinição
A implementação é uma versão simplificada de GroundingDataset, incluído no sistema, que também lê anotações JSON diretamente. Aqui são substituídos três métodos — get_img_files(), cache_labels() e get_labels() — enquanto GroundingDataset substitui mais elementos, incluindo as suas próprias verificações de hash da cache e de contagem de instâncias.
Criar a classe de dataset COCO JSON#
A classe COCODataset herda de YOLODataset e substitui a lógica de carregamento das etiquetas. Em vez de ler ficheiros .txt de um diretório de etiquetas, abre o ficheiro COCO JSON, itera pelas anotações agrupadas por imagem e converte cada caixa delimitadora do formato de píxeis COCO [x_min, y_min, width, height] para o formato de centro normalizado YOLO [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() devolve 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çados em zero, pelo que os esquemas de IDs começados 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):
"""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"]As etiquetas analisadas são guardadas num ficheiro .cache junto ao JSON (por exemplo, instances_train.cache). Nas execuções de treino seguintes, a cache é carregada diretamente, ignorando a análise do JSON.
get_hash() calcula o hash dos tamanhos e caminhos dos ficheiros, e não do conteúdo dos ficheiros, pelo que uma nova execução só analisa novamente o JSON quando a sua contagem de bytes muda. Adicionar ou remover imagens também pode alterar o tamanho do próprio diretório de imagens e desencadear uma reconstrução, mas não dependas disso — o hash nunca inspeciona ficheiros de imagem individuais, pelo que substituir uma imagem por outra pode manter o tamanho inalterado. Uma edição que preserve a contagem de bytes — ajustar uma coordenada, alterar iscrowd, trocar dois nomes de classe com o mesmo comprimento — mantém a cache obsoleta e treina com as anotações antigas sem aviso; substituir uma imagem no mesmo local é invisível pela mesma razão. Elimina o ficheiro .cache depois de editares as anotações ou substituíres imagens no mesmo local.
Ligar o dataset ao pipeline de treino#
A única alteração necessária no trainer é substituir build_dataset(). O DetectionTrainer padrão cria um YOLODataset que procura ficheiros de etiquetas .txt. Ao substituí-lo por COCODataset, o trainer lê antes do COCO JSON.
O caminho do ficheiro JSON é obtido a partir de um campo personalizado train_json / val_json na configuração dos dados (consulta Configurar dataset.yaml). Durante o treino, mode="train" é resolvido para train_json; durante a validação, mode="val" é resolvido para val_json. Ambas as chaves são obrigatórias — as duas divisões leem diretórios de imagens diferentes, pelo que o JSON de treino não pode substituir um val_json em falta.
O dataset também redefine fraction para 1.0. BaseDataset aplica esse argumento ao analisar um diretório de imagens, uma etapa que COCODataset ignora, pelo que não consegue respeitar um pedido de dataset parcial; redefini-lo impede que o dataset pareça aceitar um valor que ignora. O GroundingDataset incluído no sistema faz o mesmo compromisso pela mesma razão.
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,
)Configurar dataset.yaml para COCO JSON#
O dataset.yaml usa os campos padrão path, train e val para localizar os diretórios de imagens. Nota que path aponta aqui para a raiz das imagens, pelo que train e val são apenas nomes das divisões — ao contrário do guia de conversão, onde path é a raiz do dataset e as divisões incluem o prefixo images/. Dois campos adicionais, train_json e val_json, especificam os ficheiros de anotações COCO que COCOTrainer lê. O campo names lista os nomes das classes pela ordem ordenada de categories no JSON, e a contagem de classes é derivada dele, pelo que 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.yamlExecutar o treino em COCO JSON#
Com a classe do dataset, a classe do trainer e a configuração YAML definidas, o treino funciona através da chamada padrão model.train(). A única diferença em relação a uma execução de treino normal é o argumento trainer=COCOTrainer, que indica ao Ultralytics para usar o carregador de dataset personalizado em vez do predefinido.
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)Todo o pipeline de treino é executado conforme esperado, incluindo a validação durante o treino, a gravação de checkpoints e o registo de métricas.
Apenas a validação durante o treino passa por COCOTrainer.build_dataset. Uma chamada model.val() separada cria o YOLODataset padrão, que procura etiquetas .txt junto às imagens e não encontra nenhuma. Não gera um erro: as imagens são contabilizadas como fundos, pelo que a validação termina e comunica todas as métricas como 0, emitindo 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 treino, cria uma subclasse do validator com a mesma substituição build_dataset e passa-a a model.val(validator=...).
Implementação completa#
Para tua comodidade, a implementação completa é fornecida abaixo como um único script pronto a copiar e colar. Inclui o dataset personalizado, o trainer personalizado e a chamada de treino. Guarda-o junto do teu dataset.yaml e executa-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 tens um dataset e um trainer mínimos que treinam o Ultralytics YOLO diretamente em COCO JSON, mantendo as anotações como fonte única da verdade e sem ficheiros .txt intermédios. Expande o método cache_labels() com segments ou keypoints para abranger segmentação e pose, e consulta o guia Dicas para treino de modelos para recomendações de ajuste de hiperparâmetros.
Perguntas frequentes#
convert_coco()grava ficheiros de etiquetas.txtno disco como conversão única. Esta abordagem analisa o JSON no início de cada execução de treino e converte as anotações em memória. Usaconvert_coco()quando preferires etiquetas permanentes no formato YOLO; usa esta abordagem para manter o COCO JSON como fonte única da verdade sem gerar ficheiros adicionais.Não com o pipeline atual do Ultralytics, que espera etiquetas YOLO
.txtpor predefinição. Este guia fornece o código personalizado mínimo necessário — uma classe de dataset e uma classe de trainer. Depois de definidas, para treinar só precisas de uma chamada padrãomodel.train().Este guia abrange a deteção de objetos. Para adicionar suporte à segmentação de instâncias, inclui os dados de polígonos
segmentationdas anotações COCO no camposegmentsde cada dicionário de etiquetas. Para estimativa de pose, incluikeypoints. O código-fonte deGroundingDatasetfornece uma implementação de referência para processar segmentos.Sim.
COCODatasetestendeYOLODataset, pelo que todas as ampliações de dados incluídas — mosaic, mixup, copy-paste e outras — funcionam sem alterações.As categorias são ordenadas por
ide mapeadas para índices sequenciais a partir de 0. Isto processa IDs começados em 1 (COCO padrão), IDs começados em 0 e IDs não contíguos. O dicionárionamesemdataset.yamldeve seguir a mesma ordem ordenada que o array COCOcategories.O COCO JSON é analisado uma vez na primeira execução de treino. As etiquetas analisadas são guardadas num ficheiro
.cache, pelo que as execuções seguintes carregam-nas instantaneamente, sem nova análise. A velocidade de treino é idêntica à do treino YOLO padrão, uma vez que as anotações são mantidas em memória. A cache baseia-se no tamanho do ficheiro JSON, pelo que deves eliminar o ficheiro.cachedepois de qualquer edição que mantenha o ficheiro com o mesmo comprimento.