| 1 | """One-shot detection snapshot. |
| 2 | |
| 3 | Opens the capture card directly (bypasses ZMQ), grabs one frame, runs the |
| 4 | ball and PCI detectors, and saves an annotated PNG to logs/snapshot.png. |
| 5 | Useful for eyeballing what the current HSV ranges actually pick up on |
| 6 | whatever screen the Xbox is showing right now. |
| 7 | """ |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | import sys |
| 11 | import time |
| 12 | from pathlib import Path |
| 13 | |
| 14 | import cv2 |
| 15 | import numpy as np |
| 16 | from rich.console import Console |
| 17 | |
| 18 | sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
| 19 | from capture.ingest import open_capture |
| 20 | from cv._common import load_config |
| 21 | from cv.ball_tracker import detect_ball |
| 22 | from cv.pci_tracker import detect_by_hsv as pci_detect |
| 23 | |
| 24 | console = Console() |
| 25 | OUT_DIR = Path(__file__).resolve().parents[1] / "logs" |
| 26 | |
| 27 | def main() -> int: |
| 28 | OUT_DIR.mkdir(parents=True, exist_ok=True) |
| 29 | cfg = load_config() |
| 30 | cap = open_capture(cfg) |
| 31 | if cap is None or not cap.isOpened(): |
| 32 | console.print("[red]Could not open capture card.[/red]") |
| 33 | return 2 |
| 34 | |
| 35 | for _ in range(5): |
| 36 | cap.read() |
| 37 | time.sleep(0.02) |
| 38 | |
| 39 | ok, frame = cap.read() |
| 40 | cap.release() |
| 41 | if not ok or frame is None: |
| 42 | console.print("[red]Capture returned no frame.[/red]") |
| 43 | return 2 |
| 44 | |
| 45 | raw_out = OUT_DIR / "snapshot_raw.png" |
| 46 | cv2.imwrite(str(raw_out), frame) |
| 47 | console.print(f"[green]Saved raw -> {raw_out}[/green]") |
| 48 | |
| 49 | overlay = frame.copy() |
| 50 | h, w = frame.shape[:2] |
| 51 | plate_y = int(h * float(cfg["cv"].get("plate_y_frac", 0.72))) |
| 52 | cv2.line(overlay, (0, plate_y), (w, plate_y), (255, 255, 255), 1, cv2.LINE_AA) |
| 53 | |
| 54 | ball = detect_ball(frame, cfg) |
| 55 | if ball is not None: |
| 56 | cv2.circle(overlay, (int(ball.x), int(ball.y)), max(int(ball.r), 4), (0, 255, 255), 2) |
| 57 | cv2.putText( |
| 58 | overlay, |
| 59 | f"ball r={ball.r:.0f} s={ball.score:.2f}", |
| 60 | (int(ball.x) + 10, int(ball.y) - 10), |
| 61 | cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2, cv2.LINE_AA, |
| 62 | ) |
| 63 | console.print(f"[bold]ball:[/bold] x={ball.x:.0f} y={ball.y:.0f} r={ball.r:.1f} score={ball.score:.2f}") |
| 64 | else: |
| 65 | console.print("[yellow]ball: none[/yellow]") |
| 66 | |
| 67 | pci = pci_detect(frame, cfg["cv"]["pci"]) |
| 68 | if pci is not None: |
| 69 | cv2.circle(overlay, (int(pci["x"]), int(pci["y"])), int(pci["r"]), (0, 255, 0), 2) |
| 70 | cv2.putText( |
| 71 | overlay, |
| 72 | f"pci r={pci['r']:.0f}", |
| 73 | (int(pci["x"]) + 10, int(pci["y"]) + 20), |
| 74 | cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA, |
| 75 | ) |
| 76 | console.print(f"[bold]pci:[/bold] x={pci['x']:.0f} y={pci['y']:.0f} r={pci['r']:.1f} score={pci['score']:.2f}") |
| 77 | else: |
| 78 | console.print("[yellow]pci: none[/yellow]") |
| 79 | |
| 80 | out = OUT_DIR / "snapshot.png" |
| 81 | cv2.imwrite(str(out), overlay) |
| 82 | console.print(f"[green]Saved -> {out}[/green]") |
| 83 | return 0 |
| 84 | |
| 85 | if __name__ == "__main__": |
| 86 | sys.exit(main()) |