Spaces:
Sleeping
Sleeping
| """Đ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()) | |