Когда в пайплайне детекции всё вроде настроено правильно, а mAP упорно не растёт, проблема нередко оказывается не в модели и не в оптимизаторе, а в разметке после аугментации. Вы отразили изображение, сделали кроп, повернули кадр, а координаты боксов либо не сдвинулись, либо были интерпретированы в неправильном формате. Код не падает, массивы выглядят валидно, обучение идёт — но модель учится на мусоре.

Именно поэтому аугментация для детекции отличается от аугментации для классификации. Здесь недостаточно «поменять пиксели». Нужно, чтобы каждая пространственная трансформация двигала и изображение, и все ограничивающие боксы строго синхронно.

Если вы только входите в тему аугментаций, полезно сначала посмотреть два предыдущих материала:

Этот текст опирается на те идеи, но отвечает на более узкий и практический вопрос: как правильно применять аугментации, если ваши метки — это ограничивающие боксы.

В основе статьи лежит документация Albumentations, open-source библиотеки для аугментации изображений с 15k+ звёзд на GitHub и 140M+ загрузок.

Содержание

  1. Форматы ограничивающих боксов

  2. Сборка пайплайна для детекции

  3. Передача меток и метаданных

  4. Что делает A.BboxParams

  5. Стратегии кадрирования

  6. Типичные ошибки

  7. Что почитать дальше

Форматы ограничивающих боксов

Разные датасеты и фреймворки используют разные соглашения о координатах. Albumentations поддерживает пять форматов. В A.BboxParams нужно передать тот, который уже использует ваша разметка, через параметр coord_format.

Формат

Координаты

Значения

Где встречается

pascal_voc

[x_min, y_min, x_max, y_max]

Пиксели

PASCAL VOC, многие кастомные датасеты

albumentations

[x_min, y_min, x_max, y_max]

Нормализованные значения [0, 1]

Внутренний формат Albumentations

coco

[x_min, y_min, box_width, box_height]

Пиксели

COCO

yolo

[x_center, y_center, box_width, box_height]

Нормализованные значения [0, 1]

Ultralytics YOLO, Darknet

cxcywh

[x_center, y_center, box_width, box_height]

Пиксели

Формат как у YOLO, но без нормализации

Если вы приходите из Ultralytics (YOLOv5/YOLOv8/YOLOv11), ваши аннотации уже лежат в формате yolo, значит в BboxParams нужно указывать coord_format='yolo'.

Для изображения размером 640 × 480 и бокса от пикселя (98, 345) до (420, 462) одни и те же координаты будут выглядеть так:

Пример ограничивающего бокса
Пример ограничивающего бокса
  • pascal_voc: [98, 345, 420, 462] — углы в пикселях

  • albumentations: [0.153, 0.719, 0.656, 0.962] — те же углы, но нормализованные по размерам изображения

  • coco: [98, 345, 322, 117] — верхний левый угол плюс ширина (420 - 98) и высота (462 - 345)

  • yolo: [0.405, 0.841, 0.503, 0.244] — центр плюс размер, всё нормализовано

  • cxcywh: [259, 403.5, 322, 117] — центр плюс размер в пикселях

Сравнение форматов ограничивающих боксов
Сравнение форматов ограничивающих боксов

Самая частая ошибка в работе с bbox — неверный coord_format. Числа всё равно будут выглядеть правдоподобно, пайплайн не выбросит исключение, но каждый ограничивающий бокс укажет не туда. Поэтому первое, что стоит перепроверить в любой задаче детекции, — какой именно формат отдаёт ваш инструмент разметки или исходный датасет.

Как собрать пайплайн для детекции

import albumentations as A
import cv2
import numpy as np

Создайте A.Compose и передайте в него A.BboxParams, чтобы библиотека знала, как интерпретировать и обновлять ограничивающие боксы:

train_transform = A.Compose([
    A.RandomCrop(width=450, height=450, p=1.0),
    A.HorizontalFlip(p=0.5),
    A.RandomBrightnessContrast(p=0.2),
], bbox_params=A.BboxParams(
    coord_format='coco',
    label_fields=['class_labels'],
), seed=137)

В одном пайплайне можно свободно смешивать разные типы трансформаций. Пиксельные трансформации вроде RandomBrightnessContrast меняют только изображение и не трогают боксы. Пространственные трансформации вроде HorizontalFlip обновляют и пиксели, и координаты боксов. За счёт этого результат остаётся согласованным: ограничивающие боксы всегда соответствуют аугментированному изображению.

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

Как применять пайплайн

Загрузите изображение и подготовьте боксы как массив NumPy формы (num_boxes, 4):

image = cv2.imread("image.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

bboxes = np.array([
    [23, 74, 295, 388],
    [377, 294, 252, 161],
    [333, 421, 49, 49],
], dtype=np.float32)

class_labels = np.array(['dog', 'cat', 'sports ball'])

Дальше передайте всё в transform. Метки нужно подавать через именованные аргументы с теми же именами, которые указаны в label_fields:

result = train_transform(image=image, bboxes=bboxes, class_labels=class_labels)

augmented_image = result['image']
augmented_bboxes = result['bboxes']
augmented_labels = result['class_labels']
Вход и выход с отдельными метками
Вход и выход с отдельными метками

На выходе боксов может оказаться меньше, чем было на входе. Это нормально: боксы, которые полностью ушли за пределы изображения или стали слишком маленькими после аугментации, Albumentations отфильтрует автоматически.

Как привязывать метаданные к bbox

Метки классов не обязательны. Можно передавать только координаты, вообще без дополнительной информации:

result = transform(image=image, bboxes=bboxes)

Но если к каждому боксу нужно привязать класс, идентификатор объекта, флаг сложности или что-то ещё, есть два нормальных способа это сделать.

Способ 1: отдельные поля через label_fields

Объявите имена полей в label_fields и передайте каждое отдельным аргументом. Значения могут быть и строками, и числами:

Изображение с несколькими ограничивающими боксами
Изображение с несколькими ограничивающими боксами
bbox_params = A.BboxParams(
    coord_format='pascal_voc',
    label_fields=['class_labels', 'difficult_flags'],
)

result = transform(
    image=image,
    bboxes=bboxes,
    class_labels=['dog', 'cat', 'ball'],   # строки
    difficult_flags=[0, 0, 1],             # числа
)

Таких полей можно объявить сколько угодно. Если в ходе аугментации какой-то ограничивающий бокс отфильтровывается, соответствующая запись удаляется из каждого поля автоматически. Синхронизацию руками поддерживать не нужно.

Этот механизм полезен не только для классов. Любую информацию, которую нужно сохранить вместе с боксом после кропов и фильтрации, можно передавать через label_fields.

Видео. Если вы складываете боксы из нескольких кадров в один массив bboxes, можно добавить поле frame_ids, чтобы отслеживать, из какого кадра пришёл каждый бокс:

bbox_params = A.BboxParams(
    coord_format='pascal_voc',
    label_fields=['class_labels', 'frame_ids'],
)

bboxes = [[10, 20, 100, 200], [50, 60, 150, 250], [30, 40, 80, 180]]
class_labels = ['car', 'car', 'person']
frame_ids = [0, 0, 1]

result = transform(images=images, bboxes=bboxes, class_labels=class_labels, frame_ids=frame_ids)

После аугментации result['frame_ids'] покажет, какие боксы пережили фильтрацию и из каких кадров они пришли.

Instance segmentation. Если каждому боксу соответствует маска экземпляра, можно передать instance_ids, чтобы связь между маской и bbox не потерялась:

bbox_params = A.BboxParams(
    coord_format='pascal_voc',
    label_fields=['class_labels', 'instance_ids'],
)

result = transform(
    image=image,
    bboxes=bboxes,
    class_labels=['person', 'person', 'car'],
    instance_ids=[0, 1, 2],
)
# используйте result['instance_ids'], чтобы индексировать массив масок

Способ 2: упаковать метаданные прямо в массив bbox

Если вся метаинформация числовая, её можно просто добавить в массив боксов как дополнительные столбцы. Например, массив формы (num_boxes, 6) содержит 4 столбца координат и ещё 2 столбца метаданных:

bboxes = np.array([
    [23, 74, 295, 388, 1, 17],   # координаты + class_id + track_id
    [377, 294, 252, 161, 2, 23],
], dtype=np.float32)

bbox_params = A.BboxParams(coord_format='coco')

result = transform(image=image, bboxes=bboxes)
# result['bboxes'] всё ещё имеет форму (n, 6) — дополнительные столбцы сохранены

Этот способ компактнее, но подходит только для числовых признаков. Если нужны строковые классы или доступ к полям по именам, label_fields удобнее.

Что реально управляет A.BboxParams

Именно A.BboxParams определяет, как Albumentations интерпретирует и фильтрует ограничивающие боксы:

  • coord_format — обязательный параметр. Один из 'pascal_voc', 'albumentations', 'coco', 'yolo', 'cxcywh'.

  • bbox_type'hbb' для обычных осево-выровненных боксов с 4 координатами или 'obb' для ориентированных боксов с углом. Для повернутых объектов есть отдельный разбор: Ориентированные ограничивающие боксы.

  • label_fields — список имён аргументов, в которых лежат метки и другая информация, привязанная к каждому боксу.

  • min_area — минимальная площадь бокса в пикселях после аугментации. Всё, что меньше, отбрасывается. По умолчанию 0.0.

  • min_visibility — минимальная доля исходной площади, которая должна остаться видимой после аугментации. По умолчанию 0.0.

  • min_width — минимальная ширина бокса в пикселях или нормализованных единицах. По умолчанию 0.0.

  • min_height — минимальная высота бокса в пикселях или нормализованных единицах. По умолчанию 0.0.

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

  • filter_invalid_bboxes — удалить невалидные боксы вроде x_max < x_min до аугментации. Если включён clip_bboxes_on_input=True, фильтрация происходит уже после обрезки координат. По умолчанию False.

  • max_accept_ratio — максимально допустимое отношение сторон max(w / h, h / w). Боксы, которые его превышают, отбрасываются. Значение None отключает эту проверку.

На практике чаще всего важны не все параметры сразу, а четыре вещи: coord_format, label_fields, min_visibility и поведение на кривой разметке (clip_bboxes_on_input плюс filter_invalid_bboxes).

Что делать с неидеальной разметкой

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

В таком случае есть смысл включить:

bbox_params = A.BboxParams(
    coord_format='yolo',
    label_fields=['class_labels'],
    clip_bboxes_on_input=True,
    filter_invalid_bboxes=True,
)

clip_bboxes_on_input=True жёстко ограничит координаты границами изображения до аугментации. filter_invalid_bboxes=True после этого уберёт вырожденные случаи вроде боксов нулевой ширины или высоты.

Когда нужны min_area и min_visibility

После кропа часть боксов превращается в тонкие обрезки по краям кадра. Формально они ещё существуют, но обучающего сигнала часто уже не несут.

Исходное изображение с двумя боксами
Исходное изображение с двумя боксами
После  без  и
После без и
После  с
После с
После  с
После с

min_area убирает боксы, которые стали слишком маленькими в абсолютном смысле. min_visibility убирает боксы, у которых после кропа осталось слишком мало от исходной площади. Выбор между ними зависит от задачи:

  • если важен физический размер бокса после трансформации — смотрите на min_area;

  • если важна доля объекта, которая реально осталась в кадре, — смотрите на min_visibility.

Во многих детекционных пайплайнах именно min_visibility оказывается самым полезным фильтром: он не даёт обучать модель на почти полностью отрезанных объектах.

Кропы в детекции: обычный RandomCrop часто плохая идея

RandomCrop легко выдаёт кропы, на которых не остаётся ни одного ограничивающего бокса. Для классификации это не проблема. Для детекции — это впустую потраченный обучающий пример.

Поэтому в Albumentations есть bbox-aware варианты кропа:

  • AtLeastOneBboxRandomCrop гарантирует, что после кропа в кадре останется хотя бы один бокс. Остальные могут потеряться. Это полезно, когда объектов много и вы хотите более разнообразные кропы.

  • BBoxSafeRandomCrop гарантирует, что сохранятся все боксы. Область кропа подбирается так, чтобы ничего не потерять. Это подходит для редких объектов или сценариев, где нельзя терять ни одной аннотации.

  • RandomSizedBBoxSafeCrop берёт случайную область изображения, сохраняет все боксы, а потом делает ресайз к целевому размеру. На практике это один из самых полезных вариантов для обучения детекторов: он даёт вариацию масштаба и кадрирования, не ломая разметку.

Если коротко: для детекции не стоит бездумно вставлять обычный RandomCrop только потому, что он хорошо работает в классификации.

Где пайплайн с bbox ломается чаще всего

Неверный coord_format

Это ошибка номер один. Если ваши аннотации лежат в формате YOLO, а вы передали coord_format='coco', код отработает без жалоб — но каждый ограничивающий бокс уедет в неправильную область изображения.

Именно поэтому визуальная проверка аугментированных примеров перед обучением обязательна. Смотрите не только на картинку, но и на наложенные боксы.

Все боксы отфильтровались

Агрессивные кропы в комбинации с жёсткими min_area или min_visibility легко приводят к ситуации, когда после аугментации bboxes становится пустым массивом.

Ваш класс датасета или тренировочный цикл должны уметь это переживать:

  • либо пропускать такие примеры;

  • либо использовать bbox-safe кропы, чтобы не получать пустые таргеты слишком часто.

Перепутали нормализованные и абсолютные координаты

Формат yolo ожидает значения в диапазоне [0, 1]. Если вы передали туда пиксельные координаты, пайплайн обрежет их к [0, 1], и в результате получится крошечный бокс в углу.

Обратная ошибка тоже типична: если подать нормализованные координаты как pascal_voc, боксы окажутся размером в доли пикселя и почти сразу отфильтруются.

Добавили трансформацию без поддержки bbox

Не каждая трансформация умеет обновлять координаты боксов. Если в пайплайн с A.BboxParams добавить несовместимую трансформацию, Albumentations выбросит исключение уже при инициализации. Это хорошее поведение: ошибка всплывает сразу, а не в середине обучения.

Но всё равно стоит заранее смотреть в таблицу поддерживаемых таргетов, особенно если вы собираете длинный кастомный пайплайн.

Визуализируете данные после A.Normalize

A.Normalize переводит пиксели в float, вычитает среднее и делит на стандартное отклонение. Если попытаться отрисовать изображение после этого шага, оно будет выглядеть как шум.

Поэтому для визуальной отладки детекционного пайплайна смотрите изображения до A.Normalize и до A.ToTensorV2.

Заключение

Аугментация в детекции — это не просто «сделать картинку разнообразнее». Здесь любая пространственная трансформация автоматически становится задачей синхронизации таргетов. Если формат координат выбран неверно, если кропы режут объекты слишком агрессивно, если фильтрация настроена без понимания min_visibility, модель начинает учиться на сломанной разметке.

Практически это означает очень простую вещь: сначала добейтесь корректной геометрии и корректной фильтрации bbox, и только потом наращивайте агрессивность аугментаций.

Практический чек-лист:

  1. Проверьте, что coord_format совпадает с реальным форматом вашей разметки.

  2. Вынесите все связанные с bbox метки и идентификаторы в label_fields, если они не упакованы прямо в массив.

  3. Для кропов в детекции предпочитайте bbox-aware варианты вместо обычного RandomCrop.

  4. Подберите min_visibility и min_area под свою задачу, а не оставляйте значения по умолчанию наугад.

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


Если хотите углубиться в тему, дальше логично посмотреть, как Как подбирать аугментации: гипотезы, протокол и метрики, какие трансформации поддерживают разные типы таргетов и как работать с ориентированными ограничивающими боксами.