AsianPlayer/TriDrive / code /tools /review_tool /build_relabel_pass3.py
AsianPlayer's picture
download
raw
9.19 kB
#!/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 &lt; 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.