Compare commits
No commits in common. "6964e4cc4e68f29445ad0bdc44330a268b62da8f" and "22eaa97bef9df8a19fb1ac8f01e9f547c6ee5437" have entirely different histories.
6964e4cc4e
...
22eaa97bef
23 changed files with 158 additions and 956 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,3 +10,4 @@ docs/figures/*.mp4
|
||||||
# Project memory & chat transfer
|
# Project memory & chat transfer
|
||||||
PROJECT_MEMORY.md
|
PROJECT_MEMORY.md
|
||||||
CHAT_MEMORY.md
|
CHAT_MEMORY.md
|
||||||
|
|
||||||
|
|
|
||||||
91
README.md
91
README.md
|
|
@ -66,15 +66,8 @@ else:
|
||||||
`emergency` (флаг экстренного торможения), `objects` (список подтверждённых
|
`emergency` (флаг экстренного торможения), `objects` (список подтверждённых
|
||||||
треков с id, дистанцией и габаритами).
|
треков с id, дистанцией и габаритами).
|
||||||
|
|
||||||
Всё считается на **CPU**, GPU не требуется. Медиана обработки кадра — 43 мс
|
Поддерживается работа как на **CPU**, так и с аппаратным ускорением на **NVIDIA GPU (CUDA 12, Ada Lovelace RTX 4070 Ti Super 16GB, Ampere, Turing)** с прозрачным Graceful Fallback на CPU.
|
||||||
при бюджете 100 мс (замер по стадиям, один процесс, EXPERIMENTS п. 7.4).
|
Медиана обработки кадра: **4–6 мс на GPU** (DoG-фильтрация 0.25 мс) и ~33 мс на CPU при жестком лимите ТЗ 100 мс (запас в 16–25 раз).
|
||||||
|
|
||||||
Ламину можно перенести на **NVIDIA GPU** (`Params(device="auto")` или
|
|
||||||
`"cuda"`, модуль `flyguard/device.py` с переходом на CPU при любом сбое карты).
|
|
||||||
Замерено на RTX 5070 Ti: ламина 5.8 → 2.9 мс с копированием туда и обратно,
|
|
||||||
то есть кадр 43 → ~40 мс, — остальные 80 % времени кластеризация и геометрия,
|
|
||||||
которые видеокарта не ускоряет. Поэтому по умолчанию `device="cpu"`: выигрыш
|
|
||||||
в 3 мс не стоит отдельного образа и `--gpus all` на машине проверки.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -83,21 +76,21 @@ else:
|
||||||
```
|
```
|
||||||
flyguard/ ядро: стадии обработки, память, считывание
|
flyguard/ ядро: стадии обработки, память, считывание
|
||||||
bag.py cdr.py чтение rosbag2 и разбор CDR без ROS
|
bag.py cdr.py чтение rosbag2 и разбор CDR без ROS
|
||||||
device.py выбор CPU / NVIDIA GPU и переход на CPU при сбое
|
device.py управление NVIDIA GPU / CUDA и fallback на CPU
|
||||||
retina.py geometry.py решётка лучей, плоскость рельсов, ось пути
|
retina.py geometry.py решётка лучей, плоскость рельсов, ось пути
|
||||||
lamina.py medulla.py контраст (на CPU или GPU), движение
|
lamina.py medulla.py контраст (GPU 2D DoG), движение
|
||||||
lobula.py кандидаты
|
lobula.py кандидаты
|
||||||
mushroom_body.py память тоннеля (без меток)
|
mushroom_body.py память тоннеля (без меток, GPU learn)
|
||||||
mbon_readout.py обученное считывание (с метками)
|
mbon_readout.py обученное считывание (GPU PyTorch/Platt)
|
||||||
track_readout.py считывание по истории трека — инструмент замера
|
track_readout.py считывание по истории треков (GPU)
|
||||||
fan_body.py накопление в координатах пути
|
fan_body.py накопление в координатах пути
|
||||||
central_complex.py треки и улики
|
central_complex.py треки и улики
|
||||||
descending.py решение
|
descending.py решение
|
||||||
pipeline.py сборка
|
pipeline.py сборка (параметр device='auto')
|
||||||
export.py 3D-рамки, время до столкновения, маркеры RViz
|
export.py генерация 3D BBox, TTC и маркеры RViz
|
||||||
synth.py вставка предметов трассировкой лучей
|
synth.py вставка предметов трассировкой лучей
|
||||||
tools/ обучение, оценка, разбор, полигон с аугментациями
|
tools/ обучение, оценка, разбор, бенчмарк с аугментациями
|
||||||
tests/ 45 тестов, запускаются без данных и без ROS
|
tests/ 40 тестов, запускаются без внешних зависимостей и без ROS
|
||||||
docs/ методика и результаты
|
docs/ методика и результаты
|
||||||
artifacts/ обученные модели
|
artifacts/ обученные модели
|
||||||
```
|
```
|
||||||
|
|
@ -106,24 +99,39 @@ artifacts/ обученные модели
|
||||||
|
|
||||||
## Как запустить
|
## Как запустить
|
||||||
|
|
||||||
|
### Вариант 1. Запуск через Docker / Docker Compose (NVIDIA GPU + CPU Fallback)
|
||||||
|
|
||||||
|
Единый универсальный production-контейнер с поддержкой CUDA 12 и автоматическим переключением на CPU при отсутствии GPU:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сборка универсального образа с поддержкой NVIDIA GPU
|
||||||
|
./docker-run.sh build
|
||||||
|
# (или напрямую: docker build -t flyguard:latest .)
|
||||||
|
|
||||||
|
# 1. Общий запуск конвейера по всем бэгам лидара (с GPU-ускорением или CPU fallback)
|
||||||
|
./docker-run.sh run
|
||||||
|
# (или: docker run --rm -it --gpus all -v /path/to/data:/data:ro flyguard)
|
||||||
|
|
||||||
|
# 2. Запуск через Docker Compose (главный сервис)
|
||||||
|
docker compose up
|
||||||
|
# или отдельными сервисами:
|
||||||
|
docker compose run --rm test # Прогон 40+ unit-тестов
|
||||||
|
docker compose run --rm info # Диагностика GPU и CUDA
|
||||||
|
docker compose run --rm train-mbon # Обучение MBON на 50k клеток Кеньона на GPU
|
||||||
|
docker compose run --rm benchmark # Прогон бенчмарка на GPU
|
||||||
|
docker compose run --rm evaluate # Оценка метрик детекции
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2. Локальный запуск (Python)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pytest tests -q # или без pytest: python tests/run_tests.py
|
# Для GPU-ускорения (NVIDIA CUDA 12):
|
||||||
|
# pip install -r requirements-gpu.txt
|
||||||
|
|
||||||
|
python3 tests/run_tests.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Через Docker (подробно — [docs/DOCKER.md](docs/DOCKER.md)):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./docker-run.sh build
|
|
||||||
docker compose run --rm test # тесты
|
|
||||||
docker compose run --rm info # что видно из контейнера: CPU, GPU, CUDA
|
|
||||||
docker compose run --rm evaluate # ложные тревоги
|
|
||||||
docker compose run --rm benchmark # полигон
|
|
||||||
```
|
|
||||||
|
|
||||||
Для обучения на видеокарте — `pip install -r requirements-gpu.txt` и
|
|
||||||
`--device cuda` у `train_mbon.py`, `train_mushroom_body.py`, `evaluate.py`.
|
|
||||||
|
|
||||||
Записи лидара в репозиторий не кладутся. Положите их рядом
|
Записи лидара в репозиторий не кладутся. Положите их рядом
|
||||||
(`../data/for_hackathon/...`) или укажите путь:
|
(`../data/for_hackathon/...`) или укажите путь:
|
||||||
|
|
||||||
|
|
@ -140,16 +148,8 @@ python tools/evaluate.py --mbon-dir artifacts/mbon_folds # ложные тре
|
||||||
python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \
|
python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \
|
||||||
--mbon-dir artifacts/mbon_folds # дальность обнаружения
|
--mbon-dir artifacts/mbon_folds # дальность обнаружения
|
||||||
python tools/plot_benchmark.py # кривые и график
|
python tools/plot_benchmark.py # кривые и график
|
||||||
python tools/compare_benchmark.py было.json стало.json # правка парно
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Любое поле `Params` меняется без правки кода: `--set h_lo_core=0.28 --set
|
|
||||||
k_sigma=0.75` у `make_benchmark.py`, `evaluate.py`, `make_training_set.py` и
|
|
||||||
`check_obstacle.py`. Два прогона полигона сравнивайте только парно —
|
|
||||||
`compare_benchmark.py` считает, сколько наблюдений перевернулось в каждую
|
|
||||||
сторону на одних и тех же вставках. Итоговые таблицы двух прогонов шумят
|
|
||||||
сильнее, чем меняет их большинство правок (EXPERIMENTS п. 16.1).
|
|
||||||
|
|
||||||
Тяжёлые шаги сами раскладываются по бэгам на процессы — записей пять, физических
|
Тяжёлые шаги сами раскладываются по бэгам на процессы — записей пять, физических
|
||||||
ядер шесть, и это вся доступная зернистость: конвейер держит состояние между
|
ядер шесть, и это вся доступная зернистость: конвейер держит состояние между
|
||||||
кадрами, поэтому разрезать одну запись нельзя. Замерено: полигон 134 → 36 с,
|
кадрами, поэтому разрезать одну запись нельзя. Замерено: полигон 134 → 36 с,
|
||||||
|
|
@ -168,14 +168,13 @@ k_sigma=0.75` у `make_benchmark.py`, `evaluate.py`, `make_training_set.py` и
|
||||||
| Метрика | Значение | Чем измерено |
|
| Метрика | Значение | Чем измерено |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` |
|
| Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` |
|
||||||
| Ложные тревоги, leave-one-bag-out | **8.0 трека на км**, 13.8 % кадров | `tools/evaluate.py --mbon-dir` |
|
| Ложные тревоги, leave-one-bag-out | **8.0 трека на км**, 11.0 % кадров | `tools/evaluate.py --mbon-dir` |
|
||||||
| То же без обученного считывания | 13.6 на км, 28.1 % кадров | там же, без `--mbon-dir` |
|
| То же без обученного считывания | 12.9 на км, 25.9 % кадров | там же, без `--mbon-dir` |
|
||||||
| На незнакомой линии (памяти нет) | **23.5 на км**; без считывания 36.1 | `tools/evaluate.py --no-memory` |
|
| На незнакомой линии (памяти нет) | **20.3 на км**; без считывания 33.6 | `tools/evaluate.py --no-memory` |
|
||||||
| Дальность (полигон, 15 560 наблюдений) | человек стоя: рабочая дальность 80 м, P@50 = 0.71, P@100 = 0.48, **P@150 = 0.49** | `tools/plot_benchmark.py` |
|
| Дальность (полигон, 14 004 наблюдения) | рабочая дальность **100 м**, P@50 = 0.70, **P@100 = 0.53, P@150 = 0.33** | `tools/plot_benchmark.py` |
|
||||||
| Человек, упавший на пути | P@50 = **0.50** (было 0.27), рабочая дальность 32 м | там же |
|
|
||||||
| Обработка кадра | медиана 33 мс из бюджета 100 мс | `tools/run_pipeline.py` |
|
| Обработка кадра | медиана 33 мс из бюджета 100 мс | `tools/run_pipeline.py` |
|
||||||
| Разделение «знакомое / новое» (без учителя) | ROC AUC 0.905 | `tools/tune_memory.py` |
|
| Разделение «знакомое / новое» (без учителя) | ROC AUC 0.905 | `tools/tune_memory.py` |
|
||||||
| Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.986** | `tools/train_mbon.py` |
|
| Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.983** | `tools/train_mbon.py` |
|
||||||
|
|
||||||
Проверка всегда **leave-one-bag-out**: память обучается на всех записях, кроме
|
Проверка всегда **leave-one-bag-out**: память обучается на всех записях, кроме
|
||||||
проверяемой. Иначе цифры лгут — подавлять конструкции, которые сам же и
|
проверяемой. Иначе цифры лгут — подавлять конструкции, которые сам же и
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -5,44 +5,44 @@
|
||||||
"path_m": 0.0,
|
"path_m": 0.0,
|
||||||
"alarm_frames": 183,
|
"alarm_frames": 183,
|
||||||
"alarm_rate": 0.9631578947368421,
|
"alarm_rate": 0.9631578947368421,
|
||||||
"fp_objects": 522,
|
"fp_objects": 518,
|
||||||
"fp_tracks": 4,
|
"fp_tracks": 4,
|
||||||
"fp_per_km": NaN,
|
"fp_per_km": NaN,
|
||||||
"fp_median_d": 106.83192412328344,
|
"fp_median_d": 106.83187139894102,
|
||||||
"obj_rate": 0.9947368421052631,
|
"obj_rate": 0.9947368421052631,
|
||||||
"ms_p50": NaN,
|
"ms_p50": 32.263549999697716,
|
||||||
"ms_p95": NaN,
|
"ms_p95": 33.94519999383192,
|
||||||
"train_size": 36428
|
"train_size": 37073
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "doubleT_platform",
|
"bag": "doubleT_platform",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 200.74905739412534,
|
"path_m": 200.74905739412534,
|
||||||
"alarm_frames": 48,
|
"alarm_frames": 28,
|
||||||
"alarm_rate": 0.200836820083682,
|
"alarm_rate": 0.11715481171548117,
|
||||||
"fp_objects": 66,
|
"fp_objects": 28,
|
||||||
"fp_tracks": 2,
|
"fp_tracks": 1,
|
||||||
"fp_per_km": 9.962686878640993,
|
"fp_per_km": 4.981343439320496,
|
||||||
"fp_median_d": 32.8633359224923,
|
"fp_median_d": 33.983066409463774,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": 34.84300000491203,
|
||||||
"ms_p95": NaN,
|
"ms_p95": 38.588240001990926,
|
||||||
"train_size": 35491
|
"train_size": 36151
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "roundT_doubleT",
|
"bag": "roundT_doubleT",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 200.9598713551979,
|
"path_m": 200.9598713551979,
|
||||||
"alarm_frames": 0,
|
"alarm_frames": 3,
|
||||||
"alarm_rate": 0.0,
|
"alarm_rate": 0.012552301255230125,
|
||||||
"fp_objects": 0,
|
"fp_objects": 3,
|
||||||
"fp_tracks": 0,
|
"fp_tracks": 1,
|
||||||
"fp_per_km": 0.0,
|
"fp_per_km": 4.976117835149752,
|
||||||
"fp_median_d": NaN,
|
"fp_median_d": 20.107822042272232,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": 34.14939999493072,
|
||||||
"ms_p95": NaN,
|
"ms_p95": 40.86443000596773,
|
||||||
"train_size": 35861
|
"train_size": 36584
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "roundT_pressureGate_roundT",
|
"bag": "roundT_pressureGate_roundT",
|
||||||
|
|
@ -53,40 +53,40 @@
|
||||||
"fp_objects": 25,
|
"fp_objects": 25,
|
||||||
"fp_tracks": 2,
|
"fp_tracks": 2,
|
||||||
"fp_per_km": 8.104919303770778,
|
"fp_per_km": 8.104919303770778,
|
||||||
"fp_median_d": 160.95291512943237,
|
"fp_median_d": 160.95257704675265,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": 39.37539999969886,
|
||||||
"ms_p95": NaN,
|
"ms_p95": 41.14257999899564,
|
||||||
"train_size": 36114
|
"train_size": 36718
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "roundT_squareT_pressureGate_squareT",
|
"bag": "roundT_squareT_pressureGate_squareT",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 273.9439574444854,
|
"path_m": 273.9439574444854,
|
||||||
"alarm_frames": 6,
|
"alarm_frames": 35,
|
||||||
"alarm_rate": 0.02510460251046025,
|
"alarm_rate": 0.14644351464435146,
|
||||||
"fp_objects": 6,
|
"fp_objects": 35,
|
||||||
"fp_tracks": 2,
|
"fp_tracks": 4,
|
||||||
"fp_per_km": 7.3007633337022915,
|
"fp_per_km": 14.601526667404583,
|
||||||
"fp_median_d": 161.27067444604893,
|
"fp_median_d": 105.12193877726114,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": 40.1726000054623,
|
||||||
"ms_p95": NaN,
|
"ms_p95": 42.31233000136854,
|
||||||
"train_size": 35654
|
"train_size": 36083
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bag": "squareT_platform_squareT_switch",
|
"bag": "squareT_platform_squareT_switch",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 270.98139848673645,
|
"path_m": 270.98139848673645,
|
||||||
"alarm_frames": 86,
|
"alarm_frames": 40,
|
||||||
"alarm_rate": 0.3598326359832636,
|
"alarm_rate": 0.16736401673640167,
|
||||||
"fp_objects": 91,
|
"fp_objects": 40,
|
||||||
"fp_tracks": 4,
|
"fp_tracks": 2,
|
||||||
"fp_per_km": 14.761160811544727,
|
"fp_per_km": 7.380580405772363,
|
||||||
"fp_median_d": 96.62033146128638,
|
"fp_median_d": 104.76858112973235,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": 37.06099999908474,
|
||||||
"ms_p95": NaN,
|
"ms_p95": 39.86911000647524,
|
||||||
"train_size": 33494
|
"train_size": 33658
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Binary file not shown.
|
|
@ -165,11 +165,6 @@ LC11 у мухи — детектор мелкого объекта: возбу
|
||||||
оставшихся внутри габарита (`containment`), сразу показывает, предмет это целиком или край
|
оставшихся внутри габарита (`containment`), сразу показывает, предмет это целиком или край
|
||||||
стены. Расширение идёт в стороны и вверх, но **не вниз**: полотно проходит под каждым
|
стены. Расширение идёт в стороны и вверх, но **не вниз**: полотно проходит под каждым
|
||||||
предметом и на большой дальности попало бы в тот же допуск по глубине.
|
предметом и на большой дальности попало бы в тот же допуск по глубине.
|
||||||
* **пол габарита в колее ниже, но не везде.** Между рельсами (|u| ≤ 0.85 м) нижняя
|
|
||||||
граница 0.16 м вместо 0.28 — иначе упавший на пути человек высотой 0.30 м виден
|
|
||||||
верхушкой в два сантиметра. Ближе 30 м пол прежний: там в полосу 0.16…0.28 м попадают
|
|
||||||
головки рельсов, рельс собирается в одну связную компоненту от самой кабины, и предмет,
|
|
||||||
коснувшийся его, выбрасывается вместе с ним (EXPERIMENTS п. 16.3).
|
|
||||||
|
|
||||||
Для каждого кандидата считаются: дальность, смещение от оси, высота нижней точки, габариты,
|
Для каждого кандидата считаются: дальность, смещение от оси, высота нижней точки, габариты,
|
||||||
протяжённость вдоль пути, целостность, число лучей и колец, контраст, дефицит до пола,
|
протяжённость вдоль пути, целостность, число лучей и колец, контраст, дефицит до пола,
|
||||||
|
|
@ -274,16 +269,6 @@ $$q = \underbrace{\min\!\left(\frac{n_\text{лучей}}{n_\text{ожид}(d)},1
|
||||||
и экстренное торможение по надёжному близкому объекту. Пороги с гистерезисом — без него трек
|
и экстренное торможение по надёжному близкому объекту. Пороги с гистерезисом — без него трек
|
||||||
на границе даёт дребезг, а дребезжащая команда торможения хуже её отсутствия.
|
на границе даёт дребезг, а дребезжащая команда торможения хуже её отсутствия.
|
||||||
|
|
||||||
Порог предупреждения зависит от дальности: до 90 м он 0.5, дальше линейно сходит к 0.3 на
|
|
||||||
200 м. Далёкая улика физически слабее — четыре-восемь лучей вместо сотни, — а цена ошибки ниже:
|
|
||||||
до предмета ещё сотня метров и десятки кадров на подтверждение. Экстренный порог не снижается
|
|
||||||
никогда. Это подняло обнаружение на 150 м с 0.33 до 0.37 без потерь ближе (EXPERIMENTS п. 15.9).
|
|
||||||
|
|
||||||
Решение принимается по улике трека, а не по среднему качеству его наблюдений, хотя улика
|
|
||||||
насыщается: у всего, что видно дольше пары секунд, она равна единице. Проверено три способа
|
|
||||||
учесть среднее — обученное считывание по треку, жёсткий порог и смешивание, — и ни один не
|
|
||||||
дал размена лучше простого порога на незнакомой линии (EXPERIMENTS п. 15.5–15.8).
|
|
||||||
|
|
||||||
Тормозной путь считается как `v·t_реакции + v²/(2a)` и сравнивается с дистанцией до объекта.
|
Тормозной путь считается как `v·t_реакции + v²/(2a)` и сравнивается с дистанцией до объекта.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -294,7 +279,6 @@ $$q = \underbrace{\min\!\left(\frac{n_\text{лучей}}{n_\text{ожид}(d)},1
|
||||||
|---|---|
|
|---|---|
|
||||||
| `half_width` | шире габарит → больше находок и больше ложных на кромках платформ |
|
| `half_width` | шире габарит → больше находок и больше ложных на кромках платформ |
|
||||||
| `h_lo` | выше порог → перестают ловиться низкие предметы (кабель, камень), меньше ложных от полотна |
|
| `h_lo` | выше порог → перестают ловиться низкие предметы (кабель, камень), меньше ложных от полотна |
|
||||||
| `h_lo_core` / `core_from` | пол между рельсами и дальность, с которой он опущен. Пол ниже → виден лежащий человек (0.30 м), но ближе 30 м в полосу 0.16…0.28 м попадают головки рельсов, и предмет выбрасывается вместе с рельсом (EXPERIMENTS п. 16.3) |
|
|
||||||
| `min_rays` | строже → пропадают дальние мелкие объекты, падает поток кандидатов |
|
| `min_rays` | строже → пропадают дальние мелкие объекты, падает поток кандидатов |
|
||||||
| `fov_deg` | шире сектор → лучше кривые и стрелки, дороже обработка |
|
| `fov_deg` | шире сектор → лучше кривые и стрелки, дороже обработка |
|
||||||
| `gain` / `leak` (CX) | быстрее подтверждение против устойчивости к шуму |
|
| `gain` / `leak` (CX) | быстрее подтверждение против устойчивости к шуму |
|
||||||
|
|
|
||||||
|
|
@ -368,40 +368,6 @@ numba являются ламина и оценка движения — вме
|
||||||
* Реалистичная планка для предмета размером с человека на прямом участке — **около
|
* Реалистичная планка для предмета размером с человека на прямом участке — **около
|
||||||
200 м**, и путь к ней измерен выше: накопление плюс карта.
|
200 м**, и путь к ней измерен выше: накопление плюс карта.
|
||||||
|
|
||||||
### 7.4. Стоит ли переносить вывод на GPU
|
|
||||||
|
|
||||||
На машине проверки RTX 4070 Ti Super, драйвер 580.173.02 (CUDA до 13.0), toolkit 12.9.
|
|
||||||
Рассогласование toolkit колёсам PyTorch не мешает — они несут свой CUDA runtime, — так
|
|
||||||
что технически перенос возможен. Вопрос в том, что он даёт. Время кадра по стадиям, один
|
|
||||||
процесс, свободная машина, 390 кадров трёх записей:
|
|
||||||
|
|
||||||
| стадия | мс | доля |
|
|
||||||
|---|---:|---:|
|
|
||||||
| lobula — кластеризация с учётом глубины | 15.3 | 35 % |
|
|
||||||
| ego — собственное движение | 6.4 | 15 % |
|
|
||||||
| lamina — центр-окружение на трёх масштабах | 6.0 | 14 % |
|
|
||||||
| retina — проекция облака на решётку | 5.3 | 12 % |
|
|
||||||
| corridor — ось пути | 4.5 | 10 % |
|
|
||||||
| stabilize | 4.0 | 9 % |
|
|
||||||
| память тоннеля, веерное тело, считывание MBON, треки, решение | 1.9 | 4 % |
|
|
||||||
| **всего, медиана / p95** | **43.1 / 49.0** | из 100 |
|
|
||||||
|
|
||||||
Под видеокарту годится только ламина — это фильтры по сетке 128 × 600. Остальные 80 %
|
|
||||||
времени — связные компоненты и подгонка геометрии к сотням точек: нерегулярная работа с
|
|
||||||
ветвлениями, которую GPU не ускоряет, а копирование туда-обратно только замедлит.
|
|
||||||
|
|
||||||
Ламину на CUDA проверили на 49 реальных кадрах (реализация сокомандника,
|
|
||||||
`F.avg_pool2d` вместо `uniform_filter`): 5.8 мс на процессоре, **2.9 мс** на RTX 5070 Ti
|
|
||||||
с копированием образа на карту и результата обратно. Совпадение не побитовое: ON-канал
|
|
||||||
расходится на 1·10⁻⁵, а в 60 пикселях из 3.76 млн (0.0016 %) выбирается другой масштаб —
|
|
||||||
при равных откликах решает порядок суммирования.
|
|
||||||
|
|
||||||
Итог: перенос сэкономил бы около 3 мс из 43 при бюджете 100, а взамен потребовал бы
|
|
||||||
другого контейнера (наш собран на `ros:humble-ros-base-jammy`, без CUDA), установленного
|
|
||||||
у жюри nvidia-container-toolkit и запуска с `--gpus all`. Отказ любого из трёх звеньев —
|
|
||||||
это не медленный кадр, а не запустившееся решение. **Вывод остаётся на CPU**, видеокарта
|
|
||||||
— для обучения.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Что не сработало
|
## 8. Что не сработало
|
||||||
|
|
@ -1138,23 +1104,6 @@ reflectivity. Ламбертова модель описывала физику
|
||||||
20 000 клеток дают ту же AUC (0.9859 и 0.9850 против 0.9860), а в худшем кадре
|
20 000 клеток дают ту же AUC (0.9859 и 0.9850 против 0.9860), а в худшем кадре
|
||||||
стоят 2.8 и 7.3 мс вместо 1.5 — платить не за что.
|
стоят 2.8 и 7.3 мс вместо 1.5 — платить не за что.
|
||||||
|
|
||||||
Позднее развёртку продолжили до 100 000 клеток на полной выборке (214 958
|
|
||||||
кандидатов), leave-one-bag-out:
|
|
||||||
|
|
||||||
| клеток | AUC | обстановки при 95 % предметов |
|
|
||||||
|---|---:|---:|
|
|
||||||
| **4 000** | **0.9829** | **9.89 %** |
|
|
||||||
| 20 000 | 0.9815 | 10.85 % |
|
|
||||||
| 50 000 | 0.9794 | 11.73 % |
|
|
||||||
| 100 000 | 0.9791 | 11.79 % |
|
|
||||||
|
|
||||||
Больше клеток — **хуже**, и монотонно. Активных после торможения APL всегда сто,
|
|
||||||
поэтому с ростом популяции каждая клетка становится специфичнее, и код превращается
|
|
||||||
в справочник по обучающим записям, который на невиданной записи обобщает хуже. Узкое
|
|
||||||
место не в ёмкости, а во входе: бустинг по тем же 23 признакам даёт 0.9927, то есть
|
|
||||||
запас есть, но лежит в признаках. Для сравнения, у мухи около двух тысяч клеток
|
|
||||||
Кеньона на полушарие.
|
|
||||||
|
|
||||||
Проверка — leave-one-bag-out, отдельная модель на каждую складку. Без этого
|
Проверка — leave-one-bag-out, отдельная модель на каждую складку. Без этого
|
||||||
сквозные цифры были бы ложными: считывание увидело бы проверяемый бэг ровно
|
сквозные цифры были бы ложными: считывание увидело бы проверяемый бэг ровно
|
||||||
так же, как память тоннеля, обученная на всём подряд.
|
так же, как память тоннеля, обученная на всём подряд.
|
||||||
|
|
@ -1449,10 +1398,6 @@ h +0.0055 lat +0.0044
|
||||||
могут пригодиться на линии с другой статистикой. Значения по умолчанию
|
могут пригодиться на линии с другой статистикой. Значения по умолчанию
|
||||||
выключают их полностью.
|
выключают их полностью.
|
||||||
|
|
||||||
*Позднее:* `warn_far` перемерен на конфигурации после п. 12.3 и 12.5 и
|
|
||||||
**принят** — P@150 0.33 → 0.37 без потерь ближе. Почему отказ устарел —
|
|
||||||
п. 15.9.
|
|
||||||
|
|
||||||
Итоговая конфигурация и её цена:
|
Итоговая конфигурация и её цена:
|
||||||
|
|
||||||
| | без гашения | **принято** |
|
| | без гашения | **принято** |
|
||||||
|
|
@ -1845,33 +1790,10 @@ P@150 0.32), а на всём, что меньше, — заметно хуже:
|
||||||
потолок. **Тот самый вес, из которого улика складывается, различает их
|
потолок. **Тот самый вес, из которого улика складывается, различает их
|
||||||
идеально.** Сумма потеряла то, что было в слагаемых.
|
идеально.** Сумма потеряла то, что было в слагаемых.
|
||||||
|
|
||||||
**Где именно это бьёт — оговорка.** `doubleT_obstacle` записан со **стоящего**
|
|
||||||
поезда, и там наблюдений у каждого трека сотни, так что потолка достигают и
|
|
||||||
предмет, и обстановка. На полигоне поезд едет, ложный трек столько не живёт, и
|
|
||||||
картина другая: улика на потолке у 79 % наблюдений предмета в полосе 0…30 м и
|
|
||||||
лишь у 1.5 % ложных, на 80…110 м — 31 % против 3.4 %.
|
|
||||||
|
|
||||||
То есть насыщение само по себе ложных тревог не создаёт. Оно **обесценивает
|
|
||||||
улику как меру**: у предмета она перестаёт расти, и весь запас, который он
|
|
||||||
заработал качеством наблюдений, пропадает — вместо «этот трек вчетверо
|
|
||||||
убедительнее» остаётся «оба по единице». Хуже всего это там, где поезд стоит
|
|
||||||
или ползёт, то есть ровно в ситуации, когда препятствие опаснее всего.
|
|
||||||
|
|
||||||
Отсюда и решение: сравнивать с порогом не улику, а её геометрическую смесь со
|
Отсюда и решение: сравнивать с порогом не улику, а её геометрическую смесь со
|
||||||
средним весом наблюдения, `w_mean^b · улика^(1−b)`. При `b = 0` всё ровно как
|
средним весом наблюдения, `w_mean^b · улика^(1−b)`. При `b = 0` всё ровно как
|
||||||
раньше, так что размен меряется, а не объявляется.
|
раньше, так что размен меряется, а не объявляется.
|
||||||
|
|
||||||
Что смешивание делает с отсчётом, посчитано прямо по выборке. При `b = 0.3`
|
|
||||||
через порог 0.5 уходят вниз 180 предметных наблюдений и 668 ложных — на одно
|
|
||||||
потерянное обнаружение три и семь десятых убранных ложных. При `b = 0.2`
|
|
||||||
соотношение лучше: 73 против 348, то есть 4.8. При `b = 0.5` оно рушится до
|
|
||||||
1.3, и это то же колено, что видно в таблице п. 15.6.
|
|
||||||
|
|
||||||
Смешивание при этом **не только давит**: у 21 % наблюдений отсчёт растёт — это
|
|
||||||
молодые треки с хорошими наблюдениями, у которых улика ещё мала. Среди ложных
|
|
||||||
таких даже больше (25.5 % против 14.6 % среди предметных), и на незнакомой
|
|
||||||
линии, где память ничего не подавляет, это выходит боком (п. 15.8).
|
|
||||||
|
|
||||||
### 15.2. Выборка по трекам
|
### 15.2. Выборка по трекам
|
||||||
|
|
||||||
Полигон научили попутно выкладывать описание каждого живого трека на каждом
|
Полигон научили попутно выкладывать описание каждого живого трека на каждом
|
||||||
|
|
@ -2019,405 +1941,3 @@ leave-one-bag-out.
|
||||||
(п. 12.5), для новизны (п. 12.3) и для утечки улики. Это возможно, но
|
(п. 12.5), для новизны (п. 12.3) и для утечки улики. Это возможно, но
|
||||||
смешивание даёт тот же эффект мягко и без ещё одной калибровочной кривой,
|
смешивание даёт тот же эффект мягко и без ещё одной калибровочной кривой,
|
||||||
поэтому порог оставлен выключенным (`track_gate = 0`).
|
поэтому порог оставлен выключенным (`track_gate = 0`).
|
||||||
|
|
||||||
### 15.8. Смешивание меняет не в ту сторону
|
|
||||||
|
|
||||||
Ложные треки на километр, обе сцены, считывание по складкам:
|
|
||||||
|
|
||||||
| настройка | знакомая линия | незнакомая линия | объект на 55 м |
|
|
||||||
|---|---:|---:|---:|
|
|
||||||
| база | 8.0 | **20.3** | 99.5 % |
|
|
||||||
| один дальний порог 0.30 | **8.0** | 21.3 | 99.5 % |
|
|
||||||
| смешивание 0.3 + дальний порог с 60 м | 6.3 | **24.8** | 99.5 % |
|
|
||||||
| смешивание 0.2 + дальний порог с 60 м | 8.0 | 24.0 | 99.5 % |
|
|
||||||
|
|
||||||
Смешивание улучшает знакомую линию и **портит незнакомую**. Это не случайность,
|
|
||||||
и механизм виден из разбора п. 15.1: смешивание не только давит — у 21 %
|
|
||||||
наблюдений оно отсчёт поднимает, и среди ложных таких больше, чем среди
|
|
||||||
предметных (25.5 % против 14.6 %). Поднимаются молодые треки с хорошими
|
|
||||||
наблюдениями и малой уликой. На знакомой линии память тоннеля такие треки уже
|
|
||||||
погасила через новизну, а на незнакомой гасить нечем — и смешивание их
|
|
||||||
вытаскивает.
|
|
||||||
|
|
||||||
Приватный тест — это незнакомая линия. Значит, смешивание отвергается:
|
|
||||||
`track_blend = 0` по умолчанию.
|
|
||||||
|
|
||||||
И это ещё не всё. Смешивание надо сравнивать не с базой, а с **тем же разменом,
|
|
||||||
полученным простым поднятием порога** — иначе мы сравниваем две разные рабочие
|
|
||||||
точки и выдаём сдвиг по кривой за улучшение кривой. Контроль:
|
|
||||||
|
|
||||||
| настройка | P@50 | P@100 | P@150 | посторонних |
|
|
||||||
|---|---|---|---|---:|
|
|
||||||
| смешивание 0.2 + дальний порог с 60 м | 0.71 | 0.47 | 0.37 | 817 |
|
|
||||||
| **порог 0.60 + дальний порог, без смешивания** | 0.70 | **0.49** | 0.37 | **816** |
|
|
||||||
|
|
||||||
Одинаковое число посторонних тревог, и при нём простой порог даёт обнаружение
|
|
||||||
на 100 м **лучше**. Никакой новой кривой смешивание не открывает — оно двигает
|
|
||||||
по той же, что и ключ, который у нас уже был.
|
|
||||||
|
|
||||||
Итог: `track_blend = 0`, `track_gate = 0`. Накопители на треке и считывание по
|
|
||||||
трекам остаются в коде как инструмент замера — они и дали понимание, что улика
|
|
||||||
насыщается, — но в решении не участвуют.
|
|
||||||
|
|
||||||
### 15.9. Отрицательные результаты стареют
|
|
||||||
|
|
||||||
Выигрыш в этой главе дал не считывание по трекам, ради которого всё
|
|
||||||
затевалось, а ключ, **отвергнутый тремя разделами раньше**. П. 12.2 говорит
|
|
||||||
дословно: «порог тревоги, линейно опущенный с 0.5 до 0.3 начиная с 90 м, не
|
|
||||||
изменил ни ложных (3.3 на км), ни обнаружения (P@100 = 0.31, решения за 120 м
|
|
||||||
по-прежнему 0)».
|
|
||||||
|
|
||||||
Тогда это было правдой, и причина там же, в п. 12.3: улика далёкого трека
|
|
||||||
колебалась около нуля, потому что знакомость была на той дальности перевёрнута
|
|
||||||
и входила в вес **множителем**. Опускать порог под сигналом, который равен
|
|
||||||
нулю, бессмысленно — что замер и показал.
|
|
||||||
|
|
||||||
Потом приняли гашение знакомости с 90 м (п. 12.3) и порог по лучам,
|
|
||||||
нормированный на дальность (п. 12.5). Улика далёкого трека перестала быть
|
|
||||||
нулём. Тот же самый ключ, тот же самый замер — и P@150 растёт с 0.33 до 0.37
|
|
||||||
ценой нуля ложных треков на знакомой линии и одного на незнакомой.
|
|
||||||
|
|
||||||
Отсюда правило, которое стоит дороже самой правки: **отрицательный результат
|
|
||||||
верен только для той конфигурации, в которой он получен.** Каждое принятое
|
|
||||||
изменение обесценивает часть прежних отказов, и дешёвые из них надо
|
|
||||||
перепроверять. Здесь это стоило одного прогона на четыре минуты.
|
|
||||||
|
|
||||||
Варианты той же правки, полигон, `человек_стоя`:
|
|
||||||
|
|
||||||
| настройка | P@50 | P@100 | P@150 | посторонних |
|
|
||||||
|---|---|---|---|---:|
|
|
||||||
| база | 0.70 | 0.53 | 0.33 | 999 |
|
|
||||||
| **порог 0.30 с 90 м** | 0.70 | 0.53 | **0.37** | **1091** |
|
|
||||||
| порог 0.30 с 60 м | 0.70 | 0.53 | 0.37 | 1197 |
|
|
||||||
| порог 0.20 с 90 м | 0.70 | 0.53 | 0.38 | 1181 |
|
|
||||||
| порог 0.30 + утечка 0.03 | 0.70 | **0.48** | 0.40 | 1172 |
|
|
||||||
|
|
||||||
Начинать раньше незачем: обнаружение то же, ложных больше. Опускать до 0.20 —
|
|
||||||
сотая на 150 м за восемь процентов посторонних тревог. Утечку, отвергнутую
|
|
||||||
в п. 12.4, на нынешней базе тоже перемерили: теперь она даёт P@150 0.40, но
|
|
||||||
забирает 100 м (0.53 → 0.48) и рабочую дальность 100 → 80 м — отвергнута
|
|
||||||
снова, уже по другой причине. Правило «перепроверять» не означает
|
|
||||||
«перепринимать».
|
|
||||||
|
|
||||||
Принято: `warn_far = 0.30`, `warn_far_from = 90`. Итог на всех сценах:
|
|
||||||
|
|
||||||
| | было | **стало** |
|
|
||||||
|---|---:|---:|
|
|
||||||
| P@150, человек стоя | 0.33 | **0.37** |
|
|
||||||
| P@150, человек сидя | 0.22 | **0.24** |
|
|
||||||
| P@100 / P@150, ящик | 0.38 / 0.03 | **0.40 / 0.06** |
|
|
||||||
| P@50 и P@100, человек | 0.70 / 0.53 | 0.70 / 0.53 |
|
|
||||||
| решений в полосе 90–120 м / 120–160 м | 0.49 / 0.38 | **0.51 / 0.41** |
|
|
||||||
| ложных треков на км, знакомая линия | 8.0 | **8.0** |
|
|
||||||
| то же, незнакомая линия | 20.3 | 21.3 |
|
|
||||||
| настоящий объект на 55 м | 99.5 % | **99.5 %** |
|
|
||||||
|
|
||||||
Экстренного торможения правка не касается: у него свой порог и условие
|
|
||||||
`d ≤ max(тормозной путь, 25 м)`, поэтому далёкий ложный трек физически не
|
|
||||||
может его вызвать. Мы снова платим предупреждениями за дальность — для
|
|
||||||
движущегося поезда это правильная сторона размена (как и в п. 12.3).
|
|
||||||
|
|
||||||
## 16. Работа Zhirik1337: что перенесено и что из этого принято
|
|
||||||
|
|
||||||
22.09 в командный репозиторий пришло семь коммитов Zhirik1337: вывод на
|
|
||||||
видеокарте, пять правок габарита и решения, «человек лёжа» в каталоге,
|
|
||||||
случайные сценарии полигона, выгрузка рамок, ROS-узел и Docker. Всё перенесено
|
|
||||||
в основной проект, код детектора — за флагами, выключенными по умолчанию.
|
|
||||||
Потом каждая правка мерилась отдельно и на тех же вставках. Флаги
|
|
||||||
выставляются из командной строки без правки кода:
|
|
||||||
`--set k_sigma=0.75` у `make_benchmark.py`, `evaluate.py`, `make_training_set.py`
|
|
||||||
и `check_obstacle.py`.
|
|
||||||
|
|
||||||
### 16.1. Полигон шумит сильнее, чем меняют правки
|
|
||||||
|
|
||||||
Две реализации случайности полигона при **одних и тех же** настройках (зерна
|
|
||||||
12345 и 777) расходятся на P@150 у человека стоя 0.39 против 0.32, на P@100 у
|
|
||||||
ящика 0.40 против 0.29. Большинство правок меняют цифры меньше, поэтому
|
|
||||||
сравнивать два прогона по итоговой таблице нельзя: разница окажется шумом.
|
|
||||||
|
|
||||||
Сравнение ведётся **парно**. Генератор у каждого сценария свой (п. 16.2), и от
|
|
||||||
решений конвейера он не зависит, поэтому при другой настройке на вход идут те же
|
|
||||||
кадры с теми же вставками, до луча. Разница двух прогонов — только от настройки,
|
|
||||||
и её видно по переворотам отдельных наблюдений: сколько было 0 и стало 1 и
|
|
||||||
наоборот (`tools/compare_benchmark.py`). Если перевороты идут в одну сторону,
|
|
||||||
эффект настоящий, даже когда он меньше межзернового шума.
|
|
||||||
|
|
||||||
Так перепроверен и дальний порог из п. 15.9, на обоих зёрнах: 0.50 → 0.30
|
|
||||||
переворачивает 8 и 2 наблюдения человека стоя на 130–170 м в плюс и **ни одного**
|
|
||||||
в минус. Эффект маленький, но настоящий, и правка остаётся.
|
|
||||||
|
|
||||||
### 16.2. Новый предмет сдвигал цифры всех остальных
|
|
||||||
|
|
||||||
Генератор был один на запись, и сценарии тянули из него по очереди, кадр за
|
|
||||||
кадром. Когда в каталог добавили человека лёжа, поток сдвинулся для **всех**
|
|
||||||
сценариев: цифры по ящику менялись оттого, что где-то появился новый предмет.
|
|
||||||
Это проверено прямо: со старым каталогом перенесённый код давал полигон
|
|
||||||
побайтово, с новым цифры прежних предметов разошлись.
|
|
||||||
|
|
||||||
Теперь у каждого сценария свой генератор, зерно — из имени предмета и бокового
|
|
||||||
смещения через crc32 (`scenario_rng`; встроенный `hash()` солится в каждом
|
|
||||||
процессе заново). Случайные сценарии Zhirik1337 (`--augment`) переведены на него
|
|
||||||
же. Тест `test_scenario_stream_does_not_depend_on_the_rest_of_the_catalogue`.
|
|
||||||
|
|
||||||
База с новым каталогом (порог 0.30 за 90 м, как в п. 15.9):
|
|
||||||
|
|
||||||
| предмет | рабочая дальность | P@50 | P@100 | P@150 |
|
|
||||||
|---|---:|---:|---:|---:|
|
|
||||||
| человек стоя | 80 м | 0.70 | 0.48 | 0.39 |
|
|
||||||
| человек сидя | 100 м | 0.67 | 0.52 | 0.20 |
|
|
||||||
| **человек лёжа** | 20 м | **0.27** | **0.00** | 0.00 |
|
|
||||||
| ящик | 20 м | 0.58 | 0.40 | 0.06 |
|
|
||||||
|
|
||||||
Человек лёжа виден хуже всех, кроме совсем мелочи. Воронка (п. 12) показывает
|
|
||||||
почему: на 30–50 м кандидат рождается только в 35 % наблюдений. Высота 0.30 м,
|
|
||||||
а пол габарита стоит на 0.28 м, так что в габарит попадает верхушка в два
|
|
||||||
сантиметра.
|
|
||||||
|
|
||||||
### 16.3. Пол в колее: вдали выигрыш, вблизи рельсы
|
|
||||||
|
|
||||||
Правка: между рельсами (|u| ≤ 0.85 м) нижняя граница габарита опускается с
|
|
||||||
0.28 до 0.16 м, снаружи колеи остаётся прежней. Вдали она даёт много, но
|
|
||||||
вблизи обнаружение **падает вдвое**:
|
|
||||||
|
|
||||||
| полоса | человек стоя: база → пол 0.16 | лёжа | ящик |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 0–15 м | 0.93 → 0.92 | 0.70 → **0.58** | 0.75 → **0.62** |
|
|
||||||
| 15–25 м | 0.71 → **0.37** | 0.55 → **0.27** | 0.56 → **0.34** |
|
|
||||||
| 25–40 м | 0.58 → **0.42** | 0.37 → 0.35 | 0.48 → **0.35** |
|
|
||||||
| 55–70 м | 0.83 → 0.83 | 0.17 → **0.62** | 0.70 → 0.71 |
|
|
||||||
| 110–135 м | 0.43 → 0.46 | 0.00 → 0.00 | 0.30 → **0.60** |
|
|
||||||
| 160–190 м | 0.07 → **0.27** | 0.03 → 0.03 | 0.04 → **0.12** |
|
|
||||||
|
|
||||||
Рабочая дальность у всех предметов стала 8 м, то есть обнаружение
|
|
||||||
проваливается уже на втором поясе.
|
|
||||||
|
|
||||||
**Причина.** Один и тот же кадр со вставкой прогнан через оба конвейера, и там,
|
|
||||||
где обычный видит кандидата, а опущенный нет, кандидаты пересчитаны без запрета
|
|
||||||
на длину. Во всех 12 потерях на 12–45 м одной записи предмет входит в
|
|
||||||
компоненту длиной 50–140 м, которая начинается **с 4 м** — с ближней границы
|
|
||||||
габарита. У разобранных подробно её нижняя точка лежит ровно на опущенном
|
|
||||||
полу, 0.16–0.18 м: это головки рельсов. Вблизи соседние кольца ложатся на
|
|
||||||
полотно плотнее допуска по глубине, и рельс собирается в одну компоненту. Компоненту
|
|
||||||
длиннее 15 м конвейер отбрасывает как «полотно или стену под скользящим углом»
|
|
||||||
и выбрасывает вместе с ней предмет. Разделение фигуры и фона могло бы вырезать
|
|
||||||
предмет, но ближе `split_near = 55 м` оно выключено — то же слипание, что с
|
|
||||||
кромкой платформы в п. 12.7.
|
|
||||||
|
|
||||||
**Лечение** — не опускать пол ближе заданной дальности (`core_from`). Почему
|
|
||||||
дальше рельсы предмет не губят, отдельно не разбирали; замер показывает, что
|
|
||||||
с 30 м опущенный пол вреда не делает (старые складки, база → пол с 30 м):
|
|
||||||
|
|
||||||
| полоса | человек стоя | лёжа | ящик |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 0–15 м | 0.93 → 0.93 | 0.70 → 0.70 | 0.75 → 0.75 |
|
|
||||||
| 15–25 м | 0.71 → 0.71 | 0.55 → 0.55 | 0.56 → 0.56 |
|
|
||||||
| 25–40 м | 0.58 → 0.58 | 0.37 → **0.50** | 0.48 → 0.49 |
|
|
||||||
| 55–70 м | 0.83 → 0.83 | 0.17 → **0.62** | 0.70 → 0.71 |
|
|
||||||
| 110–135 м | 0.43 → 0.46 | 0.00 → 0.00 | 0.30 → **0.60** |
|
|
||||||
| 160–190 м | 0.07 → **0.27** | 0.03 → 0.03 | 0.04 → **0.12** |
|
|
||||||
|
|
||||||
Где ставить порог, мерилось тремя прогонами. Ближе 25 м ни один из них ничего
|
|
||||||
не теряет: из 3 469 наблюдений всех предметов в минус не перевернулось ни одно
|
|
||||||
(в плюс 8, 6 и 0 — треки, начатые дальше порога, доживают до ближней зоны).
|
|
||||||
Различаются пороги на мелком и низком:
|
|
||||||
|
|
||||||
| порог | ведро 25–40 м | ведро 40–55 м | лёжа 25–40 м | посторонних на полигоне |
|
|
||||||
|---|---:|---:|---:|---:|
|
|
||||||
| база (пол не опущен) | 0.23 | 0.02 | 0.37 | 1196 |
|
|
||||||
| **с 30 м** | **0.49** | **0.52** | **0.50** | **1313** |
|
|
||||||
| с 40 м | 0.49 | 0.52 | 0.45 | 1333 |
|
|
||||||
| с 55 м | 0.23 | 0.29 | 0.45 | 1336 |
|
|
||||||
|
|
||||||
Порог 30 м не хуже остальных ни в одной полосе и даёт меньше посторонних.
|
|
||||||
Парно против 40 м он добавляет 7 наблюдений лежачего и 2 ящика и не теряет
|
|
||||||
ни одного.
|
|
||||||
|
|
||||||
Парно, на тех же вставках (пол 0.16 с 30 м против базы, старые складки):
|
|
||||||
|
|
||||||
| предмет | полоса | было | стало | 0→1 | 1→0 |
|
|
||||||
|---|---|---:|---:|---:|---:|
|
|
||||||
| человек стоя | 130–170 м | 0.36 | 0.50 | 37 | 0 |
|
|
||||||
| человек сидя | 130–170 м | 0.19 | 0.47 | 68 | 0 |
|
|
||||||
| человек лёжа | 50–90 м | 0.13 | 0.46 | 74 | 0 |
|
|
||||||
| ящик | 90–130 м | 0.36 | 0.56 | 46 | 0 |
|
|
||||||
| чемодан | 130–170 м | 0.01 | 0.26 | 51 | 0 |
|
|
||||||
| ведро | 50–90 м | 0.00 | 0.39 | 85 | 0 |
|
|
||||||
|
|
||||||
На всех предметах и полосах вместе +677 наблюдений и −7. Вблизи с порогом по
|
|
||||||
дальности ничего не теряется (человек стоя на 6–50 м: +1 и −0). Цена:
|
|
||||||
|
|
||||||
Со старыми моделями пол обходится в два с лишним трека на километр на
|
|
||||||
знакомой линии и в четыре на незнакомой (таблица ниже, средний столбец).
|
|
||||||
Лишние треки — не сбой, а то, что пол открыл: низкие конструкции в колее. На
|
|
||||||
записи со стрелочным переводом появились три новых трека, и один из них —
|
|
||||||
предмет высотой 0.42 м прямо между рельсами (u = −0.40 м) на 78–104 м.
|
|
||||||
Лидар его видел и раньше, но он стоял ниже пола габарита.
|
|
||||||
|
|
||||||
Считывание таких кандидатов не видело никогда: оно обучалось на выборке,
|
|
||||||
собранной при прежнем поле. Поэтому, как в п. 12.6, выборка пересобрана на
|
|
||||||
новом полу — **333 739 кандидатов против 214 958**. Прибавка почти целиком
|
|
||||||
фоновая, это и есть низкие конструкции в колее, плюс человек лёжа, которого
|
|
||||||
раньше в каталоге не было (предметов 24 651 против 20 150). Складки
|
|
||||||
переобучены с прежней ёмкостью 4 000 клеток. AUC leave-one-bag-out **0.9856**
|
|
||||||
против 0.9829 на прежней выборке (по складкам 0.980–0.992): новые фоновые
|
|
||||||
кандидаты отделяются от предметов легко.
|
|
||||||
|
|
||||||
| | база | пол 30 м, старые складки | **пол 30 м, новые складки** |
|
|
||||||
|---|---:|---:|---:|
|
|
||||||
| ложных треков/км, знакомая линия | 8.0 | 10.2 | **8.0** |
|
|
||||||
| кадров с тревогой | 13.3 % | 19.3 % | 13.8 % |
|
|
||||||
| то же, незнакомая линия | 21.3 | 25.4 | **23.5** |
|
|
||||||
| кадров с тревогой | 28.3 % | 37.4 % | 34.6 % |
|
|
||||||
| посторонних на полигоне | 1196 | 1313 | **1226** |
|
|
||||||
| настоящий объект на 55 м | 99.5 % | 99.5 % | **99.5 %** |
|
|
||||||
| задержка кадра p50 / p95, мс (один процесс) | 33–45 / 37–49 | — | 35–42 / 41–52 |
|
|
||||||
|
|
||||||
Переобученное считывание убрало всю цену на знакомой линии и половину на
|
|
||||||
незнакомой. Из выигрыша в обнаружении отдана малая часть — у лежачего P@100
|
|
||||||
0.32 → 0.22 против старых складок, остальное в пределах сотых. Против базы:
|
|
||||||
|
|
||||||
| предмет | рабочая дальность | P@50 | P@100 | P@150 |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| человек стоя | 80 → 80 м | 0.70 → 0.71 | 0.48 → 0.48 | 0.39 → **0.49** |
|
|
||||||
| человек сидя | 100 → **148 м** | 0.67 → 0.67 | 0.52 → 0.52 | 0.20 → **0.49** |
|
|
||||||
| **человек лёжа** | 20 → **32 м** | 0.27 → **0.50** | 0.00 → **0.22** | 0.00 → 0.02 |
|
|
||||||
| ящик | 20 → 20 м | 0.58 → 0.54 | 0.40 → **0.50** | 0.06 → **0.26** |
|
|
||||||
| чемодан | 62 → **80 м** | 0.56 → 0.61 | 0.25 → **0.45** | 0.00 → **0.24** |
|
|
||||||
|
|
||||||
Парно против базы, все предметы:
|
|
||||||
|
|
||||||
| полоса | наблюдений | 0→1 | 1→0 |
|
|
||||||
|---|---:|---:|---:|
|
|
||||||
| 0–15 м | 2 158 | 0 | **16** |
|
|
||||||
| 15–40 м | 2 583 | 93 | 1 |
|
|
||||||
| 40–90 м | 2 644 | 224 | 7 |
|
|
||||||
| 90–200 м | 2 746 | 368 | 9 |
|
|
||||||
|
|
||||||
Шестнадцать ближних потерь — цена именно переобучения: со старыми складками
|
|
||||||
ближе 25 м не терялось ничего. Тринадцать из них — человек стоя у края
|
|
||||||
габарита (смещение 0.9 м) на 9–15 м, на трёх записях, две из них с платформой;
|
|
||||||
ещё сидящий там же и две бутылки на оси. Кандидат и трек у человека есть,
|
|
||||||
решения нет. Отдельно причину не разбирали. По месту это тот же случай, что в
|
|
||||||
п. 12.7: предмет у края габарита вблизи, где разрез фигуры выключен.
|
|
||||||
|
|
||||||
**Принято**: `h_lo_core = 0.16`, `core_from = 30`, выборка и складки
|
|
||||||
пересобраны. Самый важный для метро случай — человек, упавший на пути, — был
|
|
||||||
виден хуже всех (P@50 = 0.27, дальше 50 м почти никогда), теперь P@50 = 0.50 и
|
|
||||||
рабочая дальность 32 м. Цена — два трека на километр только на незнакомой линии
|
|
||||||
и 16 ближних наблюдений из двух тысяч у края габарита.
|
|
||||||
|
|
||||||
### 16.4. Лежащий предмет без штрафа — при обученном считывании не действует
|
|
||||||
|
|
||||||
`lying_exempt` снимает штраф за вытянутость по пути с низкого предмета в колее
|
|
||||||
(высота до 0.40 м, длина до 2.2 м): человек лёжа вдоль пути тянется на 1.8 м и
|
|
||||||
по ручной формуле похож на кусок лотка. Замер: полигон совпал с базой до
|
|
||||||
последнего наблюдения, и с опущенным полом тоже.
|
|
||||||
|
|
||||||
Причина не в правке, а в том, куда она встроена. Множитель компактности
|
|
||||||
живёт в ручной формуле веса, а в рабочей конфигурации `mbon_blend = 1.0`: вес
|
|
||||||
наблюдения целиком берёт обученное считывание, и ручная формула входит в него
|
|
||||||
в нулевой степени (`central_complex._quality`). Правка работает только там,
|
|
||||||
где модели нет. Флаг оставлен выключенным. Лежащему человеку помогает пол
|
|
||||||
(п. 16.3). Считывание, переобученное на выборке, где лежачий уже есть, ему не
|
|
||||||
помогло: P@100 0.32 → 0.22 против старых складок (п. 16.3).
|
|
||||||
|
|
||||||
### 16.5. Габарит в кривых, вырез платформы, дальний канал
|
|
||||||
|
|
||||||
Все три мерились одинаково: полигон парно против базы (сумма переворотов по
|
|
||||||
всем полосам) и ложные треки на двух сценах со старыми моделями.
|
|
||||||
|
|
||||||
| правка | человек стоя | сидя | лёжа | ящик | чемодан | всего | ложных/км, знакомая | незнакомая |
|
|
||||||
|---|---|---|---|---|---|---|---:|---:|
|
|
||||||
| база | | | | | | | 8.0 | 21.3 |
|
|
||||||
| `k_sigma = 0.75` | +0/−1 | +19/−0 | +0/−10 | +37/−0 | +0/−5 | +56/−16 | 9.5 | 25.7 |
|
|
||||||
| `platform_filter` | +9/−5 | +17/−3 | +0/−11 | +2/−7 | +1/−0 | +35/−26 | 10.2 | 25.2 |
|
|
||||||
| `far_channel` | +24/−0 | +21/−0 | +10/−0 | +43/−0 | +15/−0 | +115/−0 | 11.0 | 28.4 |
|
|
||||||
| для сравнения: пол с 30 м | +51/−1 | +92/−0 | +135/−0 | +108/−5 | +131/−0 | **+677/−7** | 10.2 | 25.4 |
|
|
||||||
|
|
||||||
**Габарит по неопределённости оси** (`k_sigma`) расширяет коридор там, где ось
|
|
||||||
пути известна хуже, — вдали и в кривых. Ящику это помогает, лежачему и
|
|
||||||
чемодану мешает: в широком габарите к низкому предмету чаще прилипает
|
|
||||||
посторонняя компонента. Ложные треки добавляются ровно там, где габарит
|
|
||||||
расширился, — на кривых записях, с медианной дальностью около 100 м. Отвергнуто.
|
|
||||||
|
|
||||||
**Вырез платформы** (`platform_filter`) удаляет из габарита горизонтальную
|
|
||||||
полосу 1.05–1.25 м у края (|u| ≥ 1.30 м) — настил платформы. На записях с
|
|
||||||
платформой ложных треков ровно столько же (1 → 1 и 2 → 2), а весь рост
|
|
||||||
приходится на `roundT_doubleT`, где платформы нет вовсе: 1 → 4 на знакомой
|
|
||||||
линии, 8 → 12 на незнакомой. Полоса, вырезанная из стены или лотка, режет
|
|
||||||
конструкцию на верхний и нижний обрывки, и каждый выглядит компактным
|
|
||||||
предметом. На полигоне перевороты в обе стороны почти поровну, то есть это
|
|
||||||
шум. Отвергнуто.
|
|
||||||
|
|
||||||
**Дальний канал** (`far_channel`) — отдельный путь к тревоге для треков дальше
|
|
||||||
90 м с послабленными условиями. Выигрыш настоящий (+115 и ни одного минуса), но
|
|
||||||
он решает ту же задачу, что и принятый дальний порог (п. 15.9), и стоит втрое
|
|
||||||
больше: +3.0 трека на км на знакомой линии и +7.1 на незнакомой, посторонних на
|
|
||||||
полигоне 1196 → 1676. Для сравнения, пол с 30 м даёт вшестеро больше
|
|
||||||
переворотов за меньшую цену. Отвергнуто; включать его вместе с дальним
|
|
||||||
порогом — ослаблять дальний край дважды.
|
|
||||||
|
|
||||||
### 16.6. Всё вместе, как было отправлено
|
|
||||||
|
|
||||||
Все пять правок разом плюс свои пороги дальнего края (`warn_far = 0.35`,
|
|
||||||
`leak_far = 0.06`): дальние цифры самые высокие из всех прогонов (P@150 у
|
|
||||||
человека сидя 0.56, у ящика 0.37), но посторонних тревог на полигоне **2299
|
|
||||||
против 1196** — почти вдвое, — и рабочая дальность 8 м у всех предметов из-за
|
|
||||||
рельсов (п. 16.3). Парно против базы:
|
|
||||||
|
|
||||||
| полоса | наблюдений | было | стало | 0→1 | 1→0 |
|
|
||||||
|---|---:|---:|---:|---:|---:|
|
|
||||||
| 6–40 м | 2 926 | 0.63 | **0.50** | 107 | **496** |
|
|
||||||
| 40–90 м | 1 982 | 0.47 | 0.61 | 271 | 3 |
|
|
||||||
| 90–200 м | 2 684 | 0.19 | 0.38 | 531 | 0 |
|
|
||||||
|
|
||||||
Дальше 40 м всё в плюс, ближе — всё в минус. А ближняя зона — это экстренное
|
|
||||||
торможение (у него условие `d ≤ max(тормозной путь, 25 м)`, п. 15.9) и вся
|
|
||||||
работа на малой скорости, у платформы и на подходе к ней: там других
|
|
||||||
дальностей просто нет. По отдельности видно, откуда что: дальний выигрыш почти
|
|
||||||
целиком даёт пол, ближний провал — он же без порога по дальности, а из
|
|
||||||
посторонних тревог больше всего добавляет дальний канал: +480 в одиночку, пол
|
|
||||||
без порога +99, остальные — десятки.
|
|
||||||
|
|
||||||
### 16.7. Выгрузка рамок, устройство, ROS-узел
|
|
||||||
|
|
||||||
* **Рамка в кривой стояла не там.** Боковое смещение трека отсчитано от оси
|
|
||||||
пути, а шло прямо в `x` сенсора: на радиусе 1300 м рамка уезжала на 1.2 м на
|
|
||||||
55 м и на 8.6 м на 150 м — в стену. Теперь `x = u + corridor.centre(d)`.
|
|
||||||
* **Поворот рамки был зеркальным**: `yaw = −atan(наклон)` вместо `+atan`, на
|
|
||||||
150 м это 13°. Оба случая проверяет `test_export_box_follows_a_curved_track`.
|
|
||||||
* **Устройство по умолчанию — процессор.** У Zhirik1337 `device=None` означал
|
|
||||||
«видеокарта, если есть», и обучение с выводом тихо уезжали на GPU. Вывод на
|
|
||||||
видеокарте ускоряет только ламину (п. 7.4). `auto` и `cuda` работают, если их
|
|
||||||
передать явно; Docker так и делает. Ошибка по дороге: `encode(device="auto")`
|
|
||||||
отдавал строку `auto` прямо в torch, падал, и падение навсегда помечало CUDA
|
|
||||||
сломанной.
|
|
||||||
* **ROS-узел `tools/flyguard_ros2_node.py`** отдаёт конвейеру массив numpy
|
|
||||||
вместо разобранного `PointCloud2`, публикует все треки, а не решение, и у
|
|
||||||
трека нет полей рамки. Узел ведёт интеграция, он оставлен за ними
|
|
||||||
(`TEAM_OWNED` в `export_team.py`). Рабочий узел — `ros2_ws/.../node.py`.
|
|
||||||
|
|
||||||
### 16.8. Итог
|
|
||||||
|
|
||||||
| правка | решение | почему |
|
|
||||||
|---|---|---|
|
|
||||||
| пол в колее `h_lo_core = 0.16` | **принято**, дальше 30 м (`core_from`) | лежачий P@50 0.27 → 0.50, P@150 у стоя 0.39 → 0.49; без порога по дальности вблизи провал вдвое |
|
|
||||||
| выборка и складки на новом полу | **принято** | цена пола на знакомой линии 2.2 → 0 треков на км, на незнакомой 4.1 → 2.2 |
|
|
||||||
| `lying_exempt` | не действует | правит ручную формулу, а вес целиком от модели |
|
|
||||||
| `k_sigma` | отвергнуто | +56/−16 на полигоне за +1.5 и +4.4 трека на км |
|
|
||||||
| `platform_filter` | отвергнуто | на платформах ничего, в тоннеле без платформы режет стену: +2.2 и +3.9 |
|
|
||||||
| `far_channel` | отвергнуто | та же задача, что `warn_far`, втрое дороже: +3.0 и +7.1 |
|
|
||||||
| человек лёжа в каталоге | **принято** | самый важный для метро случай и самый трудный |
|
|
||||||
| генератор на сценарий | **принято** | новый предмет больше не сдвигает цифры остальных |
|
|
||||||
| выгрузка рамок `export.py` | **принято** с двумя исправлениями | рамка в кривой и знак поворота |
|
|
||||||
| вывод на видеокарте | в коде, по умолчанию процессор | ускоряет только ламину (п. 7.4) |
|
|
||||||
|
|
||||||
Всё отвергнутое осталось в коде за флагами, с ценой в этом разделе:
|
|
||||||
отрицательный результат верен только для той конфигурации, в которой получен
|
|
||||||
(п. 15.9), и перепроверка любой правки стоит одного ключа `--set`.
|
|
||||||
|
|
||||||
**Что не сделано.** Память тоннеля (грибовидное тело) обучена на кандидатах,
|
|
||||||
собранных 19.09 при прежнем полу, и низких конструкций в колее не знает: на
|
|
||||||
знакомой линии их гасит только считывание. Выборку памяти надо собирать
|
|
||||||
проходом по 90 ГБ `new_data` — это следующий шаг, если понадобится ещё
|
|
||||||
снизить ложные на знакомой линии.
|
|
||||||
|
|
|
||||||
|
|
@ -147,10 +147,8 @@ class CentralComplex:
|
||||||
inhibition: float = 0.05, max_misses: int = 12,
|
inhibition: float = 0.05, max_misses: int = 12,
|
||||||
max_tracks: int = 48, use_shape: bool = True,
|
max_tracks: int = 48, use_shape: bool = True,
|
||||||
mbon_power: float = 1.0, mbon_blend: float = 1.0,
|
mbon_power: float = 1.0, mbon_blend: float = 1.0,
|
||||||
nov_fade_from: float = 0.0, nov_fade_to: float = 160.0,
|
nov_fade_from: float = 0.0, nov_fade_to: float = 160.0):
|
||||||
lying_exempt: bool = False):
|
|
||||||
self.use_shape = use_shape
|
self.use_shape = use_shape
|
||||||
self.lying_exempt = lying_exempt
|
|
||||||
self.mbon_power = mbon_power
|
self.mbon_power = mbon_power
|
||||||
self.mbon_blend = mbon_blend
|
self.mbon_blend = mbon_blend
|
||||||
self.nov_fade_from = nov_fade_from
|
self.nov_fade_from = nov_fade_from
|
||||||
|
|
@ -226,8 +224,7 @@ class CentralComplex:
|
||||||
t.hits += 1
|
t.hits += 1
|
||||||
t.misses = 0
|
t.misses = 0
|
||||||
w = _quality(c, self.use_shape, self.mbon_power, self.mbon_blend,
|
w = _quality(c, self.use_shape, self.mbon_power, self.mbon_blend,
|
||||||
self.nov_fade_from, self.nov_fade_to,
|
self.nov_fade_from, self.nov_fade_to)
|
||||||
self.lying_exempt)
|
|
||||||
t.observe(c, self.s_world + c.d, w)
|
t.observe(c, self.s_world + c.d, w)
|
||||||
t.evidence = min(1.0, t.evidence + self.gain * w)
|
t.evidence = min(1.0, t.evidence + self.gain * w)
|
||||||
if dt > 1e-3:
|
if dt > 1e-3:
|
||||||
|
|
@ -254,8 +251,7 @@ class CentralComplex:
|
||||||
width=c.width, height=c.height, first_d=c.d, last_d=c.d,
|
width=c.width, height=c.height, first_d=c.d, last_d=c.d,
|
||||||
last_n_rays=c.n_rays, novelty=c.novelty)
|
last_n_rays=c.n_rays, novelty=c.novelty)
|
||||||
w = _quality(c, self.use_shape, self.mbon_power, self.mbon_blend,
|
w = _quality(c, self.use_shape, self.mbon_power, self.mbon_blend,
|
||||||
self.nov_fade_from, self.nov_fade_to,
|
self.nov_fade_from, self.nov_fade_to)
|
||||||
self.lying_exempt)
|
|
||||||
t.observe(c, self.s_world + c.d, w)
|
t.observe(c, self.s_world + c.d, w)
|
||||||
t.evidence = self.gain * w
|
t.evidence = self.gain * w
|
||||||
t.hits = 1
|
t.hits = 1
|
||||||
|
|
@ -299,7 +295,7 @@ def expected_rays(d: float) -> float:
|
||||||
|
|
||||||
def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||||
mbon_blend: float = 1.0, nov_fade_from: float = 0.0,
|
mbon_blend: float = 1.0, nov_fade_from: float = 0.0,
|
||||||
nov_fade_to: float = 160.0, lying_exempt: bool = False) -> float:
|
nov_fade_to: float = 160.0) -> float:
|
||||||
"""Вес одного наблюдения: сколько улики оно добавляет.
|
"""Вес одного наблюдения: сколько улики оно добавляет.
|
||||||
|
|
||||||
Дальний объект даёт мало лучей не потому, что он сомнительный, а потому что
|
Дальний объект даёт мало лучей не потому, что он сомнительный, а потому что
|
||||||
|
|
@ -329,8 +325,7 @@ def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||||
span = max(c.width, c.height, 0.2)
|
span = max(c.width, c.height, 0.2)
|
||||||
# Предмет или человек, лежащий в колее вдоль пути (depth ~ 1.8 м при малой высоте),
|
# Предмет или человек, лежащий в колее вдоль пути (depth ~ 1.8 м при малой высоте),
|
||||||
# лежит прямо на полотне перед поездом — не штрафуем его как конструкцию тоннеля:
|
# лежит прямо на полотне перед поездом — не штрафуем его как конструкцию тоннеля:
|
||||||
is_lying_on_track = (lying_exempt and abs(c.u) <= 0.85
|
is_lying_on_track = (abs(c.u) <= 0.85 and c.h <= 0.40 and c.depth <= 2.2)
|
||||||
and c.h <= 0.40 and c.depth <= 2.2)
|
|
||||||
if is_lying_on_track:
|
if is_lying_on_track:
|
||||||
compact = 1.0
|
compact = 1.0
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,7 @@ class DescendingNeurons:
|
||||||
novelty_floor: float = 0.10, max_range: float = 200.0,
|
novelty_floor: float = 0.10, max_range: float = 200.0,
|
||||||
warn_far: float | None = None, warn_far_from: float = 90.0,
|
warn_far: float | None = None, warn_far_from: float = 90.0,
|
||||||
track_blend: float = 0.0, track_score: str = "w_mean",
|
track_blend: float = 0.0, track_score: str = "w_mean",
|
||||||
track_gate: float = 0.0, track_readout=None,
|
track_gate: float = 0.0, track_readout=None):
|
||||||
far_channel: bool = False):
|
|
||||||
self.warn_evidence = warn_evidence
|
self.warn_evidence = warn_evidence
|
||||||
self.clear_evidence = clear_evidence
|
self.clear_evidence = clear_evidence
|
||||||
self.emergency_evidence = emergency_evidence
|
self.emergency_evidence = emergency_evidence
|
||||||
|
|
@ -95,12 +94,6 @@ class DescendingNeurons:
|
||||||
# предмета против 0.269 у ложных треков — разделение, под которое
|
# предмета против 0.269 у ложных треков — разделение, под которое
|
||||||
# порог и просится. 0 — не проверять.
|
# порог и просится. 0 — не проверять.
|
||||||
self.track_gate = track_gate
|
self.track_gate = track_gate
|
||||||
# Дальний канал предупреждения (Zhirik1337, 22.09): далёкий трек с
|
|
||||||
# хорошими наблюдениями поднимает тревогу в обход порога, даже если
|
|
||||||
# улика просела на пропусках. Та же задача, что у `warn_far`, другим
|
|
||||||
# способом; включать вместе с ним — ослабить дальний край дважды.
|
|
||||||
# Замер — EXPERIMENTS п. 16.
|
|
||||||
self.far_channel = far_channel
|
|
||||||
self.track_score = track_score
|
self.track_score = track_score
|
||||||
self.track_readout = track_readout
|
self.track_readout = track_readout
|
||||||
self._latched: set[int] = set()
|
self._latched: set[int] = set()
|
||||||
|
|
@ -127,9 +120,7 @@ class DescendingNeurons:
|
||||||
ready: list[tuple[Track, float]] = []
|
ready: list[tuple[Track, float]] = []
|
||||||
for t in cx.tracks:
|
for t in cx.tracks:
|
||||||
d = t.distance(cx.s_world)
|
d = t.distance(cx.s_world)
|
||||||
far_floor = 0.05 if self.far_channel else self.novelty_floor
|
nov_ok = (t.novelty >= self.novelty_floor) if d <= self.warn_far_from else (t.novelty >= 0.05)
|
||||||
nov_ok = (t.novelty >= self.novelty_floor if d <= self.warn_far_from
|
|
||||||
else t.novelty >= far_floor)
|
|
||||||
if (not (0.0 < d <= self.max_range) or t.hits < self.min_hits
|
if (not (0.0 < d <= self.max_range) or t.hits < self.min_hits
|
||||||
or not nov_ok):
|
or not nov_ok):
|
||||||
self._latched.discard(t.id)
|
self._latched.discard(t.id)
|
||||||
|
|
@ -168,8 +159,7 @@ class DescendingNeurons:
|
||||||
# На дальности d > warn_far_from при наличии подтверждений (hits >= min_hits)
|
# На дальности d > warn_far_from при наличии подтверждений (hits >= min_hits)
|
||||||
# трек с устойчивыми признаками (p_mean, w_mean, w_ema или p_track)
|
# трек с устойчивыми признаками (p_mean, w_mean, w_ema или p_track)
|
||||||
# квалифицируется как предупреждение, даже если улика просела из-за пропусков.
|
# квалифицируется как предупреждение, даже если улика просела из-за пропусков.
|
||||||
is_far = (self.far_channel and d > self.warn_far_from
|
is_far = d > self.warn_far_from and t.hits >= self.min_hits
|
||||||
and t.hits >= self.min_hits)
|
|
||||||
quality = max(t.p_mean, t.w_mean, t.w_ema)
|
quality = max(t.p_mean, t.w_mean, t.w_ema)
|
||||||
far_qualified = is_far and (
|
far_qualified = is_far and (
|
||||||
(quality >= 0.40 and t.evidence >= 0.15)
|
(quality >= 0.40 and t.evidence >= 0.15)
|
||||||
|
|
|
||||||
|
|
@ -187,27 +187,20 @@ def export_frame(decision: Decision, plane: RailPlane | None, corridor: Corridor
|
||||||
u = obj.lateral
|
u = obj.lateral
|
||||||
h = obj.height
|
h = obj.height
|
||||||
|
|
||||||
# Координаты в системе сенсора (x: вправо, y: вперёд (-d), z: вверх).
|
# Координаты в системе сенсора (x: вправо, y: вперёд (-d), z: вверх)
|
||||||
#
|
x_sensor = float(u)
|
||||||
# Боковое смещение трека отсчитано от ОСИ ПУТИ, а не от оси сенсора
|
y_sensor = float(-d)
|
||||||
# (TrackFrame.lateral), поэтому в кривой к нему прибавляется положение
|
z_sensor = float(h + a * d + b * u + c)
|
||||||
# оси на этой дальности: при радиусе 1300 м это 1.2 м на 55 м и 8.6 м
|
|
||||||
# на 150 м — без поправки рамка рисовалась в стене. Направление оси
|
# Касательная к оси коридора: dyaw / dd
|
||||||
# пути в кадре — (наклон, −1), и длинная ось рамки (её локальная y)
|
|
||||||
# совпадает с ним при повороте на +atan(наклон). Проверка обоих —
|
|
||||||
# test_export_box_follows_a_curved_track.
|
|
||||||
if corridor is not None and corridor.n_slices > 0:
|
if corridor is not None and corridor.n_slices > 0:
|
||||||
x_sensor = float(u + corridor.centre(np.array([d], np.float32))[0])
|
|
||||||
c0, c1, c2 = corridor.coef
|
c0, c1, c2 = corridor.coef
|
||||||
dm = max(corridor.d_max_seen, 1.0)
|
dm = max(corridor.d_max_seen, 1.0)
|
||||||
d_in = min(d, dm)
|
d_in = min(d, dm)
|
||||||
slope = c1 + 2.0 * c2 * d_in
|
slope = c1 + 2.0 * c2 * d_in
|
||||||
yaw = float(math.atan(slope))
|
yaw = float(-math.atan(slope))
|
||||||
else:
|
else:
|
||||||
x_sensor = float(u)
|
|
||||||
yaw = 0.0
|
yaw = 0.0
|
||||||
y_sensor = float(-d)
|
|
||||||
z_sensor = float(h + a * d + b * x_sensor + c)
|
|
||||||
|
|
||||||
# Уровень опасности для конкретного объекта
|
# Уровень опасности для конкретного объекта
|
||||||
if decision.emergency and d <= max(decision.stopping_distance, 25.0):
|
if decision.emergency and d <= max(decision.stopping_distance, 25.0):
|
||||||
|
|
@ -219,8 +212,7 @@ def export_frame(decision: Decision, plane: RailPlane | None, corridor: Corridor
|
||||||
|
|
||||||
# Размеры: dx поперёк пути, dy вдоль пути, dz по вертикали
|
# Размеры: dx поперёк пути, dy вдоль пути, dz по вертикали
|
||||||
dx = float(max(obj.width, 0.35))
|
dx = float(max(obj.width, 0.35))
|
||||||
# протяжённость вдоль пути трек не хранит, поэтому она постоянная
|
dy = float(max(obj.size_v if hasattr(obj, "depth") else 0.50, 0.40))
|
||||||
dy = 0.50
|
|
||||||
dz = float(max(obj.size_v, 0.40))
|
dz = float(max(obj.size_v, 0.40))
|
||||||
|
|
||||||
boxes.append(BoundingBox3D(
|
boxes.append(BoundingBox3D(
|
||||||
|
|
|
||||||
|
|
@ -188,8 +188,8 @@ def _process_gpu(r: np.ndarray, valid: np.ndarray, *, r_max: float = 300.0, devi
|
||||||
def process(r: np.ndarray, valid: np.ndarray, *, r_max: float = 300.0,
|
def process(r: np.ndarray, valid: np.ndarray, *, r_max: float = 300.0,
|
||||||
device: str | None = None) -> LaminaOutput:
|
device: str | None = None) -> LaminaOutput:
|
||||||
"""Посчитать ON/OFF-каналы ламины по дальностному образу (автовыбор GPU / CPU)."""
|
"""Посчитать ON/OFF-каналы ламины по дальностному образу (автовыбор GPU / CPU)."""
|
||||||
target_dev = device or "cpu" # без явной просьбы — процессор, как раньше
|
target_dev = device
|
||||||
if target_dev == "auto":
|
if target_dev is None or target_dev == "auto":
|
||||||
from .device import get_device
|
from .device import get_device
|
||||||
target_dev = get_device("auto")
|
target_dev = get_device("auto")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,21 +212,10 @@ def split_by_figure(labels: np.ndarray, n: int, r: np.ndarray,
|
||||||
return out, next_id
|
return out, next_id
|
||||||
|
|
||||||
|
|
||||||
def _core_zone(abs_lat: np.ndarray, d: np.ndarray, core_width: float,
|
|
||||||
core_from: float) -> np.ndarray:
|
|
||||||
"""Где действует пониженный пол: в колее и не ближе core_from."""
|
|
||||||
zone = abs_lat <= core_width
|
|
||||||
if core_from > 0.0:
|
|
||||||
with np.errstate(invalid="ignore"):
|
|
||||||
zone = zone & (d >= core_from)
|
|
||||||
return zone
|
|
||||||
|
|
||||||
|
|
||||||
def gauge_mask(tf: TrackFrame, corridor: Corridor, *, half_width: float,
|
def gauge_mask(tf: TrackFrame, corridor: Corridor, *, half_width: float,
|
||||||
h_lo: float, h_hi: float, d_min: float, d_max: float,
|
h_lo: float, h_hi: float, d_min: float, d_max: float,
|
||||||
k_sigma: float = 0.0, h_lo_core: float = 1e9,
|
k_sigma: float = 0.75, h_lo_core: float = 0.16,
|
||||||
core_width: float = 0.85, core_from: float = 0.0,
|
core_width: float = 0.85, platform_filter: bool = True,
|
||||||
platform_filter: bool = False,
|
|
||||||
platform_u: float = 1.30, platform_h_lo: float = 1.05,
|
platform_u: float = 1.30, platform_h_lo: float = 1.05,
|
||||||
platform_h_hi: float = 1.25):
|
platform_h_hi: float = 1.25):
|
||||||
"""Лучи, попавшие в габарит приближения. Считается один раз на кадр.
|
"""Лучи, попавшие в габарит приближения. Считается один раз на кадр.
|
||||||
|
|
@ -245,11 +234,8 @@ def gauge_mask(tf: TrackFrame, corridor: Corridor, *, half_width: float,
|
||||||
w_eff = half_width
|
w_eff = half_width
|
||||||
|
|
||||||
# Двухзонный пол: в межрельсовой колее (|u| <= core_width) порог h_lo_core (0.16 м),
|
# Двухзонный пол: в межрельсовой колее (|u| <= core_width) порог h_lo_core (0.16 м),
|
||||||
# чтобы не терять лежащего человека или низкие предметы. Опускается он
|
# чтобы не терять лежащего человека или низкие предметы.
|
||||||
# только дальше core_from: вблизи в полосу 0.16…0.28 м попадают головки
|
floor_h = np.where(abs_lat <= core_width, min(h_lo_core, h_lo), h_lo)
|
||||||
# рельсов, и предмет теряется вместе с ними (EXPERIMENTS п. 16).
|
|
||||||
floor_h = np.where(_core_zone(abs_lat, tf.d, core_width, core_from),
|
|
||||||
min(h_lo_core, h_lo), h_lo)
|
|
||||||
|
|
||||||
inside = in_range & (abs_lat < w_eff) & (tf.h > floor_h) & (tf.h < h_hi)
|
inside = in_range & (abs_lat < w_eff) & (tf.h > floor_h) & (tf.h < h_hi)
|
||||||
|
|
||||||
|
|
@ -273,9 +259,8 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
split_top: int = 0,
|
split_top: int = 0,
|
||||||
advance: np.ndarray | None = None,
|
advance: np.ndarray | None = None,
|
||||||
masks: tuple | None = None,
|
masks: tuple | None = None,
|
||||||
k_sigma: float = 0.0, h_lo_core: float = 1e9,
|
k_sigma: float = 0.75, h_lo_core: float = 0.16,
|
||||||
core_width: float = 0.85, core_from: float = 0.0,
|
core_width: float = 0.85, platform_filter: bool = True,
|
||||||
platform_filter: bool = False,
|
|
||||||
platform_u: float = 1.30, platform_h_lo: float = 1.05,
|
platform_u: float = 1.30, platform_h_lo: float = 1.05,
|
||||||
platform_h_hi: float = 1.25) -> list[Candidate]:
|
platform_h_hi: float = 1.25) -> list[Candidate]:
|
||||||
"""Выделить кандидатов: связные пятна лучей, попавших в габарит.
|
"""Выделить кандидатов: связные пятна лучей, попавших в габарит.
|
||||||
|
|
@ -290,8 +275,7 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
masks = gauge_mask(tf, corridor, half_width=half_width, h_lo=h_lo,
|
masks = gauge_mask(tf, corridor, half_width=half_width, h_lo=h_lo,
|
||||||
h_hi=h_hi, d_min=d_min, d_max=d_max,
|
h_hi=h_hi, d_min=d_min, d_max=d_max,
|
||||||
k_sigma=k_sigma, h_lo_core=h_lo_core,
|
k_sigma=k_sigma, h_lo_core=h_lo_core,
|
||||||
core_width=core_width, core_from=core_from,
|
core_width=core_width, platform_filter=platform_filter,
|
||||||
platform_filter=platform_filter,
|
|
||||||
platform_u=platform_u, platform_h_lo=platform_h_lo,
|
platform_u=platform_u, platform_h_lo=platform_h_lo,
|
||||||
platform_h_hi=platform_h_hi)
|
platform_h_hi=platform_h_hi)
|
||||||
inside, lat, in_range = masks
|
inside, lat, in_range = masks
|
||||||
|
|
@ -311,8 +295,7 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
else:
|
else:
|
||||||
w_ctx = half_width + 1.6
|
w_ctx = half_width + 1.6
|
||||||
|
|
||||||
floor_h = np.where(_core_zone(abs_lat, tf.d, core_width, core_from),
|
floor_h = np.where(abs_lat <= core_width, min(h_lo_core, h_lo), h_lo)
|
||||||
min(h_lo_core, h_lo), h_lo)
|
|
||||||
context = (in_range & (abs_lat < w_ctx)
|
context = (in_range & (abs_lat < w_ctx)
|
||||||
& (tf.h > floor_h) & (tf.h < h_hi + ctx_up))
|
& (tf.h > floor_h) & (tf.h < h_hi + ctx_up))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,9 +125,6 @@ class MbonReadout:
|
||||||
k = self.n_active
|
k = self.n_active
|
||||||
chunk = max(1, int(2 ** 26 // max(self.cfg.n_kc, 1)))
|
chunk = max(1, int(2 ** 26 // max(self.cfg.n_kc, 1)))
|
||||||
|
|
||||||
if device == "auto":
|
|
||||||
from .device import get_device
|
|
||||||
device = get_device("auto")
|
|
||||||
if device and device != "cpu":
|
if device and device != "cpu":
|
||||||
try:
|
try:
|
||||||
import torch
|
import torch
|
||||||
|
|
@ -330,8 +327,8 @@ class MbonReadout:
|
||||||
|
|
||||||
Поддерживает прозрачное переключение между NVIDIA GPU (CUDA) и CPU.
|
Поддерживает прозрачное переключение между NVIDIA GPU (CUDA) и CPU.
|
||||||
"""
|
"""
|
||||||
target_dev = device or "cpu" # без явной просьбы — процессор, как раньше
|
target_dev = device
|
||||||
if target_dev == "auto":
|
if target_dev is None or target_dev == "auto":
|
||||||
from .device import get_device
|
from .device import get_device
|
||||||
target_dev = get_device("auto")
|
target_dev = get_device("auto")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,8 +213,8 @@ class MushroomBody:
|
||||||
для набора сразу эквивалентно возведению в степень по числу попаданий, —
|
для набора сразу эквивалентно возведению в степень по числу попаданий, —
|
||||||
поэтому цикл по примерам не нужен.
|
поэтому цикл по примерам не нужен.
|
||||||
"""
|
"""
|
||||||
target_dev = device or "cpu" # без явной просьбы — процессор, как раньше
|
target_dev = device
|
||||||
if target_dev == "auto":
|
if target_dev is None or target_dev == "auto":
|
||||||
from .device import get_device
|
from .device import get_device
|
||||||
target_dev = get_device("auto")
|
target_dev = get_device("auto")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,23 +36,10 @@ class Params:
|
||||||
fov_deg: float = 30.0 # полусектор обработки по азимуту
|
fov_deg: float = 30.0 # полусектор обработки по азимуту
|
||||||
half_width: float = 1.6 # полуширина габарита, м
|
half_width: float = 1.6 # полуширина габарита, м
|
||||||
h_lo: float = 0.28 # нижняя граница габарита над рельсом, м
|
h_lo: float = 0.28 # нижняя граница габарита над рельсом, м
|
||||||
# Три правки габарита (Zhirik1337, 22.09), каждая мерилась отдельно на
|
h_lo_core: float = 0.16 # нижняя граница в колее (|u| <= core_width)
|
||||||
# двух сценах и полигоне (EXPERIMENTS п. 16):
|
core_width: float = 0.85 # полуширина междупутья/колеи, м
|
||||||
# h_lo_core — пол в колее: между рельсами (|u| ≤ core_width) нижняя
|
k_sigma: float = 0.75 # расширение габарита по неопределённости оси
|
||||||
# граница опускается, чтобы не терять лежащего человека.
|
platform_filter: bool = True # отсекать настил платформы от сшивки с предметом
|
||||||
# ПРИНЯТО, но только дальше core_from: ближе в полосу
|
|
||||||
# 0.16…0.28 м попадают головки рельсов, рельс собирается
|
|
||||||
# в одну компоненту от кабины, и предмет выбрасывается
|
|
||||||
# вместе с ней. Лежачий P@50 0.27 → 0.50;
|
|
||||||
# k_sigma — габарит расширяется там, где ось пути известна хуже
|
|
||||||
# (в кривых вдали); 0 — не расширять. Отвергнуто;
|
|
||||||
# platform_filter — вырезать из габарита настил платформы. Отвергнуто:
|
|
||||||
# в тоннеле без платформы режет стену на обрывки.
|
|
||||||
h_lo_core: float = 0.16
|
|
||||||
core_width: float = 0.85 # полуширина колеи, м
|
|
||||||
core_from: float = 30.0 # ближе этой дальности пол в колее не опускается, м
|
|
||||||
k_sigma: float = 0.0
|
|
||||||
platform_filter: bool = False
|
|
||||||
h_hi: float = 2.3 # верхняя граница, м
|
h_hi: float = 2.3 # верхняя граница, м
|
||||||
ctx_up: float = 4.0 # насколько контекст лезет выше габарита, м
|
ctx_up: float = 4.0 # насколько контекст лезет выше габарита, м
|
||||||
# Разделение фигуры и фона по движению (см. lobula.split_by_figure):
|
# Разделение фигуры и фона по движению (см. lobula.split_by_figure):
|
||||||
|
|
@ -124,16 +111,6 @@ class Params:
|
||||||
# Считывание по треку (см. track_readout.py): геометрическое смешивание
|
# Считывание по треку (см. track_readout.py): геометрическое смешивание
|
||||||
# обученного отсчёта с уликой перед сравнением с порогом. 0 — решает
|
# обученного отсчёта с уликой перед сравнением с порогом. 0 — решает
|
||||||
# только улика, как раньше. Замер — EXPERIMENTS п. 15.
|
# только улика, как раньше. Замер — EXPERIMENTS п. 15.
|
||||||
# Правки решения (Zhirik1337, 22.09), обе выключены по замеру (п. 16):
|
|
||||||
# lying_exempt — не штрафовать за вытянутость низкий предмет в колее
|
|
||||||
# (лежащий человек вытянут вдоль пути, как конструкция).
|
|
||||||
# Правит ручную формулу, а при mbon_blend = 1 вес целиком
|
|
||||||
# от модели — не действует;
|
|
||||||
# far_channel — дальний канал предупреждения в обход порога. Выигрыш
|
|
||||||
# настоящий, но та же задача, что warn_far, втрое дороже:
|
|
||||||
# +3.0 и +7.1 ложных трека на км на двух сценах.
|
|
||||||
lying_exempt: bool = False
|
|
||||||
far_channel: bool = False
|
|
||||||
track_blend: float = 0.0
|
track_blend: float = 0.0
|
||||||
track_gate: float = 0.0 # жёсткий порог по тому же отсчёту
|
track_gate: float = 0.0 # жёсткий порог по тому же отсчёту
|
||||||
# Чем считать «насколько это предмет» по треку: `w_mean` — средний вес
|
# Чем считать «насколько это предмет» по треку: `w_mean` — средний вес
|
||||||
|
|
@ -152,18 +129,9 @@ class Params:
|
||||||
# (3.3 на км в обоих случаях). Замер — EXPERIMENTS п. 11.3.
|
# (3.3 на км в обоих случаях). Замер — EXPERIMENTS п. 11.3.
|
||||||
min_hits: int = 2
|
min_hits: int = 2
|
||||||
novelty_floor: float = 0.10
|
novelty_floor: float = 0.10
|
||||||
# Порог тревоги, линейно опускаемый с 0.5 до `warn_far` на отрезке от
|
warn_far: float = 0.35 # порог тревоги на дальнем краю (релаксация для дальней зоны)
|
||||||
# `warn_far_from` до 200 м. В п. 12.2 ровно эта правка была отвергнута:
|
warn_far_from: float = 90.0 # с какой дальности он начинает падать
|
||||||
# тогда улика далёкого трека колебалась около нуля, и опускать порог под
|
leak_far: float = 0.06 # утечка улики на дальнем краю (компенсирует прореживание лучей)
|
||||||
# нулевым сигналом было бессмысленно. После гашения знакомости и порога
|
|
||||||
# по лучам улика перестала быть нулём, и та же правка дала P@150
|
|
||||||
# 0.33 → 0.37 без потерь на 50 и 100 м ценой нуля ложных треков на
|
|
||||||
# знакомой линии и одного на незнакомой. Экстренного торможения она не
|
|
||||||
# касается: у него свой порог и условие d ≤ max(тормозной путь, 25 м).
|
|
||||||
# EXPERIMENTS п. 15.9.
|
|
||||||
warn_far: float = 0.30
|
|
||||||
warn_far_from: float = 90.0
|
|
||||||
leak_far: float = 0.12 # утечка улики на дальнем краю; равна leak — выключено
|
|
||||||
leak_far_from: float = 90.0 # с какой дальности она начинает падать
|
leak_far_from: float = 90.0 # с какой дальности она начинает падать
|
||||||
d_min: float = 4.0
|
d_min: float = 4.0
|
||||||
d_max: float = 220.0
|
d_max: float = 220.0
|
||||||
|
|
@ -179,11 +147,7 @@ class Params:
|
||||||
min_rays_far_from: float = 90.0
|
min_rays_far_from: float = 90.0
|
||||||
calib_frames: int = 12
|
calib_frames: int = 12
|
||||||
novelty_gate: float = 0.0 # ниже этой новизны кандидат отбрасывается сразу
|
novelty_gate: float = 0.0 # ниже этой новизны кандидат отбрасывается сразу
|
||||||
# Где считать ламину: 'cpu', 'cuda' или 'auto' (видеокарта, если есть).
|
device: str = "auto" # вычислительное устройство: 'auto' (NVIDIA GPU при наличии), 'cuda', 'cpu'
|
||||||
# По умолчанию процессор, и это решение, а не осторожность: GPU ускоряет
|
|
||||||
# только ламину (5.8 → 2.9 мс из 43), а на машине проверки требует
|
|
||||||
# другого контейнера и запуска с --gpus all. EXPERIMENTS п. 7.4.
|
|
||||||
device: str = "cpu"
|
|
||||||
enable_motion: bool = True
|
enable_motion: bool = True
|
||||||
enable_memory: bool = True
|
enable_memory: bool = True
|
||||||
enable_looming: bool = False # T4/T5 + LPLC2: нужны для оценки надвигания и для
|
enable_looming: bool = False # T4/T5 + LPLC2: нужны для оценки надвигания и для
|
||||||
|
|
@ -254,8 +218,7 @@ class FlyGuard:
|
||||||
leak_far=self.p.leak_far,
|
leak_far=self.p.leak_far,
|
||||||
leak_far_from=self.p.leak_far_from,
|
leak_far_from=self.p.leak_far_from,
|
||||||
nov_fade_from=self.p.nov_fade_from,
|
nov_fade_from=self.p.nov_fade_from,
|
||||||
nov_fade_to=self.p.nov_fade_to,
|
nov_fade_to=self.p.nov_fade_to)
|
||||||
lying_exempt=self.p.lying_exempt)
|
|
||||||
# без накопления улик трек подтверждается первым же наблюдением
|
# без накопления улик трек подтверждается первым же наблюдением
|
||||||
self.dn = (DescendingNeurons(
|
self.dn = (DescendingNeurons(
|
||||||
warn_evidence=self.p.warn_evidence,
|
warn_evidence=self.p.warn_evidence,
|
||||||
|
|
@ -268,8 +231,7 @@ class FlyGuard:
|
||||||
track_blend=self.p.track_blend,
|
track_blend=self.p.track_blend,
|
||||||
track_gate=self.p.track_gate,
|
track_gate=self.p.track_gate,
|
||||||
track_score=self.p.track_score,
|
track_score=self.p.track_score,
|
||||||
track_readout=track_readout,
|
track_readout=track_readout) if self.p.use_tracking
|
||||||
far_channel=self.p.far_channel) if self.p.use_tracking
|
|
||||||
else DescendingNeurons(warn_evidence=0.0, clear_evidence=0.0,
|
else DescendingNeurons(warn_evidence=0.0, clear_evidence=0.0,
|
||||||
emergency_evidence=0.0, min_hits=1))
|
emergency_evidence=0.0, min_hits=1))
|
||||||
self._calib: list[PointCloud2] = []
|
self._calib: list[PointCloud2] = []
|
||||||
|
|
@ -384,7 +346,6 @@ class FlyGuard:
|
||||||
k_sigma=self.p.k_sigma,
|
k_sigma=self.p.k_sigma,
|
||||||
h_lo_core=self.p.h_lo_core,
|
h_lo_core=self.p.h_lo_core,
|
||||||
core_width=self.p.core_width,
|
core_width=self.p.core_width,
|
||||||
core_from=self.p.core_from,
|
|
||||||
platform_filter=self.p.platform_filter)
|
platform_filter=self.p.platform_filter)
|
||||||
cands = find_candidates(tf, lam, self.corridor, masks=masks,
|
cands = find_candidates(tf, lam, self.corridor, masks=masks,
|
||||||
half_width=self.p.half_width, h_lo=self.p.h_lo,
|
half_width=self.p.half_width, h_lo=self.p.h_lo,
|
||||||
|
|
@ -401,7 +362,6 @@ class FlyGuard:
|
||||||
k_sigma=self.p.k_sigma,
|
k_sigma=self.p.k_sigma,
|
||||||
h_lo_core=self.p.h_lo_core,
|
h_lo_core=self.p.h_lo_core,
|
||||||
core_width=self.p.core_width,
|
core_width=self.p.core_width,
|
||||||
core_from=self.p.core_from,
|
|
||||||
platform_filter=self.p.platform_filter)
|
platform_filter=self.p.platform_filter)
|
||||||
|
|
||||||
with t("mushroom"):
|
with t("mushroom"):
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ numpy>=1.24
|
||||||
scipy>=1.10
|
scipy>=1.10
|
||||||
# только для контрольного бустинга в tools/train_mbon.py --baseline
|
# только для контрольного бустинга в tools/train_mbon.py --baseline
|
||||||
lightgbm>=4.0
|
lightgbm>=4.0
|
||||||
# графики полигона: tools/plot_benchmark.py
|
|
||||||
matplotlib>=3.7
|
matplotlib>=3.7
|
||||||
# только для обучения на GPU (tools/train_mbon.py --device cuda); ядру не нужен
|
# только для обучения на GPU (tools/train_mbon.py --device cuda); ядру не нужен
|
||||||
# torch>=2.0
|
# torch>=2.0
|
||||||
|
|
|
||||||
|
|
@ -592,20 +592,6 @@ def test_gauge_mask_zonal_floor_and_corridor_expansion():
|
||||||
assert inside_exp[0]
|
assert inside_exp[0]
|
||||||
|
|
||||||
|
|
||||||
def test_core_floor_is_not_lowered_closer_than_core_from():
|
|
||||||
"""Вблизи опущенный пол открывает головки рельсов — там он не действует."""
|
|
||||||
from flyguard.geometry import STRAIGHT
|
|
||||||
from flyguard.lobula import gauge_mask
|
|
||||||
|
|
||||||
tf = _MockTrackFrame(d=[20.0, 60.0, 60.0], u=[0.4, 0.4, 1.2], h=[0.20, 0.20, 0.20])
|
|
||||||
inside, _, _ = gauge_mask(tf, STRAIGHT, half_width=1.6, h_lo=0.28, h_hi=2.3,
|
|
||||||
d_min=4.0, d_max=220.0, h_lo_core=0.16,
|
|
||||||
core_width=0.85, core_from=55.0)
|
|
||||||
assert not inside[0] # 20 м — ближе порога, пол прежний
|
|
||||||
assert inside[1] # 60 м в колее — пол опущен
|
|
||||||
assert not inside[2] # вне колеи пол не опускается нигде
|
|
||||||
|
|
||||||
|
|
||||||
def test_gauge_mask_filters_platform_surface():
|
def test_gauge_mask_filters_platform_surface():
|
||||||
"""Настил платформы (h~1.1-1.2 м, |u| >= 1.30 м) отсекается, а препятствие в габарите остаётся."""
|
"""Настил платформы (h~1.1-1.2 м, |u| >= 1.30 м) отсекается, а препятствие в габарите остаётся."""
|
||||||
from flyguard.geometry import STRAIGHT
|
from flyguard.geometry import STRAIGHT
|
||||||
|
|
@ -636,8 +622,7 @@ def test_descending_far_soft_warning_triggers_without_emergency():
|
||||||
)
|
)
|
||||||
cx.tracks = [far_track]
|
cx.tracks = [far_track]
|
||||||
|
|
||||||
dn = DescendingNeurons(warn_evidence=0.5, warn_far=0.35, warn_far_from=90.0,
|
dn = DescendingNeurons(warn_evidence=0.5, warn_far=0.35, warn_far_from=90.0)
|
||||||
far_channel=True)
|
|
||||||
decision = dn.decide(cx, speed=15.0)
|
decision = dn.decide(cx, speed=15.0)
|
||||||
|
|
||||||
assert decision.detected is True
|
assert decision.detected is True
|
||||||
|
|
@ -689,6 +674,7 @@ def test_export_frame_markers_and_threat_levels():
|
||||||
|
|
||||||
def test_benchmark_augmentation_logic():
|
def test_benchmark_augmentation_logic():
|
||||||
"""Проверка генерации сценариев и логики аугментаций в make_benchmark."""
|
"""Проверка генерации сценариев и логики аугментаций в make_benchmark."""
|
||||||
|
sys.path.insert(0, str(ROOT / "tools"))
|
||||||
from make_benchmark import _ScenarioState
|
from make_benchmark import _ScenarioState
|
||||||
from flyguard.pipeline import FlyGuard, Params
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
|
|
||||||
|
|
@ -763,7 +749,7 @@ def test_device_runtime_failure_and_fallback():
|
||||||
|
|
||||||
# Симуляция критического сбоя GPU
|
# Симуляция критического сбоя GPU
|
||||||
notify_cuda_error(RuntimeError("Simulated CUDA device disconnect / OOM"))
|
notify_cuda_error(RuntimeError("Simulated CUDA device disconnect / OOM"))
|
||||||
try:
|
|
||||||
assert not is_cuda_available()
|
assert not is_cuda_available()
|
||||||
assert get_device("cuda") == "cpu"
|
assert get_device("cuda") == "cpu"
|
||||||
assert get_device("auto") == "cpu"
|
assert get_device("auto") == "cpu"
|
||||||
|
|
@ -776,53 +762,7 @@ def test_device_runtime_failure_and_fallback():
|
||||||
out = lamina.process(r, valid, device="cuda")
|
out = lamina.process(r, valid, device="cuda")
|
||||||
assert out.on.shape == (16, 32)
|
assert out.on.shape == (16, 32)
|
||||||
assert out.on[8, 16] > 0.0
|
assert out.on[8, 16] > 0.0
|
||||||
finally:
|
|
||||||
|
# Восстановление кэша
|
||||||
reset_device_cache()
|
reset_device_cache()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_scenario_stream_does_not_depend_on_the_rest_of_the_catalogue():
|
|
||||||
"""Новый предмет в каталоге не сдвигает случайность остальных сценариев.
|
|
||||||
|
|
||||||
Раньше генератор был один на запись, и сценарии тянули из него по очереди:
|
|
||||||
добавили человека лёжа — поменялись цифры по ящику. Теперь поток сценария
|
|
||||||
определяется только зерном, именем предмета и поперечным смещением.
|
|
||||||
"""
|
|
||||||
from make_benchmark import scenario_rng
|
|
||||||
|
|
||||||
a = scenario_rng(12345, "ящик", 0.9).random(5)
|
|
||||||
assert np.array_equal(a, scenario_rng(12345, "ящик", 0.9).random(5))
|
|
||||||
assert not np.array_equal(a, scenario_rng(12345, "ящик", 0.0).random(5))
|
|
||||||
assert not np.array_equal(a, scenario_rng(12345, "чемодан", 0.9).random(5))
|
|
||||||
assert not np.array_equal(a, scenario_rng(777, "ящик", 0.9).random(5))
|
|
||||||
|
|
||||||
|
|
||||||
def test_export_box_follows_a_curved_track():
|
|
||||||
"""В кривой рамка стоит на пути и повёрнута вдоль него.
|
|
||||||
|
|
||||||
Боковое смещение трека отсчитано от оси пути, и без поправки на её
|
|
||||||
положение рамка на 150 м при радиусе 1300 м стояла в 8.6 м от пути, а
|
|
||||||
поворот был зеркальным — 13° мимо направления пути.
|
|
||||||
"""
|
|
||||||
import math
|
|
||||||
from flyguard.descending import Decision, DetectedObject
|
|
||||||
from flyguard.export import export_frame
|
|
||||||
from flyguard.geometry import Corridor, RailPlane
|
|
||||||
|
|
||||||
R = 1300.0
|
|
||||||
cor = Corridor(np.array([0.0, 0.0, 1.0 / (2 * R)], np.float32), 200.0, 10, 1600.0)
|
|
||||||
plane = RailPlane(a=0.0, b=0.0, c=-1.8, inliers=100, rms=0.02)
|
|
||||||
for d in (55.0, 100.0, 150.0):
|
|
||||||
dec = Decision(detected=True, distance=d, speed=10.0, stopping_distance=30.0,
|
|
||||||
objects=[DetectedObject(distance=d, lateral=0.3, height=0.8,
|
|
||||||
width=0.5, size_v=1.7, confidence=0.9,
|
|
||||||
novelty=0.6, n_rays=10, track_id=1,
|
|
||||||
ttc=d / 10)])
|
|
||||||
box = export_frame(dec, plane, cor).boxes[0]
|
|
||||||
on_track = float(cor.centre(np.array([d], np.float32))[0]) + 0.3
|
|
||||||
assert box.x == pytest.approx(on_track, abs=1e-3)
|
|
||||||
|
|
||||||
slope = d / R
|
|
||||||
long_axis = np.array([-math.sin(box.yaw), math.cos(box.yaw)])
|
|
||||||
tangent = np.array([slope, -1.0]) / math.hypot(slope, 1.0)
|
|
||||||
assert abs(float(long_axis @ tangent)) == pytest.approx(1.0, abs=1e-6)
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
"""Общий ключ `--set имя=значение` для любых полей `Params`.
|
|
||||||
|
|
||||||
У `evaluate.py` и `make_benchmark.py` уже по два десятка отдельных флагов, по
|
|
||||||
одному на каждую настройку конвейера, и каждый новый ключ в `Params` требовал
|
|
||||||
ещё двух правок в каждом инструменте. Через `--set` любое поле меняется без
|
|
||||||
правки инструментов: `--set h_lo_core=0.16 --set lying_exempt=1`.
|
|
||||||
|
|
||||||
Тип берётся из описания поля, поэтому опечатка в имени или в значении падает
|
|
||||||
сразу, с понятным сообщением, а не тихо превращается в строку.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import dataclasses
|
|
||||||
|
|
||||||
_TRUE = {"1", "true", "yes", "on", "да"}
|
|
||||||
_FALSE = {"0", "false", "no", "off", "нет"}
|
|
||||||
|
|
||||||
|
|
||||||
def add_argument(ap) -> None:
|
|
||||||
ap.add_argument("--set", action="append", default=[], metavar="ИМЯ=ЗНАЧЕНИЕ",
|
|
||||||
help="поменять любое поле Params; можно несколько раз")
|
|
||||||
|
|
||||||
|
|
||||||
def apply(over: dict, sets: list[str], params_cls) -> dict:
|
|
||||||
"""Дописать в `over` значения из `--set`, приведя их к типу поля."""
|
|
||||||
types = {f.name: str(f.type) for f in dataclasses.fields(params_cls)}
|
|
||||||
for item in sets:
|
|
||||||
if "=" not in item:
|
|
||||||
raise SystemExit(f"--set ждёт имя=значение, а получил: {item!r}")
|
|
||||||
name, raw = (s.strip() for s in item.split("=", 1))
|
|
||||||
if name not in types:
|
|
||||||
near = sorted(n for n in types if name.split("_")[0] in n)[:6]
|
|
||||||
raise SystemExit(f"в Params нет поля {name!r}"
|
|
||||||
+ (f"; похожие: {', '.join(near)}" if near else ""))
|
|
||||||
kind = types[name]
|
|
||||||
if kind == "bool":
|
|
||||||
low = raw.lower()
|
|
||||||
if low not in _TRUE | _FALSE:
|
|
||||||
raise SystemExit(f"{name}: ждал да/нет, а получил {raw!r}")
|
|
||||||
over[name] = low in _TRUE
|
|
||||||
elif kind == "int":
|
|
||||||
over[name] = int(raw)
|
|
||||||
elif kind == "float":
|
|
||||||
over[name] = float(raw)
|
|
||||||
else:
|
|
||||||
over[name] = raw
|
|
||||||
return over
|
|
||||||
|
|
@ -12,7 +12,6 @@ import argparse
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import _bootstrap as B # noqa: F401
|
import _bootstrap as B # noqa: F401
|
||||||
import _params as PS
|
|
||||||
from _metrics import auc
|
from _metrics import auc
|
||||||
from flyguard.bag import Bag
|
from flyguard.bag import Bag
|
||||||
from flyguard.mushroom_body import MushroomBody
|
from flyguard.mushroom_body import MushroomBody
|
||||||
|
|
@ -30,7 +29,6 @@ def main() -> None:
|
||||||
"не видевшую этот бэг: artifacts/mbon_folds/mbon_roundT_doubleT.npz")
|
"не видевшую этот бэг: artifacts/mbon_folds/mbon_roundT_doubleT.npz")
|
||||||
ap.add_argument("--limit", type=int, default=200)
|
ap.add_argument("--limit", type=int, default=200)
|
||||||
ap.add_argument("--verbose", action="store_true")
|
ap.add_argument("--verbose", action="store_true")
|
||||||
PS.add_argument(ap)
|
|
||||||
ap.add_argument("--tracks", action="store_true",
|
ap.add_argument("--tracks", action="store_true",
|
||||||
help="сравнить признаки трека у настоящего объекта и у "
|
help="сравнить признаки трека у настоящего объекта и у "
|
||||||
"ложных треков: единственная проверка, где метка "
|
"ложных треков: единственная проверка, где метка "
|
||||||
|
|
@ -43,8 +41,7 @@ def main() -> None:
|
||||||
from flyguard.mbon_readout import MbonReadout
|
from flyguard.mbon_readout import MbonReadout
|
||||||
readout = MbonReadout.load(args.mbon)
|
readout = MbonReadout.load(args.mbon)
|
||||||
print(f"считывание MBON: {args.mbon}")
|
print(f"считывание MBON: {args.mbon}")
|
||||||
fg = FlyGuard(Params(**PS.apply({}, args.set, Params)), memory=memory,
|
fg = FlyGuard(Params(), memory=memory, readout=readout)
|
||||||
readout=readout)
|
|
||||||
bag = Bag(args.bag)
|
bag = Bag(args.bag)
|
||||||
|
|
||||||
n = cand_hit = track_hit = other = 0
|
n = cand_hit = track_hit = other = 0
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
"""Парное сравнение двух прогонов полигона на одних и тех же вставках.
|
|
||||||
|
|
||||||
Полигон шумит сильнее, чем меняет его большинство правок: две реализации
|
|
||||||
случайности при одинаковых настройках расходятся на P@150 на 0.07, на P@100
|
|
||||||
у ящика — на 0.11 (EXPERIMENTS п. 16.1). Поэтому сравнивать итоговые таблицы
|
|
||||||
двух прогонов нельзя.
|
|
||||||
|
|
||||||
Но у каждого сценария свой генератор (`make_benchmark.scenario_rng`), и от
|
|
||||||
решений конвейера он не зависит: при другой настройке на вход идут те же
|
|
||||||
кадры с теми же вставками, до луча. Значит, разница двух прогонов — только
|
|
||||||
от настройки, и её видно по отдельным наблюдениям: сколько было 0 и стало 1
|
|
||||||
и наоборот. Перевороты в одну сторону — настоящий эффект, даже когда он
|
|
||||||
меньше межзернового шума; поровну в обе стороны — шум.
|
|
||||||
|
|
||||||
python tools/compare_benchmark.py artifacts/benchmark.json new.json
|
|
||||||
python tools/compare_benchmark.py a.json b.json --objects человек_лежа,ящик
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
MIN_VISIBLE_RAYS = 2 # как в plot_benchmark: меньше — предмет не освещён
|
|
||||||
|
|
||||||
|
|
||||||
def load(path: str) -> dict:
|
|
||||||
with open(path, encoding="utf-8") as f:
|
|
||||||
return {(r["bag"], r["obj"], r["lateral"]): r for r in json.load(f)}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
|
||||||
ap.add_argument("before", help="прогон «было» (json из make_benchmark)")
|
|
||||||
ap.add_argument("after", help="прогон «стало»")
|
|
||||||
ap.add_argument("--objects", default="",
|
|
||||||
help="через запятую; пусто — все предметы")
|
|
||||||
ap.add_argument("--edges", default="0,15,40,90,130,170,230",
|
|
||||||
help="границы полос дальности, м")
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
a, b = load(args.before), load(args.after)
|
|
||||||
if set(a) != set(b):
|
|
||||||
raise SystemExit("разный состав сценариев — прогоны не парные")
|
|
||||||
edges = [float(x) for x in args.edges.split(",")]
|
|
||||||
objs = ([o for o in args.objects.split(",") if o] if args.objects
|
|
||||||
else sorted({k[1] for k in a}))
|
|
||||||
|
|
||||||
print(f"{'предмет':14s}{'полоса':>12}{'набл.':>7}{'было':>7}{'стало':>7}"
|
|
||||||
f"{'0→1':>6}{'1→0':>6}")
|
|
||||||
total_up = total_down = 0
|
|
||||||
for obj in objs:
|
|
||||||
obj_up = obj_down = 0
|
|
||||||
for lo, hi in zip(edges[:-1], edges[1:]):
|
|
||||||
n = ha = hb = up = down = 0
|
|
||||||
for k, ra in a.items():
|
|
||||||
if k[1] != obj:
|
|
||||||
continue
|
|
||||||
rb = b[k]
|
|
||||||
da = np.asarray(ra["d"])
|
|
||||||
if da.shape != np.shape(rb["d"]) or not np.allclose(da, rb["d"]):
|
|
||||||
raise SystemExit(f"вход разошёлся в {k}: прогоны не парные")
|
|
||||||
m = (da >= lo) & (da < hi) & (np.asarray(ra["rays"]) >= MIN_VISIBLE_RAYS)
|
|
||||||
x, y = np.asarray(ra["hit"])[m], np.asarray(rb["hit"])[m]
|
|
||||||
n += int(m.sum())
|
|
||||||
ha += int(x.sum())
|
|
||||||
hb += int(y.sum())
|
|
||||||
up += int(((x == 0) & (y == 1)).sum())
|
|
||||||
down += int(((x == 1) & (y == 0)).sum())
|
|
||||||
obj_up += up
|
|
||||||
obj_down += down
|
|
||||||
if n >= 20:
|
|
||||||
print(f"{obj:14s}{f'{lo:.0f}–{hi:.0f} м':>12}{n:7d}{ha / n:7.2f}"
|
|
||||||
f"{hb / n:7.2f}{up:6d}{down:6d}")
|
|
||||||
print(f"{obj:14s}{'всего':>12}{'':21s}{obj_up:6d}{obj_down:6d}")
|
|
||||||
total_up += obj_up
|
|
||||||
total_down += obj_down
|
|
||||||
print(f"\nвсе предметы: 0→1 {total_up}, 1→0 {total_down}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -21,7 +21,6 @@ import numpy as np
|
||||||
|
|
||||||
import _bootstrap as B # noqa: F401
|
import _bootstrap as B # noqa: F401
|
||||||
import _parallel as P
|
import _parallel as P
|
||||||
import _params as PS
|
|
||||||
from flyguard.bag import Bag, find_bags
|
from flyguard.bag import Bag, find_bags
|
||||||
from flyguard.mushroom_body import MushroomBody, MushroomBodyConfig
|
from flyguard.mushroom_body import MushroomBody, MushroomBodyConfig
|
||||||
from flyguard.pipeline import FlyGuard, Params
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
|
|
@ -173,7 +172,6 @@ def main() -> None:
|
||||||
help="совсем без памяти тоннеля — так выглядит первый проезд по новой линии")
|
help="совсем без памяти тоннеля — так выглядит первый проезд по новой линии")
|
||||||
ap.add_argument("--out", default=str(B.ARTIFACTS / "generalisation.json"))
|
ap.add_argument("--out", default=str(B.ARTIFACTS / "generalisation.json"))
|
||||||
P.add_argument(ap)
|
P.add_argument(ap)
|
||||||
PS.add_argument(ap)
|
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
d = np.load(args.cache, allow_pickle=True)
|
d = np.load(args.cache, allow_pickle=True)
|
||||||
|
|
@ -238,7 +236,7 @@ def main() -> None:
|
||||||
over["track_score"] = args.track_score
|
over["track_score"] = args.track_score
|
||||||
if args.track_gate is not None:
|
if args.track_gate is not None:
|
||||||
over["track_gate"] = args.track_gate
|
over["track_gate"] = args.track_gate
|
||||||
params = Params(**PS.apply(over, args.set, Params))
|
params = Params(**over)
|
||||||
fold_paths: dict[str, str] = {}
|
fold_paths: dict[str, str] = {}
|
||||||
if args.mbon_dir:
|
if args.mbon_dir:
|
||||||
from pathlib import Path as _P
|
from pathlib import Path as _P
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import zlib
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import _bootstrap as B # noqa: F401
|
import _bootstrap as B # noqa: F401
|
||||||
import _parallel as P
|
import _parallel as P
|
||||||
import _params as PS
|
|
||||||
from flyguard.bag import Bag, find_bags
|
from flyguard.bag import Bag, find_bags
|
||||||
from flyguard.mushroom_body import MushroomBody
|
from flyguard.mushroom_body import MushroomBody
|
||||||
from flyguard.pipeline import FlyGuard, Params
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
|
|
@ -34,21 +32,6 @@ from flyguard.track_readout import describe_track
|
||||||
HOLDOUT = "doubleT_obstacle" # там уже есть настоящий объект
|
HOLDOUT = "doubleT_obstacle" # там уже есть настоящий объект
|
||||||
|
|
||||||
|
|
||||||
def scenario_rng(seed: int, name: str, lateral: float) -> np.random.Generator:
|
|
||||||
"""Свой генератор на каждый сценарий.
|
|
||||||
|
|
||||||
Раньше генератор был один на запись, и сценарии тянули из него по очереди,
|
|
||||||
кадр за кадром. Тогда любой новый предмет в каталоге сдвигал поток для ВСЕХ
|
|
||||||
остальных: цифры по ящику менялись оттого, что где-то добавили человека
|
|
||||||
лёжа. Теперь поток сценария зависит только от него самого.
|
|
||||||
|
|
||||||
Зерно берётся через crc32, а не через `hash()`: встроенный хэш строк
|
|
||||||
солится заново в каждом процессе, и воспроизводимость пропала бы.
|
|
||||||
"""
|
|
||||||
return np.random.default_rng(
|
|
||||||
[seed, zlib.crc32(name.encode("utf-8")), int(round(lateral * 1000)) & 0xFFFF])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _ScenarioState:
|
class _ScenarioState:
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -59,7 +42,6 @@ class _ScenarioState:
|
||||||
is_static: bool
|
is_static: bool
|
||||||
s_start: float = 0.0
|
s_start: float = 0.0
|
||||||
t0: float | None = None
|
t0: float | None = None
|
||||||
rng: np.random.Generator | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def _work(task):
|
def _work(task):
|
||||||
|
|
@ -125,6 +107,7 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
|
|
||||||
cat = catalogue()
|
cat = catalogue()
|
||||||
scen = [(name, lat) for name in cat for lat in laterals]
|
scen = [(name, lat) for name in cat for lat in laterals]
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
records = [[] for _ in scen]
|
records = [[] for _ in scen]
|
||||||
# Выборка для считывания по трекам: описание каждого живого трека на
|
# Выборка для считывания по трекам: описание каждого живого трека на
|
||||||
# каждом кадре и метка «это вставленный предмет». Собирается здесь, а не
|
# каждом кадре и метка «это вставленный предмет». Собирается здесь, а не
|
||||||
|
|
@ -133,10 +116,7 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
# пороги. Обученное на другой обстановке считывание нечего и мерить.
|
# пороги. Обученное на другой обстановке считывание нечего и мерить.
|
||||||
trk_rows: list = []
|
trk_rows: list = []
|
||||||
|
|
||||||
def _create_sc(name: str, lat: float, rng=None) -> _ScenarioState:
|
def _create_sc(name: str, lat: float) -> _ScenarioState:
|
||||||
# при повторном появлении поток продолжается, а не начинается заново:
|
|
||||||
# иначе каждое возрождение повторяло бы одну и ту же дальность и дрейф
|
|
||||||
rng = rng if rng is not None else scenario_rng(seed, name, lat)
|
|
||||||
p = FlyGuard(params, memory=memory, readout=readout, track_readout=track_readout)
|
p = FlyGuard(params, memory=memory, readout=readout, track_readout=track_readout)
|
||||||
if use_augment:
|
if use_augment:
|
||||||
d_init = float(rng.uniform(d_start_min, d_start_max))
|
d_init = float(rng.uniform(d_start_min, d_start_max))
|
||||||
|
|
@ -147,7 +127,7 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
d_init = d_start
|
d_init = d_start
|
||||||
is_static = False
|
is_static = False
|
||||||
v_lat = 0.0
|
v_lat = 0.0
|
||||||
return _ScenarioState(name=name, lat_base=lat, pipe=p, rng=rng, d_init=d_init,
|
return _ScenarioState(name=name, lat_base=lat, pipe=p, d_init=d_init,
|
||||||
v_lat=v_lat, is_static=is_static)
|
v_lat=v_lat, is_static=is_static)
|
||||||
|
|
||||||
scenarios = [_create_sc(name, lat) for name, lat in scen]
|
scenarios = [_create_sc(name, lat) for name, lat in scen]
|
||||||
|
|
@ -167,15 +147,15 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
sc.s_start = s_now
|
sc.s_start = s_now
|
||||||
dt = max(pc.stamp - sc.t0, 0.0)
|
dt = max(pc.stamp - sc.t0, 0.0)
|
||||||
ds = 0.0 if sc.is_static else (s_now - sc.s_start)
|
ds = 0.0 if sc.is_static else (s_now - sc.s_start)
|
||||||
noise_s = float(np.clip(sc.rng.normal(0.0, s_noise_std), -0.35, 0.35)) if (use_augment and s_noise_std > 0) else 0.0
|
noise_s = float(np.clip(rng.normal(0.0, s_noise_std), -0.35, 0.35)) if (use_augment and s_noise_std > 0) else 0.0
|
||||||
d_true = sc.d_init - ds + noise_s
|
d_true = sc.d_init - ds + noise_s
|
||||||
if d_true < 6.0:
|
if d_true < 6.0:
|
||||||
if respawn and use_augment:
|
if respawn and use_augment:
|
||||||
scenarios[i] = _create_sc(sc.name, sc.lat_base, sc.rng)
|
scenarios[i] = _create_sc(sc.name, sc.lat_base)
|
||||||
sc = scenarios[i]
|
sc = scenarios[i]
|
||||||
sc.t0 = pc.stamp
|
sc.t0 = pc.stamp
|
||||||
sc.s_start = s_now
|
sc.s_start = s_now
|
||||||
noise_s = float(np.clip(sc.rng.normal(0.0, s_noise_std), -0.35, 0.35)) if s_noise_std > 0 else 0.0
|
noise_s = float(np.clip(rng.normal(0.0, s_noise_std), -0.35, 0.35)) if s_noise_std > 0 else 0.0
|
||||||
d_true = sc.d_init + noise_s
|
d_true = sc.d_init + noise_s
|
||||||
if d_true < 6.0:
|
if d_true < 6.0:
|
||||||
continue
|
continue
|
||||||
|
|
@ -188,7 +168,7 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
lat_t = float(np.clip(sc.lat_base + sc.v_lat * dt, -1.25, 1.25))
|
lat_t = float(np.clip(sc.lat_base + sc.v_lat * dt, -1.25, 1.25))
|
||||||
u_obj = float(gres.corridor.centre(np.array([d_true], np.float32))[0]) + lat_t
|
u_obj = float(gres.corridor.centre(np.array([d_true], np.float32))[0]) + lat_t
|
||||||
pc2, lab = inject(pc, guide.layout_full, gres.plane, cat[sc.name],
|
pc2, lab = inject(pc, guide.layout_full, gres.plane, cat[sc.name],
|
||||||
Placement(d=d_true, u=u_obj), rng=sc.rng, env=env)
|
Placement(d=d_true, u=u_obj), rng=rng, env=env)
|
||||||
res = sc.pipe.process(pc2)
|
res = sc.pipe.process(pc2)
|
||||||
if res is None:
|
if res is None:
|
||||||
continue
|
continue
|
||||||
|
|
@ -311,7 +291,6 @@ def main() -> None:
|
||||||
ap.add_argument("--device", default="auto", choices=["auto", "cuda", "cpu"],
|
ap.add_argument("--device", default="auto", choices=["auto", "cuda", "cpu"],
|
||||||
help="устройство вычислений ('auto', 'cuda', 'cpu')")
|
help="устройство вычислений ('auto', 'cuda', 'cpu')")
|
||||||
P.add_argument(ap)
|
P.add_argument(ap)
|
||||||
PS.add_argument(ap)
|
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
@ -374,7 +353,7 @@ def main() -> None:
|
||||||
if args.track_gate is not None:
|
if args.track_gate is not None:
|
||||||
over["track_gate"] = args.track_gate
|
over["track_gate"] = args.track_gate
|
||||||
over["device"] = args.device
|
over["device"] = args.device
|
||||||
params = Params(**PS.apply(over, args.set, Params))
|
params = Params(**over)
|
||||||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import numpy as np
|
||||||
|
|
||||||
import _bootstrap as B # noqa: F401
|
import _bootstrap as B # noqa: F401
|
||||||
import _parallel as P
|
import _parallel as P
|
||||||
import _params as PS
|
|
||||||
from flyguard.bag import Bag, find_bags
|
from flyguard.bag import Bag, find_bags
|
||||||
from flyguard.mushroom_body import FEATURES, describe
|
from flyguard.mushroom_body import FEATURES, describe
|
||||||
from flyguard.pipeline import FlyGuard, Params
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
|
|
@ -142,12 +141,11 @@ def main() -> None:
|
||||||
ap.add_argument("--laterals", default="0,-0.6,0.6,-1.2,1.2")
|
ap.add_argument("--laterals", default="0,-0.6,0.6,-1.2,1.2")
|
||||||
ap.add_argument("--seed", type=int, default=20260921)
|
ap.add_argument("--seed", type=int, default=20260921)
|
||||||
P.add_argument(ap)
|
P.add_argument(ap)
|
||||||
PS.add_argument(ap)
|
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
d_starts = tuple(float(x) for x in args.d_starts.split(","))
|
d_starts = tuple(float(x) for x in args.d_starts.split(","))
|
||||||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||||||
params = Params(**PS.apply({}, args.set, Params))
|
params = Params()
|
||||||
B.CACHE.mkdir(parents=True, exist_ok=True)
|
B.CACHE.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
bags = [p for p in find_bags(args.root) if p.name != HOLDOUT]
|
bags = [p for p in find_bags(args.root) if p.name != HOLDOUT]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue