"""Test phần THUẦN của poolcoach_cv.overlay (lát A2 phần 3) — venv app, không cv2/torch: RecordingModel (ghi bbox quanh predict) + _uncomp_xy (nghịch đảo bù độ cao — vẽ ngược lên frame phải ra chỗ bi HIỆN RA). Render thật nghiệm thu bằng smoke trên clip cú 03 rack_a1 (venv CV, 13/08) + gate A2.2 khi rerun trọn VOD. """ from __future__ import annotations import numpy as np import pytest from poolcoach_cv.camera import HeightCompensation from poolcoach_cv.overlay import BALL_BGR, RecordingModel, _uncomp_xy class _Box: def __init__(self, cls_i, conf, xyxy): self.cls = cls_i self.conf = conf self.xyxy = [xyxy] class _Res: names = {0: "Cue", 1: "Solid"} def __init__(self, boxes): self.boxes = boxes class _FakeYolo: def predict(self, imgs, **kw): # mỗi ảnh 1 kết quả — ảnh thứ hai không có det nào out = [_Res([_Box(0, 0.9, (10.0, 20.0, 30.0, 40.0)), _Box(1, 0.8, (50.0, 60.0, 70.0, 80.0))])] out += [_Res([]) for _ in imgs[1:]] return out def test_recording_model_ghi_bbox_theo_thu_tu_frame(): rec = RecordingModel(_FakeYolo()) res = rec.predict(["f0", "f1"], conf=0.3, imgsz=640, verbose=False) assert len(res) == 2 # forward nguyên kết quả assert len(rec.frames) == 2 assert rec.frames[0] == [("Cue", 0.9, (10.0, 20.0, 30.0, 40.0)), ("Solid", 0.8, (50.0, 60.0, 70.0, 80.0))] assert rec.frames[1] == [] rec.predict(["f2"], conf=0.3) assert len(rec.frames) == 3 # batch sau nối tiếp, không đè def test_uncomp_xy_dao_dung_bu_do_cao(): """apply (bù) rồi _uncomp_xy (đảo) phải về đúng toạ độ map gốc — vẽ ngược qua homography mới trúng chỗ bi hiện ra trên frame.""" comp = HeightCompensation(True, cam_xy=(0.6, 1.2), h_m=2.41, f_px=2236.0, ball_r=0.028575) raw = np.array([[0.2, 0.4], [1.1, 2.3]]) compensated = comp.apply(raw) meta = {"on": True, "h_m": 2.41, "cx_m": 0.6, "cy_m": 1.2} assert _uncomp_xy(compensated, meta) == pytest.approx(raw, abs=1e-9) def test_uncomp_xy_off_hay_thieu_camera_giu_nguyen(): pts = np.array([[0.5, 0.5]]) assert _uncomp_xy(pts, {"on": False}) is pts assert _uncomp_xy(pts, None) is pts # on nhưng thiếu cx/cy (kết quả thời kỳ BG31 trước BG32) — không đoán assert _uncomp_xy(pts, {"on": True, "h_m": 2.4}) is pts def test_bang_mau_bi_du_1_den_9(): assert set(BALL_BGR) == set(range(1, 10))