- Dockerfile на python:3.11-slim с libgomp1, непривилегированным пользователем и healthcheck; - docker-entrypoint.sh с поддержкой шорткатов test/benchmark/evaluate/pipeline; - docker-compose.yml с профилями тестирования, оценки и бенчмарка; - docs/DOCKER.md: руководство по сборке, запуску и монтированию томов. |
||
|---|---|---|
| artifacts | ||
| docs | ||
| flyguard | ||
| tests | ||
| tools | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| README.md | ||
| requirements.txt | ||
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 не требуется. Медиана обработки кадра — 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/ 32 теста, запускаются без данных и без ROS
docs/ методика и результаты
artifacts/ обученные модели
Как запустить
pip install -r requirements.txt
pytest tests -q
Записи лидара в репозиторий не кладутся. Положите их рядом
(../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 # кривые и график
Тяжёлые шаги сами раскладываются по бэгам на процессы — записей пять, физических
ядер шесть, и это вся доступная зернистость: конвейер держит состояние между
кадрами, поэтому разрезать одну запись нельзя. Замерено: полигон 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 трека на км, 11.0 % кадров | tools/evaluate.py --mbon-dir |
| То же без обученного считывания | 12.9 на км, 25.9 % кадров | там же, без --mbon-dir |
| На незнакомой линии (памяти нет) | 20.3 на км; без считывания 33.6 | tools/evaluate.py --no-memory |
| Дальность (полигон, 14 004 наблюдения) | рабочая дальность 100 м, P@50 = 0.70, P@100 = 0.53, P@150 = 0.33 | 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— что взято из коннектома как число, а что как идея.