forked from Dan4ick/Lidar_Muxa
параллельный прогон инструментов по бэгам
This commit is contained in:
parent
d76c99eb60
commit
00e61bcabc
6 changed files with 869 additions and 660 deletions
13
README.md
13
README.md
|
|
@ -87,7 +87,7 @@ flyguard/ ядро: стадии обработки, память, сч
|
||||||
pipeline.py сборка
|
pipeline.py сборка
|
||||||
synth.py вставка предметов трассировкой лучей
|
synth.py вставка предметов трассировкой лучей
|
||||||
tools/ обучение, оценка, разбор
|
tools/ обучение, оценка, разбор
|
||||||
tests/ 27 тестов, запускаются без данных и без ROS
|
tests/ 29 тестов, запускаются без данных и без ROS
|
||||||
docs/ методика и результаты
|
docs/ методика и результаты
|
||||||
artifacts/ обученные модели
|
artifacts/ обученные модели
|
||||||
```
|
```
|
||||||
|
|
@ -119,6 +119,17 @@ python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \
|
||||||
python tools/plot_benchmark.py # кривые и график
|
python tools/plot_benchmark.py # кривые и график
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Тяжёлые шаги сами раскладываются по бэгам на процессы — записей пять, физических
|
||||||
|
ядер шесть, и это вся доступная зернистость: конвейер держит состояние между
|
||||||
|
кадрами, поэтому разрезать одну запись нельзя. Замерено: полигон 134 → 36 с,
|
||||||
|
сбор выборки 96 → 26 с на облегчённой конфигурации, то есть 3.7–3.8×, и файл на
|
||||||
|
выходе совпадает с последовательным **побайтово**. Отключается `--jobs 1`.
|
||||||
|
|
||||||
|
Для замера задержки кадра `--jobs 1` обязателен: под пятью процессами время
|
||||||
|
кадра растёт с 32 до 56 мс. Это свойство замера, а не конвейера, поэтому
|
||||||
|
`evaluate.py` в параллельном режиме печатает задержку как `nan` — чтобы такое
|
||||||
|
число нельзя было случайно привести в отчёте.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Где мы сейчас
|
## Где мы сейчас
|
||||||
|
|
|
||||||
|
|
@ -404,3 +404,49 @@ def test_real_bag_projects_without_angular_error():
|
||||||
r = img.r_near[img.valid]
|
r = img.r_near[img.valid]
|
||||||
assert r.min() > 0 and r.max() < 250
|
assert r.min() > 0 and r.max() < 250
|
||||||
assert np.all(img.r_far[img.valid] >= img.r_near[img.valid] - 1e-3)
|
assert np.all(img.r_far[img.valid] >= img.r_near[img.valid] - 1e-3)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ раскладка по бэгам
|
||||||
|
|
||||||
|
_TOOLS = ROOT / "tools" # выгрузка: tools лежит рядом с тестами
|
||||||
|
if not _TOOLS.exists():
|
||||||
|
_TOOLS = ROOT.parents[2] / "tools" # основной проект: ros2_ws/src/flyguard
|
||||||
|
if str(_TOOLS) not in sys.path:
|
||||||
|
sys.path.insert(0, str(_TOOLS))
|
||||||
|
|
||||||
|
import _parallel as _P # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _twice(x):
|
||||||
|
"""Задача для проверки. Верхнего уровня: иначе её не передать в процесс."""
|
||||||
|
return x * 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_parallel_keeps_task_order_when_results_arrive_out_of_order():
|
||||||
|
"""Считается по готовности, складывается по номеру задачи.
|
||||||
|
|
||||||
|
Ломается это незаметно и опасно: цифры остаются правдоподобными, просто
|
||||||
|
приписываются не тому бэгу. Поэтому проверяется не «столько же строк», а
|
||||||
|
что результат каждой задачи лёг на своё место.
|
||||||
|
"""
|
||||||
|
tasks = list(range(7))
|
||||||
|
want = [(t, t * 2) for t in tasks]
|
||||||
|
|
||||||
|
seq = [None] * len(tasks)
|
||||||
|
for i, t, r, _ in _P.run(_twice, tasks, jobs=1):
|
||||||
|
seq[i] = (t, r)
|
||||||
|
|
||||||
|
par = [None] * len(tasks)
|
||||||
|
for i, t, r, _ in _P.run(_twice, tasks, jobs=4):
|
||||||
|
par[i] = (t, r)
|
||||||
|
|
||||||
|
assert seq == want
|
||||||
|
assert par == want
|
||||||
|
|
||||||
|
|
||||||
|
def test_parallel_never_starts_more_processes_than_there_are_bags():
|
||||||
|
"""Бэгов пять, и шестой процесс занять нечем."""
|
||||||
|
assert _P.resolve(0, 1) == 1 # одна задача — без пула вовсе
|
||||||
|
assert _P.resolve(8, 3) == 3 # просили больше, чем есть работы
|
||||||
|
assert _P.resolve(1, 5) == 1 # явная последовательная отладка
|
||||||
|
assert 1 <= _P.resolve(0, 5) <= 5
|
||||||
|
|
|
||||||
91
tools/_parallel.py
Normal file
91
tools/_parallel.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""Раскладка задач по бэгам на процессы.
|
||||||
|
|
||||||
|
Конвейер держит состояние между кадрами — пройденный путь, треки, накопитель
|
||||||
|
складчатого тела, — поэтому разрезать один бэг нельзя. Зато бэги независимы
|
||||||
|
друг от друга, и это ровно та зернистость, которая нужна: пять записей на
|
||||||
|
шесть физических ядер.
|
||||||
|
|
||||||
|
Результат совпадает с последовательным прогоном **точно**, а не «примерно»:
|
||||||
|
генератор случайных чисел создаётся внутри задачи от того же зерна, общей
|
||||||
|
изменяемой памяти между бэгами нет. Порядок родитель восстанавливает сам —
|
||||||
|
печатает по готовности, а сохраняет в исходном.
|
||||||
|
|
||||||
|
Ускорение упирается не в ядра, а в число бэгов: их пять, и больше пяти
|
||||||
|
процессов тут просто нечем занять.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
|
|
||||||
|
|
||||||
|
def limit_threads() -> None:
|
||||||
|
"""Один поток BLAS на процесс.
|
||||||
|
|
||||||
|
Вызывать ДО создания пула: переменные наследуются дочерним процессом при
|
||||||
|
запуске, а число потоков BLAS выбирает один раз при импорте numpy и потом
|
||||||
|
не меняет. Без этого каждый из пяти воркеров разворачивается на все ядра
|
||||||
|
и они дерутся за те же шесть.
|
||||||
|
"""
|
||||||
|
for v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS",
|
||||||
|
"NUMEXPR_NUM_THREADS", "VECLIB_MAXIMUM_THREADS"):
|
||||||
|
os.environ.setdefault(v, "1")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(jobs: int, n_tasks: int) -> int:
|
||||||
|
"""Сколько процессов поднимать: 0 — по числу задач, но не больше ядер."""
|
||||||
|
if n_tasks <= 1:
|
||||||
|
return 1
|
||||||
|
if jobs > 0:
|
||||||
|
return max(1, min(jobs, n_tasks))
|
||||||
|
cpu = os.cpu_count() or 2
|
||||||
|
return max(1, min(n_tasks, cpu // 2)) # логических ядер вдвое больше физических
|
||||||
|
|
||||||
|
|
||||||
|
class _Timed:
|
||||||
|
"""Часы держит сам воркер.
|
||||||
|
|
||||||
|
В родителе видно только момент готовности, а он у всех задач, стартовавших
|
||||||
|
разом, почти один и тот же — по такому замеру не понять, какой бэг тяжёлый.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fn):
|
||||||
|
self.fn = fn
|
||||||
|
|
||||||
|
def __call__(self, task):
|
||||||
|
t0 = time.time()
|
||||||
|
return self.fn(task), time.time() - t0
|
||||||
|
|
||||||
|
|
||||||
|
def run(work, tasks, jobs: int):
|
||||||
|
"""Выполнить `work(task)` по всем задачам, отдавая `(i, task, res, с)`.
|
||||||
|
|
||||||
|
Отдаёт по мере готовности, поэтому `i` — исходный номер задачи, и по нему
|
||||||
|
вызывающий раскладывает результаты обратно в порядок бэгов.
|
||||||
|
|
||||||
|
При одном процессе всё считается прямо здесь, без пула: остаётся чем
|
||||||
|
отлаживать, и трассировка ошибки не проходит через межпроцессную передачу.
|
||||||
|
"""
|
||||||
|
tasks = list(tasks)
|
||||||
|
n = resolve(jobs, len(tasks))
|
||||||
|
if n <= 1:
|
||||||
|
for i, t in enumerate(tasks):
|
||||||
|
t0 = time.time()
|
||||||
|
yield i, t, work(t), time.time() - t0
|
||||||
|
return
|
||||||
|
|
||||||
|
limit_threads()
|
||||||
|
with ProcessPoolExecutor(max_workers=n) as ex:
|
||||||
|
fut = {ex.submit(_Timed(work), t): (i, t) for i, t in enumerate(tasks)}
|
||||||
|
for f in as_completed(fut):
|
||||||
|
i, t = fut[f]
|
||||||
|
res, secs = f.result()
|
||||||
|
yield i, t, res, secs
|
||||||
|
|
||||||
|
|
||||||
|
def add_argument(ap) -> None:
|
||||||
|
"""Один и тот же флаг во всех инструментах, чтобы не помнить разные."""
|
||||||
|
ap.add_argument("--jobs", type=int, default=0,
|
||||||
|
help="сколько бэгов считать разом; 0 — по числу бэгов, "
|
||||||
|
"но не больше физических ядер; 1 — в один процесс")
|
||||||
|
|
@ -1,246 +1,281 @@
|
||||||
"""Оценка обобщаемости: leave-one-bag-out.
|
"""Оценка обобщаемости: leave-one-bag-out.
|
||||||
|
|
||||||
Память тоннеля обучается на всех данных, **кроме** проверяемого бэга, и только
|
Память тоннеля обучается на всех данных, **кроме** проверяемого бэга, и только
|
||||||
после этого конвейер прогоняется по нему. Иначе цифры лгут: подавлять
|
после этого конвейер прогоняется по нему. Иначе цифры лгут: подавлять
|
||||||
конструкции, которые сам же и запомнил, умеет кто угодно, а на приватном тесте
|
конструкции, которые сам же и запомнил, умеет кто угодно, а на приватном тесте
|
||||||
будет новый участок тоннеля.
|
будет новый участок тоннеля.
|
||||||
|
|
||||||
Отчёт: ложные тревоги на километр пути и доля кадров с тревогой для пустых
|
Отчёт: ложные тревоги на километр пути и доля кадров с тревогой для пустых
|
||||||
бэгов; для `doubleT_obstacle` — ещё и доля кадров, в которых найден настоящий
|
бэгов; для `doubleT_obstacle` — ещё и доля кадров, в которых найден настоящий
|
||||||
объект на ~55 м.
|
объект на ~55 м.
|
||||||
|
|
||||||
python tools/evaluate.py --device cuda --out artifacts/generalisation.json
|
python tools/evaluate.py --device cuda --out artifacts/generalisation.json
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import time
|
import tempfile
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import _bootstrap as B # noqa: F401
|
import _bootstrap as B # noqa: F401
|
||||||
from flyguard.bag import Bag, find_bags
|
import _parallel as P
|
||||||
from flyguard.mushroom_body import MushroomBody, MushroomBodyConfig
|
from flyguard.bag import Bag, find_bags
|
||||||
from flyguard.pipeline import FlyGuard, Params
|
from flyguard.mushroom_body import MushroomBody, MushroomBodyConfig
|
||||||
|
from flyguard.pipeline import FlyGuard, Params
|
||||||
OBSTACLE_BAG = "doubleT_obstacle"
|
|
||||||
TRUE_D = (50.0, 62.0)
|
OBSTACLE_BAG = "doubleT_obstacle"
|
||||||
|
TRUE_D = (50.0, 62.0)
|
||||||
|
|
||||||
def train_excluding(per_bag: dict[str, np.ndarray], extra: np.ndarray | None,
|
|
||||||
exclude: str, target: float, device: str) -> MushroomBody:
|
def _work(task):
|
||||||
parts = [v for k, v in per_bag.items() if k not in (exclude, OBSTACLE_BAG)]
|
"""Одна задача — один бэг. Память и считывание грузятся по пути уже здесь."""
|
||||||
if extra is not None:
|
path, params, limit, mem_path, rd_path = task
|
||||||
parts.append(extra)
|
memory = MushroomBody.load(mem_path) if mem_path else None
|
||||||
X = np.concatenate(parts).astype(np.float32)
|
readout = None
|
||||||
mb = MushroomBody(MushroomBodyConfig())
|
if rd_path:
|
||||||
mb.fit_normalizer(X)
|
from flyguard.mbon_readout import MbonReadout
|
||||||
mb.learn(X, rate=mb.auto_rate(X.shape[0], target), device=device)
|
readout = MbonReadout.load(rd_path)
|
||||||
return mb
|
return run_bag(path, memory, limit, params, readout=readout)
|
||||||
|
|
||||||
|
|
||||||
def run_bag(bag_path, memory, limit: int, params: Params | None = None,
|
def train_excluding(per_bag: dict[str, np.ndarray], extra: np.ndarray | None,
|
||||||
readout=None) -> dict:
|
exclude: str, target: float, device: str) -> MushroomBody:
|
||||||
fg = FlyGuard(params or Params(), memory=memory, readout=readout)
|
parts = [v for k, v in per_bag.items() if k not in (exclude, OBSTACLE_BAG)]
|
||||||
bag = Bag(bag_path)
|
if extra is not None:
|
||||||
n = alarms = obj_hits = fp_objects = 0
|
parts.append(extra)
|
||||||
path_m = 0.0
|
X = np.concatenate(parts).astype(np.float32)
|
||||||
fp_dists, times = [], []
|
mb = MushroomBody(MushroomBodyConfig())
|
||||||
# один и тот же лоток, попавший в треки, виден сотню кадров подряд; для
|
mb.fit_normalizer(X)
|
||||||
# эксплуатации важно не это, а сколько РАЗНЫХ ложных объектов возникло —
|
mb.learn(X, rate=mb.auto_rate(X.shape[0], target), device=device)
|
||||||
# именно столько раз поезд затормозил бы напрасно
|
return mb
|
||||||
fp_tracks: set[int] = set()
|
|
||||||
for _, pc in bag.frames(stop=limit):
|
|
||||||
res = fg.process(pc)
|
def run_bag(bag_path, memory, limit: int, params: Params | None = None,
|
||||||
if res is None:
|
readout=None) -> dict:
|
||||||
continue
|
fg = FlyGuard(params or Params(), memory=memory, readout=readout)
|
||||||
n += 1
|
bag = Bag(bag_path)
|
||||||
path_m += res.ego.ds if res.ego else 0.0
|
n = alarms = obj_hits = fp_objects = 0
|
||||||
times.append(res.total_ms)
|
path_m = 0.0
|
||||||
d = res.decision
|
fp_dists, times = [], []
|
||||||
is_obstacle_bag = bag.path.name == OBSTACLE_BAG
|
# один и тот же лоток, попавший в треки, виден сотню кадров подряд; для
|
||||||
mine = [o for o in d.objects if TRUE_D[0] < o.distance < TRUE_D[1]] \
|
# эксплуатации важно не это, а сколько РАЗНЫХ ложных объектов возникло —
|
||||||
if is_obstacle_bag else []
|
# именно столько раз поезд затормозил бы напрасно
|
||||||
others = [o for o in d.objects if o not in mine]
|
fp_tracks: set[int] = set()
|
||||||
if mine:
|
for _, pc in bag.frames(stop=limit):
|
||||||
obj_hits += 1
|
res = fg.process(pc)
|
||||||
if others:
|
if res is None:
|
||||||
alarms += 1
|
continue
|
||||||
fp_objects += len(others)
|
n += 1
|
||||||
fp_tracks.update(o.track_id for o in others)
|
path_m += res.ego.ds if res.ego else 0.0
|
||||||
fp_dists.extend(o.distance for o in others)
|
times.append(res.total_ms)
|
||||||
km = max(path_m / 1000.0, 1e-6)
|
d = res.decision
|
||||||
return dict(bag=bag.path.name, frames=n, path_m=path_m,
|
is_obstacle_bag = bag.path.name == OBSTACLE_BAG
|
||||||
alarm_frames=alarms, alarm_rate=alarms / max(n, 1),
|
mine = [o for o in d.objects if TRUE_D[0] < o.distance < TRUE_D[1]] \
|
||||||
fp_objects=fp_objects, fp_tracks=len(fp_tracks),
|
if is_obstacle_bag else []
|
||||||
fp_per_km=(len(fp_tracks) / km) if path_m > 5 else float("nan"),
|
others = [o for o in d.objects if o not in mine]
|
||||||
fp_median_d=float(np.median(fp_dists)) if fp_dists else float("nan"),
|
if mine:
|
||||||
obj_rate=obj_hits / max(n, 1) if bag.path.name == OBSTACLE_BAG else None,
|
obj_hits += 1
|
||||||
ms_p50=float(np.median(times)) if times else 0.0,
|
if others:
|
||||||
ms_p95=float(np.percentile(times, 95)) if times else 0.0)
|
alarms += 1
|
||||||
|
fp_objects += len(others)
|
||||||
|
fp_tracks.update(o.track_id for o in others)
|
||||||
def main() -> None:
|
fp_dists.extend(o.distance for o in others)
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
km = max(path_m / 1000.0, 1e-6)
|
||||||
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
return dict(bag=bag.path.name, frames=n, path_m=path_m,
|
||||||
ap.add_argument("--cache", default=str(B.CACHE / "tune_candidates.npz"))
|
alarm_frames=alarms, alarm_rate=alarms / max(n, 1),
|
||||||
ap.add_argument("--extra-cache", default=str(B.CACHE / "new_data_candidates.npz"))
|
fp_objects=fp_objects, fp_tracks=len(fp_tracks),
|
||||||
ap.add_argument("--limit", type=int, default=250)
|
fp_per_km=(len(fp_tracks) / km) if path_m > 5 else float("nan"),
|
||||||
ap.add_argument("--target", type=float, default=0.4)
|
fp_median_d=float(np.median(fp_dists)) if fp_dists else float("nan"),
|
||||||
ap.add_argument("--device", default="cpu")
|
obj_rate=obj_hits / max(n, 1) if bag.path.name == OBSTACLE_BAG else None,
|
||||||
ap.add_argument("--split-adv", type=float, default=None,
|
ms_p50=float(np.median(times)) if times else 0.0,
|
||||||
help="порог разделения фигуры и фона; 0 — выключить")
|
ms_p95=float(np.percentile(times, 95)) if times else 0.0)
|
||||||
ap.add_argument("--split-gap", type=float, default=None,
|
|
||||||
help="порог разреза по контрасту ламины, м; 0 — выключить")
|
|
||||||
ap.add_argument("--split-near", type=float, default=None,
|
def main() -> None:
|
||||||
help="ближе этой дальности не резать, м")
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
ap.add_argument("--split-top", type=int, default=None,
|
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
||||||
help="сколько фигур выносить из одной компоненты; 0 — все")
|
ap.add_argument("--cache", default=str(B.CACHE / "tune_candidates.npz"))
|
||||||
ap.add_argument("--no-acc", action="store_true", help="выключить накопитель")
|
ap.add_argument("--extra-cache", default=str(B.CACHE / "new_data_candidates.npz"))
|
||||||
ap.add_argument("--no-hab", action="store_true", help="выключить привыкание")
|
ap.add_argument("--limit", type=int, default=250)
|
||||||
ap.add_argument("--mbon", default="", help="путь к обученному считыванию MBON")
|
ap.add_argument("--target", type=float, default=0.4)
|
||||||
ap.add_argument("--mbon-dir", default="",
|
ap.add_argument("--device", default="cpu")
|
||||||
help="каталог с моделями по складкам (mbon_<бэг>.npz): "
|
ap.add_argument("--split-adv", type=float, default=None,
|
||||||
"для каждого бэга берётся модель, его не видевшая")
|
help="порог разделения фигуры и фона; 0 — выключить")
|
||||||
ap.add_argument("--mbon-blend", type=float, default=None,
|
ap.add_argument("--split-gap", type=float, default=None,
|
||||||
help="1 — только модель, 0 — только ручная формула")
|
help="порог разреза по контрасту ламины, м; 0 — выключить")
|
||||||
ap.add_argument("--mbon-power", type=float, default=None,
|
ap.add_argument("--split-near", type=float, default=None,
|
||||||
help="резкость вероятности модели")
|
help="ближе этой дальности не резать, м")
|
||||||
ap.add_argument("--warn", type=float, default=None,
|
ap.add_argument("--split-top", type=int, default=None,
|
||||||
help="порог улики для тревоги (по умолчанию 0.5)")
|
help="сколько фигур выносить из одной компоненты; 0 — все")
|
||||||
ap.add_argument("--clear", type=float, default=None,
|
ap.add_argument("--no-acc", action="store_true", help="выключить накопитель")
|
||||||
help="нижний порог гистерезиса (по умолчанию 0.3)")
|
ap.add_argument("--no-hab", action="store_true", help="выключить привыкание")
|
||||||
ap.add_argument("--min-hits", type=int, default=None,
|
ap.add_argument("--mbon", default="", help="путь к обученному считыванию MBON")
|
||||||
help="наблюдений, без которых трек не считается")
|
ap.add_argument("--mbon-dir", default="",
|
||||||
ap.add_argument("--warn-far", type=float, default=None,
|
help="каталог с моделями по складкам (mbon_<бэг>.npz): "
|
||||||
help="порог тревоги на дальнем краю; ниже обычного — послабление далёким трекам")
|
"для каждого бэга берётся модель, его не видевшая")
|
||||||
ap.add_argument("--warn-far-from", type=float, default=None,
|
ap.add_argument("--mbon-blend", type=float, default=None,
|
||||||
help="с какой дальности порог начинает падать, м")
|
help="1 — только модель, 0 — только ручная формула")
|
||||||
ap.add_argument("--novelty-floor", type=float, default=None,
|
ap.add_argument("--mbon-power", type=float, default=None,
|
||||||
help="ниже этой новизны трек не считается")
|
help="резкость вероятности модели")
|
||||||
ap.add_argument("--near-long", type=float, default=None,
|
ap.add_argument("--warn", type=float, default=None,
|
||||||
help="разброс дальности, выше которого компоненту режут и вблизи; 0 — не резать вблизи")
|
help="порог улики для тревоги (по умолчанию 0.5)")
|
||||||
ap.add_argument("--min-rays", type=int, default=None,
|
ap.add_argument("--clear", type=float, default=None,
|
||||||
help="сколько лучей минимум образуют кандидата")
|
help="нижний порог гистерезиса (по умолчанию 0.3)")
|
||||||
ap.add_argument("--min-rays-far", type=int, default=None,
|
ap.add_argument("--min-hits", type=int, default=None,
|
||||||
help="порог по лучам за `--min-rays-far-from`; 0 — не различать")
|
help="наблюдений, без которых трек не считается")
|
||||||
ap.add_argument("--min-rays-far-from", type=float, default=None)
|
ap.add_argument("--warn-far", type=float, default=None,
|
||||||
ap.add_argument("--leak-far", type=float, default=None,
|
help="порог тревоги на дальнем краю; ниже обычного — послабление далёким трекам")
|
||||||
help="утечка улики на дальнем краю; ниже обычной — далёкий трек прощает промахи")
|
ap.add_argument("--warn-far-from", type=float, default=None,
|
||||||
ap.add_argument("--leak-far-from", type=float, default=None,
|
help="с какой дальности порог начинает падать, м")
|
||||||
help="с какой дальности утечка начинает падать, м")
|
ap.add_argument("--novelty-floor", type=float, default=None,
|
||||||
ap.add_argument("--nov-fade-from", type=float, default=None,
|
help="ниже этой новизны трек не считается")
|
||||||
help="с какой дальности гасить вклад знакомости; 0 — не гасить")
|
ap.add_argument("--near-long", type=float, default=None,
|
||||||
ap.add_argument("--nov-fade-to", type=float, default=None,
|
help="разброс дальности, выше которого компоненту режут и вблизи; 0 — не резать вблизи")
|
||||||
help="к какой дальности вклад знакомости обнуляется")
|
ap.add_argument("--min-rays", type=int, default=None,
|
||||||
ap.add_argument("--mbon-prior-from", type=float, default=None,
|
help="сколько лучей минимум образуют кандидата")
|
||||||
help="с какой дальности поправлять оценку модели на распространённость предметов; 0 — не поправлять")
|
ap.add_argument("--min-rays-far", type=int, default=None,
|
||||||
ap.add_argument("--no-memory", action="store_true",
|
help="порог по лучам за `--min-rays-far-from`; 0 — не различать")
|
||||||
help="совсем без памяти тоннеля — так выглядит первый проезд по новой линии")
|
ap.add_argument("--min-rays-far-from", type=float, default=None)
|
||||||
ap.add_argument("--out", default=str(B.ARTIFACTS / "generalisation.json"))
|
ap.add_argument("--leak-far", type=float, default=None,
|
||||||
args = ap.parse_args()
|
help="утечка улики на дальнем краю; ниже обычной — далёкий трек прощает промахи")
|
||||||
|
ap.add_argument("--leak-far-from", type=float, default=None,
|
||||||
d = np.load(args.cache, allow_pickle=True)
|
help="с какой дальности утечка начинает падать, м")
|
||||||
per_bag = {str(k): d[f"X_{k}"].astype(np.float32) for k in d["names"]}
|
ap.add_argument("--nov-fade-from", type=float, default=None,
|
||||||
extra = None
|
help="с какой дальности гасить вклад знакомости; 0 — не гасить")
|
||||||
from pathlib import Path
|
ap.add_argument("--nov-fade-to", type=float, default=None,
|
||||||
if Path(args.extra_cache).exists():
|
help="к какой дальности вклад знакомости обнуляется")
|
||||||
extra = np.load(args.extra_cache)["X"].astype(np.float32)
|
ap.add_argument("--mbon-prior-from", type=float, default=None,
|
||||||
print(f"дополнительно в обучение: {extra.shape[0]} кандидатов из new_data")
|
help="с какой дальности поправлять оценку модели на распространённость предметов; 0 — не поправлять")
|
||||||
|
ap.add_argument("--no-memory", action="store_true",
|
||||||
over = {}
|
help="совсем без памяти тоннеля — так выглядит первый проезд по новой линии")
|
||||||
if args.split_adv is not None:
|
ap.add_argument("--out", default=str(B.ARTIFACTS / "generalisation.json"))
|
||||||
over["split_adv"] = args.split_adv
|
P.add_argument(ap)
|
||||||
if args.split_gap is not None:
|
args = ap.parse_args()
|
||||||
over["split_gap"] = args.split_gap
|
|
||||||
if args.split_near is not None:
|
d = np.load(args.cache, allow_pickle=True)
|
||||||
over["split_near"] = args.split_near
|
per_bag = {str(k): d[f"X_{k}"].astype(np.float32) for k in d["names"]}
|
||||||
if args.split_top is not None:
|
extra = None
|
||||||
over["split_top"] = args.split_top
|
from pathlib import Path
|
||||||
if args.no_acc:
|
if Path(args.extra_cache).exists():
|
||||||
over["enable_accumulator"] = False
|
extra = np.load(args.extra_cache)["X"].astype(np.float32)
|
||||||
if args.no_hab:
|
print(f"дополнительно в обучение: {extra.shape[0]} кандидатов из new_data")
|
||||||
over["enable_habituation"] = False
|
|
||||||
if args.mbon_blend is not None:
|
over = {}
|
||||||
over["mbon_blend"] = args.mbon_blend
|
if args.split_adv is not None:
|
||||||
if args.mbon_power is not None:
|
over["split_adv"] = args.split_adv
|
||||||
over["mbon_power"] = args.mbon_power
|
if args.split_gap is not None:
|
||||||
if args.warn is not None:
|
over["split_gap"] = args.split_gap
|
||||||
over["warn_evidence"] = args.warn
|
if args.split_near is not None:
|
||||||
over["clear_evidence"] = args.warn * 0.6 if args.clear is None else args.clear
|
over["split_near"] = args.split_near
|
||||||
elif args.clear is not None:
|
if args.split_top is not None:
|
||||||
over["clear_evidence"] = args.clear
|
over["split_top"] = args.split_top
|
||||||
if args.min_hits is not None:
|
if args.no_acc:
|
||||||
over["min_hits"] = args.min_hits
|
over["enable_accumulator"] = False
|
||||||
if args.warn_far is not None:
|
if args.no_hab:
|
||||||
over["warn_far"] = args.warn_far
|
over["enable_habituation"] = False
|
||||||
if args.warn_far_from is not None:
|
if args.mbon_blend is not None:
|
||||||
over["warn_far_from"] = args.warn_far_from
|
over["mbon_blend"] = args.mbon_blend
|
||||||
if args.novelty_floor is not None:
|
if args.mbon_power is not None:
|
||||||
over["novelty_floor"] = args.novelty_floor
|
over["mbon_power"] = args.mbon_power
|
||||||
if args.near_long is not None:
|
if args.warn is not None:
|
||||||
over["near_long"] = args.near_long
|
over["warn_evidence"] = args.warn
|
||||||
if args.min_rays is not None:
|
over["clear_evidence"] = args.warn * 0.6 if args.clear is None else args.clear
|
||||||
over["min_rays"] = args.min_rays
|
elif args.clear is not None:
|
||||||
if args.min_rays_far is not None:
|
over["clear_evidence"] = args.clear
|
||||||
over["min_rays_far"] = args.min_rays_far
|
if args.min_hits is not None:
|
||||||
if args.min_rays_far_from is not None:
|
over["min_hits"] = args.min_hits
|
||||||
over["min_rays_far_from"] = args.min_rays_far_from
|
if args.warn_far is not None:
|
||||||
if args.leak_far is not None:
|
over["warn_far"] = args.warn_far
|
||||||
over["leak_far"] = args.leak_far
|
if args.warn_far_from is not None:
|
||||||
if args.leak_far_from is not None:
|
over["warn_far_from"] = args.warn_far_from
|
||||||
over["leak_far_from"] = args.leak_far_from
|
if args.novelty_floor is not None:
|
||||||
if args.nov_fade_from is not None:
|
over["novelty_floor"] = args.novelty_floor
|
||||||
over["nov_fade_from"] = args.nov_fade_from
|
if args.near_long is not None:
|
||||||
if args.nov_fade_to is not None:
|
over["near_long"] = args.near_long
|
||||||
over["nov_fade_to"] = args.nov_fade_to
|
if args.min_rays is not None:
|
||||||
if args.mbon_prior_from is not None:
|
over["min_rays"] = args.min_rays
|
||||||
over["mbon_prior_from"] = args.mbon_prior_from
|
if args.min_rays_far is not None:
|
||||||
params = Params(**over)
|
over["min_rays_far"] = args.min_rays_far
|
||||||
readout = None
|
if args.min_rays_far_from is not None:
|
||||||
folds = {}
|
over["min_rays_far_from"] = args.min_rays_far_from
|
||||||
if args.mbon_dir:
|
if args.leak_far is not None:
|
||||||
from pathlib import Path as _P
|
over["leak_far"] = args.leak_far
|
||||||
from flyguard.mbon_readout import MbonReadout
|
if args.leak_far_from is not None:
|
||||||
for f in _P(args.mbon_dir).glob("mbon_*.npz"):
|
over["leak_far_from"] = args.leak_far_from
|
||||||
folds[f.stem[len("mbon_"):]] = MbonReadout.load(f)
|
if args.nov_fade_from is not None:
|
||||||
print(f"считывание MBON по складкам: {args.mbon_dir} "
|
over["nov_fade_from"] = args.nov_fade_from
|
||||||
f"({len(folds)} моделей)")
|
if args.nov_fade_to is not None:
|
||||||
elif args.mbon:
|
over["nov_fade_to"] = args.nov_fade_to
|
||||||
from flyguard.mbon_readout import MbonReadout
|
if args.mbon_prior_from is not None:
|
||||||
readout = MbonReadout.load(args.mbon)
|
over["mbon_prior_from"] = args.mbon_prior_from
|
||||||
print(f"считывание MBON: {args.mbon}")
|
params = Params(**over)
|
||||||
rows = []
|
fold_paths: dict[str, str] = {}
|
||||||
for p in find_bags(args.root):
|
if args.mbon_dir:
|
||||||
t0 = time.time()
|
from pathlib import Path as _P
|
||||||
mem = (None if args.no_memory else
|
for f in _P(args.mbon_dir).glob("mbon_*.npz"):
|
||||||
train_excluding(per_bag, extra, p.name, args.target, args.device))
|
fold_paths[f.stem[len("mbon_"):]] = str(f)
|
||||||
rd = folds.get(p.name, readout) if folds else readout
|
print(f"считывание MBON по складкам: {args.mbon_dir} "
|
||||||
if folds and p.name not in folds and p.name != OBSTACLE_BAG:
|
f"({len(fold_paths)} моделей)")
|
||||||
print(f" внимание: для {p.name} нет своей складки")
|
elif args.mbon:
|
||||||
r = run_bag(p, mem, args.limit, params, readout=rd)
|
print(f"считывание MBON: {args.mbon}")
|
||||||
r["train_size"] = int(mem.n_seen) if mem is not None else 0
|
|
||||||
rows.append(r)
|
# Память обучается ЗДЕСЬ, а не в воркере: обучение идёт на видеокарте, и
|
||||||
obj = f"объект {r['obj_rate']:6.1%} | " if r["obj_rate"] is not None else ""
|
# делить одну карту на пять процессов незачем. Воркеру достаётся готовый
|
||||||
print(f"{r['bag']:40s} кадров {r['frames']:4d} путь {r['path_m']:6.0f} м | "
|
# файл — долгая часть это проход по записи, а не обучение.
|
||||||
f"{obj}тревог {r['alarm_rate']:6.1%} | ложных треков {r['fp_tracks']:3d} "
|
tmp = None if args.no_memory else tempfile.TemporaryDirectory(prefix="fg_mem_")
|
||||||
f"({r['fp_per_km']:6.1f} на км) | {r['ms_p50']:5.1f}/{r['ms_p95']:5.1f} мс | "
|
tasks, sizes = [], []
|
||||||
f"{time.time()-t0:5.0f} с", flush=True)
|
for p in find_bags(args.root):
|
||||||
|
mem_path, n_seen = "", 0
|
||||||
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
if tmp is not None:
|
||||||
with open(args.out, "w", encoding="utf-8") as f:
|
mem = train_excluding(per_bag, extra, p.name, args.target, args.device)
|
||||||
json.dump(rows, f, ensure_ascii=False, indent=1)
|
mem_path = str(Path(tmp.name) / f"mem_{p.name}.npz")
|
||||||
empty = [r for r in rows if r["bag"] != OBSTACLE_BAG]
|
mem.save(mem_path)
|
||||||
print("\nсводка по пустым бэгам:")
|
n_seen = int(mem.n_seen)
|
||||||
print(f" доля кадров с ложной тревогой: {np.mean([r['alarm_rate'] for r in empty]):.2%}")
|
del mem
|
||||||
fp_km = [r["fp_per_km"] for r in empty if np.isfinite(r["fp_per_km"])]
|
rd = fold_paths.get(p.name, args.mbon) if fold_paths else args.mbon
|
||||||
if fp_km:
|
if fold_paths and p.name not in fold_paths and p.name != OBSTACLE_BAG:
|
||||||
print(f" разных ложных треков на километр: {np.mean(fp_km):.1f} "
|
print(f" внимание: для {p.name} нет своей складки")
|
||||||
f"(медиана {np.median(fp_km):.1f})")
|
tasks.append((p, params, args.limit, mem_path, rd))
|
||||||
print(f" суммарный путь: {sum(r['path_m'] for r in empty):.0f} м")
|
sizes.append(n_seen)
|
||||||
print("сохранено:", args.out)
|
|
||||||
|
# Задержка кадра под пятью процессами вырастает с 32 до 56 мс — это
|
||||||
|
# свойство замера, а не конвейера, и такое число нельзя показывать как
|
||||||
if __name__ == "__main__":
|
# запас по бюджету. Поэтому в параллельном режиме оно не печатается
|
||||||
main()
|
# вовсе: перепутать nan с честным замером невозможно, а предупреждение
|
||||||
|
# в шапке пролистывается.
|
||||||
|
par = P.resolve(args.jobs, len(tasks)) > 1
|
||||||
|
if par:
|
||||||
|
print("параллельно: задержка кадра не измеряется, для неё нужен --jobs 1")
|
||||||
|
slots: list = [None] * len(tasks)
|
||||||
|
for i, task, r, secs in P.run(_work, tasks, args.jobs):
|
||||||
|
r["train_size"] = sizes[i]
|
||||||
|
if par:
|
||||||
|
r["ms_p50"] = r["ms_p95"] = float("nan")
|
||||||
|
slots[i] = r
|
||||||
|
obj = f"объект {r['obj_rate']:6.1%} | " if r["obj_rate"] is not None else ""
|
||||||
|
print(f"{r['bag']:40s} кадров {r['frames']:4d} путь {r['path_m']:6.0f} м | "
|
||||||
|
f"{obj}тревог {r['alarm_rate']:6.1%} | ложных треков {r['fp_tracks']:3d} "
|
||||||
|
f"({r['fp_per_km']:6.1f} на км) | {r['ms_p50']:5.1f}/{r['ms_p95']:5.1f} мс | "
|
||||||
|
f"{secs:5.0f} с", flush=True)
|
||||||
|
rows = [r for r in slots if r is not None]
|
||||||
|
if tmp is not None:
|
||||||
|
tmp.cleanup()
|
||||||
|
|
||||||
|
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(args.out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(rows, f, ensure_ascii=False, indent=1)
|
||||||
|
empty = [r for r in rows if r["bag"] != OBSTACLE_BAG]
|
||||||
|
print("\nсводка по пустым бэгам:")
|
||||||
|
print(f" доля кадров с ложной тревогой: {np.mean([r['alarm_rate'] for r in empty]):.2%}")
|
||||||
|
fp_km = [r["fp_per_km"] for r in empty if np.isfinite(r["fp_per_km"])]
|
||||||
|
if fp_km:
|
||||||
|
print(f" разных ложных треков на километр: {np.mean(fp_km):.1f} "
|
||||||
|
f"(медиана {np.median(fp_km):.1f})")
|
||||||
|
print(f" суммарный путь: {sum(r['path_m'] for r in empty):.0f} м")
|
||||||
|
print("сохранено:", args.out)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
|
||||||
|
|
@ -1,236 +1,253 @@
|
||||||
"""Размеченный полигон: сценарии сближения с синтетическим препятствием.
|
"""Размеченный полигон: сценарии сближения с синтетическим препятствием.
|
||||||
|
|
||||||
Для каждого бэга и каждого типа предмета строится сценарий: предмет ставится
|
Для каждого бэга и каждого типа предмета строится сценарий: предмет ставится
|
||||||
в фиксированную точку тоннеля далеко впереди, поезд к нему подъезжает, и на
|
в фиксированную точку тоннеля далеко впереди, поезд к нему подъезжает, и на
|
||||||
каждом кадре известна истинная дистанция. Отсюда получаются именно те цифры,
|
каждом кадре известна истинная дистанция. Отсюда получаются именно те цифры,
|
||||||
которые просит ТЗ: с какой дальности предмет уверенно виден, сколько ложных
|
которые просит ТЗ: с какой дальности предмет уверенно виден, сколько ложных
|
||||||
тревог и как это зависит от размера.
|
тревог и как это зависит от размера.
|
||||||
|
|
||||||
Первый проход считает собственное движение по чистым данным (это и есть
|
Первый проход считает собственное движение по чистым данным (это и есть
|
||||||
разметка по дистанции), второй — гоняет конвейер по кадрам со вставленным
|
разметка по дистанции), второй — гоняет конвейер по кадрам со вставленным
|
||||||
предметом. Все сценарии одного бэга обрабатываются в одном проходе по файлу:
|
предметом. Все сценарии одного бэга обрабатываются в одном проходе по файлу:
|
||||||
чтение данных дороже самой обработки.
|
чтение данных дороже самой обработки.
|
||||||
|
|
||||||
python tools/make_benchmark.py --out artifacts/benchmark.npz --memory artifacts/mushroom_body.npz
|
python tools/make_benchmark.py --out artifacts/benchmark.npz --memory artifacts/mushroom_body.npz
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
|
import numpy as np
|
||||||
import numpy as np
|
|
||||||
|
import _bootstrap as B # noqa: F401
|
||||||
import _bootstrap as B # noqa: F401
|
import _parallel as P
|
||||||
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
|
||||||
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
|
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
|
||||||
|
|
||||||
HOLDOUT = "doubleT_obstacle" # там уже есть настоящий объект
|
HOLDOUT = "doubleT_obstacle" # там уже есть настоящий объект
|
||||||
|
|
||||||
|
|
||||||
def ego_track(bag: Bag, params: Params, limit: int | None):
|
def _work(task):
|
||||||
"""Первый проход: пройденный путь на каждом кадре (разметка по дистанции)."""
|
"""Одна задача — один бэг.
|
||||||
fg = FlyGuard(params, memory=None)
|
|
||||||
s, stamps = [], []
|
Обученное грузится путями и уже внутри процесса: передавать модели через
|
||||||
total = 0.0
|
межпроцессную границу незачем, а свою складку каждый воркер берёт сам.
|
||||||
for _, pc in bag.frames(stop=limit):
|
"""
|
||||||
res = fg.process(pc)
|
path, params, limit, d_start, laterals, seed, mem_path, rd_path = task
|
||||||
if res is None:
|
memory = MushroomBody.load(mem_path) if mem_path else None
|
||||||
s.append(None); stamps.append(pc.stamp); continue
|
readout = None
|
||||||
total += res.ego.ds if res.ego else 0.0
|
if rd_path:
|
||||||
s.append(total); stamps.append(pc.stamp)
|
from flyguard.mbon_readout import MbonReadout
|
||||||
return s, stamps
|
readout = MbonReadout.load(rd_path)
|
||||||
|
return run_bag(path, params, memory, limit, d_start, laterals, seed,
|
||||||
|
readout=readout)
|
||||||
def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
|
||||||
laterals: tuple[float, ...], seed: int, readout=None) -> list[dict]:
|
|
||||||
bag = Bag(bag_path)
|
def ego_track(bag: Bag, params: Params, limit: int | None):
|
||||||
s_track, _ = ego_track(bag, params, limit)
|
"""Первый проход: пройденный путь на каждом кадре (разметка по дистанции)."""
|
||||||
have = [x for x in s_track if x is not None]
|
fg = FlyGuard(params, memory=None)
|
||||||
if len(have) < 20:
|
s, stamps = [], []
|
||||||
return []
|
total = 0.0
|
||||||
travel = have[-1] - have[0]
|
for _, pc in bag.frames(stop=limit):
|
||||||
|
res = fg.process(pc)
|
||||||
cat = catalogue()
|
if res is None:
|
||||||
scen = [(name, lat) for name in cat for lat in laterals]
|
s.append(None); stamps.append(pc.stamp); continue
|
||||||
pipes = [FlyGuard(params, memory=memory, readout=readout) for _ in scen]
|
total += res.ego.ds if res.ego else 0.0
|
||||||
rng = np.random.default_rng(seed)
|
s.append(total); stamps.append(pc.stamp)
|
||||||
records = [[] for _ in scen]
|
return s, stamps
|
||||||
|
|
||||||
# решётка и поза нужны для вставки — берутся из отдельного «чистого» конвейера
|
|
||||||
guide = FlyGuard(params, memory=None)
|
def run_bag(bag_path, params: Params, memory, limit: int, d_start: float,
|
||||||
|
laterals: tuple[float, ...], seed: int, readout=None) -> list[dict]:
|
||||||
for k, (_, pc) in enumerate(bag.frames(stop=limit)):
|
bag = Bag(bag_path)
|
||||||
gres = guide.process(pc)
|
s_track, _ = ego_track(bag, params, limit)
|
||||||
if gres is None or s_track[k] is None:
|
have = [x for x in s_track if x is not None]
|
||||||
continue
|
if len(have) < 20:
|
||||||
s_now = s_track[k] - have[0]
|
return []
|
||||||
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
travel = have[-1] - have[0]
|
||||||
for i, (name, lat) in enumerate(scen):
|
|
||||||
d_true = d_start - s_now
|
cat = catalogue()
|
||||||
if d_true < 6.0:
|
scen = [(name, lat) for name in cat for lat in laterals]
|
||||||
continue
|
pipes = [FlyGuard(params, memory=memory, readout=readout) for _ in scen]
|
||||||
# Предмет лежит НА ПУТИ, а путь в кривой уходит вбок: на 150 м при
|
rng = np.random.default_rng(seed)
|
||||||
# радиусе 1300 м это 8.6 м. Если ставить его в поперечных координатах
|
records = [[] for _ in scen]
|
||||||
# сенсора, он окажется в стене, а не в габарите.
|
|
||||||
u_obj = float(gres.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
# решётка и поза нужны для вставки — берутся из отдельного «чистого» конвейера
|
||||||
pc2, lab = inject(pc, guide.layout_full, gres.plane, cat[name],
|
guide = FlyGuard(params, memory=None)
|
||||||
Placement(d=d_true, u=u_obj), rng=rng, env=env)
|
|
||||||
res = pipes[i].process(pc2)
|
for k, (_, pc) in enumerate(bag.frames(stop=limit)):
|
||||||
if res is None:
|
gres = guide.process(pc)
|
||||||
continue
|
if gres is None or s_track[k] is None:
|
||||||
tol = max(3.0, 0.12 * d_true)
|
continue
|
||||||
hit = any(abs(o.distance - d_true) < tol for o in res.decision.objects)
|
s_now = s_track[k] - have[0]
|
||||||
fp = sum(1 for o in res.decision.objects if abs(o.distance - d_true) >= tol)
|
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
||||||
# Воронка потерь. Лучи в предмет попали — а дальше он может
|
for i, (name, lat) in enumerate(scen):
|
||||||
# пропасть на любой из трёх ступеней, и лечатся они по-разному:
|
d_true = d_start - s_now
|
||||||
# нет кандидата — вопрос к кластеризации и разделению фигуры и
|
if d_true < 6.0:
|
||||||
# фона, нет трека — к сопоставлению по кадрам, нет решения —
|
continue
|
||||||
# к порогу. Без этого разбиения улучшать нечего, кроме удачи.
|
# Предмет лежит НА ПУТИ, а путь в кривой уходит вбок: на 150 м при
|
||||||
cand = any(abs(c.d - d_true) < tol for c in res.candidates)
|
# радиусе 1300 м это 8.6 м. Если ставить его в поперечных координатах
|
||||||
cx = pipes[i].cx
|
# сенсора, он окажется в стене, а не в габарите.
|
||||||
trk = any(abs(tr.distance(cx.s_world) - d_true) < tol
|
u_obj = float(gres.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
||||||
for tr in cx.tracks)
|
pc2, lab = inject(pc, guide.layout_full, gres.plane, cat[name],
|
||||||
records[i].append((d_true, int(hit), fp, lab["hit_rays"],
|
Placement(d=d_true, u=u_obj), rng=rng, env=env)
|
||||||
int(cand), int(trk)))
|
res = pipes[i].process(pc2)
|
||||||
|
if res is None:
|
||||||
out = []
|
continue
|
||||||
for (name, lat), rec in zip(scen, records):
|
tol = max(3.0, 0.12 * d_true)
|
||||||
if not rec:
|
hit = any(abs(o.distance - d_true) < tol for o in res.decision.objects)
|
||||||
continue
|
fp = sum(1 for o in res.decision.objects if abs(o.distance - d_true) >= tol)
|
||||||
a = np.array(rec, np.float32)
|
# Воронка потерь. Лучи в предмет попали — а дальше он может
|
||||||
out.append(dict(bag=bag.path.name, obj=name, lateral=lat,
|
# пропасть на любой из трёх ступеней, и лечатся они по-разному:
|
||||||
d=a[:, 0].tolist(), hit=a[:, 1].tolist(),
|
# нет кандидата — вопрос к кластеризации и разделению фигуры и
|
||||||
fp=a[:, 2].tolist(), rays=a[:, 3].tolist(),
|
# фона, нет трека — к сопоставлению по кадрам, нет решения —
|
||||||
cand=a[:, 4].tolist(), trk=a[:, 5].tolist(),
|
# к порогу. Без этого разбиения улучшать нечего, кроме удачи.
|
||||||
travel=float(travel)))
|
cand = any(abs(c.d - d_true) < tol for c in res.candidates)
|
||||||
return out
|
cx = pipes[i].cx
|
||||||
|
trk = any(abs(tr.distance(cx.s_world) - d_true) < tol
|
||||||
|
for tr in cx.tracks)
|
||||||
def main() -> None:
|
records[i].append((d_true, int(hit), fp, lab["hit_rays"],
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
int(cand), int(trk)))
|
||||||
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
|
||||||
ap.add_argument("--memory")
|
out = []
|
||||||
ap.add_argument("--mbon", default="",
|
for (name, lat), rec in zip(scen, records):
|
||||||
help="обученное считывание MBON одной моделью; она видела "
|
if not rec:
|
||||||
"эти бэги — годится только для отладки")
|
continue
|
||||||
ap.add_argument("--mbon-dir", default="",
|
a = np.array(rec, np.float32)
|
||||||
help="каталог с моделями по складкам (mbon_<бэг>.npz): для "
|
out.append(dict(bag=bag.path.name, obj=name, lateral=lat,
|
||||||
"каждого бэга берётся модель, его НЕ видевшая. Иначе "
|
d=a[:, 0].tolist(), hit=a[:, 1].tolist(),
|
||||||
"дальность завышена: считывание обучалось ровно на "
|
fp=a[:, 2].tolist(), rays=a[:, 3].tolist(),
|
||||||
"таких же вставках в этот же тоннель")
|
cand=a[:, 4].tolist(), trk=a[:, 5].tolist(),
|
||||||
ap.add_argument("--out", default=str(B.ARTIFACTS / "benchmark.json"))
|
travel=float(travel)))
|
||||||
ap.add_argument("--limit", type=int, default=250)
|
return out
|
||||||
ap.add_argument("--d-start", type=float, default=200.0)
|
|
||||||
ap.add_argument("--laterals", default="0.0,0.9")
|
|
||||||
ap.add_argument("--seed", type=int, default=12345)
|
def main() -> None:
|
||||||
ap.add_argument("--mbon-blend", type=float, default=None,
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
help="1 — только модель, 0 — только ручная формула")
|
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
||||||
ap.add_argument("--warn", type=float, default=None,
|
ap.add_argument("--memory")
|
||||||
help="порог улики для тревоги (по умолчанию 0.5)")
|
ap.add_argument("--mbon", default="",
|
||||||
ap.add_argument("--clear", type=float, default=None)
|
help="обученное считывание MBON одной моделью; она видела "
|
||||||
ap.add_argument("--min-hits", type=int, default=None)
|
"эти бэги — годится только для отладки")
|
||||||
ap.add_argument("--warn-far", type=float, default=None,
|
ap.add_argument("--mbon-dir", default="",
|
||||||
help="порог тревоги на дальнем краю; ниже обычного — послабление далёким трекам")
|
help="каталог с моделями по складкам (mbon_<бэг>.npz): для "
|
||||||
ap.add_argument("--warn-far-from", type=float, default=None,
|
"каждого бэга берётся модель, его НЕ видевшая. Иначе "
|
||||||
help="с какой дальности порог начинает падать, м")
|
"дальность завышена: считывание обучалось ровно на "
|
||||||
ap.add_argument("--novelty-floor", type=float, default=None,
|
"таких же вставках в этот же тоннель")
|
||||||
help="ниже этой новизны трек не считается")
|
ap.add_argument("--out", default=str(B.ARTIFACTS / "benchmark.json"))
|
||||||
ap.add_argument("--near-long", type=float, default=None,
|
ap.add_argument("--limit", type=int, default=250)
|
||||||
help="разброс дальности, выше которого компоненту режут и вблизи; 0 — не резать вблизи")
|
ap.add_argument("--d-start", type=float, default=200.0)
|
||||||
ap.add_argument("--min-rays", type=int, default=None,
|
ap.add_argument("--laterals", default="0.0,0.9")
|
||||||
help="сколько лучей минимум образуют кандидата")
|
ap.add_argument("--seed", type=int, default=12345)
|
||||||
ap.add_argument("--min-rays-far", type=int, default=None,
|
ap.add_argument("--mbon-blend", type=float, default=None,
|
||||||
help="порог по лучам за `--min-rays-far-from`; 0 — не различать")
|
help="1 — только модель, 0 — только ручная формула")
|
||||||
ap.add_argument("--min-rays-far-from", type=float, default=None)
|
ap.add_argument("--warn", type=float, default=None,
|
||||||
ap.add_argument("--leak-far", type=float, default=None,
|
help="порог улики для тревоги (по умолчанию 0.5)")
|
||||||
help="утечка улики на дальнем краю; ниже обычной — далёкий трек прощает промахи")
|
ap.add_argument("--clear", type=float, default=None)
|
||||||
ap.add_argument("--leak-far-from", type=float, default=None,
|
ap.add_argument("--min-hits", type=int, default=None)
|
||||||
help="с какой дальности утечка начинает падать, м")
|
ap.add_argument("--warn-far", type=float, default=None,
|
||||||
ap.add_argument("--nov-fade-from", type=float, default=None,
|
help="порог тревоги на дальнем краю; ниже обычного — послабление далёким трекам")
|
||||||
help="с какой дальности гасить вклад знакомости; 0 — не гасить")
|
ap.add_argument("--warn-far-from", type=float, default=None,
|
||||||
ap.add_argument("--nov-fade-to", type=float, default=None,
|
help="с какой дальности порог начинает падать, м")
|
||||||
help="к какой дальности вклад знакомости обнуляется")
|
ap.add_argument("--novelty-floor", type=float, default=None,
|
||||||
ap.add_argument("--mbon-prior-from", type=float, default=None,
|
help="ниже этой новизны трек не считается")
|
||||||
help="с какой дальности поправлять оценку модели на распространённость предметов; 0 — не поправлять")
|
ap.add_argument("--near-long", type=float, default=None,
|
||||||
args = ap.parse_args()
|
help="разброс дальности, выше которого компоненту режут и вблизи; 0 — не резать вблизи")
|
||||||
|
ap.add_argument("--min-rays", type=int, default=None,
|
||||||
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
help="сколько лучей минимум образуют кандидата")
|
||||||
memory = MushroomBody.load(args.memory) if args.memory else None
|
ap.add_argument("--min-rays-far", type=int, default=None,
|
||||||
readout = None
|
help="порог по лучам за `--min-rays-far-from`; 0 — не различать")
|
||||||
folds: dict = {}
|
ap.add_argument("--min-rays-far-from", type=float, default=None)
|
||||||
if args.mbon_dir:
|
ap.add_argument("--leak-far", type=float, default=None,
|
||||||
from pathlib import Path as _P
|
help="утечка улики на дальнем краю; ниже обычной — далёкий трек прощает промахи")
|
||||||
from flyguard.mbon_readout import MbonReadout
|
ap.add_argument("--leak-far-from", type=float, default=None,
|
||||||
for f in _P(args.mbon_dir).glob("mbon_*.npz"):
|
help="с какой дальности утечка начинает падать, м")
|
||||||
folds[f.stem[len("mbon_"):]] = MbonReadout.load(f)
|
ap.add_argument("--nov-fade-from", type=float, default=None,
|
||||||
print(f"считывание MBON по складкам: {args.mbon_dir} "
|
help="с какой дальности гасить вклад знакомости; 0 — не гасить")
|
||||||
f"({len(folds)} моделей)")
|
ap.add_argument("--nov-fade-to", type=float, default=None,
|
||||||
elif args.mbon:
|
help="к какой дальности вклад знакомости обнуляется")
|
||||||
from flyguard.mbon_readout import MbonReadout
|
ap.add_argument("--mbon-prior-from", type=float, default=None,
|
||||||
readout = MbonReadout.load(args.mbon)
|
help="с какой дальности поправлять оценку модели на распространённость предметов; 0 — не поправлять")
|
||||||
print(f"считывание MBON: {args.mbon}")
|
P.add_argument(ap)
|
||||||
over = {}
|
args = ap.parse_args()
|
||||||
if args.mbon_blend is not None:
|
|
||||||
over["mbon_blend"] = args.mbon_blend
|
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||||
if args.warn is not None:
|
fold_paths: dict[str, str] = {}
|
||||||
over["warn_evidence"] = args.warn
|
if args.mbon_dir:
|
||||||
over["clear_evidence"] = args.warn * 0.6 if args.clear is None else args.clear
|
from pathlib import Path as _P
|
||||||
elif args.clear is not None:
|
for f in _P(args.mbon_dir).glob("mbon_*.npz"):
|
||||||
over["clear_evidence"] = args.clear
|
fold_paths[f.stem[len("mbon_"):]] = str(f)
|
||||||
if args.min_hits is not None:
|
print(f"считывание MBON по складкам: {args.mbon_dir} "
|
||||||
over["min_hits"] = args.min_hits
|
f"({len(fold_paths)} моделей)")
|
||||||
if args.warn_far is not None:
|
elif args.mbon:
|
||||||
over["warn_far"] = args.warn_far
|
print(f"считывание MBON: {args.mbon}")
|
||||||
if args.warn_far_from is not None:
|
over = {}
|
||||||
over["warn_far_from"] = args.warn_far_from
|
if args.mbon_blend is not None:
|
||||||
if args.novelty_floor is not None:
|
over["mbon_blend"] = args.mbon_blend
|
||||||
over["novelty_floor"] = args.novelty_floor
|
if args.warn is not None:
|
||||||
if args.near_long is not None:
|
over["warn_evidence"] = args.warn
|
||||||
over["near_long"] = args.near_long
|
over["clear_evidence"] = args.warn * 0.6 if args.clear is None else args.clear
|
||||||
if args.min_rays is not None:
|
elif args.clear is not None:
|
||||||
over["min_rays"] = args.min_rays
|
over["clear_evidence"] = args.clear
|
||||||
if args.min_rays_far is not None:
|
if args.min_hits is not None:
|
||||||
over["min_rays_far"] = args.min_rays_far
|
over["min_hits"] = args.min_hits
|
||||||
if args.min_rays_far_from is not None:
|
if args.warn_far is not None:
|
||||||
over["min_rays_far_from"] = args.min_rays_far_from
|
over["warn_far"] = args.warn_far
|
||||||
if args.leak_far is not None:
|
if args.warn_far_from is not None:
|
||||||
over["leak_far"] = args.leak_far
|
over["warn_far_from"] = args.warn_far_from
|
||||||
if args.leak_far_from is not None:
|
if args.novelty_floor is not None:
|
||||||
over["leak_far_from"] = args.leak_far_from
|
over["novelty_floor"] = args.novelty_floor
|
||||||
if args.nov_fade_from is not None:
|
if args.near_long is not None:
|
||||||
over["nov_fade_from"] = args.nov_fade_from
|
over["near_long"] = args.near_long
|
||||||
if args.nov_fade_to is not None:
|
if args.min_rays is not None:
|
||||||
over["nov_fade_to"] = args.nov_fade_to
|
over["min_rays"] = args.min_rays
|
||||||
if args.mbon_prior_from is not None:
|
if args.min_rays_far is not None:
|
||||||
over["mbon_prior_from"] = args.mbon_prior_from
|
over["min_rays_far"] = args.min_rays_far
|
||||||
params = Params(**over)
|
if args.min_rays_far_from is not None:
|
||||||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
over["min_rays_far_from"] = args.min_rays_far_from
|
||||||
|
if args.leak_far is not None:
|
||||||
all_rec = []
|
over["leak_far"] = args.leak_far
|
||||||
for p in find_bags(args.root):
|
if args.leak_far_from is not None:
|
||||||
if p.name == HOLDOUT:
|
over["leak_far_from"] = args.leak_far_from
|
||||||
continue
|
if args.nov_fade_from is not None:
|
||||||
t0 = time.time()
|
over["nov_fade_from"] = args.nov_fade_from
|
||||||
rd = readout
|
if args.nov_fade_to is not None:
|
||||||
if folds:
|
over["nov_fade_to"] = args.nov_fade_to
|
||||||
rd = folds.get(p.name)
|
if args.mbon_prior_from is not None:
|
||||||
if rd is None:
|
over["mbon_prior_from"] = args.mbon_prior_from
|
||||||
print(f" внимание: для {p.name} нет своей складки — пропуск")
|
params = Params(**over)
|
||||||
continue
|
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||||||
rec = run_bag(p, params, memory, args.limit, args.d_start, laterals,
|
|
||||||
args.seed, readout=rd)
|
tasks = []
|
||||||
all_rec.extend(rec)
|
for p in find_bags(args.root):
|
||||||
n = sum(len(r["d"]) for r in rec)
|
if p.name == HOLDOUT:
|
||||||
print(f" {p.name:42s} сценариев {len(rec):3d}, наблюдений {n:6d}, "
|
continue
|
||||||
f"{time.time()-t0:6.1f} с", flush=True)
|
rd = args.mbon
|
||||||
|
if fold_paths:
|
||||||
with open(args.out, "w", encoding="utf-8") as f:
|
rd = fold_paths.get(p.name, "")
|
||||||
json.dump(all_rec, f, ensure_ascii=False)
|
if not rd:
|
||||||
print(f"сохранено: {args.out} ({len(all_rec)} сценариев)")
|
print(f" внимание: для {p.name} нет своей складки — пропуск")
|
||||||
|
continue
|
||||||
|
tasks.append((p, params, args.limit, args.d_start, laterals, args.seed,
|
||||||
if __name__ == "__main__":
|
args.memory or "", rd))
|
||||||
main()
|
|
||||||
|
# Печатается по готовности, собирается по номеру задачи: порядок сценариев
|
||||||
|
# в файле не должен зависеть от того, какой бэг досчитался первым.
|
||||||
|
slots: list = [None] * len(tasks)
|
||||||
|
for i, task, rec, secs in P.run(_work, tasks, args.jobs):
|
||||||
|
slots[i] = rec
|
||||||
|
n = sum(len(r["d"]) for r in rec)
|
||||||
|
print(f" {task[0].name:42s} сценариев {len(rec):3d}, наблюдений {n:6d}, "
|
||||||
|
f"{secs:6.1f} с", flush=True)
|
||||||
|
all_rec = [r for rec in slots if rec for r in rec]
|
||||||
|
|
||||||
|
with open(args.out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(all_rec, f, ensure_ascii=False)
|
||||||
|
print(f"сохранено: {args.out} ({len(all_rec)} сценариев)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
|
||||||
|
|
@ -1,177 +1,186 @@
|
||||||
"""Размеченная выборка кандидатов: предмет против тоннельной обстановки.
|
"""Размеченная выборка кандидатов: предмет против тоннельной обстановки.
|
||||||
|
|
||||||
Разметки в датасете нет, и до сих пор это определяло архитектуру: грибовидное
|
Разметки в датасете нет, и до сих пор это определяло архитектуру: грибовидное
|
||||||
тело учится **без меток**, запоминая частоту обстановки. Но у нас есть
|
тело учится **без меток**, запоминая частоту обстановки. Но у нас есть
|
||||||
физически обоснованный генератор предметов (`flyguard.synth`), сверенный с
|
физически обоснованный генератор предметов (`flyguard.synth`), сверенный с
|
||||||
единственным реальным объектом: настоящий 0.67 × 1.35 м на 55 м даёт 47–69
|
единственным реальным объектом: настоящий 0.67 × 1.35 м на 55 м даёт 47–69
|
||||||
лучей, синтетический человек 0.44 × 1.71 м на 60 м — 50. Значит, метки можно
|
лучей, синтетический человек 0.44 × 1.71 м на 60 м — 50. Значит, метки можно
|
||||||
изготовить, и изготовить достоверно.
|
изготовить, и изготовить достоверно.
|
||||||
|
|
||||||
Каждый кандидат помечается по **пересечению лучей**, а не «по дальности
|
Каждый кандидат помечается по **пересечению лучей**, а не «по дальности
|
||||||
примерно»: `inject` возвращает индексы лучей, в которые предмет действительно
|
примерно»: `inject` возвращает индексы лучей, в которые предмет действительно
|
||||||
записан, и кандидат считается предметом, если его ядро состоит из этих лучей.
|
записан, и кандидат считается предметом, если его ядро состоит из этих лучей.
|
||||||
Так структура тоннеля, случайно оказавшаяся на той же дальности, в
|
Так структура тоннеля, случайно оказавшаяся на той же дальности, в
|
||||||
положительные не попадает.
|
положительные не попадает.
|
||||||
|
|
||||||
Сценарии намеренно ставят предмет в РАЗНЫЕ точки тоннеля (`--d-starts`): замер
|
Сценарии намеренно ставят предмет в РАЗНЫЕ точки тоннеля (`--d-starts`): замер
|
||||||
показал, что одна и та же дальность в разных местах перегона ведёт себя
|
показал, что одна и та же дальность в разных местах перегона ведёт себя
|
||||||
совершенно по-разному — где-то предмет виден целиком, где-то за поворотом
|
совершенно по-разному — где-то предмет виден целиком, где-то за поворотом
|
||||||
(EXPERIMENTS п. 9.6). Обучаться на одной точке постановки значит выучить эту
|
(EXPERIMENTS п. 9.6). Обучаться на одной точке постановки значит выучить эту
|
||||||
точку.
|
точку.
|
||||||
|
|
||||||
`doubleT_obstacle` исключён целиком: там настоящий объект, и он остаётся
|
`doubleT_obstacle` исключён целиком: там настоящий объект, и он остаётся
|
||||||
независимой проверкой того, что модель выучила предмет, а не «синтетику».
|
независимой проверкой того, что модель выучила предмет, а не «синтетику».
|
||||||
|
|
||||||
python tools/make_training_set.py --out data/cache/training_set.npz
|
python tools/make_training_set.py --out data/cache/training_set.npz
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import time
|
|
||||||
|
import numpy as np
|
||||||
import numpy as np
|
|
||||||
|
import _bootstrap as B # noqa: F401
|
||||||
import _bootstrap as B # noqa: F401
|
import _parallel as P
|
||||||
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
|
||||||
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
|
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
|
||||||
|
|
||||||
HOLDOUT = "doubleT_obstacle" # там реальный объект — только для проверки
|
HOLDOUT = "doubleT_obstacle" # там реальный объект — только для проверки
|
||||||
MIN_OVERLAP = 0.5 # доля лучей ядра, пришедших от предмета
|
MIN_OVERLAP = 0.5 # доля лучей ядра, пришедших от предмета
|
||||||
|
|
||||||
|
|
||||||
def ego_track(bag: Bag, params: Params, limit: int):
|
def _work(task):
|
||||||
"""Первый проход: пройденный путь на каждом кадре и общая решётка."""
|
"""Одна задача — один бэг. Верхнего уровня: иначе не передать в процесс."""
|
||||||
fg = FlyGuard(params, memory=None)
|
path, params, limit, d_starts, laterals, seed = task
|
||||||
s, total = [], 0.0
|
return collect_bag(path, params, limit, d_starts, laterals, seed)
|
||||||
for _, pc in bag.frames(stop=limit):
|
|
||||||
res = fg.process(pc)
|
|
||||||
if res is None:
|
def ego_track(bag: Bag, params: Params, limit: int):
|
||||||
s.append(None)
|
"""Первый проход: пройденный путь на каждом кадре и общая решётка."""
|
||||||
continue
|
fg = FlyGuard(params, memory=None)
|
||||||
total += res.ego.ds if res.ego else 0.0
|
s, total = [], 0.0
|
||||||
s.append(total)
|
for _, pc in bag.frames(stop=limit):
|
||||||
return s, fg
|
res = fg.process(pc)
|
||||||
|
if res is None:
|
||||||
|
s.append(None)
|
||||||
def collect_bag(path, params: Params, limit: int, d_starts, laterals, seed: int):
|
continue
|
||||||
bag = Bag(path)
|
total += res.ego.ds if res.ego else 0.0
|
||||||
s_track, ego_fg = ego_track(bag, params, limit)
|
s.append(total)
|
||||||
have = [x for x in s_track if x is not None]
|
return s, fg
|
||||||
if len(have) < 30:
|
|
||||||
return None
|
|
||||||
s0 = have[0]
|
def collect_bag(path, params: Params, limit: int, d_starts, laterals, seed: int):
|
||||||
layout = ego_fg.layout_full
|
bag = Bag(path)
|
||||||
col0 = ego_fg.cols.start
|
s_track, ego_fg = ego_track(bag, params, limit)
|
||||||
|
have = [x for x in s_track if x is not None]
|
||||||
cat = catalogue()
|
if len(have) < 30:
|
||||||
scen = [(n, lat, d0) for n in cat for lat in laterals for d0 in d_starts]
|
return None
|
||||||
guide = FlyGuard(params, memory=None, layout=layout)
|
s0 = have[0]
|
||||||
pipes = [FlyGuard(params, memory=None, layout=layout) for _ in scen]
|
layout = ego_fg.layout_full
|
||||||
rng = np.random.default_rng(seed)
|
col0 = ego_fg.cols.start
|
||||||
|
|
||||||
X, y, dd, obj, lat_out = [], [], [], [], []
|
cat = catalogue()
|
||||||
for k, (_, pc) in enumerate(bag.frames(stop=limit)):
|
scen = [(n, lat, d0) for n in cat for lat in laterals for d0 in d_starts]
|
||||||
g = guide.process(pc)
|
guide = FlyGuard(params, memory=None, layout=layout)
|
||||||
if g is None or s_track[k] is None:
|
pipes = [FlyGuard(params, memory=None, layout=layout) for _ in scen]
|
||||||
continue
|
rng = np.random.default_rng(seed)
|
||||||
s_now = s_track[k] - s0
|
|
||||||
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
X, y, dd, obj, lat_out = [], [], [], [], []
|
||||||
for i, (name, lat, d0) in enumerate(scen):
|
for k, (_, pc) in enumerate(bag.frames(stop=limit)):
|
||||||
d_true = d0 - s_now
|
g = guide.process(pc)
|
||||||
if d_true < 6.0:
|
if g is None or s_track[k] is None:
|
||||||
continue
|
continue
|
||||||
u = float(g.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
s_now = s_track[k] - s0
|
||||||
pc2, lab = inject(pc, layout, g.plane, cat[name],
|
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
||||||
Placement(d=d_true, u=u), rng=rng, env=env)
|
for i, (name, lat, d0) in enumerate(scen):
|
||||||
res = pipes[i].process(pc2)
|
d_true = d0 - s_now
|
||||||
if res is None or not res.candidates:
|
if d_true < 6.0:
|
||||||
continue
|
continue
|
||||||
rr = lab.get("rays")
|
u = float(g.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
||||||
if rr is None or lab["hit_rays"] == 0:
|
pc2, lab = inject(pc, layout, g.plane, cat[name],
|
||||||
truth = None
|
Placement(d=d_true, u=u), rng=rng, env=env)
|
||||||
else:
|
res = pipes[i].process(pc2)
|
||||||
# лучи предмета в координатах полной решётки → плоский индекс;
|
if res is None or not res.candidates:
|
||||||
# отсортированный массив, а не множество: проверка идёт
|
continue
|
||||||
# сотни тысяч раз, и `in` по множеству тут заметно дороже
|
rr = lab.get("rays")
|
||||||
truth = np.sort((rr[0].astype(np.int64) << 20)
|
if rr is None or lab["hit_rays"] == 0:
|
||||||
| rr[1].astype(np.int64))
|
truth = None
|
||||||
for c in res.candidates:
|
else:
|
||||||
ii, jj = c.extra.get("rays", (None, None))
|
# лучи предмета в координатах полной решётки → плоский индекс;
|
||||||
if ii is None:
|
# отсортированный массив, а не множество: проверка идёт
|
||||||
continue
|
# сотни тысяч раз, и `in` по множеству тут заметно дороже
|
||||||
lbl = 0
|
truth = np.sort((rr[0].astype(np.int64) << 20)
|
||||||
if truth is not None and truth.size:
|
| rr[1].astype(np.int64))
|
||||||
key = ((ii.astype(np.int64) << 20)
|
for c in res.candidates:
|
||||||
| (jj.astype(np.int64) + col0))
|
ii, jj = c.extra.get("rays", (None, None))
|
||||||
pos = np.searchsorted(truth, key)
|
if ii is None:
|
||||||
np.clip(pos, 0, truth.size - 1, out=pos)
|
continue
|
||||||
frac = float((truth[pos] == key).mean())
|
lbl = 0
|
||||||
lbl = int(frac >= MIN_OVERLAP)
|
if truth is not None and truth.size:
|
||||||
v = describe(c)
|
key = ((ii.astype(np.int64) << 20)
|
||||||
acc = float(c.extra.get("acc_support", 0.0)) if c.extra else 0.0
|
| (jj.astype(np.int64) + col0))
|
||||||
X.append(np.append(v, acc).astype(np.float32))
|
pos = np.searchsorted(truth, key)
|
||||||
y.append(lbl)
|
np.clip(pos, 0, truth.size - 1, out=pos)
|
||||||
dd.append(c.d)
|
frac = float((truth[pos] == key).mean())
|
||||||
obj.append(name if lbl else "")
|
lbl = int(frac >= MIN_OVERLAP)
|
||||||
lat_out.append(lat)
|
v = describe(c)
|
||||||
return (np.asarray(X, np.float32), np.asarray(y, np.int8),
|
acc = float(c.extra.get("acc_support", 0.0)) if c.extra else 0.0
|
||||||
np.asarray(dd, np.float32), np.asarray(obj),
|
X.append(np.append(v, acc).astype(np.float32))
|
||||||
np.asarray(lat_out, np.float32))
|
y.append(lbl)
|
||||||
|
dd.append(c.d)
|
||||||
|
obj.append(name if lbl else "")
|
||||||
def main() -> None:
|
lat_out.append(lat)
|
||||||
ap = argparse.ArgumentParser(description=__doc__)
|
return (np.asarray(X, np.float32), np.asarray(y, np.int8),
|
||||||
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
np.asarray(dd, np.float32), np.asarray(obj),
|
||||||
ap.add_argument("--out", default=str(B.CACHE / "training_set.npz"))
|
np.asarray(lat_out, np.float32))
|
||||||
ap.add_argument("--limit", type=int, default=250)
|
|
||||||
ap.add_argument("--d-starts", default="80,140,200")
|
|
||||||
# Поперечные положения намеренно кроют ВЕСЬ габарит, а не только ось.
|
def main() -> None:
|
||||||
# Иначе выборка вырождается: предметы у оси, обстановка у стен, и модель
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
# выучивает «всё, что у оси — предмет» вместо признаков предмета. Проверено
|
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
||||||
# на пробном прогоне с одной постановкой: AUC 1.000 по всем бэгам — цифра
|
ap.add_argument("--out", default=str(B.CACHE / "training_set.npz"))
|
||||||
# красивая и бессмысленная, а в тоннеле у оси полно штатных конструкций.
|
ap.add_argument("--limit", type=int, default=250)
|
||||||
ap.add_argument("--laterals", default="0,-0.6,0.6,-1.2,1.2")
|
ap.add_argument("--d-starts", default="80,140,200")
|
||||||
ap.add_argument("--seed", type=int, default=20260921)
|
# Поперечные положения намеренно кроют ВЕСЬ габарит, а не только ось.
|
||||||
args = ap.parse_args()
|
# Иначе выборка вырождается: предметы у оси, обстановка у стен, и модель
|
||||||
|
# выучивает «всё, что у оси — предмет» вместо признаков предмета. Проверено
|
||||||
d_starts = tuple(float(x) for x in args.d_starts.split(","))
|
# на пробном прогоне с одной постановкой: AUC 1.000 по всем бэгам — цифра
|
||||||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
# красивая и бессмысленная, а в тоннеле у оси полно штатных конструкций.
|
||||||
params = Params()
|
ap.add_argument("--laterals", default="0,-0.6,0.6,-1.2,1.2")
|
||||||
B.CACHE.mkdir(parents=True, exist_ok=True)
|
ap.add_argument("--seed", type=int, default=20260921)
|
||||||
|
P.add_argument(ap)
|
||||||
parts = {}
|
args = ap.parse_args()
|
||||||
for p in find_bags(args.root):
|
|
||||||
if p.name == HOLDOUT:
|
d_starts = tuple(float(x) for x in args.d_starts.split(","))
|
||||||
continue
|
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||||||
t0 = time.time()
|
params = Params()
|
||||||
got = collect_bag(p, params, args.limit, d_starts, laterals, args.seed)
|
B.CACHE.mkdir(parents=True, exist_ok=True)
|
||||||
if got is None:
|
|
||||||
print(f" {p.name:42s} пропущен")
|
bags = [p for p in find_bags(args.root) if p.name != HOLDOUT]
|
||||||
continue
|
tasks = [(p, params, args.limit, d_starts, laterals, args.seed) for p in bags]
|
||||||
parts[p.name] = got
|
# Печатается по готовности, складывается по номеру задачи: порядок бэгов
|
||||||
Xb, yb = got[0], got[1]
|
# в файле не должен зависеть от того, какой из них досчитался первым.
|
||||||
print(f" {p.name:42s} {Xb.shape[0]:7d} кандидатов, "
|
slots: list = [None] * len(tasks)
|
||||||
f"предметов {int(yb.sum()):6d} ({yb.mean():5.1%}), "
|
for i, task, got, secs in P.run(_work, tasks, args.jobs):
|
||||||
f"{time.time() - t0:6.0f} с", flush=True)
|
name = task[0].name
|
||||||
|
if got is None:
|
||||||
if not parts:
|
print(f" {name:42s} пропущен")
|
||||||
raise SystemExit("ничего не собрано")
|
continue
|
||||||
out = {}
|
slots[i] = (name, got)
|
||||||
for name, (Xb, yb, db, ob, lb) in parts.items():
|
Xb, yb = got[0], got[1]
|
||||||
out[f"X_{name}"] = Xb
|
print(f" {name:42s} {Xb.shape[0]:7d} кандидатов, "
|
||||||
out[f"y_{name}"] = yb
|
f"предметов {int(yb.sum()):6d} ({yb.mean():5.1%}), "
|
||||||
out[f"d_{name}"] = db
|
f"{secs:6.0f} с", flush=True)
|
||||||
out[f"obj_{name}"] = ob
|
parts = {name: got for name, got in (s for s in slots if s is not None)}
|
||||||
out[f"lat_{name}"] = lb
|
|
||||||
np.savez_compressed(args.out, names=np.array(list(parts)),
|
if not parts:
|
||||||
features=np.array(FEATURES + ("acc_support",)), **out)
|
raise SystemExit("ничего не собрано")
|
||||||
tot = sum(v[0].shape[0] for v in parts.values())
|
out = {}
|
||||||
pos = sum(int(v[1].sum()) for v in parts.values())
|
for name, (Xb, yb, db, ob, lb) in parts.items():
|
||||||
print(f"\nвсего {tot} кандидатов, предметов {pos} ({pos / tot:.1%})")
|
out[f"X_{name}"] = Xb
|
||||||
print("сохранено:", args.out)
|
out[f"y_{name}"] = yb
|
||||||
|
out[f"d_{name}"] = db
|
||||||
|
out[f"obj_{name}"] = ob
|
||||||
if __name__ == "__main__":
|
out[f"lat_{name}"] = lb
|
||||||
main()
|
np.savez_compressed(args.out, names=np.array(list(parts)),
|
||||||
|
features=np.array(FEATURES + ("acc_support",)), **out)
|
||||||
|
tot = sum(v[0].shape[0] for v in parts.values())
|
||||||
|
pos = sum(int(v[1].sum()) for v in parts.values())
|
||||||
|
print(f"\nвсего {tot} кандидатов, предметов {pos} ({pos / tot:.1%})")
|
||||||
|
print("сохранено:", args.out)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue