forked from Dan4ick/Lidar_Muxa
фантомы: штраф за плоское у пола, отдельный проход по верхней секции для висящего, верх секции 3.3 м
This commit is contained in:
parent
d1da4ec117
commit
5aa3dead0e
8 changed files with 307 additions and 42 deletions
|
|
@ -196,14 +196,14 @@ k_sigma=0.75` у `make_benchmark.py`, `evaluate.py`, `make_training_set.py` и
|
||||||
| Метрика | Значение | Чем измерено |
|
| Метрика | Значение | Чем измерено |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` |
|
| Реальный объект 0.67 × 1.35 м на 55 м | **99.5 %** кадров | `tools/check_obstacle.py` |
|
||||||
| Синтетика организаторов, 10 предметов | **8 из 10** верно | `tools/eval_org_synth.py` |
|
| Синтетика организаторов, 10 предметов | **9 из 10** верно | `tools/eval_org_synth.py` |
|
||||||
| Ложные тревоги, leave-one-bag-out | **5.7 трека на км**, 8.4 % кадров | `tools/evaluate.py --mbon-dir` |
|
| Ложные тревоги, leave-one-bag-out | **5.7 трека на км**, 8.4 % кадров | `tools/evaluate.py --mbon-dir` |
|
||||||
| То же без обученного считывания | 11.9 на км, 20.3 % кадров | там же, без `--mbon-dir` |
|
| То же без обученного считывания | 11.9 на км, 20.3 % кадров | там же, без `--mbon-dir` |
|
||||||
| На незнакомой линии (памяти нет) | **13.8 на км**; без считывания 36.1 | `tools/evaluate.py --no-memory` |
|
| На незнакомой линии (памяти нет) | **12.8 на км**; без считывания 36.1 | `tools/evaluate.py --no-memory` |
|
||||||
| Вторая половина `new_data`: другой день, 3.41 км, не видена при обучении | **14.4 на км**, 20.3 % кадров | `tools/eval_new_data.py` |
|
| Вторая половина `new_data`: другой день, 3.41 км, не видена при обучении | **11.7 на км**, 17.9 % кадров | `tools/eval_new_data.py` |
|
||||||
| Дальность (полигон, 15 560 наблюдений) | человек стоя: рабочая дальность **100 м**, P@50 = 0.72, P@100 = 0.52, P@150 = 0.46 | `tools/plot_benchmark.py` |
|
| Дальность (полигон, 15 560 наблюдений) | человек стоя: рабочая дальность **100 м**, P@50 = 0.72, P@100 = 0.52, P@150 = 0.46 | `tools/plot_benchmark.py` |
|
||||||
| Человек, упавший на пути | P@50 = **0.50** (было 0.27), рабочая дальность 20 м | там же |
|
| Человек, упавший на пути | P@50 = **0.50** (было 0.27), рабочая дальность 20 м | там же |
|
||||||
| Обработка кадра | медиана 31 мс из бюджета 100 мс; на ядре уровня стенда жюри 50 / 60 мс (медиана / p95) | контейнер, EXPERIMENTS п. 18.5 |
|
| Обработка кадра | медиана 33 мс из бюджета 100 мс; на ядре уровня стенда жюри 55 / 65 мс (медиана / p95) | контейнер, EXPERIMENTS п. 18.5, 19.5 |
|
||||||
| Разделение «знакомое / новое» (без учителя) | ROC AUC 0.905 | `tools/tune_memory.py` |
|
| Разделение «знакомое / новое» (без учителя) | ROC AUC 0.905 | `tools/tune_memory.py` |
|
||||||
| Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.986** | `tools/train_mbon.py` |
|
| Считывание MBON «предмет / тоннель» (с учителем) | ROC AUC **0.986** | `tools/train_mbon.py` |
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -3,12 +3,12 @@
|
||||||
"bag": "doubleT_obstacle",
|
"bag": "doubleT_obstacle",
|
||||||
"frames": 190,
|
"frames": 190,
|
||||||
"path_m": 0.0,
|
"path_m": 0.0,
|
||||||
"alarm_frames": 130,
|
"alarm_frames": 167,
|
||||||
"alarm_rate": 0.6842105263157895,
|
"alarm_rate": 0.8789473684210526,
|
||||||
"fp_objects": 232,
|
"fp_objects": 369,
|
||||||
"fp_tracks": 5,
|
"fp_tracks": 4,
|
||||||
"fp_per_km": NaN,
|
"fp_per_km": NaN,
|
||||||
"fp_median_d": 106.83262984484747,
|
"fp_median_d": 106.82908159892504,
|
||||||
"obj_rate": 0.9947368421052631,
|
"obj_rate": 0.9947368421052631,
|
||||||
"ms_p50": NaN,
|
"ms_p50": NaN,
|
||||||
"ms_p95": NaN,
|
"ms_p95": NaN,
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
"fp_objects": 42,
|
"fp_objects": 42,
|
||||||
"fp_tracks": 2,
|
"fp_tracks": 2,
|
||||||
"fp_per_km": 9.962686878640993,
|
"fp_per_km": 9.962686878640993,
|
||||||
"fp_median_d": 34.009163839731684,
|
"fp_median_d": 34.00916383973144,
|
||||||
"obj_rate": null,
|
"obj_rate": null,
|
||||||
"ms_p50": NaN,
|
"ms_p50": NaN,
|
||||||
"ms_p95": NaN,
|
"ms_p95": NaN,
|
||||||
|
|
|
||||||
|
|
@ -3008,3 +3008,134 @@ rosbag2 — пауза, ±3 с, кадр вперёд, скорость ×0.1…
|
||||||
в 0.19 м над рельсом (вблизи — 1–6 см высотой, память её знает) и
|
в 0.19 м над рельсом (вблизи — 1–6 см высотой, память её знает) и
|
||||||
дальнее пятно на 147 м, которого при подъезде нет. Настоящий объект в
|
дальнее пятно на 147 м, которого при подъезде нет. Настоящий объект в
|
||||||
`for_hackathon` один — в `doubleT_obstacle`.
|
`for_hackathon` один — в `doubleT_obstacle`.
|
||||||
|
|
||||||
|
### 18.11. «Человек» слева в RViz — тень детали у лидара
|
||||||
|
|
||||||
|
На `doubleT_obstacle` в RViz слева от габарита виден тёмный силуэт, похожий на
|
||||||
|
человека. Это не предмет: тёмное в облаке обзора — место без точек, а человек
|
||||||
|
выглядел бы пятном точек, как объект на 55 м. Между краем габарита и левой
|
||||||
|
стеной от 3 до 9 м выше 1.1 м нет ни одной точки, в 6 м вокруг лидара — только
|
||||||
|
стена, низкий короб у пути (0.3–0.5 м) и части самого поезда.
|
||||||
|
|
||||||
|
Дыру даёт деталь в 0.48–0.62 м от лидара — спереди-слева и ниже него (азимут
|
||||||
|
14–34° влево, угол места 12–25° вниз), во всех 200 кадрах на месте, по форме —
|
||||||
|
перевёрнутое «Г». Лучи, которые она перекрывает, иначе легли бы на пол в
|
||||||
|
3.3–5.5 м впереди от края габарита до стены и на низ стены. Саму деталь облако
|
||||||
|
обзора не показывает (оно начинается с 0.5 м), поэтому видна одна тень
|
||||||
|
(`docs/figures/doubleT_mount_shadow.png`). Это и слепое пятно прибора: полоса
|
||||||
|
пола у левого края габарита в 3–5 м впереди на этой записи не наблюдается.
|
||||||
|
|
||||||
|
## 19. Фантомы и висящее: плоское у пола, отдельный проход по верхней секции (25.09)
|
||||||
|
|
||||||
|
Задача от пользователя: «чтобы всё проходило на `cloud_with_fake_obj`» и убрать
|
||||||
|
фантомы — «предмет появляется на 1–2 секунды, а при приближении исчезает».
|
||||||
|
|
||||||
|
### 19.1. Откуда фантомы
|
||||||
|
|
||||||
|
Подробный журнал конвейера (у каждого кандидата вес улики, отсчёт считывания
|
||||||
|
MBON, новизна; у каждого трека ход улики) на синтетике и шести записях.
|
||||||
|
Настоящие предметы копят улику до 1.0 и живут десятки кадров; фантомы живут
|
||||||
|
9–21 кадр. Все восемь (пять на синтетике, три на записях с поезда) — трёх
|
||||||
|
видов:
|
||||||
|
|
||||||
|
| вид | где | признак |
|
||||||
|
|---|---|---|
|
||||||
|
| плоское у пола | синтетика 40→25 и 32→22 м; порог у гермозатвора 65→31 м (28 кадров) | высота 0.01–0.05 м, верх 0.18–0.23 м над рельсом |
|
||||||
|
| дальнее | синтетика 178 и 120 м; `squareT_…` 103→85 и 146→136 м | 3–8 лучей, вблизи на этом месте ничего нет |
|
||||||
|
| у края габарита | синтетика 16→14 м, u −1.01…−1.17 | конструкция, выходящая из габарита по мере подъезда |
|
||||||
|
|
||||||
|
Важная деталь: при `mbon_blend = 1` вес наблюдения целиком решает обученное
|
||||||
|
считывание, и плоским фантомам оно ставит 0.78–0.99 — в его обучении был
|
||||||
|
кабель на путях. Значит, правило должно стоять поверх модели.
|
||||||
|
|
||||||
|
### 19.2. Плоское у пола
|
||||||
|
|
||||||
|
Вблизи (до 70 м) кольца лидара идут достаточно часто, чтобы судить о высоте:
|
||||||
|
настоящий низкий предмет (0.3 м на рельсе, 2×0.2 м поперёк путей) даёт
|
||||||
|
0.1–0.4 м по высоте и верх выше 0.4 м. Кандидат с высотой меньше 0.06 м и
|
||||||
|
верхом ниже 0.26 м над рельсом теперь получает вес ×0.15
|
||||||
|
(`flat_h`/`flat_top`/`flat_d`/`flat_w`). Множитель, а не запрет: предмет,
|
||||||
|
хоть раз показавший высоту, своё доберёт. Организаторы подтвердили, что лежащее
|
||||||
|
в жёлобе препятствием не считается.
|
||||||
|
|
||||||
|
Итог: порог гермозатвора и оба плоских фантома синтетики ушли; на пустых
|
||||||
|
записях 3 → 2 ложных трека, 73 → 45 кадров с тревогой.
|
||||||
|
|
||||||
|
### 19.3. Висящее со свода
|
||||||
|
|
||||||
|
Стержень 5 см (№ 10) не находился вовсе. Основной проход тянет контекст
|
||||||
|
кластеризации до свода (`ctx_up`) — иначе срез колонны по верху габарита сам
|
||||||
|
выглядит предметом. Но поэтому всё свисающее склеивается со сводом, доля лучей
|
||||||
|
в габарите у стержня — ноль, вес ~0.005.
|
||||||
|
|
||||||
|
Первая попытка — не пускать свод над верхней секцией в контекст
|
||||||
|
(`top_ceiling_cut`). Стержень нашёлся с 13 м, но на `roundT_doubleT` вышла новая
|
||||||
|
тревога: на 33 м по оси — стойка от пола до 2.75 м с коробкой на 2.0–2.6 м,
|
||||||
|
похоже на светофор у стрелки, к которому поезд не едет (двухпутный тоннель, наша
|
||||||
|
ось смотрит прямо). Раньше стойку держал в компоненте свод, над которым она
|
||||||
|
крепится. Срез освободил и её. Отвергнуто.
|
||||||
|
|
||||||
|
Принято: отдельный проход по верхней секции (`lobula._hanging`, `top_detect`).
|
||||||
|
Его контекст — только сама секция с запасом 0.3 м по сторонам и 0.4 м вниз, без
|
||||||
|
свода. Компонента, у которой есть лучи ниже `h_hi`, уходит к полу — это стоящий
|
||||||
|
предмет, его ведёт основной проход (светофор так и остаётся без тревоги).
|
||||||
|
Кандидаты основного прохода, целиком лежащие в верхней секции, отбрасываются,
|
||||||
|
чтобы не двоить трек. Ещё три вещи понадобились по ходу:
|
||||||
|
|
||||||
|
* **новизна и MBON в верхней секции выключены.** Обе обученные части собирались
|
||||||
|
до того, как у габарита появилась верхняя секция, и таких форм не видели.
|
||||||
|
Опора по числу лучей для висящего не ниже 0.5 — нормировка рассчитана на
|
||||||
|
предмет с человека, а стержень тонок по природе и упирался в нижний край 0.25;
|
||||||
|
* **плоское у верхней границы — свод.** На 83 м ошибка наклона плоскости пути
|
||||||
|
опустила свод до 3.66–3.69 м, в секцию; такой кандидат отбрасывается;
|
||||||
|
* **верх секции 3.3 м, а не 3.7.** После выключения новизны в тревоги пошли
|
||||||
|
кабели и кронштейны свода на 3.2–3.6 м (`doubleT_platform` 61 м,
|
||||||
|
`roundT_doubleT` 22 м). Новизна их не отделяет (медиана 0.24–0.41 у них,
|
||||||
|
0.29–0.57 у стержня), высота низа — отделяет: у деталей свода он в основном
|
||||||
|
выше 3.2 м, у предметов организаторов — 2.8 и 3.0 м.
|
||||||
|
|
||||||
|
Итог: стержень найден, но поздно — с 13.4 м: в поясе до 3.3 м на нём вдвое
|
||||||
|
меньше лучей, чем до 3.7 (при 3.7 он находился с 50 м, но ценой двух ложных
|
||||||
|
треков на записях). «2×2 сверху» держится 48 кадров вместо 24, с 87 м. Попутно
|
||||||
|
«0.3 на рельсе» находится с 76 м вместо 36. Механизм не разбирался; вероятно,
|
||||||
|
раньше рядом жили мусорные треки из верхней секции, склеенные со сводом, и
|
||||||
|
торможение соседних треков (`_inhibit`) гасило его улику.
|
||||||
|
|
||||||
|
### 19.4. Дальние фантомы: возраст трека — отвергнуто
|
||||||
|
|
||||||
|
Дальний фантом по одному кадру неотличим от настоящего предмета: те же 3–4
|
||||||
|
луча, отсчёт MBON 0.2–0.8, новизна на дальности выключена (п. 12.3). Отличает
|
||||||
|
его только время: он пропадает при подъезде. Проверено правило «дальше 100 м
|
||||||
|
тревога только за треком, прожившим 15 кадров» (`far_confirm`): на синтетике
|
||||||
|
фантомов 3 → 2 и кадров тревоги 24 → 9, но полигон теряет 200 наблюдений из
|
||||||
|
15 560, P@150 у человека стоя 0.46 → 0.37, сидя 0.39 → 0.21, у чемодана и
|
||||||
|
ящика вдвое. Посторонних тревог на полигоне меньше на 14 %, но такой ценой
|
||||||
|
дальности не берём. Параметр оставлен выключенным.
|
||||||
|
|
||||||
|
### 19.5. Итог
|
||||||
|
|
||||||
|
Принято: `flat_h = 0.06`, `top_detect = true`, `h_top = 3.3`. Все прогоны парные:
|
||||||
|
|
||||||
|
| проверка | было | стало |
|
||||||
|
|---|---|---|
|
||||||
|
| синтетика организаторов | 8 из 10, фантомов 5 (43 кадра) | **9 из 10**, фантомов **3 (24 кадра)** |
|
||||||
|
| пустые записи с поезда | 3 трека, 73 кадра | **2 трека, 45 кадров** |
|
||||||
|
| знакомая линия (LOO) | 5.7 на км, 8.4 % кадров | 5.7 на км, 8.4 % |
|
||||||
|
| незнакомая (без памяти) | 13.8 на км, 24.3 % | **12.8 на км, 20.7 %** |
|
||||||
|
| вторая половина `new_data` | 49 треков, 14.4 на км, 20.3 % | **40 треков, 11.7 на км, 17.9 %** |
|
||||||
|
| полигон, наблюдения | — | 0→1: 0, 1→0: 3 (каска) |
|
||||||
|
| полигон, посторонние | 742 | 758 |
|
||||||
|
| `doubleT_obstacle` | 99.5 % | 99.5 % |
|
||||||
|
| кадр в контейнере, `doubleT_obstacle` / синтетика | 30.7 / 41.8 мс | 33.4 / 43.5 мс |
|
||||||
|
| на ядре уровня жюри (квота 0.61), p95 | 60.2 / 71.2 мс | 64–66 / 79.7 мс |
|
||||||
|
|
||||||
|
Что не взято и почему:
|
||||||
|
|
||||||
|
* **№ 6, 2×2 у края.** В габарит он заходит на 0.06–0.28 м (граница оценки оси
|
||||||
|
плавает от кадра к кадру), остальные 2 м — снаружи. Для конвейера это то же,
|
||||||
|
что шкаф у стены: MBON даёт 0.00–0.02, доля лучей в габарите — ноль. Правило
|
||||||
|
«компактное, стоит на полу, уходит за габарит» усилило бы и фантом у края на
|
||||||
|
16 м, который как раз такой.
|
||||||
|
* **Дальние фантомы** — п. 19.4.
|
||||||
|
* **Фантом у края на 16 м**: настоящая конструкция, заходящая в габарит на
|
||||||
|
0.03–0.19 м; с габаритом 1.2 м он граничный.
|
||||||
|
|
|
||||||
|
|
@ -148,10 +148,12 @@ class CentralComplex:
|
||||||
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, top_from: float = float("inf")):
|
lying_exempt: bool = False, top_from: float = float("inf"),
|
||||||
|
flat: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0)):
|
||||||
self.use_shape = use_shape
|
self.use_shape = use_shape
|
||||||
self.lying_exempt = lying_exempt
|
self.lying_exempt = lying_exempt
|
||||||
self.top_from = top_from # низ выше этого — предмет в верхней секции габарита
|
self.top_from = top_from # низ выше этого — предмет в верхней секции габарита
|
||||||
|
self.flat = flat # (высота, верх, дальность, множитель), см. _quality
|
||||||
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
|
||||||
|
|
@ -228,7 +230,7 @@ class CentralComplex:
|
||||||
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, self.top_from)
|
self.lying_exempt, self.top_from, self.flat)
|
||||||
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:
|
||||||
|
|
@ -256,7 +258,7 @@ class CentralComplex:
|
||||||
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, self.top_from)
|
self.lying_exempt, self.top_from, self.flat)
|
||||||
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
|
||||||
|
|
@ -301,7 +303,8 @@ def expected_rays(d: float) -> float:
|
||||||
def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||||
mbon_blend: float = 1.0, nov_fade_from: float = 0.0,
|
mbon_blend: float = 1.0, nov_fade_from: float = 0.0,
|
||||||
nov_fade_to: float = 160.0, lying_exempt: bool = False,
|
nov_fade_to: float = 160.0, lying_exempt: bool = False,
|
||||||
top_from: float = float("inf")) -> float:
|
top_from: float = float("inf"),
|
||||||
|
flat: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0)) -> float:
|
||||||
"""Вес одного наблюдения: сколько улики оно добавляет.
|
"""Вес одного наблюдения: сколько улики оно добавляет.
|
||||||
|
|
||||||
Дальний объект даёт мало лучей не потому, что он сомнительный, а потому что
|
Дальний объект даёт мало лучей не потому, что он сомнительный, а потому что
|
||||||
|
|
@ -311,7 +314,16 @@ def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||||
стена тянутся дальше и в стороны. Новизна из грибовидного тела входит сюда
|
стена тянутся дальше и в стороны. Новизна из грибовидного тела входит сюда
|
||||||
же множителем.
|
же множителем.
|
||||||
"""
|
"""
|
||||||
support = float(np.clip(c.n_rays / expected_rays(c.d), 0.25, 1.0))
|
# Висящее в верхней секции (`top_from`) — отдельная статья. Обе обученные
|
||||||
|
# части, считывание MBON и память тоннеля, собирались до того, как у
|
||||||
|
# габарита появилась верхняя секция, и таких форм не видели: их ответ там
|
||||||
|
# не значит ничего, и оба канала здесь выключены. Нормировка числа лучей
|
||||||
|
# рассчитана на предмет с человека, а свисающий кабель или стержень тонок
|
||||||
|
# по природе: у стержня 5 см опора упиралась в нижний край 0.25 на любой
|
||||||
|
# дальности. Пояс над вагоном в исправном тоннеле пуст, поэтому нижний
|
||||||
|
# край для висящего поднят до 0.5. EXPERIMENTS п. 19.
|
||||||
|
top = c.h_min >= top_from
|
||||||
|
support = float(np.clip(c.n_rays / expected_rays(c.d), 0.5 if top else 0.25, 1.0))
|
||||||
|
|
||||||
# Контраст к фону — главная улика вблизи и недоступная вдали. На 170 м
|
# Контраст к фону — главная улика вблизи и недоступная вдали. На 170 м
|
||||||
# кольцо окружения ламины упирается в стену тоннеля, стоящую на той же
|
# кольцо окружения ламины упирается в стену тоннеля, стоящую на той же
|
||||||
|
|
@ -341,8 +353,7 @@ def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||||
# лоток или кронштейн висят на стене, и под ними пусто. Кроме верхней
|
# лоток или кронштейн висят на стене, и под ними пусто. Кроме верхней
|
||||||
# секции габарита (`top_from`): у оси под сводом стен нет, и висящее
|
# секции габарита (`top_from`): у оси под сводом стен нет, и висящее
|
||||||
# там — это то, что свисает в путь вагона.
|
# там — это то, что свисает в путь вагона.
|
||||||
grounded = (1.0 if c.h_min >= top_from
|
grounded = 1.0 if top else float(np.clip(1.25 - c.h_min / 1.2, 0.15, 1.0))
|
||||||
else float(np.clip(1.25 - c.h_min / 1.2, 0.15, 1.0)))
|
|
||||||
else:
|
else:
|
||||||
whole = compact = grounded = 1.0
|
whole = compact = grounded = 1.0
|
||||||
# Знакомость на большой дальности не просто бесполезна, а вредна. Замер:
|
# Знакомость на большой дальности не просто бесполезна, а вредна. Замер:
|
||||||
|
|
@ -352,8 +363,8 @@ def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||||
# вырождается, и память узнаёт в предмете любую далёкую конструкцию.
|
# вырождается, и память узнаёт в предмете любую далёкую конструкцию.
|
||||||
# Поэтому вклад новизны гасится с дальностью, а вблизи, где она и
|
# Поэтому вклад новизны гасится с дальностью, а вблизи, где она и
|
||||||
# работает, остаётся как был. 0 — не гасить (EXPERIMENTS п. 12.3).
|
# работает, остаётся как был. 0 — не гасить (EXPERIMENTS п. 12.3).
|
||||||
novel = novelty_gain(c.novelty)
|
novel = 1.0 if top else novelty_gain(c.novelty)
|
||||||
if nov_fade_from > 0.0 and c.d > nov_fade_from:
|
if not top and nov_fade_from > 0.0 and c.d > nov_fade_from:
|
||||||
span = max(nov_fade_to - nov_fade_from, 1e-3)
|
span = max(nov_fade_to - nov_fade_from, 1e-3)
|
||||||
w = 1.0 - min((c.d - nov_fade_from) / span, 1.0)
|
w = 1.0 - min((c.d - nov_fade_from) / span, 1.0)
|
||||||
novel = novel ** w
|
novel = novel ** w
|
||||||
|
|
@ -370,10 +381,23 @@ def _quality(c: Candidate, use_shape: bool = True, mbon_power: float = 1.0,
|
||||||
p = c.extra.get("mbon") if c.extra else None
|
p = c.extra.get("mbon") if c.extra else None
|
||||||
# висящее в верхней секции модель не видела ни разу (обучена на стоящих
|
# висящее в верхней секции модель не видела ни разу (обучена на стоящих
|
||||||
# предметах) и гасит его — здесь решает ручная формула
|
# предметах) и гасит его — здесь решает ручная формула
|
||||||
if p is not None and mbon_blend > 0.0 and c.h_min < top_from:
|
if p is not None and mbon_blend > 0.0 and not top:
|
||||||
pm = float(np.clip(p, 1e-4, 1.0)) ** max(mbon_power, 1e-3)
|
pm = float(np.clip(p, 1e-4, 1.0)) ** max(mbon_power, 1e-3)
|
||||||
b = float(np.clip(mbon_blend, 0.0, 1.0))
|
b = float(np.clip(mbon_blend, 0.0, 1.0))
|
||||||
hand = pm ** b * max(hand, 1e-4) ** (1.0 - b)
|
hand = pm ** b * max(hand, 1e-4) ** (1.0 - b)
|
||||||
|
# Плоское у самого пола: высотой в сантиметры, целиком ниже `flat[1]` над
|
||||||
|
# рельсом. Это пластины и кромки на полотне, края жёлоба, порог
|
||||||
|
# гермозатвора — по ответу организаторов то, что в жёлобе, препятствием не
|
||||||
|
# считается. Модель таким ставит 0.8–0.99: в её обучении был кабель на
|
||||||
|
# путях. А ложных тревог это давало больше всего: порог гермозатвора на
|
||||||
|
# 65→31 м и два фантома на синтетике. Настоящий низкий предмет (0.3 м на
|
||||||
|
# рельсе, 2×0.2 м поперёк путей) вблизи даёт 0.1–0.4 м по высоте и верх
|
||||||
|
# выше 0.4 м. Дальше `flat[2]` решётка слишком редкая, чтобы судить о
|
||||||
|
# высоте, и там правило молчит. Множитель, а не запрет: если предмет хоть в
|
||||||
|
# одном кадре покажет высоту, он своё доберёт. EXPERIMENTS п. 19.
|
||||||
|
fh, ftop, fd, fw = flat
|
||||||
|
if fh > 0.0 and c.d <= fd and c.height < fh and c.h_min + c.height < ftop:
|
||||||
|
hand *= fw
|
||||||
return float(np.clip(hand * novel, 0.0, 1.0))
|
return float(np.clip(hand * novel, 0.0, 1.0))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,8 @@ class DescendingNeurons:
|
||||||
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):
|
far_channel: bool = False, far_confirm: int = 0,
|
||||||
|
far_confirm_from: float = 100.0):
|
||||||
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
|
||||||
|
|
@ -101,6 +102,13 @@ class DescendingNeurons:
|
||||||
# способом; включать вместе с ним — ослабить дальний край дважды.
|
# способом; включать вместе с ним — ослабить дальний край дважды.
|
||||||
# Замер — EXPERIMENTS п. 16.
|
# Замер — EXPERIMENTS п. 16.
|
||||||
self.far_channel = far_channel
|
self.far_channel = far_channel
|
||||||
|
# Дальний трек поднимает тревогу, только прожив `far_confirm` кадров.
|
||||||
|
# Вдали предмет — три-четыре луча, и по одному кадру он неотличим от
|
||||||
|
# вспышки на кромке или стыке; фантомы на синтетике и в записях жили
|
||||||
|
# 0.5–1 с и пропадали при подъезде. Торможению дальний трек не нужен
|
||||||
|
# срочно: до него сотня метров. 0 — выключено. EXPERIMENTS п. 19.
|
||||||
|
self.far_confirm = far_confirm
|
||||||
|
self.far_confirm_from = far_confirm_from
|
||||||
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()
|
||||||
|
|
@ -134,6 +142,9 @@ class DescendingNeurons:
|
||||||
or not nov_ok):
|
or not nov_ok):
|
||||||
self._latched.discard(t.id)
|
self._latched.discard(t.id)
|
||||||
continue
|
continue
|
||||||
|
if (self.far_confirm > 0 and d > self.far_confirm_from
|
||||||
|
and t.age < self.far_confirm and t.id not in self._latched):
|
||||||
|
continue
|
||||||
ready.append((t, d))
|
ready.append((t, d))
|
||||||
|
|
||||||
use_track = ((self.track_blend > 0.0 or self.track_gate > 0.0)
|
use_track = ((self.track_blend > 0.0 or self.track_gate > 0.0)
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,10 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
core_width: float = 0.85, core_from: float = 0.0,
|
core_width: float = 0.85, core_from: float = 0.0,
|
||||||
platform_filter: bool = False,
|
platform_filter: bool = False,
|
||||||
platform_u: float = 1.30, platform_h_lo: float = 1.05,
|
platform_u: float = 1.30, platform_h_lo: float = 1.05,
|
||||||
platform_h_hi: float = 1.25) -> list[Candidate]:
|
platform_h_hi: float = 1.25,
|
||||||
|
h_top: float = 0.0, half_width_top: float = 0.0,
|
||||||
|
top_d_max: float = 90.0, top_detect: bool = False,
|
||||||
|
top_min_rays: int = 3) -> list[Candidate]:
|
||||||
"""Выделить кандидатов: связные пятна лучей, попавших в габарит.
|
"""Выделить кандидатов: связные пятна лучей, попавших в габарит.
|
||||||
|
|
||||||
Кластеризация ведётся по **расширенной** области, а членство в габарите
|
Кластеризация ведётся по **расширенной** области, а членство в габарите
|
||||||
|
|
@ -410,33 +413,27 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
sorted_lbl = labels.ravel()[order]
|
sorted_lbl = labels.ravel()[order]
|
||||||
starts = np.searchsorted(sorted_lbl, np.arange(n + 2))
|
starts = np.searchsorted(sorted_lbl, np.arange(n + 2))
|
||||||
|
|
||||||
inside_flat = inside.ravel()
|
width_img = labels.shape[1]
|
||||||
out: list[Candidate] = []
|
|
||||||
for lbl in keep:
|
|
||||||
ctx_idx = order[starts[lbl]:starts[lbl + 1]]
|
|
||||||
sel = ctx_idx[inside_flat[ctx_idx]] # признаки считаем по ядру в габарите
|
|
||||||
if sel.size < need[lbl]:
|
|
||||||
continue
|
|
||||||
ii, jj = np.divmod(sel, labels.shape[1])
|
|
||||||
|
|
||||||
|
def build(sel: np.ndarray, n_ctx: int) -> Candidate | None:
|
||||||
|
"""Кандидат по лучам ядра `sel` (плоские индексы) из компоненты в `n_ctx` лучей."""
|
||||||
|
ii, jj = np.divmod(sel, width_img)
|
||||||
d = tf.d[ii, jj]
|
d = tf.d[ii, jj]
|
||||||
u = lat[ii, jj]
|
u = lat[ii, jj]
|
||||||
h = tf.h[ii, jj]
|
h = tf.h[ii, jj]
|
||||||
nn = d.size
|
nn = d.size
|
||||||
|
|
||||||
depth = float(d.max() - d.min()) if nn > 1 else 0.0
|
depth = float(d.max() - d.min()) if nn > 1 else 0.0
|
||||||
if depth > max_depth:
|
if depth > max_depth:
|
||||||
# ни один посторонний предмет не тянется на десятки метров вдоль пути:
|
# ни один посторонний предмет не тянется на десятки метров вдоль пути:
|
||||||
# это полотно, стена или кабельный лоток, взятые под скользящим углом
|
# это полотно, стена или кабельный лоток, взятые под скользящим углом
|
||||||
continue
|
return None
|
||||||
|
|
||||||
cand = Candidate(
|
cand = Candidate(
|
||||||
d=float(np.median(d)), u=float(np.median(u)), h=float(np.median(h)),
|
d=float(np.median(d)), u=float(np.median(u)), h=float(np.median(h)),
|
||||||
d_min=float(d.min()), h_min=float(h.min()),
|
d_min=float(d.min()), h_min=float(h.min()),
|
||||||
width=float(u.max() - u.min()) if nn > 1 else 0.0,
|
width=float(u.max() - u.min()) if nn > 1 else 0.0,
|
||||||
height=float(h.max() - h.min()) if nn > 1 else 0.0,
|
height=float(h.max() - h.min()) if nn > 1 else 0.0,
|
||||||
depth=depth,
|
depth=depth,
|
||||||
containment=float(sel.size / max(ctx_idx.size, 1)),
|
containment=float(sel.size / max(n_ctx, 1)),
|
||||||
n_rays=int(nn), n_rings=int(np.unique(ii).size), n_cols=int(np.unique(jj).size),
|
n_rays=int(nn), n_rings=int(np.unique(ii).size), n_cols=int(np.unique(jj).size),
|
||||||
gap=float(np.median(gap_img[ii, jj])),
|
gap=float(np.median(gap_img[ii, jj])),
|
||||||
on=float(np.median(lam.on[ii, jj])),
|
on=float(np.median(lam.on[ii, jj])),
|
||||||
|
|
@ -449,7 +446,82 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
|
||||||
)
|
)
|
||||||
# индексы лучей нужны только для отладочной визуализации; они небольшие
|
# индексы лучей нужны только для отладочной визуализации; они небольшие
|
||||||
cand.extra["rays"] = (ii, jj)
|
cand.extra["rays"] = (ii, jj)
|
||||||
|
return cand
|
||||||
|
|
||||||
|
use_top = top_detect and h_top > h_hi and half_width_top > 0.0
|
||||||
|
h_flat = tf.h.ravel()
|
||||||
|
inside_flat = inside.ravel()
|
||||||
|
out: list[Candidate] = []
|
||||||
|
for lbl in keep:
|
||||||
|
ctx_idx = order[starts[lbl]:starts[lbl + 1]]
|
||||||
|
sel = ctx_idx[inside_flat[ctx_idx]] # признаки считаем по ядру в габарите
|
||||||
|
if sel.size < need[lbl]:
|
||||||
|
continue
|
||||||
|
if use_top and np.all(h_flat[sel] >= h_hi):
|
||||||
|
# целиком в верхней секции: это работа отдельного прохода ниже —
|
||||||
|
# здесь такой кандидат склеен со сводом и только двоил бы трек
|
||||||
|
continue
|
||||||
|
cand = build(sel, ctx_idx.size)
|
||||||
|
if cand is not None:
|
||||||
out.append(cand)
|
out.append(cand)
|
||||||
|
|
||||||
|
if use_top:
|
||||||
|
out += _hanging(tf, abs_lat, in_range, build, h_hi=h_hi, h_top=h_top,
|
||||||
|
half_width_top=half_width_top, top_d_max=top_d_max,
|
||||||
|
min_rays=top_min_rays, col_reach=dilate_cols + 1)
|
||||||
|
|
||||||
out.sort(key=lambda c: c.d)
|
out.sort(key=lambda c: c.d)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _hanging(tf: TrackFrame, abs_lat, in_range, build, *, h_hi: float, h_top: float,
|
||||||
|
half_width_top: float, top_d_max: float, min_rays: int,
|
||||||
|
col_reach: int, margin_u: float = 0.3, below: float = 0.4) -> list[Candidate]:
|
||||||
|
"""Свисающее со свода в путь вагона: отдельный проход по верхней секции.
|
||||||
|
|
||||||
|
Основной проход тянет контекст до свода, чтобы колонна не выглядела
|
||||||
|
предметом, — и поэтому всё, что свисает с потолка, склеивается со сводом:
|
||||||
|
доля лучей в габарите у стержня из синтетики организаторов была ноль.
|
||||||
|
Просто не пускать свод в контекст нельзя: тогда освобождаются и стоящие
|
||||||
|
конструкции, прицепленные к своду, — на `roundT_doubleT` так вышла тревога
|
||||||
|
на светофоре у стрелки. Поэтому здесь свой проход: контекст — только сама
|
||||||
|
верхняя секция с запасом `margin_u` по сторонам и `below` вниз. Компонента,
|
||||||
|
у которой есть лучи ниже `h_hi`, продолжается к полу: это стоящий предмет
|
||||||
|
(столб, светофор, человек высокого роста), и его ведёт основной проход.
|
||||||
|
Висящему не нужна опора снизу, и обученное считывание его не судит
|
||||||
|
(см. central_complex._quality, `top_from`). EXPERIMENTS п. 19.
|
||||||
|
"""
|
||||||
|
band = (in_range & (tf.d < top_d_max) & (abs_lat < half_width_top)
|
||||||
|
& (tf.h >= h_hi) & (tf.h < h_top))
|
||||||
|
if not band.any():
|
||||||
|
return []
|
||||||
|
ext = (in_range & (tf.d < top_d_max) & (abs_lat < half_width_top + margin_u)
|
||||||
|
& (tf.h > h_hi - below) & (tf.h < h_top))
|
||||||
|
labels, n = cluster_by_depth(ext, tf.r, col_reach=col_reach)
|
||||||
|
if n == 0:
|
||||||
|
return []
|
||||||
|
flat_lbl = labels.ravel()
|
||||||
|
order = np.argsort(flat_lbl, kind="stable")
|
||||||
|
starts = np.searchsorted(flat_lbl[order], np.arange(n + 2))
|
||||||
|
band_flat = band.ravel()
|
||||||
|
low_flat = (tf.h < h_hi).ravel()
|
||||||
|
out = []
|
||||||
|
for lbl in range(1, n + 1):
|
||||||
|
idx = order[starts[lbl]:starts[lbl + 1]]
|
||||||
|
if idx.size < min_rays or low_flat[idx].any():
|
||||||
|
continue
|
||||||
|
sel = idx[band_flat[idx]]
|
||||||
|
if sel.size < min_rays:
|
||||||
|
continue
|
||||||
|
cand = build(sel, idx.size)
|
||||||
|
if cand is None:
|
||||||
|
continue
|
||||||
|
if cand.height < 0.1 and cand.h_min > h_top - 0.2:
|
||||||
|
# Плоское у самой верхней границы — это свод: на 80–90 м ошибка
|
||||||
|
# наклона плоскости пути опускает его в секцию (на `squareT_…`
|
||||||
|
# так вышла тревога на 83 м, высота 3.66–3.69 м при границе 3.7).
|
||||||
|
# Свисающее начинается ниже и имеет высоту.
|
||||||
|
continue
|
||||||
|
cand.extra["top"] = True
|
||||||
|
out.append(cand)
|
||||||
|
return out
|
||||||
|
|
|
||||||
|
|
@ -62,11 +62,30 @@ class Params:
|
||||||
# Верхняя секция габарита: от h_hi до h_top при |u| < half_width_top —
|
# Верхняя секция габарита: от h_hi до h_top при |u| < half_width_top —
|
||||||
# то, что свисает со свода в путь вагона (см. lobula.gauge_mask). Вагон
|
# то, что свисает со свода в путь вагона (см. lobula.gauge_mask). Вагон
|
||||||
# выше 3.5 м, а предмет «2×2 сверху» у организаторов висит низом на 3.0 м
|
# выше 3.5 м, а предмет «2×2 сверху» у организаторов висит низом на 3.0 м
|
||||||
# над рельсом — при верхе 2.3 м он был не виден вовсе. h_top <= h_hi —
|
# над рельсом — при верхе 2.3 м он был не виден вовсе. Верх 3.3, а не 3.7:
|
||||||
# секции нет. EXPERIMENTS п. 18.4.
|
# у свода в настоящих тоннелях свои кабели и кронштейны, и ошибка наклона
|
||||||
h_top: float = 3.7
|
# плоскости пути на 60–90 м опускает их на 0.1–0.3 м — при 3.7 они шли в
|
||||||
|
# тревоги (низ у них в основном выше 3.2 м, у предметов организаторов —
|
||||||
|
# 2.8 и 3.0). h_top <= h_hi — секции нет. EXPERIMENTS п. 18.4, 19.
|
||||||
|
h_top: float = 3.3
|
||||||
half_width_top: float = 1.0
|
half_width_top: float = 1.0
|
||||||
top_d_max: float = 90.0 # дальше верхней секции нет, м
|
top_d_max: float = 90.0 # дальше верхней секции нет, м
|
||||||
|
# Плоское у пола (см. central_complex._quality): вертикальный размер меньше
|
||||||
|
# flat_h, верх ниже flat_top над рельсом, не дальше flat_d — вес наблюдения
|
||||||
|
# умножается на flat_w. flat_h: 0 — выключено. EXPERIMENTS п. 19.
|
||||||
|
flat_h: float = 0.06
|
||||||
|
# Отдельный проход по верхней секции для свисающего со свода (см.
|
||||||
|
# lobula._hanging): в основном проходе оно склеено со сводом.
|
||||||
|
top_detect: bool = True
|
||||||
|
# Дальний трек (дальше far_confirm_from) поднимает тревогу, только прожив
|
||||||
|
# far_confirm кадров (см. descending.py). 0 — выключено. Замерено и
|
||||||
|
# отвергнуто: 15 кадров убирают короткие дальние фантомы, но полигон теряет
|
||||||
|
# 200 наблюдений, P@150 у человека стоя 0.46 → 0.37. EXPERIMENTS п. 19.
|
||||||
|
far_confirm: int = 0
|
||||||
|
far_confirm_from: float = 100.0
|
||||||
|
flat_top: float = 0.26
|
||||||
|
flat_d: float = 70.0
|
||||||
|
flat_w: float = 0.15
|
||||||
ctx_up: float = 4.0 # насколько контекст лезет выше габарита, м
|
ctx_up: float = 4.0 # насколько контекст лезет выше габарита, м
|
||||||
# Разделение фигуры и фона по движению (см. lobula.split_by_figure):
|
# Разделение фигуры и фона по движению (см. lobula.split_by_figure):
|
||||||
# порог скорости сближения вдоль луча, в долях пройденного пути.
|
# порог скорости сближения вдоль луча, в долях пройденного пути.
|
||||||
|
|
@ -292,7 +311,9 @@ class FlyGuard:
|
||||||
nov_fade_to=self.p.nov_fade_to,
|
nov_fade_to=self.p.nov_fade_to,
|
||||||
lying_exempt=self.p.lying_exempt,
|
lying_exempt=self.p.lying_exempt,
|
||||||
top_from=(self.p.h_hi if self.p.h_top > self.p.h_hi
|
top_from=(self.p.h_hi if self.p.h_top > self.p.h_hi
|
||||||
else float("inf")))
|
else float("inf")),
|
||||||
|
flat=(self.p.flat_h, self.p.flat_top, self.p.flat_d,
|
||||||
|
self.p.flat_w))
|
||||||
# без накопления улик трек подтверждается первым же наблюдением
|
# без накопления улик трек подтверждается первым же наблюдением
|
||||||
self.dn = (DescendingNeurons(
|
self.dn = (DescendingNeurons(
|
||||||
warn_evidence=self.p.warn_evidence,
|
warn_evidence=self.p.warn_evidence,
|
||||||
|
|
@ -306,7 +327,9 @@ class FlyGuard:
|
||||||
track_gate=self.p.track_gate,
|
track_gate=self.p.track_gate,
|
||||||
track_score=self.p.track_score,
|
track_score=self.p.track_score,
|
||||||
track_readout=track_readout,
|
track_readout=track_readout,
|
||||||
far_channel=self.p.far_channel) if self.p.use_tracking
|
far_channel=self.p.far_channel,
|
||||||
|
far_confirm=self.p.far_confirm,
|
||||||
|
far_confirm_from=self.p.far_confirm_from) 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))
|
||||||
|
|
||||||
|
|
@ -448,7 +471,11 @@ class FlyGuard:
|
||||||
h_lo_core=self.p.h_lo_core,
|
h_lo_core=self.p.h_lo_core,
|
||||||
core_width=self.p.core_width,
|
core_width=self.p.core_width,
|
||||||
core_from=self.p.core_from,
|
core_from=self.p.core_from,
|
||||||
platform_filter=self.p.platform_filter)
|
platform_filter=self.p.platform_filter,
|
||||||
|
h_top=self.p.h_top,
|
||||||
|
half_width_top=self.p.half_width_top,
|
||||||
|
top_d_max=self.p.top_d_max,
|
||||||
|
top_detect=self.p.top_detect)
|
||||||
|
|
||||||
with t("mushroom"):
|
with t("mushroom"):
|
||||||
cands = self.memory.annotate(cands)
|
cands = self.memory.annotate(cands)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue