AsianPlayer/TriDrive / code /data /build_gaze_targets.py
AsianPlayer's picture
download
raw
14.9 kB
"""cwm3/data/build_gaze_targets.py — the causal `gaze_off` state as a DRIVER TARGET CHAIN on the s12 anchor grid (roadmap §B3 / coordinator 2026-09-12:
"gaze_off as a driver target chain", the engaged lever). Mirrors build_kin_causal.main_routes (hands_on / head_turned chains) for a third state.
Source state. `gaze_off` = build_face_causal's hysteresis state on |lm_yaw_dev| (OUR OWN RTMW HALPE-136 face landmarks of the route-level causal pose;
on / off thresholds = the TRAIN-split moving-frame P90 / P75 stored in the face pack's `thresholds`, 0.2121 / 0.1033). The face pack only carries the
state at t (F0_c[:, 7]) and its 8 s history (Fh8_c[:, :, 7]); the FUTURE state is needed for a chain target, so the state series is re-derived on the
10 Hz route grid exactly as build_face_causal pass 1-2 does — same landmarks (build_face_causal.raw_face / IDX), same validity, the SAME causal
per-route references (results/targets_face_causal_routes_v2/route_face_refs.npz, expanding median over MOVING valid frames <= t initialised from the
device's earlier routes) and the same hysteresis (build_kin_causal.hysteresis, > 1 s gap resets) — and asserted equal to F0_c[:, 7] / Fh8_c[:, -1, 7]
on every anchor where both are finite (exact reproduction, not an approximation). Nothing here reads an openpilot DMS quantity.
Outputs (row-aligned with s12_wm/results/targets_{regime}_{split}.npz AND results/targets_kin_causal_routes_v2/kin_causal_{regime}_{split}.npz —
uid / t equality asserted against both) -> <out-dir>/gaze_{regime}_{split}.npz:
uid, dongle, t, row_ok[N] (anchor inside the decoded route series with a frame present, = the face pack's row_ok), route_decoded[N]
S_g f32[N,25] gaze_off state at t+0.2..t+5.0 (NaN = invalid frame / outside the series / s12-censored M)
S0_g f32[N] state at t (NaN invalid)
E_g u8[N,25,2] first-passage onsets: [:, :, 0] gaze_off_on (0 -> 1), [:, :, 1] gaze_off_off (1 -> 0); EM_g bool[N,25,2] their masks
(z_prev and z both finite; build_kin_causal.onsets), E0_g f32[N,2] = [S0_g, 1 - S0_g]
absdev0 f32[N] |lm_yaw_dev| at t (face pack F0_c[:, 1]; the continuous quantity behind the hysteresis -> the current-state RULE comparator)
dwell0 f32[N] gaze_off_dwell_s at t (face pack F0_c[:, 8])
state_names ["gaze_off"], event_names ["gaze_off_on", "gaze_off_off"], thresholds (json), source, pose_dir
+ build_stats_<utc>.json (+ _latest), run_build_gaze_targets_<utc>.json.
python data/build_gaze_targets.py [--out-dir results/targets_gaze_causal_routes_v2]
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from collections import defaultdict
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "common"))
sys.path.insert(0, str(ROOT / "data"))
from runmeta import write_run_json # noqa: E402
import build_kin_causal as BK # noqa: E402 (route_key, hysteresis, onsets, S12R)
import build_face_causal as BF # noqa: E402 (raw_face, IDX, POSE_R; the face channel definitions)
S12R = BK.S12R
KINC_V2 = ROOT / "results" / "targets_kin_causal_routes_v2"
FACE = ROOT / "results" / "targets_face_causal_routes_v2"
OUT_DEFAULT = ROOT / "results" / "targets_gaze_causal_routes_v2"
H = 25
STATES, EVENTS = ["gaze_off"], ["gaze_off_on", "gaze_off_off"]
IDX_YAW_DEV, IDX_GAZE, IDX_DWELL = 1, 7, 8 # build_face_causal.CHANNELS positions
def gaze_series(pose_path, ref, gaze_on, gaze_off):
"""one route -> (k0, state[M] f32 NaN-invalid on the 10 Hz grid, frame[M] bool): build_face_causal pass 1 (yaw_dev with the stored causal reference) + pass 2."""
z = np.load(pose_path)
t = z["t"].astype(np.float64)
W, Hh = [int(v) for v in z["native_wh"]]
kp = z["kpts"][:, BF.IDX]
frame = np.isfinite(z["box"][:, 0]) & np.isfinite(kp[:, 0, 2].astype(np.float32))
raw = BF.raw_face(kp, W, Hh)
raw["V"] &= frame
raw["q"][~raw["V"]] = np.nan
assert ref.shape == (len(t), len(BF.REFQ)), (pose_path, ref.shape, len(t))
V = raw["V"] & np.isfinite(ref[:, 0]) & np.isfinite(ref[:, 1]) & np.isfinite(ref[:, 2])
yaw_dev = (raw["q"][:, 0] - ref[:, 0]).astype(np.float32)
yaw_dev[~V] = np.nan
k = np.round(t * 10).astype(np.int64)
k0, k1 = int(k.min()), int(k.max())
M = k1 - k0 + 1
GG = np.full(M, np.nan, np.float32)
FF = np.zeros(M, bool)
GG[k - k0] = yaw_dev
FF[k - k0] = frame
st = BK.hysteresis(np.abs(GG), gaze_on, gaze_off) # NaN where the frame is invalid; > 1 s gap resets (GAP_RESET 10 steps)
return k0, st.astype(np.float32), FF
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--face-dir", default=str(FACE))
ap.add_argument("--out-dir", default=str(OUT_DEFAULT))
ap.add_argument("--regimes", default="manual,engaged")
ap.add_argument("--splits", default="train,val,test")
ap.add_argument("--limit-routes", type=int, default=0)
a = ap.parse_args()
t_start = time.time()
FD, OUT = Path(a.face_dir), Path(a.out_dir)
OUT.mkdir(parents=True, exist_ok=True)
regs, sps = a.regimes.split(","), a.splits.split(",")
# thresholds + pose dir from the face pack (any split file carries them)
z0 = np.load(FD / f"facehist_causal_{regs[0]}_{sps[0]}.npz", allow_pickle=True)
thr = json.loads(str(z0["thresholds"]))
gaze_on, gaze_off = float(thr["gaze_off_on"]), float(thr["gaze_off_off"])
P2 = ROOT / str(z0["pose_dir"]) if not Path(str(z0["pose_dir"])).is_absolute() else Path(str(z0["pose_dir"]))
assert [str(c) for c in z0["channels"]][IDX_GAZE] == "gaze_off" and [str(c) for c in z0["channels"]][IDX_YAW_DEV] == "lm_yaw_dev"
del z0
def pose_path(u):
p = P2 / f"{u}.npz"
return p if p.exists() else BF.POSE_R / f"{u}.npz"
refs = np.load(FD / "route_face_refs.npz", allow_pickle=True)
assert [str(q) for q in refs["ref_quantities"]] == BF.REFQ
uids = sorted(set().union(*[set(np.unique(np.load(S12R / f"targets_{r}_{s}.npz", allow_pickle=True)["uid"]).tolist()) for r in ("manual", "engaged") for s in ("train", "val", "test")]))
have = [u for u in uids if pose_path(u).exists() and f"ref__{u}" in refs.files]
if a.limit_routes:
have = have[:a.limit_routes]
print(f"s12 routes {len(uids)}, with causal route pose + stored face reference {len(have)}; gaze_off thresholds on {gaze_on:.4f} / off {gaze_off:.4f} (TRAIN P{thr['gaze_pct_on']:.0f} / P{thr['gaze_pct_off']:.0f})", flush=True)
# ---- route series (device order only for progress; the stored references already encode the causal pools)
by_dev = defaultdict(list)
for u in have:
by_dev[u.split("__")[1]].append(u)
series, t_last, n_frames, n_valid = {}, time.time(), 0, 0
for di, (dev, dus) in enumerate(sorted(by_dev.items())):
dus.sort(key=BK.route_key)
for u in dus:
k0, st, FF = gaze_series(pose_path(u), refs[f"ref__{u}"].astype(np.float32), gaze_on, gaze_off)
series[u] = dict(k0=k0, st=st, F=FF)
n_frames += int(FF.sum())
n_valid += int(np.isfinite(st).sum())
if time.time() - t_last > 60:
t_last = time.time()
print(f" devices {di + 1}/{len(by_dev)} routes {len(series)}/{len(have)} {time.time() - t_start:.0f}s", flush=True)
print(f"series for {len(series)} routes: frames {n_frames:,}, state defined on {n_valid / max(n_frames, 1):.3f} of them ({time.time() - t_start:.0f}s)", flush=True)
offs_f = np.arange(1, H + 1) * 2 # t+0.2..t+5.0 on the 10 Hz grid
stats = dict(source="routes", pose_dir=str(P2), pose_fallback_dir=str(BF.POSE_R), face_dir=str(FD), n_routes_s12=len(uids), n_routes_built=len(series),
thresholds=thr, states=STATES, events=EVENTS, n_frames=n_frames, state_defined_frac=n_valid / max(n_frames, 1), splits={})
n_mismatch_total = 0
for reg in regs:
for sp in sps:
d = np.load(S12R / f"targets_{reg}_{sp}.npz", allow_pickle=True)
uid, don, t, Mc = d["uid"], d["dongle"], d["t"], d["M"].astype(bool)
N = len(t)
kin = np.load(KINC_V2 / f"kin_causal_{reg}_{sp}.npz", allow_pickle=True)
assert np.array_equal(kin["uid"], uid) and np.allclose(kin["t"], t), f"kin_causal_{reg}_{sp}: uid/t witness"
fz = np.load(FD / f"facehist_causal_{reg}_{sp}.npz", allow_pickle=True)
assert np.array_equal(fz["uid"], uid) and np.allclose(fz["t"], t), f"facehist_causal_{reg}_{sp}: uid/t witness"
F0, F0v, Fh8, Fh8v, row_ok_face = fz["F0_c"].astype(np.float32), fz["F0valid"].astype(bool), fz["Fh8_c"], fz["Fh8valid"].astype(bool), fz["row_ok"].astype(bool)
S = np.full((N, H), np.nan, np.float32)
S0 = np.full(N, np.nan, np.float32)
route_dec = np.zeros(N, bool)
row_ok = np.zeros(N, bool)
order = np.argsort(uid, kind="stable")
bounds = np.flatnonzero(np.r_[1, uid[order][1:] != uid[order][:-1], 1])
for a_, b_ in zip(bounds[:-1], bounds[1:]):
rows = order[a_:b_]
u = uid[rows[0]]
if u not in series:
continue
rs = series[u]
route_dec[rows] = True
ka = np.round(t[rows] * 10).astype(np.int64) - rs["k0"]
M = len(rs["st"])
idx = ka[:, None] + offs_f[None, :]
ok = (idx >= 0) & (idx < M)
S[rows] = np.where(ok, rs["st"][np.clip(idx, 0, M - 1)], np.nan)
ok0 = (ka >= 0) & (ka < M)
kc = np.clip(ka, 0, M - 1)
S0[rows] = np.where(ok0, rs["st"][kc], np.nan)
row_ok[rows] = ok0 & rs["F"][kc]
# ---- exact-reproduction checks against the face pack (state at t and the last history step)
both = np.isfinite(S0) & F0v & np.isfinite(F0[:, IDX_GAZE])
n_mis = int((S0[both] != F0[both, IDX_GAZE]).sum())
last = Fh8[:, -1, IDX_GAZE].astype(np.float32)
both2 = np.isfinite(S0) & Fh8v[:, -1] & np.isfinite(last)
n_mis2 = int((S0[both2] != last[both2]).sum())
n_mismatch_total += n_mis + n_mis2
assert n_mis == 0 and n_mis2 == 0, f"{reg}/{sp}: gaze_off state at t differs from the face pack on {n_mis} / {n_mis2} anchors (of {int(both.sum())})"
assert np.array_equal(row_ok, row_ok_face), f"{reg}/{sp}: row_ok differs from the face pack"
S[~Mc] = np.nan # s12 censoring (as build_kin_causal)
E = np.zeros((N, H, 2), np.uint8)
EM = np.zeros((N, H, 2), bool)
E[:, :, 0], EM[:, :, 0] = BK.onsets(S0, S)
E[:, :, 1], EM[:, :, 1] = BK.onsets(1 - S0, 1 - S)
E0 = np.stack([S0, 1 - S0], 1).astype(np.float32)
assert not (EM & ~Mc[:, :, None]).any()
absdev0 = np.where(F0v, np.abs(F0[:, IDX_YAW_DEV]), np.nan).astype(np.float32)
dwell0 = np.where(F0v, F0[:, IDX_DWELL], np.nan).astype(np.float32)
np.savez_compressed(OUT / f"gaze_{reg}_{sp}.npz", uid=uid, dongle=don, t=t, row_ok=row_ok, route_decoded=route_dec, S_g=S, S0_g=S0, E_g=E, EM_g=EM, E0_g=E0,
absdev0=absdev0, dwell0=dwell0, state_names=np.array(STATES), event_names=np.array(EVENTS), thresholds=np.array(json.dumps(thr)),
source=np.array("routes"), pose_dir=np.array(str(P2)), face_dir=np.array(str(FD)))
st = dict(N=int(N), routes=int(len(np.unique(uid))), routes_built=int(len({x for x in np.unique(uid) if x in series})), coverage_route_decoded=float(route_dec.mean()),
n_row_ok=int(row_ok.sum()), coverage_row_ok=float(row_ok.mean()), S0_defined_frac_row_ok=float(np.isfinite(S0[row_ok]).mean()) if row_ok.any() else None,
S_all25_defined_frac_row_ok=float(np.isfinite(S[row_ok]).all(1).mean()) if row_ok.any() else None,
gaze_off_prev_anchor=float(np.nanmean(S0)) if np.isfinite(S0).any() else None, gaze_off_prev_future=float(np.nanmean(S)) if np.isfinite(S).any() else None,
n_checked_vs_face_F0=int(both.sum()), n_checked_vs_face_Fh8_last=int(both2.sum()), mismatches=n_mis + n_mis2, events={})
for j, name in enumerate(EVENTS):
z0_, z_ = (S0, S) if j == 0 else (1 - S0, 1 - S)
zp = np.concatenate([z0_[:, None], z_[:, :-1]], 1)
m = EM[:, :, j]
m_tr = m & (zp < 0.5)
inact0 = E0[:, j] < 0.5
rec = dict(n_inactive_at_anchor=int(inact0.sum()), hazard_per_step=float(E[:, :, j][m_tr].mean()) if m_tr.any() else None)
for tau, kk in (("1s", 5), ("3s", 15), ("5s", 25)):
sel = inact0 & m[:, :kk].all(1)
y = E[sel, :kk, j].any(1)
rec[f"n@{tau}"], rec[f"pos@{tau}"], rec[f"base_rate@{tau}"] = int(sel.sum()), int(y.sum()), (float(y.mean()) if sel.any() else None)
st["events"][name] = rec
stats["splits"][f"{reg}_{sp}"] = st
print(f"{reg}/{sp}: N={N} row_ok={row_ok.mean() * 100:.2f}% S0 defined|row_ok={st['S0_defined_frac_row_ok']:.4f} all25={st['S_all25_defined_frac_row_ok']:.4f} "
f"gaze_off@t={st['gaze_off_prev_anchor']:.4f} checked {int(both.sum())}+{int(both2.sum())} anchors vs face pack, mismatches 0; "
+ " ".join(f"{e}@3s={st['events'][e]['pos@3s']}/{st['events'][e]['n@3s']} ({st['events'][e]['base_rate@3s']:.4f})" for e in EVENTS), flush=True)
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
stats["mismatches_total"] = n_mismatch_total
(OUT / f"build_stats_{stamp}.json").write_text(json.dumps(stats, indent=1))
(OUT / "build_stats_latest.json").write_text(json.dumps(stats, indent=1))
write_run_json(OUT, "build_gaze_targets", [Path(__file__), Path(BK.__file__), Path(BF.__file__)],
[S12R / f"targets_{r}_{s}.npz" for r in regs for s in sps] + [FD / f"facehist_causal_{r}_{s}.npz" for r in regs for s in sps] + [FD / "route_face_refs.npz"],
None, t_start, extra=dict(summary={k: v for k, v in stats.items() if k != "splits"},
coverage={k: dict(N=v["N"], row_ok=v["coverage_row_ok"], S0_defined=v["S0_defined_frac_row_ok"], **{e: v["events"][e]["base_rate@3s"] for e in EVENTS}) for k, v in stats["splits"].items()},
stats_file=f"build_stats_{stamp}.json", dms_fields_used=[]), input_listing=True)
print("GAZE_TARGETS_DONE", flush=True)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
14.9 kB
·
Xet hash:
458d4bac3211f52d0af20afa0f61174cce5b5480827c3a1d0dbfb754293a32cb

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.