656 lines
62 KiB
Markdown
656 lines
62 KiB
Markdown
# FlyGuard — обнаружение посторонних объектов в тоннеле метро по данным 3D-лидара
|
||
|
||
Решение кейса «Система обнаружения посторонних объектов для беспилотных поездов в тоннеле метро»
|
||
(хакатон «Лидеры цифровой трансформации 2026», направление «Город»; заказчик — Департамент
|
||
транспорта Москвы / ГУП «Московский метрополитен»).
|
||
|
||
На вход — поток облаков точек Hesai Pandar128. На выход — ответ на единственный важный вопрос:
|
||
**«путь свободен» или «впереди препятствие на N метров»**.
|
||
|
||
---
|
||
|
||
## В двух словах
|
||
|
||
ТЗ формулирует главный вызов так:
|
||
|
||
> «Возможно, нужно научиться хорошо описывать нормальный тоннель, а затем искать всё, что в него
|
||
> не вписывается.»
|
||
|
||
Это дословное описание того, чем занимается мозг **Drosophila melanogaster**. Муха решает ровно
|
||
нашу задачу — на лету, без разметки, без одометрии, с мизерным бюджетом нейронов и в жёстком
|
||
реальном времени. Поэтому архитектура FlyGuard собрана из её вычислительных схем, взятых из
|
||
коннектома (FlyWire / hemibrain):
|
||
|
||
| Проблема кейса | Схема мухи | Модуль |
|
||
|---|---|---|
|
||
| Разметки нет, почти всё — пустой тоннель | грибовидное тело: KC + APL + MBON, подавление **знакомого** | `mushroom_body.py` |
|
||
| Нельзя путать своё движение с чужим объектом | T4/T5 → LPTC, широкопольный оптический поток | `medulla.py` |
|
||
| Увидеть заранее, независимо от размера | LPLC2 — детектор надвигания | `medulla.py` |
|
||
| Мелкий объект = 5 лучей на кадр | кольцевой аттрактор эллипсоидного тела: накопление улик | `central_complex.py` |
|
||
| На 170 м контраст к фону равен нулю | веерное тело: улики копятся в координатах мира, а не кадра | `fan_body.py` |
|
||
| Не реагировать на штатные конструкции | депрессия синапсов KC→MBON | `mushroom_body.py` |
|
||
| Сенсор закреплён нежёстко | жужжальца и оцеллии: стабилизация «взгляда» | `geometry.py` |
|
||
| 100 мс на кадр, слабый CPU | разрежённый бинарный код, 0.1 % активных клеток | `mushroom_body.py` |
|
||
|
||
Подробный разбор с привязкой к типам нейронов — в [docs/ALGORITHM.md](docs/ALGORITHM.md)
|
||
и [docs/CONNECTOME.md](docs/CONNECTOME.md).
|
||
|
||
---
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
ROS 2 bag → /lidar_points (PointCloud2, 0.3–0.9 млн точек, 10 Гц)
|
||
│
|
||
├─ RETINA омматидиальная решётка → дальностный образ 128 × N
|
||
├─ HALTERES плоскость пути: крен, тангаж, высота сенсора
|
||
├─ LAMINA диспаритет 1/R, ON/OFF, центр-окружение на 3 масштабах
|
||
├─ MEDULLA/LP T4/T5 → LPTC: скорость поезда без одометрии; LPLC2: надвигание
|
||
├─ LOBULA LC11: кандидаты; разрез по контрасту отделяет предмет от стены
|
||
├─ MUSHROOM BODY новизна: подавление знакомой обстановки тоннеля
|
||
├─ FAN-SHAPED BODY накопление лучей в координатах пути: улика там, где нет контраста
|
||
├─ CENTRAL COMPLEX накопление улик в координатах пути, треки
|
||
└─ DESCENDING два порога: предупреждение и экстренное торможение
|
||
│
|
||
▼
|
||
/flyguard/obstacle · /flyguard/markers · /flyguard/brain · /flyguard/diagnostics
|
||
```
|
||
|
||
Детально — [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
||
|
||
---
|
||
|
||
## Сборка
|
||
|
||
```bash
|
||
docker build -t flyguard .
|
||
```
|
||
|
||
Образ основан на `ros:humble-ros-base-jammy` (Ubuntu 22.04 + ROS 2 Humble). Все зависимости
|
||
ставятся на этапе сборки; **во время работы сеть не нужна**. Обе обученные части лежат в
|
||
образе и подключаются сами: память тоннеля (`artifacts/mushroom_body.npz`) и считывание MBON
|
||
(`artifacts/mbon_readout.npz`) — ровно та конфигурация, что замерена в разделе «Результаты».
|
||
|
||
Собранный образ проверяется одной командой:
|
||
|
||
```bash
|
||
docker run --rm -v "$PWD/docker:/smoke:ro" flyguard bash /smoke/smoke_test.sh
|
||
```
|
||
|
||
### Стенд без интернета
|
||
|
||
Организаторы подтвердили: на тестовом сервере сети нет. Узлу она и не нужна — всё
|
||
ставится при сборке. Но сама `docker build` без сети не пройдёт: ей нужны базовый образ
|
||
`ros:humble-ros-base-jammy` и пакеты apt. Поэтому образ собирается там, где сеть есть, и
|
||
переносится файлом:
|
||
|
||
```bash
|
||
docker save flyguard | gzip > flyguard_image.tar.gz # на машине с сетью
|
||
```
|
||
```bash
|
||
docker load -i flyguard_image.tar.gz # на стенде
|
||
```
|
||
|
||
После `docker load` все команды ниже работают как есть. Образ — 9.3 ГБ, из них 6.6 ГБ —
|
||
PyTorch с библиотеками CUDA (раздел «Видеокарта»); архив — около 5 ГБ. Наш готовый архив —
|
||
`flyguard_image.tar`, 4.7 ГБ: слои в нём уже сжаты, поэтому `gzip` поверх не нужен, а
|
||
`docker load -i` принимает и `.tar`, и `.tar.gz`. Проверено на чистом движке: архив загружен в
|
||
Docker Desktop 29.8 (Windows 11, WSL 2) и работает с `--gpus all` и без него.
|
||
|
||
Если сети нет и там, где собирается образ, колёса PyTorch скачиваются на соседней машине
|
||
(`python docker/fetch_wheels.py`) и отдаются сборке любым HTTP-сервером — аргумент
|
||
`TORCH_WHEELS`, подробности в [docker/wheels/README.md](docker/wheels/README.md).
|
||
|
||
## Запуск
|
||
|
||
Одной командой — детектор и проигрывание бэга вместе:
|
||
|
||
```bash
|
||
docker run --rm -it --gpus all --network host --ipc host -v /path/to/bags:/data flyguard \
|
||
ros2 launch flyguard detect.launch.py bag:=/data/doubleT_obstacle
|
||
```
|
||
|
||
Или раздельно — узел в контейнере, а `ros2 bag play` на хосте, как на поезде:
|
||
|
||
```bash
|
||
docker run --rm -it --gpus all --network host --ipc host flyguard
|
||
```
|
||
|
||
```bash
|
||
ros2 bag play --read-ahead-queue-size 10 /path/to/bags/doubleT_obstacle
|
||
```
|
||
|
||
**`--ipc host` обязателен, если bag проигрывается на хосте.** С `--network host` Fast DDS
|
||
считает контейнер и хост одной машиной и передаёт кадры через разделяемую память
|
||
`/dev/shm`, а без `--ipc host` у контейнера она своя. Кадры тогда теряются молча: замерено,
|
||
узел не получает **ни одного** кадра, а с флагом — 252 из 252. Если флаг всё же забыт, узел
|
||
через пять секунд после появления издателя пишет в журнал, в чём дело. Проигрывание внутри
|
||
контейнера (первая команда или второй контейнер из этого же образа) работает и без флага:
|
||
точка входа видит собственную `/dev/shm` и переводит транспорт на UDP.
|
||
|
||
Профиль транспорта для кадров в 24 МБ лежит в образе и подключается сам
|
||
(`docker/fastdds_large.xml`); снять — `FLYGUARD_DDS_PROFILE=0`.
|
||
|
||
`--read-ahead-queue-size 10` тоже не косметика: по умолчанию проигрыватель читает вперёд
|
||
1000 сообщений, при кадре в 24 МБ это 24 ГБ, и пока он их читает, первые секунды записи
|
||
успевают «просрочиться» и не публикуются вовсе (без флага доходит 119 кадров из 201).
|
||
В первой команде launch ставит его сам.
|
||
|
||
С RViz окну нужен доступ к экрану хоста:
|
||
|
||
```bash
|
||
xhost +local:
|
||
docker run --rm -it --network host --ipc host -e DISPLAY=$DISPLAY \
|
||
-v /tmp/.X11-unix:/tmp/.X11-unix -v /path/to/bags:/data flyguard \
|
||
ros2 launch flyguard detect.launch.py bag:=/data/doubleT_obstacle rviz:=true
|
||
```
|
||
|
||
**RViz в контейнере без доступа к видеокарте рисует программно** (Mesa `llvmpipe` на
|
||
процессоре): вращение мышью идёт рывками по 2–5 кадров в секунду, хотя в настройках стоит
|
||
`Frame Rate: 30` — это потолок, а не факт. Замер на `doubleT_obstacle`: программный RViz
|
||
занимает 1.9 ядра, с видеокартой — 0.2 ядра, в 9 раз меньше. Видеокарта пробрасывается так:
|
||
|
||
| Где | Добавить к `docker run` |
|
||
|---|---|
|
||
| Linux, NVIDIA (нужен nvidia-container-toolkit) | `--gpus all` (графика драйвера включена в образе: `NVIDIA_DRIVER_CAPABILITIES=all`) |
|
||
| Linux, встроенная Intel/AMD | `--device /dev/dri` |
|
||
| Windows: Docker Desktop или Docker в WSL 2 | `--device /dev/dxg -v /usr/lib/wsl:/usr/lib/wsl:ro -e LD_LIBRARY_PATH=/usr/lib/wsl/lib` (этих флагов хватает и для CUDA; одного `--gpus all` в Docker Desktop мало — CUDA будет, а RViz рисует процессором) |
|
||
|
||
Проверено на Windows 11 + WSL 2 (RTX 5070 Ti, `D3D12`) — в Docker внутри WSL и в Docker
|
||
Desktop 29.8; строки для Linux — стандартные флаги, на стенде не проверялись. Экран на
|
||
Windows даёт WSLg: из терминала Ubuntu — `-e DISPLAY=:0 -v /tmp/.X11-unix:/tmp/.X11-unix`,
|
||
из PowerShell с Docker Desktop — `-e DISPLAY=:0 -v /run/desktop/mnt/host/wslg/.X11-unix:/tmp/.X11-unix`.
|
||
Записи на Windows держите в файловой системе WSL (`/root/bags` и т. п.): с диска `C:` через
|
||
общую папку Docker Desktop кадры по 24 МБ идут около одного в секунду — замерено, за минуту
|
||
дошло 62 кадра из 201. Чем рисует OpenGL, показывает проверка без окна:
|
||
|
||
```bash
|
||
docker run --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix <флаги видеокарты> \
|
||
-v "$PWD/docker:/smoke:ro" flyguard python3 /smoke/gl_probe.py
|
||
```
|
||
|
||
Можно и вовсе запустить RViz на хосте с нашим конфигом, если там стоит ROS 2:
|
||
`rviz2 -d ros2_ws/src/flyguard/config/flyguard.rviz`. На обнаружение всё это не влияет:
|
||
детектор с программным RViz и с аппаратным обработал одни и те же 190 кадров из 190.
|
||
|
||
RViz показывает не сырое облако, а **облако обзора**, которое публикует сам узел
|
||
(`/flyguard/view_cloud`): лучи сектора обработки, выровненные по плоскости рельсов, — до
|
||
77 тысяч точек вместо 900 тысяч. Рамки препятствий стоят в тех же координатах, пол пути
|
||
лежит на сетке. Сырое облако RViz не тянет (24 МБ на кадр), и топик у записей разный —
|
||
`/lidar_points` в пяти записях и `/sensing/lidar/hesai128/pointcloud` в `doubleT_obstacle`, —
|
||
а облако обзора есть всегда, из какого бы топика узел ни читал. Публикуется, только когда на
|
||
него кто-то подписан, и в замеры задержки не входит.
|
||
|
||
**Оранжевый контур — габарит, который узел проверяет.** Ось пути впереди узел оценивает в
|
||
каждом кадре по сечению тоннеля, и в кривой габарит изгибается вместе с ней. Проверяется
|
||
объединение изогнутого габарита и прямого, вдоль оси лидара: неточная оценка оси не должна
|
||
сужать зону поиска. На прямом пути контуры совпадают, в кривой рисуются оба — изогнутый ярко,
|
||
прямой бледно ([рисунок](docs/figures/curve_gauge.png)). Рамки препятствий стоят у своих
|
||
точек. На кривой синтетики организаторов (радиус около 760 м) ось сходится с положением
|
||
предмета до 0.04 м на 22 м, до 0.15 м на 39 м и до 0.27 м на 64 м (EXPERIMENTS п. 20).
|
||
|
||
Чтобы не ждать до нужного места, проигрывание начинается с любой секунды: `start:=18`.
|
||
|
||
### Видеокарта
|
||
|
||
Раскладка точек по решётке, ламина и кластеризация — три стадии, работающие с целым
|
||
образом 128 × 600 лучей, — считаются на видеокарте, если узел её видит; остальное на
|
||
процессоре. Для этого контейнеру нужен `--gpus all` (на Linux — с nvidia-container-toolkit).
|
||
Без флага, без видеокарты или при любом её сбое на ходу узел считает на процессоре — **с тем
|
||
же результатом**: на видеокарте те же операции в тех же типах, и решения совпадают с
|
||
процессорными покадрово на синтетике и шести записях ([EXPERIMENTS](docs/EXPERIMENTS.md),
|
||
п. 21). Где идёт счёт, узел пишет в журнал:
|
||
|
||
```
|
||
вычисления: пока процессор — видеокарту проверяю и прогреваю в фоне (в Docker под WSL до 20 с), результат тот же
|
||
вычисления: видеокарта NVIDIA GeForce RTX 5070 Ti (16 ГБ, CUDA 12.8) — сетчатка, ламина, кластеризация; остальное на процессоре (готова через 16.0 с после старта)
|
||
```
|
||
|
||
**Видеокарта поднимается в фоне**, а узел подписывается на лидар сразу и первые кадры считает
|
||
на процессоре; на видеокарту он переходит между кадрами. Первый запуск ядер CUDA в новом
|
||
контейнере под WSL длится 15 с (на Windows без контейнера — 0.9 с), и раньше узел всё это
|
||
время не был подписан: запись, пущенная сразу, теряла первые 15 с. Теперь запуск одновременно
|
||
с записью даёт те же 201 кадр из 201 и 189 обнаружений из 190. Зависни драйвер совсем, узел
|
||
останется на процессоре, а не встанет.
|
||
|
||
Кадр в контейнере (медиана / p95), видеокарта против процессора:
|
||
|
||
| Где | `doubleT_obstacle` | синтетика организаторов |
|
||
|---|---|---|
|
||
| Docker Desktop 29.8 (Windows 11, WSL 2), `--gpus all` | 23.9 / 35.2 мс против 34.8 / 39.4 | 26.4 / 42.6 мс против 42.7 / 48.0 |
|
||
| Docker Engine в Ubuntu под WSL, `/dev/dxg` | 23.4 / 30.2 мс против 31.8 / 34.9 | — |
|
||
|
||
Решения одинаковые: 189 обнаружений из 190 и 398 кадров с тревогой из 1499 в обоих режимах.
|
||
Принудительно на процессоре — `device:=cpu`. В Docker Engine, поставленном прямо в Ubuntu под
|
||
WSL (без Docker Desktop), вместо `--gpus all`:
|
||
`--device /dev/dxg -v /usr/lib/wsl:/usr/lib/wsl:ro -e LD_LIBRARY_PATH=/usr/lib/wsl/lib`.
|
||
Под WSL драйвер CUDA пишет в каждый новый контейнер свой кэш, около 330 МБ, а виртуальный диск
|
||
Docker от этого растёт и сам не сжимается; если на диске тесно — `-e CUDA_CACHE_DISABLE=1`, на
|
||
скорость это не влияет.
|
||
|
||
### Пауза, перемотка, покадрово
|
||
|
||
Проигрыватель, запущенный из launch (`bag:=...`), клавиатуры не слышит. Для него в образе
|
||
есть пульт: контейнер запускается с именем, а пульт — во втором терминале.
|
||
|
||
```bash
|
||
docker run --rm -it --name flyguard_demo --network host --ipc host -e DISPLAY=$DISPLAY \
|
||
-v /tmp/.X11-unix:/tmp/.X11-unix -v /path/to/bags:/data flyguard \
|
||
ros2 launch flyguard detect.launch.py bag:=/data/doubleT_obstacle rviz:=true
|
||
```
|
||
```bash
|
||
docker exec -it flyguard_demo flyguard-keys /data/doubleT_obstacle
|
||
```
|
||
|
||
| Клавиша | Действие |
|
||
|---|---|
|
||
| пробел | пауза / продолжить |
|
||
| ← → | на 3 секунды назад / вперёд |
|
||
| `.` | один кадр вперёд (на паузе) |
|
||
| ↑ ↓ | быстрее / медленнее (×0.1 … ×4) |
|
||
| `0` | в начало записи |
|
||
| `q` | выйти, запись играет дальше |
|
||
|
||
На паузе RViz держит последний кадр и рамки, и сцену можно крутить. После перемотки узел сам
|
||
замечает скачок времени записи и начинает треки, одометрию и ось пути заново — иначе
|
||
предмет с прошлого места всплыл бы там, где его нет; то же самое происходит на каждом круге
|
||
`ros2 bag play --loop`. Если `ros2 bag play` запущен вручную в своём терминале, пульт не нужен:
|
||
у проигрывателя свои клавиши (пробел — пауза, → — следующий кадр, ↑ ↓ — скорость).
|
||
|
||
Все записи подряд, по строке сводки на каждую (узел поднимается заново для каждой записи):
|
||
|
||
```bash
|
||
docker run --rm --network host --ipc host -v /path/to/bags:/data:ro \
|
||
-v "$PWD/docker:/smoke:ro" flyguard bash /smoke/check_all.sh
|
||
```
|
||
|
||
Итог узел пишет прямо в консоль контейнера — смену состояния и, пока тревога держится,
|
||
ближайшую дальность раз в секунду; когда тревога снимается, пишет `путь свободен`. Так
|
||
выглядит запись с настоящим препятствием (`doubleT_obstacle`, объект на 55–57 м):
|
||
|
||
```
|
||
[detector-1] [WARN] [1790249076.653486850] [flyguard]: ПРЕПЯТСТВИЕ: 55.7 м, уверенность 0.36, объектов 1
|
||
[detector-1] [WARN] [1790249327.983883704] [flyguard]: препятствие: 55.9 м, уверенность 0.51, объектов 1
|
||
[detector-1] [WARN] [1790249336.988973904] [flyguard]: препятствие: 56.0 м, уверенность 0.68, объектов 2
|
||
```
|
||
|
||
Если поезд движется, к строке добавляется время до столкновения.
|
||
|
||
Полный программный выход без RViz:
|
||
|
||
```bash
|
||
ros2 topic echo /flyguard/obstacle
|
||
```
|
||
|
||
### Облака без поля `ring` и с нарушенным порядком точек
|
||
|
||
Синтетический бэг организаторов (`cloud_with_fake_obj`, 24.09) устроен не так, как записи с
|
||
поезда: в облаке только `x, y, z, intensity`, без `ring` и `timestamp`, а там, где вставлен
|
||
предмет, заслонённые им точки удалены и точки предмета вписаны в середину массива. Число точек
|
||
в кадре гуляет от 214 до 353 тысяч, и порядок «столбец · эхо · кольцо» нарушен даже в кадрах
|
||
ровно на 307 200 точек — ни в одном из первых двенадцати он не цел. Узел это переносит сам:
|
||
|
||
* кольца восстанавливаются по гистограмме элевации (у лазерного канала она постоянна до
|
||
0.0001°, соседние каналы Pandar128 разнесены на 0.086°);
|
||
* каждый кадр проверяется: у целого кадра элевация каждой точки совпадает с элевацией её
|
||
кольца. Такой кадр идёт быстрым путём по порядку точек, а кадр с нарушенным порядком
|
||
раскладывается в ту же решётку **по углам каждой точки**. На целом кадре оба пути дают
|
||
образ бит в бит; раскладка по углам дороже: 11 мс против 6 на синтетике (скан 120°),
|
||
22 против 6 на круговом скане. В контейнере кадр синтетики — 43.7 / 50.2 мс (медиана / p95).
|
||
|
||
До этой правки узел на синтетике не обработал бы ни одного кадра: калибровка решётки
|
||
требовала поле `ring`.
|
||
|
||
### Демонстрация со схемой мозга мухи
|
||
|
||
```bash
|
||
docker run --rm -it --network host --ipc host -e DISPLAY=$DISPLAY \
|
||
-v /tmp/.X11-unix:/tmp/.X11-unix -v /path/to/bags:/data flyguard \
|
||
ros2 launch flyguard detect.launch.py bag:=/data/doubleT_obstacle rviz:=true brain_view:=true
|
||
```
|
||
|
||
Топик `/flyguard/brain` отдаёт мозг дрозофилы, подсвеченный **живой активностью**: видно,
|
||
как загорается ламина на контрасте, лобулярная пластинка на скорости, доли грибовидного тела
|
||
на новизне и гигантское волокно в момент решения. Вид отключён по умолчанию, чтобы не попадать
|
||
в замеры задержки; когда включён, рисуется пять раз в секунду по 15 мс. С `brain_view:=true`
|
||
RViz берёт свой конфиг: схема справа во всю высоту, панель Displays свёрнута — открывается
|
||
стрелкой у левого края окна.
|
||
|
||
Три варианта, `brain_style`:
|
||
|
||
| Значение | Что показывает |
|
||
|---|---|
|
||
| `scheme` | нарисованная схема нейропилей с подписями |
|
||
| `cloud` | **139 255 настоящих нейронов** коннектома FlyWire на своих анатомических местах |
|
||
| `hybrid` | по умолчанию: сверху панели решётки лучей (дальность, ON/OFF ламины, кандидаты), снизу облако нейронов |
|
||
|
||
Облако — не симуляция. Мембранные потенциалы 139 тысяч клеток никто не интегрирует: коннектом
|
||
даёт анатомию и принадлежность клеток, конвейер даёт активность по стадиям, а вид накладывает
|
||
одно на другое. Привязка держится на именах типов клеток, и все они есть в выгрузке поимённо —
|
||
LC11 (127 нейронов), LPLC2 (210), HS/VS (22), T4/T5 (12 245), клетки Кеньона (5177),
|
||
MBON (96), APL (2), гигантское волокно DNp01 (2). Ровно те схемы, из которых собран FlyGuard.
|
||
|
||
Для экрана и видео вид рисуется крупнее: `brain_scale:=2` — 2360 × 1572 (под 2K),
|
||
`brain_scale:=3` — 3540 × 2358 (под 4K). Сомы берутся из атласа своего размера, это настоящие
|
||
координаты, а не растянутая картинка; шрифты, линии и отступы растут вместе с масштабом.
|
||
Кадр вида рисуется 14 мс в 1×, 57 мс в 2× и 113 мс в 3×, пять раз в секунду; в 2× на записи
|
||
с поезда и на синтетике узел принял все кадры. По умолчанию масштаб 1 — прежние 1180 пикселей
|
||
по ширине.
|
||
|
||
Атласы (по 620–660 КБ, три размера) лежат в пакете и пересобираются из публичных выгрузок Codex:
|
||
|
||
```bash
|
||
python tools/build_brain_atlas.py
|
||
python tools/build_brain_atlas.py --width 2360 --height 1240 --margin 36 --out ros2_ws/src/flyguard/flyguard/data/brain_atlas_x2.npz
|
||
```
|
||
|
||
Данные FlyWire — CC-BY 4.0 (Dorkenwald et al., Schlegel et al., Nature 2024).
|
||
|
||
---
|
||
|
||
## Выходные данные
|
||
|
||
| Топик | Тип | Назначение |
|
||
|---|---|---|
|
||
| `/flyguard/obstacle` | `flyguard_msgs/ObstacleStatus` | основной программный выход |
|
||
| `/flyguard/detected` | `std_msgs/Bool` | бинарный статус для простой интеграции |
|
||
| `/flyguard/distance` | `std_msgs/Float32` | расстояние до ближайшего объекта, м (−1 — свободно) |
|
||
| `/flyguard/markers` | `visualization_msgs/MarkerArray` | рамки объектов для RViz |
|
||
| `/flyguard/view_cloud` | `sensor_msgs/PointCloud2` | облако обзора для RViz: сектор обработки в координатах пути, только при подписчике |
|
||
| `/flyguard/brain` | `sensor_msgs/Image` | мозг мухи с живой активностью (схема, облако нейронов или гибрид) |
|
||
| `/flyguard/diagnostics` | `diagnostic_msgs/DiagnosticArray` | задержки по стадиям, скорость, радиус кривой |
|
||
|
||
`ObstacleStatus` содержит: `detected`, `emergency`, `distance`, `time_to_collision`,
|
||
`confidence`, `speed`, `stopping_distance`, `processing_ms` и список объектов с габаритами,
|
||
числом лучей, новизной и стабильным `track_id`.
|
||
|
||
---
|
||
|
||
## Параметры
|
||
|
||
Все настройки — в [ros2_ws/src/flyguard/config/flyguard.yaml](ros2_ws/src/flyguard/config/flyguard.yaml),
|
||
переопределяются при запуске:
|
||
|
||
```bash
|
||
ros2 launch flyguard detect.launch.py fov_deg:=35.0 half_width:=1.5
|
||
```
|
||
|
||
| Параметр | По умолчанию | Смысл |
|
||
|---|---|---|
|
||
| `input_topic` | `/lidar_points` | топик лидара. Узел слушает и запасные имена (`/sensing/lidar/hesai128/pointcloud`, `/points_raw`), а если облако идёт в топик с другим именем, через секунду находит его сам, подписывается и пишет об этом в журнал |
|
||
| `device` | `auto` | где считать сетчатку, ламину и кластеризацию: `auto` — видеокарта, если есть (контейнер с `--gpus all`), иначе процессор; `cuda`; `cpu`. Видеокарта поднимается в фоне, первые кадры считает процессор. Результат одинаков, см. раздел «Видеокарта» |
|
||
| `mbon_path` | из образа | обученное считывание MBON (`.npz`); пусто — ручная формула веса улики |
|
||
| `mbon_power` | `1.5` | резкость считывания: вес наблюдения — вероятность в этой степени, см. [EXPERIMENTS](docs/EXPERIMENTS.md) п. 17.4 |
|
||
| `half_width` | `1.2` | полуширина габарита, м. Так его задают организаторы в синтетике 24.09: «у края» — до 1.13 м от оси, «вне габарита, но близко» — с 1.14 м. При прежних 1.6 второй давал ложную тревогу, см. [EXPERIMENTS](docs/EXPERIMENTS.md) п. 18.4 |
|
||
| `h_lo` / `h_hi` | `0.28` / `2.3` | границы основной части габарита по высоте над головкой рельса, м |
|
||
| `h_top` / `half_width_top` / `top_d_max` | `3.3` / `1.0` / `90` | верхняя секция: от `h_hi` до `h_top`, уже основной и не дальше `top_d_max` — то, что свисает со свода в путь вагона. Ищется отдельным проходом (`top_detect`), в котором свод не склеивается со свисающим; стоящее, что уходит вниз к полу, остаётся основному проходу. Верх 3.3, а не выше: у свода свои кабели и кронштейны, и на 60–90 м ошибка наклона опускает их в секцию. `h_top: 0` — выключить |
|
||
| `flat_h` / `flat_top` / `flat_d` / `flat_w` | `0.06` / `0.26` / `70` / `0.15` | плоское у пола: высотой меньше `flat_h`, целиком ниже `flat_top` над рельсом, не дальше `flat_d` — вес наблюдения умножается на `flat_w`. Пластины на полотне, края жёлоба, порог гермозатвора: организаторы подтвердили, что в жёлобе — не препятствие. `flat_h: 0` — выключить |
|
||
| `h_lo_core` / `core_from` | `0.16` / `30.0` | пол между рельсами и дальность, с которой он опущен: иначе упавший на пути человек (0.30 м) виден верхушкой в два сантиметра. Ближе 30 м пол прежний — там в полосу попадают головки рельсов, см. [EXPERIMENTS](docs/EXPERIMENTS.md) п. 16.3 |
|
||
| `d_min` / `d_max` | `4.0` / `220.0` | зона поиска по дальности, м |
|
||
| `fov_deg` | `30.0` | полусектор обработки по азимуту, ° |
|
||
| `min_rays` | `4` | минимум лучей на кандидата |
|
||
| `memory_path` | из образа | обученная память тоннеля (`.npz`) |
|
||
| `brain_view` | `false` | публиковать вид мозга |
|
||
| `brain_style` | `hybrid` | `scheme` · `cloud` · `hybrid` — что именно рисовать |
|
||
| `brain_scale` | `1` | масштаб вида мозга: `2` — 2360 × 1572 для 2K, `3` — для 4K (облако и гибрид) |
|
||
| `ctx_up` | `4.0` | насколько кластеризация смотрит выше габарита, м; меньше — и колонна, срезанная по верхней границе, выглядит предметом |
|
||
| `split_adv` | `0.0` | разделение фигуры и фона по скорости сближения: рабочая дальность 62 → 80 м ценой вчетверо больших ложных тревог. По умолчанию выключено, см. [EXPERIMENTS](docs/EXPERIMENTS.md) п. 9.4 |
|
||
| `enable_accumulator` | `true` | накопление лучей в координатах пути: удваивает рабочую дальность там, где локальный контраст равен нулю |
|
||
| `acc_near` / `acc_gain` | `55.0` / `1.5` | с какой дальности включается накопление и какая опора считается полным контрастом |
|
||
| `split_gap` | `6.0` | разрез компоненты, растёкшейся вдоль стены, по контрасту ламины: гладкая стена даёт ноль по построению, предмет на ней — ступеньку. `0` — выключить |
|
||
| `split_near` / `split_top` | `55.0` / `1` | ближе какой дальности не резать и сколько фигур выносить из компоненты; одна фигура вместо всех — 6.4 против 9.4 ложных трека на км при той же дальности |
|
||
| `enable_habituation` | `false` | привыкание внутри проезда. Выключено: измерено, что избирательности нет, см. [EXPERIMENTS](docs/EXPERIMENTS.md) п. 10 |
|
||
| `best_effort` | `false` | QoS: RELIABLE. При BEST_EFFORT потеря одного UDP-фрагмента отбрасывает весь 24-мегабайтный кадр |
|
||
| `queue_depth` | `10` | глубина очереди подписки |
|
||
| `raw_subscription` | `true` | брать кадр сырыми байтами CDR, минуя сборку Python-объекта `sensor_msgs` |
|
||
| `async_worker` | `false` | обрабатывать в отдельном потоке; по умолчанию в колбэке — поток борется за GIL с приёмом |
|
||
|
||
Ничего про геометрию сенсора не захардкожено: решётка лучей, высота установки, крен и тангаж
|
||
**калибруются по самим данным** на первых кадрах. В предоставленных записях встречаются две
|
||
разные раскладки скана (3600 азимутов на 360° и 1200 на 120°) и две высоты установки сенсора
|
||
(1.31 м и 1.70 м) — решение работает с обеими без единой правки.
|
||
|
||
---
|
||
|
||
## Результаты
|
||
|
||
Полный разбор с методикой — [docs/EXPERIMENTS.md](docs/EXPERIMENTS.md).
|
||
|
||
| Метрика | Значение |
|
||
|---|---|
|
||
| Реальный объект (0.67 × 1.35 м на 55 м) | обнаружен в **99.5 %** кадров |
|
||
| Синтетика организаторов (`cloud_with_fake_obj`, 10 предметов) | **9 из 10** верно: из восьми предметов в габарите найдены семь (2×2 посередине — с 95 м, 2×2 сверху — с 87 м, длинный на рельсах — с 79 м, 0.3 на рельсе — с 76 м, стержень 5 см с потолка — только с 13 м), оба предмета вне габарита — без тревоги; не найден 2×2, заходящий в габарит на 0.3 м. Ложных тревог за 151 с — три коротких (`tools/eval_org_synth.py`, EXPERIMENTS п. 18–19) |
|
||
| Ложные тревоги, leave-one-bag-out | **5.7 разных ложных трека на километр** (8.4 % кадров) — без обученного считывания 11.9 |
|
||
| То же на незнакомой линии, памяти нет | **12.8 на км** (20.7 % кадров) — без считывания 36.1 |
|
||
| То же на второй половине `new_data` — другой день, 3.41 км, ни в каком виде не видена при обучении, памяти нет | **11.7 на км** (17.9 % кадров) |
|
||
| Время обработки кадра | итоговый образ в контейнере: `doubleT_obstacle` **33.4 / 37.9 мс** (медиана / p95), синтетика **43.5 / 48.8 мс** (требование 100 мс) |
|
||
| То же на ядре уровня стенда жюри (i7-9700E ≈ 0.61 нашего по PassMark, `docker/jury_cpu_test.sh`) | реальная запись **55 / 65 мс** (медиана / p95), 201 из 201; синтетика **68 / 80 мс**, 1510 из 1510 |
|
||
| Приём в контейнере | **201 из 201** и **252 из 252**, отброшено **0** |
|
||
| Рабочая дальность, размеченный полигон | **100 м** для человека стоя, **122 м** сидя, **100 м** для чемодана, 20 м для человека лёжа |
|
||
| Обнаружение на 50 м (при видимости) | **0.72** человек стоя; **0.50** человек лёжа — против 0.27 до пола в колее |
|
||
| Обнаружение на 100 м (при видимости) | **0.52** стоя и **0.54** сидя — против 0.19 без обученного считывания; лёжа **0.19** против 0.00 |
|
||
| Обнаружение на 150 м (при видимости) | **0.46** стоя и **0.39** сидя — против 0.39 и 0.20 до пола в колее |
|
||
| Оценка скорости без одометрии | согласие перепроекции 0.76–0.95 |
|
||
| Разделение «знакомое / новое» | ROC AUC 0.905 |
|
||
| Обобщение на форму, которой не было в обучении | ROC AUC **0.965–0.997**, потеря не больше 0.028 |
|
||
| Вклад накопления улик | без него ложных объектов в 13.5 раза больше |
|
||
| Вклад памяти тоннеля | вдвое меньше ложных тревог, обнаружение не страдает |
|
||
| Вклад обученного считывания MBON | при итоговых настройках ложных вдвое меньше: 11.9 → 5.7 на км на знакомой линии, 36.1 → 14.7 на незнакомой |
|
||
| Вклад гашения знакомости на дальности | P@100 0.31 → 0.53, P@150 0.00 → 0.33, рабочая дальность 80 → 100 м |
|
||
| Вклад дальнего порога тревоги | P@150 0.33 → 0.37, человек сидя 0.22 → 0.24; ложных на знакомой линии 8.0 → 8.0, на незнакомой 20.3 → 21.3 |
|
||
| Вклад резкости считывания 1.5 | ложных на незнакомой линии 23.5 → 14.7 на км, на знакомой 6.5 → 5.7, на второй половине `new_data` 23.9 → 17.4; полигон парно +1 / −142 из 7 600 наблюдений (39 вблизи у края габарита, 82 за 90 м). Сравнение при равной строгости с моделью, обученной на `new_data`, — EXPERIMENTS п. 17.4 |
|
||
| Вклад габарита 1.2 м и верхней секции (тогда до 3.7 м, теперь до 3.3 — п. 19) | синтетика организаторов 6 → 8 из 10; полигон парно +43 / −33, посторонних 793 → 742; ложных на незнакомой линии 14.7 → 13.8 на км, на знакомой 5.7 → 5.7; цена — ящик на 100 м (P@100 0.50 → 0.40). EXPERIMENTS п. 18.4 |
|
||
| Вклад сброса при скачке времени | вторая половина `new_data` (70 с выпавших кадров): 17.4 → 13.8 ложного трека на км; на записях без разрывов не меняет ничего. EXPERIMENTS п. 18.9 |
|
||
| Вклад штрафа за плоское у пола и прохода по верхней секции | синтетика 8 → 9 из 10, фантомов на ней 5 → 3; пустые записи 3 → 2 ложных трека (73 → 45 кадров); незнакомая линия 13.8 → 12.8 на км, вторая половина `new_data` 14.4 → 11.7; знакомая 5.7 → 5.7; полигон парно 0 / −3 (каска), посторонних 742 → 758. EXPERIMENTS п. 19 |
|
||
| Вклад пола в колее (с переобучением считывания) | лёжа P@50 0.27 → 0.50, P@150 стоя 0.39 → 0.49, ящик 0.06 → 0.26; парно +685 наблюдений против −33; ложных на знакомой линии 8.0 → 8.0, на незнакомой 21.3 → 23.5 |
|
||
| Вклад разреза по контрасту | 9.1 → 7.5 ложных трека на км *(замер до обученного считывания)*, и слепая полоса 40–90 м на `roundT_pressureGate_roundT` 0.00 → 0.45…1.00 |
|
||
|
||
Строки «вклад …» для отдельных механизмов измерены до появления обученного
|
||
считывания, и опора у них 7.4–7.5 трека на километр, а не 3.3.
|
||
|
||
**Про полигон честно.** 23.09 он пересобран: у каждого сценария теперь свой
|
||
генератор случайности, а в каталоге появился человек лёжа. При **тех же**
|
||
настройках человек стоя дал 80 м рабочей дальности и P@100 = 0.48 вместо
|
||
прежних 100 м и 0.53 — это шум полигона, а не поломка: две реализации
|
||
случайности расходятся на P@150 до 0.07. Поэтому правки теперь меряются
|
||
**парно**, на одних и тех же вставках (`tools/compare_benchmark.py`): считается,
|
||
сколько наблюдений перевернулось из «не видел» в «видел» и обратно. Разбор —
|
||
[EXPERIMENTS](docs/EXPERIMENTS.md), п. 16.1.
|
||
|
||
**Про слепые участки честно.** Размеченный полигон вскрыл то, чего не видно на одном
|
||
реальном объекте: на части участков предмет сливается со стеной по дальности, попадает
|
||
с ней в одну связную компоненту и отбрасывается вместе с ней. Человек на оси пути
|
||
обнаруживался в 100 % кадров на трёх бэгах из пяти и в 0 % на двух — при 65 лучах на
|
||
предмете, то есть не из-за видимости.
|
||
|
||
**Один из двух слепых бэгов прозрел.** На `roundT_pressureGate_roundT` — той самой
|
||
записи, где компонента из 42 000 лучей течёт вдоль стены от 4 до 99 м и уносит
|
||
предмет с собой, — полностью слепая полоса 40–90 м стала 0.45 / 1.00 / 0.57.
|
||
Помог разрез компоненты по контрасту ламины: гладкая стена даёт нулевой
|
||
центр-окружение по построению, а предмет на ней — ступеньку. Из переглубокой
|
||
компоненты выносится ровно одна, сильнейшая фигура; без этого ограничения разрез
|
||
отрезает фон, протяжённость кандидата падает с 6.9 до 0.8 м, вместе с ней пропадает
|
||
множитель компактности в весе улики, и каждое наблюдение начинает весить вдесятеро
|
||
больше. Измеренная цена одной фигуры против всех — 6.4 против 9.4 ложных трека на км
|
||
при одинаковой дальности. Второй слепой бэг, `roundT_doubleT`, за 40 м остаётся
|
||
слепым, и это разобрано покадрово: за 50 м до предмета **не доходит линия взгляда** —
|
||
98 % лучей упираются в преграду ближе него независимо от того, куда поперёк его
|
||
ставить, — а на 35–52 м мешает наш порог `split_near`, снижать который вышло
|
||
слишком дорого (7.4 → 17.1 ложного трека на км ради девяти метров). Подробно —
|
||
[EXPERIMENTS](docs/EXPERIMENTS.md), п. 9.6.
|
||
|
||
Два предыдущих подхода к тому же — разрез по допуску глубины и разрез по скорости
|
||
сближения (`split_adv`) — тоже возвращали зрение, но стоили 25.8 и 21.5 трека на
|
||
километр и остались выключенными. Разбор всех трёх — [EXPERIMENTS](docs/EXPERIMENTS.md),
|
||
п. 9.3–9.5.
|
||
|
||
**Про новый участок честно.** Обученная память на незнакомой линии бесполезна по
|
||
определению: без неё ложных треков 21.8 на километр против 9.1. Мы попробовали
|
||
закрыть это привыканием внутри проезда — гасить форму, встретившуюся в нескольких
|
||
разных точках пути, то есть штатную повторяющуюся обстановку. Механизм сделан,
|
||
доведён до работы и **отвергнут по замеру**: при ёмкости, достаточной чтобы
|
||
популяция не насыщалась, он не меняет ничего (7.5 против 7.5 ложных треков на км),
|
||
а весь видимый эффект маленькой популяции оказался глобальным глушением, которое
|
||
давит предмет сильнее обстановки (новизна вставленного предмета 0.24 при медианной
|
||
новизне кандидата 0.43…0.71). Код оставлен и выключен, полный разбор с таблицами —
|
||
[EXPERIMENTS](docs/EXPERIMENTS.md), п. 10.
|
||
|
||
Работает на новом участке то, что и работало: перенос долговременной памяти. В
|
||
дескрипторе намеренно смешаны признаки формы и углового размера (переносятся на
|
||
любой тоннель) с положением в сечении (запоминает конкретную обстановку), и первая
|
||
половина снижает ложные тревоги с 21.8 до 9.1 трека на километр на бэге, которого
|
||
память не видела.
|
||
|
||
**Про обученное считывание честно.** Метки для него сделаны вставкой предметов
|
||
трассировкой лучей, и первые две модели сенсора оказались неверными: сначала
|
||
яркость вставки считалась по ламбертовой ρ·cosθ/r², и за 110 м предмет выходил
|
||
тусклее тоннеля, потом — постоянной, и он стал ярче тоннеля впятеро. В обоих
|
||
случаях модель училась узнавать вставку по яркости, а не по форме, и полигонная
|
||
дальность была завышена. Абсолютной шкалы интенсивности в этих записях нет вовсе:
|
||
медиана по кандидатам обстановки 3…7 в пяти бэгах и 23.5 в шестом. Сейчас вставка
|
||
берёт яркость реальных возвратов с тех же лучей, признак стал неинформативным, и
|
||
все цифры выше получены уже так. Заявленные до этого разбора 100 м рабочей
|
||
дальности и P@100 = 0.53 были получены на полигоне с артефактом: после починки
|
||
модели сенсора те же замеры дали 80 м и 0.31. Нынешние 100 м и 0.53 —
|
||
совпадение по величине, но получены они уже на честном полигоне и другими
|
||
средствами (гашение знакомости и порог по лучам, зависящий от дальности).
|
||
Разбор с таблицами — [EXPERIMENTS](docs/EXPERIMENTS.md), п. 11.
|
||
|
||
**Про дальность и ложные честно.** Улика далёкого предмета — произведение
|
||
нескольких множителей, и один из них на дальности оказался перевёрнутым:
|
||
у вставленного человека на 120–185 м новизна 0.150 против 0.199 у окружающей
|
||
обстановки (AUC 0.293). На шести лучах дескриптор вырождается, и память
|
||
тоннеля узнаёт в предмете любую далёкую конструкцию — то есть мы сами гасили
|
||
свой сигнал. После гашения вклада новизны за 90 м обнаружение на 150 м
|
||
выросло с нуля до 0.33, а на 100 м с 0.31 до 0.53. Вместе с порогом по числу
|
||
лучей, зависящим от дальности (четыре вблизи, три за 90 м), рабочая дальность
|
||
по человеку выросла с 80 до 100 м. Цена — ложные тревоги на **знакомой**
|
||
линии 3.5 → 8.0 трека на километр; на незнакомой плата нулевая, потому что
|
||
там подавлять нечем. Кому дороже тишина, тот ставит `nov_fade_from: 0` и
|
||
получает 3.5 на километр, теряя дальнюю зону. Лишние далёкие треки дают предупреждения, а
|
||
не торможение: экстренный уровень требует близкой дистанции. Разбор —
|
||
[EXPERIMENTS](docs/EXPERIMENTS.md), п. 12.
|
||
|
||
**Про решение по треку честно.** Замышлялось обученное считывание по истории
|
||
трека — и оно **не обогнало ни один из признаков, которые ему же и дали**
|
||
(AUC 0.880 против 0.895 у одного среднего отсчёта). Зато по дороге нашлось, что
|
||
улика насыщается: на настоящем объекте она 1.000 и у предмета, и у ложных
|
||
треков (AUC 0.624), а средний вес наблюдения, из которого она складывается, —
|
||
0.998 против 0.269. Смешивать его с уликой оказалось бесполезно: при равном
|
||
числе ложных тревог простой порог даёт обнаружение не хуже, а на незнакомой
|
||
линии смешивание вытаскивает лишние треки (20.3 → 24.0 на км). В итоге приняли
|
||
не модель, а ключ, **отвергнутый тремя разделами раньше**, — порог тревоги,
|
||
опускаемый с 0.5 до 0.3 за 90 м. Тогда он ничего не давал, потому что улика
|
||
далёкого трека была нулём; после гашения знакомости она им быть перестала, и
|
||
тот же ключ поднял P@150 с 0.33 до 0.37. Отрицательный результат верен только
|
||
для конфигурации, в которой получен. Разбор — [EXPERIMENTS](docs/EXPERIMENTS.md),
|
||
п. 15.
|
||
|
||
**Про упавшего на пути человека честно.** Самый важный для метро случай был
|
||
виден хуже всех крупных предметов: при высоте 0.30 м и поле габарита 0.28 м в
|
||
габарит попадала верхушка в два сантиметра (P@50 = 0.27, на 100 м — ноль). Пол
|
||
между рельсами опущен до 0.16 м (предложение Zhirik1337), но не везде: без
|
||
порога по дальности обнаружение **вблизи падало вдвое** — в полосу 0.16…0.28 м
|
||
попадают головки рельсов, рельс собирается в одну компоненту от самой кабины, и
|
||
предмет выбрасывается вместе с ней. Дальше 30 м вреда нет, и после
|
||
переобучения считывания на кандидатах нового пола лежачий человек P@50 = 0.50,
|
||
рабочая дальность 32 м; ложных на знакомой линии столько же, на незнакомой
|
||
+2.2 на км. Ещё четыре правки из того же набора замерены и отвергнуты: каждая
|
||
давала меньше, чем стоила. Разбор — [EXPERIMENTS](docs/EXPERIMENTS.md), п. 16.
|
||
|
||
**Про ось пути честно.** Ось берётся не из рельсов, а из дрейфа центра сечения
|
||
тоннеля с дальностью: на 100 м рельсы дают единицы точек, а свод — тысячи.
|
||
Поэтому ось не дрожит: сдвиг между соседними кадрами в одной точке пути на
|
||
100 м — p90 не хуже 0.35 м при полуширине габарита 1.6 м. Но наблюдается
|
||
сечение только до 62–107 м, дальше ось продолжается по касательной, и
|
||
касательная расходится с кривой до 1.19 м на 150 м. Продолжать вместо неё
|
||
измеренную кривизну пробовали — хуже: кривизна оценивается на коротком плече,
|
||
гуляет между кадрами и даёт до 6 м расхождения.
|
||
|
||
**Про край габарита честно.** На синтетике организаторов граница между «у края, внутри» и
|
||
«вне, но близко» — сантиметры: 1.13 и 1.14 м от оси. Ошибка нашей оси на 40–80 м —
|
||
0.2–0.4 м, проверено по их предметам (EXPERIMENTS п. 20.1). Поэтому ящик 2×2, заходящий в
|
||
габарит на 0.06–0.28 м, мы не берём: по форме и перепаду дальности он неотличим от
|
||
настоящих конструкций у края, которые из-за той же ошибки оси оказываются у нас на
|
||
1.0–1.1 м. Правило, которое его бы нашло, на записях без препятствий срабатывает в 16
|
||
местах, одно держится 451 кадр (п. 20.3).
|
||
|
||
Из этого следует оговорка к дальним цифрам полигона. Предмет вставляется на
|
||
**нашу же** оценку оси, поэтому ошибку оси полигон не мерит в принципе. В
|
||
эксплуатации предмет на 150 м может оказаться в метре от того места, где мы
|
||
считаем путь, и у края габарита из него выпасть. Дальние цифры оптимистичны
|
||
именно по этой причине, а не из-за обнаружения. Разбор —
|
||
[EXPERIMENTS](docs/EXPERIMENTS.md), п. 13.
|
||
|
||
**Про дальность честно.** Паспортный максимум Pandar128 — 200 м, максимальное эхо в датасете —
|
||
208.8 м. Но реальная **прямая видимость в этих тоннелях 121–167 м**: тоннели кривые (радиусы
|
||
1300–8700 м), и дальше линия взгляда упирается в стену. Заявленные в ТЗ «300 м → отлично» на
|
||
предоставленных участках физически недостижимы никаким алгоритмом.
|
||
|
||
Что мешает дотянуться до 200 м там, где видимость позволяет, тоже измерено и оказалось не
|
||
тем, чего ожидаешь. Человек на 170 м освещён **в каждом кадре** (5 лучей) и за проход набирает
|
||
около 84 попаданий в одну точку мира; кандидат формируется, размер определяется верно. Не
|
||
растёт улика: кольцо окружения ламины на 170 м упирается в стену тоннеля, которая там же, и
|
||
локальный контраст обнуляется. Лечится не порогом, а накоплением лучей в координатах пути и
|
||
геометрической картой линии — разбор в [docs/EXPERIMENTS.md](docs/EXPERIMENTS.md), п. 7.3.
|
||
|
||
**Про стекло честно.** Лидар светит на 905 нм, и прозрачное стекло для него почти
|
||
прозрачно: луч уходит насквозь и возвращается от того, что за стеклом, а гладкая
|
||
поверхность отражает зеркально — в сторону, а не назад к прибору. Стеклянный предмет
|
||
виден только тем, что в нём не прозрачно: этикеткой, пробкой, рамой, грязью, бликом
|
||
там, где луч падает на поверхность почти по нормали. Отдельного приёма для стекла у нас
|
||
нет, и честно его не сделать: из пустоты предмет не восстановить. Для масштаба — даже
|
||
непрозрачная бутылка (0.03 м²) на полигоне почти не видна: на 40–55 м на ней 3 луча, а
|
||
ниже примерно десяти лучей предмет не отличить от шума (EXPERIMENTS п. 9).
|
||
|
||
---
|
||
|
||
## Разработка без ROS
|
||
|
||
Весь конвейер работает и офлайн, прямо по `.db3`, без установленного ROS — это удобно для
|
||
экспериментов на Windows и для воспроизведения метрик:
|
||
|
||
```bash
|
||
python tools/inspect_bags.py --root data/for_hackathon # калибровка решётки
|
||
python tools/run_pipeline.py --all --memory artifacts/mushroom_body.npz
|
||
python tools/evaluate.py --device cuda # leave-one-bag-out
|
||
python tools/make_benchmark.py --memory artifacts/mushroom_body.npz
|
||
python tools/render_brain.py --bag data/for_hackathon/doubleT_obstacle --video brain.mp4
|
||
```
|
||
|
||
`evaluate.py`, `make_benchmark.py` и `make_training_set.py` раскладывают работу по
|
||
бэгам на процессы (`--jobs`, по умолчанию — по числу записей, но не больше
|
||
физических ядер). Результат совпадает с последовательным побайтово: случайность
|
||
у каждой записи своя, общей изменяемой памяти между ними нет. Задержку кадра при
|
||
этом мерить нельзя — нужен `--jobs 1`.
|
||
|
||
Обучение памяти тоннеля (без единой метки, на пустых проездах):
|
||
|
||
```bash
|
||
python tools/train_mushroom_body.py --device cuda \
|
||
--extra-cache data/cache/new_data_candidates.npz
|
||
```
|
||
|
||
---
|
||
|
||
## Состав репозитория
|
||
|
||
```
|
||
ros2_ws/src/flyguard/ ROS 2-пакет: конвейер, узел, launch, конфиги, RViz, вид мозга
|
||
ros2_ws/src/flyguard_msgs/ сообщения ObstacleStatus и DetectedObject
|
||
docker/ Dockerfile образа, точка входа, проверки в контейнере
|
||
flyguard/ то же ядро, что в ROS-пакете, — для инструментов и тестов без ROS
|
||
tools/ офлайн-инструменты: калибровка, обучение, метрики, полигон
|
||
tests/ тесты без ROS: pytest tests
|
||
docs/ архитектура, алгоритм, эксперименты, коннектом
|
||
artifacts/ обученная память тоннеля и результаты замеров
|
||
```
|
||
|
||
Ядро в `flyguard/` и в `ros2_ws/src/flyguard/flyguard/` — один и тот же код из одного
|
||
источника. Корневой `Dockerfile` и `docker/Dockerfile` собирают один и тот же образ. В корне
|
||
лежат ещё `Dockerfile.offline`, `Dockerfile.gpu` и `docker-compose.yml` для прогона ядра по
|
||
записям без ROS и `Dockerfile.ros2` с узлом `flyguard/flyguard_ros2_node.py`.
|
||
|
||
## Лицензия
|
||
|
||
MIT.
|