12 KiB
Архитектура решения
Цепочка, которую просит ТЗ: ROS 2 bag → обработка облака → алгоритм обнаружения → результат детекции → визуализация.
1. Общая схема
ros2 bag play / реальный лидар
│
sensor_msgs/PointCloud2
0.3–0.9 млн точек, 10 Гц
│
┌──────────────────────────▼──────────────────────────────┐
│ flyguard/node.py — ROS 2-нода │
│ подписка (BEST_EFFORT) → очередь на 1 кадр │
│ обработка в отдельном потоке, старые кадры отброшены │
└──────────────────────────┬──────────────────────────────┘
│ flyguard.cdr.PointCloud2 (без копирования)
┌──────────────────────────▼──────────────────────────────┐
│ flyguard/pipeline.py — конвейер, хранит состояние │
└──────────────────────────┬──────────────────────────────┘
│
retina.py RETINA облако → дальностный образ 128 × N
│ выпрямление скоса каналов, слияние эх
▼
geometry.py HALTERES плоскость рельсов: крен, тангаж, высота
│ ось пути: дуга u(d) = c₁d + c₂d²
▼
lamina.py LAMINA диспаритет 1/R → ON/OFF, центр-окружение ×3
│
├────────────────► medulla.py MEDULLA / LOBULA PLATE
│ T4/T5 → LPTC: скорость без одометрии
│ LPLC2: надвигание
▼
lobula.py LOBULA связность с учётом глубины → кандидаты
│ разрез по контрасту: фигура из компоненты,
│ растёкшейся вдоль стены
│ признаки: габариты, целостность, тень, опора
▼
mushroom_body MUSHROOM BODY PN→KC (случайно, 6 входов) → APL → MBON
│ новизна: 1 — незнакомо, 0 — штатная обстановка
│ (привыкание внутри проезда сделано и выключено: п. 10
│ EXPERIMENTS — избирательности у механизма нет)
▼
fan_body.py FAN-SHAPED BODY сетка в координатах пути: опора для кандидата
│ там, где контраст структурно равен нулю
▼
central_complex CENTRAL COMPLEX накопление улик в координатах пути, треки
│
▼
descending.py DESCENDING два порога с гистерезисом → решение
│
▼
┌──────────────────────────────────────────────────────────┐
│ /flyguard/obstacle ObstacleStatus — программный выход │
│ /flyguard/detected Bool — бинарный статус │
│ /flyguard/distance Float32 — расстояние, м │
│ /flyguard/markers MarkerArray — рамки для RViz2 │
│ /flyguard/brain Image — схема мозга мухи │
│ /flyguard/diagnostics DiagnosticArray — задержки, скорость│
└──────────────────────────────────────────────────────────┘
2. Состояние между кадрами
Конвейер не обрабатывает кадры независимо. Между вызовами он хранит:
| Что | Где | Зачем |
|---|---|---|
| решётка лучей | FlyGuard.layout |
калибруется по первым 12 кадрам, дальше не меняется |
| плоскость пути | FlyGuard.plane |
сглаживание по кадрам, устойчивость к качке |
| ось пути | FlyGuard.corridor |
сглаживание и ограничение скорости изменения |
| профиль и точки | EgoMotionEstimator |
сопоставление с предыдущим кадром → скорость |
| задержанный сигнал | EmdBank |
вторая половина коррелятора T4/T5 |
| сетка пути | FanBody |
накопление лучей в координатах мира |
| треки | CentralComplex |
накопление улик в координатах пути |
| гистерезис | DescendingNeurons |
защёлка тревоги |
Поэтому один экземпляр FlyGuard обслуживает один поток данных. Для офлайн-экспериментов
с несколькими сценариями одновременно создаётся несколько экземпляров.
3. Потоки и реальное время
Нода разделена на два потока:
- поток ROS принимает облака и кладёт в слот на один кадр; если предыдущий ещё не
обработан, он отбрасывается и счётчик
dropped_framesрастёт; - рабочий поток берёт последний кадр и гоняет конвейер.
Так система реального времени отвечает на текущую обстановку, а не доедает накопившееся прошлое. Число отброшенных кадров публикуется в диагностике: если оно растёт, значит машина не тянет, и это видно сразу, а не проявляется скрытой задержкой.
Замер по стадиям ведётся всегда и публикуется в /flyguard/diagnostics, поэтому
профилировать решение можно прямо на стенде, не пересобирая его.
4. Разделение на пакеты
ros2_ws/src/
├── flyguard_msgs/ ament_cmake — только сообщения
│ └── msg/ObstacleStatus.msg, msg/DetectedObject.msg
└── flyguard/ ament_python — конвейер и нода
├── flyguard/
│ ├── cdr.py разбор PointCloud2 без ROS (офлайн-режим)
│ ├── bag.py чтение rosbag2 sqlite3 без ROS
│ ├── ros_conv.py sensor_msgs → внутреннее представление
│ ├── retina.py решётка лучей, дальностный образ
│ ├── geometry.py плоскость пути, ось, координаты (d, u, h)
│ ├── lamina.py ON/OFF, центр-окружение
│ ├── medulla.py T4/T5, LPTC, LPLC2, оценка движения
│ ├── lobula.py кандидаты, связность с учётом глубины
│ ├── mushroom_body.py новизна
│ ├── central_complex.py треки
│ ├── descending.py решение
│ ├── synth.py синтетические препятствия (для полигона)
│ ├── brain_view.py схема мозга мухи
│ ├── pipeline.py сборка
│ ├── node.py ROS 2-нода
│ └── data/pandar128_channels.csv поканальная таблица из руководства
├── launch/detect.launch.py
├── config/flyguard.yaml, config/flyguard.rviz
└── test/test_pipeline.py
Ключевое решение: ядро не зависит от ROS. rclpy импортируется только в node.py.
Благодаря этому весь конвейер запускается офлайн прямо по .db3, что дало возможность
отлаживать и мерить качество на Windows без ROS и быстро гонять полигон в несколько
параллельных сценариев.
5. Артефакты
| Файл | Что это | Как получен |
|---|---|---|
artifacts/mushroom_body.npz |
память тоннеля, без учителя | tools/train_mushroom_body.py на пустых проездах |
artifacts/mbon_readout.npz |
обученное считывание MBON | tools/train_mbon.py на размеченной вставками выборке |
artifacts/mbon_folds/ |
по модели на складку, для честной проверки | он же, ключ --save-folds |
data/cache/training_set.npz |
размеченная выборка кандидатов | tools/make_training_set.py |
artifacts/generalisation.json |
leave-one-bag-out | tools/evaluate.py |
artifacts/benchmark.json |
кривые дальности | tools/make_benchmark.py |
flyguard/data/pandar128_channels.csv |
128 каналов лидара | tools/extract_channel_table.py из руководства |
Память тоннеля копируется в Docker-образ и подхватывается launch-файлом автоматически;
путь переопределяется параметром memory_path.
6. Потоки данных в цифрах
Для кадра 128 × 3600 × 2 эха (полный круговой скан, 921 600 точек):
| Стадия | Объём на входе | Время, мс |
|---|---|---|
| приём и разбор сообщения | 24 МБ | ~2 (без копирования) |
| retina (оконная проекция) | только нужный сектор | 6–9 |
| стабилизация | ~150 тыс. точек | 3–4 |
| ось пути | 30 срезов | 4–5 |
| ламина | 128 × 600 × 3 масштаба | 6–7 |
| оценка движения | 6000 точек × ~20 проб | 5–8 |
| лобула | связность по маске | 3–5 |
| грибовидное тело | единицы кандидатов | 1–2 |
| центральный комплекс и решение | десятки треков | <0.3 |
Оконная проекция — важная оптимизация: при секторе обработки ±30° тяжёлая арифметика выполняется только над теми сырыми столбцами, которые в него попадут с учётом скоса каналов. На круговом скане это сократило стадию ретины с 29 до 7 мс без изменения результата (проверено побитовым сравнением).