
Всем привет, на связи команда NeuroCore. Сегодня расскажем про кейс разработки системы видеоаналитики для магазинов самообслуживания: почему fisheye-камеры - настоящее проклятие, почему SORT и DeepSORT не справились с задачей, как мы выстроили конвейер от детекции до бизнес-событий, и какие инженерные решения позволили добиться стабильной работы в продакшене.
Дано: магазины самообслуживания, которые работают без кассиров и продавцов. Покупатель входит по QR, выбирает товары, рассчитывается и выходит. Заказчику нужна автоматизированная система отслеживания: кто находится внутри, сколько времени, в каких зонах, а также распознает несанкционированный доступ и вход группами. В случае нарушений, система должна генерировать алерты по 7 типам событий.
Что есть: одна потолочная fisheye-камера, которая покрывает весь зал. Это идеальный выбор для ритейла: угол обзора 180 градусов, не нужно ставить десятки обычных камер, не нужно сшивать панорамы. Но за этот комфорт приходится платить.
Что не так с “рыбьим глазом”
Fisheye-объектив проецирует сферическую сцену на плоский сенсор через нелинейное отображение. Модель искажения OpenCV описывается формулой:
θ_dist = θ × (1 + k1×θ² + k2×θ⁴ + k3×θ⁶ + k4×θ⁸)
На практике это означает:
Нелинейный масштаб. Один и тот же человек у центра кадра и у края выглядит по-разному: у центра — нормальный силуэт сверху, у края — вытянутый и искривлённый. Смещение на 50 пикселей в центре и у края — это разные реальные расстояния.
Форма детекций “плывёт”. Когда человек идёт от центра к краю, его bounding box меняет пропорции от почти квадратного до вытянутого прямоугольника. Трекер, ориентирующийся на IoU, воспринимает это как потерю объекта.
Полные окклюзии при виде сверху. Камера смотрит строго вниз, люди ходят друг за другом. Один полностью перекрывает другого, IoU между детекциями падает до нуля за один кадр.
Почему SORT и DeepSORT не справляются
SORT и DeepSORT для трекинга - логичный выбор в данном классе задач. Это стандартные алгоритмы, которые используют для отслеживания людей в видеопотоке.
SORT опирается на IoU между предсказанной и обнаруженной рамкой плюс модель движения на основе фильтра Калмана. На fisheye оба механизма ломаются одновременно: IoU ненадёжен из-за геометрических искажений, модель движения некорректна в неевклидовом пространстве.
DeepSORT добавляет ReID-эмбеддинги, но по-прежнему опирается на IoU и предсказание позиции. При длительных окклюзиях — а на fisheye они норма— ID теряется, потому что трекер не ждёт возвращения человека достаточно долго.
Прогнали оба на реальной записи с торгового зала. При первом же скоплении людей — через 30–40 секунд — ID начали перемешиваться. Счётчик посетителей после этого не восстанавливался.
Архитектура системы
Мы построили pipeline из шести компонентов, каждый из которых решает свою задачу:
Компонент |
Технология |
Задача |
Детектор |
YOLO |
Обнаружение людей на fisheye кадре |
Трекер |
DeepOcSort (BoxMOT) |
Связывание детекций между кадрами |
ReID-модель |
OSNet / ResNet50 |
Извлечение вектора внешности (эмбеддинга) |
ReID менеджер |
Галерея эмбеддингов + EMA |
Восстановление потерянных ID |
Менеджер треков |
Алгоритмы |
Подсчёт посетителей, слияния/разделения |
Оценщик скорости |
FisheyeUndistorter и VelocityEstimator |
Расчёт реальной скорости в м/с |
Детекция и трекинг работают на сыром fisheye-кадре, без выпрямления геометрии.Тестировали undistort для компенсации геометрических искажений объектива, которая переводит координаты из искажённого fisheye-пространства в евклидово, чтобы “выпрямить” кадр перед подачей в детектор.
На практике это не дало особого выигрыша. Нейросети детектора и ReID устойчиво работали на искажённом кадре — они обучены достаточно, чтобы справляться с перспективными искажениями. Выпрямление не улучшало качество детекций.
Undistort остался только в одном месте — в расчёте скорости, где он действительно нужен: пиксельное смещение на fisheye нелинейно, и перевести его в метры без выпрямления координат нельзя. Но это точечная трансформация координат, а не рендер всего кадра.
Детекция: YOLO на fisheye
Детектор на базе YOLO фильтрует по классу — из всего, что есть в кадре, нужны только люди. Мелкие детекции отсекаются: на краях fisheye-кадра артефакты искажения дают ложные срабатывания, и без фильтрации по размеру они засоряют трекер.
Проблемой стала форма рамок. ReID-модели обучены на датасетах пешеходов с уличных камер, где человек в кадре вертикальный. Fisheye смотрит сверху, и силуэт человека там часто шире, чем выше. Если подавать такой bbox в ReID без предобработки, модель получает на входе то, на чём не обучалась, и эмбеддинги выходят малоинформативными. Метод normalize_boxes_for_reid приводит рамку к квадратной форме с центрировкой по вертикали — после этого качество ассоциаций заметно выросло.
Трекер: почему OC-SORT, а не DeepSORT
Использовали OC-SORT с ReID-эмбеддингами из библиотеки BoxMOT. Главное отличие от классического SORT: OC-SORT обновляет состояние трека, отталкиваясь от факта наблюдения, а не от предсказания модели движения. В толпе, где траектория непредсказуема, это принципиально.
Параметры, подобранные под fisheye-условия:
w_association_emb = 0.85 - 85% решения об ассоциации принимается на основе сходства внешности.
Формула:
cost = (1 - 0.85) × motion_cost + 0.85 × (1 - cosine_similarity)
iou_thresh = 0.05 - крайне низкий порог IoU. Обычно ставят 0.3–0.5. У нас 0.05, потому что в толпе люди стоят вплотную, их детекты перекрываются и при высоком пороге трекер склеивает двух разных людей в один трек.
max_age = 100 кадров время жизни трека без ассоциации. При 10-15 FPS это примерно 7–10 секунд позволяет удерживать ID, даже если человек надолго пропал.
EMA-обновление эмбеддинга с alpha ≈ 0.985 плавное обновление внешности сглаживает случайные выбросы
ReID-менеджер
ReidManager работает поверх трекера и решает три задачи:
Галерея эмбеддингов. Для каждого активного трека хранится очередь из последних 30 эмбеддингов. На их основе рассчитывается прототип — усреднённый EMA-вектор, представляющий типичную внешность. Чем больше наблюдений, тем стабильнее прототип.
ReLink. Когда BoxMOT создаёт новый трек, менеджер проверяет, не тот ли это человек, которого потеряли раньше. Эмбеддинг нового трека сравнивается с прототипами недавно потерянных. Порог восстановления relink_dist_thresh = 0.25 — если косинусное расстояние ниже, старому треку возвращается его ID. Дополнительно учитывается близость к точке исчезновения: если новый трек появился рядом с тем местом, где пропал старый, порог смягчается. Один ID не может быть присвоен нескольким трекам в одном кадре.
Защита от ID-swap. ID-swap — когда два человека проходят мимо друг друга, их bounding box'ы перекрываются, и трекер меняет их идентификаторы местами. На fisheye это происходит регулярно. Механизм защиты: если новый эмбеддинг сильно отличается от прототипа, он помечается как подозрительный. После двух подозрительных эмбеддингов подряд система отказывается добавлять их в галерею и перекалибрует прототип по последним стабильным наблюдениям.
Защита от подмены ID
Коварная ошибка системы: когда два человека подходят друг к другу, их bounding box'ы сливаются в один, один трек поглощает другой. Затем они расходятся — нужно восстановить потерянный ID. TrackManager обрабатывает это через двухфазную логику:
Детекция слияния. Трек пропал, рядом с ним появился другой трек с заметно увеличенным bbox. Критерии: расстояние центров не превышает заданного порога, площадь bbox выросла больше чем в 1.5 раза.
Детекция разделения. После зафиксированного слияния рядом с поглотившим треком появляется новая небольшая детекция — система пытается присвоить ей ID поглощённого трека. Восстановление по минимальному расстоянию между центрами с проверкой размера бокса.
Расчёт скорости
Пиксельное смещение на fisheye нельзя прямо переводить в метры — пространство нелинейно. Для расчёта скорости нужно евклидово пространство, поэтому здесь и только здесь применяется undistort — не полный рендер кадра, а точечная трансформация координат.
Конвейер расчёта:
Сглаживание bbox — отдельно позиция центра и размер. Разделение убирает дрожание детекций, но сохраняет отзывчивость на реальное перемещение.
Undistort bbox — четыре угла сглаженного бокса переводятся в выпрямленное пространство.
Расчёт footpoint — при виде сверху через fisheye ноги человека смещены к центру кадра. Используется алгоритм с экспоненциальным затуханием смещения от центра.
Сглаживание footpoint через EMA, чтобы скорость не прыгала от кадра к кадру.
Расчёт скорости — смещение footpoint между кадрами, умноженное на коэффициент пиксель/метр и делённое на dt.
Обратная проекция — footpoint возвращается в искажённые координаты для проверки попадания в зоны.
События: от координат к бизнес-логике
Финальное звено конвейера это бизнес логика. Оно получает список детекций с координатами и скоростью, проверяет попадание в зоны и генерирует семь типов событий:
Событие |
Описание |
visitor_enter |
Посетитель вошёл в зону магазина |
visitor_exit |
Посетитель покинул зону |
visitor_at_checkout |
Посетитель вошёл в зону кассы |
rapid_movement |
Резкие действия посетителя (скорость более 5 м/с) |
slow_movement |
Длительная остановка (скорость < 0.005 м/с за 5 сек) |
long_stay |
Длительное нахождение в зоне (> 30 сек) |
group_entry |
Групповой вход (3+ человек за 5 сек) |
Выход из зоны не генерируется мгновенно — через механизм PendingExit с временной задержкой. Если человек ушел за границу полигона и вернулся, это не засчитывается как выход. Без этой логики любое касание края зоны генерировало ложное событие.
Проблемы
Две сложности проявились сразу при работе с реальным потоком.
Первая — счётчик посетителей. Короткие треки, когда человек зашёл в кадр и сразу скрылся за стеллажом, засчитывались как новые при каждом появлении. Добавили задержку: трек фиксируется как новый посетитель только после нескольких секунд устойчивого присутствия. Остаточный эффект — задержка до 5 секунд между фактическим входом и записью в базу.
Вторая — настройка зон. Алгоритм теряет людей чаще, если граница зоны обрезает часть траектории: человек выходит за полигон на несколько кадров и трекер засчитывает это как выход. Для операторов на стороне клиента это оказалось неочевидным. Закрыли документацией с конкретными правилами расстановки зон под типовые планировки — зона должна перекрывать траекторию с запасом, не обрезая её по краю.
Резюме
Вместо того чтобы чинить fisheye-искажения для старых алгоритмов, мы выстроили архитектуру, которая умеет работать с искажениями:
ReID-first подход. 85% решения об ассоциации по внешности. Даже если человек полностью пропал из виду и появился в другой части кадра, система узнаёт его по эмбеддингу.
Адаптация к геометрии. Детекция и трекинг в искажённом пространстве. Выпрямление точечное, только для скорости. YOLO и ReID работают с теми данными, на которых обучались.
Продвинутое управление ID. ReLink восстанавливает потерянные идентификаторы, Anti-Swap защищает от подмены, merge/split обрабатывает слияние и разделение людей.
Observation-centric трекинг. OC-SORT не полагается на модель движения — он обновляет состояние трека по факту наблюдения. В толпе, где траектория непредсказуема, это работает надёжнее.
Семь типов бизнес-событий с продуманной логикой задержек, warmup-периодов и дедупликации — превращают сырые координаты в полезную аналитику для ритейла.
Какой эффект дала система для заказчика? В рабочей конфигурации система снимает с оператора просмотр видео для подсчёта посетителей, определения времени пребывания у кассы и фиксации событий долгого стояния. Данные пишутся в базу в структурированном виде и подтягиваются в аналитику магазина.
Что по-прежнему требует человека: первичная калибровка зон под конкретную камеру и планировку зала, разбор нестандартных инцидентов — система генерирует событие slow_movement, но интерпретация остаётся на операторе.
Если вы работали с трекингом на нестандартных камерах — потолочных, панорамных, подводных — интересно услышать, какие решения использовали для удержания ID при длительных окклюзиях.