Brainrot_Muxa/tools/make_benchmark.py

329 lines
18 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 numpy as np
import _bootstrap as B # noqa: F401
import _parallel as P
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
from flyguard.track_readout import describe_track
HOLDOUT = "doubleT_obstacle" # там уже есть настоящий объект
def _work(task):
"""Одна задача — один бэг.
Обученное грузится путями и уже внутри процесса: передавать модели через
межпроцессную границу незачем, а свою складку каждый воркер берёт сам.
"""
(path, params, limit, d_start, laterals, seed, mem_path, rd_path,
want_tracks, trk_path) = task
memory = MushroomBody.load(mem_path) if mem_path else None
readout = None
if rd_path:
from flyguard.mbon_readout import MbonReadout
readout = MbonReadout.load(rd_path)
track_rd = None
if trk_path:
from flyguard.track_readout import TrackReadout
track_rd = TrackReadout.load(trk_path)
return run_bag(path, params, memory, limit, d_start, laterals, seed,
readout=readout, want_tracks=want_tracks,
track_readout=track_rd)
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,
want_tracks: bool = False, track_readout=None):
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,
track_readout=track_readout) for _ in scen]
rng = np.random.default_rng(seed)
records = [[] for _ in scen]
# Выборка для считывания по трекам: описание каждого живого трека на
# каждом кадре и метка «это вставленный предмет». Собирается здесь, а не
# отдельным инструментом, ровно затем, чтобы распределение совпадало с
# рабочим: та же память тоннеля, то же покадровое считывание, те же
# пороги. Обученное на другой обстановке считывание нечего и мерить.
trk_rows: list = []
# решётка и поза нужны для вставки — берутся из отдельного «чистого» конвейера
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)))
if want_tracks:
for tr in cx.tracks:
dd = tr.distance(cx.s_world)
if not (0.0 < dd <= params.d_max):
continue
trk_rows.append((describe_track(tr, cx.s_world),
int(abs(dd - d_true) < tol), dd))
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)))
tracks = None
if want_tracks and trk_rows:
tracks = (np.stack([r[0] for r in trk_rows]).astype(np.float32),
np.array([r[1] for r in trk_rows], np.int8),
np.array([r[2] for r in trk_rows], np.float32))
return out, tracks
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("--tracks-out", default="",
help="куда сложить выборку по трекам (npz); пусто — не собирать")
ap.add_argument("--track", default="",
help="считывание по трекам одной моделью; она видела эти "
"записи — годится только для отладки")
ap.add_argument("--track-dir", default="",
help="каталог с моделями по складкам (track_<бэг>.npz)")
ap.add_argument("--track-blend", type=float, default=None,
help="доля считывания по треку в решении; 0 — только улика")
ap.add_argument("--track-score", default=None,
choices=("w_mean", "w_ema", "p_mean", "model"),
help="чем мерить трек: средний вес наблюдения, средний "
"отсчёт считывания или обученная модель")
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("--near-long", type=float, default=None,
help="разброс дальности, выше которого компоненту режут и вблизи; 0 — не резать вблизи")
ap.add_argument("--min-rays", type=int, default=None,
help="сколько лучей минимум образуют кандидата")
ap.add_argument("--min-rays-far", type=int, default=None,
help="порог по лучам за `--min-rays-far-from`; 0 — не различать")
ap.add_argument("--min-rays-far-from", type=float, default=None)
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 — не поправлять")
P.add_argument(ap)
args = ap.parse_args()
B.ARTIFACTS.mkdir(parents=True, exist_ok=True)
fold_paths: dict[str, str] = {}
if args.mbon_dir:
from pathlib import Path as _P
for f in _P(args.mbon_dir).glob("mbon_*.npz"):
fold_paths[f.stem[len("mbon_"):]] = str(f)
print(f"считывание MBON по складкам: {args.mbon_dir} "
f"({len(fold_paths)} моделей)")
elif args.mbon:
print(f"считывание MBON: {args.mbon}")
track_paths: dict[str, str] = {}
if args.track_dir:
from pathlib import Path as _P2
for f in _P2(args.track_dir).glob("track_*.npz"):
track_paths[f.stem[len("track_"):]] = str(f)
print(f"считывание по трекам по складкам: {args.track_dir} "
f"({len(track_paths)} моделей)")
elif args.track:
print(f"считывание по трекам: {args.track}")
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.near_long is not None:
over["near_long"] = args.near_long
if args.min_rays is not None:
over["min_rays"] = args.min_rays
if args.min_rays_far is not None:
over["min_rays_far"] = args.min_rays_far
if args.min_rays_far_from is not None:
over["min_rays_far_from"] = args.min_rays_far_from
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
if args.track_blend is not None:
over["track_blend"] = args.track_blend
if args.track_score is not None:
over["track_score"] = args.track_score
params = Params(**over)
laterals = tuple(float(x) for x in args.laterals.split(","))
tasks = []
for p in find_bags(args.root):
if p.name == HOLDOUT:
continue
rd = args.mbon
if fold_paths:
rd = fold_paths.get(p.name, "")
if not rd:
print(f" внимание: для {p.name} нет своей складки — пропуск")
continue
trk = track_paths.get(p.name, args.track) if track_paths else args.track
tasks.append((p, params, args.limit, args.d_start, laterals, args.seed,
args.memory or "", rd, bool(args.tracks_out), trk))
# Печатается по готовности, собирается по номеру задачи: порядок сценариев
# в файле не должен зависеть от того, какой бэг досчитался первым.
slots: list = [None] * len(tasks)
trk_slots: list = [None] * len(tasks)
for i, task, got, secs in P.run(_work, tasks, args.jobs):
rec, tracks = got
slots[i] = rec
trk_slots[i] = (task[0].name, tracks)
n = sum(len(r["d"]) for r in rec)
extra = f", треков {tracks[0].shape[0]:7d}" if tracks is not None else ""
print(f" {task[0].name:42s} сценариев {len(rec):3d}, наблюдений {n:6d}"
f"{extra}, {secs:6.1f} с", flush=True)
all_rec = [r for rec in slots if rec for r in rec]
if args.tracks_out:
from flyguard.track_readout import TRACK_FEATURES
out_arr, names = {}, []
for got in trk_slots:
if got is None or got[1] is None:
continue
name, (Xb, yb, db) = got
names.append(name)
out_arr[f"X_{name}"] = Xb
out_arr[f"y_{name}"] = yb
out_arr[f"d_{name}"] = db
np.savez_compressed(args.tracks_out, names=np.array(names),
features=np.array(TRACK_FEATURES), **out_arr)
tot = sum(v.shape[0] for k, v in out_arr.items() if k.startswith("X_"))
pos = sum(int(v.sum()) for k, v in out_arr.items() if k.startswith("y_"))
print(f"выборка по трекам: {tot} наблюдений, предметных {pos} "
f"({pos / max(tot, 1):.1%}) -> {args.tracks_out}")
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()