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 «на лету» и внедряет его в конвейер обучения через кастомный тренер.

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

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

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

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

Потребуются два класса:

  1. COCODataset — считывает COCO JSON и конвертирует bounding boxes в формат 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, перебирает аннотации, сгруппированные по изображениям, и конвертирует каждый bounding box из пиксельного формата 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Подключение датасета к конвейеру обучения#

Единственное изменение, необходимое в тренере — это переопределение build_dataset(). Стандартный DetectionTrainer создает YOLODataset, который ищет файлы меток .txt. Заменив его на COCODataset, тренер начинает считывать данные из 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#

Когда класс датасета, класс тренера и 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Полная реализация#

Для удобства полная реализация представлена ниже в виде готового скрипта. Он включает кастомный датасет, кастомный тренер и вызов обучения. Сохрани этот код рядом с 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. Это руководство предоставляет минимально необходимый кастомный код — один класс датасета и один класс тренера. После их определения для обучения потребуется только стандартный вызов 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. Это позволяет работать с 1-базированными ID (стандарт COCO), 0-базированными и несмежными ID. Словарь names в dataset.yaml должен следовать тому же отсортированному порядку, что и массив categories в COCO.

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

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

Комментарии