# Комплексный план развития FlyGuard · Хакатон ЛЦТ-2026 (Кейс 05) Документ фиксирует согласованную архитектурную стратегию и детальный план доработок ядра `FlyGuard` для максимизации качества решения на хакатоне ЛЦТ-2026 («Обнаружение посторонних объектов в тоннеле метро по данным 3D-лидара»). --- ## 1. Контекст и текущее состояние Система [`FlyGuard`](../flyguard/pipeline.py) построена на принципах зрительной системы и грибовидных тел дрозофилы: ``` облако точек 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 два порога с гистерезисом → решение ``` ### Текущие измеренные показатели (Ryzen 7600X, Hesai Pandar128): * **Реальный объект на 55 м (`doubleT_obstacle`)**: **99.5 %** кадров с детекцией. * **Задержка кадра на CPU**: **32–33 мс** при лимите 100 мс (запас ~3×). * **Ложные тревоги (leave-one-bag-out)**: **8.0 трека/км** (11.0 % кадров). * **Дальность обнаружения стоящего человека**: рабочая дальность **100 м**, P@50 = 0.70, **P@100 = 0.53, P@150 = 0.33**. --- ### Главные узкие места, выявленные в ходе анализа: 1. **Застревание улики трека на 120–160 м**: На 150 м кандидат появляется в ~62 % кадров, но от предмета приходит лишь 4–6 лучей. Покадровое считывание MBON выдаёт малое $p \approx 0.05$ (из-за априорной редкости), улика прирастает на $+gain \cdot w \approx +0.017$, а утечка за промах отнимает $-leak = -0.12$. В итоге $evidence$ колеблется около нуля, и порог $0.5$ никогда не пробивается. 2. **Артефакты синтетики в [`TrackReadout`](../flyguard/track_readout.py)**: Признаки $s_{std}$ и $u_{std}$ перевернулись на реальном объекте ($0.365$ м у человека против $0.013$ м у фона) из-за того, что в синтетике предмет спавнился строго неподвижно относительно оценки движения, а в реальности поезд стоял, а человек двигался. Признаки $closed$ и $age$ заучили фиксированный спавн с 200 м. В текущей версии `track_blend = 0.0` (выключен). 3. **Неиспользуемая неопределённость коридора [`Corridor.sigma`](../flyguard/geometry.py)**: Ось коридора на кривой за горизонтом видимости (62–107 м) продолжается по касательной и расходится с истинным путем до 1.19 м на 150 м. Полуширина габарита жестко зафиксирована на $1.6$ м, поэтому крайние предметы выпадают из маски габарита ещё до кластеризации. 4. **Потеря кандидатов вдали (38 % пропусков на 120–160 м)**: Порог $min\_rays\_far = 3$ отсекает кадры с 1–2 лучами. При этом накопитель [`FanBody`](../flyguard/fan_body.py) за 15 кадров аккумулирует в вокселе десятки попаданий, но никак не помогает формированию покадрового кандидата. 5. **Скачок ложных тревог на новой линии ($8.0 \to 20.3$ треков/км)**: На незнакомом участке без обученной долговременной памяти штатные лотки и кронштейны получают высокую новизну. Габарит сейчас однороден и одинаково доверяет предметам на путях и конструкциям на стенах. 6. **Слипание с платформой на 30–50 м**: При смещении предмета $+0.9$ м к кромке платформы он объединяется с ней в связную компоненту длиной $>15$ м и выбрасывается правилом $max\_depth$. 7. **Отсутствие лежащего человека**: В [`catalogue`](../flyguard/synth.py) нет класса лежащего человека, а нижняя граница габарита $h_{lo} = 0.28$ м срезает упавшего на рельсы пассажира. --- ## 2. Поэтапный план улучшений ``` ┌─────────────────────────────────────────────────────────────┐ │ ФАЗА 1: Устранение артефактов данных и синтетики │ │ • Аугментация спавна, движения, стоянки в make_benchmark │ │ • Добавление «человек_лежа» (1.8×0.5×0.3 м) │ │ • Честное переобучение и валидация TrackReadout │ └──────────────────────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ФАЗА 2: Пробитие дальней зоны (100–160 м) │ │ • Двухканальное решение в DescendingNeurons (soft-warning) │ │ • Динамическое расширение габарита по Corridor.sigma(d) │ │ • Обратная связь FanBody → Lobula (порог 2 луча по опоре) │ └──────────────────────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ФАЗА 3: Обобщаемость на новый тоннель и ближняя зона │ │ • Иерархический габарит (опасная колея vs стены/лотки) │ │ • Вычитание плоскости платформы (h~1.1 м, |u|>1.2 м) │ │ • Понижение h_lo до 0.16 м в межрельсовой зоне │ └──────────────────────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ ФАЗА 4: ROS2 Экспорт, Замеры и Протокол Стенда Жюри │ │ • Модуль экспорта: 3D Bounding Boxes, Threat Level, RViz │ │ • Сквозной Leave-one-bag-out замер и стресс-тест на CPU │ └─────────────────────────────────────────────────────────────┘ ``` --- ### Фаза 1: Устранение артефактов данных и синтетики **Цель:** сделать выборку для обучения [`TrackReadout`](../flyguard/track_readout.py) инвариантной к реальным условиям, исключить переобучение на искусственные признаки генератора. #### 1.1. Аугментация генерации в [`tools/make_benchmark.py`](../tools/make_benchmark.py) * **Случайная дальность спавна**: вместо жесткой фиксации на $d_{start} = 200$ м задавать $d_{start} \sim \mathcal{U}(40, 200)$ м. Это разрушает фиктивную корреляцию между истинностью объекта и признаками $closed = \ln(d_{start}/d)$ и $age$. * **Поперечное и продольное блуждание объекта**: * Случайная скорость движения человека: $v_{lat} \sim \mathcal{U}(-0.4, 0.4)$ м/с. * Реалистичный шум положения $\delta s \sim \mathcal{N}(0, \sigma_s^2)$ с $\sigma_s = 0.15$ м (моделирует погрешность одометрии и скольжение лучей по телу). * Благодаря этому $s_{std}$ и $u_{std}$ перестают быть нулевыми для синтетических объектов. * **Сценарии со стоящим поездом**: * Генерация препятствий в кадры со стоящим поездом ($v = 0$, например, кадры бэга `doubleT_obstacle`), чтобы модель не считала нулевую скорость признаком отсутствия опасности. #### 1.2. Добавление класса «человек_лежа» в [`catalogue`](../flyguard/synth.py) * Добавить геометрию: ```python "человек_лежа": ObjectModel("человек_лежа", [ Box(length=1.80, width=0.50, height=0.30, h_base=0.0) ], reflectivity=0.35) ``` * В [`_quality`](../flyguard/central_complex.py) учесть, что вытянутость вдоль пути ($depth \approx 1.8$ м) при малой высоте ($h \le 0.35$ м) и расположении в межрельсовой зоне ($|u| \le 0.75$ м) не должна штрафоваться множителем $compact$. #### 1.3. Переобучение [`TrackReadout`](../tools/train_track.py) * Собрать обновлённый кэш треков: ```bash python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \ --mbon-dir artifacts/mbon_folds --tracks-out data/cache/track_set_aug.npz ``` * Обучить [`TrackReadout`](../tools/train_track.py) с контролем ROC AUC по всем полосам (целевой AUC $>0.90$ на полосе 110–160 м). * Проверить валидацию на `doubleT_obstacle`: оценка $p_{track}$ для реального объекта должна составлять $> 0.95$. --- ### Фаза 2: Пробитие дальней зоны (100–160 м) **Цель:** гарантировать, что появляющиеся на 120–160 м кандидаты и треки превращаются в надежное раннее предупреждение, не застревая в утечке $leak$. #### 2.1. Двухканальное решение в [`DescendingNeurons`](../flyguard/descending.py) Биологическая аналогия: разделение Giant Fiber (DNp01, жесткий аварийный порог) и DNp02/DNp11 (раннее предупреждение). ```python # 1. Аварийный канал (Giant Fiber / DNp01) — экстренное торможение is_emergency = (t.evidence >= self.emergency_evidence) and (d <= stopping_distance) # 2. Канал раннего предупреждения (DNp02 / DNp11) — ранний soft-warning is_far_warning = (d > 90.0) and (t.hits >= 2) and (t.p_track >= 0.65) is_near_warning = (d <= 90.0) and (t.evidence >= self.warn_evidence) decision.detected = is_emergency or is_far_warning or is_near_warning decision.emergency = is_emergency ``` * **Безопасность**: далёкий ложный трек физически не может вызвать экстренное торможение (`emergency = False`), но даёт заблаговременное оповещение на 150 м. #### 2.2. Динамический габарит по [`Corridor.sigma(d)`](../flyguard/geometry.py) В [`geometry.py`](../flyguard/geometry.py) уже заложен расчет неопределенности оси: $$\sigma(d) = base + rate \cdot d + 0.02 \cdot \max(d - d_{max\_seen}, 0)$$ На 150 м $\sigma(d) \approx 1.1$ м. В функции `gauge_mask` в [`flyguard/lobula.py`](../flyguard/lobula.py): $$W_{eff}(d) = half\_width + k_\sigma \cdot \sigma(d), \quad k_\sigma = 0.8$$ * На 50 м: $W_{eff} = 1.6 + 0.8 \cdot 0.45 \approx 1.96$ м. * На 150 м: $W_{eff} = 1.6 + 0.8 \cdot 1.10 \approx 2.48$ м. * На кривых радиуса 1300 м объект у внешнего рельса больше не вываливается за границу габарита. Стены тоннеля, попадающие в расширенную зону, отсекаются признаками $containment$ и низким $p_{mbon}$. #### 2.3. Обратная связь [`FanBody`](../flyguard/fan_body.py) $\to$ [`find_candidates`](../flyguard/lobula.py) [`FanBody`](../flyguard/fan_body.py) аккумулирует лучи в воксельной сетке пути $2.0 \times 0.2 \times 0.25$ м. В [`find_candidates`](../flyguard/lobula.py) вводится адаптивный порог числа лучей: ```python # Если в данной точке пространства накопитель уже видит стабильную опору: support = fan.support_at(d_comp, u_comp) if fan is not None else 0.0 need_rays = 2 if (d_comp >= 90.0 and support >= 0.6) else (3 if d_comp >= 90.0 else 4) ``` * Снижает пропуски кандидатов на 120–160 м с 38 % до $< 15$ % для слабых эхо. --- ### Фаза 3: Обобщаемость на незнакомой линии и ближняя зона **Цель:** снизить ложные тревоги на незнакомой линии с 20.3 до $<10$ треков/км и закрыть потерю объектов возле платформ на 30–50 м. #### 3.1. Иерархический двухзонный габарит (Core vs Peripheral) Сечение пути разделяется на две зоны ответственности: $$\text{Зона 1 (Колея)}: |u| \le 0.90\,\text{м}, \quad h \in [0.16, 1.90]\,\text{м}$$ $$\text{Зона 2 (Периферия)}: 0.90 < |u| \le 1.60\,\text{м} \quad \text{или} \quad h > 1.90\,\text{м}$$ 1. **Для Зоны 1 (Колея)**: * Нижний порог $h_{lo}$ снижается до **$0.16$ м** (захватывает лежащего человека). * Множитель компактности в [`_quality`](../flyguard/central_complex.py) не штрафует протяженность $depth$ до $2.0$ м при малой высоте ($h \le 0.35$ м). * Максимальный приоритет подтверждения трека ($gain = 0.40$). 2. **Для Зоны 2 (Периферия)**: * Пространство кабельных лотков, сигналов и кронштейнов. * Требование опоры на полотно: $grounded \le 0.3$ для висящих на стенах объектов подавляет их прирост улики. * Для формирования трека требуется $p_{mbon} \ge 0.50$. * **Результат**: на незнакомой линии ложные срабатывания от кабелей и ниш падают с $20.3$ до $< 10.0$ на км. #### 3.2. Геометрическая сегментация платформы на 30–50 м Платформа в метро имеет стандартизованные параметры: высота $1.10 \pm 0.05$ м над рельсом, удаление $|u| \ge 1.35$ м. В [`find_candidates`](../flyguard/lobula.py): ```python # Маска горизонтальной поверхности платформы: platform_mask = (np.abs(lat) > 1.30) & (tf.h >= 1.05) & (tf.h <= 1.25) # Исключаем лучи настила платформы из контекста связности: clustering_context = context & ~platform_mask ``` * Препятствие у края габарита ($u = +0.9$ м) больше не сшивается с настилом платформы в длинную 20-метровую стену и не отбрасывается по $max\_depth$. --- ### Фаза 4: ROS2-интеграция, API и валидация **Цель:** подготовить модуль для интеграции с ребятами, делающими ROS2-узел, сохранив ядро 100 % независимым и легковесным (чистый CPU, NumPy/SciPy). #### 4.1. Модуль экспорта для ROS2 ([`flyguard/export.py`](../flyguard/pipeline.py)) Обеспечивает чистый интерфейс между ядром и ROS2-нодой: * Преобразование треков [`Track`](../flyguard/central_complex.py) в 3D Bounding Boxes в координатах сенсора `(x, y, z, dx, dy, dz, yaw)`. * Расчет метрик безопасности: * `distance_along_track` ($d$, м) * `lateral_offset` ($u$, м) * `ttc` (Time to Collision, с) * `threat_level`: `CLEAR`, `WARNING`, `EMERGENCY`. * Генератор структуры для публикации `visualization_msgs/MarkerArray`: * Bounding boxes с цветовым кодированием (красный — авария, желтый — предупреждение). * 3D линия оси коридора тоннеля. * Текстовые метки с расстоянием и скоростью сближения над объектами. #### 4.2. Контроль рантайма на CPU * **Зависимости**: строго `numpy` и `scipy` в рантайме. * **Бюджет времени**: * Новые операции (lookup в FanBody, проверка 2 зон, маска платформы) выполняются на готовых массивах NumPy и добавляют $< 1.2$ мс. * Ожидаемая медиана на Ryzen 7600X: **$\approx 34$ мс**, на стенде жюри (i7-9700E): **$\approx 70–75$ мс** при лимите **100 мс**. --- ## 3. Сводная таблица ожидаемых метрик | Метрика | До улучшений | После реализации | Ключевой фактор роста | |---|:---:|:---:|---| | **Реальный объект на 55 м (`doubleT_obstacle`)** | 99.5 % | **> 99.5 %** | Сохранение надежности канала Giant Fiber | | **P@150 м (стоящий человек)** | 0.33 | **> 0.50** | Двухканальное решение по $p_{track}$ + $\sigma(d)$ | | **P@100 м (сидящий человек / чемодан)** | 0.34 / 0.21 | **> 0.55 / 0.40** | Порог 2 лучей при накоплении в [`FanBody`](../flyguard/fan_body.py) | | **P@50 м (лежащий человек)** | 0.0 % (срезался) | **> 0.75** | $h_{lo} = 0.16$ м в колее + класс в каталоге | | **Ложные тревоги (незнакомая линия)** | 20.3 на км | **< 10.0 на км** | Двухзонный габарит (периферия фильтрует лотки) | | **Обнаружение у платформ (30–50 м)** | 66 % (потеря 34 %) | **> 90 %** | Сегментация настила платформы до кластеризации | | **Время кадра (CPU)** | 33 мс | **< 36 мс** | Векторизованный NumPy/SciPy без оверхеда | --- ## 4. Дорожная карта выполнения (Action Checklist) - [ ] **Шаг 1 (Данные и синтетика)**: * [x] В [`flyguard/synth.py`](../flyguard/synth.py) добавлен класс `человек_лежа`. * [x] В [`flyguard/central_complex.py`](../flyguard/central_complex.py) скорректирован расчет `compact` в `_quality` для лежащих на путях объектов. * [ ] В [`tools/make_benchmark.py`](../tools/make_benchmark.py) добавить случайный $d_{start}$, поперечный дрейф $v_{lat}$, шум $\delta s$ и статичные кадры. * [ ] Пересобрать кэш `track_set.npz` и переобучить [`TrackReadout`](../tools/train_track.py). - [x] **Шаг 2 (Дальняя зона)**: * [x] В [`flyguard/descending.py`](../flyguard/descending.py) имплементирован двухканальный триггер soft-warning (DNp02/DNp11) для $d > 90$ м. * [x] В [`flyguard/lobula.py`](../flyguard/lobula.py) применён динамический габарит $W_{eff}(d) = half\_width + k_\sigma \cdot \max(\sigma(d) - 0.25, 0.0)$. * [x] Оптимизированы параметры `warn_far = 0.35` и `leak_far = 0.06` для предотвращения вымывания улики на дальности. - [x] **Шаг 3 (Зонирование и платформы)**: * [x] Двухзонный габарит: колея ($|u| \le 0.85$ м, $h_{lo\_core} = 0.16$ м) и периферия ($h_{lo} = 0.28$ м). * [x] Геометрическая фильтрация настила платформ ($|u| \ge 1.30$ м, $h \in [1.05, 1.25]$ м) для устранения слипания объектов со стенкой. - [ ] **Шаг 4 (ROS2 и валидация)**: * [x] Создан модуль экспорта [`flyguard/export.py`](../flyguard/export.py): расчет `BoundingBox3D`, `ThreatLevel` (`CLEAR`/`WARNING`/`EMERGENCY`), `TTC` и генерация структур для `visualization_msgs/MarkerArray`. * [x] Добавлены и успешно пройдены 35 модульных тестов (`tests/test_pipeline.py`). * [ ] Запустить сквозную валидацию и зафиксировать метрики в `docs/EXPERIMENTS.md`.