Lidar_Muxa/tools/make_benchmark.py

225 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Размеченный полигон: сценарии сближения с синтетическим препятствием.
Для каждого бэга и каждого типа предмета строится сценарий: предмет ставится
в фиксированную точку тоннеля далеко впереди, поезд к нему подъезжает, и на
каждом кадре известна истинная дистанция. Отсюда получаются именно те цифры,
которые просит ТЗ: с какой дальности предмет уверенно виден, сколько ложных
тревог и как это зависит от размера.
Первый проход считает собственное движение по чистым данным (это и есть
разметка по дистанции), второй — гоняет конвейер по кадрам со вставленным
предметом. Все сценарии одного бэга обрабатываются в одном проходе по файлу:
чтение данных дороже самой обработки.
python tools/make_benchmark.py --out artifacts/benchmark.npz --memory artifacts/mushroom_body.npz
"""
from __future__ import annotations
import argparse
import json
import time
import numpy as np
import _bootstrap as B # noqa: F401
from flyguard.bag import Bag, find_bags
from flyguard.mushroom_body import MushroomBody
from flyguard.pipeline import FlyGuard, Params
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
HOLDOUT = "doubleT_obstacle" # там уже есть настоящий объект
def ego_track(bag: Bag, params: Params, limit: int | None):
"""Первый проход: пройденный путь на каждом кадре (разметка по дистанции)."""
fg = FlyGuard(params, memory=None)
s, stamps = [], []
total = 0.0
for _, pc in bag.frames(stop=limit):
res = fg.process(pc)
if res is None:
s.append(None); stamps.append(pc.stamp); continue
total += res.ego.ds if res.ego else 0.0
s.append(total); stamps.append(pc.stamp)
return s, stamps
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)
s_track, _ = ego_track(bag, params, limit)
have = [x for x in s_track if x is not None]
if len(have) < 20:
return []
travel = have[-1] - have[0]
cat = catalogue()
scen = [(name, lat) for name in cat for lat in laterals]
pipes = [FlyGuard(params, memory=memory, readout=readout) for _ in scen]
rng = np.random.default_rng(seed)
records = [[] for _ in scen]
# решётка и поза нужны для вставки — берутся из отдельного «чистого» конвейера
guide = FlyGuard(params, memory=None)
for k, (_, pc) in enumerate(bag.frames(stop=limit)):
gres = guide.process(pc)
if gres is None or s_track[k] is None:
continue
s_now = s_track[k] - have[0]
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
for i, (name, lat) in enumerate(scen):
d_true = d_start - s_now
if d_true < 6.0:
continue
# Предмет лежит НА ПУТИ, а путь в кривой уходит вбок: на 150 м при
# радиусе 1300 м это 8.6 м. Если ставить его в поперечных координатах
# сенсора, он окажется в стене, а не в габарите.
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],
Placement(d=d_true, u=u_obj), rng=rng, env=env)
res = pipes[i].process(pc2)
if res is None:
continue
tol = max(3.0, 0.12 * d_true)
hit = any(abs(o.distance - d_true) < tol for o in res.decision.objects)
fp = sum(1 for o in res.decision.objects if abs(o.distance - d_true) >= tol)
# Воронка потерь. Лучи в предмет попали — а дальше он может
# пропасть на любой из трёх ступеней, и лечатся они по-разному:
# нет кандидата — вопрос к кластеризации и разделению фигуры и
# фона, нет трека — к сопоставлению по кадрам, нет решения —
# к порогу. Без этого разбиения улучшать нечего, кроме удачи.
cand = any(abs(c.d - d_true) < tol for c in res.candidates)
cx = pipes[i].cx
trk = any(abs(tr.distance(cx.s_world) - d_true) < tol
for tr in cx.tracks)
records[i].append((d_true, int(hit), fp, lab["hit_rays"],
int(cand), int(trk)))
out = []
for (name, lat), rec in zip(scen, records):
if not rec:
continue
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)))
return out
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
ap.add_argument("--memory")
ap.add_argument("--mbon", default="",
help="обученное считывание MBON одной моделью; она видела "
"эти бэги — годится только для отладки")
ap.add_argument("--mbon-dir", default="",
help="каталог с моделями по складкам (mbon_<бэг>.npz): для "
"каждого бэга берётся модель, его НЕ видевшая. Иначе "
"дальность завышена: считывание обучалось ровно на "
"таких же вставках в этот же тоннель")
ap.add_argument("--out", default=str(B.ARTIFACTS / "benchmark.json"))
ap.add_argument("--limit", type=int, default=250)
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)
ap.add_argument("--mbon-blend", type=float, default=None,
help="1 — только модель, 0 — только ручная формула")
ap.add_argument("--warn", type=float, default=None,
help="порог улики для тревоги (по умолчанию 0.5)")
ap.add_argument("--clear", type=float, default=None)
ap.add_argument("--min-hits", type=int, default=None)
ap.add_argument("--warn-far", type=float, default=None,
help="порог тревоги на дальнем краю; ниже обычного — послабление далёким трекам")
ap.add_argument("--warn-far-from", type=float, default=None,
help="с какой дальности порог начинает падать, м")
ap.add_argument("--novelty-floor", type=float, default=None,
help="ниже этой новизны трек не считается")
ap.add_argument("--min-rays", type=int, default=None,
help="сколько лучей минимум образуют кандидата")
ap.add_argument("--leak-far", type=float, default=None,
help="утечка улики на дальнем краю; ниже обычной — далёкий трек прощает промахи")
ap.add_argument("--leak-far-from", type=float, default=None,
help="с какой дальности утечка начинает падать, м")
ap.add_argument("--nov-fade-from", type=float, default=None,
help="с какой дальности гасить вклад знакомости; 0 — не гасить")
ap.add_argument("--nov-fade-to", type=float, default=None,
help="к какой дальности вклад знакомости обнуляется")
ap.add_argument("--mbon-prior-from", type=float, default=None,
help="с какой дальности поправлять оценку модели на распространённость предметов; 0 — не поправлять")
args = ap.parse_args()
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
memory = MushroomBody.load(args.memory) if args.memory else None
readout = None
folds: dict = {}
if args.mbon_dir:
from pathlib import Path as _P
from flyguard.mbon_readout import MbonReadout
for f in _P(args.mbon_dir).glob("mbon_*.npz"):
folds[f.stem[len("mbon_"):]] = MbonReadout.load(f)
print(f"считывание MBON по складкам: {args.mbon_dir} "
f"({len(folds)} моделей)")
elif args.mbon:
from flyguard.mbon_readout import MbonReadout
readout = MbonReadout.load(args.mbon)
print(f"считывание MBON: {args.mbon}")
over = {}
if args.mbon_blend is not None:
over["mbon_blend"] = args.mbon_blend
if args.warn is not None:
over["warn_evidence"] = args.warn
over["clear_evidence"] = args.warn * 0.6 if args.clear is None else args.clear
elif args.clear is not None:
over["clear_evidence"] = args.clear
if args.min_hits is not None:
over["min_hits"] = args.min_hits
if args.warn_far is not None:
over["warn_far"] = args.warn_far
if args.warn_far_from is not None:
over["warn_far_from"] = args.warn_far_from
if args.novelty_floor is not None:
over["novelty_floor"] = args.novelty_floor
if args.min_rays is not None:
over["min_rays"] = args.min_rays
if args.leak_far is not None:
over["leak_far"] = args.leak_far
if args.leak_far_from is not None:
over["leak_far_from"] = args.leak_far_from
if args.nov_fade_from is not None:
over["nov_fade_from"] = args.nov_fade_from
if args.nov_fade_to is not None:
over["nov_fade_to"] = args.nov_fade_to
if args.mbon_prior_from is not None:
over["mbon_prior_from"] = args.mbon_prior_from
params = Params(**over)
laterals = tuple(float(x) for x in args.laterals.split(","))
all_rec = []
for p in find_bags(args.root):
if p.name == HOLDOUT:
continue
t0 = time.time()
rd = readout
if folds:
rd = folds.get(p.name)
if rd is None:
print(f" внимание: для {p.name} нет своей складки — пропуск")
continue
rec = run_bag(p, params, memory, args.limit, args.d_start, laterals,
args.seed, readout=rd)
all_rec.extend(rec)
n = sum(len(r["d"]) for r in rec)
print(f" {p.name:42s} сценариев {len(rec):3d}, наблюдений {n:6d}, "
f"{time.time()-t0:6.1f} с", flush=True)
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()