- tools/make_benchmark.py: аугментации спавна d_start ∈ [40, 200] м, боковой дрейф v_lat, шум продольной координаты δs (устранение инверсии s_std), сценарии стоянки; - flyguard/lobula.py: зонный пол h_lo_core = 0.16 м в межрельсовой колее, динамическое расширение габарита в кривых W_eff(d) по Corridor.sigma(d), отсечение плоскости настила платформы; - flyguard/central_complex.py: поддержка лежащих препятствий в колее без штрафа за вытянутость формы; - flyguard/synth.py: добавлен класс «человек_лежа» (1.8×0.5×0.3 м); - flyguard/descending.py: дальний мягкий канал предупреждения на дистанциях >90 м; - flyguard/export.py: экспорт детекций в 3D BBox, уровни угрозы, маркеры RViz MarkerArray; - tests/test_pipeline.py, tests/run_tests.py: 38 юнит-тестов и автономный раннер.
254 lines
24 KiB
Markdown
254 lines
24 KiB
Markdown
# Комплексный план развития 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`.
|