186 lines
9.7 KiB
Python
186 lines
9.7 KiB
Python
"""Размеченная выборка кандидатов: предмет против тоннельной обстановки.
|
||
|
||
Разметки в датасете нет, и до сих пор это определяло архитектуру: грибовидное
|
||
тело учится **без меток**, запоминая частоту обстановки. Но у нас есть
|
||
физически обоснованный генератор предметов (`flyguard.synth`), сверенный с
|
||
единственным реальным объектом: настоящий 0.67 × 1.35 м на 55 м даёт 47–69
|
||
лучей, синтетический человек 0.44 × 1.71 м на 60 м — 50. Значит, метки можно
|
||
изготовить, и изготовить достоверно.
|
||
|
||
Каждый кандидат помечается по **пересечению лучей**, а не «по дальности
|
||
примерно»: `inject` возвращает индексы лучей, в которые предмет действительно
|
||
записан, и кандидат считается предметом, если его ядро состоит из этих лучей.
|
||
Так структура тоннеля, случайно оказавшаяся на той же дальности, в
|
||
положительные не попадает.
|
||
|
||
Сценарии намеренно ставят предмет в РАЗНЫЕ точки тоннеля (`--d-starts`): замер
|
||
показал, что одна и та же дальность в разных местах перегона ведёт себя
|
||
совершенно по-разному — где-то предмет виден целиком, где-то за поворотом
|
||
(EXPERIMENTS п. 9.6). Обучаться на одной точке постановки значит выучить эту
|
||
точку.
|
||
|
||
`doubleT_obstacle` исключён целиком: там настоящий объект, и он остаётся
|
||
независимой проверкой того, что модель выучила предмет, а не «синтетику».
|
||
|
||
python tools/make_training_set.py --out data/cache/training_set.npz
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
|
||
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 FEATURES, describe
|
||
from flyguard.pipeline import FlyGuard, Params
|
||
from flyguard.synth import IntensityEnv, Placement, catalogue, inject
|
||
|
||
HOLDOUT = "doubleT_obstacle" # там реальный объект — только для проверки
|
||
MIN_OVERLAP = 0.5 # доля лучей ядра, пришедших от предмета
|
||
|
||
|
||
def _work(task):
|
||
"""Одна задача — один бэг. Верхнего уровня: иначе не передать в процесс."""
|
||
path, params, limit, d_starts, laterals, seed = task
|
||
return collect_bag(path, params, limit, d_starts, laterals, seed)
|
||
|
||
|
||
def ego_track(bag: Bag, params: Params, limit: int):
|
||
"""Первый проход: пройденный путь на каждом кадре и общая решётка."""
|
||
fg = FlyGuard(params, memory=None)
|
||
s, total = [], 0.0
|
||
for _, pc in bag.frames(stop=limit):
|
||
res = fg.process(pc)
|
||
if res is None:
|
||
s.append(None)
|
||
continue
|
||
total += res.ego.ds if res.ego else 0.0
|
||
s.append(total)
|
||
return s, fg
|
||
|
||
|
||
def collect_bag(path, params: Params, limit: int, d_starts, laterals, seed: int):
|
||
bag = Bag(path)
|
||
s_track, ego_fg = ego_track(bag, params, limit)
|
||
have = [x for x in s_track if x is not None]
|
||
if len(have) < 30:
|
||
return None
|
||
s0 = have[0]
|
||
layout = ego_fg.layout_full
|
||
col0 = ego_fg.cols.start
|
||
|
||
cat = catalogue()
|
||
scen = [(n, lat, d0) for n in cat for lat in laterals for d0 in d_starts]
|
||
guide = FlyGuard(params, memory=None, layout=layout)
|
||
pipes = [FlyGuard(params, memory=None, layout=layout) for _ in scen]
|
||
rng = np.random.default_rng(seed)
|
||
|
||
X, y, dd, obj, lat_out = [], [], [], [], []
|
||
for k, (_, pc) in enumerate(bag.frames(stop=limit)):
|
||
g = guide.process(pc)
|
||
if g is None or s_track[k] is None:
|
||
continue
|
||
s_now = s_track[k] - s0
|
||
env = IntensityEnv(pc) # один раз на кадр, общий для сценариев
|
||
for i, (name, lat, d0) in enumerate(scen):
|
||
d_true = d0 - s_now
|
||
if d_true < 6.0:
|
||
continue
|
||
u = float(g.corridor.centre(np.array([d_true], np.float32))[0]) + lat
|
||
pc2, lab = inject(pc, layout, g.plane, cat[name],
|
||
Placement(d=d_true, u=u), rng=rng, env=env)
|
||
res = pipes[i].process(pc2)
|
||
if res is None or not res.candidates:
|
||
continue
|
||
rr = lab.get("rays")
|
||
if rr is None or lab["hit_rays"] == 0:
|
||
truth = None
|
||
else:
|
||
# лучи предмета в координатах полной решётки → плоский индекс;
|
||
# отсортированный массив, а не множество: проверка идёт
|
||
# сотни тысяч раз, и `in` по множеству тут заметно дороже
|
||
truth = np.sort((rr[0].astype(np.int64) << 20)
|
||
| rr[1].astype(np.int64))
|
||
for c in res.candidates:
|
||
ii, jj = c.extra.get("rays", (None, None))
|
||
if ii is None:
|
||
continue
|
||
lbl = 0
|
||
if truth is not None and truth.size:
|
||
key = ((ii.astype(np.int64) << 20)
|
||
| (jj.astype(np.int64) + col0))
|
||
pos = np.searchsorted(truth, key)
|
||
np.clip(pos, 0, truth.size - 1, out=pos)
|
||
frac = float((truth[pos] == key).mean())
|
||
lbl = int(frac >= MIN_OVERLAP)
|
||
v = describe(c)
|
||
acc = float(c.extra.get("acc_support", 0.0)) if c.extra else 0.0
|
||
X.append(np.append(v, acc).astype(np.float32))
|
||
y.append(lbl)
|
||
dd.append(c.d)
|
||
obj.append(name if lbl else "")
|
||
lat_out.append(lat)
|
||
return (np.asarray(X, np.float32), np.asarray(y, np.int8),
|
||
np.asarray(dd, np.float32), np.asarray(obj),
|
||
np.asarray(lat_out, np.float32))
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser(description=__doc__)
|
||
ap.add_argument("--root", default=str(B.DATA / "for_hackathon"))
|
||
ap.add_argument("--out", default=str(B.CACHE / "training_set.npz"))
|
||
ap.add_argument("--limit", type=int, default=250)
|
||
ap.add_argument("--d-starts", default="80,140,200")
|
||
# Поперечные положения намеренно кроют ВЕСЬ габарит, а не только ось.
|
||
# Иначе выборка вырождается: предметы у оси, обстановка у стен, и модель
|
||
# выучивает «всё, что у оси — предмет» вместо признаков предмета. Проверено
|
||
# на пробном прогоне с одной постановкой: AUC 1.000 по всем бэгам — цифра
|
||
# красивая и бессмысленная, а в тоннеле у оси полно штатных конструкций.
|
||
ap.add_argument("--laterals", default="0,-0.6,0.6,-1.2,1.2")
|
||
ap.add_argument("--seed", type=int, default=20260921)
|
||
P.add_argument(ap)
|
||
args = ap.parse_args()
|
||
|
||
d_starts = tuple(float(x) for x in args.d_starts.split(","))
|
||
laterals = tuple(float(x) for x in args.laterals.split(","))
|
||
params = Params()
|
||
B.CACHE.mkdir(parents=True, exist_ok=True)
|
||
|
||
bags = [p for p in find_bags(args.root) if p.name != HOLDOUT]
|
||
tasks = [(p, params, args.limit, d_starts, laterals, args.seed) for p in bags]
|
||
# Печатается по готовности, складывается по номеру задачи: порядок бэгов
|
||
# в файле не должен зависеть от того, какой из них досчитался первым.
|
||
slots: list = [None] * len(tasks)
|
||
for i, task, got, secs in P.run(_work, tasks, args.jobs):
|
||
name = task[0].name
|
||
if got is None:
|
||
print(f" {name:42s} пропущен")
|
||
continue
|
||
slots[i] = (name, got)
|
||
Xb, yb = got[0], got[1]
|
||
print(f" {name:42s} {Xb.shape[0]:7d} кандидатов, "
|
||
f"предметов {int(yb.sum()):6d} ({yb.mean():5.1%}), "
|
||
f"{secs:6.0f} с", flush=True)
|
||
parts = {name: got for name, got in (s for s in slots if s is not None)}
|
||
|
||
if not parts:
|
||
raise SystemExit("ничего не собрано")
|
||
out = {}
|
||
for name, (Xb, yb, db, ob, lb) in parts.items():
|
||
out[f"X_{name}"] = Xb
|
||
out[f"y_{name}"] = yb
|
||
out[f"d_{name}"] = db
|
||
out[f"obj_{name}"] = ob
|
||
out[f"lat_{name}"] = lb
|
||
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()
|