Как обучать YOLO на COCO JSON без преобразования
Зачем обучать напрямую на COCO JSON
Аннотации в COCO JSON формат может быть использован напрямую для Ultralytics YOLO обучения без предварительного преобразования в .txt файлы. Это достигается путем наследования YOLODataset для анализа COCO JSON на лету и его интеграции в конвейер обучения через пользовательский класс обучения.
Этот подход сохраняет COCO JSON как единственный источник истины — без convert_coco() вызова, без реорганизации каталогов, без промежуточных файлов меток. YOLO26 и все другие модели detect Ultralytics YOLO поддерживаются. Segmentation и модели позы требуют дополнительных полей меток (см. Часто задаваемые вопросы).
Ищете однократное преобразование вместо этого?
Смотрите Руководство по преобразованию COCO в YOLO для стандартного convert_coco() рабочего процесса.
Обзор архитектуры
Требуются два класса:
COCODataset— считывает COCO JSON и преобразует ограничивающие рамки в формат YOLO в памяти во время обученияCOCOTrainer— переопределяетbuild_dataset()для использованияCOCODatasetвместо стандартногоYOLODataset
Реализация следует тому же шаблону, что и встроенный GroundingDataset, который также напрямую считывает JSON-аннотации. Переопределяются три метода: get_img_files(), cache_labels()и get_labels().
Создание класса набора данных COCO JSON
Параметр COCODataset класс наследуется от YOLODataset и переопределяет логику загрузки меток. Вместо чтения .txt файлов из каталога меток, он открывает JSON-файл COCO, итерирует по аннотациям, сгруппированным по изображениям, и преобразует каждый ограничивающий прямоугольник из пиксельного формата COCO [x_min, y_min, width, height] в нормализованный центральный формат YOLO [x_center, y_center, width, height]. Аннотации толпы (iscrowd: 1) и прямоугольники с нулевой площадью пропускаются автоматически.
Параметр get_img_files() метод возвращает пустой список, поскольку пути к изображениям разрешаются из JSON file_name поля внутри cache_labels(). Идентификаторы категорий сортируются и переназначаются на индексы классов с нулевой базой, поэтому как 1-базовые (стандартный COCO), так и несмежные схемы ID работают корректно.
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"]
Разобранные метки сохраняются в .cache файл рядом с JSON (например, instances_train.cache). При последующих запусках обучения кэш загружается напрямую, минуя парсинг JSON. Если JSON-файл изменяется, проверка хеша завершается неудачей, и кэш автоматически перестраивается.
Подключение набора данных к конвейеру обучения
Единственное изменение, необходимое в тренере, — это переопределение build_dataset(). DetectionTrainer создает YOLODataset , который сканирует на наличие .txt файлов меток. Заменив его на COCODataset, тренер считывает данные из JSON COCO.
Путь к JSON-файлу извлекается из пользовательского train_json / val_json поля в конфигурации данных (см. Шаг 3). Во время обучения, mode="train" разрешается в train_json; во время валидации, mode="val" разрешается в val_json. Если val_json не установлен, он возвращается к 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,
)
Настройка dataset.yaml для COCO JSON
Параметр dataset.yaml использует стандартные path, trainи val поля для определения каталогов изображений. Два дополнительных поля, train_json и val_json, указывают файлы аннотаций COCO, которые COCOTrainer считывает. Поля nc и names определяют количество классов и их названия, соответствующие отсортированному порядку categories в 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 names
Ожидаемая структура каталогов:
my_dataset/
images/
train/
img_001.jpg
...
val/
img_100.jpg
...
annotations/
instances_train.json
instances_val.json
dataset.yaml
Запуск обучения на COCO JSON
При наличии класса набора данных, класса тренера и конфигурации yaml обучение выполняется с помощью стандартного model.train() вызова. Единственное отличие от обычного запуска обучения — это аргумент trainer=COCOTrainer , который указывает Ultralytics использовать пользовательский загрузчик набора данных вместо стандартного.
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)
Полный конвейер обучения работает как ожидается, включая валидацию, сохранение контрольных точек и логирование метрик.
Полная реализация
Для удобства полная реализация представлена ниже в виде единого скрипта для копирования и вставки. Он включает пользовательский набор данных, пользовательский тренер и вызов обучения. Сохраните его рядом с вашим dataset.yaml и запустите его напрямую.
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)
Рекомендации по гиперпараметрам см. в руководстве Советы по обучению моделей.
Часто задаваемые вопросы
В чем разница между этим и convert_coco()?
convert_coco() записывает .txt файлы меток на диск в качестве однократного преобразования. Этот подход разбирает JSON в начале каждого запуска обучения и преобразует аннотации в памяти. Используйте convert_coco() , когда предпочтительны постоянные метки формата YOLO; используйте этот подход, чтобы сохранить COCO JSON как единственный источник истины без создания дополнительных файлов.
Может ли YOLO обучаться на COCO JSON без пользовательского кода?
Не с текущим конвейером Ultralytics, который по умолчанию ожидает метки YOLO .txt по умолчанию. Это руководство предоставляет минимальный необходимый пользовательский код — один класс набора данных и один класс тренера. После определения обучение требует только стандартного model.train() вызова.
Поддерживает ли это segmentation и оценку позы?
Это руководство охватывает обнаружения объектов. Чтобы добавить сегментация экземпляров поддержку, включите segmentation данные полигонов из аннотаций COCO в поле segments каждого словаря меток. Для оценка позы, включите keypoints. GroundingDataset исходный код предоставляет эталонную реализацию для обработки сегментов.
Работают ли аугментации с этим пользовательским набором данных?
Да. COCODataset расширяет YOLODataset, поэтому все встроенные аугментации данных — mosaic, mixup, copy-paste, и другие — работают без изменений.
Как идентификаторы категорий сопоставляются с индексами классов?
Категории сортируются по id и сопоставляются с последовательными индексами, начиная с 0. Это обрабатывает 1-базовые ID (стандарт COCO), 0-базовые ID и несмежные ID. Словарь names в dataset.yaml должен следовать тому же отсортированному порядку, что и массив COCO categories .
Есть ли накладные расходы на производительность по сравнению с предварительно преобразованными метками?
COCO JSON парсится один раз при первом запуске обучения. Распарсенные метки сохраняются в файл .cache , поэтому последующие запуски загружаются мгновенно без повторного парсинга. Скорость обучения идентична стандартному обучению YOLO, так как аннотации хранятся в памяти. Кэш автоматически перестраивается, если файл JSON изменяется.