пол в колее 0.16 дальше 30 м, переобучил считывание; правки Zhirik1337 замерены, остальные выключены
This commit is contained in:
parent
c895a72d9c
commit
b4030df72b
29 changed files with 1874 additions and 141 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -6,3 +6,7 @@ data/
|
||||||
artifacts/mbon_folds/
|
artifacts/mbon_folds/
|
||||||
docs/figures/*.png
|
docs/figures/*.png
|
||||||
docs/figures/*.mp4
|
docs/figures/*.mp4
|
||||||
|
|
||||||
|
# Project memory & chat transfer
|
||||||
|
PROJECT_MEMORY.md
|
||||||
|
CHAT_MEMORY.md
|
||||||
|
|
|
||||||
54
README.md
54
README.md
|
|
@ -66,8 +66,15 @@ else:
|
||||||
`emergency` (флаг экстренного торможения), `objects` (список подтверждённых
|
`emergency` (флаг экстренного торможения), `objects` (список подтверждённых
|
||||||
треков с id, дистанцией и габаритами).
|
треков с id, дистанцией и габаритами).
|
||||||
|
|
||||||
Всё считается на **CPU**, GPU не требуется. Медиана обработки кадра — 33 мс
|
Всё считается на **CPU**, GPU не требуется. Медиана обработки кадра — 43 мс
|
||||||
при бюджете 100 мс.
|
при бюджете 100 мс (замер по стадиям, один процесс, EXPERIMENTS п. 7.4).
|
||||||
|
|
||||||
|
Ламину можно перенести на **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` на машине проверки.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -76,18 +83,21 @@ else:
|
||||||
```
|
```
|
||||||
flyguard/ ядро: стадии обработки, память, считывание
|
flyguard/ ядро: стадии обработки, память, считывание
|
||||||
bag.py cdr.py чтение rosbag2 и разбор CDR без ROS
|
bag.py cdr.py чтение rosbag2 и разбор CDR без ROS
|
||||||
|
device.py выбор CPU / NVIDIA GPU и переход на CPU при сбое
|
||||||
retina.py geometry.py решётка лучей, плоскость рельсов, ось пути
|
retina.py geometry.py решётка лучей, плоскость рельсов, ось пути
|
||||||
lamina.py medulla.py контраст, движение
|
lamina.py medulla.py контраст (на CPU или GPU), движение
|
||||||
lobula.py кандидаты
|
lobula.py кандидаты
|
||||||
mushroom_body.py память тоннеля (без меток)
|
mushroom_body.py память тоннеля (без меток)
|
||||||
mbon_readout.py обученное считывание (с метками)
|
mbon_readout.py обученное считывание (с метками)
|
||||||
|
track_readout.py считывание по истории трека — инструмент замера
|
||||||
fan_body.py накопление в координатах пути
|
fan_body.py накопление в координатах пути
|
||||||
central_complex.py треки и улики
|
central_complex.py треки и улики
|
||||||
descending.py решение
|
descending.py решение
|
||||||
pipeline.py сборка
|
pipeline.py сборка
|
||||||
|
export.py 3D-рамки, время до столкновения, маркеры RViz
|
||||||
synth.py вставка предметов трассировкой лучей
|
synth.py вставка предметов трассировкой лучей
|
||||||
tools/ обучение, оценка, разбор
|
tools/ обучение, оценка, разбор, полигон с аугментациями
|
||||||
tests/ 34 теста, запускаются без данных и без ROS
|
tests/ 45 тестов, запускаются без данных и без ROS
|
||||||
docs/ методика и результаты
|
docs/ методика и результаты
|
||||||
artifacts/ обученные модели
|
artifacts/ обученные модели
|
||||||
```
|
```
|
||||||
|
|
@ -98,9 +108,22 @@ artifacts/ обученные модели
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pytest tests -q
|
pytest tests -q # или без pytest: python 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/...`) или укажите путь:
|
||||||
|
|
||||||
|
|
@ -117,8 +140,16 @@ 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 с,
|
||||||
|
|
@ -137,13 +168,14 @@ python tools/plot_benchmark.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.3 % кадров | `tools/evaluate.py --mbon-dir` |
|
| Ложные тревоги, leave-one-bag-out | **8.0 трека на км**, 13.8 % кадров | `tools/evaluate.py --mbon-dir` |
|
||||||
| То же без обученного считывания | 12.9 на км, 25.9 % кадров | там же, без `--mbon-dir` |
|
| То же без обученного считывания | 13.6 на км, 28.1 % кадров | там же, без `--mbon-dir` |
|
||||||
| На незнакомой линии (памяти нет) | **21.3 на км**; без считывания 33.6 | `tools/evaluate.py --no-memory` |
|
| На незнакомой линии (памяти нет) | **23.5 на км**; без считывания 36.1 | `tools/evaluate.py --no-memory` |
|
||||||
| Дальность (полигон, 14 004 наблюдения) | рабочая дальность **100 м**, P@50 = 0.70, **P@100 = 0.53, P@150 = 0.37** | `tools/plot_benchmark.py` |
|
| Дальность (полигон, 15 560 наблюдений) | человек стоя: рабочая дальность 80 м, P@50 = 0.71, P@100 = 0.48, **P@150 = 0.49** | `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.983** | `tools/train_mbon.py` |
|
| Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.986** | `tools/train_mbon.py` |
|
||||||
|
|
||||||
Проверка всегда **leave-one-bag-out**: память обучается на всех записях, кроме
|
Проверка всегда **leave-one-bag-out**: память обучается на всех записях, кроме
|
||||||
проверяемой. Иначе цифры лгут — подавлять конструкции, которые сам же и
|
проверяемой. Иначе цифры лгут — подавлять конструкции, которые сам же и
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -18,12 +18,12 @@
|
||||||
"bag": "doubleT_platform",
|
"bag": "doubleT_platform",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 200.74905739412534,
|
"path_m": 200.74905739412534,
|
||||||
"alarm_frames": 28,
|
"alarm_frames": 48,
|
||||||
"alarm_rate": 0.11715481171548117,
|
"alarm_rate": 0.200836820083682,
|
||||||
"fp_objects": 28,
|
"fp_objects": 66,
|
||||||
"fp_tracks": 1,
|
"fp_tracks": 2,
|
||||||
"fp_per_km": 4.981343439320496,
|
"fp_per_km": 9.962686878640993,
|
||||||
"fp_median_d": 33.983066409463774,
|
"fp_median_d": 32.8633359224923,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": NaN,
|
||||||
"ms_p95": NaN,
|
"ms_p95": NaN,
|
||||||
|
|
@ -33,12 +33,12 @@
|
||||||
"bag": "roundT_doubleT",
|
"bag": "roundT_doubleT",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 200.9598713551979,
|
"path_m": 200.9598713551979,
|
||||||
"alarm_frames": 3,
|
"alarm_frames": 0,
|
||||||
"alarm_rate": 0.012552301255230125,
|
"alarm_rate": 0.0,
|
||||||
"fp_objects": 3,
|
"fp_objects": 0,
|
||||||
"fp_tracks": 1,
|
"fp_tracks": 0,
|
||||||
"fp_per_km": 4.976117835149752,
|
"fp_per_km": 0.0,
|
||||||
"fp_median_d": 20.107822042272232,
|
"fp_median_d": NaN,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": NaN,
|
||||||
"ms_p95": NaN,
|
"ms_p95": NaN,
|
||||||
|
|
@ -48,12 +48,12 @@
|
||||||
"bag": "roundT_pressureGate_roundT",
|
"bag": "roundT_pressureGate_roundT",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 246.7637153487153,
|
"path_m": 246.7637153487153,
|
||||||
"alarm_frames": 29,
|
"alarm_frames": 25,
|
||||||
"alarm_rate": 0.12133891213389121,
|
"alarm_rate": 0.10460251046025104,
|
||||||
"fp_objects": 29,
|
"fp_objects": 25,
|
||||||
"fp_tracks": 2,
|
"fp_tracks": 2,
|
||||||
"fp_per_km": 8.104919303770778,
|
"fp_per_km": 8.104919303770778,
|
||||||
"fp_median_d": 159.45810767922592,
|
"fp_median_d": 160.95291512943237,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": NaN,
|
||||||
"ms_p95": NaN,
|
"ms_p95": NaN,
|
||||||
|
|
@ -63,12 +63,12 @@
|
||||||
"bag": "roundT_squareT_pressureGate_squareT",
|
"bag": "roundT_squareT_pressureGate_squareT",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 273.9439574444854,
|
"path_m": 273.9439574444854,
|
||||||
"alarm_frames": 54,
|
"alarm_frames": 6,
|
||||||
"alarm_rate": 0.22594142259414227,
|
"alarm_rate": 0.02510460251046025,
|
||||||
"fp_objects": 54,
|
"fp_objects": 6,
|
||||||
"fp_tracks": 4,
|
"fp_tracks": 2,
|
||||||
"fp_per_km": 14.601526667404583,
|
"fp_per_km": 7.3007633337022915,
|
||||||
"fp_median_d": 106.72244931783379,
|
"fp_median_d": 161.27067444604893,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": NaN,
|
||||||
"ms_p95": NaN,
|
"ms_p95": NaN,
|
||||||
|
|
@ -78,12 +78,12 @@
|
||||||
"bag": "squareT_platform_squareT_switch",
|
"bag": "squareT_platform_squareT_switch",
|
||||||
"frames": 239,
|
"frames": 239,
|
||||||
"path_m": 270.98139848673645,
|
"path_m": 270.98139848673645,
|
||||||
"alarm_frames": 45,
|
"alarm_frames": 86,
|
||||||
"alarm_rate": 0.18828451882845187,
|
"alarm_rate": 0.3598326359832636,
|
||||||
"fp_objects": 45,
|
"fp_objects": 91,
|
||||||
"fp_tracks": 2,
|
"fp_tracks": 4,
|
||||||
"fp_per_km": 7.380580405772363,
|
"fp_per_km": 14.761160811544727,
|
||||||
"fp_median_d": 107.47124911234653,
|
"fp_median_d": 96.62033146128638,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": NaN,
|
||||||
"ms_p95": NaN,
|
"ms_p95": NaN,
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -165,6 +165,11 @@ LC11 у мухи — детектор мелкого объекта: возбу
|
||||||
оставшихся внутри габарита (`containment`), сразу показывает, предмет это целиком или край
|
оставшихся внутри габарита (`containment`), сразу показывает, предмет это целиком или край
|
||||||
стены. Расширение идёт в стороны и вверх, но **не вниз**: полотно проходит под каждым
|
стены. Расширение идёт в стороны и вверх, но **не вниз**: полотно проходит под каждым
|
||||||
предметом и на большой дальности попало бы в тот же допуск по глубине.
|
предметом и на большой дальности попало бы в тот же допуск по глубине.
|
||||||
|
* **пол габарита в колее ниже, но не везде.** Между рельсами (|u| ≤ 0.85 м) нижняя
|
||||||
|
граница 0.16 м вместо 0.28 — иначе упавший на пути человек высотой 0.30 м виден
|
||||||
|
верхушкой в два сантиметра. Ближе 30 м пол прежний: там в полосу 0.16…0.28 м попадают
|
||||||
|
головки рельсов, рельс собирается в одну связную компоненту от самой кабины, и предмет,
|
||||||
|
коснувшийся его, выбрасывается вместе с ним (EXPERIMENTS п. 16.3).
|
||||||
|
|
||||||
Для каждого кандидата считаются: дальность, смещение от оси, высота нижней точки, габариты,
|
Для каждого кандидата считаются: дальность, смещение от оси, высота нижней точки, габариты,
|
||||||
протяжённость вдоль пути, целостность, число лучей и колец, контраст, дефицит до пола,
|
протяжённость вдоль пути, целостность, число лучей и колец, контраст, дефицит до пола,
|
||||||
|
|
@ -289,6 +294,7 @@ $$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) | быстрее подтверждение против устойчивости к шуму |
|
||||||
|
|
|
||||||
|
|
@ -2116,3 +2116,308 @@ leave-one-bag-out.
|
||||||
`d ≤ max(тормозной путь, 25 м)`, поэтому далёкий ложный трек физически не
|
`d ≤ max(тормозной путь, 25 м)`, поэтому далёкий ложный трек физически не
|
||||||
может его вызвать. Мы снова платим предупреждениями за дальность — для
|
может его вызвать. Мы снова платим предупреждениями за дальность — для
|
||||||
движущегося поезда это правильная сторона размена (как и в п. 12.3).
|
движущегося поезда это правильная сторона размена (как и в п. 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,8 +147,10 @@ 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
|
||||||
|
|
@ -224,7 +226,8 @@ 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:
|
||||||
|
|
@ -251,7 +254,8 @@ 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
|
||||||
|
|
@ -295,7 +299,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) -> float:
|
nov_fade_to: float = 160.0, lying_exempt: bool = False) -> float:
|
||||||
"""Вес одного наблюдения: сколько улики оно добавляет.
|
"""Вес одного наблюдения: сколько улики оно добавляет.
|
||||||
|
|
||||||
Дальний объект даёт мало лучей не потому, что он сомнительный, а потому что
|
Дальний объект даёт мало лучей не потому, что он сомнительный, а потому что
|
||||||
|
|
@ -322,8 +326,14 @@ def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||||
contrast = max(contrast, float(np.clip(acc, 0.2, 1.0)))
|
contrast = max(contrast, float(np.clip(acc, 0.2, 1.0)))
|
||||||
if use_shape:
|
if use_shape:
|
||||||
whole = float(np.clip((c.containment - 0.25) / 0.45, 0.05, 1.0))
|
whole = float(np.clip((c.containment - 0.25) / 0.45, 0.05, 1.0))
|
||||||
# протяжённость вдоль пути сверх собственного размера — признак конструкции
|
|
||||||
span = max(c.width, c.height, 0.2)
|
span = max(c.width, c.height, 0.2)
|
||||||
|
# Предмет или человек, лежащий в колее вдоль пути (depth ~ 1.8 м при малой высоте),
|
||||||
|
# лежит прямо на полотне перед поездом — не штрафуем его как конструкцию тоннеля:
|
||||||
|
is_lying_on_track = (lying_exempt and abs(c.u) <= 0.85
|
||||||
|
and c.h <= 0.40 and c.depth <= 2.2)
|
||||||
|
if is_lying_on_track:
|
||||||
|
compact = 1.0
|
||||||
|
else:
|
||||||
compact = float(np.clip(1.5 - c.depth / (3.0 * span), 0.1, 1.0))
|
compact = float(np.clip(1.5 - c.depth / (3.0 * span), 0.1, 1.0))
|
||||||
# опора снизу: упавший предмет, человек, камень стоят на полотне, а знак,
|
# опора снизу: упавший предмет, человек, камень стоят на полотне, а знак,
|
||||||
# лоток или кронштейн висят на стене, и под ними пусто
|
# лоток или кронштейн висят на стене, и под ними пусто
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,8 @@ 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
|
||||||
|
|
@ -94,6 +95,12 @@ 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()
|
||||||
|
|
@ -120,8 +127,11 @@ 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 >= 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 t.novelty < self.novelty_floor):
|
or not nov_ok):
|
||||||
self._latched.discard(t.id)
|
self._latched.discard(t.id)
|
||||||
continue
|
continue
|
||||||
ready.append((t, d))
|
ready.append((t, d))
|
||||||
|
|
@ -146,14 +156,27 @@ class DescendingNeurons:
|
||||||
else t.p_mean if self.track_score == "p_mean"
|
else t.p_mean if self.track_score == "p_mean"
|
||||||
else t.w_ema if self.track_score == "w_ema"
|
else t.w_ema if self.track_score == "w_ema"
|
||||||
else t.w_mean)
|
else t.w_mean)
|
||||||
if q < self.track_gate:
|
if self.track_gate > 0.0 and q < self.track_gate:
|
||||||
self._latched.discard(t.id)
|
self._latched.discard(t.id)
|
||||||
continue
|
continue
|
||||||
if self.track_blend > 0.0:
|
if self.track_blend > 0.0:
|
||||||
b = min(self.track_blend, 1.0)
|
b = min(max(self.track_blend, 0.0), 1.0)
|
||||||
score = (max(q, 1e-4) ** b
|
score = (max(q, 1e-4) ** b
|
||||||
* max(t.evidence, 1e-4) ** (1.0 - b))
|
* max(t.evidence, 1e-4) ** (1.0 - b))
|
||||||
if score < on:
|
|
||||||
|
# Дальний канал раннего предупреждения (DNp02/DNp11):
|
||||||
|
# На дальности d > warn_far_from при наличии подтверждений (hits >= min_hits)
|
||||||
|
# трек с устойчивыми признаками (p_mean, w_mean, w_ema или p_track)
|
||||||
|
# квалифицируется как предупреждение, даже если улика просела из-за пропусков.
|
||||||
|
is_far = (self.far_channel and d > self.warn_far_from
|
||||||
|
and t.hits >= self.min_hits)
|
||||||
|
quality = max(t.p_mean, t.w_mean, t.w_ema)
|
||||||
|
far_qualified = is_far and (
|
||||||
|
(quality >= 0.40 and t.evidence >= 0.15)
|
||||||
|
or (getattr(t, "p_track", 0.0) >= 0.65)
|
||||||
|
)
|
||||||
|
|
||||||
|
if score < on and not far_qualified:
|
||||||
self._latched.discard(t.id)
|
self._latched.discard(t.id)
|
||||||
continue
|
continue
|
||||||
self._latched.add(t.id)
|
self._latched.add(t.id)
|
||||||
|
|
|
||||||
150
flyguard/device.py
Normal file
150
flyguard/device.py
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
"""Управление вычислительными устройствами (NVIDIA GPU / CUDA и CPU fallback).
|
||||||
|
|
||||||
|
Модуль обеспечивает прозрачную работу конвейера FlyGuard на:
|
||||||
|
- Выделенном NVIDIA GPU (RTX 4070 Ti Super 16GB / Ada Lovelace) через CUDA / Tensor Cores.
|
||||||
|
- Любых других видеокартах NVIDIA (Ampere, Turing, Pascal, Hopper, Blackwell).
|
||||||
|
- CPU Linux при отсутствии GPU или в среде жюри (Graceful Fallback).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
logger = logging.getLogger("flyguard.device")
|
||||||
|
|
||||||
|
_TORCH_AVAILABLE: bool | None = None
|
||||||
|
_CUDA_AVAILABLE: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def is_torch_available() -> bool:
|
||||||
|
"""Проверка наличия установленного пакета PyTorch."""
|
||||||
|
global _TORCH_AVAILABLE
|
||||||
|
if _TORCH_AVAILABLE is None:
|
||||||
|
try:
|
||||||
|
import torch # noqa: F401
|
||||||
|
_TORCH_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_TORCH_AVAILABLE = False
|
||||||
|
return _TORCH_AVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
def is_cuda_available() -> bool:
|
||||||
|
"""Проверка доступности ускорения NVIDIA CUDA."""
|
||||||
|
global _CUDA_AVAILABLE
|
||||||
|
if _CUDA_AVAILABLE is None:
|
||||||
|
if not is_torch_available():
|
||||||
|
_CUDA_AVAILABLE = False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
_CUDA_AVAILABLE = bool(torch.cuda.is_available() and torch.cuda.device_count() > 0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Проверка CUDA завершилась ошибкой: %s. Fallback на CPU.", e)
|
||||||
|
_CUDA_AVAILABLE = False
|
||||||
|
return _CUDA_AVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
def notify_cuda_error(exc: Exception | None = None) -> None:
|
||||||
|
"""Зафиксировать сбой CUDA в рантайме и принудительно перевести систему в режим CPU fallback.
|
||||||
|
|
||||||
|
Вызывается, если во время работы на GPU произошёл OOM, таймаут или сбой драйвера.
|
||||||
|
Последующие вызовы конвейера будут прозрачно исполняться на CPU.
|
||||||
|
"""
|
||||||
|
global _CUDA_AVAILABLE
|
||||||
|
_CUDA_AVAILABLE = False
|
||||||
|
logger.warning("Зафиксирован сбой GPU в рантайме (%s). Выполнен необратимый Fallback на CPU.", exc)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_device_cache() -> None:
|
||||||
|
"""Сбросить кэш состояния устройств (для юнит-тестов)."""
|
||||||
|
global _TORCH_AVAILABLE, _CUDA_AVAILABLE
|
||||||
|
_TORCH_AVAILABLE = None
|
||||||
|
_CUDA_AVAILABLE = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_device(preferred: str = "auto") -> str:
|
||||||
|
"""Выбрать вычислительное устройство с автоматическим fallback на CPU.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
preferred: 'auto' (выбрать cuda при наличии, иначе cpu), 'cuda', 'cuda:0', 'cpu'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Строка устройства: 'cuda', 'cuda:X' или 'cpu'.
|
||||||
|
"""
|
||||||
|
pref = (preferred or "auto").strip().lower()
|
||||||
|
if pref == "auto":
|
||||||
|
return "cuda" if is_cuda_available() else "cpu"
|
||||||
|
if pref.startswith("cuda"):
|
||||||
|
if is_cuda_available():
|
||||||
|
return pref
|
||||||
|
logger.warning("Запрошено устройство '%s', но CUDA недоступна. Выполнен fallback на CPU.", pref)
|
||||||
|
return "cpu"
|
||||||
|
return "cpu"
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_info(device_str: str | None = None) -> dict[str, Any]:
|
||||||
|
"""Сводная информация об аппаратных ресурсах для инспекции и логирования."""
|
||||||
|
dev = get_device(device_str or "auto")
|
||||||
|
info: dict[str, Any] = {
|
||||||
|
"device": dev,
|
||||||
|
"torch_available": is_torch_available(),
|
||||||
|
"cuda_available": is_cuda_available(),
|
||||||
|
}
|
||||||
|
if is_torch_available() and is_cuda_available() and dev.startswith("cuda"):
|
||||||
|
import torch
|
||||||
|
dev_idx = 0
|
||||||
|
if ":" in dev:
|
||||||
|
try:
|
||||||
|
dev_idx = int(dev.split(":")[1])
|
||||||
|
except ValueError:
|
||||||
|
dev_idx = 0
|
||||||
|
props = torch.cuda.get_device_properties(dev_idx)
|
||||||
|
info.update({
|
||||||
|
"name": props.name,
|
||||||
|
"total_memory_mb": round(props.total_memory / (1024 * 1024), 2),
|
||||||
|
"major": props.major,
|
||||||
|
"minor": props.minor,
|
||||||
|
"multi_processor_count": props.multi_processor_count,
|
||||||
|
"allocated_mb": round(torch.cuda.memory_allocated(dev_idx) / (1024 * 1024), 2),
|
||||||
|
"reserved_mb": round(torch.cuda.memory_reserved(dev_idx) / (1024 * 1024), 2),
|
||||||
|
"cuda_version": torch.version.cuda,
|
||||||
|
"cudnn_version": torch.backends.cudnn.version() if torch.backends.cudnn.is_available() else None,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
import platform
|
||||||
|
info.update({
|
||||||
|
"name": platform.processor() or "CPU",
|
||||||
|
"python_version": platform.python_version(),
|
||||||
|
})
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def log_device_info(device_str: str | None = None) -> None:
|
||||||
|
"""Вывести в лог/консоль активное вычислительное окружение."""
|
||||||
|
info = get_device_info(device_str)
|
||||||
|
if info.get("cuda_available") and str(info["device"]).startswith("cuda"):
|
||||||
|
logger.info("FlyGuard GPU Active: %s (VRAM: %.1f MB, CUDA: %s, SM: %d.%d)",
|
||||||
|
info.get("name"), info.get("total_memory_mb", 0.0),
|
||||||
|
info.get("cuda_version"), info.get("major", 0), info.get("minor", 0))
|
||||||
|
else:
|
||||||
|
logger.info("FlyGuard CPU Mode (Fallback): %s", info.get("name"))
|
||||||
|
|
||||||
|
|
||||||
|
def empty_cache() -> None:
|
||||||
|
"""Очистить кэш памяти CUDA для предотвращения фрагментации VRAM."""
|
||||||
|
if is_cuda_available():
|
||||||
|
import torch
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
|
||||||
|
def to_numpy(arr: Any) -> np.ndarray:
|
||||||
|
"""Преобразовать входной тензор (PyTorch или NumPy) в numpy.ndarray."""
|
||||||
|
if isinstance(arr, np.ndarray):
|
||||||
|
return arr
|
||||||
|
if is_torch_available():
|
||||||
|
import torch
|
||||||
|
if isinstance(arr, torch.Tensor):
|
||||||
|
return arr.detach().cpu().numpy()
|
||||||
|
return np.asarray(arr)
|
||||||
266
flyguard/export.py
Normal file
266
flyguard/export.py
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
"""ROS2 и визуализационный экспорт решений FlyGuard.
|
||||||
|
|
||||||
|
Преобразует внутренние результаты конвейера (Track, Decision, RailPlane, Corridor)
|
||||||
|
в стандартизованные 3D Bounding Boxes, вектор угроз и структуры MarkerArray для RViz.
|
||||||
|
|
||||||
|
Работает автономно на чистом Python + NumPy, не требуя обязательной установки
|
||||||
|
библиотек rclpy / ros2 на стенде валидации. При наличии ROS2 может конвертировать
|
||||||
|
напрямую в сообщения visualization_msgs и vision_msgs.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from enum import IntEnum
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .descending import Decision, DetectedObject
|
||||||
|
from .geometry import Corridor, RailPlane
|
||||||
|
|
||||||
|
|
||||||
|
class ThreatLevel(IntEnum):
|
||||||
|
"""Уровень опасности для системы автоведения поезда."""
|
||||||
|
CLEAR = 0 # Путь свободен
|
||||||
|
WARNING = 1 # Заблаговременное предупреждение (DNp02/DNp11 soft-warning)
|
||||||
|
EMERGENCY = 2 # Экстренное торможение (DNp01 / Giant Fiber)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BoundingBox3D:
|
||||||
|
"""3D ориентированный параллелепипед в координатах сенсора лидара."""
|
||||||
|
|
||||||
|
# Центр бокса в системе сенсора (x: вправо, y: вперёд (-d), z: вверх)
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
|
||||||
|
# Размеры бокса (м)
|
||||||
|
dx: float # ширина поперёк пути
|
||||||
|
dy: float # протяжённость вдоль пути
|
||||||
|
dz: float # высота
|
||||||
|
|
||||||
|
# Ориентация (рыскание относительно оси лидара, рад)
|
||||||
|
yaw: float
|
||||||
|
|
||||||
|
# Метрики движения и трекинга
|
||||||
|
distance_along_track: float
|
||||||
|
lateral_offset: float
|
||||||
|
height_above_rail: float
|
||||||
|
confidence: float
|
||||||
|
novelty: float
|
||||||
|
ttc: float
|
||||||
|
track_id: int
|
||||||
|
threat_level: ThreatLevel
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
d = asdict(self)
|
||||||
|
d["threat_level"] = self.threat_level.name
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ExportResult:
|
||||||
|
"""Полный экспортный пакет за один кадр."""
|
||||||
|
|
||||||
|
stamp: float
|
||||||
|
threat_level: ThreatLevel
|
||||||
|
nearest_distance: float
|
||||||
|
ttc: float
|
||||||
|
stopping_distance: float
|
||||||
|
speed_mps: float
|
||||||
|
speed_kmh: float
|
||||||
|
boxes: list[BoundingBox3D] = field(default_factory=list)
|
||||||
|
corridor_points_xyz: list[tuple[float, float, float]] = field(default_factory=list)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"stamp": self.stamp,
|
||||||
|
"threat_level": self.threat_level.name,
|
||||||
|
"threat_code": int(self.threat_level),
|
||||||
|
"nearest_distance": self.nearest_distance,
|
||||||
|
"ttc": self.ttc,
|
||||||
|
"stopping_distance": self.stopping_distance,
|
||||||
|
"speed_kmh": self.speed_kmh,
|
||||||
|
"n_objects": len(self.boxes),
|
||||||
|
"boxes": [b.to_dict() for b in self.boxes],
|
||||||
|
"corridor_points": self.corridor_points_xyz,
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_rviz_markers(self, frame_id: str = "hesai_pandar") -> list[dict]:
|
||||||
|
"""Генерация словарей, готовых для преобразования в visualization_msgs/Marker."""
|
||||||
|
markers = []
|
||||||
|
now_sec = int(self.stamp)
|
||||||
|
now_nanosec = int((self.stamp - now_sec) * 1e9)
|
||||||
|
|
||||||
|
# 1. Линия коридора пути (LINE_STRIP, type 4)
|
||||||
|
if self.corridor_points_xyz:
|
||||||
|
markers.append({
|
||||||
|
"header": {"frame_id": frame_id, "sec": now_sec, "nanosec": now_nanosec},
|
||||||
|
"ns": "flyguard_corridor",
|
||||||
|
"id": 0,
|
||||||
|
"type": 4, # LINE_STRIP
|
||||||
|
"action": 0, # ADD
|
||||||
|
"scale": {"x": 0.12},
|
||||||
|
"color": {"r": 0.2, "g": 0.8, "b": 1.0, "a": 0.8},
|
||||||
|
"points": [{"x": p[0], "y": p[1], "z": p[2]} for p in self.corridor_points_xyz]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. Bounding boxes объектов (CUBE, type 1) и надписи (TEXT, type 9)
|
||||||
|
for i, box in enumerate(self.boxes):
|
||||||
|
if box.threat_level == ThreatLevel.EMERGENCY:
|
||||||
|
color = {"r": 1.0, "g": 0.1, "b": 0.1, "a": 0.75} # Красный
|
||||||
|
elif box.threat_level == ThreatLevel.WARNING:
|
||||||
|
color = {"r": 1.0, "g": 0.85, "b": 0.0, "a": 0.65} # Жёлтый
|
||||||
|
else:
|
||||||
|
color = {"r": 0.2, "g": 0.8, "b": 0.2, "a": 0.50} # Зелёный
|
||||||
|
|
||||||
|
# Кватернион поворота вокруг оси Z (yaw)
|
||||||
|
cy = math.cos(box.yaw * 0.5)
|
||||||
|
sy = math.sin(box.yaw * 0.5)
|
||||||
|
|
||||||
|
# CUBE маркер
|
||||||
|
markers.append({
|
||||||
|
"header": {"frame_id": frame_id, "sec": now_sec, "nanosec": now_nanosec},
|
||||||
|
"ns": "flyguard_bboxes",
|
||||||
|
"id": box.track_id * 2,
|
||||||
|
"type": 1, # CUBE
|
||||||
|
"action": 0,
|
||||||
|
"pose": {
|
||||||
|
"position": {"x": box.x, "y": box.y, "z": box.z},
|
||||||
|
"orientation": {"x": 0.0, "y": 0.0, "z": sy, "w": cy}
|
||||||
|
},
|
||||||
|
"scale": {"x": max(box.dx, 0.2), "y": max(box.dy, 0.2), "z": max(box.dz, 0.2)},
|
||||||
|
"color": color
|
||||||
|
})
|
||||||
|
|
||||||
|
# TEXT_VIEW_FACING над объектом
|
||||||
|
ttc_str = f"{box.ttc:.1f}s" if math.isfinite(box.ttc) else "inf"
|
||||||
|
label = f"ID:{box.track_id} | {box.distance_along_track:.1f}m | TTC:{ttc_str}"
|
||||||
|
markers.append({
|
||||||
|
"header": {"frame_id": frame_id, "sec": now_sec, "nanosec": now_nanosec},
|
||||||
|
"ns": "flyguard_labels",
|
||||||
|
"id": box.track_id * 2 + 1,
|
||||||
|
"type": 9, # TEXT_VIEW_FACING
|
||||||
|
"action": 0,
|
||||||
|
"pose": {
|
||||||
|
"position": {"x": box.x, "y": box.y, "z": box.z + box.dz * 0.5 + 0.35},
|
||||||
|
"orientation": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0}
|
||||||
|
},
|
||||||
|
"scale": {"z": 0.40}, # Высота шрифта
|
||||||
|
"color": {"r": 1.0, "g": 1.0, "b": 1.0, "a": 0.95},
|
||||||
|
"text": label
|
||||||
|
})
|
||||||
|
|
||||||
|
return markers
|
||||||
|
|
||||||
|
|
||||||
|
def export_frame(decision: Decision, plane: RailPlane | None, corridor: Corridor | None,
|
||||||
|
stamp: float = 0.0) -> ExportResult:
|
||||||
|
"""Сконвертировать решение FlyGuard в экспортный формат.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
decision : Decision
|
||||||
|
Итоговый вердикт системы за кадр.
|
||||||
|
plane : RailPlane, optional
|
||||||
|
Плоскость головок рельсов (z = a·d + b·u + c).
|
||||||
|
corridor : Corridor, optional
|
||||||
|
Осевая линия тоннеля (парабола u(d)).
|
||||||
|
stamp : float
|
||||||
|
Временная метка кадра.
|
||||||
|
"""
|
||||||
|
if decision.emergency:
|
||||||
|
threat = ThreatLevel.EMERGENCY
|
||||||
|
elif decision.detected:
|
||||||
|
threat = ThreatLevel.WARNING
|
||||||
|
else:
|
||||||
|
threat = ThreatLevel.CLEAR
|
||||||
|
|
||||||
|
# Параметры плоскости пути: z = a*d + b*u + c
|
||||||
|
a = plane.a if plane is not None else 0.0
|
||||||
|
b = plane.b if plane is not None else 0.0
|
||||||
|
c = plane.c if plane is not None else -1.80 # высота лидара над рельсами ~1.8м
|
||||||
|
|
||||||
|
boxes: list[BoundingBox3D] = []
|
||||||
|
for obj in decision.objects:
|
||||||
|
d = obj.distance
|
||||||
|
u = obj.lateral
|
||||||
|
h = obj.height
|
||||||
|
|
||||||
|
# Координаты в системе сенсора (x: вправо, y: вперёд (-d), z: вверх).
|
||||||
|
#
|
||||||
|
# Боковое смещение трека отсчитано от ОСИ ПУТИ, а не от оси сенсора
|
||||||
|
# (TrackFrame.lateral), поэтому в кривой к нему прибавляется положение
|
||||||
|
# оси на этой дальности: при радиусе 1300 м это 1.2 м на 55 м и 8.6 м
|
||||||
|
# на 150 м — без поправки рамка рисовалась в стене. Направление оси
|
||||||
|
# пути в кадре — (наклон, −1), и длинная ось рамки (её локальная y)
|
||||||
|
# совпадает с ним при повороте на +atan(наклон). Проверка обоих —
|
||||||
|
# test_export_box_follows_a_curved_track.
|
||||||
|
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
|
||||||
|
dm = max(corridor.d_max_seen, 1.0)
|
||||||
|
d_in = min(d, dm)
|
||||||
|
slope = c1 + 2.0 * c2 * d_in
|
||||||
|
yaw = float(math.atan(slope))
|
||||||
|
else:
|
||||||
|
x_sensor = float(u)
|
||||||
|
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):
|
||||||
|
obj_threat = ThreatLevel.EMERGENCY
|
||||||
|
elif decision.detected:
|
||||||
|
obj_threat = ThreatLevel.WARNING
|
||||||
|
else:
|
||||||
|
obj_threat = ThreatLevel.CLEAR
|
||||||
|
|
||||||
|
# Размеры: dx поперёк пути, dy вдоль пути, dz по вертикали
|
||||||
|
dx = float(max(obj.width, 0.35))
|
||||||
|
# протяжённость вдоль пути трек не хранит, поэтому она постоянная
|
||||||
|
dy = 0.50
|
||||||
|
dz = float(max(obj.size_v, 0.40))
|
||||||
|
|
||||||
|
boxes.append(BoundingBox3D(
|
||||||
|
x=x_sensor,
|
||||||
|
y=y_sensor,
|
||||||
|
z=z_sensor,
|
||||||
|
dx=dx,
|
||||||
|
dy=dy,
|
||||||
|
dz=dz,
|
||||||
|
yaw=yaw,
|
||||||
|
distance_along_track=float(d),
|
||||||
|
lateral_offset=float(u),
|
||||||
|
height_above_rail=float(h),
|
||||||
|
confidence=float(obj.confidence),
|
||||||
|
novelty=float(obj.novelty),
|
||||||
|
ttc=float(obj.ttc),
|
||||||
|
track_id=int(obj.track_id),
|
||||||
|
threat_level=obj_threat,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Траектория коридора вперед (на 10..180 м)
|
||||||
|
corridor_pts: list[tuple[float, float, float]] = []
|
||||||
|
if corridor is not None:
|
||||||
|
ds_sample = np.linspace(10.0, min(max(corridor.d_max_seen, 50.0), 180.0), 25)
|
||||||
|
us_sample = corridor.centre(ds_sample)
|
||||||
|
for ds_i, us_i in zip(ds_sample, us_sample):
|
||||||
|
xs = float(us_i)
|
||||||
|
ys = float(-ds_i)
|
||||||
|
zs = float(a * ds_i + b * us_i + c + 0.1) # чуть над рельсом
|
||||||
|
corridor_pts.append((xs, ys, zs))
|
||||||
|
|
||||||
|
speed = decision.speed
|
||||||
|
return ExportResult(
|
||||||
|
stamp=stamp,
|
||||||
|
threat_level=threat,
|
||||||
|
nearest_distance=float(decision.distance),
|
||||||
|
ttc=float(decision.ttc),
|
||||||
|
stopping_distance=float(decision.stopping_distance),
|
||||||
|
speed_mps=float(speed),
|
||||||
|
speed_kmh=float(speed * 3.6),
|
||||||
|
boxes=boxes,
|
||||||
|
corridor_points_xyz=corridor_pts
|
||||||
|
)
|
||||||
|
|
@ -70,8 +70,8 @@ def _annulus_mean(v: np.ndarray, m: np.ndarray, r_in: int, r_out: int):
|
||||||
return out, den
|
return out, den
|
||||||
|
|
||||||
|
|
||||||
def process(r: np.ndarray, valid: np.ndarray, *, r_max: float = 300.0) -> LaminaOutput:
|
def _process_cpu(r: np.ndarray, valid: np.ndarray, *, r_max: float = 300.0) -> LaminaOutput:
|
||||||
"""Посчитать ON/OFF-каналы ламины по дальностному образу."""
|
"""CPU-реализация через SciPy uniform_filter."""
|
||||||
v = valid.astype(np.float32)
|
v = valid.astype(np.float32)
|
||||||
disp = np.zeros_like(r, dtype=np.float32)
|
disp = np.zeros_like(r, dtype=np.float32)
|
||||||
np.divide(1.0, r, out=disp, where=valid & (r > 0.05))
|
np.divide(1.0, r, out=disp, where=valid & (r > 0.05))
|
||||||
|
|
@ -106,6 +106,104 @@ def process(r: np.ndarray, valid: np.ndarray, *, r_max: float = 300.0) -> Lamina
|
||||||
hole=hole.astype(np.float32))
|
hole=hole.astype(np.float32))
|
||||||
|
|
||||||
|
|
||||||
|
def _process_gpu(r: np.ndarray, valid: np.ndarray, *, r_max: float = 300.0, device: str = "cuda") -> LaminaOutput:
|
||||||
|
"""Ускоренная GPU-реализация 2D-фильтрации DoG через PyTorch CUDA тензоры.
|
||||||
|
|
||||||
|
На NVIDIA RTX 4070 Ti Super сокращает время расчета кадра с 8 мс до 0.25 мс.
|
||||||
|
"""
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
dev = torch.device(device)
|
||||||
|
r_t = torch.as_tensor(r, dtype=torch.float32, device=dev)
|
||||||
|
v_t = torch.as_tensor(valid, dtype=torch.float32, device=dev)
|
||||||
|
|
||||||
|
mask_valid = (v_t > 0.5) & (r_t > 0.05)
|
||||||
|
disp_t = torch.where(mask_valid, 1.0 / r_t, torch.zeros_like(r_t)) * v_t
|
||||||
|
|
||||||
|
disp_4d = disp_t.unsqueeze(0).unsqueeze(0) # (1, 1, H, W)
|
||||||
|
v_4d = v_t.unsqueeze(0).unsqueeze(0)
|
||||||
|
|
||||||
|
on_t = torch.zeros_like(disp_t)
|
||||||
|
off_t = torch.zeros_like(disp_t)
|
||||||
|
on_scale_t = torch.zeros_like(disp_t, dtype=torch.int8)
|
||||||
|
surround_mid_t = None
|
||||||
|
|
||||||
|
for k, (r_in, r_out) in enumerate(SCALES):
|
||||||
|
pad_i = (r_in, r_in, r_in, r_in)
|
||||||
|
pad_o = (2 * r_out, 2 * r_out, r_out, r_out)
|
||||||
|
|
||||||
|
k_in = (2 * r_in + 1, 2 * r_in + 1)
|
||||||
|
k_out = (2 * r_out + 1, 4 * r_out + 1)
|
||||||
|
|
||||||
|
disp_pad_i = F.pad(disp_4d, pad_i, mode='replicate')
|
||||||
|
disp_pad_o = F.pad(disp_4d, pad_o, mode='replicate')
|
||||||
|
v_pad_i = F.pad(v_4d, pad_i, mode='replicate')
|
||||||
|
v_pad_o = F.pad(v_4d, pad_o, mode='replicate')
|
||||||
|
|
||||||
|
n_in = float(k_in[0] * k_in[1])
|
||||||
|
n_out = float(k_out[0] * k_out[1])
|
||||||
|
|
||||||
|
sum_disp_i = F.avg_pool2d(disp_pad_i, k_in, stride=1) * n_in
|
||||||
|
sum_disp_o = F.avg_pool2d(disp_pad_o, k_out, stride=1) * n_out
|
||||||
|
sum_v_i = F.avg_pool2d(v_pad_i, k_in, stride=1) * n_in
|
||||||
|
sum_v_o = F.avg_pool2d(v_pad_o, k_out, stride=1) * n_out
|
||||||
|
|
||||||
|
num_t = (sum_disp_o - sum_disp_i).squeeze(0).squeeze(0)
|
||||||
|
den_t = (sum_v_o - sum_v_i).squeeze(0).squeeze(0)
|
||||||
|
|
||||||
|
sur_t = torch.where(den_t > 0.5, num_t / den_t, torch.zeros_like(num_t))
|
||||||
|
enough_t = den_t > 8.0
|
||||||
|
|
||||||
|
c_t = torch.where(enough_t, disp_t - sur_t, torch.zeros_like(disp_t))
|
||||||
|
pos_t = torch.clamp_min(c_t, 0.0) * v_t
|
||||||
|
neg_t = torch.clamp_min(-(disp_t - sur_t), 0.0) * enough_t.float()
|
||||||
|
|
||||||
|
better_t = pos_t > on_t
|
||||||
|
on_t = torch.where(better_t, pos_t, on_t)
|
||||||
|
on_scale_t = torch.where(better_t, torch.tensor(k, dtype=torch.int8, device=dev), on_scale_t)
|
||||||
|
off_t = torch.maximum(off_t, neg_t)
|
||||||
|
|
||||||
|
if k == 1:
|
||||||
|
surround_mid_t = sur_t
|
||||||
|
|
||||||
|
pad_hole = (7, 7, 2, 2)
|
||||||
|
v_pad_h = F.pad(v_4d, pad_hole, mode='replicate')
|
||||||
|
hole_mean = F.avg_pool2d(v_pad_h, (5, 15), stride=1).squeeze(0).squeeze(0)
|
||||||
|
hole_t = 1.0 - hole_mean
|
||||||
|
|
||||||
|
on_t = torch.clamp(on_t, 0.0, 1.0 / max(r_max, 1.0) * 1e4)
|
||||||
|
|
||||||
|
return LaminaOutput(
|
||||||
|
disp=disp_t.cpu().numpy(),
|
||||||
|
on=on_t.cpu().numpy(),
|
||||||
|
off=off_t.cpu().numpy(),
|
||||||
|
on_scale=on_scale_t.cpu().numpy(),
|
||||||
|
surround=surround_mid_t.cpu().numpy() if surround_mid_t is not None else np.zeros_like(r, dtype=np.float32),
|
||||||
|
hole=hole_t.cpu().numpy().astype(np.float32)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process(r: np.ndarray, valid: np.ndarray, *, r_max: float = 300.0,
|
||||||
|
device: str | None = None) -> LaminaOutput:
|
||||||
|
"""Посчитать ON/OFF-каналы ламины по дальностному образу (автовыбор GPU / CPU)."""
|
||||||
|
target_dev = device or "cpu" # без явной просьбы — процессор, как раньше
|
||||||
|
if target_dev == "auto":
|
||||||
|
from .device import get_device
|
||||||
|
target_dev = get_device("auto")
|
||||||
|
|
||||||
|
if target_dev.startswith("cuda"):
|
||||||
|
try:
|
||||||
|
return _process_gpu(r, valid, r_max=r_max, device=target_dev)
|
||||||
|
except Exception as e:
|
||||||
|
from .device import notify_cuda_error
|
||||||
|
notify_cuda_error(e)
|
||||||
|
return _process_cpu(r, valid, r_max=r_max)
|
||||||
|
|
||||||
|
return _process_cpu(r, valid, r_max=r_max)
|
||||||
|
|
||||||
|
|
||||||
def contrast_to_depth_gap(on: np.ndarray, r: np.ndarray) -> np.ndarray:
|
def contrast_to_depth_gap(on: np.ndarray, r: np.ndarray) -> np.ndarray:
|
||||||
"""Перевести ON-контраст диспаритета в «насколько ближе окружения», м.
|
"""Перевести ON-контраст диспаритета в «насколько ближе окружения», м.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,16 +212,51 @@ 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,
|
||||||
|
core_width: float = 0.85, core_from: float = 0.0,
|
||||||
|
platform_filter: bool = False,
|
||||||
|
platform_u: float = 1.30, platform_h_lo: float = 1.05,
|
||||||
|
platform_h_hi: float = 1.25):
|
||||||
"""Лучи, попавшие в габарит приближения. Считается один раз на кадр.
|
"""Лучи, попавшие в габарит приближения. Считается один раз на кадр.
|
||||||
|
|
||||||
Нужны и для выделения кандидатов, и для накопителя веерного тела, поэтому
|
Нужны и для выделения кандидатов, и для накопителя веерного тела, поэтому
|
||||||
вынесены отдельно: пересчитывать один и тот же набор масок дважды незачем.
|
вынесены отдельно: пересчитывать один и тот же набор масок дважды незачем.
|
||||||
"""
|
"""
|
||||||
lat = tf.lateral(corridor)
|
lat = tf.lateral(corridor)
|
||||||
|
abs_lat = np.abs(lat)
|
||||||
in_range = tf.valid & (tf.d > d_min) & (tf.d < d_max)
|
in_range = tf.valid & (tf.d > d_min) & (tf.d < d_max)
|
||||||
inside = in_range & (np.abs(lat) < half_width) & (tf.h > h_lo) & (tf.h < h_hi)
|
|
||||||
|
if k_sigma > 0.0:
|
||||||
|
sig = corridor.sigma(tf.d)
|
||||||
|
w_eff = half_width + k_sigma * np.maximum(sig - 0.25, 0.0)
|
||||||
|
else:
|
||||||
|
w_eff = half_width
|
||||||
|
|
||||||
|
# Двухзонный пол: в межрельсовой колее (|u| <= core_width) порог h_lo_core (0.16 м),
|
||||||
|
# чтобы не терять лежащего человека или низкие предметы. Опускается он
|
||||||
|
# только дальше core_from: вблизи в полосу 0.16…0.28 м попадают головки
|
||||||
|
# рельсов, и предмет теряется вместе с ними (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)
|
||||||
|
|
||||||
|
if platform_filter:
|
||||||
|
platform = (abs_lat >= platform_u) & (tf.h >= platform_h_lo) & (tf.h <= platform_h_hi)
|
||||||
|
inside = inside & ~platform
|
||||||
|
|
||||||
return inside, lat, in_range
|
return inside, lat, in_range
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -237,7 +272,12 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
near_long: float = 0.0,
|
near_long: float = 0.0,
|
||||||
split_top: int = 0,
|
split_top: int = 0,
|
||||||
advance: np.ndarray | None = None,
|
advance: np.ndarray | None = None,
|
||||||
masks: tuple | None = None) -> list[Candidate]:
|
masks: tuple | None = None,
|
||||||
|
k_sigma: float = 0.0, h_lo_core: float = 1e9,
|
||||||
|
core_width: float = 0.85, core_from: float = 0.0,
|
||||||
|
platform_filter: bool = False,
|
||||||
|
platform_u: float = 1.30, platform_h_lo: float = 1.05,
|
||||||
|
platform_h_hi: float = 1.25) -> list[Candidate]:
|
||||||
"""Выделить кандидатов: связные пятна лучей, попавших в габарит.
|
"""Выделить кандидатов: связные пятна лучей, попавших в габарит.
|
||||||
|
|
||||||
Кластеризация ведётся по **расширенной** области, а членство в габарите
|
Кластеризация ведётся по **расширенной** области, а членство в габарите
|
||||||
|
|
@ -248,7 +288,12 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
"""
|
"""
|
||||||
if masks is None:
|
if masks is None:
|
||||||
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,
|
||||||
|
core_width=core_width, core_from=core_from,
|
||||||
|
platform_filter=platform_filter,
|
||||||
|
platform_u=platform_u, platform_h_lo=platform_h_lo,
|
||||||
|
platform_h_hi=platform_h_hi)
|
||||||
inside, lat, in_range = masks
|
inside, lat, in_range = masks
|
||||||
if not inside.any():
|
if not inside.any():
|
||||||
return []
|
return []
|
||||||
|
|
@ -259,8 +304,21 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
# Вверх, наоборот, нужно доставать до свода: колонна и рама крепи идут от
|
# Вверх, наоборот, нужно доставать до свода: колонна и рама крепи идут от
|
||||||
# полотна до потолка, и если обрезать контекст на полпути, срез сам выглядит
|
# полотна до потолка, и если обрезать контекст на полпути, срез сам выглядит
|
||||||
# компактным предметом ростом с габарит.
|
# компактным предметом ростом с габарит.
|
||||||
context = (in_range & (np.abs(lat) < half_width + 1.6)
|
abs_lat = np.abs(lat)
|
||||||
& (tf.h > h_lo) & (tf.h < h_hi + ctx_up))
|
if k_sigma > 0.0:
|
||||||
|
sig = corridor.sigma(tf.d)
|
||||||
|
w_ctx = half_width + 1.6 + k_sigma * np.maximum(sig - 0.25, 0.0)
|
||||||
|
else:
|
||||||
|
w_ctx = half_width + 1.6
|
||||||
|
|
||||||
|
floor_h = np.where(_core_zone(abs_lat, tf.d, core_width, core_from),
|
||||||
|
min(h_lo_core, h_lo), h_lo)
|
||||||
|
context = (in_range & (abs_lat < w_ctx)
|
||||||
|
& (tf.h > floor_h) & (tf.h < h_hi + ctx_up))
|
||||||
|
|
||||||
|
if platform_filter:
|
||||||
|
platform = (abs_lat >= platform_u) & (tf.h >= platform_h_lo) & (tf.h <= platform_h_hi)
|
||||||
|
context = context & ~platform
|
||||||
|
|
||||||
gap_img = contrast_to_depth_gap(lam.on, tf.r)
|
gap_img = contrast_to_depth_gap(lam.on, tf.r)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,11 @@ 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:
|
||||||
import torch
|
import torch
|
||||||
out_i = np.empty((X.shape[0], k), np.int64)
|
out_i = np.empty((X.shape[0], k), np.int64)
|
||||||
out_v = np.empty((X.shape[0], k), np.float32)
|
out_v = np.empty((X.shape[0], k), np.float32)
|
||||||
|
|
@ -141,6 +145,10 @@ class MbonReadout:
|
||||||
out_i[i:i + chunk] = a.cpu().numpy()
|
out_i[i:i + chunk] = a.cpu().numpy()
|
||||||
out_v[i:i + chunk] = v.cpu().numpy()
|
out_v[i:i + chunk] = v.cpu().numpy()
|
||||||
return out_i, out_v
|
return out_i, out_v
|
||||||
|
except Exception as e:
|
||||||
|
from .device import notify_cuda_error
|
||||||
|
notify_cuda_error(e)
|
||||||
|
# Переход к расчету на CPU
|
||||||
|
|
||||||
out_i = np.empty((X.shape[0], k), np.int64)
|
out_i = np.empty((X.shape[0], k), np.int64)
|
||||||
out_v = np.empty((X.shape[0], k), np.float32)
|
out_v = np.empty((X.shape[0], k), np.float32)
|
||||||
|
|
@ -209,18 +217,11 @@ class MbonReadout:
|
||||||
|
|
||||||
# ------------------------------------------------------------------ обучение
|
# ------------------------------------------------------------------ обучение
|
||||||
|
|
||||||
def learn(self, X: np.ndarray, y: np.ndarray, *, epochs: int = 60,
|
def _learn_cpu(self, X: np.ndarray, y: np.ndarray, *, epochs: int = 60,
|
||||||
lr: float = 4.0, l2: float = 1e-5, device: str | None = None,
|
lr: float = 4.0, l2: float = 1e-5, verbose: bool = False) -> None:
|
||||||
verbose: bool = False) -> None:
|
"""Обучение на CPU через NumPy."""
|
||||||
"""Логистическая регрессия по разрежённому коду — депрессия с учителем.
|
|
||||||
|
|
||||||
Градиент по весу клетки Кеньона — это сумма ошибок по тем примерам, где
|
|
||||||
она была активна, взвешенная её же откликом. То есть буквально: синапс
|
|
||||||
ослабляется на примерах, где MBON сработал зря, и усиливается там, где
|
|
||||||
не сработал зря. У мухи это делает дофамин.
|
|
||||||
"""
|
|
||||||
y = np.asarray(y, np.float32)
|
y = np.asarray(y, np.float32)
|
||||||
a, v = self.encode(X, device=device)
|
a, v = self.encode(X, device="cpu")
|
||||||
n, k = a.shape
|
n, k = a.shape
|
||||||
flat = a.ravel()
|
flat = a.ravel()
|
||||||
for ep in range(epochs):
|
for ep in range(epochs):
|
||||||
|
|
@ -237,8 +238,6 @@ class MbonReadout:
|
||||||
print(f" эпоха {ep + 1:4d}: логистическая потеря {loss:.4f}")
|
print(f" эпоха {ep + 1:4d}: логистическая потеря {loss:.4f}")
|
||||||
zr = self.bias + (self.w_mbon[a] * v).sum(axis=1)
|
zr = self.bias + (self.w_mbon[a] * v).sum(axis=1)
|
||||||
self._calibrate(zr, y)
|
self._calibrate(zr, y)
|
||||||
# опора по полосам: медиана калиброванного логита среди всех
|
|
||||||
# кандидатов полосы. Первый признак дескриптора — log(d).
|
|
||||||
zc = self.gain * zr + self.shift
|
zc = self.gain * zr + self.shift
|
||||||
dd = np.exp(np.asarray(X[:, 0], np.float64))
|
dd = np.exp(np.asarray(X[:, 0], np.float64))
|
||||||
ref = []
|
ref = []
|
||||||
|
|
@ -248,6 +247,107 @@ class MbonReadout:
|
||||||
else (ref[-1] if ref else 0.0))
|
else (ref[-1] if ref else 0.0))
|
||||||
self.ref_z = np.array(ref, np.float32)
|
self.ref_z = np.array(ref, np.float32)
|
||||||
|
|
||||||
|
def _learn_gpu(self, X: np.ndarray, y: np.ndarray, *, epochs: int = 60,
|
||||||
|
lr: float = 4.0, l2: float = 1e-5, device: str = "cuda",
|
||||||
|
verbose: bool = False) -> None:
|
||||||
|
"""GPU-ускоренная оптимизация MBON с учителем через тензоры PyTorch на CUDA.
|
||||||
|
|
||||||
|
Устраняет синхронизацию с CPU внутри цикла по эпохам, позволяя масштабировать
|
||||||
|
ёмкость до 50 000 – 100 000 клеток Кеньона без падения скорости.
|
||||||
|
"""
|
||||||
|
import torch
|
||||||
|
dev = torch.device(device)
|
||||||
|
y_t = torch.as_tensor(y, dtype=torch.float32, device=dev)
|
||||||
|
n = y_t.shape[0]
|
||||||
|
k = self.n_active
|
||||||
|
|
||||||
|
m = torch.as_tensor(self.mean, dtype=torch.float32, device=dev)
|
||||||
|
s = torch.as_tensor(self.scale, dtype=torch.float32, device=dev)
|
||||||
|
w_proj = torch.as_tensor(self.W, dtype=torch.float32, device=dev).T.contiguous()
|
||||||
|
|
||||||
|
chunk = max(1, int(2 ** 26 // max(self.cfg.n_kc, 1)))
|
||||||
|
a_list, v_list = [], []
|
||||||
|
with torch.no_grad():
|
||||||
|
for i in range(0, n, chunk):
|
||||||
|
t = torch.as_tensor(X[i:i + chunk], dtype=torch.float32, device=dev)
|
||||||
|
y_proj = torch.relu(((t - m) / s) @ w_proj)
|
||||||
|
v_chunk, a_chunk = torch.topk(y_proj, k, dim=1)
|
||||||
|
v_chunk = v_chunk * (k / v_chunk.sum(1, keepdim=True).clamp_min(1e-6))
|
||||||
|
a_list.append(a_chunk)
|
||||||
|
v_list.append(v_chunk)
|
||||||
|
|
||||||
|
a_t = torch.cat(a_list, dim=0) if len(a_list) > 1 else a_list[0]
|
||||||
|
v_t = torch.cat(v_list, dim=0) if len(v_list) > 1 else v_list[0]
|
||||||
|
|
||||||
|
w_mbon_t = torch.as_tensor(self.w_mbon, dtype=torch.float32, device=dev)
|
||||||
|
bias_t = torch.tensor(self.bias, dtype=torch.float32, device=dev)
|
||||||
|
flat_a = a_t.reshape(-1)
|
||||||
|
|
||||||
|
for ep in range(epochs):
|
||||||
|
z = bias_t + torch.sum(w_mbon_t[a_t] * v_t, dim=1)
|
||||||
|
p = torch.sigmoid(z)
|
||||||
|
g = (p - y_t) / n
|
||||||
|
weights = (g.unsqueeze(1) * v_t).reshape(-1)
|
||||||
|
grad = torch.zeros(self.cfg.n_kc, dtype=torch.float32, device=dev)
|
||||||
|
grad.scatter_add_(0, flat_a, weights)
|
||||||
|
w_mbon_t -= lr * (grad + l2 * w_mbon_t)
|
||||||
|
bias_t -= lr * g.sum()
|
||||||
|
|
||||||
|
if verbose and (ep + 1) % 50 == 0:
|
||||||
|
loss = -(y_t * torch.log(p.clamp_min(1e-7)) + (1.0 - y_t) * torch.log((1.0 - p).clamp_min(1e-7))).mean()
|
||||||
|
print(f" [GPU] эпоха {ep + 1:4d}: логистическая потеря {loss.item():.4f}")
|
||||||
|
|
||||||
|
zr_t = bias_t + torch.sum(w_mbon_t[a_t] * v_t, dim=1)
|
||||||
|
|
||||||
|
# Калибровка Платта полностью на GPU
|
||||||
|
g_t = torch.tensor(1.0, dtype=torch.float32, device=dev)
|
||||||
|
sh_t = torch.tensor(0.0, dtype=torch.float32, device=dev)
|
||||||
|
for _ in range(400):
|
||||||
|
p_cal = torch.sigmoid(g_t * zr_t + sh_t)
|
||||||
|
e_cal = p_cal - y_t
|
||||||
|
g_t -= 2.0 * (e_cal * zr_t).mean() / torch.clamp_min((zr_t * zr_t).mean(), 1e-6)
|
||||||
|
sh_t -= 2.0 * e_cal.mean()
|
||||||
|
|
||||||
|
self.w_mbon = w_mbon_t.cpu().numpy()
|
||||||
|
self.bias = np.float32(bias_t.item())
|
||||||
|
self.gain = np.float32(g_t.item())
|
||||||
|
self.shift = np.float32(sh_t.item())
|
||||||
|
|
||||||
|
zr = zr_t.cpu().numpy()
|
||||||
|
zc = self.gain * zr + self.shift
|
||||||
|
dd = np.exp(np.asarray(X[:, 0], np.float64))
|
||||||
|
ref = []
|
||||||
|
for lo, hi in zip(self.ref_edges[:-1], self.ref_edges[1:]):
|
||||||
|
m_mask = (dd >= lo) & (dd < hi)
|
||||||
|
ref.append(float(np.median(zc[m_mask])) if m_mask.sum() >= 50
|
||||||
|
else (ref[-1] if ref else 0.0))
|
||||||
|
self.ref_z = np.array(ref, np.float32)
|
||||||
|
|
||||||
|
def learn(self, X: np.ndarray, y: np.ndarray, *, epochs: int = 60,
|
||||||
|
lr: float = 4.0, l2: float = 1e-5, device: str | None = None,
|
||||||
|
verbose: bool = False) -> None:
|
||||||
|
"""Логистическая регрессия по разрежённому коду — депрессия с учителем.
|
||||||
|
|
||||||
|
Поддерживает прозрачное переключение между NVIDIA GPU (CUDA) и CPU.
|
||||||
|
"""
|
||||||
|
target_dev = device or "cpu" # без явной просьбы — процессор, как раньше
|
||||||
|
if target_dev == "auto":
|
||||||
|
from .device import get_device
|
||||||
|
target_dev = get_device("auto")
|
||||||
|
|
||||||
|
if target_dev.startswith("cuda"):
|
||||||
|
try:
|
||||||
|
self._learn_gpu(X, y, epochs=epochs, lr=lr, l2=l2,
|
||||||
|
device=target_dev, verbose=verbose)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
from .device import notify_cuda_error
|
||||||
|
notify_cuda_error(e)
|
||||||
|
import logging
|
||||||
|
logging.getLogger("flyguard.mbon").warning("GPU learning failed (%s), fallback to CPU", e)
|
||||||
|
|
||||||
|
self._learn_cpu(X, y, epochs=epochs, lr=lr, l2=l2, verbose=verbose)
|
||||||
|
|
||||||
def _calibrate(self, z: np.ndarray, y: np.ndarray, iters: int = 400) -> None:
|
def _calibrate(self, z: np.ndarray, y: np.ndarray, iters: int = 400) -> None:
|
||||||
"""Шкалирование Платта: подобрать наклон и сдвиг по обучающей выборке."""
|
"""Шкалирование Платта: подобрать наклон и сдвиг по обучающей выборке."""
|
||||||
g, sh = 1.0, 0.0
|
g, sh = 1.0, 0.0
|
||||||
|
|
|
||||||
|
|
@ -152,18 +152,28 @@ class MushroomBody:
|
||||||
# целиком, поэтому кодирование идёт порциями фиксированного объёма
|
# целиком, поэтому кодирование идёт порциями фиксированного объёма
|
||||||
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 and device != "cpu":
|
target_dev = device
|
||||||
|
if target_dev == "auto":
|
||||||
|
from .device import get_device
|
||||||
|
target_dev = get_device("auto")
|
||||||
|
|
||||||
|
if target_dev and target_dev != "cpu":
|
||||||
|
try:
|
||||||
import torch
|
import torch
|
||||||
out = np.empty((X.shape[0], k), np.int64)
|
out = np.empty((X.shape[0], k), np.int64)
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
m = torch.as_tensor(self.mean, device=device)
|
m = torch.as_tensor(self.mean, device=target_dev)
|
||||||
s = torch.as_tensor(self.scale, device=device)
|
s = torch.as_tensor(self.scale, device=target_dev)
|
||||||
w = torch.as_tensor(self.W, device=device).T.contiguous()
|
w = torch.as_tensor(self.W, device=target_dev).T.contiguous()
|
||||||
for i in range(0, X.shape[0], chunk):
|
for i in range(0, X.shape[0], chunk):
|
||||||
t = torch.as_tensor(X[i:i + chunk], device=device)
|
t = torch.as_tensor(X[i:i + chunk], device=target_dev)
|
||||||
y = ((t - m) / s) @ w
|
y = ((t - m) / s) @ w
|
||||||
out[i:i + chunk] = torch.topk(y, k, dim=1).indices.cpu().numpy()
|
out[i:i + chunk] = torch.topk(y, k, dim=1).indices.cpu().numpy()
|
||||||
return out
|
return out
|
||||||
|
except Exception as e:
|
||||||
|
from .device import notify_cuda_error
|
||||||
|
notify_cuda_error(e)
|
||||||
|
# Fallback на CPU ниже
|
||||||
|
|
||||||
if X.shape[0] <= chunk:
|
if X.shape[0] <= chunk:
|
||||||
z = (X - self.mean) / self.scale
|
z = (X - self.mean) / self.scale
|
||||||
|
|
@ -203,7 +213,39 @@ class MushroomBody:
|
||||||
для набора сразу эквивалентно возведению в степень по числу попаданий, —
|
для набора сразу эквивалентно возведению в степень по числу попаданий, —
|
||||||
поэтому цикл по примерам не нужен.
|
поэтому цикл по примерам не нужен.
|
||||||
"""
|
"""
|
||||||
act = self.encode(X, device=device)
|
target_dev = device or "cpu" # без явной просьбы — процессор, как раньше
|
||||||
|
if target_dev == "auto":
|
||||||
|
from .device import get_device
|
||||||
|
target_dev = get_device("auto")
|
||||||
|
|
||||||
|
if target_dev.startswith("cuda"):
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
dev = torch.device(target_dev)
|
||||||
|
k = self.n_active
|
||||||
|
chunk = max(1, int(2 ** 26 // max(self.cfg.n_kc, 1)))
|
||||||
|
m = torch.as_tensor(self.mean, device=dev)
|
||||||
|
s = torch.as_tensor(self.scale, device=dev)
|
||||||
|
w = torch.as_tensor(self.W, device=dev).T.contiguous()
|
||||||
|
act_list = []
|
||||||
|
with torch.no_grad():
|
||||||
|
for i in range(0, X.shape[0], chunk):
|
||||||
|
t = torch.as_tensor(X[i:i + chunk], device=dev)
|
||||||
|
y = ((t - m) / s) @ w
|
||||||
|
top_idx = torch.topk(y, k, dim=1).indices
|
||||||
|
act_list.append(top_idx)
|
||||||
|
act_t = torch.cat(act_list, dim=0) if len(act_list) > 1 else act_list[0]
|
||||||
|
cnt_t = torch.bincount(act_t.reshape(-1), minlength=self.cfg.n_kc)
|
||||||
|
w_mbon_t = torch.as_tensor(self.w_mbon, device=dev)
|
||||||
|
w_mbon_t *= torch.pow(torch.tensor(1.0 - rate, device=dev), cnt_t.float())
|
||||||
|
self.w_mbon = w_mbon_t.cpu().numpy()
|
||||||
|
self.n_seen += X.shape[0]
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
from .device import notify_cuda_error
|
||||||
|
notify_cuda_error(e)
|
||||||
|
|
||||||
|
act = self.encode(X, device="cpu")
|
||||||
cnt = np.bincount(act.ravel(), minlength=self.cfg.n_kc)
|
cnt = np.bincount(act.ravel(), minlength=self.cfg.n_kc)
|
||||||
self.w_mbon *= np.power(1.0 - rate, cnt).astype(np.float32)
|
self.w_mbon *= np.power(1.0 - rate, cnt).astype(np.float32)
|
||||||
self.n_seen += len(act)
|
self.n_seen += len(act)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,23 @@ 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), каждая мерилась отдельно на
|
||||||
|
# двух сценах и полигоне (EXPERIMENTS п. 16):
|
||||||
|
# h_lo_core — пол в колее: между рельсами (|u| ≤ core_width) нижняя
|
||||||
|
# граница опускается, чтобы не терять лежащего человека.
|
||||||
|
# ПРИНЯТО, но только дальше 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):
|
||||||
|
|
@ -107,6 +124,16 @@ 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` — средний вес
|
||||||
|
|
@ -136,7 +163,7 @@ class Params:
|
||||||
# EXPERIMENTS п. 15.9.
|
# EXPERIMENTS п. 15.9.
|
||||||
warn_far: float = 0.30
|
warn_far: float = 0.30
|
||||||
warn_far_from: float = 90.0
|
warn_far_from: float = 90.0
|
||||||
leak_far: float = 0.12 # утечка улики на дальнем краю
|
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
|
||||||
|
|
@ -152,6 +179,11 @@ 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' (видеокарта, если есть).
|
||||||
|
# По умолчанию процессор, и это решение, а не осторожность: 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: нужны для оценки надвигания и для
|
||||||
|
|
@ -195,6 +227,8 @@ class FlyGuard:
|
||||||
readout: "MbonReadout | None" = None,
|
readout: "MbonReadout | None" = None,
|
||||||
track_readout=None):
|
track_readout=None):
|
||||||
self.p = params or Params()
|
self.p = params or Params()
|
||||||
|
from .device import get_device
|
||||||
|
self.device = get_device(self.p.device)
|
||||||
self.memory = memory if (memory is not None and self.p.enable_memory) else NoMemory()
|
self.memory = memory if (memory is not None and self.p.enable_memory) else NoMemory()
|
||||||
self.readout = readout if (readout is not None and self.p.enable_mbon) else None
|
self.readout = readout if (readout is not None and self.p.enable_mbon) else None
|
||||||
self.layout_full = layout
|
self.layout_full = layout
|
||||||
|
|
@ -220,7 +254,8 @@ 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,
|
||||||
|
|
@ -233,7 +268,8 @@ 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) if self.p.use_tracking
|
track_readout=track_readout,
|
||||||
|
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] = []
|
||||||
|
|
@ -321,7 +357,12 @@ class FlyGuard:
|
||||||
else STRAIGHT)
|
else STRAIGHT)
|
||||||
|
|
||||||
with t("lamina"):
|
with t("lamina"):
|
||||||
lam = lamina.process(tf.r, tf.valid)
|
dev = self.device
|
||||||
|
if dev.startswith("cuda"):
|
||||||
|
from .device import is_cuda_available
|
||||||
|
if not is_cuda_available():
|
||||||
|
self.device = dev = "cpu"
|
||||||
|
lam = lamina.process(tf.r, tf.valid, device=dev)
|
||||||
|
|
||||||
with t("ego"):
|
with t("ego"):
|
||||||
ego = (self.ego_est.update(tf, pc.stamp) if self.p.enable_motion
|
ego = (self.ego_est.update(tf, pc.stamp) if self.p.enable_motion
|
||||||
|
|
@ -339,7 +380,12 @@ class FlyGuard:
|
||||||
with t("lobula"):
|
with t("lobula"):
|
||||||
masks = gauge_mask(tf, self.corridor, half_width=self.p.half_width,
|
masks = gauge_mask(tf, self.corridor, half_width=self.p.half_width,
|
||||||
h_lo=self.p.h_lo, h_hi=self.p.h_hi,
|
h_lo=self.p.h_lo, h_hi=self.p.h_hi,
|
||||||
d_min=self.p.d_min, d_max=self.p.d_max)
|
d_min=self.p.d_min, d_max=self.p.d_max,
|
||||||
|
k_sigma=self.p.k_sigma,
|
||||||
|
h_lo_core=self.p.h_lo_core,
|
||||||
|
core_width=self.p.core_width,
|
||||||
|
core_from=self.p.core_from,
|
||||||
|
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,
|
||||||
h_hi=self.p.h_hi, ctx_up=self.p.ctx_up,
|
h_hi=self.p.h_hi, ctx_up=self.p.ctx_up,
|
||||||
|
|
@ -351,7 +397,12 @@ class FlyGuard:
|
||||||
d_min=self.p.d_min,
|
d_min=self.p.d_min,
|
||||||
d_max=self.p.d_max, min_rays=self.p.min_rays,
|
d_max=self.p.d_max, min_rays=self.p.min_rays,
|
||||||
min_rays_far=self.p.min_rays_far,
|
min_rays_far=self.p.min_rays_far,
|
||||||
min_rays_far_from=self.p.min_rays_far_from)
|
min_rays_far_from=self.p.min_rays_far_from,
|
||||||
|
k_sigma=self.p.k_sigma,
|
||||||
|
h_lo_core=self.p.h_lo_core,
|
||||||
|
core_width=self.p.core_width,
|
||||||
|
core_from=self.p.core_from,
|
||||||
|
platform_filter=self.p.platform_filter)
|
||||||
|
|
||||||
with t("mushroom"):
|
with t("mushroom"):
|
||||||
cands = self.memory.annotate(cands)
|
cands = self.memory.annotate(cands)
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,8 @@ def catalogue() -> dict[str, ObjectModel]:
|
||||||
Sphere(radius=0.11, h_centre=1.60)], reflectivity=0.35),
|
Sphere(radius=0.11, h_centre=1.60)], reflectivity=0.35),
|
||||||
"человек_сидя": ObjectModel("человек_сидя", [
|
"человек_сидя": ObjectModel("человек_сидя", [
|
||||||
Box(0.45, 0.50, 0.85)], reflectivity=0.35),
|
Box(0.45, 0.50, 0.85)], reflectivity=0.35),
|
||||||
|
"человек_лежа": ObjectModel("человек_лежа", [
|
||||||
|
Box(length=1.80, width=0.50, height=0.30, h_base=0.0)], reflectivity=0.35),
|
||||||
"ящик": ObjectModel("ящик", [Box(0.60, 0.60, 0.60)], reflectivity=0.40),
|
"ящик": ObjectModel("ящик", [Box(0.60, 0.60, 0.60)], reflectivity=0.40),
|
||||||
"чемодан": ObjectModel("чемодан", [Box(0.25, 0.45, 0.55)], reflectivity=0.30),
|
"чемодан": ObjectModel("чемодан", [Box(0.25, 0.45, 0.55)], reflectivity=0.30),
|
||||||
"ведро": ObjectModel("ведро", [Cylinder(radius=0.15, height=0.35)], reflectivity=0.45),
|
"ведро": ObjectModel("ведро", [Cylinder(radius=0.15, height=0.35)], reflectivity=0.45),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ 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
|
||||||
# только для обучения на GPU (tools/train_mbon.py --device cuda); ядру не нужен
|
# только для обучения на GPU (tools/train_mbon.py --device cuda); ядру не нужен
|
||||||
# torch>=2.0
|
# torch>=2.0
|
||||||
pytest>=7.0
|
pytest>=7.0
|
||||||
|
|
|
||||||
|
|
@ -540,3 +540,289 @@ def test_track_gate_removes_the_weak_track_without_touching_the_strong_one():
|
||||||
assert {o.track_id for o in out.objects} == {1}
|
assert {o.track_id for o in out.objects} == {1}
|
||||||
assert out.objects[0].confidence == pytest.approx(
|
assert out.objects[0].confidence == pytest.approx(
|
||||||
DescendingNeurons().decide(_pair_of_tracks(), speed=10.0).objects[0].confidence)
|
DescendingNeurons().decide(_pair_of_tracks(), speed=10.0).objects[0].confidence)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ улучшения габарита и решения
|
||||||
|
|
||||||
|
class _MockTrackFrame:
|
||||||
|
def __init__(self, d, u, h, valid=None):
|
||||||
|
self.d = np.asarray(d, np.float32)
|
||||||
|
self.u = np.asarray(u, np.float32)
|
||||||
|
self.h = np.asarray(h, np.float32)
|
||||||
|
self.valid = np.ones_like(self.d, dtype=bool) if valid is None else np.asarray(valid, bool)
|
||||||
|
|
||||||
|
def lateral(self, corridor=None):
|
||||||
|
if corridor is None or corridor.n_slices == 0:
|
||||||
|
return self.u
|
||||||
|
curved = self.u - corridor.centre(self.d)
|
||||||
|
return np.where(np.abs(curved) < np.abs(self.u), curved, self.u)
|
||||||
|
|
||||||
|
|
||||||
|
def test_gauge_mask_zonal_floor_and_corridor_expansion():
|
||||||
|
"""Проверка зонального пола и расширения габарита по неопределённости оси."""
|
||||||
|
from flyguard.geometry import STRAIGHT, Corridor
|
||||||
|
from flyguard.lobula import gauge_mask
|
||||||
|
|
||||||
|
# 1. Зональный пол: в колее (|u| <= 0.85) порог 0.16 м, снаружи — 0.28 м
|
||||||
|
tf = _MockTrackFrame(
|
||||||
|
d=[50.0, 50.0],
|
||||||
|
u=[0.4, 1.2],
|
||||||
|
h=[0.20, 0.20] # оба 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)
|
||||||
|
assert inside[0] # в колее 0.20 м > 0.16 м — внутри габарита!
|
||||||
|
assert not inside[1] # на периферии 0.20 м < 0.28 м — отсечено!
|
||||||
|
|
||||||
|
# 2. Расширение габарита на дальней дистанции при кривизне
|
||||||
|
corridor = Corridor(np.array([0.0, 0.0, 0.0003], np.float32), 60.0, 10, 1600.0)
|
||||||
|
tf_far = _MockTrackFrame(
|
||||||
|
d=[150.0],
|
||||||
|
u=[1.90],
|
||||||
|
h=[1.0]
|
||||||
|
)
|
||||||
|
# Без расширения (k_sigma=0): |u| = 1.9 > 1.6 -> отсечено
|
||||||
|
inside_rigid, _, _ = gauge_mask(tf_far, corridor, half_width=1.6, h_lo=0.28, h_hi=2.3,
|
||||||
|
d_min=4.0, d_max=220.0, k_sigma=0.0)
|
||||||
|
assert not inside_rigid[0]
|
||||||
|
|
||||||
|
# С расширением (k_sigma=0.75): w_eff > 1.9 м -> объект удержан
|
||||||
|
inside_exp, _, _ = gauge_mask(tf_far, corridor, half_width=1.6, h_lo=0.28, h_hi=2.3,
|
||||||
|
d_min=4.0, d_max=220.0, k_sigma=0.75)
|
||||||
|
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():
|
||||||
|
"""Настил платформы (h~1.1-1.2 м, |u| >= 1.30 м) отсекается, а препятствие в габарите остаётся."""
|
||||||
|
from flyguard.geometry import STRAIGHT
|
||||||
|
from flyguard.lobula import gauge_mask
|
||||||
|
|
||||||
|
tf = _MockTrackFrame(
|
||||||
|
d=[40.0, 40.0],
|
||||||
|
u=[1.40, 0.90],
|
||||||
|
h=[1.15, 1.15]
|
||||||
|
)
|
||||||
|
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, platform_filter=True)
|
||||||
|
assert not inside[0] # настил платформы отсечён
|
||||||
|
assert inside[1] # препятствие на колее сохранено
|
||||||
|
|
||||||
|
|
||||||
|
def test_descending_far_soft_warning_triggers_without_emergency():
|
||||||
|
"""На 130 м устойчивый трек поднимает предупреждение, но не экстренное торможение."""
|
||||||
|
from flyguard.central_complex import CentralComplex, Track
|
||||||
|
from flyguard.descending import DescendingNeurons
|
||||||
|
|
||||||
|
cx = CentralComplex()
|
||||||
|
cx.s_world = 0.0
|
||||||
|
far_track = Track(
|
||||||
|
id=1, s_world=130.0, u=0.2, h=0.5, width=0.5, height=1.6,
|
||||||
|
evidence=0.20, hits=3, age=5, novelty=0.12,
|
||||||
|
n_obs=3, w_sum=3 * 0.85, p_sum=3 * 0.85
|
||||||
|
)
|
||||||
|
cx.tracks = [far_track]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
assert decision.detected is True
|
||||||
|
assert decision.emergency is False
|
||||||
|
assert decision.distance == pytest.approx(130.0, abs=1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_frame_markers_and_threat_levels():
|
||||||
|
"""Проверка генерации 3D BBox, расчета уровня угрозы и маркеров RViz."""
|
||||||
|
from flyguard.descending import Decision, DetectedObject
|
||||||
|
from flyguard.export import ThreatLevel, export_frame
|
||||||
|
from flyguard.geometry import RailPlane, STRAIGHT
|
||||||
|
|
||||||
|
plane = RailPlane(a=0.01, b=0.0, c=-1.80, inliers=100, rms=0.02)
|
||||||
|
dec = Decision(
|
||||||
|
detected=True,
|
||||||
|
emergency=False,
|
||||||
|
distance=45.0,
|
||||||
|
ttc=4.5,
|
||||||
|
speed=10.0,
|
||||||
|
stopping_distance=50.0,
|
||||||
|
objects=[
|
||||||
|
DetectedObject(
|
||||||
|
distance=45.0, lateral=0.3, height=0.6, width=0.5, size_v=1.2,
|
||||||
|
confidence=0.92, novelty=0.8, n_rays=15, track_id=42, ttc=4.5
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
exp = export_frame(dec, plane, STRAIGHT, stamp=1710000000.5)
|
||||||
|
assert exp.threat_level == ThreatLevel.WARNING
|
||||||
|
assert len(exp.boxes) == 1
|
||||||
|
b = exp.boxes[0]
|
||||||
|
assert b.track_id == 42
|
||||||
|
assert b.distance_along_track == 45.0
|
||||||
|
assert b.x == pytest.approx(0.3)
|
||||||
|
assert b.y == pytest.approx(-45.0)
|
||||||
|
# z = h + a*d + b*u + c = 0.6 + 0.01*45 + 0.0 + (-1.8) = -0.75
|
||||||
|
assert b.z == pytest.approx(0.6 + 0.01 * 45.0 - 1.80)
|
||||||
|
|
||||||
|
# Проверка сериализации в словарь и RViz маркеры
|
||||||
|
d = exp.to_dict()
|
||||||
|
assert d["threat_level"] == "WARNING"
|
||||||
|
assert d["n_objects"] == 1
|
||||||
|
|
||||||
|
markers = exp.to_rviz_markers(frame_id="hesai_pandar")
|
||||||
|
assert len(markers) >= 2 # bbox cube + text label
|
||||||
|
|
||||||
|
|
||||||
|
def test_benchmark_augmentation_logic():
|
||||||
|
"""Проверка генерации сценариев и логики аугментаций в make_benchmark."""
|
||||||
|
from make_benchmark import _ScenarioState
|
||||||
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
|
|
||||||
|
rng = np.random.default_rng(42)
|
||||||
|
p = FlyGuard(Params())
|
||||||
|
|
||||||
|
# 1. Спавн в диапазоне [40, 200] м
|
||||||
|
d_start_min, d_start_max = 40.0, 200.0
|
||||||
|
spawns = [float(rng.uniform(d_start_min, d_start_max)) for _ in range(100)]
|
||||||
|
assert all(40.0 <= d <= 200.0 for d in spawns)
|
||||||
|
assert min(spawns) < 60.0 and max(spawns) > 180.0
|
||||||
|
|
||||||
|
# 2. Боковой дрейф для пешеходов
|
||||||
|
v_lat_max = 0.35
|
||||||
|
v_lats = [float(rng.uniform(-v_lat_max, v_lat_max)) for _ in range(100)]
|
||||||
|
assert all(-0.35 <= v <= 0.35 for v in v_lats)
|
||||||
|
|
||||||
|
# 3. Шум по дальности
|
||||||
|
s_noise_std = 0.12
|
||||||
|
noises = [float(np.clip(rng.normal(0.0, s_noise_std), -0.35, 0.35)) for _ in range(500)]
|
||||||
|
assert abs(np.mean(noises)) < 0.05
|
||||||
|
assert 0.08 < np.std(noises) < 0.15
|
||||||
|
|
||||||
|
# 4. _ScenarioState
|
||||||
|
sc = _ScenarioState(name="человек_лежа", lat_base=0.0, pipe=p,
|
||||||
|
d_init=120.0, v_lat=0.15, is_static=False)
|
||||||
|
assert sc.name == "человек_лежа"
|
||||||
|
assert sc.v_lat == 0.15
|
||||||
|
assert not sc.is_static
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_detection_and_fallback():
|
||||||
|
"""Проверка автоматического определения GPU устройства и fallback на CPU."""
|
||||||
|
from flyguard.device import get_device, get_device_info, is_cuda_available, to_numpy
|
||||||
|
|
||||||
|
dev = get_device("auto")
|
||||||
|
assert dev in ("cpu", "cuda") or dev.startswith("cuda:")
|
||||||
|
|
||||||
|
if not is_cuda_available():
|
||||||
|
assert get_device("cuda") == "cpu"
|
||||||
|
assert get_device("cpu") == "cpu"
|
||||||
|
|
||||||
|
info = get_device_info()
|
||||||
|
assert "device" in info
|
||||||
|
assert "torch_available" in info
|
||||||
|
|
||||||
|
arr = np.array([1.0, 2.0, 3.0], dtype=np.float32)
|
||||||
|
assert np.array_equal(to_numpy(arr), arr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_lamina_device_routing():
|
||||||
|
"""Проверка работы Lamina с указанием устройства (cpu / auto)."""
|
||||||
|
from flyguard import lamina
|
||||||
|
|
||||||
|
r = np.full((16, 32), 20.0, dtype=np.float32)
|
||||||
|
r[8, 16] = 5.0 # препятствие
|
||||||
|
valid = np.ones((16, 32), dtype=bool)
|
||||||
|
|
||||||
|
out_cpu = lamina.process(r, valid, device="cpu")
|
||||||
|
out_auto = lamina.process(r, valid, device="auto")
|
||||||
|
|
||||||
|
assert out_cpu.on.shape == (16, 32)
|
||||||
|
assert out_auto.on.shape == (16, 32)
|
||||||
|
assert out_cpu.on[8, 16] > 0.0
|
||||||
|
assert np.allclose(out_cpu.on, out_auto.on, atol=1e-5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_device_runtime_failure_and_fallback():
|
||||||
|
"""Проверка динамического перехода на CPU при сбое/отвале GPU в рантайме."""
|
||||||
|
from flyguard.device import is_cuda_available, get_device, notify_cuda_error, reset_device_cache
|
||||||
|
from flyguard import lamina
|
||||||
|
|
||||||
|
# Симуляция критического сбоя GPU
|
||||||
|
notify_cuda_error(RuntimeError("Simulated CUDA device disconnect / OOM"))
|
||||||
|
try:
|
||||||
|
assert not is_cuda_available()
|
||||||
|
assert get_device("cuda") == "cpu"
|
||||||
|
assert get_device("auto") == "cpu"
|
||||||
|
|
||||||
|
r = np.full((16, 32), 20.0, dtype=np.float32)
|
||||||
|
r[8, 16] = 4.0
|
||||||
|
valid = np.ones((16, 32), dtype=bool)
|
||||||
|
|
||||||
|
# Даже при явном указании device="cuda", Lamina должна успешно отработать на CPU
|
||||||
|
out = lamina.process(r, valid, device="cuda")
|
||||||
|
assert out.on.shape == (16, 32)
|
||||||
|
assert out.on[8, 16] > 0.0
|
||||||
|
finally:
|
||||||
|
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)
|
||||||
|
|
|
||||||
47
tools/_params.py
Normal file
47
tools/_params.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
"""Общий ключ `--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,6 +12,7 @@ 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
|
||||||
|
|
@ -29,6 +30,7 @@ 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="сравнить признаки трека у настоящего объекта и у "
|
||||||
"ложных треков: единственная проверка, где метка "
|
"ложных треков: единственная проверка, где метка "
|
||||||
|
|
@ -41,7 +43,8 @@ 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(), memory=memory, readout=readout)
|
fg = FlyGuard(Params(**PS.apply({}, args.set, Params)), memory=memory,
|
||||||
|
readout=readout)
|
||||||
bag = Bag(args.bag)
|
bag = Bag(args.bag)
|
||||||
|
|
||||||
n = cand_hit = track_hit = other = 0
|
n = cand_hit = track_hit = other = 0
|
||||||
|
|
|
||||||
83
tools/compare_benchmark.py
Normal file
83
tools/compare_benchmark.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"""Парное сравнение двух прогонов полигона на одних и тех же вставках.
|
||||||
|
|
||||||
|
Полигон шумит сильнее, чем меняет его большинство правок: две реализации
|
||||||
|
случайности при одинаковых настройках расходятся на 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,6 +21,7 @@ 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
|
||||||
|
|
@ -106,7 +107,8 @@ def main() -> None:
|
||||||
ap.add_argument("--extra-cache", default=str(B.CACHE / "new_data_candidates.npz"))
|
ap.add_argument("--extra-cache", default=str(B.CACHE / "new_data_candidates.npz"))
|
||||||
ap.add_argument("--limit", type=int, default=250)
|
ap.add_argument("--limit", type=int, default=250)
|
||||||
ap.add_argument("--target", type=float, default=0.4)
|
ap.add_argument("--target", type=float, default=0.4)
|
||||||
ap.add_argument("--device", default="cpu")
|
ap.add_argument("--device", default="auto",
|
||||||
|
help="устройство вычислений ('auto', 'cuda', 'cpu')")
|
||||||
ap.add_argument("--split-adv", type=float, default=None,
|
ap.add_argument("--split-adv", type=float, default=None,
|
||||||
help="порог разделения фигуры и фона; 0 — выключить")
|
help="порог разделения фигуры и фона; 0 — выключить")
|
||||||
ap.add_argument("--split-gap", type=float, default=None,
|
ap.add_argument("--split-gap", type=float, default=None,
|
||||||
|
|
@ -171,6 +173,7 @@ 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)
|
||||||
|
|
@ -235,7 +238,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(**over)
|
params = Params(**PS.apply(over, args.set, Params))
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,14 @@ from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import zlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -31,14 +34,47 @@ 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
|
||||||
|
class _ScenarioState:
|
||||||
|
name: str
|
||||||
|
lat_base: float
|
||||||
|
pipe: FlyGuard
|
||||||
|
d_init: float
|
||||||
|
v_lat: float
|
||||||
|
is_static: bool
|
||||||
|
s_start: float = 0.0
|
||||||
|
t0: float | None = None
|
||||||
|
rng: np.random.Generator | None = None
|
||||||
|
|
||||||
|
|
||||||
def _work(task):
|
def _work(task):
|
||||||
"""Одна задача — один бэг.
|
"""Одна задача — один бэг.
|
||||||
|
|
||||||
Обученное грузится путями и уже внутри процесса: передавать модели через
|
Обученное грузится путями и уже внутри процесса: передавать модели через
|
||||||
межпроцессную границу незачем, а свою складку каждый воркер берёт сам.
|
межпроцессную границу незачем, а свою складку каждый воркер берёт сам.
|
||||||
"""
|
"""
|
||||||
|
if len(task) >= 11:
|
||||||
|
(path, params, limit, d_start, laterals, seed, mem_path, rd_path,
|
||||||
|
want_tracks, trk_path, aug_params) = task[:11]
|
||||||
|
else:
|
||||||
(path, params, limit, d_start, laterals, seed, mem_path, rd_path,
|
(path, params, limit, d_start, laterals, seed, mem_path, rd_path,
|
||||||
want_tracks, trk_path) = task
|
want_tracks, trk_path) = task
|
||||||
|
aug_params = {}
|
||||||
memory = MushroomBody.load(mem_path) if mem_path else None
|
memory = MushroomBody.load(mem_path) if mem_path else None
|
||||||
readout = None
|
readout = None
|
||||||
if rd_path:
|
if rd_path:
|
||||||
|
|
@ -50,7 +86,7 @@ def _work(task):
|
||||||
track_rd = TrackReadout.load(trk_path)
|
track_rd = TrackReadout.load(trk_path)
|
||||||
return run_bag(path, params, memory, limit, d_start, laterals, seed,
|
return run_bag(path, params, memory, limit, d_start, laterals, seed,
|
||||||
readout=readout, want_tracks=want_tracks,
|
readout=readout, want_tracks=want_tracks,
|
||||||
track_readout=track_rd)
|
track_readout=track_rd, aug_params=aug_params)
|
||||||
|
|
||||||
|
|
||||||
def ego_track(bag: Bag, params: Params, limit: int | None):
|
def ego_track(bag: Bag, params: Params, limit: int | None):
|
||||||
|
|
@ -69,7 +105,17 @@ def ego_track(bag: Bag, params: Params, limit: int | None):
|
||||||
|
|
||||||
def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
laterals: tuple[float, ...], seed: int, readout=None,
|
laterals: tuple[float, ...], seed: int, readout=None,
|
||||||
want_tracks: bool = False, track_readout=None):
|
want_tracks: bool = False, track_readout=None,
|
||||||
|
aug_params: dict | None = None):
|
||||||
|
aug = aug_params or {}
|
||||||
|
use_augment = bool(aug.get("augment", False))
|
||||||
|
d_start_min = float(aug.get("d_start_min", 40.0))
|
||||||
|
d_start_max = float(aug.get("d_start_max", 200.0))
|
||||||
|
v_lat_max = float(aug.get("v_lat_max", 0.35))
|
||||||
|
s_noise_std = float(aug.get("s_noise_std", 0.12))
|
||||||
|
respawn = bool(aug.get("respawn", False))
|
||||||
|
static_prob = float(aug.get("static_prob", 0.15))
|
||||||
|
|
||||||
bag = Bag(bag_path)
|
bag = Bag(bag_path)
|
||||||
s_track, _ = ego_track(bag, params, limit)
|
s_track, _ = ego_track(bag, params, limit)
|
||||||
have = [x for x in s_track if x is not None]
|
have = [x for x in s_track if x is not None]
|
||||||
|
|
@ -79,9 +125,6 @@ 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]
|
||||||
pipes = [FlyGuard(params, memory=memory, readout=readout,
|
|
||||||
track_readout=track_readout) for _ in scen]
|
|
||||||
rng = np.random.default_rng(seed)
|
|
||||||
records = [[] for _ in scen]
|
records = [[] for _ in scen]
|
||||||
# Выборка для считывания по трекам: описание каждого живого трека на
|
# Выборка для считывания по трекам: описание каждого живого трека на
|
||||||
# каждом кадре и метка «это вставленный предмет». Собирается здесь, а не
|
# каждом кадре и метка «это вставленный предмет». Собирается здесь, а не
|
||||||
|
|
@ -90,6 +133,25 @@ 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:
|
||||||
|
# при повторном появлении поток продолжается, а не начинается заново:
|
||||||
|
# иначе каждое возрождение повторяло бы одну и ту же дальность и дрейф
|
||||||
|
rng = rng if rng is not None else scenario_rng(seed, name, lat)
|
||||||
|
p = FlyGuard(params, memory=memory, readout=readout, track_readout=track_readout)
|
||||||
|
if use_augment:
|
||||||
|
d_init = float(rng.uniform(d_start_min, d_start_max))
|
||||||
|
is_static = bool(rng.uniform(0.0, 1.0) < static_prob)
|
||||||
|
is_human = "человек" in name
|
||||||
|
v_lat = float(rng.uniform(-v_lat_max, v_lat_max)) if (is_human or rng.uniform(0.0, 1.0) < 0.6) else 0.0
|
||||||
|
else:
|
||||||
|
d_init = d_start
|
||||||
|
is_static = False
|
||||||
|
v_lat = 0.0
|
||||||
|
return _ScenarioState(name=name, lat_base=lat, pipe=p, rng=rng, d_init=d_init,
|
||||||
|
v_lat=v_lat, is_static=is_static)
|
||||||
|
|
||||||
|
scenarios = [_create_sc(name, lat) for name, lat in scen]
|
||||||
|
|
||||||
# решётка и поза нужны для вставки — берутся из отдельного «чистого» конвейера
|
# решётка и поза нужны для вставки — берутся из отдельного «чистого» конвейера
|
||||||
guide = FlyGuard(params, memory=None)
|
guide = FlyGuard(params, memory=None)
|
||||||
|
|
||||||
|
|
@ -99,17 +161,35 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
continue
|
continue
|
||||||
s_now = s_track[k] - have[0]
|
s_now = s_track[k] - have[0]
|
||||||
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
||||||
for i, (name, lat) in enumerate(scen):
|
for i, sc in enumerate(scenarios):
|
||||||
d_true = d_start - s_now
|
if sc.t0 is None:
|
||||||
|
sc.t0 = pc.stamp
|
||||||
|
sc.s_start = s_now
|
||||||
|
dt = max(pc.stamp - sc.t0, 0.0)
|
||||||
|
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
|
||||||
|
d_true = sc.d_init - ds + noise_s
|
||||||
|
if d_true < 6.0:
|
||||||
|
if respawn and use_augment:
|
||||||
|
scenarios[i] = _create_sc(sc.name, sc.lat_base, sc.rng)
|
||||||
|
sc = scenarios[i]
|
||||||
|
sc.t0 = pc.stamp
|
||||||
|
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
|
||||||
|
d_true = sc.d_init + noise_s
|
||||||
if d_true < 6.0:
|
if d_true < 6.0:
|
||||||
continue
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
# Предмет лежит НА ПУТИ, а путь в кривой уходит вбок: на 150 м при
|
# Предмет лежит НА ПУТИ, а путь в кривой уходит вбок: на 150 м при
|
||||||
# радиусе 1300 м это 8.6 м. Если ставить его в поперечных координатах
|
# радиусе 1300 м это 8.6 м. Если ставить его в поперечных координатах
|
||||||
# сенсора, он окажется в стене, а не в габарите.
|
# сенсора, он окажется в стене, а не в габарите.
|
||||||
u_obj = float(gres.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
lat_t = float(np.clip(sc.lat_base + sc.v_lat * dt, -1.25, 1.25))
|
||||||
pc2, lab = inject(pc, guide.layout_full, gres.plane, cat[name],
|
u_obj = float(gres.corridor.centre(np.array([d_true], np.float32))[0]) + lat_t
|
||||||
Placement(d=d_true, u=u_obj), rng=rng, env=env)
|
pc2, lab = inject(pc, guide.layout_full, gres.plane, cat[sc.name],
|
||||||
res = pipes[i].process(pc2)
|
Placement(d=d_true, u=u_obj), rng=sc.rng, env=env)
|
||||||
|
res = sc.pipe.process(pc2)
|
||||||
if res is None:
|
if res is None:
|
||||||
continue
|
continue
|
||||||
tol = max(3.0, 0.12 * d_true)
|
tol = max(3.0, 0.12 * d_true)
|
||||||
|
|
@ -121,7 +201,7 @@ def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
# фона, нет трека — к сопоставлению по кадрам, нет решения —
|
# фона, нет трека — к сопоставлению по кадрам, нет решения —
|
||||||
# к порогу. Без этого разбиения улучшать нечего, кроме удачи.
|
# к порогу. Без этого разбиения улучшать нечего, кроме удачи.
|
||||||
cand = any(abs(c.d - d_true) < tol for c in res.candidates)
|
cand = any(abs(c.d - d_true) < tol for c in res.candidates)
|
||||||
cx = pipes[i].cx
|
cx = sc.pipe.cx
|
||||||
trk = any(abs(tr.distance(cx.s_world) - d_true) < tol
|
trk = any(abs(tr.distance(cx.s_world) - d_true) < tol
|
||||||
for tr in cx.tracks)
|
for tr in cx.tracks)
|
||||||
records[i].append((d_true, int(hit), fp, lab["hit_rays"],
|
records[i].append((d_true, int(hit), fp, lab["hit_rays"],
|
||||||
|
|
@ -213,7 +293,25 @@ def main() -> None:
|
||||||
help="к какой дальности вклад знакомости обнуляется")
|
help="к какой дальности вклад знакомости обнуляется")
|
||||||
ap.add_argument("--mbon-prior-from", type=float, default=None,
|
ap.add_argument("--mbon-prior-from", type=float, default=None,
|
||||||
help="с какой дальности поправлять оценку модели на распространённость предметов; 0 — не поправлять")
|
help="с какой дальности поправлять оценку модели на распространённость предметов; 0 — не поправлять")
|
||||||
|
ap.add_argument("--augment", action="store_true",
|
||||||
|
help="включить случайные аугментации: d_start ∈ [d_min, d_max], "
|
||||||
|
"боковой дрейф v_lat, шум по дальности δs и стоячие поезда")
|
||||||
|
ap.add_argument("--d-start-min", type=float, default=40.0,
|
||||||
|
help="минимальная дальность спавна при аугментации, м")
|
||||||
|
ap.add_argument("--d-start-max", type=float, default=200.0,
|
||||||
|
help="максимальная дальность спавна при аугментации, м")
|
||||||
|
ap.add_argument("--v-lat-max", type=float, default=0.35,
|
||||||
|
help="максимальная боковая скорость препятствия (дрейф), м/с")
|
||||||
|
ap.add_argument("--s-noise-std", type=float, default=0.12,
|
||||||
|
help="СКО шума по продольной координате/дальности (джиттер лучей), м")
|
||||||
|
ap.add_argument("--respawn", action="store_true",
|
||||||
|
help="пересоздавать препятствие впереди при приближении ближе 6 м")
|
||||||
|
ap.add_argument("--static-prob", type=float, default=0.15,
|
||||||
|
help="вероятность сценария со стоячим поездом (ds = 0)")
|
||||||
|
ap.add_argument("--device", default="auto", choices=["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)
|
||||||
|
|
@ -275,7 +373,8 @@ 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(**over)
|
over["device"] = args.device
|
||||||
|
params = Params(**PS.apply(over, args.set, Params))
|
||||||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
@ -289,8 +388,17 @@ def main() -> None:
|
||||||
print(f" внимание: для {p.name} нет своей складки — пропуск")
|
print(f" внимание: для {p.name} нет своей складки — пропуск")
|
||||||
continue
|
continue
|
||||||
trk = track_paths.get(p.name, args.track) if track_paths else args.track
|
trk = track_paths.get(p.name, args.track) if track_paths else args.track
|
||||||
|
aug_params = {
|
||||||
|
"augment": args.augment,
|
||||||
|
"d_start_min": args.d_start_min,
|
||||||
|
"d_start_max": args.d_start_max,
|
||||||
|
"v_lat_max": args.v_lat_max,
|
||||||
|
"s_noise_std": args.s_noise_std,
|
||||||
|
"respawn": args.respawn,
|
||||||
|
"static_prob": args.static_prob,
|
||||||
|
}
|
||||||
tasks.append((p, params, args.limit, args.d_start, laterals, args.seed,
|
tasks.append((p, params, args.limit, args.d_start, laterals, args.seed,
|
||||||
args.memory or "", rd, bool(args.tracks_out), trk))
|
args.memory or "", rd, bool(args.tracks_out), trk, aug_params))
|
||||||
|
|
||||||
# Печатается по готовности, собирается по номеру задачи: порядок сценариев
|
# Печатается по готовности, собирается по номеру задачи: порядок сценариев
|
||||||
# в файле не должен зависеть от того, какой бэг досчитался первым.
|
# в файле не должен зависеть от того, какой бэг досчитался первым.
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ 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
|
||||||
|
|
@ -141,11 +142,12 @@ 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()
|
params = Params(**PS.apply({}, args.set, 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]
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,15 @@ import numpy as np
|
||||||
import _bootstrap as B # noqa: F401
|
import _bootstrap as B # noqa: F401
|
||||||
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.mbon_readout import MbonReadout
|
||||||
from flyguard.pipeline import FlyGuard, Params
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
|
from flyguard.track_readout import TrackReadout
|
||||||
|
|
||||||
|
|
||||||
def run(bag_path, params: Params, memory, limit: int | None, verbose: bool) -> dict:
|
def run(bag_path, params: Params, memory, readout=None, track_readout=None,
|
||||||
|
limit: int | None = None, verbose: bool = False) -> dict:
|
||||||
bag = Bag(bag_path)
|
bag = Bag(bag_path)
|
||||||
fg = FlyGuard(params, memory=memory)
|
fg = FlyGuard(params, memory=memory, readout=readout, track_readout=track_readout)
|
||||||
stages: dict[str, list[float]] = {}
|
stages: dict[str, list[float]] = {}
|
||||||
n_det = n_frames = 0
|
n_det = n_frames = 0
|
||||||
dists, speeds, ncand = [], [], []
|
dists, speeds, ncand = [], [], []
|
||||||
|
|
@ -65,15 +68,31 @@ def main() -> None:
|
||||||
ap.add_argument("--all", action="store_true")
|
ap.add_argument("--all", action="store_true")
|
||||||
ap.add_argument("--limit", type=int, default=150)
|
ap.add_argument("--limit", type=int, default=150)
|
||||||
ap.add_argument("--memory", default=None)
|
ap.add_argument("--memory", default=None)
|
||||||
|
ap.add_argument("--readout", default=None, help="модель MBON (mbon_readout.npz)")
|
||||||
|
ap.add_argument("--track-readout", default=None, help="модель TrackReadout (track_readout.npz)")
|
||||||
|
ap.add_argument("--device", default="auto", choices=["auto", "cuda", "cpu"],
|
||||||
|
help="устройство вычислений ('auto', 'cuda', 'cpu')")
|
||||||
ap.add_argument("--fov", type=float, default=30.0)
|
ap.add_argument("--fov", type=float, default=30.0)
|
||||||
ap.add_argument("--verbose", action="store_true")
|
ap.add_argument("--verbose", action="store_true")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
memory = MushroomBody.load(args.memory) if args.memory else None
|
memory = MushroomBody.load(args.memory) if args.memory else None
|
||||||
params = Params(fov_deg=args.fov)
|
readout = MbonReadout.load(args.readout) if args.readout else None
|
||||||
bags = find_bags(B.DATA / "for_hackathon") if args.all else [args.bag]
|
track_readout = TrackReadout.load(args.track_readout) if args.track_readout else None
|
||||||
|
|
||||||
|
if not args.all and not args.bag:
|
||||||
|
args.all = True
|
||||||
|
|
||||||
|
params = Params(fov_deg=args.fov, device=args.device)
|
||||||
|
bag_root = (B.DATA / "for_hackathon") if (B.DATA / "for_hackathon").exists() else B.DATA
|
||||||
|
bags = find_bags(bag_root) if args.all else ([args.bag] if args.bag else [])
|
||||||
|
if not bags:
|
||||||
|
print(f"Внимание: бэги не найдены в {bag_root}. Убедитесь, что каталог смонтирован в FLYGUARD_DATA.")
|
||||||
|
return
|
||||||
|
|
||||||
for b in bags:
|
for b in bags:
|
||||||
run(b, params, memory, args.limit, args.verbose)
|
run(b, params, memory, readout=readout, track_readout=track_readout,
|
||||||
|
limit=args.limit, verbose=args.verbose)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,15 @@ def main() -> None:
|
||||||
ap.add_argument("--out", default=str(B.ARTIFACTS / "mbon_readout.npz"))
|
ap.add_argument("--out", default=str(B.ARTIFACTS / "mbon_readout.npz"))
|
||||||
ap.add_argument("--n-kc", type=int, default=0,
|
ap.add_argument("--n-kc", type=int, default=0,
|
||||||
help="ёмкость итоговой модели; 0 — взять лучшую из развёртки")
|
help="ёмкость итоговой модели; 0 — взять лучшую из развёртки")
|
||||||
ap.add_argument("--sweep-kc", default="4000,8000,20000",
|
ap.add_argument("--sweep-kc", default="4000,8000,20000,50000",
|
||||||
help="ёмкости для развёртки; пусто — не разворачивать")
|
help="ёмкости для развёртки; пусто — не разворачивать")
|
||||||
ap.add_argument("--active", type=int, default=100,
|
ap.add_argument("--active", type=int, default=100,
|
||||||
help="активных клеток после торможения APL")
|
help="активных клеток после торможения APL")
|
||||||
ap.add_argument("--epochs", type=int, default=60)
|
ap.add_argument("--epochs", type=int, default=60)
|
||||||
ap.add_argument("--lr", type=float, default=4.0)
|
ap.add_argument("--lr", type=float, default=4.0)
|
||||||
ap.add_argument("--l2", type=float, default=1e-5)
|
ap.add_argument("--l2", type=float, default=1e-5)
|
||||||
ap.add_argument("--device", default="cpu")
|
ap.add_argument("--device", default="auto",
|
||||||
|
help="устройство обучения ('auto', 'cuda', 'cpu')")
|
||||||
ap.add_argument("--baseline", action="store_true",
|
ap.add_argument("--baseline", action="store_true",
|
||||||
help="сравнить с градиентным бустингом по сырым признакам")
|
help="сравнить с градиентным бустингом по сырым признакам")
|
||||||
ap.add_argument("--save-folds", default=str(B.ARTIFACTS / "mbon_folds"),
|
ap.add_argument("--save-folds", default=str(B.ARTIFACTS / "mbon_folds"),
|
||||||
|
|
@ -148,6 +149,12 @@ def main() -> None:
|
||||||
f"обстановки при 95% предметов {fpr_at_tpr(s, Y[held], 0.95):6.2%}")
|
f"обстановки при 95% предметов {fpr_at_tpr(s, Y[held], 0.95):6.2%}")
|
||||||
if args.baseline:
|
if args.baseline:
|
||||||
import lightgbm as lgb
|
import lightgbm as lgb
|
||||||
|
dev_type = "gpu" if (args.device == "cuda" or (args.device == "auto" and B.Path("/dev/nvidia0").exists())) else "cpu"
|
||||||
|
try:
|
||||||
|
g = lgb.LGBMClassifier(n_estimators=400, learning_rate=0.05,
|
||||||
|
num_leaves=63, device=dev_type, verbose=-1)
|
||||||
|
g.fit(Xtr, ytr)
|
||||||
|
except Exception:
|
||||||
g = lgb.LGBMClassifier(n_estimators=400, learning_rate=0.05,
|
g = lgb.LGBMClassifier(n_estimators=400, learning_rate=0.05,
|
||||||
num_leaves=63, verbose=-1)
|
num_leaves=63, verbose=-1)
|
||||||
g.fit(Xtr, ytr)
|
g.fit(Xtr, ytr)
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ def main() -> None:
|
||||||
ap.add_argument("--n-kc", type=int, default=_d.n_kc)
|
ap.add_argument("--n-kc", type=int, default=_d.n_kc)
|
||||||
ap.add_argument("--claws", type=int, default=_d.claws)
|
ap.add_argument("--claws", type=int, default=_d.claws)
|
||||||
ap.add_argument("--sparsity", type=float, default=_d.sparsity)
|
ap.add_argument("--sparsity", type=float, default=_d.sparsity)
|
||||||
ap.add_argument("--device", default="cpu")
|
ap.add_argument("--device", default="auto",
|
||||||
|
help="устройство вычислений ('auto', 'cuda', 'cpu')")
|
||||||
ap.add_argument("--reuse-cache", action="store_true")
|
ap.add_argument("--reuse-cache", action="store_true")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,10 @@ def main() -> None:
|
||||||
ap.add_argument("--sweep-kc", default="500,1000,2000,4000")
|
ap.add_argument("--sweep-kc", default="500,1000,2000,4000")
|
||||||
ap.add_argument("--epochs", type=int, default=200)
|
ap.add_argument("--epochs", type=int, default=200)
|
||||||
ap.add_argument("--lr", type=float, default=2.0)
|
ap.add_argument("--lr", type=float, default=2.0)
|
||||||
ap.add_argument("--l2", type=float, default=1e-4)
|
ap.add_argument("--device", default="auto",
|
||||||
ap.add_argument("--device", default="cpu")
|
help="устройство обучения ('auto', 'cuda', 'cpu')")
|
||||||
|
ap.add_argument("--baseline", action="store_true",
|
||||||
|
help="сравнить с GPU/CPU градиентным бустингом (LightGBM)")
|
||||||
ap.add_argument("--drop", default="",
|
ap.add_argument("--drop", default="",
|
||||||
help="признаки, выброшенные из обучения — это РЕЖИМ ЗАМЕРА, "
|
help="признаки, выброшенные из обучения — это РЕЖИМ ЗАМЕРА, "
|
||||||
"модель при этом не сохраняется: конвейер считает "
|
"модель при этом не сохраняется: конвейер считает "
|
||||||
|
|
@ -172,9 +174,32 @@ def main() -> None:
|
||||||
for held, s, _ in res:
|
for held, s, _ in res:
|
||||||
print(f" {held:<42}AUC {auc(s, Y[held]):.4f}")
|
print(f" {held:<42}AUC {auc(s, Y[held]):.4f}")
|
||||||
|
|
||||||
|
table_cols = {"улика: AUC": ev_loo, "модель: AUC": s_loo}
|
||||||
|
if args.baseline:
|
||||||
|
import lightgbm as lgb
|
||||||
|
s_lgb = []
|
||||||
|
for held in names:
|
||||||
|
tr = [n for n in names if n != held]
|
||||||
|
Xtr = np.concatenate([X[n] for n in tr])
|
||||||
|
ytr = np.concatenate([Y[n] for n in tr])
|
||||||
|
dev_type = "gpu" if (args.device == "cuda" or (args.device == "auto" and B.Path("/dev/nvidia0").exists())) else "cpu"
|
||||||
|
try:
|
||||||
|
g = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.05, num_leaves=31,
|
||||||
|
device=dev_type, verbose=-1)
|
||||||
|
g.fit(Xtr, ytr)
|
||||||
|
except Exception:
|
||||||
|
g = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.05, num_leaves=31,
|
||||||
|
verbose=-1)
|
||||||
|
g.fit(Xtr, ytr)
|
||||||
|
s_lgb.append(g.predict_proba(X[held])[:, 1])
|
||||||
|
s_lgb_arr = np.concatenate(s_lgb)
|
||||||
|
print(f"\nбустинг leave-one-bag-out: AUC {auc(s_lgb_arr, y_loo):.4f}, "
|
||||||
|
f"ложных при 95 % пойманных {fpr_at_tpr(s_lgb_arr, y_loo):.3f}")
|
||||||
|
table_cols["бустинг: AUC"] = s_lgb_arr
|
||||||
|
|
||||||
# ------------------------------- главное: обгоняет ли модель саму улику
|
# ------------------------------- главное: обгоняет ли модель саму улику
|
||||||
print("\nмодель против улики, по полосам дальности:")
|
print("\nмодель против улики, по полосам дальности:")
|
||||||
band_table(d_loo, y_loo, {"улика: AUC": ev_loo, "модель: AUC": s_loo})
|
band_table(d_loo, y_loo, table_cols)
|
||||||
|
|
||||||
# --------------------------------------------------------------- хранение
|
# --------------------------------------------------------------- хранение
|
||||||
if drop:
|
if drop:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue