AsianPlayer/TriDrive / code /tools /review_tool /build_relabel_v3.py
AsianPlayer's picture
download
raw
32.5 kB
#!/usr/bin/env python3
"""tools/review_tool/build_relabel_v3.py — owner validation page for the hand-event label v3 (pre-registered docs/decisions.md
2026-09-16T16:45:31Z, "Validation (before any adoption)"): FRESH v3 events on TEST routes with local video, frame-click protocol.
Samples from results/targets_kin_causal_routes_v3/kin_causal_{regime}_test.npz (v3 E_c[:, 0, e] == 1 & EM_c[:, 0, e] & row_ok, i.e. the
event falls in the first future step -> t_event = t_anchor + 0.2 s, exactly as the task-B items of build_review_tool.py):
cell 1 engaged hand_return 34 cell 2 engaged hand_leave 34 cell 3 manual hand_return 34
at most 2 events per route per cell, seed 0, local video required, and EXCLUDING every event within +-5 s (same route) of the 34
already-reviewed engaged hand_return events (results/review_tool/items.json, task B, engaged, hand_return; passes 1-4) -> fresh items.
Frames: dt = -1.0 ... +1.0 s in 0.1 s steps (21 frames) around t_event with the causal driver box (build_review_tool.draw), one seek per
window then sequential reads (identical frames to per-frame seeks; verified in --self-check), into results/review_tool/img_v3/.
Questions: hand_return -> the FIRST frame with a hand on the wheel (ret_dt=<dt>) | never_on | always_on | unsure;
hand_leave -> the FIRST frame with both hands off the wheel (off_dt=<dt>) | never_off | always_off | unsure.
The hand_return items of both regimes are shown in ONE shuffled block without the regime (regime-blind judgement; the regime stays in
the items json for apply_review_v3.py); ids V3_<k>.
RECALL block: 30 random engaged TEST 10 s windows (seed 0, at most 1 per route) with NO v3 hand event (hand_return and hand_leave, first
future step) inside [t0 - 1 s, t0 + 10 s], all anchors of that range present (contiguous 0.2 s grid), row_ok, wheel reference defined
and hands_on state defined (so v3 COULD have fired), not overlapping the +-5 s zones of the 34 reviewed events; 10 frames at 1 Hz
(t0 ... t0 + 9 s); question 'did a hand return to the wheel and stay >= 1 s in this window?' yes | no | unsure; ids V3R_<k>.
The items json records the size of the window population (n_valid_window_starts x 0.2 s) and the number of v3 hand_return events of the
cell so that apply_review_v3.py can turn the window answers into a recall estimate.
Outputs (new files only): results/review_tool/{relabel_v3.html, items_relabel_v3.json, img_v3/*.jpg, build_relabel_v3.log,
run_build_relabel_v3_*.json}. Served by the running serve_review.py on port 8791 (/save, /labels); separate localStorage key; no
inherited rater. Read-only on all data; nothing under results/targets_* or results/runs is touched.
python tools/review_tool/build_relabel_v3.py [--n-per-cell 34] [--n-windows 30] [--seed 0] [--out results/review_tool] [--html-only]
python tools/review_tool/build_relabel_v3.py --self-check # sequential-read vs per-frame-seek equality on one event window
"""
from __future__ import annotations
import argparse, json, 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)); sys.path.insert(0, str(CWM3 / "common"))
import build_review_tool as B # noqa: E402 (VIO, grab, draw, nearest_pose_row, FPS)
from runmeta import write_run_json # noqa: E402
V3 = CWM3 / "results/targets_kin_causal_routes_v3"
ITEMS_B = CWM3 / "results/review_tool/items.json"
DTS = [round(x, 1) for x in np.arange(-2.0, 1.0001, 0.1)] # 31 frames (2026-09-16 owner request: 2 s of pre-context so the leave before a return is visible; was -1.0 … +1.0)
CELLS = (("engaged", "hand_return"), ("engaged", "hand_leave"), ("manual", "hand_return"))
EXCL_S = 5.0 # +-5 s around the 34 reviewed engaged hand_return events
STEP_S = 0.2 # target grid
WIN_S, WIN_PRE_S, WIN_FRAMES = 10.0, 1.0, 10
RESEEK_GAP = 60 # frames: larger gaps inside a window are crossed by a fresh seek (video_io convention)
EVENT_CN = {"hand_return": "手回到方向盘(hand_return)", "hand_leave": "手离开方向盘(hand_leave)"}
LABELS = {"hand_return": "ret_dt=<dt> | never_on | always_on | unsure", "hand_leave": "off_dt=<dt> | never_off | always_off | unsure",
"recall_window": "yes | no | unsure"}
# ------------------------------------------------------------------------------------------------------------- data
def load_cell(reg: str):
z = np.load(V3 / f"kin_causal_{reg}_test.npz", allow_pickle=True)
ev = [str(s) for s in z["event_names"]]
assert ev[:2] == ["hand_return", "hand_leave"], ev
assert str(z["label_version"]) == "v3", z["label_version"]
return {"uid": z["uid"].astype(str), "t": z["t"].astype(np.float64), "E1": z["E_c"][:, 0, :2] == 1, "EM1": z["EM_c"][:, 0, :2].astype(bool),
"ok": z["row_ok"].astype(bool), "wd": z["wheel_ref_defined"].astype(bool), "S0": z["S0_c"].astype(np.float32),
"d0": z["dwheel0_c"].astype(np.float32), "state_names": [str(s) for s in z["state_names"]], "event_names": ev}
def reviewed_events():
"""(uid, t_event, id) of the 34 engaged hand_return task-B items (the four-pass check) + all task-B items (informational flag)."""
items = json.load(open(ITEMS_B, encoding="utf-8"))["items"]
b = [i for i in items if i["task"] == "B"]
rev = [(i["meta"]["uid"], float(i["meta"]["t_event"]), i["id"]) for i in b if i["meta"]["regime"] == "engaged" and i["meta"]["event"] == "hand_return"]
allb = [(i["meta"]["uid"], float(i["meta"]["t_event"]), i["id"], i["meta"]["regime"], i["meta"]["event"]) for i in b]
return rev, allb
def near(uid: str, te: float, ref, tol: float):
return [r[2] for r in ref if r[0] == uid and abs(te - r[1]) <= tol]
def sample_events(z, e_idx: int, n: int, per_route: int, rng, rev, log):
rows = np.where(z["ok"] & z["E1"][:, e_idx] & z["EM1"][:, e_idx])[0]
vid = np.array([B.VIO.has_video(u) for u in z["uid"][rows]], bool)
te = z["t"][rows] + STEP_S
excl = np.zeros(len(rows), bool)
for u, tr, _ in rev:
excl |= (z["uid"][rows] == u) & (np.abs(te - tr) <= EXCL_S)
cand = rows[vid & ~excl]
log(f" candidates: rows {len(rows)} local-video {int(vid.sum())} excluded(+-{EXCL_S:.0f}s of the 34 reviewed) {int((vid & excl).sum())} -> {len(cand)} on {len(np.unique(z['uid'][cand]))} routes")
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))
return chosen, {"n_rows": int(len(rows)), "n_local_video": int(vid.sum()), "n_excluded_reviewed": int((vid & excl).sum()), "n_candidates": int(len(cand)),
"n_candidate_routes": int(len(np.unique(z["uid"][cand])))}
def valid_window_starts(z, rev, exclude_events=(0, 1)):
"""Indices i (anchor = window start t0 = t[i]) such that the anchors of [t0 - WIN_PRE_S, t0 + WIN_S] exist contiguously on the 0.2 s grid
within the same route, are row_ok, have the wheel reference and the hands_on state defined, carry no v3 hand event (first future step) of
the given kinds, and the window does not overlap a +-EXCL_S zone of a reviewed event."""
n_pre, n_post = int(round(WIN_PRE_S / STEP_S)), int(round(WIN_S / STEP_S))
good = z["ok"] & z["wd"] & np.isfinite(z["S0"][:, 0])
for e in exclude_events:
good &= ~(z["E1"][:, e] & z["EM1"][:, e])
out = []
uids = z["uid"]; t = z["t"]
bounds = np.flatnonzero(np.r_[True, uids[1:] != uids[:-1], True])
for a, b in zip(bounds[:-1], bounds[1:]):
tt = t[a:b]; g = good[a:b]; m = b - a
if m < n_pre + n_post + 1: continue
cont = np.isclose(np.diff(tt), STEP_S, atol=1e-6)
bad = np.concatenate([[0], np.cumsum(~g)]); badc = np.concatenate([[0], np.cumsum(~cont)])
i = np.arange(n_pre, m - n_post) # window start indices (relative)
ok_g = (bad[i + n_post + 1] - bad[i - n_pre]) == 0 # all good in [i-n_pre, i+n_post]
ok_c = (badc[i + n_post] - badc[i - n_pre]) == 0 # all steps contiguous in that range
sel = i[ok_g & ok_c]
u = uids[a]
for tr in [r[1] for r in rev if r[0] == u]: # window [t0, t0 + WIN_S] must not intersect [tr - EXCL_S, tr + EXCL_S]
sel = sel[~((tt[sel] <= tr + EXCL_S) & (tt[sel] + WIN_S >= tr - EXCL_S))]
out.extend(int(a + s) for s in sel)
return np.asarray(out, dtype=np.int64)
def sample_windows(z, n: int, rng, rev, log):
starts = valid_window_starts(z, rev)
log(f" recall windows: valid starts {len(starts)} (= {len(starts) * STEP_S / 3600:.2f} h of start positions) on {len(np.unique(z['uid'][starts]))} routes; anchors row_ok {int(z['ok'].sum())}")
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))
return chosen, {"n_valid_window_starts": int(len(starts)), "start_step_s": STEP_S, "window_s": WIN_S, "window_pre_s": WIN_PRE_S,
"n_valid_window_routes": int(len(np.unique(z["uid"][starts]))), "excluded_v3_events_inside": ["hand_return", "hand_leave"]}
# ------------------------------------------------------------------------------------------------------------- frames
def read_frames(cap, fis):
"""{frame_idx: frame} for the sorted frame indices `fis`: one seek (build_review_tool.grab) at the first index, then sequential reads;
a gap > RESEEK_GAP frames is crossed by a fresh grab. Sequential reads return exactly the frames the per-frame seeks would return
(cap.set(POS_FRAMES) lands with the same header-fps conversion for every index of a 2 s window) — checked by --self-check."""
out, cur = {}, None
for fi in fis:
fi = int(fi)
if cur is None or fi <= cur or fi - cur > RESEEK_GAP:
fr = B.grab(cap, fi); cur = fi
else:
while cur < fi - 1:
if not cap.grab(): break
cur += 1
ok, fr = cap.read(); cur = fi
if not ok: fr = B.grab(cap, fi)
if fr is not None: out[fi] = fr
return out
def extract(item, u, times, dts, tag_event, img_dir, prefix, log):
"""times[k] = absolute time of frame k, dts[k] its offset label; writes <prefix>_<k:02d>.jpg with the causal driver box."""
import cv2
boxes = B.VIO.load_driver_boxes(u); vp = B.VIO.video_path(u)
if boxes is None or not vp.exists():
log(f"{item['id']}: no boxes/video -> no frames"); return
fis = [int(round(tt * B.FPS)) for tt in times]
cap = cv2.VideoCapture(str(vp))
try:
frames = read_frames(cap, fis)
finally:
cap.release()
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 (tag_event and dt == 0) else ''}t={tt:.1f}s ({dt:+.1f}s)") if tag_event else f"t={tt:.1f}s (+{dt:.0f}s)"
fn = f"{prefix}_{k:02d}.jpg"; B.draw(fr, box, lab, img_dir / fn)
item["frames"].append({"file": f"img_v3/{fn}", "t": float(tt), "dt": float(dt), "frame_idx": fi})
# ------------------------------------------------------------------------------------------------------------- page
HTML = r"""<!doctype html><html lang="zh"><head><meta charset="utf-8"><title>CockpitWM 标签 v3 核验(回盘 / 离盘 / 召回窗口)</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}
.tab{padding:6px 12px;border-radius:6px;cursor:pointer;border:1px solid #bbb}.tab.active{background:#ddd6e5;border-color:#7a6b8f}
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:4px;flex-wrap:wrap}.frames img{width:min(12.5vw,180px);border:2px solid #ccc;border-radius:3px;cursor:pointer}.frames img.sel{border-color:#d33;box-shadow:0 0 0 2px #d33}
.frames.win img{width:min(18vw,290px)}
.player{display:flex;gap:12px;align-items:flex-start;margin:6px 0}.player img{width:min(40vw,560px);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}
input[type=text]{width:60%;padding:4px}
</style></head><body>
<header><b>CockpitWM 标签 v3 核验</b>
<span class="tab active" data-tab="R" onclick="setTab('R')">回盘 hand_return</span>
<span class="tab" data-tab="L" onclick="setTab('L')">离盘 hand_leave</span>
<span class="tab" data-tab="W" onclick="setTab('W')">召回窗口</span>
<span class="prog" id="prog"></span>
<button onclick="jump(-1)">◀ 上一条 (p)</button><button onclick="jump(1)">下一条 ▶ (n)</button><span class="hint" id="sync">离线模式</span>
<span class="hint">快捷键:, / . 前后一帧,空格播放,Enter = 把当前播放帧选为答案帧,1/2/3 = 三个按钮,n/p 翻页。每次选择自动保存(服务器 + 本浏览器)。</span></header>
<div class="def"><b>v3 标签定义。</b>“手在方向盘上”= 至少一只手握住或放在方向盘(轮缘/轮辐)上;“离盘”= 两手都不在方向盘上。
<b>hand_return(回盘)</b>= 离盘状态持续 ≥ 1 s 之后手回到盘上并保持 ≥ 1 s,自动标签标在回盘的第一帧;<b>hand_leave(离盘)</b>= 在盘上 ≥ 1 s 之后两手都离开并保持 ≥ 1 s,标在离盘的第一帧。
每条事件 31 帧(−2.0 … +1.0 s,0.1 s/帧;前 2 s 是回盘/离盘之前的上下文),<b>EVENT = dt 0</b> = 自动标签的时刻。<br>
<b>回盘题</b>:点选第一张“至少一只手在盘上”的帧;若 31 帧中手<b>从未</b>放到盘上 → “从未回盘”;若<b>每一帧</b>都至少一只手在盘上(根本没离开过)→ “一直在盘上”;看不清 → “不确定”。<br>
<b>离盘题</b>:点选第一张“两手都不在盘上”的帧;若 31 帧中<b>始终</b>至少一只手在盘上 → “从未离盘”;若<b>每一帧</b>两手都不在盘上(早已离开)→ “一直不在盘上”;看不清 → “不确定”。<br>
<b>召回窗口题</b>:10 帧、每帧间隔 1 s(t0 … t0+9 s)。在这个 10 s 窗口里,是否有手<b>回到</b>方向盘上并保持 ≥ 1 s?是 / 否 / 不确定(自动标签认为窗口内没有回盘/离盘事件)。<br>
只看手,不看头;红框只标出驾驶员。不显示驾驭模式和以前各轮答案,请独立判断;备注可写 phone 等。</div>
<main id="main"></main>
<script>
const ITEMS = __ITEMS__;
const KEY = 'cwm3_review_relabel_v3_v1';
let store = JSON.parse(localStorage.getItem(KEY) || '{}'); let tab='R'; let cur = 0; let _pl=null;
const SERVER = location.protocol.startsWith('http');
const BLOCK = {R:'hand_return', L:'hand_leave', W:'recall_window'};
function items(){ return ITEMS.filter(i=>i.meta.block===BLOCK[tab]); }
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 its=items(); const done=its.filter(i=>store[i.id]&&store[i.id].label).length; const all=ITEMS.filter(i=>store[i.id]&&store[i.id].label).length; document.getElementById('prog').textContent=`本块 ${done}/${its.length} 已标 · 总计 ${all}/${ITEMS.length}`; }
function lab(id){ return (store[id]||{}).label||''; }
function opts(it){ const b=it.meta.block; if(b==='hand_return') return [["never_on","31 帧中从未回盘"],['always_on','一直在盘上(没离开过)'],['unsure','不确定']];
if(b==='hand_leave') return [['never_off','31 帧中从未离盘(始终至少一手在盘上)'],['always_off','一直不在盘上(早已离开)'],['unsure','不确定']];
return [['yes','是:有手回到盘上并保持 ≥ 1 s'],['no','否:窗口内没有回盘'],['unsure','不确定 / 看不清']]; }
function ansText(it,l){ if(!l) return '(未选)'; const o=opts(it).find(x=>x[0]===l); if(o) return o[1]; const dt=parseFloat(l.split('=')[1]); return (it.meta.block==='hand_return'?'首次回盘帧':'首次离盘帧')+` dt = ${dt>=0?'+':''}${dt.toFixed(1)} s`; }
function pfx(it){ return it.meta.block==='hand_return'?'ret_dt=':'off_dt='; }
function setLabel(id,v,idx){ store[id]=Object.assign(store[id]||{}, {label:v, ts:new Date().toISOString()}); save(); pushLabel(id); renderOne(idx); }
function setNote(id,v){ store[id]=Object.assign(store[id]||{}, {note:v}); save(); clearTimeout(window._nt); window._nt=setTimeout(()=>pushLabel(id), 800); }
function fmtDt(it,f){ return it.meta.block==='recall_window' ? `+${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]; 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(it,f); }
function plPlay(idx){ if(_pl){ clearInterval(_pl); _pl=null; return; } const it=items()[idx]; let k=parseInt(document.getElementById('rg_'+idx).value); _pl=setInterval(()=>{ k=(k+1)%it.frames.length; plSet(idx,k); }, it.meta.block==='recall_window'?500:150); }
function plStep(d){ const rg=document.getElementById('rg_'+cur); if(rg) plSet(cur, parseInt(rg.value)+d); }
function pickCur(){ const rg=document.getElementById('rg_'+cur); if(!rg) return; const it=items()[cur]; if(it.meta.block==='recall_window') return; setLabel(it.id, pfx(it)+it.frames[parseInt(rg.value)].dt.toFixed(1), cur); }
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 = it.meta.block==='recall_window';
const k0 = win ? 0 : Math.max(0,it.frames.findIndex(f=>f.dt===0));
if(!it.frames.length){ d.innerHTML=`<div class="meta">${it.id} · ${it.meta.uid}</div><i>(无本地视频或无姿态框,无法出图)</i>`; return; }
const pick = win ? '' : ` <button onclick="cur=${idx};pickCur()">把当前帧选为答案帧 (Enter)</button>`;
const player=`<div class="player"><img id="pl_${idx}" src="${it.frames[k0].file}"><div><input type="range" id="rg_${idx}" min="0" max="${it.frames.length-1}" value="${k0}" style="width:320px" oninput="plSet(${idx},this.value)"> <span id="pt_${idx}">${fmtDt(it,it.frames[k0])}</span> <button onclick="plPlay(${idx})">▶ / ⏸</button>${pick}</div></div>`;
const thumbs=`<div class="frames${win?' win':''}">` + it.frames.map((f,k)=>{ const v=win?'':pfx(it)+f.dt.toFixed(1); const on = win ? `cur=${idx};plSet(${idx},${k})` : `cur=${idx};plSet(${idx},${k});setLabel('${it.id}','${v}',${idx})`;
return `<img loading="lazy" class="${(!win&&l===v)?'sel':''}" src="${f.file}" title="${fmtDt(it,f)}${win?'':'(点击 = 选为答案帧)'}" onclick="${on}">`; }).join('') + `</div>`;
let q, meta;
if(it.meta.block==='hand_return'){ q=`第 ${idx+1}/${items().length} 条 · 自动标签 <b>hand_return(手回到方向盘)</b> 于 EVENT 帧(dt=0)。第一张“至少一只手在方向盘上”的帧是?(点小图)`; meta=`${it.id} · ${it.meta.uid} · t_event=${it.meta.t_event.toFixed(1)} s`; }
else if(it.meta.block==='hand_leave'){ q=`第 ${idx+1}/${items().length} 条 · 自动标签 <b>hand_leave(手离开方向盘)</b> 于 EVENT 帧(dt=0)。第一张“两手都不在方向盘上”的帧是?(点小图)`; meta=`${it.id} · ${it.meta.uid} · t_event=${it.meta.t_event.toFixed(1)} s`; }
else { q=`第 ${idx+1}/${items().length} 条 · 10 s 窗口(10 帧,1 s/帧)。这个窗口里是否有手<b>回到</b>方向盘上并保持 ≥ 1 s?`; meta=`${it.id} · ${it.meta.uid} · 窗口 t=${it.meta.t0.toFixed(1)} … ${(it.meta.t0+9).toFixed(1)} s`; }
const btns = opts(it).map(([v,t],k)=>`<button class="${l===v?'sel':''}" onclick="setLabel('${it.id}','${v}',${idx})">${k+1}. ${t}</button>`).join(' ');
d.innerHTML = `<div class="meta">${meta}</div>${player}${thumbs}<div class="q">${q}</div><div>${btns}</div>
<div class="ans">当前答案:<b>${ansText(it,l)}</b></div>
<div><input type="text" placeholder="备注(可选,如 phone)" value="${(store[it.id]||{}).note||''}" oninput="setNote('${it.id}',this.value)"></div>`;
d.onclick=(e)=>{ cur=idx; highlight(); }; }
function render(){ if(_pl){ clearInterval(_pl); _pl=null; } 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 setTab(t){ tab=t; cur=0; document.querySelectorAll('.tab').forEach(e=>e.classList.toggle('active', e.dataset.tab===t)); render(); }
function highlight(){ document.querySelectorAll('.item').forEach((e,i)=>e.classList.toggle('cur',i===cur)); }
function jump(d){ cur=Math.min(Math.max(cur+d,0),items().length-1); highlight(); const el=document.getElementById('it_'+cur); if(el) el.scrollIntoView({behavior:'smooth',block:'start'}); }
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(); plPlay(cur); } else if(e.key==='Enter'){ pickCur(); }
else if(/^[1-3]$/.test(e.key)){ const it=items()[cur]; const o=opts(it)[parseInt(e.key)-1]; if(o){ setLabel(it.id,o[0],cur); } } });
render(); pullLabels();
</script></body></html>
"""
# ------------------------------------------------------------------------------------------------------------- main
def self_check(uid: str, te: float) -> int:
"""sequential reads (read_frames) vs per-frame seeks (B.grab) on one 21-frame window: max abs pixel difference must be 0."""
import cv2
vp = B.VIO.video_path(uid); fis = [int(round((te + dt) * B.FPS)) for dt in DTS]
cap = cv2.VideoCapture(str(vp)); seq = read_frames(cap, fis); cap.release()
cap = cv2.VideoCapture(str(vp)); ref = {fi: B.grab(cap, fi) for fi in fis}; cap.release()
worst = 0; n = 0
for fi in fis:
a, b = seq.get(fi), ref.get(fi)
if a is None or b is None or a.shape != b.shape: print(f"frame {fi}: missing/shape mismatch"); return 1
worst = max(worst, int(np.abs(a.astype(np.int16) - b.astype(np.int16)).max())); n += 1
print(f"self-check {uid[:40]} t_event={te:.1f}: {n} frames, max |seq - seek| = {worst}")
return 0 if worst == 0 else 1
def main(argv=None) -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=str(CWM3 / "results/review_tool"))
ap.add_argument("--n-per-cell", type=int, default=34); ap.add_argument("--n-windows", type=int, default=30)
ap.add_argument("--per-route", type=int, default=2); ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--html-only", action="store_true", help="rewrite relabel_v3.html from the existing items_relabel_v3.json (no extraction)")
ap.add_argument("--self-check", action="store_true", help="sequential-read vs per-frame-seek equality on the first sampled engaged hand_return event")
a = ap.parse_args(argv)
out = Path(a.out); img = out / "img_v3"; img.mkdir(parents=True, exist_ok=True)
if a.html_only:
items = json.load(open(out / "items_relabel_v3.json", encoding="utf-8"))["items"]
(out / "relabel_v3.html").write_text(HTML.replace("__ITEMS__", json.dumps(items, ensure_ascii=False)), encoding="utf-8")
print(f"relabel_v3.html rewritten from items_relabel_v3.json ({len(items)} items)"); return 0
t0 = time.time(); logf = open(out / "build_relabel_v3.log", "a", encoding="utf-8")
def log(s):
print(s, flush=True); logf.write(f"{time.strftime('%H:%M:%S')} {s}\n"); logf.flush()
log(f"build_relabel_v3 start seed={a.seed} n_per_cell={a.n_per_cell} n_windows={a.n_windows} per_route={a.per_route} v3={V3}")
rev, allb = reviewed_events()
log(f"reviewed engaged hand_return events to exclude (+-{EXCL_S:.0f} s): {len(rev)} on {len({r[0] for r in rev})} routes")
rng = np.random.default_rng(a.seed)
cells = {reg: load_cell(reg) for reg in ("engaged", "manual")}
# ---- sample the three event cells (fixed order, one generator) ----------------------------------------------------------
picked, stats = [], {}
for reg, ev in CELLS:
z = cells[reg]; e_idx = z["event_names"].index(ev)
log(f"cell {reg} {ev}:")
rows, st = sample_events(z, e_idx, a.n_per_cell, a.per_route, rng, rev, log)
stats[f"{reg}_{ev}"] = dict(st, n_chosen=len(rows), n_routes_chosen=int(len(np.unique(z["uid"][rows]))))
log(f" chosen {len(rows)} on {stats[f'{reg}_{ev}']['n_routes_chosen']} routes")
for r in rows:
u, ta = z["uid"][r], float(z["t"][r]); te = ta + STEP_S
picked.append({"block": ev, "regime": reg, "event": ev, "cell": f"{reg}_{ev}", "uid": u, "car": u.split("__")[0], "row": int(r),
"t_anchor": ta, "t_event": te, "state0": [float(v) for v in z["S0"][r]], "state_names": z["state_names"],
"dwheel0_c": float(z["d0"][r]) if np.isfinite(z["d0"][r]) else None, "wheel_ref_defined": bool(z["wd"][r]),
"near_task_b_items_5s": near(u, te, [(x[0], x[1], f"{x[2]}:{x[3]}:{x[4]}") for x in allb], EXCL_S)})
# regime-blind hand_return block: shuffle engaged + manual together; hand_leave block after it
order = [i for i, p in enumerate(picked) if p["block"] == "hand_return"]; rng.shuffle(order)
order += [i for i, p in enumerate(picked) if p["block"] == "hand_leave"]
items = []
for k, i in enumerate(order):
m = picked[i]; m["event_cn"] = EVENT_CN[m["event"]]; m["frame_step_s"] = 0.1; m["label_version"] = "v3"
items.append({"id": f"V3_{k:03d}", "task": "V3E", "meta": m, "frames": []})
# ---- recall windows -----------------------------------------------------------------------------------------------------
rng_w = np.random.default_rng(a.seed)
ze = cells["engaged"]
log("recall block (engaged test, 10 s windows, no v3 hand event inside):")
wstarts, wst = sample_windows(ze, a.n_windows, rng_w, rev, log)
n_ret_eng = int((ze["ok"] & ze["E1"][:, 0] & ze["EM1"][:, 0]).sum()); n_lea_eng = int((ze["ok"] & ze["E1"][:, 1] & ze["EM1"][:, 1]).sum())
stats["recall_windows"] = dict(wst, n_chosen=len(wstarts), n_v3_hand_return_engaged_test=n_ret_eng, n_v3_hand_leave_engaged_test=n_lea_eng,
n_anchors_row_ok_engaged_test=int(ze["ok"].sum()), anchor_step_s=STEP_S)
log(f" chosen {len(wstarts)} windows; engaged test v3 hand_return rows {n_ret_eng}, hand_leave rows {n_lea_eng}")
witems = []
for k, i in enumerate(wstarts):
u, t0w = ze["uid"][i], float(ze["t"][i]); j1 = i + int(round(WIN_S / STEP_S))
s0 = ze["S0"][i:j1 + 1, 0]; d0 = ze["d0"][i:j1 + 1]
m = {"block": "recall_window", "regime": "engaged", "cell": "engaged_recall_window", "uid": u, "car": u.split("__")[0], "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", "near_task_b_items_5s": near(u, t0w + 5.0, [(x[0], x[1], f"{x[2]}:{x[3]}:{x[4]}") for x in allb], EXCL_S + 5.0)}
witems.append({"id": f"V3R_{k:02d}", "task": "V3R", "meta": m, "frames": []})
if a.self_check:
m = items[0]["meta"] if items[0]["meta"]["block"] == "hand_return" else items[0]["meta"]
return self_check(m["uid"], m["t_event"])
# ---- extraction ---------------------------------------------------------------------------------------------------------
t_ex = time.time(); n_fr = 0
for n, it in enumerate(items):
m = it["meta"]; te = m["t_event"]
extract(it, m["uid"], [te + dt for dt in DTS], DTS, True, img, it["id"], log); n_fr += len(it["frames"])
if (n + 1) % 10 == 0 or n + 1 == len(items):
log(f"events {n + 1}/{len(items)} items, {n_fr} frames, {time.time() - t_ex:.0f}s")
for n, it in enumerate(witems):
m = it["meta"]; t0w = m["t0"]
extract(it, m["uid"], [t0w + k for k in range(WIN_FRAMES)], list(range(WIN_FRAMES)), False, img, it["id"], log); n_fr += len(it["frames"])
if (n + 1) % 10 == 0 or n + 1 == len(witems):
log(f"windows {n + 1}/{len(witems)} items, {n_fr} frames total, {time.time() - t_ex:.0f}s")
wall_ex = time.time() - t_ex
all_items = items + witems
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", "frame_step_s": 0.1, "dts": DTS,
"window_s": WIN_S, "window_frames": WIN_FRAMES, "exclusion": f"events within +-{EXCL_S:.0f} s of the {len(rev)} reviewed engaged hand_return task-B items; windows not overlapping those zones",
"labels": LABELS, "acceptance": "per cell precision >= 0.80 with Wilson 95 % lower bound >= 0.65 (pre-registration 2026-09-16T16:45:31Z)",
"blocks": {"hand_return": "engaged + manual shuffled together, regime hidden on the page", "hand_leave": "engaged", "recall_window": "engaged"},
"sampling_stats": stats, "n_items": len(all_items), "n_frames": n_fr, "extraction_wall_s": round(wall_ex, 1), "items": all_items}
json.dump(rec, open(out / "items_relabel_v3.json", "w", encoding="utf-8"), indent=1, ensure_ascii=False)
(out / "relabel_v3.html").write_text(HTML.replace("__ITEMS__", json.dumps(all_items, ensure_ascii=False)), encoding="utf-8")
write_run_json(out, "build_relabel_v3", [Path(__file__), Path(B.__file__)], [V3 / "kin_causal_engaged_test.npz", V3 / "kin_causal_manual_test.npz", ITEMS_B],
a.seed, t0, extra={"n_items": len(all_items), "n_frames": n_fr, "extraction_wall_s": round(wall_ex, 1), "sampling_stats": stats,
"outputs": ["relabel_v3.html", "items_relabel_v3.json", "img_v3/", "build_relabel_v3.log"]}, input_listing=False)
log(f"done: {len(items)} event items + {len(witems)} windows = {len(all_items)} items, {n_fr} frames, extraction {wall_ex:.0f}s, total {time.time() - t0:.0f}s -> {out / 'relabel_v3.html'}")
return 0
if __name__ == "__main__":
sys.exit(main())

Xet Storage Details

Size:
32.5 kB
·
Xet hash:
b6395f3a6af5ab5b9631d0d5707db520ecc049bd50a4342af3dcbdf1ddb919b8

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