Веб-приложение для обработки видео с конвейера: детекция мешков (MMDetection), трекинг между кадрами, подсчёт прошедших мешков, асинхронная фоновая обработка и мониторинг аномалий камеры (загрязнение объектива, сдвиг ракурса). Backend - FastAPI, фоновая обработка - Celery + Redis, всё упаковано в Docker.
- Быстрый старт
- Архитектура
- Подход к детекции
- Подход к трекингу и подсчёту
- Асинхронная обработка
- Мониторинг аномалий
- Остановка конвейера при сворачивании
- Калибровка зоны конвейера, линии и зоны выхода
- Интеграция дообученной модели (после Training_model.ipynb)
- API
- Хранение данных
- Технические решения и обоснование
- Ограничения и путь развития
- Структура проекта
Требования: Docker + Docker Compose. Сборка тянет PyTorch (CPU-сборка) и
стек MMDetection (mmengine/mmcv/mmdet через mim), поэтому первая сборка
занимает несколько минут и требует доступа к PyPI и серверам OpenMMLab.
docker compose up --buildПосле старта:
- Веб-интерфейс: http://localhost:8000/ - загрузка видео, запуск обработки, отслеживание прогресса, скачивание результата.
- Swagger-документация API: http://localhost:8000/docs
- Пример через curl:
# 1. загрузка видео
JOB_ID=$(curl -s -F "file=@input.mp4" http://localhost:8000/videos | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
# 2. запуск обработки (асинхронно, ответ приходит сразу)
curl -s -X POST http://localhost:8000/videos/$JOB_ID/process
# 3. опрос статуса
curl -s http://localhost:8000/videos/$JOB_ID | python3 -m json.tool
# 4. скачивание результата, когда status == "done"
curl -s http://localhost:8000/videos/$JOB_ID/download -o processed.mp4
# 5. лог аномалий
curl -s http://localhost:8000/videos/$JOB_ID/anomalies | python3 -m json.toolЕсли сборка не может достучаться до серверов OpenMMLab за весами модели
(mim download в Dockerfile), см. scripts/download_weights.sh - веса
можно скачать отдельно и подмонтировать в /workspace/weights.
По умолчанию всё собрано под CPU (чтобы гарантированно завестись на любой
проверяющей машине без CUDA). Для GPU: в Dockerfile заменить CPU-индекс
torch/torchvision на CUDA-сборку и базовый образ на
nvidia/cuda:*-runtime (или официальный pytorch/pytorch:*-cuda), в
docker-compose.yml добавить deploy.resources.reservations.devices (или
runtime: nvidia) сервису worker, и выставить DEVICE=cuda:0.
┌─────────────┐
загрузка видео --> │ FastAPI │ --> data/raw/{job_id}.mp4
│ (api) │ + запись Job(status=uploaded) в SQLite
└──────┬──────┘
│ POST /videos/{id}/process
│ (ставит задачу в очередь и сразу отвечает -
│ HTTP-запрос не блокируется на время inference)
v
┌─────────────┐ ┌───────┐
│ Redis │<-------│worker │ (Celery)
│ (брокер) │------->│ │
└─────────────┘ └───┬───┘
│ обрабатывает видео покадрово:
│ детекция -> трекинг -> подсчёт
│ -> мониторинг аномалий -> отрисовка
v
data/processed/{job_id}.mp4 + записи Anomaly в SQLite
│
┌─────────────┐ │
GET /videos/{id} --│ FastAPI │<------------┘ (общая БД + общий volume ./data)
GET .../download │ (api) │
└─────────────┘
Два независимых процесса (api и worker) читают/пишут в общий volume
./data и общую SQLite-базу data/db/app.db, поэтому:
- HTTP-запрос на запуск обработки не ждёт инференса - он просто кладёт
задачу в очередь Celery/Redis и сразу возвращает
202-подобный ответ (status=queued). workerможно масштабировать горизонтально (docker compose up --scale worker=3) без изменений в API-слое - Celery сам распределит задачи.- Видео и БД лежат в
./dataна хосте (bind mount, не анонимный volume), поэтому переживаютdocker compose down/ пересборку образов.
Требование задания - детектор на базе MMDetection. Изначально датасет с
размеченными мешками не был предоставлен, поэтому предусмотрены два режима
работы детектора, между которыми app/services/detector.py:MMDetBackend
переключается автоматически (без переменных окружения и правок кода - см.
"Интеграция дообученной модели" ниже):
- Дообученная модель (рекомендуемый режим для реальной эксплуатации) -
RTMDet-tiny, дообученный на размеченных кадрах именно этого конвейера
(
Training_model.ipynb: Kaggle/Colab, датасет из Roboflow, transfer learning от COCO-весов). Как только.py-конфиг и.pth-чекпоинт из финального шага ноутбука оказываются вweights/, детектор их подхватывает и использует напрямую, без фильтрации по именам классов COCO (единственный класс дообученной модели - мешок). - Резервный режим: предобученный на COCO чекпоint
RTMDet-tinyиз зоопарка моделей MMDetection - используется автоматически, пока дообученной модели ещё нет. Классыsuitcase/backpack/handbagиспользуются как прокси для мешков (единственные классы COCO, геометрически близкие к мешку на конвейере). - Резервный классический детектор (
MotionBlobBackend) - фоновое вычитание (cv2.createBackgroundSubtractorMOG2), ограниченное областью конвейера (ROI-маска), с фильтрацией контуров по площади и соотношению сторон. Используется только если модель MMDetection не удалось загрузить вовсе (нет весов, ошибка импорта стека mmdet) -BagDetector.detectпереключается на него целиком, а не смешивает с выходом модели, чтобы пайплайн не падал и продолжал считать мешки даже в этом случае.
Фильтрация по ROI на выходе. Сам кадр в MMDetection передаётся
немаскированным (сеть теряет контекст на зачернённых областях, если
применить маску до инференса), но после детекции BagDetector.detect
отбрасывает любой бокс, чей центр не попадает в откалиброванный
ROI-полигон ленты. Это относится к обоим источникам детекций (MMDetection
и MotionBlobBackend) и не даёт постороннему объекту в кадре - например,
сумке в руках у рабочего рядом с лентой - породить трек, способный дойти
до линии подсчёта.
Путь к продакшену: разметить кадры реального конвейера (см.
Training_model.ipynb и раздел про интеграцию весов ниже), дообучить,
положить результат в weights/ - остальной pipeline (трекер, счётчик,
мониторинг аномалий, остановка конвейера) не требует изменений.
-
Трекер - собственная реализация SORT (
app/services/tracker.py): фильтр Калмана с моделью постоянной скорости на векторе состояния[cx, cy, s, r, vcx, vcy, vs]+ сопоставление детекций и треков по IoU через венгерский алгоритм (scipy.optimize.linear_sum_assignment). Каждому мешку присваивается стабильныйtrack_id, который сохраняется, пока трек не потерян дольшеtracker_max_ageкадров подряд.Полноценный DeepSORT/ByteTrack или интеграция mmtracking не использовались осознанно: лента конвейера - это одно направление движения без пересечений и перекрытий между мешками, то есть ровно тот сценарий, где классический SORT работает надёжно и при этом не тянет за собой отдельный тяжёлый стек зависимостей (риск для сборки Docker-образа).
-
Подсчёт (
app/services/counter.py) - виртуальная линия поперёк ленты (settings.count_line). Для каждого трека на каждом кадре проверяется знак векторного произведения относительно линии между предыдущей и текущей позицией центроида; смена знака "к тоннелю" засчитывается как проход. Защита от повторного подсчёта: флагcountedхранится в самом объектеTrackи выставляется один раз - даже если трек на секунду терялся и переприсоединился (тот же id), повторного инкремента не будет. Пересечение в обратную сторону (дрожание у линии) не засчитывается за счёт проверки направления (prev_side > 0 and curr_side <= 0).
POST /videosтолько сохраняет файл на диск и создаёт записьJobв БД - мгновенный ответ.POST /videos/{id}/processставит задачуprocess_video_taskв очередь Celery (брокер - Redis) и сразу возвращаетjobсо статусомqueued. HTTP-запрос не ждёт инференса.- Отдельный процесс/контейнер
workerразбирает очередь, обновляетJob.status(processing->done/failed) иJob.progress(0-100%, обновляется каждый кадр по номеру кадра/общему числу кадров) прямо в общей БД, которую видитapi. GET /videos/{id}даёт текущий статус/прогресс/результат в любой момент;GET /videos/{id}/downloadотдаёт файл, когдаstatus == "done".- Celery выбран вместо
BackgroundTasksFastAPI, потому что обработка 10-минутного видео - тяжёлая CPU/GPU-задача, которая не должна жить и умирать вместе с процессом веб-сервера (перезапуск api при деплое не должен убивать обработку) и должна масштабироваться отдельно от него.
Реализовано в app/services/anomaly.py. По условиям задания вид аномалий
выбирается самостоятельно; сделан явный акцент на data drift по яркости и
цвету (для загрязнений объектива пылью/маслом/грязью) и на отслеживание
сдвига камеры относительно исходного ракурса:
- Data drift по яркости - средняя яркость (grayscale) кадра
сравнивается с базовой, посчитанной по первым
anomaly_baseline_framesкадрам (относительное отклонение в %). Устойчивое затемнение - признак плёнки грязи/масла на объективе; устойчивое просветление - блик/паразитная засветка. - Data drift по цвету - гистограмма HSV (H+S каналы) в области
конвейера сравнивается с базовой через расстояние Бхаттачария
(
cv2.compareHist(..., HISTCMP_BHATTACHARYYA)). Ловит именно цветовой сдвиг (напр. желтоватый/масляный оттенок), который чистый мониторинг яркости может не заметить, если суммарная яркость почти не изменилась. - Локальное загрязнение - кадр делится на сетку
(
local_grid, по умолчанию 4x3); для каждой ячейки считается дисперсия Лапласиана (стандартная метрика резкости/фокуса) и сравнивается с базовой. Проверяются только ячейки фона (вне ROI конвейера - лента меняет резкость каждый раз, когда по ней едет мешок, это не имеет отношения к загрязнению объектива) и только "достаточно текстурные" ячейки (на однородной стене дисперсия и так близка к шуму матрицы, любой шум даст ложные "проценты падения"). Срабатывание требует одновременно существенного относительного падения резкости и статистического выброса относительно собственного шума этой ячейки (z-score) - это ловит именно локальное пятно грязи/масла в конкретной части объектива, а не общее размытие всего кадра (которое уже покрыто пп. 1-2). - Сдвиг ракурса камеры - детектор ORB находит ключевые точки в
статичном фоне (за пределами ROI конвейера - мешки на ленте никогда не
участвуют в сопоставлении), сопоставляются с ключевыми точками опорного
(первого стабильного) кадра через
BFMatcher, аффинное преобразование оценивается RANSAC'ом (cv2.estimateAffinePartial2D). Из матрицы извлекаются сдвиг (px) и поворот (градусы); превышение порогов (camera_shift_px,camera_rotation_deg) - признак того, что камеру задели/она съехала на креплении, а откалиброванные ROI/линия подсчёта больше не валидны для текущего кадра. - Бонус: "зависший" поток - почти нулевая покадровая разница
(
cv2.absdiff) несколько секунд подряд - использует те же данные, что и п.1, добавлен почти бесплатно и ловит зависшую/потерянную трансляцию.
Общий механизм устойчивости к шуму: каждая проверка не срабатывает по
одному кадру, а требует anomaly_sustain_frames подряд "положительных"
показаний (_SustainCounter), и повторно "взводится" только после того,
как условие исчезло - это гарантирует одно событие на один реальный эпизод,
а не поток срабатываний на каждом кадре аномалии.
Аномалии сохраняются в таблицу anomalies (SQLite, FK на Job), доступны
через GET /videos/{id}/anomalies, и активные на текущий момент типы
аномалий выводятся баннером поверх итогового видео
(app/utils/draw.py:draw_overlay).
Отдельный от мониторинга аномалий камеры механизм
(app/services/belt_motion.py) - специально под сценарий, при котором
конвейер физически останавливают/сворачивают на обслуживание: рабочий,
поправляющий вручную мешок на неподвижной ленте, не должен добавляться в
счётчик, а мешок, оставшийся лежать на ленте, не должен пересчитываться
заново после возобновления работы.
Почему не просто "следить за движением по всей ленте": обычная пауза между двумя мешками на этой ленте сама по себе может длиться 10-30 секунд - столько же, сколько разумно ждать для объявления "конвейер стоит". Слежка за движением по всей области ROI не может отличить одно от другого - оба случая выглядят как "почти нулевое движение". Вместо этого используется data drift в отдельной, вручную размеченной "зоне выхода" - небольшой области на самой ленте, где мешок последний раз виден перед тем, как исчезнуть из кадра. Логика: во время нормальной работы каждый мешок находится в этой зоне лишь недолго и движется дальше, то есть зона регулярно возвращается к обычному виду; мешок, оставшийся лежать, держит зону "непохожей на обычную" гораздо дольше, чем любой нормальный проход. Устойчивое отклонение - это и есть сигнал, а не отклонение само по себе.
Техника отслеживания - та же, что и в мониторинге аномалий выше (средняя яркость + расстояние Бхаттачария между HSV-гистограммами), просто применённая к маленькой зоне вместо всего кадра, с намного более низкими порогами чувствительности (12% по яркости и 0.15 по цвету против 30%/35% в общих проверках аномалий) - здесь важно поймать мешок как можно раньше, а не игнорировать обычный шум.
Ключевое инженерное решение, найденное методом проб на реальном видео:
эталон "как обычно выглядит зона" не может быть одной зафиксированной в
начале видео величиной (AnomalyMonitor использует именно такой подход) -
тестирование показало, что яркость этой зоны естественно и легитимно
дрейфует (стабилизация автоэкспозиции камеры в первые ~20 секунд, и
медленнее - на протяжении всего ролика), и с фиксированным эталоном
проверка один раз ложно срабатывает и больше никогда не возвращается в
нормальное состояние. Решение - адаптивный эталон (экспоненциальное
скользящее среднее, EMA), который продолжает "следить" за нормальным
состоянием, но обновляется только пока зона НЕ отклоняется - так он
успевает за медленным легитимным дрейфом освещения между проходами мешков,
но не "утягивается" к мешку, который сейчас неподвижно лежит в кадре (это
как раз то, что должно засчитываться как остановка).
Дополнительно:
- Период прогрева (
exit_zone_warmup_sec, 20с) - в начале видео отклонение не проверяется вовсе, эталон просто продолжает обновляться каждый кадр, пока не пройдёт естественная стабилизация экспозиции камеры. - Порог длительности (
exit_zone_stop_sustain_sec) обязательно подбирается по факту, а не "чем меньше, тем чувствительнее": на реальном видео обычный проход одного мешка через зону выхода сам по себе даёт до ~25 секунд непрерывного отклонения - более короткий порог (пробовались 4с и 20с) ложно срабатывал на совершенно нормальном трафике мешков. - Предохранитель максимальной длительности (
exit_zone_max_stopped_sec, 90с) - если состояние "остановлено" держится дольше этого времени без возврата к норме, эталон принудительно сбрасывается на текущий кадр и подсчёт возобновляется. Это не гипотетический сценарий: на приложенном видео во второй половине ролика рабочие периодически собираются у дальнего конца тоннеля с открытой дверью, принимая мешки - из-за перспективы камеры (смотрит прямо вдоль тоннеля) эта удалённая активность заметно меняет яркость/цвет даже в зоне, нарисованной как можно ближе к камере. Без предохранителя это привело бы к полной блокировке подсчёта до конца видео; с ним подсчёт гарантированно возобновляется не реже, чем раз вexit_zone_max_stopped_secсекунд. Подробный разбор - в комментарияхapp/services/belt_motion.pyи в разделе "Ограничения" ниже.
Пока конвейер считается остановленным: детекция, трекинг и подсчёт
полностью приостанавливаются (кадры по-прежнему пишутся в выходное
видео, поверх них - баннер "CONVEYOR STOPPED"); при возобновлении трекер
намеренно не сбрасывается - его состояние (включая флаг counted уже
подсчитанных мешков) просто "замораживается" на время паузы, ровно как и
должно быть для объекта, который, по определению, не двигался. Более
ранняя версия явно очищала состояние трекера при возобновлении - это и
вызывало повторный счёт мешков, оставшихся на ленте, поскольку каждый уже
посчитанный мешок терял свой флаг counted и создавал новый трек при
первой же детекции после паузы.
Координаты ROI-полигона ленты, линии подсчёта и зоны выхода не зашиты в
коде - они калибруются один раз через веб-инструмент и хранятся как JSON в
общем volume (data/config/geometry.json), поэтому переживают
пересоздание контейнеров и не требуют пересборки образа при смене камеры.
- Откройте
http://localhost:8000/calibrate.html. - Загрузите видеофайл - инструмент возьмёт из него первый кадр.
- Режим "1. Зона ленты (ROI)" - кликами по кадру отметьте контур ленты конвейера (минимум 3 точки), не захватывая рабочую зону персонала.
- Режим "2. Линия подсчёта" - кликните ровно 2 точки поперёк ленты.
- Режим "3. Зона выхода (для остановки)" - отметьте небольшую область на самой ленте, ближе к тому месту, где мешок в последний раз виден перед тем, как исчезнуть из кадра. Стоит держать эту зону подальше от любых удалённых, плохо контролируемых частей кадра (например, глубины тоннеля, где могут быть посторонние источники освещения или люди) - см. раздел "Остановка конвейера" выше про конкретный найденный на тестовом видео пример такой помехи.
- "Сохранить калибровку" - геометрия применяется к следующей запущенной задаче обработки.
Без калибровки обработка видео завершается понятной ошибкой
(Job.error_message) с указанием открыть /calibrate.html - в отличие от
более ранней версии, здесь намеренно нет запасных зашитых координат:
зона выхода специфична для каждой камеры, и запуск с "какими-то" старыми
координатами тихо ломает именно то, что и было исходной проблемой (ROI,
подобранный для одного видео, не подходит другому).
Быстрая проверка уже сохранённой калибровки без веб-интерфейса:
python scripts/preview_roi.py path/to/video.mp4 preview.pngapp/services/detector.py:MMDetBackend автоматически определяет, что
использовать - без переменных окружения и без правок кода:
- Обучите модель (см.
Training_model.ipynb- Kaggle/Colab, RTMDet-tiny, дообучение от COCO-весов на данных из Roboflow). Финальная ячейка ноутбука сохраняет ровно два файла:bag_detector.pth(веса) иbag_detector_config.py(конфиг, с которым модель обучалась). - Скопируйте оба файла в
weights/проекта (тот же каталог, что монтируется в/workspace/weightsвнутри контейнера). - Перезапустите
worker(или весьdocker compose up --build, если веса меняются на лету через volume - достаточно просто перезапуска процесса).
При старте MMDetBackend ищет .py-файл в weights_dir: если находит -
загружает именно эту дообученную модель напрямую (через DetInferencer с
вашим конфигом и чекпоинтом), без фильтрации по именам классов COCO (единственный
класс дообученной модели и так - мешок, фильтровать нечего). Если .py
файла нет - используется прежнее поведение: предобученный на COCO
rtmdet_tiny_8xb32-300e_coco через mim, с прокси-классами
suitcase/backpack/handbag. Если найдено больше одного .py-файла -
это неоднозначность: детектор сначала пытается однозначно определить
пользовательский конфиг, отфильтровав стандартный rtmdet_tiny_8xb32-300e_coco.py,
который Dockerfile скачивает при сборке; если и после этого остаётся
несколько кандидатов, предпочтение отдаётся файлу bag_detector_config.py
(имя, под которым ноутбук сохраняет свой конфиг); иначе интеграция логирует
предупреждение и откатывается на предобученную модель, а не гадает, какой
файл использовать.
Никаких других правок не требуется - трекер, счётчик, мониторинг аномалий и остановка конвейера работают одинаково независимо от того, какой из двух режимов детектора сейчас активен.
| Метод | Путь | Описание |
|---|---|---|
| POST | /videos |
загрузить видео (multipart file), создаёт Job |
| POST | /videos/{id}/process |
поставить видео в очередь на обработку |
| GET | /videos |
список всех задач |
| GET | /videos/{id} |
статус/прогресс/результат конкретной задачи + список аномалий |
| GET | /videos/{id}/anomalies |
только список аномалий |
| GET | /videos/{id}/download |
скачать обработанное видео (когда status=done) |
| POST | /calibration/frame |
извлечь первый кадр из загруженного видео (для разметки) |
| POST | /calibration/save |
сохранить ROI-полигон + линию подсчёта + зону выхода |
| GET | /calibration/current |
текущая сохранённая калибровка (если есть) |
Полная интерактивная документация - /docs (Swagger UI).
./data на хосте монтируется в /data в обоих контейнерах:
data/
raw/{job_id}.mp4 - оригинальные загруженные видео
processed/{job_id}.mp4 - обработанные видео со счётчиком/рамками
db/app.db - SQLite: задачи (Job) и аномалии (Anomaly)
Так как это bind mount (а не анонимный volume Docker), все видео и вся база
переживают docker compose down, docker compose up --build и пересоздание
контейнеров.
| Решение | Обоснование |
|---|---|
| FastAPI (обязательное требование) | Асинхронный по своей природе, встроенная валидация Pydantic, автогенерация OpenAPI/Swagger. |
| Celery + Redis для фоновой обработки | Отдельный от веб-процесса воркер, который переживает рестарт API, масштабируется независимо, даёт из коробки трекинг статуса задачи. Проще и надёжнее, чем asyncio.create_task/BackgroundTasks, которые умирают вместе с процессом сервера. |
| SQLite вместо Postgres | Для тестового задания с одним воркером и умеренной нагрузкой отдельная СУБД - избыточная сложность деплоя; файл БД лежит в том же volume, что и видео, и переживает пересоздание контейнеров. Замена на Postgres - это только смена DATABASE_URL, весь код работает через SQLAlchemy ORM. |
| Собственный SORT-трекер вместо mmtracking | Сценарий (одно направление движения, отсутствие пересечений) не требует тяжёлой ReID-модели; минимизирует зависимости и риск сборки образа. |
| Гибрид MMDetection + классический блоб-детектор как резерв | Честная интеграция MMDetection при отсутствии датасета мешков; классический детектор (MotionBlobBackend) включается только если модель не удалось загрузить вовсе, чтобы пайплайн не падал; архитектура допускает "бесшовную" замену на дообученную модель. |
| ROI фильтруется на выходе детекции, а не на входе модели | Маскирование кадра до инференса портит контекст для сети (см. BagDetector.detect); при этом без фильтрации после детекции посторонний объект вне ленты мог бы породить трек и дойти до линии подсчёта. Оба требования совмещены: вход - немаскированный, выход - обрезан по ROI-полигону (_center_in_roi). |
| Docker Compose с тремя сервисами (redis/api/worker) | Явное разделение ответственности, соответствует продовой архитектуре очередей задач, легко масштабировать worker отдельно от api. |
Данные в bind-mounted ./data, не в анонимном volume |
Прямое требование задания - видео не должны теряться при пересоздании контейнера; bind mount также упрощает инспекцию файлов с хоста при отладке. |
| Геометрия (ROI/линия/зона выхода) - JSON-калибровка через веб-UI, а не константы в коде | Координаты, подобранные под один конкретный кадр, не переживают смену видео/камеры без правки кода и пересборки образа - см. services/geometry.py. JSON в общем volume калибруется через /calibrate.html и подхватывается воркером без пересборки. |
| Остановка конвейера - data drift в отдельной зоне выхода, а не слежка за движением по всей ленте | Обычная пауза между двумя мешками на этой ленте уже сама по себе длится 10-30с - неотличимо от подлинной остановки при слежке за движением по всей ROI. Маленькая, отдельно откалиброванная зона выхода даёт содержательно другой сигнал: устойчивое (а не мгновенное) отклонение от обычного вида. |
| Адаптивный (EMA) эталон вместо фиксированной базовой линии для зоны выхода | Тестирование показало, что фиксированный эталон (как в AnomalyMonitor) ловит естественный дрейф освещения как ложную остановку и потом никогда не восстанавливается - см. services/belt_motion.py. |
Автоопределение дообученной модели по наличию .py-конфига в weights_dir |
Не требует переменных окружения или правок кода после обучения на Kaggle/Colab - положить два файла в weights/ и перезапустить worker. |
- Технику "data drift в зоне выхода" для детекции остановки конвейера
ограничивает конкретная особенность приложенного видео: камера смотрит
прямо вдоль тоннеля, и во второй половине ролика в кадре периодически
появляются рабочие у дальнего (уже вне ленты) конца тоннеля с открытой
дверью - из-за перспективы это заметно меняет яркость/цвет даже в зоне,
нарисованной как можно ближе к камере, и не является ошибкой калибровки
или слишком высоким порогом. Предохранитель (
exit_zone_max_stopped_sec) гарантирует, что подсчёт не блокируется навсегда, но результат в этом конкретном ролике заметно менее чистый, чем на первой половине видео, где такой помехи нет. При калибровке на реальной, отдельной камере (без вида на удалённую активность, не относящуюся к самой ленте) техника должна работать существенно чище - это подтверждено первой половиной того же видео, где данная проблема отсутствует. Подробный разбор экспериментов и цифры - в комментарияхapp/services/belt_motion.py. - Качество детекции ограничено отсутствием датасета мешков. Прокси-классы
COCO + классический блоб-детектор - рабочий компромисс для демонстрации
архитектуры, но не замена дообученной модели. Путь развития: разметить
кадры этого видео (например, в CVAT), дообучить RTMDet/другую конфигурацию
MMDetection на одном классе "мешок", заменить чекпоинт (см.
configs/README.md) - остальной pipeline не меняется. - Пороги мониторинга аномалий откалиброваны на одном 10-минутном ролике без размеченных инцидентов загрязнения/сдвига камеры - в проде их стоит подтверждать на более длинной "чистой" записи и, в идеале, сделать базовый уровень адаптивным (скользящее среднее с медленным темпом обновления), чтобы отличать плавный, легитимный дрейф освещения в течение дня от резкого события-аномалии.
- ROI/линия подсчёта заданы вручную под конкретный монтаж камеры - при
перестановке камеры требуется однократная ручная калибровка
(
scripts/preview_roi.py), автоматическая калибровка ленты по краям через классическое CV или сегментацию - следующий шаг. DETECT_EVERY_N_FRAMES=1по умолчанию - детекция запускается на каждом кадре для максимальной точности трекинга (при пропуске кадров предсказание Калмана между детекциями может увести трек достаточно далеко, чтобы IoU с следующей детекцией упал ниже порога, и трекер потеряет мешок). На GPU это не проблема; на CPU, если нужна скорость, можно выставить2, но только после проверки, что трекинг остаётся стабильным на конкретном видео. Переопределяется без пересборки образа: положитьDETECT_EVERY_N_FRAMES=2в.envрядом сdocker-compose.yml(Docker Compose подставит его в${DETECT_EVERY_N_FRAMES:-1}у сервисаworker) и пересоздать контейнер (docker compose up -d workerпослеdown, либо--force-recreate) - просто экспортировать переменную в шелл хоста недостаточно, её нужно явно перечислить вenvironment:контейнераworker(это уже сделано дляDETECT_EVERY_N_FRAMES, остальные пороги вconfig.pyпока заданы константами и меняются только правкой кода).- Веб-интерфейс намеренно минимальный (загрузка/прогресс/скачивание) - фокус тестового задания на backend/CV-pipeline, а не на фронтенде.
bag-counter/
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── app/
│ ├── main.py # FastAPI приложение
│ ├── config.py # все настраиваемые параметры (пороги детектора/трекера/аномалий)
│ ├── database.py / models.py / schemas.py
│ ├── celery_app.py / tasks.py # асинхронная обработка
│ ├── routers/
│ │ ├── videos.py # HTTP API загрузки/обработки видео
│ │ └── calibration.py # HTTP API калибровки геометрии
│ ├── services/
│ │ ├── detector.py # MMDetection (авто-детект дообученной/базовой модели) + блоб-детектор
│ │ ├── tracker.py # SORT-подобный трекер (Kalman + Hungarian/IoU)
│ │ ├── counter.py # подсчёт по пересечению линии
│ │ ├── belt_motion.py # остановка/возобновление конвейера (data drift в зоне выхода)
│ │ ├── anomaly.py # мониторинг аномалий (яркость/цвет/резкость/сдвиг камеры)
│ │ ├── geometry.py # загрузка/сохранение калибровки ROI/линии/зоны выхода (JSON)
│ │ └── video_processor.py # оркестрация полного пайплайна на видео
│ └── utils/draw.py # отрисовка рамок/счётчика/баннеров
├── configs/README.md # как подключить дообученный MMDetection-чекпоинт
├── scripts/
│ ├── download_weights.sh # ручная загрузка весов, если недоступно на этапе сборки
│ └── preview_roi.py # быстрая проверка сохранённой калибровки
├── static/
│ ├── index.html # минимальный веб-интерфейс
│ └── calibrate.html # калибровка ROI/линии/зоны выхода кликами по кадру
├── Training_model.ipynb # дообучение RTMDet-tiny на Kaggle/Colab (данные из Roboflow)
└── data/ # bind-mounted volume: raw/, processed/, db/, config/geometry.json