AsianPlayer/TriDrive / code /tools /review_tool /build_gold_engaged.py
AsianPlayer's picture
download
raw
49.2 kB
#!/usr/bin/env python3
"""tools/review_tool/build_gold_engaged.py — human GOLD EVENT SET labelling page for the ENGAGED hand cells (pre-registered
docs/decisions.md 2026-09-17T00:28:00Z "human GOLD EVENT SET"; evaluation only).
Candidates = v3 first-step transitions on the ENGAGED TEST split: results/targets_kin_causal_routes_v3/kin_causal_engaged_test.npz rows with
row_ok & E_c[:, 0, e] == 1 & EM_c[:, 0, e] (event in the first future step -> t_event = t_anchor + 0.2 s), local video required,
stratified <= PER_ROUTE (3) per route per event type, seed 0, random order, EXCLUDING every event within +-5 s (same route) of
(a) the 34 reviewed engaged hand_return task-B items (results/review_tool/items.json, passes 1-4),
(b) every item of the v3 validation page (results/review_tool/items_relabel_v3.json: event items by t_event, recall windows by [t0, t1]),
(c) the pass-3/4 items (results/review_tool/items_relabel_hr_engaged_01s.json).
Sizes: hand_return 300 + hand_leave 150, ordered in blocks R200 -> L100 -> R100 -> L50 (ids G_000 ...) so that stopping after 300 items leaves a
usable minimum set. If a pool (after the per-route cap) is smaller than requested, all of it is taken and the shortfall is reported.
Amendment (2026-09-17, owner can label 500+): two blocks are APPENDED after L50 —
W = 60 random ENGAGED TEST 10 s windows at 1 Hz (10 frames t0 ... t0+9 s; seed 0; <= 1 per route; no v3 hand event in [t0-1, t0+10]; not
overlapping any excluded zone; V3B.valid_window_starts) — 'did a hand return to the wheel and stay >= 1 s?' yes | no | unsure (ids GW_000 ...);
this estimates the recall of the v3 candidate set (apply_review_v3.py logic);
MR = 100 MANUAL TEST hand_return v3 candidates (same sampling/exclusion rules, <= 3 per route; 31 frames; same five-key answers; ids GM_000 ...).
Header milestones: 300 (minimum set) / end of the engaged blocks / + W / + MR (planned 300 / 450 / 510 / 610).
Frames: dt = -2.0 ... +1.0 s in 0.1 s steps (31) around t_event with the causal driver box (build_review_tool.draw), extracted with the
sequential-read reader of build_relabel_v3.py (one seek per window, then cap.grab/read), one video open per route, 4 worker processes,
into results/review_tool/img_gold/.
Page relabel_gold_engaged.html (Chinese UI, same JS skeleton as relabel_hr_engaged_r4.html / relabel_v3.html): localStorage key
cwm3_review_gold_engaged_v1, server sync POST /save + GET /labels (rater ''), REAL-TIME auto-play (100 ms per frame, once, then stops on the
EVENT frame) when an item becomes current, 31 thumbnails, one-key answers 1-5 with auto-advance; labels at_event | ret_dt=<dt> / off_dt=<dt> |
never | always | unsure. apply_gold_engaged.py turns the labels into results/review_tool/gold_engaged_events.json.
Outputs (new files only): results/review_tool/{relabel_gold_engaged.html, items_gold_engaged.json, img_gold/*.jpg}; nothing else is written.
Enrichment (2026-09-17T01:05:57Z pre-registration, APPEND-ONLY): --append-ranked <results/vlm_handson/rank_top150.json> appends block RE = the
top-150 fresh engaged hand_return candidates by the VLM deltaP (tools/vlm_handson/rank_candidates.py) as ids GE_000 ... (same meta + 'deltaP',
31 frames, same five-key answers) after MR; every pre-existing item / id / order is left byte-identical (the json is loaded and re-dumped; the
caller diffs the first N items), the header gets a compact 'jump to block' control and the milestone list gains the RE total.
python tools/review_tool/build_gold_engaged.py [--n-return 300] [--n-leave 150] [--n-windows 60] [--n-manual 100] [--per-route 3] [--seed 0] [--workers 4] [--out results/review_tool]
python tools/review_tool/build_gold_engaged.py --html-only # rewrite the html from the existing items json
python tools/review_tool/build_gold_engaged.py --check # self-checks on the existing items json (no writes)
python tools/review_tool/build_gold_engaged.py --append-ranked results/vlm_handson/rank_top150.json [--append-block RE] [--append-prefix GE] [--workers 4]
"""
from __future__ import annotations
import argparse, json, os, sys, time
from pathlib import Path
import numpy as np
CWM3 = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(Path(__file__).resolve().parent))
import build_review_tool as B # noqa: E402 (VIO, grab, draw, nearest_pose_row, FPS)
import build_relabel_v3 as V3B # noqa: E402 (read_frames: sequential-read extraction, load_cell)
V3 = CWM3 / "results/targets_kin_causal_routes_v3"
RT = CWM3 / "results/review_tool"
ITEMS_B = RT / "items.json"; ITEMS_V3 = RT / "items_relabel_v3.json"; ITEMS_01S = RT / "items_relabel_hr_engaged_01s.json"
DTS = [round(float(x), 1) + 0.0 for x in np.arange(-2.0, 1.0001, 0.1)] # 31 frames, dt 0 at index 20 (+0.0 normalises -0.0)
STEP_S = 0.2; EXCL_S = 5.0
BLOCKS = (("R200", "hand_return", 200), ("L100", "hand_leave", 100), ("R100", "hand_return", 100), ("L50", "hand_leave", 50))
BLOCK_ORDER = ("R200", "L100", "R100", "L50", "W", "MR", "RE") # RE = appended VLM-ranked block (--append-ranked)
WIN_S, WIN_FRAMES = V3B.WIN_S, V3B.WIN_FRAMES # 10 s windows, 10 frames at 1 Hz
EVENT_CN = {"hand_return": "手回到方向盘(hand_return)", "hand_leave": "两手离开方向盘(hand_leave)", "recall_window": "召回窗口(10 s,1 Hz)"}
LABELS = {"hand_return": "at_event | ret_dt=<dt> | never | always | unsure", "hand_leave": "at_event | off_dt=<dt> | never | always | unsure", "recall_window": "yes | no | unsure"}
IMG_DIR = RT / "img_gold" # module-level so the fork workers see it
# ------------------------------------------------------------------------------------------------------------- exclusion sets
def excluded_intervals():
"""[(uid, lo, hi, tag)] — an event with t_event in [lo, hi] (same uid) is excluded."""
iv = []
for it in json.load(open(ITEMS_B, encoding="utf-8"))["items"]:
m = it["meta"]
if it["task"] == "B" and m["regime"] == "engaged" and m["event"] == "hand_return":
iv.append((m["uid"], float(m["t_event"]) - EXCL_S, float(m["t_event"]) + EXCL_S, f"B:{it['id']}"))
for it in json.load(open(ITEMS_V3, encoding="utf-8"))["items"]:
m = it["meta"]
if "t_event" in m: iv.append((m["uid"], float(m["t_event"]) - EXCL_S, float(m["t_event"]) + EXCL_S, f"V3:{it['id']}"))
else: iv.append((m["uid"], float(m["t0"]) - EXCL_S, float(m["t1"]) + EXCL_S, f"V3:{it['id']}"))
for it in json.load(open(ITEMS_01S, encoding="utf-8"))["items"]:
m = it["meta"]; iv.append((m["uid"], float(m["t_event"]) - EXCL_S, float(m["t_event"]) + EXCL_S, f"P3:{it['id']}"))
return iv
def hits(uid: str, te: float, iv):
return [x[3] for x in iv if x[0] == uid and x[1] <= te <= x[2]]
# ------------------------------------------------------------------------------------------------------------- sampling
def sample(z, e_idx: int, n: int, per_route: int, rng, iv, log):
rows = np.where(z["ok"] & z["E1"][:, e_idx] & z["EM1"][:, e_idx])[0]
uids = z["uid"][rows]; te = z["t"][rows] + STEP_S
vid = np.array([B.VIO.has_video(u) for u in uids], bool)
excl = np.zeros(len(rows), bool)
for u, lo, hi, _ in iv:
excl |= (uids == u) & (te >= lo) & (te <= hi)
cand = rows[vid & ~excl]
n_routes = len(np.unique(z["uid"][cand]))
per_cap = int(sum(min(c, per_route) for c in np.unique(z["uid"][cand], return_counts=True)[1]))
log(f" rows {len(rows)} local-video {int(vid.sum())} excluded(+-{EXCL_S:.0f}s of reviewed/validation items) {int((vid & excl).sum())} -> pool {len(cand)} on {n_routes} routes; "
f"reachable under <= {per_route}/route: {per_cap}; requested {n}")
cand = cand.copy(); rng.shuffle(cand)
chosen, per = [], {}
for r in cand:
if len(chosen) >= n: break
u = z["uid"][r]
if per.get(u, 0) >= per_route: continue
per[u] = per.get(u, 0) + 1; chosen.append(int(r))
st = {"n_rows": int(len(rows)), "n_local_video": int(vid.sum()), "n_excluded": int((vid & excl).sum()), "n_pool": int(len(cand)), "n_pool_routes": int(n_routes),
"n_reachable_per_route_cap": per_cap, "n_requested": n, "n_chosen": len(chosen), "n_routes_chosen": int(len(np.unique(z["uid"][chosen]))) if chosen else 0,
"shortfall": max(0, n - len(chosen))}
return chosen, st
def sample_windows(z, n: int, rng, iv, log):
"""Block W: valid 10 s window starts (V3B.valid_window_starts without point exclusion), then drop windows whose [t0, t0 + WIN_S] intersects an
excluded interval (same route), then <= 1 per route with local video, seed order."""
starts = V3B.valid_window_starts(z, [])
t0 = z["t"][starts]; uids = z["uid"][starts]
excl = np.zeros(len(starts), bool)
for u, lo, hi, _ in iv:
excl |= (uids == u) & (t0 <= hi) & (t0 + WIN_S >= lo)
starts = starts[~excl]
log(f" valid window starts {len(starts) + int(excl.sum())} -> not overlapping excluded zones {len(starts)} (= {len(starts) * STEP_S / 3600:.2f} h of start positions) on {len(np.unique(z['uid'][starts]))} routes")
st = starts.copy(); rng.shuffle(st)
chosen, used = [], set()
for i in st:
if len(chosen) >= n: break
u = z["uid"][i]
if u in used or not B.VIO.has_video(u): continue
used.add(u); chosen.append(int(i))
n_ret = int((z["ok"] & z["E1"][:, 0] & z["EM1"][:, 0]).sum()); n_lea = int((z["ok"] & z["E1"][:, 1] & z["EM1"][:, 1]).sum())
return chosen, {"n_valid_window_starts": int(len(starts)), "start_step_s": STEP_S, "window_s": WIN_S, "window_pre_s": V3B.WIN_PRE_S, "n_valid_window_routes": int(len(np.unique(z["uid"][starts]))),
"excluded_v3_events_inside": ["hand_return", "hand_leave"], "n_requested": n, "n_chosen": len(chosen), "shortfall": max(0, n - len(chosen)),
"n_v3_hand_return_engaged_test": n_ret, "n_v3_hand_leave_engaged_test": n_lea, "n_anchors_row_ok_engaged_test": int(z["ok"].sum()), "anchor_step_s": STEP_S}
# ------------------------------------------------------------------------------------------------------------- frames (fork workers)
def extract_group(args):
"""(uid, [(item_id, times, dts, tag_event), ...]) -> {item_id: frames}; one VideoCapture per route, V3B.read_frames per window
(31 frames at 0.1 s for events, 10 frames at 1 Hz for windows); labels as in build_relabel_v3.extract."""
uid, jobs = args
import cv2
cv2.setNumThreads(1)
boxes = B.VIO.load_driver_boxes(uid); vp = B.VIO.video_path(uid)
if boxes is None or not vp.exists():
return {j[0]: [] for j in jobs}
out = {}
cap = cv2.VideoCapture(str(vp))
try:
for iid, times, dts, tag in sorted(jobs, key=lambda j: j[1][0]):
fis = [int(round(tt * B.FPS)) for tt in times]
frames = V3B.read_frames(cap, fis)
fl = []
for k, (tt, dt, fi) in enumerate(zip(times, dts, fis)):
fr = frames.get(fi)
if fr is None: continue
j = B.nearest_pose_row(boxes, tt); box = boxes["box"][j] if j is not None else None
lab = (f"{'EVENT ' if dt == 0 else ''}t={tt:.1f}s ({dt:+.1f}s)") if tag else f"t={tt:.1f}s (+{dt:.0f}s)"
fn = f"{iid}_{k:02d}.jpg"; B.draw(fr, box, lab, IMG_DIR / fn)
fl.append({"file": f"img_gold/{fn}", "t": float(tt), "dt": float(dt), "frame_idx": fi})
out[iid] = fl
finally:
cap.release()
return out
# ------------------------------------------------------------------------------------------------------------- page
HTML = r"""<!doctype html><html lang="zh"><head><meta charset="utf-8"><title>CockpitWM 金标事件标注(engaged 回盘 / 离盘 + 召回窗口 + manual 回盘)</title>
<style>
body{font-family:system-ui,Segoe UI,Arial,'Noto Sans CJK SC',sans-serif;margin:0;background:#f5f4f0;color:#111}
header{position:sticky;top:0;background:#fff;border-bottom:1px solid #ccc;padding:8px 16px;display:flex;gap:14px;align-items:center;flex-wrap:wrap;z-index:5}
button{padding:6px 12px;border:1px solid #888;border-radius:6px;background:#fff;cursor:pointer}button.sel{background:#1f5fbf;color:#fff;border-color:#1f5fbf}
main{padding:12px 16px;max-width:1500px;margin:0 auto}
.item{background:#fff;border:1px solid #ddd;border-radius:8px;padding:10px 12px;margin:10px 0}.item.done{border-color:#7ab27a;background:#f4fbf4}.item.cur{outline:3px solid #1f5fbf}
.frames{display:flex;gap:3px;flex-wrap:wrap}.th{display:flex;flex-direction:column;align-items:center;font-size:10px;color:#555}.th img{width:min(5.8vw,88px);border:2px solid #ccc;border-radius:3px;cursor:pointer}
.th img.ev{border-color:#e0a800}.frames.win .th img{width:min(9vw,140px)}.th img.sel{border-color:#d33;box-shadow:0 0 0 2px #d33}
.player{display:flex;gap:12px;align-items:flex-start;margin:6px 0}.player img{width:min(44vw,640px);border:2px solid #1f5fbf;border-radius:4px}
.q{margin:8px 0 4px;font-weight:600}.meta{font-size:12px;color:#555;word-break:break-all}.ans{margin:6px 0;font-size:14px}.hint{font-size:12px;color:#666}.prog{font-size:13px}
.def{margin:12px 16px;max-width:1500px;background:#fff8e1;border:1px solid #e0b400;border-radius:8px;padding:10px 12px;font-size:14px;line-height:1.5}
.bar{position:relative;width:260px;height:12px;background:#e6e6e6;border-radius:6px;overflow:hidden;display:inline-block;vertical-align:middle}.bar>div{height:100%;background:#1f5fbf}.bar>i{position:absolute;top:0;width:2px;height:100%;background:#d33}
.arm{color:#b00020;font-weight:600}.pl{font-size:13px;margin-top:4px}
input[type=text]{width:60%;padding:4px}
button.blk{padding:2px 7px;font-size:12px;margin-left:2px}
</style></head><body>
<header><b>CockpitWM 金标事件标注</b><span class="prog" id="prog"></span>
<button onclick="jump(-1)">◀ 上一条 (p)</button><button onclick="jump(1)">下一条 ▶ (n)</button><span class="hint" id="blocks"></span><span class="hint" id="sync">离线模式</span>
<span class="hint">快捷键:1–5 作答(1/3/4/5 自动跳下一条;2 之后点小图或用 , . 移到该帧按 Enter),空格重播,, / . 前后一帧,n/p 翻页。每次选择自动保存(服务器 + 本浏览器)。</span></header>
<div class="def"><b>定义。</b>“手在盘上(on)”= 至少一只手握住或放在方向盘的<b>轮缘或轮辐/中心</b>上;悬在盘上方、握拨杆、换挡、拿手机 = <b>off</b>。<b>回盘 hand_return</b> = 从两手都 off 变为至少一手 on;<b>离盘 hand_leave</b> = 从至少一手 on 变为<b>两手都 off</b>。
以视频判断(每条 31 帧 = −2.0 … +1.0 s,0.1 s/帧);<b>EVENT = dt 0 = 自动候选时刻</b>(金色边框)。条目成为当前条时视频以<b>真实速度</b>自动播放一遍后停在 EVENT 帧;空格重播,, . 逐帧。<br>
<b>回盘题</b>:1 = 回盘就在 EVENT 附近(±0.5 s);2 = 回盘发生但在别的时刻 → 点选那一帧;3 = 31 帧中手从未回到盘上;4 = 一直在盘上(没离开过);5 = 不确定。<br>
<b>离盘题</b>:1 = 离盘就在 EVENT 附近(±0.5 s);2 = 离盘在别的时刻 → 点选那一帧;3 = 从未离盘(始终至少一手在盘上);4 = 一直不在盘上;5 = 不确定。<br>
<b>召回窗口题(块 W)</b>:10 帧、每帧间隔 1 s(t0 … t0+9 s;自动标签认为窗口内没有回盘/离盘)。这个 10 s 窗口里是否有手<b>回到</b>方向盘上并保持 ≥ 1 s?1 = 是;2 = 否;3 = 不确定。<b>块 MR</b> = manual 模式的回盘候选,答法与回盘题相同。<br>
块顺序:R200 → L100 → R100 → L50(engaged 事件)→ W(召回窗口)→ MR(manual 回盘)→ RE(用本地 VLM 按 ΔP 排序的 engaged 回盘候选,答法与回盘题相同;排序只决定标注顺序,不影响评价)。进度里程碑:<b>300 条 = 最小集</b>,然后 engaged 事件全集 / +窗口 / +manual / +RE(计划 300 / 450 / 510 / 610;实际 300 / 408 / 468 / 568 / 718,见表头)。表头“跳到块”按钮 = 跳到该块第一条未标注的条目。只看手,不看头;红框只标出驾驶员;备注可写 phone 等。</div>
<main id="main"></main>
<script>
const ITEMS = __ITEMS__;
const KEY = 'cwm3_review_gold_engaged_v1';
const MIN_SET = 300, FULL = ITEMS.length;
const BLOCKS = ['R200','L100','R100','L50','W','MR','RE'];
const M_ENG = ITEMS.filter(i=>['R200','L100','R100','L50'].includes(i.meta.block)).length, M_W = M_ENG + ITEMS.filter(i=>i.meta.block==='W').length, M_MR = M_W + ITEMS.filter(i=>i.meta.block==='MR').length;
const MILESTONES = [[MIN_SET,'最小集'],[M_ENG,'engaged 事件全集'],[M_W,'+召回窗口'],[M_MR,'+manual 回盘'],[FULL,'+RE(VLM 排序回盘候选,全部)']].filter((m,i,a)=>i===0||m[0]>a[i-1][0]);
function blockBar(){ const e=document.getElementById('blocks'); if(!e) return; e.innerHTML='跳到块:'+BLOCKS.filter(b=>ITEMS.some(i=>i.meta.block===b)).map(b=>`<button class="blk" title="跳到块 ${b} 的第一条未标注条目" onclick="jumpBlock('${b}')">${b}</button>`).join(''); }
function jumpBlock(b){ let i=ITEMS.findIndex(it=>it.meta.block===b&&!lab(it.id)); if(i<0) i=ITEMS.findIndex(it=>it.meta.block===b); if(i>=0) setCur(i,true); }
let store = JSON.parse(localStorage.getItem(KEY) || '{}'); let cur = -1; let _pl=null; let _tok=0; let armed=false; let _lock=0;
const SERVER = location.protocol.startsWith('http');
const OPTS = {hand_return:[['at_event','回盘就在 EVENT 附近(±0.5 s)'],['__pick','回盘发生但在别的时刻 → 点选那一帧'],['never','手从未回到盘上'],['always','一直在盘上(没离开过)'],['unsure','不确定']],
hand_leave:[['at_event','离盘就在 EVENT 附近(±0.5 s)'],['__pick','离盘在别的时刻 → 点选那一帧'],['never','从未离盘(始终至少一手在盘上)'],['always','一直不在盘上'],['unsure','不确定']],
recall_window:[['yes','是:有手回到盘上并保持 ≥ 1 s'],['no','否:窗口内没有回盘'],['unsure','不确定 / 看不清']]};
function isWin(it){ return it.meta.event==='recall_window'; }
function pfx(it){ return it.meta.event==='hand_return'?'ret_dt=':'off_dt='; }
function evIdx(it){ return isWin(it)?0:Math.max(0,it.frames.findIndex(f=>f.dt===0)); }
function setSync(msg, ok){ const e=document.getElementById('sync'); e.textContent=msg; e.style.color= ok?'#2a7a2a':'#b00020'; }
async function pushLabel(id){ if(!SERVER) return; const r=store[id]||{}; try{ const res=await fetch('/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id, label:r.label, note:r.note, rater:'', ts:r.ts})}); const o=await res.json(); setSync(`已保存到服务器(${o.n_labelled} 条)`, true);}catch(e){ setSync('服务器保存失败,仅本地保存', false);} }
async function pullLabels(){ if(!SERVER) return; try{ const res=await fetch('/labels',{cache:'no-store'}); const o=await res.json(); let n=0; ITEMS.forEach(it=>{ const r=o[it.id]; if(r&&r.label&&!(store[it.id]&&store[it.id].label)){ store[it.id]={label:r.label,note:r.note,ts:r.ts}; n++; } }); save(); setSync('已从服务器同步', true); if(n) render(); }catch(e){ setSync('无法连接服务器(离线模式)', false);} }
function save(){ localStorage.setItem(KEY, JSON.stringify(store)); const done=ITEMS.filter(i=>store[i.id]&&store[i.id].label).length;
const blocks=BLOCKS.map(b=>{ const its=ITEMS.filter(i=>i.meta.block===b); const d=its.filter(i=>store[i.id]&&store[i.id].label).length; return `${b} ${d}/${its.length}`; }).join(' · ');
const ms=MILESTONES.map(([n,t])=>`${n} ${t}${done>=n?' ✓':''}`).join(' · '); const marks=MILESTONES.slice(0,-1).map(([n])=>`<i style="left:${100*n/FULL}%"></i>`).join('');
document.getElementById('prog').innerHTML=`已标 <b>${done}/${FULL}</b> <span class="bar"><div style="width:${100*done/FULL}%"></div>${marks}</span> 里程碑:${ms} · 块:${blocks}`; }
function lab(id){ return (store[id]||{}).label||''; }
function ansText(it,l){ if(!l) return '(未选)'; const o=OPTS[it.meta.event].find(x=>x[0]===l); if(o) return o[1]; if(l.indexOf('=')<0||isWin(it)) return l; const dt=parseFloat(l.split('=')[1]); return (it.meta.event==='hand_return'?'回盘在别的时刻:帧':'离盘在别的时刻:帧')+` dt = ${dt>=0?'+':''}${dt.toFixed(1)} s`; }
function setLabel(id,v,idx,advance){ armed=false; store[id]=Object.assign(store[id]||{}, {label:v, ts:new Date().toISOString()}); save(); pushLabel(id); renderOne(idx); if(advance){ setTimeout(()=>{ if(cur===idx) jump(1); }, 120); } }
function setNote(id,v){ store[id]=Object.assign(store[id]||{}, {note:v}); save(); clearTimeout(window._nt); window._nt=setTimeout(()=>pushLabel(id), 800); }
function arm(idx){ setCur(idx,false); armed=true; const a=document.getElementById('arm_'+idx); if(a) a.textContent='请点选那一帧(小图),或用 , . 把播放器移到该帧后按 Enter'; }
function fmtDt(f,it){ return (it&&isWin(it)) ? `+${f.dt.toFixed(0)} s` : `dt=${f.dt>=0?'+':''}${f.dt.toFixed(1)} s${f.dt===0?' (EVENT)':''}`; }
function plSet(idx,k){ const it=ITEMS[idx]; if(!it.frames.length) return; k=Math.min(Math.max(parseInt(k),0),it.frames.length-1); const f=it.frames[k]; document.getElementById('pl_'+idx).src=f.file; document.getElementById('rg_'+idx).value=k; document.getElementById('pt_'+idx).textContent=fmtDt(f,it); }
function plStop(){ if(_pl){ clearInterval(_pl); _pl=null; } }
function plPlay(idx){ plStop(); const it=ITEMS[idx]; if(!it||!it.frames.length) return; let k=-1; const k0=evIdx(it);
_pl=setInterval(()=>{ k++; if(k>=it.frames.length){ plStop(); plSet(idx,k0); return; } plSet(idx,k); }, isWin(it)?500:100); }
function preload(idx){ const it=ITEMS[idx]; if(!it) return Promise.resolve(); return Promise.all(it.frames.map(f=>new Promise(res=>{ const im=new Image(); im.onload=im.onerror=res; im.src=f.file; }))); }
function autoplay(idx){ const tok=++_tok; Promise.race([preload(idx), new Promise(r=>setTimeout(r,1500))]).then(()=>{ if(tok!==_tok||cur!==idx) return; plPlay(idx); preload(idx+1); }); }
function plStep(d){ const rg=document.getElementById('rg_'+cur); if(rg){ plStop(); plSet(cur, parseInt(rg.value)+d); } }
function pickCur(){ const rg=document.getElementById('rg_'+cur); if(!rg) return; const it=ITEMS[cur]; if(isWin(it)) return; plStop(); setLabel(it.id, pfx(it)+it.frames[parseInt(rg.value)].dt.toFixed(1), cur, true); }
function pickThumb(idx,k){ const it=ITEMS[idx]; setCur(idx,false); plStop(); plSet(idx,k); if(isWin(it)) return; setLabel(it.id, pfx(it)+it.frames[k].dt.toFixed(1), idx, true); }
function renderOne(idx){ const it=ITEMS[idx]; const d=document.getElementById('it_'+idx); if(!d) return; const l=lab(it.id); d.className='item'+(l?' done':'')+(idx===cur?' cur':'');
const win=isWin(it);
const meta=`${it.id} · 第 ${idx+1}/${ITEMS.length} 条 · 块 ${it.meta.block} · ${it.meta.car} · ${it.meta.uid} · ` + (win ? `窗口 t=${it.meta.t0.toFixed(1)} … ${(it.meta.t0+9).toFixed(1)} s` : `t_event=${it.meta.t_event.toFixed(1)} s`);
if(!it.frames.length){ d.innerHTML=`<div class="meta">${meta}</div><i>(无本地视频或无姿态框,无法出图)</i>`; return; }
const k0=evIdx(it);
const q = win ? `10 s 窗口(10 帧,1 s/帧;自动标签认为窗口内无回盘/离盘)。这个窗口里是否有手<b>回到</b>方向盘上并保持 ≥ 1 s?`
: `自动候选 <b>${it.meta.event_cn}</b>${it.meta.regime==='manual'?'(manual 模式)':''} 于 EVENT 帧(dt=0)。视频里${it.meta.event==='hand_return'?'回盘':'离盘'}发生在什么时候?`;
const player=`<div class="player"><img id="pl_${idx}" src="${it.frames[k0].file}"><div><div class="pl"><input type="range" id="rg_${idx}" min="0" max="${it.frames.length-1}" value="${k0}" style="width:320px" oninput="plStop();plSet(${idx},this.value)"> <span id="pt_${idx}">${fmtDt(it.frames[k0],it)}</span></div>
<div class="pl"><button onclick="setCur(${idx},false);plPlay(${idx})">▶ 重播 (空格)</button> ${win?'':`<button onclick="setCur(${idx},false);pickCur()">把当前播放帧选为答案帧 (Enter)</button>`}</div>
<div class="q">${q}</div>
<div>${OPTS[it.meta.event].map(([v,t],k)=> v==='__pick' ? `<button class="${(l.indexOf('_dt=')>=0)?'sel':''}" onclick="arm(${idx})">${k+1}. ${t}</button>` : `<button class="${l===v?'sel':''}" onclick="setCur(${idx},false);setLabel('${it.id}','${v}',${idx},true)">${k+1}. ${t}</button>`).join(' ')}</div>
<div class="ans">当前答案:<b>${ansText(it,l)}</b> <span class="arm" id="arm_${idx}"></span></div>
<div><input type="text" placeholder="备注(可选,如 phone)" value="${(store[it.id]||{}).note||''}" oninput="setNote('${it.id}',this.value)"></div></div></div>`;
const thumbs=`<div class="frames${win?' win':''}">` + it.frames.map((f,k)=>{ const v=win?'':pfx(it)+f.dt.toFixed(1); return `<div class="th"><img loading="lazy" class="${(!win&&l===v)?'sel':(f.dt===0&&!win?'ev':'')}" src="${f.file}" title="${fmtDt(f,it)}${win?'':'(点击 = 选为答案帧)'}" onclick="event.stopPropagation();pickThumb(${idx},${k})"><span>${win?`+${f.dt.toFixed(0)} s`:(f.dt>=0?'+':'')+f.dt.toFixed(1)}</span></div>`; }).join('') + `</div>`;
d.innerHTML = `<div class="meta">${meta}</div>${player}${thumbs}`;
d.onclick=(e)=>{ setCur(idx,false); }; }
function render(){ plStop(); const m=document.getElementById('main'); m.innerHTML=''; ITEMS.forEach((it,idx)=>{ const d=document.createElement('div'); d.id='it_'+idx; m.appendChild(d); renderOne(idx); }); save(); highlight(); }
function highlight(){ document.querySelectorAll('.item').forEach((e,i)=>e.classList.toggle('cur',i===cur)); }
function setCur(idx, scroll){ idx=Math.min(Math.max(idx,0),ITEMS.length-1); if(idx===cur){ if(scroll){ const el=document.getElementById('it_'+cur); if(el){ _lock=Date.now()+900; el.scrollIntoView({behavior:'smooth',block:'start'}); } } return; }
cur=idx; armed=false; plStop(); highlight(); if(scroll){ const el=document.getElementById('it_'+cur); if(el){ _lock=Date.now()+900; el.scrollIntoView({behavior:'smooth',block:'start'}); } } autoplay(cur); }
function jump(d){ setCur((cur<0?0:cur)+d, true); }
let _st=null; window.addEventListener('scroll', ()=>{ if(_st) return; _st=setTimeout(()=>{ _st=null; if(Date.now()<_lock) return; const els=document.querySelectorAll('.item'); let best=-1, bd=1e9; const H=window.innerHeight;
els.forEach((e,i)=>{ const r=e.getBoundingClientRect(); if(r.bottom<140||r.top>H*0.55) return; const dd=Math.abs(r.top-120); if(dd<bd){ bd=dd; best=i; } }); if(best>=0) setCur(best,false); }, 160); });
document.addEventListener('keydown',e=>{ if(e.target.tagName==='INPUT'&&e.target.type==='text') return;
if(e.key==='n') jump(1); else if(e.key==='p') jump(-1); else if(e.key===',') plStep(-1); else if(e.key==='.') plStep(1);
else if(e.key===' '){ e.preventDefault(); if(cur<0) setCur(0,true); else plPlay(cur); } else if(e.key==='Enter'){ if(cur>=0) pickCur(); }
else if(/^[1-5]$/.test(e.key)){ if(cur<0) return; const it=ITEMS[cur]; const o=OPTS[it.meta.event][parseInt(e.key)-1]; if(!o) return; if(o[0]==='__pick') arm(cur); else setLabel(it.id,o[0],cur,true); } });
blockBar(); render(); pullLabels().then(()=>{ let f=ITEMS.findIndex(i=>!lab(i.id)); if(f<0) f=0; setCur(f,true); });
</script></body></html>
"""
# ------------------------------------------------------------------------------------------------------------- append-only enrichment block
def make_event_item(zz, r, reg, ev, blk, pfx, k, order):
"""module-level twin of main()'s event_item (same meta keys and order) for blocks appended after the build."""
u, ta = zz["uid"][r], float(zz["t"][r]); te = round(ta + STEP_S, 6)
m = {"uid": u, "car": u.split("__")[0], "regime": reg, "event": ev, "event_cn": EVENT_CN[ev], "t_anchor": ta, "t_event": te, "block": blk, "order": order,
"row": int(r), "state0": [float(v) for v in zz["S0"][r]], "state_names": zz["state_names"], "dwheel0_c": float(zz["d0"][r]) if np.isfinite(zz["d0"][r]) else None,
"wheel_ref_defined": bool(zz["wd"][r]), "frame_step_s": 0.1, "label_version": "v3"}
return {"id": f"{pfx}_{k:03d}", "task": "GOLD", "meta": m, "frames": []}
def page_zones(items):
"""+-EXCL_S zones of every page item (events by t_event, windows by [t0, t1]) -> [(uid, lo, hi, id)]."""
z = []
for it in items:
m = it["meta"]
if m["event"] == "recall_window": z.append((m["uid"], float(m["t0"]) - EXCL_S, float(m["t1"]) + EXCL_S, it["id"]))
else: z.append((m["uid"], float(m["t_event"]) - EXCL_S, float(m["t_event"]) + EXCL_S, it["id"]))
return z
def append_ranked(out: Path, ranked_path: Path, block: str, pfx: str, workers: int) -> int:
"""APPEND block `block` (ids <pfx>_000 ...) = the ranked engaged hand_return candidates of `ranked_path` (rank_candidates.py output) to the
existing items json + page. Pre-existing items are loaded and re-dumped untouched (same indent / ensure_ascii); every new item must be an
engaged test v3 hand_return first-step event, outside every excluded zone, not within +-EXCL_S of any existing page item, not a duplicate."""
import hashlib
t0 = time.time(); logl = []
def log(s):
print(s, flush=True); logl.append(f"{time.strftime('%H:%M:%S')} {s}")
P = json.load(open(out / "items_gold_engaged.json", encoding="utf-8")); items = P["items"]; n0 = len(items); n_fr0 = sum(len(it["frames"]) for it in items)
if any(it["meta"]["block"] == block for it in items) or any(it["id"].startswith(pfx + "_") for it in items):
print(f"block {block} / prefix {pfx}_ already present -> nothing appended"); return 1
Rk = json.load(open(ranked_path, encoding="utf-8")); ranked = Rk["items"]
log(f"append_ranked start: {len(ranked)} ranked candidates from {ranked_path} (deltaP min {Rk.get('deltaP_min')}), block {block}, ids {pfx}_000.., after {n0} existing items")
iv = excluded_intervals(); zones = page_zones(items); keys = {(it["meta"]["uid"], round(float(it["meta"]["t_event"]), 3)) for it in items if "t_event" in it["meta"]}
z = V3B.load_cell("engaged"); e = z["event_names"].index("hand_return")
new, n_close_new = [], 0
for k, en in enumerate(ranked):
r = int(en["row"]); u, te = str(z["uid"][r]), round(float(z["t"][r]) + STEP_S, 6)
assert u == en["uid"] and abs(te - float(en["t_event"])) < 1e-6, (k, en["uid"], en["t_event"], u, te)
assert z["ok"][r] and z["E1"][r, e] and z["EM1"][r, e], f"row {r} is not an engaged test v3 hand_return first-step candidate"
assert B.VIO.has_video(u), f"no local video for {u}"
assert not hits(u, te, iv), f"{u} t={te} inside an excluded zone: {hits(u, te, iv)}"
near = [x[3] for x in zones[:n0] if x[0] == u and x[1] <= te <= x[2]]
assert not near, f"{u} t={te} within +-{EXCL_S:.0f} s of page item(s) {near}"
assert (u, round(te, 3)) not in keys, f"duplicate (uid, t_event) {u} {te}"
n_close_new += sum(1 for x in zones[n0:] if x[0] == u and x[1] <= te <= x[2])
it = make_event_item(z, r, "engaged", "hand_return", block, pfx, k, n0 + k); it["meta"]["deltaP"] = en["deltaP"]; new.append(it)
keys.add((u, round(te, 3))); zones.append((u, te - EXCL_S, te + EXCL_S, it["id"]))
log(f" {len(new)} new items validated (fresh vs {len(iv)} excluded zones and {n0} page items; {n_close_new} new items lie within +-{EXCL_S:.0f} s of another NEW item — allowed, reported); routes {len({it['meta']['uid'] for it in new})}, "
f"max per route {max(sum(1 for it in new if it['meta']['uid'] == u) for u in {it['meta']['uid'] for it in new})}")
groups = {}
for it in new: groups.setdefault(it["meta"]["uid"], []).append((it["id"], [it["meta"]["t_event"] + dt for dt in DTS], DTS, True))
jobs = sorted(groups.items(), key=lambda g: -len(g[1]))
log(f"extraction: {len(new)} items, {len(new) * len(DTS)} frames expected, on {len(jobs)} routes, {workers} workers -> {IMG_DIR}")
t_ex = time.time(); res = {}
import multiprocessing as mp
with mp.get_context("fork").Pool(workers) as pool:
for k, r in enumerate(pool.imap_unordered(extract_group, jobs)):
res.update(r)
if (k + 1) % 10 == 0 or k + 1 == len(jobs): log(f" routes {k + 1}/{len(jobs)}, items {len(res)}, frames {sum(len(v) for v in res.values())}, {time.time() - t_ex:.0f}s")
for it in new: it["frames"] = res.get(it["id"], [])
n_new_fr = sum(len(it["frames"]) for it in new); wall_ex = time.time() - t_ex
log(f"extraction done: {n_new_fr} frames in {wall_ex:.0f}s ({1000 * wall_ex / max(n_new_fr, 1):.0f} ms/frame); items with 31 frames: {sum(1 for it in new if len(it['frames']) == 31)}/{len(new)}")
items.extend(new)
ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
P["blocks"].append({"block": block, "event": "hand_return", "regime": "engaged", "requested": len(ranked), "n": len(new), "source": str(ranked_path), "selection": Rk.get("selection"), "deltaP_min": Rk.get("deltaP_min")})
P["milestones_actual"].append(len(items)); P["n_items"] = len(items); P["n_frames"] = n_fr0 + n_new_fr
P.setdefault("appended", []).append({"utc": ts, "block": block, "id_prefix": pfx, "n_items": len(new), "n_frames": n_new_fr, "n_items_before": n0, "n_frames_before": n_fr0, "ranked_file": str(ranked_path),
"ranked_sha256": hashlib.sha256(Path(ranked_path).read_bytes()).hexdigest(), "ranked_built_utc": Rk.get("built_utc"), "extraction_wall_s": round(wall_ex, 1), "extraction_workers": workers,
"command": " ".join([sys.executable] + sys.argv), "n_new_within_5s_of_another_new": n_close_new})
P["log"] = P.get("log", []) + logl
json.dump(P, open(out / "items_gold_engaged.json", "w", encoding="utf-8"), indent=1, ensure_ascii=False)
(out / "relabel_gold_engaged.html").write_text(HTML.replace("__ITEMS__", json.dumps(items, ensure_ascii=False)), encoding="utf-8")
log(f"done: {len(items)} items ({n0} + {len(new)}), {P['n_frames']} frames, {time.time() - t0:.0f}s -> {out / 'relabel_gold_engaged.html'}")
return 0
# ------------------------------------------------------------------------------------------------------------- self-checks
def check(out: Path) -> int:
P = json.load(open(out / "items_gold_engaged.json", encoding="utf-8")); items = P["items"]; bad = 0
iv = excluded_intervals()
cnt = {}
for it in items: cnt[it["meta"]["block"]] = cnt.get(it["meta"]["block"], 0) + 1
print("items per block:", cnt, "total", len(items))
order_ok = [it["meta"]["block"] for it in items] == sorted([it["meta"]["block"] for it in items], key=BLOCK_ORDER.index)
print("block order R200->L100->R100->L50->W->MR(->RE):", order_ok); bad += not order_ok
ev_items = [it for it in items if it["meta"]["block"] in ("R200", "L100", "R100", "L50")]; w_items = [it for it in items if it["meta"]["block"] == "W"]; m_items = [it for it in items if it["meta"]["block"] == "MR"]
re_items = [it for it in items if it["meta"]["block"] == "RE"]; all_ev = ev_items + m_items + re_items
ids_ok = (all(it["id"] == f"G_{k:03d}" for k, it in enumerate(ev_items)) and all(it["id"] == f"GW_{k:03d}" for k, it in enumerate(w_items)) and all(it["id"] == f"GM_{k:03d}" for k, it in enumerate(m_items))
and all(it["id"] == f"GE_{k:03d}" for k, it in enumerate(re_items)) and all(it["meta"]["order"] == k for k, it in enumerate(items)))
print("ids G_000.. / GW_000.. / GM_000.. / GE_000.. sequential, order 0..N-1:", ids_ok); bad += not ids_ok
fresh = sum(1 for it in all_ev if hits(it["meta"]["uid"], it["meta"]["t_event"], iv))
freshw = sum(1 for it in w_items for x in iv if x[0] == it["meta"]["uid"] and it["meta"]["t0"] <= x[2] and it["meta"]["t1"] >= x[1])
print("event items inside an excluded +-5 s zone:", fresh, "| windows overlapping one:", freshw); bad += (fresh + freshw) > 0
keys = {(it["meta"]["uid"], round(it["meta"]["t_event"], 3)) for it in all_ev}; print("duplicate (uid, t_event):", len(all_ev) - len(keys)); bad += len(all_ev) != len(keys)
n31 = sum(1 for it in all_ev if len(it["frames"]) == 31); print("event items with 31 frames:", n31, "/", len(all_ev)); bad += n31 != len(all_ev)
n10 = sum(1 for it in w_items if len(it["frames"]) == WIN_FRAMES); print("window items with 10 frames:", n10, "/", len(w_items)); bad += n10 != len(w_items)
grid = all([round(f["dt"], 1) for f in it["frames"]] == DTS for it in all_ev); print("dt grid == -2.0..+1.0 step 0.1 for all event items:", grid); bad += not grid
gridw = all([f["dt"] for f in it["frames"]] == [float(k) for k in range(WIN_FRAMES)] and abs(it["frames"][0]["t"] - it["meta"]["t0"]) < 1e-6 for it in w_items); print("window grid 0..9 s from t0:", gridw); bad += not gridw
ev = all(len(it["frames"]) == 31 and it["frames"][20]["dt"] == 0.0 and abs(it["frames"][20]["t"] - it["meta"]["t_event"]) < 1e-6 for it in all_ev)
print("EVENT frame at index 20 (dt 0, t == t_event) for all event items:", ev); bad += not ev
miss = sum(1 for it in items for f in it["frames"] if not (out / f["file"]).exists()); print("missing jpg files:", miss); bad += miss > 0
pr = sum(1 for it in ev_items + w_items if it["meta"]["regime"] != "engaged") + sum(1 for it in m_items if it["meta"]["regime"] != "manual" or it["meta"]["event"] != "hand_return")
pr += sum(1 for it in re_items if it["meta"]["regime"] != "engaged" or it["meta"]["event"] != "hand_return" or not isinstance(it["meta"].get("deltaP"), (int, float)))
print("regime/event mismatches (engaged blocks must be engaged, MR manual hand_return, RE engaged hand_return with deltaP):", pr); bad += pr > 0
per = {}
for it in ev_items + m_items: k = (it["meta"]["uid"], it["meta"]["regime"], it["meta"]["event"]); per[k] = per.get(k, 0) + 1
print("max event items per route per cell (regime x event type, random blocks):", max(per.values()), "(cap", P["per_route"], ")"); bad += max(per.values()) > P["per_route"]
wper = max([sum(1 for it in w_items if it["meta"]["uid"] == u) for u in {it["meta"]["uid"] for it in w_items}] or [0]); print("max windows per route:", wper); bad += wper > 1
if re_items:
pre = page_zones([it for it in items if it["meta"]["block"] != "RE"])
near = sum(1 for it in re_items for x in pre if x[0] == it["meta"]["uid"] and x[1] <= it["meta"]["t_event"] <= x[2])
print("RE items within +-5 s of a pre-existing page item:", near); bad += near > 0
dp = [it["meta"]["deltaP"] for it in re_items]; print("RE deltaP sorted descending:", dp == sorted(dp, reverse=True), "| min", min(dp), "max", max(dp), "| routes", len({it["meta"]["uid"] for it in re_items}),
"| max per route (no cap)", max(sum(1 for it in re_items if it["meta"]["uid"] == u) for u in {it["meta"]["uid"] for it in re_items})); bad += dp != sorted(dp, reverse=True)
print("RE block is last and milestones_actual ends with the total:", items[-1]["meta"]["block"] == "RE" and P["milestones_actual"][-1] == len(items)); bad += not (items[-1]["meta"]["block"] == "RE" and P["milestones_actual"][-1] == len(items))
html = (out / "relabel_gold_engaged.html").read_text(encoding="utf-8"); js = html.split("<script>")[1]
print("html has tabs / regime-hiding logic:", "setTab" in js or "regime-blind" in js); bad += ("setTab" in js)
print("html key / labels present:", "cwm3_review_gold_engaged_v1" in html, all(s in html for s in ("'at_event'", "'never'", "'always'", "'unsure'", "ret_dt=", "off_dt=", "'yes'", "'no'")))
print("html jump-to-block control present:", "jumpBlock" in js and 'id="blocks"' in html); bad += not ("jumpBlock" in js)
print("CHECK", "OK" if not bad else f"FAILED ({bad})"); return 1 if bad else 0
# ------------------------------------------------------------------------------------------------------------- main
def main(argv=None) -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=str(RT)); ap.add_argument("--n-return", type=int, default=300); ap.add_argument("--n-leave", type=int, default=150)
ap.add_argument("--n-windows", type=int, default=60); ap.add_argument("--n-manual", type=int, default=100)
ap.add_argument("--per-route", type=int, default=3); ap.add_argument("--seed", type=int, default=0); ap.add_argument("--workers", type=int, default=4)
ap.add_argument("--html-only", action="store_true"); ap.add_argument("--check", action="store_true")
ap.add_argument("--append-ranked", default=None, help="rank_candidates.py top list (json) -> APPEND block --append-block with ids --append-prefix_000..; existing items untouched")
ap.add_argument("--append-block", default="RE"); ap.add_argument("--append-prefix", default="GE")
a = ap.parse_args(argv)
out = Path(a.out)
global IMG_DIR
if a.check: return check(out)
if a.append_ranked:
IMG_DIR = out / "img_gold"; IMG_DIR.mkdir(parents=True, exist_ok=True)
return append_ranked(out, Path(a.append_ranked), a.append_block, a.append_prefix, a.workers)
if a.html_only:
items = json.load(open(out / "items_gold_engaged.json", encoding="utf-8"))["items"]
(out / "relabel_gold_engaged.html").write_text(HTML.replace("__ITEMS__", json.dumps(items, ensure_ascii=False)), encoding="utf-8")
print(f"relabel_gold_engaged.html rewritten ({len(items)} items)"); return 0
IMG_DIR = out / "img_gold"; IMG_DIR.mkdir(parents=True, exist_ok=True)
t0 = time.time(); logl = []
def log(s):
print(s, flush=True); logl.append(f"{time.strftime('%H:%M:%S')} {s}")
log(f"build_gold_engaged start seed={a.seed} n_return={a.n_return} n_leave={a.n_leave} n_windows={a.n_windows} n_manual={a.n_manual} per_route={a.per_route} workers={a.workers}")
iv = excluded_intervals()
log(f"exclusion intervals: {len(iv)} (B engaged hand_return {sum(1 for x in iv if x[3].startswith('B:'))}, v3 page {sum(1 for x in iv if x[3].startswith('V3:'))}, pass-3 {sum(1 for x in iv if x[3].startswith('P3:'))}) on {len({x[0] for x in iv})} routes")
z = V3B.load_cell("engaged")
rng = np.random.default_rng(a.seed)
stats, lists = {}, {}
for ev, n in (("hand_return", a.n_return), ("hand_leave", a.n_leave)):
log(f"{ev}:"); rows, st = sample(z, z["event_names"].index(ev), n, a.per_route, rng, iv, log); stats[ev] = st; lists[ev] = rows
log(f" chosen {len(rows)} on {st['n_routes_chosen']} routes (shortfall {st['shortfall']})")
# ---- blocks R200 -> L100 -> R100 -> L50 (random order inside each block: the sampled order is already shuffled) -------------
pos = {"hand_return": 0, "hand_leave": 0}; items = []; n_blk = {}
def event_item(zz, r, reg, ev, blk, pfx, k):
u, ta = zz["uid"][r], float(zz["t"][r]); te = round(ta + STEP_S, 6)
m = {"uid": u, "car": u.split("__")[0], "regime": reg, "event": ev, "event_cn": EVENT_CN[ev], "t_anchor": ta, "t_event": te, "block": blk, "order": len(items),
"row": int(r), "state0": [float(v) for v in zz["S0"][r]], "state_names": zz["state_names"], "dwheel0_c": float(zz["d0"][r]) if np.isfinite(zz["d0"][r]) else None,
"wheel_ref_defined": bool(zz["wd"][r]), "frame_step_s": 0.1, "label_version": "v3"}
return {"id": f"{pfx}_{k:03d}", "task": "GOLD", "meta": m, "frames": []}
for blk, ev, n in BLOCKS:
rows = lists[ev][pos[ev]:pos[ev] + n]; pos[ev] += len(rows)
for r in rows: items.append(event_item(z, r, "engaged", ev, blk, "G", len(items)))
n_blk[blk] = len(rows); log(f"block {blk}: {len(rows)} items (requested {n})")
# ---- block W: 60 engaged recall windows (separate generator seeded like build_relabel_v3) -------------------------------
log("block W (engaged test 10 s recall windows, no v3 hand event inside, not overlapping excluded zones):")
wstarts, wst = sample_windows(z, a.n_windows, np.random.default_rng(a.seed), iv, log); stats["recall_window"] = wst
for k, i in enumerate(wstarts):
u, t0w = z["uid"][i], float(z["t"][i]); j1 = i + int(round(WIN_S / STEP_S)); s0 = z["S0"][i:j1 + 1, 0]; d0 = z["d0"][i:j1 + 1]
m = {"uid": u, "car": u.split("__")[0], "regime": "engaged", "event": "recall_window", "event_cn": EVENT_CN["recall_window"], "block": "W", "order": len(items), "row0": int(i),
"t0": t0w, "t1": t0w + WIN_S, "hands_on_share_v3": float(np.nanmean(s0)) if np.isfinite(s0).any() else None,
"dwheel_median_v3": float(np.nanmedian(d0)) if np.isfinite(d0).any() else None, "frame_step_s": 1.0, "label_version": "v3"}
items.append({"id": f"GW_{k:03d}", "task": "GOLDW", "meta": m, "frames": []})
n_blk["W"] = len(wstarts); log(f"block W: {len(wstarts)} items (requested {a.n_windows})")
# ---- block MR: 100 manual test hand_return candidates -----------------------------------------------------------------
zm = V3B.load_cell("manual")
log("block MR (manual test hand_return):"); mrows, mst = sample(zm, zm["event_names"].index("hand_return"), a.n_manual, a.per_route, rng, iv, log); stats["manual_hand_return"] = mst
for k, r in enumerate(mrows): items.append(event_item(zm, r, "manual", "hand_return", "MR", "GM", k))
n_blk["MR"] = len(mrows); log(f"block MR: {len(mrows)} items on {mst['n_routes_chosen']} routes (requested {a.n_manual}, shortfall {mst['shortfall']})")
# ---- extraction: one video open per route, 4 fork workers ---------------------------------------------------------------
groups = {}
for it in items:
m = it["meta"]
if m["event"] == "recall_window": job = (it["id"], [m["t0"] + k for k in range(WIN_FRAMES)], [float(k) for k in range(WIN_FRAMES)], False)
else: job = (it["id"], [m["t_event"] + dt for dt in DTS], DTS, True)
groups.setdefault(m["uid"], []).append(job)
jobs = sorted(groups.items(), key=lambda g: -len(g[1]))
n_exp = sum(len(j[1]) for g in jobs for j in g[1])
log(f"extraction: {len(items)} items, {n_exp} frames expected, on {len(jobs)} routes, {a.workers} workers -> {IMG_DIR}")
t_ex = time.time(); res = {}
import multiprocessing as mp
with mp.get_context("fork").Pool(a.workers) as pool:
for k, r in enumerate(pool.imap_unordered(extract_group, jobs)):
res.update(r)
if (k + 1) % 10 == 0 or k + 1 == len(jobs):
log(f" routes {k + 1}/{len(jobs)}, items {len(res)}, frames {sum(len(v) for v in res.values())}, {time.time() - t_ex:.0f}s")
for it in items: it["frames"] = res.get(it["id"], [])
n_fr = sum(len(it["frames"]) for it in items); wall_ex = time.time() - t_ex
log(f"extraction done: {n_fr} frames in {wall_ex:.0f}s ({1000 * wall_ex / max(n_fr, 1):.0f} ms/frame)")
ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
rec = {"built_utc": ts, "seed": a.seed, "label_version": "v3", "source": str(V3.relative_to(CWM3)), "split": "test", "regime": "engaged (blocks R200/L100/R100/L50/W) + manual (block MR)", "frame_step_s": 0.1, "dts": DTS,
"window_s": WIN_S, "window_frames": WIN_FRAMES, "per_route": a.per_route, "requested": {"hand_return": a.n_return, "hand_leave": a.n_leave, "recall_window": a.n_windows, "manual_hand_return": a.n_manual},
"blocks": [{"block": b, "event": e, "requested": n, "n": n_blk[b]} for b, e, n in BLOCKS] + [{"block": "W", "event": "recall_window", "requested": a.n_windows, "n": n_blk["W"]}, {"block": "MR", "event": "hand_return", "regime": "manual", "requested": a.n_manual, "n": n_blk["MR"]}],
"milestones_planned": [300, 450, 510, 610], "milestones_actual": [300, sum(n_blk[b] for b in ("R200", "L100", "R100", "L50")), sum(n_blk[b] for b in ("R200", "L100", "R100", "L50", "W")), len(items)],
"exclusion": f"events within +-{EXCL_S:.0f} s (same route) of: items.json task B engaged hand_return items (t_event); every items_relabel_v3.json item (t_event, or [t0, t1] for windows); items_relabel_hr_engaged_01s.json items",
"n_exclusion_intervals": len(iv), "labels": LABELS, "localStorage_key": "cwm3_review_gold_engaged_v1", "sampling_stats": stats, "n_items": len(items), "n_frames": n_fr,
"extraction_wall_s": round(wall_ex, 1), "extraction_workers": a.workers, "command": " ".join([sys.executable] + sys.argv), "code": [str(Path(__file__).relative_to(CWM3)), "tools/review_tool/build_relabel_v3.py", "tools/review_tool/build_review_tool.py"],
"log": logl, "items": items}
json.dump(rec, open(out / "items_gold_engaged.json", "w", encoding="utf-8"), indent=1, ensure_ascii=False)
(out / "relabel_gold_engaged.html").write_text(HTML.replace("__ITEMS__", json.dumps(items, ensure_ascii=False)), encoding="utf-8")
log(f"done: {len(items)} items, {n_fr} frames, extraction {wall_ex:.0f}s, total {time.time() - t0:.0f}s -> {out / 'relabel_gold_engaged.html'}")
return 0
if __name__ == "__main__":
sys.exit(main())

Xet Storage Details

Size:
49.2 kB
·
Xet hash:
dc88ee2e0634d377dea72240e647c49c467b6d6b9dac4c5a26a0d4714dd8597f

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