poolcoach / scripts /broadcast /eval_autocorner.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
7.58 kB
"""Đo gợi ý 4 góc bàn tự động vs corner CHẤM TAY đã lưu (gate A.1, 14/08/2026).
Phép đo đầu tiên của ``poolcoach_cv.autocorner`` — BRIEF 14/08 không đặt
ngưỡng phải "đạt", chỉ yêu cầu số thật: **median + max lệch px per-corner**
và **tỉ lệ ca qua van sanity**. Mốc tự đặt để đọc kết quả: median > ~15px
thì kết luận "gợi ý chưa đáng tin" thay vì ship im lặng.
Nguồn chuẩn (corner tay CÓ THẬT trên đĩa, không dựng lại):
1. ``datasets/bb9_pilot/shot_XX/`` — mỗi cú một ``homography.json``
(``oriented_px``, Danh chấm 11/08 bằng ``click_homography.py``) kèm đúng
frame đã chấm ``frames/00000.jpg``. 10 cú.
2. VOD ``datasets/bb9_pilot/rack_a1/rack_5245_5805.mp4`` — corner tay là bộ
đã dùng THẬT cho run A2b ``c9fda71c`` (chép từ
``scripts/experiments/run_analyzer_a2b_vod.bat``, làm tròn 0.1px của
``shot_01``). Đây là rack KHÁC shot_01 nên bộ này là corner "dùng lại"
chứ không phải chấm riêng — ghi rõ để đừng đọc nhầm nó là chuẩn vàng.
Clip KHÔNG có corner tay (``--clips``) chỉ đóng góp **tỉ lệ qua van**, không
vào thống kê lệch — không có chuẩn thì không có lệch để đo.
Chạy (venv nào cũng được, chỉ cần cv2 + numpy):
python scripts/broadcast/eval_autocorner.py
python scripts/broadcast/eval_autocorner.py --clips "D:/Khoa luan/cu*.mp4"
Ghi CSV ra workspace (nếp bb9_*): ``autocorner_20260814.csv``.
"""
from __future__ import annotations
import argparse
import csv
import glob
import json
import statistics
import sys
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT / "src") not in sys.path:
sys.path.insert(0, str(ROOT / "src"))
from poolcoach_cv.autocorner import (corner_errors_px, # noqa: E402
suggest_corners, suggest_from_video)
from poolcoach_cv.homography import orient_corners # noqa: E402
WORKSPACE = ROOT.parent
PILOT = ROOT / "datasets" / "bb9_pilot"
VOD = PILOT / "rack_a1" / "rack_5245_5805.mp4"
# Chép nguyên từ run_analyzer_a2b_vod.bat (run c9fda71c) — xem docstring.
VOD_CORNERS = [[527.5, 869.7], [1380.1, 864.0], [1232.3, 278.5],
[673.0, 272.8]]
def _rows_pilot() -> list[dict]:
out = []
for d in sorted(PILOT.glob("shot_*")):
hj, frame = d / "homography.json", d / "frames" / "00000.jpg"
if not (hj.exists() and frame.exists()):
continue
truth = json.loads(hj.read_text(encoding="utf-8"))["oriented_px"]
out.append({"case": d.name, "kind": "pilot", "frame": frame,
"truth": truth})
return out
def _read_image(path: Path):
import cv2
img = cv2.imread(str(path), cv2.IMREAD_COLOR)
if img is None:
raise SystemExit(f"không đọc được ảnh {path}")
return img
def _eval_case(row: dict) -> dict:
if row["kind"] == "video":
res = suggest_from_video(row["frame"])
else:
res = suggest_corners(_read_image(row["frame"]))
rec = {"case": row["case"], "kind": row["kind"],
"ok": res["ok"], "reason": res["reason"] or "",
"hue": (res["cloth"] or {}).get("hue"),
"area_frac": (res["cloth"] or {}).get("area_frac"),
"quad_fill": (res["cloth"] or {}).get("quad_fill"),
"touches_border": (res["cloth"] or {}).get("touches_border"),
"cam_h_m": (res["camera"] or {}).get("h_m"),
"cam_f_px": (res["camera"] or {}).get("f_px"),
"asym_pair0": (res["rails"] or {}).get("asym_pair0"),
"asym_pair1": (res["rails"] or {}).get("asym_pair1"),
"swapped": (res["rails"] or {}).get("swapped"),
"corners": json.dumps(res["corners"])}
truth = row.get("truth")
if res["ok"] and truth is not None:
t = orient_corners(np.asarray(truth, dtype=np.float64))
errs, rot = corner_errors_px(res["corners"], t)
# xoay 1 nhịp khớp hơn HẲN ⇒ máy gán nhầm cặp băng ngắn (lỗi nặng
# nhất của tầng này) — bắt riêng, không nuốt vào phép khớp
alt = min(
float(np.linalg.norm(np.roll(np.asarray(res["corners"]), -r,
axis=0) - t, axis=1).mean())
for r in (1, 3))
rec.update(err_px=[round(e, 1) for e in errs],
err_max=round(max(errs), 1),
err_mean=round(sum(errs) / 4, 1), rot=rot,
pairing_suspect=bool(alt < sum(errs) / 4))
return rec
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--clips", default="",
help="glob video KHÔNG có corner tay — chỉ tính tỉ lệ van")
ap.add_argument("--out", default=str(WORKSPACE / "autocorner_20260814.csv"))
a = ap.parse_args()
rows = _rows_pilot()
if VOD.exists():
rows.append({"case": "rack_a1(VOD)", "kind": "video", "frame": VOD,
"truth": VOD_CORNERS})
for p in sorted(glob.glob(a.clips)) if a.clips else []:
rows.append({"case": Path(p).stem, "kind": "video", "frame": Path(p)})
if not rows:
raise SystemExit("không có ca nào để đo — kiểm datasets/bb9_pilot")
recs = [_eval_case(r) for r in rows]
fields = ["case", "kind", "ok", "reason", "err_px", "err_mean", "err_max",
"rot", "pairing_suspect", "cam_h_m", "cam_f_px", "hue",
"area_frac", "quad_fill", "touches_border", "asym_pair0",
"asym_pair1", "swapped", "corners"]
with open(a.out, "w", encoding="utf-8-sig", newline="") as f:
w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
w.writeheader()
for r in recs:
w.writerow(r)
print(f"{'ca':<16}{'van':<5}{'lech px per-corner':<30}"
f"{'max':>7}{'h_m':>8}{'f_px':>8} ghi chu")
print("-" * 92)
for r in recs:
ep = ("" if "err_px" not in r else
" ".join(f"{v:.0f}" for v in r["err_px"]))
note = r["reason"] if not r["ok"] else (
"GAN NHAM CAP BANG NGAN?" if r.get("pairing_suspect") else
("xoay 180 do" if r.get("rot") else ""))
cell = lambda k: ("" if r.get(k) is None else str(r[k])) # noqa: E731
print(f"{r['case']:<16}{'OK' if r['ok'] else 'CHAN':<5}{ep:<30}"
f"{cell('err_max'):>7}{cell('cam_h_m'):>8}"
f"{cell('cam_f_px'):>8} {note}")
n_ok = sum(1 for r in recs if r["ok"])
per_corner = [e for r in recs for e in r.get("err_px", [])]
print("-" * 92)
print(f"qua van sanity: {n_ok}/{len(recs)} ca "
f"({n_ok / len(recs):.0%})")
if per_corner:
n_gt = sum(1 for r in recs if "err_px" in r)
print(f"lech px PER-CORNER tren {n_gt} ca co corner tay "
f"({len(per_corner)} goc): median "
f"{statistics.median(per_corner):.1f}px · trung binh "
f"{statistics.fmean(per_corner):.1f}px · max "
f"{max(per_corner):.1f}px")
worst = max((r for r in recs if "err_max" in r),
key=lambda r: r["err_max"])
print(f"ca lech nhat: {worst['case']} (max {worst['err_max']}px)")
n_pair = sum(1 for r in recs if r.get("pairing_suspect"))
print(f"nghi gan nham cap bang ngan: {n_pair} ca")
print(f"CSV: {a.out}")
return 0
if __name__ == "__main__":
sys.exit(main())