# 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 м) — ядро работает с обеими без правок. --- ## Что нужно интеграции: один класс, один вызов ```python 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 не требуется. Медиана обработки кадра — 33 мс при бюджете 100 мс. --- ## Структура ``` flyguard/ ядро: стадии обработки, память, считывание bag.py cdr.py чтение rosbag2 и разбор CDR без ROS retina.py geometry.py решётка лучей, плоскость рельсов, ось пути lamina.py medulla.py контраст, движение lobula.py кандидаты mushroom_body.py память тоннеля (без меток) mbon_readout.py обученное считывание (с метками) fan_body.py накопление в координатах пути central_complex.py треки и улики descending.py решение pipeline.py сборка synth.py вставка предметов трассировкой лучей tools/ обучение, оценка, разбор tests/ 34 теста, запускаются без данных и без ROS docs/ методика и результаты artifacts/ обученные модели ``` --- ## Как запустить ```bash pip install -r requirements.txt pytest tests -q ``` Записи лидара в репозиторий не кладутся. Положите их рядом (`../data/for_hackathon/...`) или укажите путь: ```bash set FLYGUARD_DATA=D:\lidar\data ``` Обучение и оценка: ```bash 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 # кривые и график ``` Тяжёлые шаги сами раскладываются по бэгам на процессы — записей пять, физических ядер шесть, и это вся доступная зернистость: конвейер держит состояние между кадрами, поэтому разрезать одну запись нельзя. Замерено: полигон 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` | | Ложные тревоги, leave-one-bag-out | **8.0 трека на км**, 13.3 % кадров | `tools/evaluate.py --mbon-dir` | | То же без обученного считывания | 12.9 на км, 25.9 % кадров | там же, без `--mbon-dir` | | На незнакомой линии (памяти нет) | **21.3 на км**; без считывания 33.6 | `tools/evaluate.py --no-memory` | | Дальность (полигон, 14 004 наблюдения) | рабочая дальность **100 м**, P@50 = 0.70, **P@100 = 0.53, P@150 = 0.37** | `tools/plot_benchmark.py` | | Обработка кадра | медиана 33 мс из бюджета 100 мс | `tools/run_pipeline.py` | | Разделение «знакомое / новое» (без учителя) | ROC AUC 0.905 | `tools/tune_memory.py` | | Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.983** | `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` — что взято из коннектома как число, а что как идея.