Разрез вблизи: нашёл причину ближней потери, лечение не окупилось

This commit is contained in:
Данил Омелечко 2026-09-22 14:52:44 +03:00
parent 8a0badbc20
commit af317803ab
5 changed files with 98 additions and 6 deletions

View file

@ -1514,3 +1514,59 @@ P@150 0.32), а на всём, что меньше, — заметно хуже:
| тихий | `nov_fade_from: 0` | 3.5 | за 90 м почти нет | | тихий | `nov_fade_from: 0` | 3.5 | за 90 м почти нет |
| средний | `min_rays_far: 0` | 6.5 | человек стоя есть, мелочь хуже | | средний | `min_rays_far: 0` | 6.5 | человек стоя есть, мелочь хуже |
| **дальнобойный (умолчание)** | — | 8.0 | полная | | **дальнобойный (умолчание)** | — | 8.0 | полная |
### 12.7. Ближняя потеря: причина найдена, лечение отвергнуто
В воронке оставалась последняя необъяснённая дыра: на 6–30 м, где предмет
даёт сотни лучей, кандидат не появлялся в 18 % видимых наблюдений. Прямая
трассировка вставки на 6.5, 8, 10, 12, 20 и 28 м ничего не воспроизвела —
кандидат находился в **100 %** случаев (616 из 616). Ответ нашёлся в самих
данных полигона:
| смещение предмета от оси | наблюдений | без кандидата |
|---|---:|---:|
| 0.0 м | 198 | 0.01 |
| +0.9 м | 198 | **0.34** |
Хуже всего там, где есть платформа: `doubleT_platform` и
`squareT_platform_squareT_switch` по 0.27 против 0.11–0.14 у остальных.
Предмет у края габарита слипается с кромкой платформы в одну связную
компоненту и отбрасывается вместе с ней — **то же самое, что и в полосе
30–50 м**. Причина общая: разрез по контрасту запрещён ближе 55 м.
Комментарий в коде объяснял этот запрет тем, что вблизи предмет со стеной
не слипается. Замер показал, что это неверно, и объяснение исправлено.
Лечение пробовалось не «в лоб» (резать везде — уже отвергнуто в п. 12.1), а
избирательно: резать вблизи только компоненты, **слишком растянутые по
глубине**, чтобы быть предметом. Мерой служит разброс дальности внутри
компоненты: человек занимает десятки сантиметров, кромка платформы —
десятки метров. Две свёртки по меткам, доли миллисекунды.
| порог растянутости | ложных/км, знакомая | незнакомая |
|---|---:|---:|
| выключено | **8.0** | **20.3** |
| > 8 м | 10.0 | 25.2 |
| > 4 м | 12.0 | — |
| > 2 м | 12.0 | — |
Воронка при самом строгом пороге (решений от видимых наблюдений):
| полоса | без разреза | с разрезом |
|---|---:|---:|
| 6–30 м | 0.83 | **0.90** |
| 30–50 м | 0.59 | **0.68** |
| 50–70 м | **0.81** | 0.71 |
| 70–90 м | **0.62** | 0.54 |
**Отвергнуто.** Ближние полосы вылечились, но сломались средние: человек
стоя P@50 0.70 → 0.67, рабочая дальность «как есть» 80 → 62 м, посторонних
тревог на кадр 0.071 → 0.102. Механизм тот же, что и у тройки лучей вблизи:
разрез на 50–55 м дробит предмет на конкурирующие обрывки, и настоящий трек
проигрывает им в торможении по соседству.
Итог размена: мы вылечили бы полосу, которая при служебном торможении с
60 км/ч и так за тормозным путём, сломав ту, что внутри него, и заплатили бы
четвертью ложных треков на обеих сценах. Механизм оставлен в коде выключенным
(`near_long: 0`): на линии без платформ у самого габарита он может оказаться
дешевле — но проверять это надо замером на той линии.

View file

@ -234,6 +234,7 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
dilate_cols: int = 2, max_depth: float = 15.0, dilate_cols: int = 2, max_depth: float = 15.0,
ctx_up: float = 4.0, split_adv: float = 0.0, ctx_up: float = 4.0, split_adv: float = 0.0,
split_gap: float = 0.0, split_near: float = 0.0, split_gap: float = 0.0, split_near: 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) -> list[Candidate]:
@ -272,11 +273,32 @@ def find_candidates(tf: TrackFrame, lam: LaminaOutput, corridor: Corridor, *,
# и торчать из своего окружения. # и торчать из своего окружения.
fig = gap_img if (split_adv <= 0 or advance is None) else np.where(advance > split_adv, gap_img, 0.0) fig = gap_img if (split_adv <= 0 or advance is None) else np.where(advance > split_adv, gap_img, 0.0)
if split_near > 0: if split_near > 0:
# Ближе этой дальности покадровый тракт видит предмет и без разреза: # Ближе этой дальности разрез был выключен целиком, и причина
# компонента со стеной там не слипается, лучей много, форма читается. # стояла в комментарии неверная: считалось, что вблизи предмет со
# Резать там незачем, а обстановки, дающей контраст, в ближнем поле # стеной не слипается. Замер показал обратное — на 6…30 м предмет,
# на порядок больше — оттуда и берутся лишние кандидаты. # смещённый от оси на 0.9 м, теряется в 34 % наблюдений против 1 %
fig = np.where(tf.d > split_near, fig, 0.0) # на самой оси, и хуже всего там, где есть платформа. Он уходит
# вместе с её кромкой.
#
# Резать вблизи всё подряд дорого: обстановки, дающей контраст, в
# ближнем поле на порядок больше. Поэтому режем только компоненты,
# СЛИШКОМ РАСТЯНУТЫЕ по глубине, чтобы быть предметом: человек
# занимает по дальности десятки сантиметров, кромка платформы и
# стена — десятки метров. Мерой служит разброс дальности внутри
# компоненты (две свёртки по меткам, доли миллисекунды).
near = tf.d <= split_near
if near_long > 0.0 and n > 0:
lab = labels.ravel()
rr = np.where(np.isfinite(tf.r), tf.r, 0.0).ravel()
cnt = np.bincount(lab, minlength=n + 1).astype(np.float64)
s1 = np.bincount(lab, weights=rr, minlength=n + 1)
s2 = np.bincount(lab, weights=rr * rr, minlength=n + 1)
c = np.maximum(cnt, 1.0)
var = np.maximum(s2 / c - (s1 / c) ** 2, 0.0)
stretched = np.sqrt(var) > near_long
stretched[0] = False
near = near & ~stretched[labels]
fig = np.where(near, 0.0, fig)
labels, n = split_by_figure(labels, n, tf.r, fig, max_depth=max_depth, labels, n = split_by_figure(labels, n, tf.r, fig, max_depth=max_depth,
thr=split_gap, col_reach=dilate_cols + 1, thr=split_gap, col_reach=dilate_cols + 1,
top=split_top) top=split_top)

View file

@ -50,7 +50,12 @@ class Params:
# вытаскивает предмет из компоненты, растёкшейся вдоль стены круглого # вытаскивает предмет из компоненты, растёкшейся вдоль стены круглого
# тоннеля, где покадровый тракт слеп целиком. 0 — выключено. # тоннеля, где покадровый тракт слеп целиком. 0 — выключено.
split_gap: float = 6.0 split_gap: float = 6.0
split_near: float = 55.0 # ближе этого не резать: там и так видно split_near: float = 55.0 # ближе этого резать только растянутое
# Разброс дальности внутри компоненты, выше которого её режут и в
# ближней зоне: предмет по дальности занимает десятки сантиметров,
# кромка платформы и стена — десятки метров. 0 — не резать вблизи
# вовсе. Замер — EXPERIMENTS п. 12.7.
near_long: float = 0.0
split_top: int = 1 # сколько фигур выносить из одной компоненты split_top: int = 1 # сколько фигур выносить из одной компоненты
# Накопление слабых улик в координатах пути (см. fan_body.py). Заменяет # Накопление слабых улик в координатах пути (см. fan_body.py). Заменяет
# недоступный на большой дальности контраст к фону: на 140…185 м `gap` # недоступный на большой дальности контраст к фону: на 140…185 м `gap`
@ -317,6 +322,7 @@ class FlyGuard:
split_adv=self.p.split_adv, split_adv=self.p.split_adv,
split_gap=self.p.split_gap, split_gap=self.p.split_gap,
split_near=self.p.split_near, split_near=self.p.split_near,
near_long=self.p.near_long,
split_top=self.p.split_top, advance=advance, split_top=self.p.split_top, advance=advance,
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,

View file

@ -119,6 +119,8 @@ def main() -> None:
help="с какой дальности порог начинает падать, м") help="с какой дальности порог начинает падать, м")
ap.add_argument("--novelty-floor", type=float, default=None, ap.add_argument("--novelty-floor", type=float, default=None,
help="ниже этой новизны трек не считается") help="ниже этой новизны трек не считается")
ap.add_argument("--near-long", type=float, default=None,
help="разброс дальности, выше которого компоненту режут и вблизи; 0 — не резать вблизи")
ap.add_argument("--min-rays", type=int, default=None, ap.add_argument("--min-rays", type=int, default=None,
help="сколько лучей минимум образуют кандидата") help="сколько лучей минимум образуют кандидата")
ap.add_argument("--min-rays-far", type=int, default=None, ap.add_argument("--min-rays-far", type=int, default=None,
@ -177,6 +179,8 @@ def main() -> None:
over["warn_far_from"] = args.warn_far_from over["warn_far_from"] = args.warn_far_from
if args.novelty_floor is not None: if args.novelty_floor is not None:
over["novelty_floor"] = args.novelty_floor over["novelty_floor"] = args.novelty_floor
if args.near_long is not None:
over["near_long"] = args.near_long
if args.min_rays is not None: if args.min_rays is not None:
over["min_rays"] = args.min_rays over["min_rays"] = args.min_rays
if args.min_rays_far is not None: if args.min_rays_far is not None:

View file

@ -138,6 +138,8 @@ def main() -> None:
help="с какой дальности порог начинает падать, м") help="с какой дальности порог начинает падать, м")
ap.add_argument("--novelty-floor", type=float, default=None, ap.add_argument("--novelty-floor", type=float, default=None,
help="ниже этой новизны трек не считается") help="ниже этой новизны трек не считается")
ap.add_argument("--near-long", type=float, default=None,
help="разброс дальности, выше которого компоненту режут и вблизи; 0 — не резать вблизи")
ap.add_argument("--min-rays", type=int, default=None, ap.add_argument("--min-rays", type=int, default=None,
help="сколько лучей минимум образуют кандидата") help="сколько лучей минимум образуют кандидата")
ap.add_argument("--min-rays-far", type=int, default=None, ap.add_argument("--min-rays-far", type=int, default=None,
@ -186,6 +188,8 @@ def main() -> None:
over["warn_far_from"] = args.warn_far_from over["warn_far_from"] = args.warn_far_from
if args.novelty_floor is not None: if args.novelty_floor is not None:
over["novelty_floor"] = args.novelty_floor over["novelty_floor"] = args.novelty_floor
if args.near_long is not None:
over["near_long"] = args.near_long
if args.min_rays is not None: if args.min_rays is not None:
over["min_rays"] = args.min_rays over["min_rays"] = args.min_rays
if args.min_rays_far is not None: if args.min_rays_far is not None: