AsianPlayer/TriDrive / code /data /qc_pose_gate.py
AsianPlayer's picture
download
raw
15.6 kB
"""cwm3/data/qc_pose_gate.py — quality gate of the causal RTMW pose (results/pose_causal) against the old non-causal
YOLO11x-pose cache (s8_pose/cache/pose10hz{,_eng}/<uid>.npz) on the benchmark clips, at SHARED timestamps.
Shared frames: old `t = fi/20` (even frame indices, 10 Hz) vs new `frame_idx = round(t_abs·20)` (5 Hz, every 4th frame)
→ a frame is shared when round(t_old·20) == frame_idx. Both caches are frame-anchored (frame/20), no container fps.
Old → native px (s8_pose/extract_pose.py: decode 640×400, static crop [crop_x0, crop_x0+400] × [0,400], kpts/400):
x_px = (x_n·400 + crop_x0)·W/640 , y_px = y_n·400·H/400 = y_n·H
New → native px: x_px = x_n·W, y_px = y_n·H (HALPE 0–16 = COCO-17 order = the old cache order).
Metrics (COCO joints nose 0, shoulders 5/6, wrists 9/10; a pair is comparable when both conf ≥ 0.3 and finite):
PCK@0.05·H and PCK@0.10·H per joint + pooled, median px error, n pairs;
detection rate old (row not all-NaN) vs new (A: nose ∧ both shoulders conf ≥ 0.3; B: A ∧ both wrists, = Kvalid rule);
hands_on_wheel rule (cwm2/docs/kinematics_targets.md §1 / s8_pose/posefeat.py:84-90): wrist conf ≥ 0.3 else NaN,
per-hand distance to the per-route MEDIAN wrist position, hands_on = min over hands < 0.10 in crop-normalised units.
Implemented on the new coordinates by mapping them INTO the old crop frame (x_cn = (x_px·640/W − crop_x0)/400, y_cn = y_px/H)
so the threshold is identical. Variants: old(whole-route median, exactly posefeat) vs new(median over the benchmark frames
of that route) [the honest comparison]; new(old whole-route medians) [isolates keypoint differences]; old(bench-frame
medians) vs old(whole-route) [isolates the median-estimation effect].
Outputs results/pose_causal/qc_200.json and qc_contact_sheet_20.png (20 local clips, anchor frame: person_box, new body-17,
old body-17 overlaid)."""
import argparse, json, sys, time
from collections import defaultdict
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "common"))
from runmeta import write_run_json
POSE = ROOT / "results" / "pose_causal"; ROI = ROOT / "results" / "roi"
OLD = [Path("/home/USER/s8_pose/cache/pose10hz"), Path("/home/USER/s8_pose/cache/pose10hz_eng")]
CLIP_MAP = Path("/home/USER/cwm2/private/clip_map_full.json")
J = {"nose": 0, "l_shoulder": 5, "r_shoulder": 6, "l_wrist": 9, "r_wrist": 10}
TH, WHEEL_TH, DEC_W, CROP = 0.3, 0.10, 640, 400
BODY_EDGES = [(5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16),(0,5),(0,6)]
def load_old(uid):
"""manual cache has priority (as cwm2 kinematics build); engaged frames not in manual are appended. -> (fi, kpts[N,17,3] f32, crop_x0) or None"""
parts = []
for d in OLD:
p = d / f"{uid}.npz"
if p.exists():
z = np.load(p); parts.append((np.round(z["t"].astype(np.float64) * 20).astype(np.int64), z["kpts"].astype(np.float32), float(z["crop_x0"])))
if not parts: return None
fi, k, x0 = parts[0]
for fi2, k2, x02 in parts[1:]:
if x02 != x0: # shift engaged x into the manual crop frame (same rule as build_kinematics_targets)
k2 = k2.copy(); k2[..., 0] = k2[..., 0] + (x02 - x0) / CROP
new = ~np.isin(fi2, fi); fi = np.concatenate([fi, fi2[new]]); k = np.concatenate([k, k2[new]])
o = np.argsort(fi); return fi[o], k[o], x0
def old_to_px(k, x0, W, H):
px = k.copy(); px[..., 0] = (k[..., 0] * CROP + x0) * W / DEC_W; px[..., 1] = k[..., 1] * H; return px
def new_px_to_cn(px, x0, W, H):
cn = px.copy(); cn[..., 0] = (px[..., 0] * DEC_W / W - x0) / CROP; cn[..., 1] = px[..., 1] / H; return cn
def hands_on(wr_cn, conf, med):
"""wr_cn [N,2(L,R),2] crop-normalised wrists, conf [N,2], med {L:(x,y),R:(x,y)} -> float array (1/0/NaN)."""
xy = wr_cn.copy(); xy[conf < TH] = np.nan
dist = np.stack([np.hypot(xy[:, i, 0] - med[i][0], xy[:, i, 1] - med[i][1]) for i in range(2)], 1)
mn = np.fmin(dist[:, 0], dist[:, 1])
return np.where(np.isnan(mn), np.nan, (mn < WHEEL_TH).astype(np.float32))
def medians(wr_cn, conf):
xy = wr_cn.copy(); xy[conf < TH] = np.nan
return [(np.nanmedian(xy[:, i, 0]), np.nanmedian(xy[:, i, 1])) for i in range(2)]
def agree(a, b):
ok = ~np.isnan(a) & ~np.isnan(b)
return (float((a[ok] == b[ok]).mean()) if ok.any() else None), int(ok.sum())
def main():
ap = argparse.ArgumentParser(); ap.add_argument("--clips", default=str(POSE / "bench_200_clips.txt")); ap.add_argument("--sheet-n", type=int, default=20); a = ap.parse_args()
t_start = time.time()
ids = [l.strip() for l in open(a.clips) if l.strip()]
cm = json.load(open(CLIP_MAP))
per_clip = []; pairs = defaultdict(list); det = defaultdict(int)
route_new = defaultdict(list) # uid -> list of (wr_cn[N,2,2], conf[N,2], old-frame-aligned hands_on arrays ...)
clip_data = {}
for cid in ids:
p = POSE / f"{cid}.npz"
if not p.exists(): per_clip.append(dict(clip_id=cid, status="no_new_npz")); continue
d = np.load(p); meta = cm[cid]; W, H = [int(v) for v in d["native_wh"]]
old = load_old(str(d["uid"]))
if old is None: per_clip.append(dict(clip_id=cid, status="no_old_cache")); continue
fi_old, k_old, x0 = old
pos = {int(f): i for i, f in enumerate(fi_old)}
sh = [(k, pos[int(f)]) for k, f in enumerate(d["frame_idx"]) if int(f) in pos and d["frame_ok"][k]]
if not sh: per_clip.append(dict(clip_id=cid, status="no_shared_frames")); continue
kn, ko = zip(*sh); kn = np.array(kn); ko = np.array(ko)
new = d["kpts"].astype(np.float32)[kn, :17]; new_px = new.copy(); new_px[..., 0] *= W; new_px[..., 1] *= H
oldk = k_old[ko]; old_px = old_to_px(oldk, x0, W, H)
rec = dict(clip_id=cid, status="ok", uid=str(d["uid"]), n_shared=len(sh), video_source=str(d["video_source"]), roi_source=str(d["roi_source"]),
co_occupant_present=bool(d["co_occupant_present"]), W=W, H=H, joints={})
# detection
old_det = np.isfinite(oldk[:, 0, 0]); newA = (new[:, [0, 5, 6], 2] >= TH).all(1) & np.isfinite(new[:, [0, 5, 6], 0]).all(1)
newB = newA & (new[:, [9, 10], 2] >= TH).all(1) & np.isfinite(new[:, [9, 10], 0]).all(1)
oldB = old_det & (oldk[:, [0, 5, 6, 9, 10], 2] >= TH).all(1)
det["old_any"] += int(old_det.sum()); det["new_A_nose_shoulders"] += int(newA.sum()); det["new_B_kvalid"] += int(newB.sum()); det["old_B_kvalid"] += int(oldB.sum()); det["n_shared"] += len(sh)
rec.update(det_old=float(old_det.mean()), det_newA=float(newA.mean()), det_newB=float(newB.mean()), det_oldB=float(oldB.mean()))
# PCK
for name, j in J.items():
ok = np.isfinite(old_px[:, j, 0]) & np.isfinite(new_px[:, j, 0]) & (oldk[:, j, 2] >= TH) & (new[:, j, 2] >= TH)
dpx = np.hypot(*(new_px[ok, j, :2] - old_px[ok, j, :2]).T)
pairs[name].append(dpx / H)
rec["joints"][name] = dict(n=int(ok.sum()), pck05=(float((dpx < 0.05 * H).mean()) if ok.any() else None), med_px=(float(np.median(dpx)) if ok.any() else None))
# hands_on
wr_old = oldk[:, [9, 10], :2]; c_old = oldk[:, [9, 10], 2]
wr_new_cn = new_px_to_cn(new_px[:, [9, 10], :2], x0, W, H); c_new = new[:, [9, 10], 2]
med_route_old = medians(k_old[:, [9, 10], :2], k_old[:, [9, 10], 2]) # whole-route median, exactly posefeat
clip_data[cid] = dict(wr_old=wr_old, c_old=c_old, wr_new_cn=wr_new_cn, c_new=c_new, med_route_old=med_route_old, uid=str(d["uid"]), rec=rec,
kn=kn, new_px=new_px, old_px=old_px, oldk=oldk, new=new, x0=x0)
route_new[str(d["uid"])].append((wr_new_cn, c_new, wr_old, c_old))
per_clip.append(rec)
# route-level medians over the benchmark frames (new and old)
med_bench_new, med_bench_old = {}, {}
for uid, lst in route_new.items():
med_bench_new[uid] = medians(np.concatenate([x[0] for x in lst]), np.concatenate([x[1] for x in lst]))
med_bench_old[uid] = medians(np.concatenate([x[2] for x in lst]), np.concatenate([x[3] for x in lst]))
ho = defaultdict(list)
for cid, c in clip_data.items():
uid = c["uid"]
h_old_route = hands_on(c["wr_old"], c["c_old"], c["med_route_old"])
h_old_bench = hands_on(c["wr_old"], c["c_old"], med_bench_old[uid])
h_new_bench = hands_on(c["wr_new_cn"], c["c_new"], med_bench_new[uid])
h_new_oldmed = hands_on(c["wr_new_cn"], c["c_new"], c["med_route_old"])
for k, v in (("old_route", h_old_route), ("old_bench", h_old_bench), ("new_bench", h_new_bench), ("new_oldmed", h_new_oldmed)): ho[k].append(v)
ag, n = agree(h_new_bench, h_old_route); c["rec"]["hands_on_agree_new_bench_vs_old_route"] = ag; c["rec"]["hands_on_n"] = n
HO = {k: np.concatenate(v) for k, v in ho.items()} if ho else {}
def summarize(name):
v = np.concatenate(pairs[name]) if pairs[name] else np.array([])
return dict(n_pairs=int(v.size), pck05=(float((v < 0.05).mean()) if v.size else None), pck10=(float((v < 0.10).mean()) if v.size else None),
median_err_over_H=(float(np.median(v)) if v.size else None), p90_err_over_H=(float(np.percentile(v, 90)) if v.size else None))
joints = {n: summarize(n) for n in J}
allv = np.concatenate([np.concatenate(pairs[n]) for n in J if pairs[n]]) if any(pairs.values()) else np.array([])
pooled = dict(n_pairs=int(allv.size), pck05=(float((allv < 0.05).mean()) if allv.size else None), pck10=(float((allv < 0.10).mean()) if allv.size else None))
ok_clips = [r for r in per_clip if r["status"] == "ok"]
def strat(key):
out = {}
for val in sorted({str(r[key]) for r in ok_clips}):
rr = [r for r in ok_clips if str(r[key]) == val]
v = [r["joints"][n]["pck05"] for r in rr for n in J if r["joints"][n]["pck05"] is not None]
hh = [r["hands_on_agree_new_bench_vs_old_route"] for r in rr if r.get("hands_on_agree_new_bench_vs_old_route") is not None]
out[val] = dict(n_clips=len(rr), mean_clip_pck05=(float(np.mean(v)) if v else None), mean_clip_hands_on_agreement=(float(np.mean(hh)) if hh else None))
return out
res = dict(utc=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), n_clips_requested=len(ids), n_clips_compared=len(ok_clips),
status_counts={s: sum(r["status"] == s for r in per_clip) for s in sorted({r["status"] for r in per_clip})},
n_shared_frames=int(det["n_shared"]), pck=dict(joints=joints, pooled=pooled, definition="PCK@0.05·H (and 0.10·H) on shared frames where both conf >= 0.3; error normalised by native H"),
detection=dict(rates={k: (v / det["n_shared"] if det["n_shared"] else None) for k, v in det.items() if k != "n_shared"},
diff_newA_minus_old_any=((det["new_A_nose_shoulders"] - det["old_any"]) / det["n_shared"]) if det["n_shared"] else None,
diff_newB_minus_oldB_kvalid=((det["new_B_kvalid"] - det["old_B_kvalid"]) / det["n_shared"]) if det["n_shared"] else None,
definition="old_any: old row has a detection; new_A: nose & both shoulders conf>=0.3; *_B: A & both wrists (Kvalid rule)"),
hands_on=dict(agreement_new_bench_vs_old_route=agree(HO["new_bench"], HO["old_route"]) if HO else None,
agreement_new_oldmed_vs_old_route=agree(HO["new_oldmed"], HO["old_route"]) if HO else None,
agreement_old_bench_vs_old_route=agree(HO["old_bench"], HO["old_route"]) if HO else None,
positive_rate={k: float(np.nanmean(v)) for k, v in HO.items()}, defined_rate={k: float(np.mean(~np.isnan(v))) for k, v in HO.items()},
definition="min over wrists of hypot(wrist - per-route median wrist) < 0.10 crop-normalised (posefeat.py:84-90); new coords mapped into the old crop frame; (agreement, n_frames_both_defined)"),
by_video_source=strat("video_source"), by_roi_source=strat("roi_source"), by_co_occupant=strat("co_occupant_present"), per_clip=per_clip)
(POSE / "qc_200.json").write_text(json.dumps(res, indent=1))
# ---- contact sheet
sheet = None
try:
import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt; import decord, cv2
decord.bridge.set_bridge("native")
cands = [cid for cid in ids if cid in clip_data and clip_data[cid]["rec"]["video_source"] == "local"][:a.sheet_n]
cols = 5; rows = int(np.ceil(len(cands) / cols)); fig, axes = plt.subplots(rows, cols, figsize=(cols * 4.2, rows * 3.0)); axes = np.atleast_1d(axes).ravel()
C_NEW, C_OLD, C_BOX = "#00A6D6", "#F28E2B", "#FFFFFF"
for ax in axes: ax.axis("off")
for ax, cid in zip(axes, cands):
c = clip_data[cid]; meta = cm[cid]; d = np.load(POSE / f"{cid}.npz"); W, H = [int(v) for v in d["native_wh"]]
ks = list(c["kn"]); k_anchor = 25 if 25 in ks else ks[len(ks) // 2]; i = ks.index(k_anchor)
vr = decord.VideoReader(meta["dcam"], ctx=decord.cpu(0), num_threads=4); fr = vr.get_batch([int(d["frame_idx"][k_anchor])]).asnumpy()[0]; del vr
s = 640 / W; im = cv2.resize(fr, (640, int(H * s)), interpolation=cv2.INTER_AREA); ax.imshow(im)
b = d["person_box"] * s; ax.add_patch(plt.Rectangle((b[0], b[1]), b[2] - b[0], b[3] - b[1], fill=False, ec=C_BOX, lw=0.8, ls="--"))
for px, col, conf, lw in ((c["old_px"][i], C_OLD, c["oldk"][i, :, 2], 1.2), (c["new_px"][i], C_NEW, c["new"][i, :, 2], 1.2)):
for a_, b_ in BODY_EDGES:
if conf[a_] >= TH and conf[b_] >= TH and np.isfinite(px[[a_, b_], 0]).all(): ax.plot(px[[a_, b_], 0] * s, px[[a_, b_], 1] * s, "-", color=col, lw=lw)
ok = (conf >= TH) & np.isfinite(px[:, 0]); ax.scatter(px[ok, 0] * s, px[ok, 1] * s, s=6, color=col)
hn = d["kpts"].astype(np.float32)[k_anchor, 94:136]; okh = np.isfinite(hn[:, 0]) & (hn[:, 2] >= TH); ax.scatter(hn[okh, 0] * W * s, hn[okh, 1] * H * s, s=2, color=C_NEW, alpha=0.6)
p5 = [c["rec"]["joints"][n]["pck05"] for n in J if c["rec"]["joints"][n]["pck05"] is not None]
ax.set_title(f"{cid} k={k_anchor} PCK05={np.mean(p5):.2f} {c['rec']['roi_source'][:4]}{' co' if c['rec']['co_occupant_present'] else ''}", fontsize=7)
fig.suptitle(f"qc_pose_gate: new causal RTMW body-17 ({C_NEW} cyan, + hands) vs old YOLO11x pose10hz ({C_OLD} orange); dashed = causal person_box", fontsize=9)
fig.tight_layout(); sheet = POSE / f"qc_contact_sheet_{len(cands)}.png"; fig.savefig(sheet, dpi=110); plt.close(fig)
except Exception as e:
res["contact_sheet_error"] = repr(e)[:300]; (POSE / "qc_200.json").write_text(json.dumps(res, indent=1))
summ = {k: res[k] for k in ("n_clips_compared", "n_shared_frames", "status_counts")}; summ["pck_pooled"] = pooled; summ["pck_joints"] = {n: (joints[n]["pck05"], joints[n]["n_pairs"]) for n in J}
summ["detection"] = res["detection"]["rates"]; summ["hands_on"] = {k: res["hands_on"][k] for k in ("agreement_new_bench_vs_old_route", "agreement_new_oldmed_vs_old_route", "agreement_old_bench_vs_old_route")}; summ["sheet"] = str(sheet)
write_run_json(POSE, "qc_pose_gate", [__file__], [Path(a.clips)], None, t_start, extra={"summary": summ})
print("QC_DONE " + json.dumps(summ, default=str), flush=True)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
15.6 kB
·
Xet hash:
5fe53d9709362efb20a6d198828843507c414f2529040cafe50fb90b6edffd79

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