Buckets:
| #!/usr/bin/env python3 | |
| """tools/review_tool/build_relabel_pass3.py — third labelling pass for the ENGAGED hand_return automatic labels at 0.1 s per frame | |
| (owner request 2026-09-16: "engaged 的 hand return 请你划分 0.1 秒一帧然后让我重新标注, manual 保持第二轮的结果"). | |
| Reads the 34 engaged hand_return items from results/review_tool/items.json, re-extracts 21 frames at dt = -1.0 … +1.0 s (step 0.1 s) around the | |
| event time with the causal driver box (same draw() as the first build) into results/review_tool/img_r3/, and writes | |
| results/review_tool/{relabel_hr_engaged_01s.html, items_relabel_hr_engaged_01s.json} (ids <id>__r3, separate localStorage key, grid + frame player). | |
| Read-only on all data; first/second-pass labels untouched. Served by the running serve_review.py (port 8791). | |
| """ | |
| 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)) | |
| import build_review_tool as B # noqa: E402 | |
| SUFFIX = "__r3" | |
| DTS = [round(x, 1) for x in np.arange(-1.0, 1.0001, 0.1)] | |
| DEF_CN = ("<b>第 3 轮:engaged 的 hand_return(手回到方向盘),0.1 s 一帧,共 21 帧(−1.0 … +1.0 s),EVENT = 第 11 帧(dt = 0)。</b> " | |
| "自动标签认为在 dt = 0 驾驭员的手从<b>离开方向盘</b>变为<b>放回方向盘</b>:dt < 0 应当至少一只手<b>不在</b>方向盘上,dt ≥ 0 起手应当<b>握在/放在</b>方向盘上。" | |
| "只看手,不看头;红框只是标出驾驭员。可用滑块/播放键逐帧看,点小图也会跳到该帧;快捷键 , / . 前后一帧,空格播放。前两轮答案不显示,请独立判断;备注可写 phone 等。") | |
| OPTS_B_NEW = ("B:[['correct','正确:dt<0 手不在盘上,dt≈0(±0.5 s 内)手回到盘上']," | |
| "['wrong','错误:附近没有“手回到盘上”(一直在盘上 / 一直不在盘上 / 握手机或放盘旁却没握盘)']," | |
| "['timing','时间偏差:确有回盘,但比 dt=0 早或晚超过 0.5 s'],['unsure','不确定 / 看不清']]") | |
| PLAYER_JS = r""" | |
| 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=`dt=${f.dt>=0?'+':''}${f.dt.toFixed(1)} s${f.dt===0?' (EVENT)':''}`; } | |
| let _pl=null; | |
| function plPlay(idx){ if(_pl){ clearInterval(_pl); _pl=null; return; } let k=parseInt(document.getElementById('rg_'+idx).value); _pl=setInterval(()=>{ k=(k+1)%items()[idx].frames.length; plSet(idx,k); },150); } | |
| function plStep(d){ const rg=document.getElementById('rg_'+cur); if(rg) plSet(cur, parseInt(rg.value)+d); } | |
| """ | |
| def main(argv=None) -> int: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--out", default=str(CWM3 / "results/review_tool")) | |
| ap.add_argument("--regime", default="engaged"); ap.add_argument("--event", default="hand_return"); ap.add_argument("--html-only", action="store_true") | |
| a = ap.parse_args(argv) | |
| import cv2 | |
| out = Path(a.out); img = out / "img_r3"; img.mkdir(exist_ok=True) | |
| src = json.load(open(out / "items.json", encoding="utf-8"))["items"] | |
| sel = sorted([i for i in src if i["task"] == "B" and i["meta"]["event"] == a.event and i["meta"]["regime"] == a.regime], key=lambda i: i["id"]) | |
| items = [] | |
| for i in sel: | |
| m = dict(i["meta"]); m["pass"] = 3; m["orig_id"] = i["id"]; m["frame_step_s"] = 0.1 | |
| it = {"id": i["id"] + SUFFIX, "task": "B", "meta": m, "frames": []} | |
| u, te = m["uid"], float(m["t_event"]) | |
| if a.html_only: | |
| it["frames"] = [{"file": f"img_r3/{it['id']}_{k:02d}.jpg", "t": te + dt, "dt": dt} for k, dt in enumerate(DTS) if (img / f"{it['id']}_{k:02d}.jpg").exists()] | |
| items.append(it); continue | |
| boxes = B.VIO.load_driver_boxes(u); vp = B.VIO.video_path(u) | |
| if boxes is None or not vp.exists(): | |
| print(f"{i['id']}: no boxes/video -> no frames", flush=True); items.append(it); continue | |
| cap = cv2.VideoCapture(str(vp)) | |
| try: | |
| for k, dt in enumerate(DTS): | |
| tt = te + dt; j = B.nearest_pose_row(boxes, tt); fi = int(round(tt * B.FPS)); fr = B.grab(cap, fi) | |
| if fr is None: continue | |
| 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)" | |
| fn = f"{it['id']}_{k:02d}.jpg"; B.draw(fr, box, lab, img / fn) | |
| it["frames"].append({"file": f"img_r3/{fn}", "t": tt, "dt": dt}) | |
| finally: | |
| cap.release() | |
| items.append(it); print(f"{it['id']}: {len(it['frames'])} frames", flush=True) | |
| ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) | |
| json.dump({"built_utc": ts, "pass": 3, "event": a.event, "regime": a.regime, "frame_step_s": 0.1, "dts": DTS, "source_items": "items.json", "n_items": len(items), "items": items}, | |
| open(out / "items_relabel_hr_engaged_01s.json", "w", encoding="utf-8"), indent=1, ensure_ascii=False) | |
| html = B.HTML | |
| reps = [ | |
| ("const KEY = 'cwm3_review_v1';", "const KEY = 'cwm3_review_relabel_hr_engaged_01s_v1';"), | |
| ("B:[['correct','正确:事件发生在中间帧附近'],['wrong','错误:没有该事件'],['timing','时间偏差 >0.5 s'],['unsure','不确定/看不清']]", OPTS_B_NEW), | |
| ("let tab = 'A', cur = 0;", "let tab = 'B', cur = 0;"), | |
| ("<title>CockpitWM 人工核验</title>", "<title>CockpitWM 复核 3:engaged hand_return 0.1 s</title>"), | |
| (".frames{display:flex;gap:6px;flex-wrap:wrap}.frames img{width:min(30vw,420px);border:1px solid #ccc;border-radius:4px}", | |
| ".frames{display:flex;gap:4px;flex-wrap:wrap}.frames img{width:min(12.5vw,180px);border:1px solid #ccc;border-radius:3px;cursor:pointer}" | |
| ".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}"), | |
| (" <b>CockpitWM 人工核验</b>\n <span class=\"tab active\" data-tab=\"A\" onclick=\"setTab('A')\">A 司机/乘客核验</span>\n <span class=\"tab\" data-tab=\"B\" onclick=\"setTab('B')\">B 自动标签验证</span>", | |
| f" <b>CockpitWM 复核(第 3 轮):engaged hand_return,0.1 s/帧</b>\n <span class=\"tab active\" data-tab=\"B\">{len(items)} 条</span>"), | |
| (" if(r.rater && !store._rater) store._rater=r.rater;", ""), | |
| ("<main id=\"main\"></main>", f"<div class=\"item\" style=\"margin:12px 16px;max-width:1500px;background:#fff8e1;border-color:#e0b400\">{DEF_CN}</div>\n<main id=\"main\"></main>"), | |
| ("`自动标签:<b>${it.meta.event_cn}</b>,标注时刻 = 中间帧(EVENT,t=${it.meta.t_event.toFixed(1)} s,${it.meta.regime})。这个事件是否真实发生?`", | |
| "`第 ${idx+1}/${items().length} 条(<b>${it.meta.regime}</b>)· 自动标签:<b>手回到方向盘</b> 于 EVENT 帧(t=${it.meta.t_event.toFixed(1)} s,dt=0)。dt<0 手不在盘上、dt≥0 手在盘上吗?`"), | |
| ("const frames = it.frames.length ? it.frames.map(f=>`<img loading=\"lazy\" src=\"${f.file}\" title=\"t=${f.t.toFixed(1)}s\">`).join('') : '<i>(无本地视频或无姿态框,无法出图)</i>';", | |
| "const k0 = Math.max(0, it.frames.findIndex(f=>f.dt===0));\n" | |
| " const frames = it.frames.length ? `<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}\">dt=+0.0 s (EVENT)</span> <button onclick=\"plPlay(${idx})\">▶ / ⏸</button></div></div>`" | |
| " + `<div class=\"frames\">` + it.frames.map((f,k)=>`<img loading=\"lazy\" src=\"${f.file}\" title=\"dt=${f.dt.toFixed(1)}s\" onclick=\"plSet(${idx},${k})\">`).join('') + `</div>` : '<i>(无本地视频或无姿态框,无法出图)</i>';"), | |
| ("d.innerHTML = `<div class=\"meta\">${it.id} · ${meta}</div><div class=\"frames\">${frames}</div>", "d.innerHTML = `<div class=\"meta\">${it.id} · ${meta}</div>${frames}"), | |
| (" if(e.key==='n') jump(1); else if(e.key==='p') jump(-1);", " 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); }"), | |
| ("render(); pullLabels();", PLAYER_JS + "render(); pullLabels();"), | |
| ] | |
| for old, new in reps: | |
| assert old in html, f"template anchor not found: {old[:70]}" | |
| html = html.replace(old, new) | |
| html = html.replace("__ITEMS__", json.dumps(items, ensure_ascii=False)) | |
| (out / "relabel_hr_engaged_01s.html").write_text(html, encoding="utf-8") | |
| n_fr = sum(len(i["frames"]) for i in items) | |
| print(f"pass-3 page built: {len(items)} items, {n_fr} frames -> {out / 'relabel_hr_engaged_01s.html'}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |
Xet Storage Details
- Size:
- 9.19 kB
- Xet hash:
- 5b8135f1e01c624ff6042d188c7c56709a70733a019bc9586c1481c9d4a8ad60
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.