Блог

Как улучшить качество изображений с магазинных камер

Постановка задачи

Мы работаем с изображениями от потолочных и настенных камер в магазинах: камеры смотрят на торговые полки, а затем эти кадры используются в компьютерном зрении для детекции товаров, сопоставления ассортимента, распознавания ценников и контроля выкладки.
В идеальном мире камера всегда отдаёт резкий, ровно экспонированный кадр. В реальности “качество картинки” оказывается одним из главных узких мест: если изображение слегка размыто или пересвечено, downstream-модели начинают ошибаться лавинообразно. Особенно болезненно это проявляется на мелких деталях текст на упаковке, ценники, границы товаров.
Наша цель повысить качество изображений (и особенно кропов с полок/товаров) в условиях, когда:
  • мы ограничены железом и оптикой камеры;
  • сцена сложная и “живая” (покупатели, отражения, смена товаров, вибрации);
  • нужно решение, которое реально можно катить в прод и поддерживать на флоте камер.
Внутри цели было два больших направления:
  1. Стабильно получать “лучший кадр” из потока камеры (в том числе бороться с миганиями/случайными смазами).
  2. Улучшать качество используя историю кадров (несколько снимков одного и того же участка сцены).

В чём проблема на практике

1) Фокус не фиксированный, а “дрейфующий”

Даже если камеру однажды настроили “вроде нормально”, со временем появляются ситуации, когда резкость заметно падает и не возвращается сама. Плюс есть кейсы, где резкость прыгает туда-сюда. Внутренне мы называли это “миганиями”: кадр хороший → плохой → снова хороший (или наоборот).
Почему это больно: downstream-пайплайн обычно ожидает “плюс-минус одинаковое качество” от камеры, а мигания превращают данные в лотерею.
Классические причины возникновения таких миганий из наших наблюдений:
  • вибрации/неустойчивые крепления → часть кадров смазывается;
  • смена товаров (особенно фреш) меняет “текстуру” сцены и может резко менять метрики резкости, хотя фокус не ломался;
  • частичные перекрытия (люди, предметы, “обстаклы”) не закрывают вьюпорт полностью, но сбивают оценку резкости и могут провоцировать неверные решения;
  • стекло холодильников/отражения/запотевание отдельная категория странностей.

2) Полки это сцена с глубиной

Один и тот же кадр содержит объекты на разных расстояниях до камеры: верхняя часть стеллажа ближе, нижняя дальше. Даже идеально выбранный “один фокус” может давать ситуацию, когда часть полки резкая, а часть мыльная. Поэтому возникали идеи фокус-мерджа и фокус-стейкинга по регионам.

3) Экспозиция неоднородна: где-то пересвет, а где-то недосвет в одном кадре

Помимо резкости, регулярно ломает качество неравномерная экспозиция по кадру:
  • нижняя часть (условно ближе к свету/отражающим поверхностям) может быть пересвечена: детали “выгорают” и восстановить их нечем;
  • верхняя часть (в тени, под козырьком, дальше от источников) может быть недосвечена: детали тонут в шуме, текст становится нечитаемым.
Коварство в том, что это может происходить одновременно в одном кадре: мы не можем “одной ручкой” поправить экспозицию так, чтобы выиграли все зоны сразу. Поэтому в экспериментах по улучшению кропов мы отдельно учитывали пересвет в интегральной метрике качества.

Шаг 1. Выбор лучшего кадра

Прежде чем пытаться “улучшать” изображения, мы начали с более приземлённого вопроса:
а действительно ли мы вообще используем лучший кадр, который камера уже может дать?

Как работало раньше

Изначально API камеры было устроено довольно просто.
На самой камере (у неё есть встроенный мини-компьютер с возможностью запускать код на Go) запускался видеострим, и из этого стрима мы брали фиксированный кадр условно, последний или N-й (например, 25-й).
Этот подход выглядел простым и удобным, но в реальности оказался очень хрупким:
  • из-за вибраций один конкретный кадр мог быть смазан;
  • автоэкспозиция могла “поймать” неудачный момент;
  • даже при полностью статичной сцене соседние кадры могли сильно отличаться по качеству.
В итоге downstream-пайплайн получал изображения с непредсказуемым качеством.

Что сделали

Первое, что мы сделали, перенесли часть логики качества прямо на камеру.
Вместо того чтобы слепо брать один фиксированный кадр из видеопотока, мы начали:
  • анализировать несколько кадров подряд;
  • и выбирать из них наиболее резкий.
Практически это выглядело так:
  • из видеострима анализируется каждый 5-й кадр;
  • для каждого кадра считается метрика резкости (дисперсия Лапласиана);
  • из окна кадров выбирается кадр с максимальным значением метрики;
  • именно он передаётся дальше в основной пайплайн.
Это простое изменение дало неожиданно сильный эффект: мы резко сократили количество “случайно плохих” изображений ещё до любых сложных алгоритмов. Важно, что всё это выполнялось на самой камере, без передачи лишних данных наружу.

Шаг 2. Автофокус на камере как задача поиска максимума

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

Метрика резкости

Использовали дисперсию Лапласиана:
  • хорошо коррелирует с визуальной резкостью
  • дешево считается
  • подходит для edge выполнения прямо на камере
Чем выше значение, тем резче кадр.

Стартовое значение фокуса

Чтобы не перебирать заведомо плохие значения, мы:
  • собрали фокусные значения с камер, которые вручную давали стабильный результат
  • построили распределения по типам сенсоров
  • увидели, что оптимум почти всегда лежит в узком диапазоне
Для каждого сенсора выбрали разумную стартовую точку.

Локальный поиск

Дальше все просто:
  1. ставим стартовый фокус, снимаем кадр, считаем резкость
  2. делаем шаг "влево" и "вправо"
  3. по изменению метрики выбираем направление роста
  4. идем в выбранном направлении фиксированным шагом
  5. останавливаемся, когда рост прекратился или достигли лимита шагов
Критично: жестко ограничивали число снимков, иначе edge устройство и батарейка скажут "пока".

Регион интереса (опционально)

Метрику резкости считали:
  • либо по всему изображению
  • либо по прямоугольному региону (viewport)
В сценах с глубиной ROI оказался важным: фокусировали не "в среднем", а по зоне, которая реально нужна для OCR и детекции.

Результат

Комбинация "выбор лучшего кадра из потока + автофокус" дала стабильную базу качества. На нее уже можно было опираться дальше.

Шаг 3. Когда запускать автофокус: триггер по истории, а не по одному кадру

После автофокуса главный вопрос стал таким: как понять, что фокус реально деградировал и его нужно перезапустить?

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

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

Алгоритм детекции деградации фокуса

Мы работали не с кадрами, а с временным сигналом резкости.
  1. Сглаживание
  2. Сырые значения шумные, поэтому сглаживаем (например, скользящим средним), чтобы убрать мелкие флуктуации.
  3. Поиск спадов
  4. Ищем моменты, когда сглаженный сигнал начинает стабильно падать.
  5. Группировка в сегменты
  6. Последовательные кадры со спадом объединяем в сегменты - кандидаты на "ивент деградации".
  7. Агрегация сегментов
  8. Для каждого сегмента считаем длительность, начало и конец, амплитуду спада.
  9. Фильтрация
  10. Отбрасываем короткие сегменты и сегменты с малой амплитудой. Оставляем только длительные и значимые события.

Первые результаты и ограничения

В одном из первых экспериментов (окно сглаживания 5 кадров, минимальная длина спада 3 кадра) нашли:
  • 18 проблемных камер
  • 51 событие деградации
Но остались кейсы, которые детектор не решал полностью:
  • длинные мигания, не связанные с фокусом
  • скачки из-за смены продуктов
  • случаи, где автофокус отрабатывал корректно, но устойчивого улучшения не давал
Мы вручную меняли фокус на "мигающих" камерах: иногда становилось лучше на несколько итераций, но затем мигание возвращалось. Это подтвердило, что часть проблем - не оптика, а получение кадра.

Шаг 4. Борьба с засветами: экспозиционный фьюзинг

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

Почему одно значение экспозиции не работает

  • настраиваемся под яркие зоны - тени проваливаются в шум
  • тянем тени - светлые области выгорают
  • автоэкспозиция выбирает компромисс, который плох для каждой зоны
В итоге даже резкий кадр может быть непригоден для OCR и детекции из-за потери информации в светах или тенях.

Идея: объединять две экспозиции одной сцены

Мы взяли классическую идею из HDR, но с другим критерием успеха:
нам не нужен "красивый HDR", нам нужен стабильный читаемый кадр без артефактов.
Делали два кадра:
  • с пониженной экспозицией - чтобы сохранить детали в светах
  • с повышенной - чтобы вытянуть тени

Почему не делали много экспозиций

  1. Батарейка и нагрузка. Переключение экспозиции не бесплатное: доп съемки, доп вычисления.
  2. Инерция и фантомы. Экспозиция меняется не мгновенно. Если в этот момент в кадре появляется человек или перекрытие, фьюзинг дает ghosting (призраков). На двух экспозициях это еще можно контролировать, на большом числе начинает ломать изображение.

А можно ли улучшить качество, просто обработав один кадр классическими CV-фильтрами?

После автофокуса и фьюзинга мы проверили "дешевый" путь: улучшать один кадр фильтрами, без истории и без изменения съемки.
Что пробовали в OpenCV:
  • контраст (CLAHE)
  • шумоподавление (bilateral filter)
  • резкость (sharpening)
  • удаление бликов (glare removal)
Для контроля делали и ухудшающие трансформации (шум, Gaussian blur), чтобы убедиться, что метрики чувствительны.

Как оценивали

Не ограничивались "визуально лучше":
  • YOLO детектировал ценники
  • OCR распознавал текст
  • считали количество найденных ценников, число символов, средний confidence
Плюс ручная проверка: читаемость ценника глазами.

Что получилось

  • ни один фильтр не давал стабильного прироста OCR метрик на всех сценах
  • эффект был локальным и зависел от конкретного кадра
  • иногда визуально "лучше", но OCR хуже
  • разброс качества между соседними кадрами часто был больше, чем эффект фильтра
Главная причина простая: фильтры перераспределяют сигнал, но не восстанавливают потерянную информацию.

Итоги и выводы

За время работы мы попробовали широкий спектр подходов к улучшению качества изображений с магазинных камер: от классических CV-фильтров и AI-моделей до сложных схем с автофокусом, историей кадров и фьюзингом. На практике довольно быстро стало понятно, что ключевая сложность здесь не в отсутствии «умных» алгоритмов, а в реальных ограничениях продакшена: нестабильная сцена, статичное, но неравномерное освещение, ограниченная батарейка, edge-устройства и downstream-модели, которым важна не визуальная «красота», а воспроизводимая читаемость.
В результате до прода доехали только те решения, которые давали стабильный и объяснимый эффект: управление фокусом с детектором деградации и экспозиционный фьюзинг на полных кадрах. Более сложные идеи фокус-стейкинг, региональный мердж кадров с разным фокусом и экспозицией, агрессивные CV-фильтры и GAN-модели либо не давали устойчивого выигрыша, либо сильно усложняли пайплайн и начинали ломаться на реальных данных. Особенно это было заметно для AI-фильтров и GAN’ов: без доменного обучения они склонны к галлюцинациям и ухудшают downstream-задачи, даже если визуально картинка кажется лучше (этот опыт мы отдельно описывали в другой статье).
Главный инженерный вывод всей этой работы оказался довольно простым: чаще всего выгоднее правильно выбрать и стабильно получить хороший кадр, чем пытаться улучшить плохой. Контроль фокуса, осмысленные триггеры автофокуса и аккуратная работа с экспозицией дали больший эффект, чем любые попытки «дочинить» изображение постфактум. Именно с этой логикой мы и продолжаем развивать систему дальше начиная с качества входных данных, а не с их сложного и хрупкого улучшения.

Огромное спасибо нашим инженерам, Александру Коротаевскому и Артему Сметанину, за подготовленную статью.