poolcoach / scripts /cv /eval_deploy_pr.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
11.4 kB
"""P/R/F1 cua PREDICT-pipeline tai conf VAN HANH — "so luc deploy" (ban giao 19, 05/08/2026).
Do dung nhung gi CV worker deploy (scripts/cv_worker.py) lam voi MOT anh,
tren 49 anh val pix2pockets:
1. resize max-dim 1280 INTER_AREA (chep nguyen block cua ``scan_once`` —
anh val 1920x1080 va 3360x2100 deu vuot tran nen buoc nay CO tac dung),
2. ``model.predict(conf=<opconf.json>, imgsz=640)`` — NMS
``multi_label=False`` (chi class argmax), dung pipeline worker goi,
roi merge any-ball (Black+Cue+Solid+Striped -> "ball"; Dot LOAI khoi ca
pred lan GT — khong phai bi) va match GREEDY voi GT: duyet pred theo conf
GIAM DAN, moi pred lay GT chua-match co IoU cao nhat >= 0.5; pred khong
match = FP, GT thua = FN -> precision / recall / F1 toan split.
MATCHER TU VIET LA DUNG O DAY (khac ban giao 17): lan do can identity voi
val-pipeline nen matcher port bi loai; lan nay muc tieu la do THUOC DEPLOY
— khong co reference nao de identity, chi can khai phuong phap ro rang.
HAI CAI THUOC (bai hoc ban giao 17): so o day la P/R/F1 tai MOT diem conf
tren PREDICT-pipeline. KHONG phai AP, KHONG duoc so voi any-ball AP50
0.9383 (val-pipeline, NMS multi_label=True). Cap so hop le duy nhat:
P/R/F1@conf cua val-pipeline (opconf.json) vs P/R/F1@conf o day — ky vong
predict-pipeline THAP hon (multi_label=False mat cac box "duoc cuu"; Cue
tung lech -0.20 AP giua hai thuoc).
KHONG tai lap cac khau SAU predict cua worker (homography, kep mep, dedupe
tam ban, cue-dung-1): chung can corners + toa do ban, con GT o day la bbox
pixel — do o tang bbox, TRUOC homography.
Guard (BRIEF "Neu bi"): P hoac R < 0.5 -> nghi sai matcher/resize, DUNG
khong ghi so. Neo them: tong GT ball tu file nhan phai khop counters
``gt_kept`` cua opconf.json (563) — lech la parse nhan sai, DUNG.
Chay tren venv CV (can ultralytics; GPU nhu worker):
python scripts/cv/eval_deploy_pr.py --artifact-dir "D:/Khoa luan/cv_full_20260805"
Console ASCII-only (bay cp1252). Ket qua ghi ``opconf_predict.json`` vao
artifact dir, canh ``opconf.json`` de dat hai thuoc canh nhau.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2] # poolcoach-rl/
sys.path.insert(0, str(ROOT / "src"))
DATA_YAML = ROOT / "datasets" / "pix2pockets" / "yolo" / "data.yaml"
BALL_NAMES = ("Black", "Cue", "Solid", "Striped")
EXCLUDE_NAME = "Dot"
IMGSZ = 640 # = cv_worker.IMGSZ — cung imgsz train/val/worker
MAX_DIM = 1280 # = cv_worker.MAX_DIM
IOU_THR = 0.5
def resize_like_worker(img, max_dim: int = MAX_DIM):
"""Chep NGUYEN block resize cua cv_worker.scan_once — doi o do thi doi o day."""
import cv2
h, w = img.shape[:2]
scale = 1.0
if max(h, w) > max_dim:
scale = max_dim / max(h, w)
img = cv2.resize(img, (round(w * scale), round(h * scale)),
interpolation=cv2.INTER_AREA)
return img, scale
def load_gt_boxes(lbl_path: Path, w: int, h: int,
ball_ids: set[int]) -> tuple[list[list[float]], int]:
"""Nhan YOLO txt (class cx cy bw bh, normalized) -> list xyxy pixel tren
anh DA resize (cung he voi pred). Tra (boxes_ball, n_dot_dropped)."""
boxes: list[list[float]] = []
n_dot = 0
if not lbl_path.exists():
return boxes, n_dot
for line in lbl_path.read_text(encoding="utf-8").splitlines():
parts = line.split()
if not parts:
continue
cid = int(parts[0])
if cid not in ball_ids:
n_dot += 1
continue
cx, cy, bw, bh = (float(v) for v in parts[1:5])
boxes.append([(cx - bw / 2) * w, (cy - bh / 2) * h,
(cx + bw / 2) * w, (cy + bh / 2) * h])
return boxes, n_dot
def iou_xyxy(a: list[float], b: list[float]) -> float:
ix1, iy1 = max(a[0], b[0]), max(a[1], b[1])
ix2, iy2 = min(a[2], b[2]), min(a[3], b[3])
iw, ih = max(0.0, ix2 - ix1), max(0.0, iy2 - iy1)
inter = iw * ih
if inter <= 0.0:
return 0.0
area_a = (a[2] - a[0]) * (a[3] - a[1])
area_b = (b[2] - b[0]) * (b[3] - b[1])
return inter / (area_a + area_b - inter)
def greedy_match(preds: list[tuple[float, list[float]]],
gts: list[list[float]],
thr: float = IOU_THR) -> tuple[int, int, int]:
"""Match 1 anh: pred theo conf GIAM DAN, moi pred lay GT chua-match co
IoU cao nhat >= thr. Tra (tp, fp, fn). Mot GT match toi da MOT lan —
double-detection cung mot bi thanh FP, trung thuc voi detector."""
used = [False] * len(gts)
tp = fp = 0
for _conf, pb in sorted(preds, key=lambda t: -t[0]):
best_i, best_iou = -1, thr
for i, gb in enumerate(gts):
if used[i]:
continue
v = iou_xyxy(pb, gb)
if v >= best_iou:
best_i, best_iou = i, v
if best_i >= 0:
used[best_i] = True
tp += 1
else:
fp += 1
fn = used.count(False)
return tp, fp, fn
def main() -> None:
ap = argparse.ArgumentParser(
description="P/R/F1 predict-pipeline (deploy ruler) at operating conf")
ap.add_argument("--artifact-dir", type=Path, required=True,
help="chua opconf.json (doc conf + weights); ghi opconf_predict.json")
ap.add_argument("--weights", type=Path, default=None,
help="mac dinh: truong weights cua opconf.json")
ap.add_argument("--conf", type=float, default=None,
help="mac dinh: truong conf cua opconf.json")
ap.add_argument("--device", default=None,
help="mac dinh: khong truyen (model tu chon, nhu worker)")
args = ap.parse_args()
import torch
import yaml
import cv2
from ultralytics import YOLO
opconf = json.loads((args.artifact_dir / "opconf.json").read_text(encoding="utf-8"))
conf = args.conf if args.conf is not None else float(opconf["conf"])
weights = args.weights or Path(opconf["weights"])
predict_kw = {"device": args.device} if args.device else {}
print(f"[env] torch={torch.__version__} cuda_available={torch.cuda.is_available()}"
f" conf={conf:g} weights={weights}")
data = yaml.safe_load(DATA_YAML.read_text(encoding="utf-8"))
names = {i: n for i, n in enumerate(data["names"])}
name_to_id = {n: i for i, n in names.items()}
missing = [n for n in (*BALL_NAMES, EXCLUDE_NAME) if n not in name_to_id]
if missing:
sys.exit(f"[FAIL] data.yaml thieu class {missing}")
ball_ids = {name_to_id[n] for n in BALL_NAMES}
img_dir = Path(data["path"]) / data["val"]
lbl_dir = Path(str(img_dir).replace("images", "labels"))
imgs = sorted(img_dir.glob("*.jpg"))
if not imgs:
sys.exit(f"[FAIL] khong thay anh val o {img_dir}")
model = YOLO(str(weights))
if dict(model.names) != names:
sys.exit(f"[ERROR] names checkpoint {model.names} khac data.yaml {names}")
tot = {"tp": 0, "fp": 0, "fn": 0, "gt_ball": 0, "gt_dot": 0,
"pred_ball": 0, "pred_dot": 0}
for p in imgs:
img = cv2.imread(str(p))
if img is None:
sys.exit(f"[FAIL] khong doc duoc anh {p}")
img, _scale = resize_like_worker(img)
h, w = img.shape[:2]
res = model.predict(img, conf=conf, imgsz=IMGSZ, verbose=False,
**predict_kw)[0]
preds: list[tuple[float, list[float]]] = []
for b in res.boxes:
if names[int(b.cls)] not in BALL_NAMES:
tot["pred_dot"] += 1
continue
preds.append((float(b.conf), [float(v) for v in b.xyxy[0]]))
gts, n_dot = load_gt_boxes(
lbl_dir / (p.stem + ".txt"), w, h, ball_ids)
tp, fp, fn = greedy_match(preds, gts)
tot["tp"] += tp
tot["fp"] += fp
tot["fn"] += fn
tot["gt_ball"] += len(gts)
tot["gt_dot"] += n_dot
tot["pred_ball"] += len(preds)
# --- neo: tong GT ball tu file nhan phai khop validator da nghiem thu ---
ref_gt = opconf.get("counters", {}).get("gt_kept")
if ref_gt is not None and tot["gt_ball"] != ref_gt:
sys.exit(f"[FAIL] GT ball tu file nhan = {tot['gt_ball']} khac "
f"counters.gt_kept = {ref_gt} cua opconf.json - parse nhan "
f"sai, DUNG khong ghi so.")
prec = tot["tp"] / (tot["tp"] + tot["fp"]) if tot["tp"] + tot["fp"] else 0.0
rec = tot["tp"] / (tot["tp"] + tot["fn"]) if tot["tp"] + tot["fn"] else 0.0
f1 = 2 * prec * rec / (prec + rec) if prec + rec else 0.0
print(f"[deploy-pr] {len(imgs)} anh val; GT ball {tot['gt_ball']} "
f"(Dot loai {tot['gt_dot']}); pred ball {tot['pred_ball']} "
f"(pred Dot bo {tot['pred_dot']})")
print(f"[deploy-pr] TP {tot['tp']} FP {tot['fp']} FN {tot['fn']}")
print(f"[deploy-pr] conf {conf:g}: P {prec:.4f} R {rec:.4f} F1 {f1:.4f}"
f" (predict-pipeline)")
print(f"[deploy-pr] val-pipeline cung conf (opconf.json): "
f"P {opconf['precision']:.4f} R {opconf['recall']:.4f} "
f"F1 {opconf['f1']:.4f} - HAI CAI THUOC, ky vong predict thap hon")
# BRIEF "Neu bi": so vo ly la dau hieu sai matcher/resize -> DUNG, bao lai
if prec < 0.5 or rec < 0.5:
sys.exit(f"[FAIL] P {prec:.4f} / R {rec:.4f} < 0.5 - nghi sai "
f"matcher/resize, DUNG khong ghi opconf_predict.json, "
f"bao lai Cowork (BRIEF).")
out = {
"date": "2026-08-05",
"weights": str(weights),
"split": "val",
"conf": conf,
"method": (
"worker predict-pipeline: resize max-dim 1280 INTER_AREA (chep "
"block scan_once cua cv_worker.py) + model.predict imgsz=640 "
"NMS multi_label=False tai conf van hanh; any-ball merge "
"(Black+Cue+Solid+Striped -> ball), Dot loai ca pred lan GT; "
"greedy match theo conf giam dan, IoU >= 0.5, moi GT match "
"toi da 1 lan. Day la P/R/F1 tai MOT diem conf - KHONG phai "
"AP, KHONG so voi anyball AP50 val-pipeline (hai cai thuoc, "
"ban giao 17). Khong tai lap cac khau sau predict cua worker "
"(homography, dedupe tam ban, cue-dung-1)."),
"predict_pipeline": {"precision": round(prec, 4),
"recall": round(rec, 4),
"f1": round(f1, 4),
"tp": tot["tp"], "fp": tot["fp"], "fn": tot["fn"]},
"val_pipeline_ref": {"precision": opconf["precision"],
"recall": opconf["recall"],
"f1": opconf["f1"],
"source": "opconf.json (cung conf, val-pipeline)"},
"counters": {"n_images": len(imgs),
"gt_ball": tot["gt_ball"],
"gt_dot_excluded": tot["gt_dot"],
"pred_ball": tot["pred_ball"],
"pred_dot_dropped": tot["pred_dot"]},
"env": {"torch": torch.__version__,
"cuda_available": torch.cuda.is_available(),
"device_arg": args.device},
}
out_path = args.artifact_dir / "opconf_predict.json"
out_path.write_text(json.dumps(out, indent=2), encoding="utf-8")
print(f"[artifact] -> {out_path}")
if __name__ == "__main__":
main()