Большие фотографии тормозят сервис: как выбрать размер входа по бюджету пикселей

После подключения нового поставщика фотографий сервис стал отвечать заметно дольше. Вес модели не менялся, число запросов почти прежнее. Оказалось, вместо снимков 1600×1200 начали приходить изображения 8000×6000. Даже когда модель в итоге видит небольшой тензор, декодирование и подготовка огромного исходника потребляют память и время. Если динамический вход доходит до модели, растёт и её нагрузка.

Первый полезный контракт — максимальная площадь и максимальная сторона изображения. Ограничение только ширины пропускает очень высокие документы, только числа байтов — хорошо сжатые крупные картинки. Размер входа выбирают с учётом самых маленьких значимых объектов: уменьшение ускоряет обработку, но может стереть деталь, ради которой фотографию загружали.

Посчитаем масштаб до изменения изображения

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

from math import sqrt, floor
from PIL import Image
def target_size(width, height, max_pixels=1_000_000, max_side=1600):
    if width <= 0 or height <= 0:
        raise ValueError("invalid dimensions")
    scale = min(1.0, max_side/max(width, height), sqrt(max_pixels/(width*height)))
    return max(1, floor(width*scale)), max(1, floor(height*scale))
for original in [(4000, 3000), (1000, 500), (100, 10000)]:
    target = target_size(*original)
    print(original, "->", target, "pixels", target[0]*target[1])
    assert target[0]*target[1] <= 1_000_000
    assert max(target) <= 1600
image = Image.new("RGB", (4000, 3000), "white")
resized = image.resize(target_size(*image.size), Image.Resampling.LANCZOS)
assert resized.size == (1154, 866)
print("actual_image", resized.size)

Большой снимок уменьшается до 1154×866, небольшой остаётся 1000×500, а узкая длинная картинка ограничивается по большей стороне. Проверка выполняет настоящий resize Pillow на созданном изображении. Конкретный лимит в миллион пикселей здесь выбран для примера; подбирать рабочее значение надо по качеству задачи и измерению полного запроса на целевом оборудовании.

Проверим, что осталось от объекта

Предположим, знак на исходной фотографии занимает восемь пикселей по высоте. После уменьшения в несколько раз у него останется всего несколько пикселей. Хорошая общая метрика на крупных объектах не защитит этот случай. Разделите проверочную выборку по исходному размеру объектов и измерьте пропуски после каждого варианта подготовки. Для OCR смотрите точность нужных полей, а не только визуальную читаемость страницы человеком.

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

Где поставить ограничения

До полного декодирования проверьте заявленные размеры и допустимый формат средствами выбранной библиотеки. Затем соблюдайте ограничения декодера на чрезмерные изображения и проверяйте фактический размер. Не отключайте защиту только потому, что размер загруженного файла мал. Сжатие уменьшает передачу, но распакованное изображение всё равно занимает память по числу пикселей и каналов.

После resize сохраняйте исходные размеры и использованный масштаб рядом с результатом. Рамки нужно вернуть в координаты оригинала, а время подготовки учитывать отдельно от времени нейросети. Замер только model.forward покажет стабильную скорость и пропустит проблему декодирования. Для API измеряйте полный путь от принятого файла до сериализованного ответа, включая очередь.

Сравните несколько бюджетов на одном наборе изображений. В отчёте нужны качество, доля отказов по размеру, p95 задержки и пиковая память при целевой параллельности. Лимит должен выражать допустимое действие сервиса: уменьшить, обработать по частям или вернуть понятную ошибку. Тогда случайная фотография огромного разрешения не определит поведение всей очереди.

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

В курсе вы ускоряете инференс, работаете с динамическими входами, собираете API и контролируете задержку и стоимость обработки. Следующая задача — выбрать размер входа по измеренному качеству и бюджету сервиса, затем закрепить подготовку в воспроизводимом CV-проекте. Откройте программу платного курса «Computer Vision Engineer: от детекции и Doc-AI до продакшна».

Как оценивать пропуски мелких объектов.

Продолжить на практике

  • 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.