слияние с origin/main: интеграция жёсткого порога по треку (track_gate) с алгоритмическими улучшениями и докером

This commit is contained in:
Данил Омелечко 2026-09-22 21:18:31 +03:00
commit f5cbe2af17
9 changed files with 285 additions and 35 deletions

View file

@ -87,7 +87,7 @@ flyguard/ ядро: стадии обработки, память, сч
pipeline.py сборка pipeline.py сборка
synth.py вставка предметов трассировкой лучей synth.py вставка предметов трассировкой лучей
tools/ обучение, оценка, разбор tools/ обучение, оценка, разбор
tests/ 32 теста, запускаются без данных и без ROS tests/ 34 теста, запускаются без данных и без ROS
docs/ методика и результаты docs/ методика и результаты
artifacts/ обученные модели artifacts/ обученные модели
``` ```

View file

@ -1747,3 +1747,197 @@ P@150 0.32), а на всём, что меньше, — заметно хуже:
И отдельно: о ложных тревогах в той работе не сказано **ничего** — ни одного И отдельно: о ложных тревогах в той работе не сказано **ничего** — ни одного
упоминания false positive или false alarm. У нас они измерены на двух сценах упоминания false positive или false alarm. У нас они измерены на двух сценах
и выражены в треках на километр пути. и выражены в треках на километр пути.
---
## 15. Решение по треку, а не по кадру
Воронка потерь (п. 12) разделила потери на две болезни, и вторая до сих пор не
лечилась: на 90…160 м трек **заводится, а решения не даёт**. В базовой настройке
это 0.20 и 0.24 от видимых наблюдений — больше, чем теряется на всех остальных
ступенях вместе.
Порогом это не чинится, и это уже измерено (п. 12.4): 0.5 → 0.3 не меняет
ничего, потому что улика у далёкого трека не «чуть ниже порога», а около нуля.
Значит, дело не в пороге, а в том, **чем** мы меряем трек.
### 15.1. Улика суммируется и упирается в потолок
Улика — это `min(1, Σ gain·w)` с утечкой за промахи. Два её свойства работают
против нас:
* она **суммируется**, то есть отвечает на вопрос «давно ли я на это смотрю», а
не «на что именно я смотрю»;
* она **обрезана единицей**, и у всего, что наблюдается дольше пары секунд, она
просто равна 1.
Второе проверяется не на синтетике, а на единственной записи с настоящим
объектом — `doubleT_obstacle`, 753 наблюдения трека, из них 188 приходится на
реальный объект 0.67 × 1.35 м на 55 м
(`tools/check_obstacle.py --tracks --memory ... --mbon artifacts/mbon_folds/mbon_roundT_doubleT.npz`):
| признак | объект | ложные треки | AUC |
|---|---|---|---|
| **улика** | **1.000** | **1.000** | **0.624** |
| средний вес наблюдения `w_mean` | 0.998 | 0.269 | 1.000 |
| средний отсчёт считывания `p_mean` | 0.998 | 0.582 | 1.000 |
| максимум отсчёта `p_max` | 1.000 | 0.782 | 1.000 |
| лучей к ожидаемым, в среднем | 6.483 | 1.384 | 1.000 |
| возраст трека, кадров | 94.5 | 52.0 | 0.679 |
Улика различает предмет и ложные треки с AUC 0.624 — чуть лучше монетки, — и
не потому, что сигнала нет, а потому что и у того, и у другого она упёрлась в
потолок. **Тот самый вес, из которого улика складывается, различает их
идеально.** Сумма потеряла то, что было в слагаемых.
Отсюда и решение: сравнивать с порогом не улику, а её геометрическую смесь со
средним весом наблюдения, `w_mean^b · улика^(1−b)`. При `b = 0` всё ровно как
раньше, так что размен меряется, а не объявляется.
### 15.2. Выборка по трекам
Полигон научили попутно выкладывать описание каждого живого трека на каждом
кадре с меткой «это вставленный предмет» (`make_benchmark.py --tracks-out`).
Собирается это внутри полигона нарочно: та же память тоннеля, то же покадровое
считывание, те же пороги — обученное на другой обстановке считывание нечего и
мерить. Вышло 10 819 наблюдений трека, после отбора «не меньше двух
подтверждений» (столько же требует `descending.py`) — 9 299, предметных 42.1 %.
Что даёт каждый признак сам по себе, по полосам дальности:
| полоса | набл. | улика | `p_mean` | `p_max` | `w_mean` | `−s_std` |
|---|---|---|---|---|---|---|
| 0–30 м | 2208 | 0.962 | 0.954 | 0.956 | 0.946 | 0.876 |
| 30–55 м | 1216 | 0.892 | **0.989** | 0.924 | 0.965 | 0.769 |
| 55–80 м | 1474 | 0.983 | 0.949 | **0.994** | 0.914 | 0.562 |
| 80–110 м | 1777 | 0.688 | 0.801 | **0.845** | 0.832 | 0.459 |
| 110–160 м | 1977 | 0.724 | 0.849 | 0.815 | **0.900** | 0.694 |
| 160–230 м | 647 | 0.306 | 0.218 | 0.218 | 0.218 | **0.869** |
| всё | 9299 | 0.865 | 0.895 | 0.890 | 0.880 | 0.670 |
Главное здесь — полосы 80…160 м, ровно те, где воронка и теряет: улика 0.688 и
0.724, а история наблюдений 0.832 и 0.900.
### 15.3. Почему за 160 м всё переворачивается
В последней полосе AUC 0.218…0.306 — то есть признаки указывают **в обратную
сторону**. Разбор показывает, что дело не в признаках:
| | наблюдений | возраст | подтверждений | улика | `p_mean` |
|---|---|---|---|---|---|
| настоящий предмет | 114 | 2.0 | 2.0 | 0.094 | 0.104 |
| ложные треки | 533 | 5.0 | 4.0 | 0.234 | 0.351 |
На самом краю дальности предмет — **всегда самое молодое, что есть в сцене**:
он только что вошёл в зону видимости, а всё, что уже ведётся, ведётся дольше.
Любая величина, растущая со временем наблюдения, там обязана ранжировать
наоборот. Это не лечится ни моделью, ни порогом; это граница метода, и её надо
называть, а не прятать.
### 15.4. Привязка к месту: признак, который врёт на синтетике
Самым заманчивым признаком казался `s_std` — разброс места в тоннеле, которое
подразумевает наблюдение (пройденный путь плюс измеренная дальность). У
неподвижного предмета оно обязано быть постоянным. На полигоне признак и
выглядел блестяще: за 160 м AUC 0.869 при том, что все остальные там
перевёрнуты, разброс 0.01 м у предмета против 1.08 м у ложных треков.
На настоящем объекте он **перевёрнут**: 0.365 м у предмета против 0.013 м у
ложных треков, AUC 0.223.
Причина в устройстве вставки. Синтетический предмет ставится на дальность
`d_start − s(t)`, где `s(t)` — тот же самый проинтегрированный путь, который
двигает мировую координату треков. Ошибка оценки собственного движения входит в
обе величины и сокращается: у вставленного предмета привязка не дрожит **по
построению**. У настоящего объекта она дрожит на реальную ошибку одометрии, а
у ложных треков, прилипших к геометрии тоннеля, — не дрожит вовсе.
Это тот же класс ошибки, что артефакт яркости (п. 11.1), и найден он тем же
способом: цифру с полигона проверили на единственной реальной метке, какая
есть. Оба разброса (`s_std`, `u_std`) выброшены из обучения; в дескрипторе они
оставлены нарочно, чтобы проверку можно было повторить.
### 15.5. Обученное считывание по треку — отрицательный результат
Сделали то же, что с покадровым считыванием: те же клетки Кеньона, то же
торможение APL, один выход, вход — 21 признак трека
(`flyguard/track_readout.py`, `tools/train_track.py`). Проверка —
leave-one-bag-out.
| ёмкость | AUC |
|---|---|
| 500 клеток | 0.8713 |
| 1000 | 0.8641 |
| 2000 | 0.8651 |
| 4000 | 0.8730 |
| 4000, без обоих разбросов | 0.8801 |
Для сравнения: улика сама по себе даёт 0.865, `w_mean` — 0.880, `p_mean` —
0.895, произведение `p_mean · улика` — 0.904.
**Модель не обгоняет ни один из признаков, которые ей же и скормили.** По
полосам она выигрывает у улики на 30…55 и 80…110 м и проигрывает на 55…80 и
за 160 м. Причина понятна и её стоит назвать: наблюдений 9 299, но они не
независимы — это сотни кадров подряд по одному и тому же треку. Независимых
треков тут сотни, а не тысячи, и разрежённому коду на такой выборке учиться
нечему.
Вывод: считывание по трекам остаётся в репозитории как инструмент замера, в
решении **не участвует** (`track_score` по умолчанию `w_mean`, модель
включается только явным `--track-score model`). Цена вопроса — один вечер и
28 секунд обучения; сэкономлено то, что модель не поехала в продукт.
### 15.6. Размен: что стоит смешивание
Полигон, `человек_стоя`, память тоннеля на месте, считывание по складкам.
«Посторонних» — тревоги, не совпавшие с вставленным предметом, на 14 004
наблюдения.
| настройка | P@50 | P@100 | P@150 | посторонних |
|---|---|---|---|---|
| улика одна (`track_blend = 0`) | 0.70 | 0.53 | 0.33 | 999 |
| смешивание 0.3 | 0.71 | 0.43 | 0.26 | **354** |
| смешивание 0.5 | 0.63 | 0.17 | 0.10 | 305 |
| смешивание 0.7 | 0.63 | 0.17 | 0.10 | 296 |
Колено видно сразу: за 0.3 ложные почти не падают (354 → 305 → 296), а
обнаружение рушится. Причина арифметическая, и её стоит назвать: обе величины
меньше единицы, поэтому их произведение **всегда ниже улики**, и при
неподвижном пороге смешивание работает как его повышение. Сравнивать настройки
имеет смысл только на выровненной рабочей точке — то есть опустив порог на
столько, на сколько смешивание опустило отсчёт.
Полезная доля смешивания, таким образом, мала, и весь вопрос в том, на что
потратить освободившиеся три четверти ложных тревог.
### 15.7. Жёсткий порог вместо смешивания — отрицательный результат
Смешивание `w_mean^b · улика^(1−b)` опускает отсчёт **и предмету тоже**, потому
что обе величины меньше единицы. Напрашивается вместо него порог: трек с
низким средним весом просто не рассматривать, а прошедшему не отнимать ничего.
Тем более что на реальном объекте разделение выглядит идеальным — 0.998 против
0.269.
Замер (полигон, `человек_стоя`):
| настройка | P@50 | P@100 | P@150 | посторонних |
|---|---|---|---|---|
| база | 0.70 | 0.53 | 0.33 | 999 |
| порог 0.40 | 0.60 | **0.03** | 0.10 | 158 |
| порог 0.55 | 0.37 | **0.00** | 0.01 | 85 |
Ложные тревоги падают в шесть и в двенадцать раз, а обнаружение исчезает.
Воронка показывает где: в полосе 90…120 м до решения доходит **0.02** от
видимых наблюдений против 0.49 в базе.
Причина — та же, что у всех остальных порогов в этой работе: **неподвижный
порог слеп к дальности**. Средний вес наблюдения сам падает с расстоянием, и
сильно: в него входит контраст к фону, который за 140 м структурно равен нулю
(п. 7.3), и вероятность считывания, которая падает вместе с числом лучей.
Порог 0.55 отсекает не «плохие треки», а «всё, что дальше семидесяти метров».
Чтобы порог тут работал, его пришлось бы нормировать на ожидаемый для этой
дальности вес — то есть сделать ровно то, что уже сделано для числа лучей
(п. 12.5), для новизны (п. 12.3) и для утечки улики. Это возможно, но
смешивание даёт тот же эффект мягко и без ещё одной калибровочной кривой,
поэтому порог оставлен выключенным (`track_gate = 0`).

View file

@ -61,6 +61,7 @@ class Track:
w_max: float = 0.0 w_max: float = 0.0
p_sum: float = 0.0 # отсчёт считывания по кандидату, если оно есть p_sum: float = 0.0 # отсчёт считывания по кандидату, если оно есть
p_max: float = 0.0 p_max: float = 0.0
p_n: int = 0 # сколько раз он вообще был
w_ema: float = 0.0 # он же, но со скользящим забыванием w_ema: float = 0.0 # он же, но со скользящим забыванием
r_sum: float = 0.0 # лучи, нормированные на ожидаемые для дальности r_sum: float = 0.0 # лучи, нормированные на ожидаемые для дальности
r_last: float = 0.0 r_last: float = 0.0
@ -86,8 +87,13 @@ class Track:
@property @property
def p_mean(self) -> float: def p_mean(self) -> float:
"""Средний покадровый отсчёт считывания MBON за жизнь трека.""" """Средний покадровый отсчёт считывания MBON за жизнь трека.
return self.p_sum / max(self.n_obs, 1)
Без переданной конвейеру модели отсчёта нет вовсе, и возвращать тут
ноль значило бы тихо погасить все треки разом. Поэтому в таком случае
отдаётся средний вес наблюдения — он считается всегда.
"""
return self.p_sum / self.p_n if self.p_n else self.w_mean
def distance(self, s_now: float) -> float: def distance(self, s_now: float) -> float:
return self.s_world - s_now return self.s_world - s_now
@ -95,11 +101,15 @@ class Track:
def observe(self, c: Candidate, s_obs: float, w: float) -> None: def observe(self, c: Candidate, s_obs: float, w: float) -> None:
"""Записать наблюдение в сводку. """Записать наблюдение в сводку.
`s_obs` — место в тоннеле, которое подразумевает это наблюдение: Полезное здесь — `w_sum` и `p_sum`: улика суммируется и упирается в
пройденный путь плюс измеренная дальность. У НАСТОЯЩЕГО неподвижного потолок, а среднее того же веса различает предмет и обстановку и после
предмета оно не должно меняться от кадра к кадру, как бы поезд ни насыщения (EXPERIMENTS п. 15.1).
ехал, и разброс этой величины — самая прямая проверка гипотезы,
какая у трека вообще есть. В улику она сейчас не входит никак. `s_obs` — место в тоннеле, которое подразумевает наблюдение: путь
плюс дальность. Копится ТОЛЬКО ради замеров: на полигоне его разброс
выглядит сильным признаком, а на настоящем объекте он перевёрнут,
потому что вставка стоит на дальности, посчитанной из той же оценки
движения (п. 15.4). В решении не участвует и участвовать не должен.
""" """
i = float(self.n_obs) i = float(self.n_obs)
self.n_obs += 1 self.n_obs += 1
@ -115,9 +125,11 @@ class Track:
# Скользящее забывание (полупериод около трёх наблюдений) отвечает # Скользящее забывание (полупериод около трёх наблюдений) отвечает
# на вопрос «каков он сейчас», а не «каким был с самого начала». # на вопрос «каков он сейчас», а не «каким был с самого начала».
self.w_ema = w if self.n_obs == 1 else 0.8 * self.w_ema + 0.2 * w self.w_ema = w if self.n_obs == 1 else 0.8 * self.w_ema + 0.2 * w
p = float(c.extra.get("mbon", 0.0)) if c.extra else 0.0 p = c.extra.get("mbon") if c.extra else None
self.p_sum += p if p is not None:
self.p_max = max(self.p_max, p) self.p_n += 1
self.p_sum += float(p)
self.p_max = max(self.p_max, float(p))
r = c.n_rays / expected_rays(c.d) r = c.n_rays / expected_rays(c.d)
self.r_sum += r self.r_sum += r
self.r_last = r self.r_last = r

View file

@ -64,7 +64,7 @@ class DescendingNeurons:
novelty_floor: float = 0.10, max_range: float = 200.0, novelty_floor: float = 0.10, max_range: float = 200.0,
warn_far: float | None = None, warn_far_from: float = 90.0, warn_far: float | None = None, warn_far_from: float = 90.0,
track_blend: float = 0.0, track_score: str = "w_mean", track_blend: float = 0.0, track_score: str = "w_mean",
track_readout=None): track_gate: float = 0.0, track_readout=None):
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
@ -87,6 +87,13 @@ class DescendingNeurons:
# покадрового считывания: 0 — решает только улика, ровно как # покадрового считывания: 0 — решает только улика, ровно как
# раньше, 1 — только модель. Так размен меряется, а не объявляется. # раньше, 1 — только модель. Так размен меряется, а не объявляется.
self.track_blend = track_blend self.track_blend = track_blend
# Жёсткий порог по тому же отсчёту — вместо смешивания или вместе с
# ним. Разница существенная: смешивание опускает отсчёт И предмету
# тоже (обе величины меньше единицы), а порог ничего не отнимает у
# того, кто его прошёл. На реальном объекте средний вес 0.998 у
# предмета против 0.269 у ложных треков — разделение, под которое
# порог и просится. 0 — не проверять.
self.track_gate = track_gate
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,7 +127,8 @@ class DescendingNeurons:
continue continue
ready.append((t, d)) ready.append((t, d))
use_track = self.track_blend > 0.0 and bool(ready) use_track = ((self.track_blend > 0.0 or self.track_gate > 0.0)
and bool(ready))
if use_track and self.track_score == "model": if use_track and self.track_score == "model":
if self.track_readout is None: if self.track_readout is None:
use_track = False use_track = False
@ -139,6 +147,10 @@ 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 self.track_gate > 0.0 and q < self.track_gate:
self._latched.discard(t.id)
continue
if self.track_blend > 0.0:
b = min(max(self.track_blend, 0.0), 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))

View file

@ -112,6 +112,7 @@ class Params:
# обученного отсчёта с уликой перед сравнением с порогом. 0 — решает # обученного отсчёта с уликой перед сравнением с порогом. 0 — решает
# только улика, как раньше. Замер — EXPERIMENTS п. 15. # только улика, как раньше. Замер — EXPERIMENTS п. 15.
track_blend: float = 0.0 track_blend: float = 0.0
track_gate: float = 0.0 # жёсткий порог по тому же отсчёту
# Чем считать «насколько это предмет» по треку: `w_mean` — средний вес # Чем считать «насколько это предмет» по треку: `w_mean` — средний вес
# наблюдения за всю жизнь, `w_ema` — он же со скользящим забыванием, # наблюдения за всю жизнь, `w_ema` — он же со скользящим забыванием,
# `p_mean` — средний покадровый отсчёт считывания, `model` — обученное # `p_mean` — средний покадровый отсчёт считывания, `model` — обученное
@ -225,6 +226,7 @@ class FlyGuard:
warn_far=self.p.warn_far, warn_far=self.p.warn_far,
warn_far_from=self.p.warn_far_from, warn_far_from=self.p.warn_far_from,
track_blend=self.p.track_blend, track_blend=self.p.track_blend,
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) 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,

View file

@ -11,26 +11,24 @@
здесь и теряется дальнее обнаружение: на 120…160 м трек заводится в 62 % здесь и теряется дальнее обнаружение: на 120…160 м трек заводится в 62 %
наблюдений, а до решения доходит 38 % — улика есть, порога не набирает. наблюдений, а до решения доходит 38 % — улика есть, порога не набирает.
Поэтому вход здесь другой: Поэтому вход здесь другой: сводки за всю жизнь трека, а не последнее
значение, — средний и максимальный вес наблюдения, средний и максимальный
покадровый отсчёт MBON, наклон числа лучей по времени, разбросы положения.
* `s_std` — разброс места в тоннеле, которое подразумевают наблюдения. **ЧЕМ ЭТО КОНЧИЛОСЬ.** Обученная модель не обгоняет ни один из признаков,
У неподвижного предмета оно постоянно по построению, у ложного трека, которые ей же и скормили: leave-one-bag-out AUC 0.873 против 0.880 у одного
собранного из разных кусков стены, — гуляет. В улику этот признак среднего веса наблюдения и 0.895 у среднего отсчёта считывания. Причина — не в
сейчас не входит вообще, ни одним множителем; схеме: наблюдений 9 299, но это сотни кадров подряд по одному и тому же треку,
* `rays_slope` — растёт ли число лучей так, как обязано расти при и независимых примеров тут сотни, а не тысячи. Поэтому в решении участвует
сближении. У предмета оно идёт по закону решётки, у случайного **не модель, а прямо среднее** (`descending.track_score`, по умолчанию
совпадения — как придётся; `w_mean`), а всё в этом файле — инструмент замера. Разбор — EXPERIMENTS п. 15.
* сводки веса наблюдения и покадрового отсчёта MBON: среднее и максимум
за всю жизнь трека, а не последнее значение.
Схема при этом не меняется: те же клетки Кеньона по шесть когтей, то же Отдельно: `s_std` и `u_std` в решении использовать нельзя. На полигоне разброс
торможение APL, тот же один выход. Это ровно та же операция, что MBON привязки к месту выглядит лучшим признаком дальней полосы (AUC 0.869 за 160 м),
проделывает над кандидатом, только этажом выше — над тем, что центральный а на настоящем объекте он перевёрнут (0.223): вставка ставится на дальность,
комплекс успел накопить. У мухи так же: нисходящие нейроны получают сходящийся посчитанную из той же оценки собственного движения, что двигает мировую
вход и от зрительных путей, и от центрального комплекса, а не только от сетчатки. координату, и разброса у неё нет по построению. Признаки оставлены в
дескрипторе нарочно — чтобы проверку можно было повторить (п. 15.4).
Стоимость — одно умножение матрицы на трек, и только для тех треков, что уже
прошли дешёвые проверки в `descending.py`.
""" """
from __future__ import annotations from __future__ import annotations
@ -55,6 +53,8 @@ TRACK_FEATURES = (
"rays_slope", # и наклон по времени "rays_slope", # и наклон по времени
"w_mean", "w_max", # вес наблюдения "w_mean", "w_max", # вес наблюдения
"p_mean", "p_max", # покадровый отсчёт считывания "p_mean", "p_max", # покадровый отсчёт считывания
# Оба разброса — ТОЛЬКО для замеров: на синтетике сильны, на реальном
# объекте перевёрнуты. См. шапку файла и EXPERIMENTS п. 15.4.
"s_std", # разброс места в тоннеле, м "s_std", # разброс места в тоннеле, м
"u_std", # разброс поперечного положения, м "u_std", # разброс поперечного положения, м
) )

View file

@ -519,6 +519,29 @@ def test_track_accumulators_do_not_touch_the_evidence():
assert abs(t.evidence - cx.gain * t.w_mean) < 1e-6 assert abs(t.evidence - cx.gain * t.w_mean) < 1e-6
def test_track_p_mean_falls_back_when_there_is_no_readout():
"""Без модели покадрового считывания отсчёта нет, и ноль тут погасил бы всё."""
from flyguard.central_complex import Track
t = Track(id=1, s_world=60.0, u=0.0, h=0.3, width=0.5, height=1.7,
n_obs=4, w_sum=4 * 0.8)
assert t.p_n == 0
assert abs(t.p_mean - 0.8) < 1e-9
t.p_n, t.p_sum = 4, 4 * 0.3
assert abs(t.p_mean - 0.3) < 1e-9
def test_track_gate_removes_the_weak_track_without_touching_the_strong_one():
"""Порог ничего не отнимает у прошедшего — в отличие от смешивания."""
from flyguard.descending import DescendingNeurons
out = DescendingNeurons(track_gate=0.5, track_score="w_mean").decide(
_pair_of_tracks(), speed=10.0)
assert {o.track_id for o in out.objects} == {1}
assert out.objects[0].confidence == pytest.approx(
DescendingNeurons().decide(_pair_of_tracks(), speed=10.0).objects[0].confidence)
# ------------------------------------------------------------ улучшения габарита и решения # ------------------------------------------------------------ улучшения габарита и решения
class _MockTrackFrame: class _MockTrackFrame:
@ -681,4 +704,3 @@ def test_benchmark_augmentation_logic():
assert sc.name == "человек_лежа" assert sc.name == "человек_лежа"
assert sc.v_lat == 0.15 assert sc.v_lat == 0.15
assert not sc.is_static assert not sc.is_static

View file

@ -161,6 +161,8 @@ def main() -> None:
help="каталог с моделями по складкам (track_<бэг>.npz)") help="каталог с моделями по складкам (track_<бэг>.npz)")
ap.add_argument("--track-blend", type=float, default=None, ap.add_argument("--track-blend", type=float, default=None,
help="доля считывания по треку в решении; 0 — только улика") help="доля считывания по треку в решении; 0 — только улика")
ap.add_argument("--track-gate", type=float, default=None,
help="жёсткий порог по отсчёту трека; 0 — не проверять")
ap.add_argument("--track-score", default=None, ap.add_argument("--track-score", default=None,
choices=("w_mean", "w_ema", "p_mean", "model"), choices=("w_mean", "w_ema", "p_mean", "model"),
help="чем мерить трек: средний вес наблюдения, средний " help="чем мерить трек: средний вес наблюдения, средний "
@ -231,6 +233,8 @@ def main() -> None:
over["track_blend"] = args.track_blend over["track_blend"] = args.track_blend
if args.track_score is not None: if args.track_score is not None:
over["track_score"] = args.track_score over["track_score"] = args.track_score
if args.track_gate is not None:
over["track_gate"] = args.track_gate
params = Params(**over) params = Params(**over)
fold_paths: dict[str, str] = {} fold_paths: dict[str, str] = {}
if args.mbon_dir: if args.mbon_dir:

View file

@ -234,6 +234,8 @@ def main() -> None:
help="каталог с моделями по складкам (track_<бэг>.npz)") help="каталог с моделями по складкам (track_<бэг>.npz)")
ap.add_argument("--track-blend", type=float, default=None, ap.add_argument("--track-blend", type=float, default=None,
help="доля считывания по треку в решении; 0 — только улика") help="доля считывания по треку в решении; 0 — только улика")
ap.add_argument("--track-gate", type=float, default=None,
help="жёсткий порог по отсчёту трека; 0 — не проверять")
ap.add_argument("--track-score", default=None, ap.add_argument("--track-score", default=None,
choices=("w_mean", "w_ema", "p_mean", "model"), choices=("w_mean", "w_ema", "p_mean", "model"),
help="чем мерить трек: средний вес наблюдения, средний " help="чем мерить трек: средний вес наблюдения, средний "
@ -346,6 +348,8 @@ def main() -> None:
over["track_blend"] = args.track_blend over["track_blend"] = args.track_blend
if args.track_score is not None: if args.track_score is not None:
over["track_score"] = args.track_score over["track_score"] = args.track_score
if args.track_gate is not None:
over["track_gate"] = args.track_gate
params = Params(**over) params = Params(**over)
laterals = tuple(float(x) for x in args.laterals.split(",")) laterals = tuple(float(x) for x in args.laterals.split(","))