Brainrot_Muxa/README.md
2026-09-26 20:01:02 +03:00

16 KiB
Raw Blame History

FlyGuard · ML-ядро

Обнаружение посторонних объектов в тоннеле метро по данным 3D-лидара Hesai Pandar128. Кейс 05, ЛЦТ-2026.

Это ядро обработки: облако точек на входе, решение о препятствии на выходе. Узел ROS 2, транспорт, контейнер и визуализация живут отдельно и сюда не входят — ядро от них не зависит и проверяется без ROS вообще.


Чем это не является

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

облако точек 0.3–0.9 млн, 10 Гц
   │
   ├─ RETINA           омматидиальная решётка → дальностный образ 128 × N
   ├─ HALTERES         плоскость рельсов: крен, тангаж, высота сенсора
   ├─ LAMINA           диспаритет 1/R → ON/OFF, центр-окружение на 3 масштабах
   ├─ MEDULLA / LP     T4/T5 → LPTC: скорость поезда без одометрии
   ├─ LOBULA           LC11: кандидаты; разрез компоненты по контрасту
   ├─ MUSHROOM BODY    KC → APL → MBON: новизна формы (без меток)
   │                   + обученное считывание MBON (с метками из физики)
   ├─ FAN-SHAPED BODY  накопление лучей в координатах пути
   ├─ CENTRAL COMPLEX  накопление улик в координатах пути, треки
   └─ DESCENDING       два порога с гистерезисом → решение

Ничего про геометрию сенсора не захардкожено: решётка лучей, высота установки, крен и тангаж калибруются по самим данным на первых кадрах. В записях встречаются две раскладки скана (3600 азимутов на 360° и 1200 на 120°) и две высоты установки (1.31 и 1.70 м) — ядро работает с обеими без правок.


Что нужно интеграции: один класс, один вызов

from flyguard.pipeline import FlyGuard, Params
from flyguard.mushroom_body import MushroomBody
from flyguard.mbon_readout import MbonReadout

fg = FlyGuard(Params(),
              memory=MushroomBody.load("artifacts/mushroom_body.npz"),
              readout=MbonReadout.load("artifacts/mbon_readout.npz"))

res = fg.process(cloud)          # cloud: flyguard.cdr.PointCloud2
if res is None:
    ...                          # первые 12 кадров уходят на калибровку решётки
else:
    d = res.decision             # detected, distance, confidence, emergency, objects
    res.total_ms                 # время обработки кадра

Требования к входу: flyguard.cdr.PointCloud2 — поля x, y, z, intensity, порядок точек как в сыром CDR. Конвейер хранит состояние между кадрами (решётка, плоскость пути, ось пути, треки, накопитель), поэтому один экземпляр обслуживает один поток данных; для параллельных сценариев нужны разные экземпляры.

Выход Decision: detected, distance (м), confidence (0…1), emergency (флаг экстренного торможения), objects (список подтверждённых треков с id, дистанцией и габаритами).

Всё считается на CPU, GPU не требуется. Медиана обработки кадра — 43 мс при бюджете 100 мс (замер по стадиям, один процесс, EXPERIMENTS п. 7.4).

Ламину можно перенести на NVIDIA GPU (Params(device="auto") или "cuda", модуль flyguard/device.py с переходом на CPU при любом сбое карты). Замерено на RTX 5070 Ti: ламина 5.8 → 2.9 мс с копированием туда и обратно, то есть кадр 43 → ~40 мс, — остальные 80 % времени кластеризация и геометрия, которые видеокарта не ускоряет. Поэтому по умолчанию device="cpu": выигрыш в 3 мс не стоит отдельного образа и --gpus all на машине проверки.


Структура

flyguard/          ядро: стадии обработки, память, считывание
  bag.py  cdr.py            чтение rosbag2 и разбор CDR без ROS
  device.py                 выбор CPU / NVIDIA GPU и переход на CPU при сбое
  retina.py  geometry.py    решётка лучей, плоскость рельсов, ось пути
  lamina.py  medulla.py     контраст (на CPU или GPU), движение
  lobula.py                 кандидаты
  mushroom_body.py          память тоннеля (без меток)
  mbon_readout.py           обученное считывание (с метками)
  track_readout.py          считывание по истории трека — инструмент замера
  fan_body.py               накопление в координатах пути
  central_complex.py        треки и улики
  descending.py             решение
  pipeline.py               сборка
  export.py                 3D-рамки, время до столкновения, маркеры RViz
  synth.py                  вставка предметов трассировкой лучей
tools/             обучение, оценка, разбор, полигон с аугментациями
tests/             53 теста, запускаются без данных и без ROS
docs/              методика и результаты
artifacts/         обученные модели

Как запустить

pip install -r requirements.txt
pytest tests -q          # или без pytest: python tests/run_tests.py

Через Docker (подробно — docs/DOCKER.md):

./docker-run.sh build
docker compose run --rm test         # тесты
docker compose run --rm info         # что видно из контейнера: CPU, GPU, CUDA
docker compose run --rm evaluate     # ложные тревоги
docker compose run --rm benchmark    # полигон

Для обучения на видеокарте — pip install -r requirements-gpu.txt и --device cuda у train_mbon.py, train_mushroom_body.py, evaluate.py.

Записи лидара в репозиторий не кладутся. Положите их рядом (../data/for_hackathon/...) или укажите путь:

set FLYGUARD_DATA=D:\lidar\data

Обучение и оценка:

python tools/make_training_set.py                    # размеченная выборка
python tools/train_mbon.py --device cuda --baseline  # считывание MBON
python tools/evaluate.py --mbon-dir artifacts/mbon_folds   # ложные тревоги
python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \
    --mbon-dir artifacts/mbon_folds                  # дальность обнаружения
python tools/plot_benchmark.py                       # кривые и график
python tools/compare_benchmark.py было.json стало.json   # правка парно

Большой бэг new_data (90 ГБ) не распаковывается целиком — инструменты читают его кусками прямо из архива (--tar, по умолчанию ../датасет/new_data или FLYGUARD_NEW_DATA):

python tools/make_training_set.py --new-data 0:110 --out data/cache/training_set_nd.npz
python tools/train_mbon.py --device cuda --data data/cache/training_set.npz \
    --data data/cache/training_set_nd.npz --train-only new_data_ --out nd.npz
python tools/eval_new_data.py --shards 110: \
    --readout было=artifacts/mbon_readout.npz --readout стало=nd.npz --out nd.json

Проверка на второй половине честная: вставки идут только в первую, а вторую модель не видит ни в каком виде (EXPERIMENTS п. 17.3).

Синтетику организаторов (cloud_with_fake_obj, 10 предметов) сверяет с эталоном отдельный инструмент. Разметки к бэгу нет, но вставленные точки не лежат на элевациях колец, и эталон извлекается из самого бэга (п. 18.2):

python tools/eval_org_synth.py                        # итоговые настройки
python tools/eval_org_synth.py --set half_width=1.6 --set h_top=0

В этом бэге нет поля ring, а порядок точек сбит вставками: retina.py восстанавливает кольца по элевации и раскладывает такие кадры по углам точек. До этой правки ядро на нём не обрабатывало ни одного кадра (п. 18.1).

Любое поле Params меняется без правки кода: --set h_lo_core=0.28 --set k_sigma=0.75 у make_benchmark.py, evaluate.py, make_training_set.py и check_obstacle.py. Два прогона полигона сравнивайте только парно — compare_benchmark.py считает, сколько наблюдений перевернулось в каждую сторону на одних и тех же вставках. Итоговые таблицы двух прогонов шумят сильнее, чем меняет их большинство правок (EXPERIMENTS п. 16.1).

Тяжёлые шаги сами раскладываются по бэгам на процессы — записей пять, физических ядер шесть, и это вся доступная зернистость: конвейер держит состояние между кадрами, поэтому разрезать одну запись нельзя. Замерено: полигон 134 → 36 с, сбор выборки 96 → 26 с на облегчённой конфигурации, то есть 3.7–3.8×, и файл на выходе совпадает с последовательным побайтово. Отключается --jobs 1.

Для замера задержки кадра --jobs 1 обязателен: под пятью процессами время кадра растёт с 32 до 56 мс. Это свойство замера, а не конвейера, поэтому evaluate.py в параллельном режиме печатает задержку как nan — чтобы такое число нельзя было случайно привести в отчёте.


Где мы сейчас

Метрика Значение Чем измерено
Реальный объект 0.67 × 1.35 м на 55 м 99.5 % кадров tools/check_obstacle.py
Синтетика организаторов, 10 предметов 9 из 10 верно tools/eval_org_synth.py
Ложные тревоги, leave-one-bag-out 5.7 трека на км, 8.4 % кадров tools/evaluate.py --mbon-dir
То же без обученного считывания 11.9 на км, 20.3 % кадров там же, без --mbon-dir
На незнакомой линии (памяти нет) 12.8 на км; без считывания 36.1 tools/evaluate.py --no-memory
Вторая половина new_data: другой день, 3.41 км, не видена при обучении 11.7 на км, 17.9 % кадров tools/eval_new_data.py
Дальность (полигон, 15 560 наблюдений) человек стоя: рабочая дальность 100 м, P@50 = 0.72, P@100 = 0.52, P@150 = 0.46 tools/plot_benchmark.py
Человек, упавший на пути P@50 = 0.50 (было 0.27), рабочая дальность 20 м там же
Обработка кадра медиана 33 мс из бюджета 100 мс; на ядре уровня стенда жюри 55 / 65 мс (медиана / p95) контейнер, EXPERIMENTS п. 18.5, 19.5
Разделение «знакомое / новое» (без учителя) ROC AUC 0.905 tools/tune_memory.py
Считывание MBON «предмет / тоннель» (с учителем) ROC AUC 0.986 tools/train_mbon.py

Проверка всегда leave-one-bag-out: память обучается на всех записях, кроме проверяемой. Иначе цифры лгут — подавлять конструкции, которые сам же и запомнил, умеет кто угодно, а на приватном тесте будет новый участок.


Что честно не работает

Разобрано замерами, подробности — в docs/EXPERIMENTS.md:

  • За 200 м на этих участках не увидит никто: прямая видимость в тоннелях 121–167 м, дальше линия взгляда упирается в стену кривой. На отдельных перегонах и того меньше — 49–90 м.
  • Мелкие предметы на большой дальности невозможны с этим сенсором: ведро (0.1 м²) на 160–190 м даёт один луч, каска и бутылка — ноль.
  • За 80–90 м прирельсовая зона не наблюдается вовсе: луч скользит по полотну, и самая низкая видимая точка у оси пути оказывается выше головки рельса на 0.1–0.6 м.
  • Привыкание внутри проезда сделано и отвергнуто замером — п. 10.
  • Яркость как признак мы себе не засчитываем. Абсолютной шкалы интенсивности в записях нет: медиана по кандидатам обстановки 3…7 в пяти бэгах и 23.5 в шестом. Вставка берёт яркость реальных возвратов с тех же лучей, то есть признак намеренно обесточен, и дальность из-за этого занижена — настоящий предмет был в 1.47 раза ярче окружения. Разбор — п. 11.1.

Документация

  • docs/ALGORITHM.md — что делает каждая стадия и почему именно так.
  • docs/EXPERIMENTS.md — все замеры, включая отрицательные результаты.
  • docs/CONNECTOME.md — что взято из коннектома как число, а что как идея.