Buckets:
| #!/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.