Meet YOLO26: next-gen vision AI.

Link to this sectionКак обучать YOLO на COCO JSON без конвертации#

Link to this sectionПочему стоит обучать напрямую на COCO JSON#

Аннотации в формате COCO JSON можно использовать напрямую для обучения Ultralytics YOLO, не конвертируя их предварительно в файлы .txt. Это делается путем создания подкласса YOLODataset для парсинга COCO JSON «на лету» и подключения его в конвейер обучения через кастомный класс trainer.

Такой подход позволяет сохранить COCO JSON как единый источник достоверных данных — без вызова convert_coco(), без реорганизации директорий и без создания промежуточных файлов с разметкой. YOLO26 и все другие модели детектирования Ultralytics YOLO поддерживаются. Модели сегментации и оценки поз требуют дополнительных полей с разметкой (см. FAQ).

Ищешь способ сделать конвертацию один раз?

Смотри руководство по конвертации из COCO в YOLO для стандартного рабочего процесса с convert_coco().

Link to this sectionОбзор архитектуры#

Нужны два класса:

  1. COCODataset — считывает COCO JSON и преобразует ограничивающие рамки в формат YOLO в оперативной памяти во время обучения
  2. COCOTrainer — переопределяет build_dataset(), чтобы использовать COCODataset вместо стандартного YOLODataset

Реализация следует тому же шаблону, что и встроенный GroundingDataset, который также считывает аннотации JSON напрямую. Переопределяются три метода: get_img_files(), cache_labels() и get_labels().

Link to this sectionСоздание класса набора данных COCO JSON#

Класс COCODataset наследуется от YOLODataset и переопределяет логику загрузки меток. Вместо чтения файлов .txt из каталога меток, он открывает файл COCO JSON, перебирает аннотации, сгруппированные по изображениям, и преобразует каждую ограничивающую рамку из пиксельного формата COCO [x_min, y_min, width, height] в нормализованный центральный формат YOLO [x_center, y_center, width, height]. Аннотации для групп объектов (iscrowd: 1) и рамки с нулевой площадью пропускаются автоматически.

Метод get_img_files() возвращает пустой список, так как пути к изображениям определяются из поля file_name в JSON внутри cache_labels(). Идентификаторы категорий сортируются и переотображаются в индексы классов, начинающиеся с нуля, поэтому корректно работают как схемы ID, начинающиеся с 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):
        """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."""
        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 изменяется, проверка хеша не проходит, и кэш пересоздается автоматически.

Link to this sectionПодключение набора данных к конвейеру обучения#

Единственное изменение, необходимое в trainer, — это переопределение build_dataset(). Стандартный DetectionTrainer создает YOLODataset, который ищет файлы меток .txt. Заменив его на COCODataset, trainer будет считывать данные из COCO JSON.

Путь к файлу 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):
        """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.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,
        )

Link to this sectionНастройка 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

Link to this sectionЗапуск обучения на COCO JSON#

С готовым классом набора данных, классом trainer и конфигурацией 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)

Полный конвейер обучения выполняется в обычном режиме, включая валидацию, сохранение контрольных точек и логирование метрик.

Link to this sectionПолная реализация#

Для удобства полная реализация приведена ниже в виде единого скрипта, который можно скопировать и вставить. Он включает кастомный набор данных, кастомный trainer и вызов обучения. Сохрани его рядом с твоим 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):
        """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."""
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        """Parse COCO JSON and convert annotations to YOLO format, saving results 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"]}
        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):
        """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.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)

Рекомендации по гиперпараметрам см. в руководстве Советы по обучению моделей.

Link to this sectionFAQ#

Link to this sectionВ чем разница между этим методом и convert_coco()?#

convert_coco() записывает файлы меток .txt на диск как единовременную конвертацию. Данный подход парсит JSON в начале каждого запуска обучения и преобразует аннотации в оперативной памяти. Используй convert_coco(), если предпочитаешь постоянные метки в формате YOLO; используй этот подход, чтобы сохранить COCO JSON как единый источник достоверных данных без создания дополнительных файлов.

Link to this sectionМожет ли YOLO обучаться на COCO JSON без написания кастомного кода?#

Нет, с текущим конвейером Ultralytics, который по умолчанию ожидает метки YOLO .txt. Это руководство предоставляет минимально необходимый кастомный код — один класс набора данных и один класс trainer. После их определения для обучения требуется только стандартный вызов model.train().

Link to this sectionПоддерживает ли этот метод сегментацию и оценку поз?#

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.

Link to this sectionРаботают ли аугментации с этим кастомным набором данных?#

Да. COCODataset расширяет YOLODataset, поэтому все встроенные аугментации данныхmosaic, mixup, copy-paste и другие — работают без изменений.

Link to this sectionКак ID категорий отображаются на индексы классов?#

Категории сортируются по id и отображаются на последовательные индексы, начинающиеся с 0. Это обрабатывает ID, начинающиеся с 1 (стандарт COCO), ID, начинающиеся с 0, и несмежные ID. Словарь names в dataset.yaml должен следовать тому же отсортированному порядку, что и массив categories в COCO.

Link to this sectionЕсть ли снижение производительности по сравнению с заранее конвертированными метками?#

COCO JSON парсится один раз при первом запуске обучения. Разобранные метки сохраняются в файл .cache, поэтому последующие запуски загружаются мгновенно без повторного парсинга. Скорость обучения идентична стандартному обучению YOLO, так как аннотации хранятся в оперативной памяти. Кэш автоматически пересоздается, если файл JSON изменяется.

Контрибьюторы

Комментарии