Рамки съехали после letterbox: как вернуть координаты на исходное фото
Детектор возвращает разумные рамки на квадратном входе, а поверх исходной фотографии они оказываются выше предметов. Частая причина — потерянные поля, добавленные при подготовке изображения. Масштабирование рамки пропорцией ширины не отменяет сдвиг. Чтобы вернуть геометрию, надо обратить именно то преобразование, которое получил вход модели.
Что произошло с изображением
Пусть исходное фото имеет ширину 1280 и высоту 720 пикселей. Модель принимает квадрат 640 на 640. Чтобы сохранить пропорции, изображение уменьшают до 640 на 360, а оставшиеся 280 пикселей высоты заполняют полями. При симметричном размещении сверху и снизу будет по 140 пикселей. Такой подход обычно называют letterbox.
Точка исходного изображения сначала умножается на коэффициент масштаба, затем к ней прибавляется смещение поля. Для x это левое поле, для y — верхнее. Поэтому обратный путь начинается с вычитания соответствующего поля и продолжается делением на масштаб. Для прямоугольной рамки операция применяется к обеим угловым точкам.
Сразу договоритесь о формате. Здесь рамка записывается как x1, y1, x2, y2 в пикселях, правая и нижняя границы относятся к непрерывной геометрии. Ширина равна x2−x1. Если модель возвращает центр, ширину и высоту или координаты от нуля до единицы, сначала переведите их в этот формат в системе координат входного тензора.
Сохраним метаданные вместе с тензором
Не восстанавливайте поля только по предполагаемому размеру модели. В рабочем пайплайне фактический вход может зависеть от округления, stride, режима минимальных полей и параметров экспорта. Передавайте рядом с тензором исходный размер, реальный размер после resize и фактические left/top. Так постобработка будет использовать результат preprocessing, который действительно произошёл.
original_w, original_h = 1280, 720
resized_w, resized_h = 640, 360
left, top = 0, 140
sx, sy = resized_w / original_w, resized_h / original_h
source = (200.0, 100.0, 1000.0, 600.0)
def forward(box):
x1, y1, x2, y2 = box
return (x1 * sx + left, y1 * sy + top,
x2 * sx + left, y2 * sy + top)
def restore(box):
x1, y1, x2, y2 = box
result = ((x1 - left) / sx, (y1 - top) / sy,
(x2 - left) / sx, (y2 - top) / sy)
return tuple(max(0.0, min(v, limit)) for v, limit in zip(
result, (original_w, original_h, original_w, original_h)))
network_box = forward(source)
recovered = restore(network_box)
wrong = tuple(v / sx for v in network_box)
assert all(abs(a - b) < 1e-9 for a, b in zip(source, recovered))
assert restore((-10, 130, 650, 510)) == (0.0, 0.0, 1280, 720)
print("network", network_box)
print("restored", recovered)
print("without_padding", wrong)
На входе модели рамка становится (100, 190, 500, 440). После восстановления получаем исходные (200, 100, 1000, 600). Если просто поделить все координаты на 0,5, получится (200, 380, 1000, 880): высота рамки сохранится, но вся она съедет вниз на 280 пикселей. Обрезка нижней границы по размеру фото замаскирует часть симптомов, но положение объекта не исправит.
В коде отдельно сохранены sx и sy. При идеально пропорциональном resize они равны; при целочисленном округлении размеров возможно небольшое различие. Нужно следовать принятому контракту preprocessing и обучения: если преобразование аннотаций использует единый коэффициент, обратная операция должна использовать тот же коэффициент. Главное — не смешать две разные реализации в одном сервисе.
Проверим весь путь до интерфейса
Сначала нарисуйте несколько контрольных прямоугольников на синтетическом изображении. Пропустите их координаты через прямое и обратное преобразование. Проверьте горизонтальное и вертикальное фото, нечётные размеры, рамку у границы и рамку, частично попавшую в поле. Для рамок после clipping отбрасывайте нулевую площадь, чтобы пустой прямоугольник не ушёл в последующие расчёты.
Далее откройте реальные изображения с наложенными рамками. Если геометрический тест проходит, а реальные рамки всё равно съезжают, проверьте ориентацию фото, порядок ширины и высоты и преобразования между этапами. Поворот по EXIF, выполненный только в браузере, тоже создаёт разные координатные системы. Размер изображения в ответе API должен относиться к той версии, на которой рассчитывались рамки.
Готовый высокоуровневый предиктор может уже возвращать рамки в координатах исходного изображения: например, у Ultralytics результат boxes.xyxy относится к исходному размеру. Повторное применение нашего восстановления тогда испортит правильный ответ. Этот пример нужен для собственного preprocessing и сырых выходов экспортированной модели; сначала проверьте контракт того компонента, откуда берёте координаты.
В API возвращайте координатную систему явно: размеры исходного изображения, формат рамки и единицу измерения. Если интерфейс показывает фотографию уменьшенной, он должен отдельно масштабировать рамки под размер отображения. Сервер при этом продолжает выдавать пиксели исходника. Смешение экранных и исходных координат особенно заметно на телефонах, где один CSS-пиксель может соответствовать нескольким физическим пикселям экрана, а контейнер страницы меняет размер независимо от модели.
Не округляйте промежуточные координаты после каждого шага. Сначала выполните обратное преобразование с дробными значениями, затем ограничьте рамку границами и только для конкретной операции задайте правило округления. Для рисования допустимы дробные координаты, а для вырезания участка понадобятся целочисленные границы. Если верхний угол округляется вниз, а нижний вверх, это правило должно быть одинаковым в сервисе и тестах. Так небольшая рамка у края не исчезнет из-за двух разных округлений.
В платном курсе «Computer Vision Engineer» вы переходите от детекции к экспорту модели и скрипту инференса, затем к API. Это помогает связать подготовку изображения, выход модели и координаты, которые получает приложение. Откройте программу курса и посмотрите блоки детекции и развёртывания.
Продолжить на практике
- Computer Vision Engineer: от детекции и Doc-AI до продакшна ↗
Практический курс по Computer Vision. Соберёте продакшн-сервис: датасет → обучение (YOLO/RT-DETR, SAM), трекинг, OCR/Doc-AI, веб-приложение (UI+API) и мониторинг. Экспорт в ONNX/TensorRT/OpenVINO, батчинг/квантование под CPU/GPU. Плюс профессиональные блоки: mAP/IoU/Dice и калибровка доверия, QA разметки и аугментации, p95/стоимость, дрейф данных и алерты, guardrails (NMS/правила), A/B-тесты, Triton/gRPC/FastAPI.
