File size: 5,734 Bytes
78738de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Chọn CONF VẬN HÀNH cho CV worker từ F1-curve any-ball trên val (bàn giao 18).

Chạy trên `poolcoach-cv-env` (cần ultralytics + GPU/CPU như lúc đo anyball):

    python scripts/cv/pick_conf.py --weights "D:/Khoa luan/cv_full_20260805/best.pt" \
        --artifact-dir "D:/Khoa luan/cv_full_20260805"

Cách làm: chạy ĐÚNG validator any-ball của ``eval_anyball.py`` (4 class bi
gộp thành "ball", Dot loại khỏi pred+GT, val-pipeline NMS multi_label=True)
rồi đọc các đường cong F1/P/R theo conf từ ``DetMetrics.curves_results``.
Conf vận hành = argmax của F1 đã làm mượt (cùng ``smooth`` window 0.1 mà
ultralytics dùng khi tự chọn điểm hiển thị P/R).

Neo số (self-check): AP50 any-ball của lần chạy này phải khớp
``anyball.json`` đã nghiệm thu (05/08) trong ±0.005 — lệch là DỪNG, không
ghi số (validator trôi thì mọi con số kéo theo đều vô nghĩa).

LƯU Ý HAI-CÁI-THƯỚC (bàn giao 17): các số P/R ở đây đo bằng VAL-pipeline
(rect dataloader, NMS multi_label=True). Worker deploy chạy PREDICT-pipeline
(multi_label=False) — conf chọn ở đây là điểm vận hành hợp lý, nhưng P/R
tại conf KHÔNG so ngang được với số đo trên predict-pipeline.

BRIEF "Nếu bí": conf ngoài [0.1, 0.9] là nghi có gì sai — script tự DỪNG.
Console ASCII-only (bẫy cp1252). Kết quả ghi ``opconf.json`` vào artifact dir.
"""

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"))
sys.path.insert(0, str(Path(__file__).resolve().parent))  # import eval_anyball

from eval_anyball import run_val  # noqa: E402 — cùng validator, không chép lại


def main() -> None:
    ap = argparse.ArgumentParser(description="Pick operating conf from any-ball F1 curve (val)")
    ap.add_argument("--weights", type=Path, required=True)
    ap.add_argument("--artifact-dir", type=Path, required=True,
                    help="noi ghi opconf.json + doc anyball.json lam neo self-check")
    ap.add_argument("--device", default=None,
                    help="mac dinh: truong env.device cua anyball.json")
    ap.add_argument("--tolerance", type=float, default=0.005,
                    help="nguong lech AP50 vs anyball.json")
    args = ap.parse_args()

    import numpy as np
    from ultralytics.utils.metrics import smooth

    anyball_json = args.artifact_dir / "anyball.json"
    ref = json.loads(anyball_json.read_text(encoding="utf-8"))
    device = args.device if args.device is not None else str(ref["env"]["device"])
    tag = args.weights.resolve().parent.name

    metrics, counters = run_val(args.weights, merged=True, device=device,
                                run_name=f"pickconf_{tag}")

    # --- neo self-check: AP50 lan nay phai bang anyball.json da nghiem thu ---
    ap50 = float(metrics.box.map50)
    ref_ap50 = float(ref["anyball"]["ap50"])
    d = abs(ap50 - ref_ap50)
    print(f"[selfcheck] anyball AP50 lan nay {ap50:.4f} vs {ref_ap50:.4f} "
          f"(anyball.json) -> lech {d:.4f}")
    if d > args.tolerance:
        sys.exit(f"[FAIL] lech {d:.4f} > {args.tolerance} - validator troi, "
                 f"KHONG ghi opconf.json.")

    # --- doc curve F1/P/R theo conf tu curves_results (API cong khai) ---
    curves = {ylab: (np.asarray(x), np.asarray(y))
              for x, y, _xlab, ylab in metrics.curves_results
              if ylab in ("F1", "Precision", "Recall")}
    missing = {"F1", "Precision", "Recall"} - set(curves)
    if missing:
        sys.exit(f"[FAIL] curves_results thieu {missing} - phien ban "
                 f"ultralytics doi API?")
    px, f1 = curves["F1"]
    f1 = f1.mean(axis=0)                     # merged: 1 class, mean = chinh no
    p = curves["Precision"][1].mean(axis=0)
    r = curves["Recall"][1].mean(axis=0)

    # cung cach ultralytics chon diem hien thi: smooth(f1, 0.1) roi argmax
    i = int(smooth(f1, 0.1).argmax())
    conf_op = float(px[i])
    if not (0.1 <= conf_op <= 0.9):
        sys.exit(f"[FAIL] conf argmax F1 = {conf_op:.3f} ngoai [0.1, 0.9] - "
                 f"BRIEF bao nghi co gi sai, DUNG va bao lai Cowork.")

    print(f"[pickconf] conf vanh hanh = {conf_op:.3f} "
          f"(F1 {f1[i]:.4f}, P {p[i]:.4f}, R {r[i]:.4f}) - val-pipeline")
    print(f"[pickconf] {'conf':>6} {'F1':>7} {'P':>7} {'R':>7}   (lan can)")
    for j in range(max(0, i - 100), min(len(px), i + 101), 50):
        mark = " <- chon" if j == i else ""
        print(f"[pickconf] {px[j]:6.3f} {f1[j]:7.4f} {p[j]:7.4f} {r[j]:7.4f}{mark}")

    out = {
        "date": "2026-08-05",
        "weights": str(args.weights),
        "split": "val",
        "method": ("any-ball merged validator cua eval_anyball (val-pipeline, "
                   "NMS multi_label=True); conf = argmax smooth(F1, 0.1); "
                   "P/R doc tai cung index tren p_curve/r_curve. KHONG so "
                   "ngang voi predict-pipeline cua worker deploy."),
        "conf": round(conf_op, 4),
        "f1": round(float(f1[i]), 4),
        "precision": round(float(p[i]), 4),
        "recall": round(float(r[i]), 4),
        "selfcheck": {"ap50": round(ap50, 4), "ref_ap50": ref_ap50,
                      "abs_diff": round(d, 4), "tolerance": args.tolerance},
        "counters": counters,
        "env": {"device": device},
    }
    (args.artifact_dir / "opconf.json").write_text(json.dumps(out, indent=2),
                                                   encoding="utf-8")
    print(f"[artifact] -> {args.artifact_dir / 'opconf.json'}")


if __name__ == "__main__":
    main()