| 1 | """Extract a PCI template from a frame. |
| 2 | |
| 3 | Uses HSV green masking to find the PCI cluster near frame center, then crops |
| 4 | a bounding box around it and saves it as configs/templates/pci_circle.png |
| 5 | so pci_tracker.py will auto-switch to template matching. |
| 6 | |
| 7 | Also saves the HSV mask as logs/pci_mask_<frame>.png so you can see what the |
| 8 | green threshold is picking up. |
| 9 | """ |
| 10 | from __future__ import annotations |
| 11 | |
| 12 | import argparse |
| 13 | import sys |
| 14 | from pathlib import Path |
| 15 | |
| 16 | import cv2 |
| 17 | import numpy as np |
| 18 | from rich.console import Console |
| 19 | |
| 20 | sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
| 21 | from cv._common import load_config |
| 22 | |
| 23 | console = Console() |
| 24 | ROOT = Path(__file__).resolve().parents[1] |
| 25 | TEMPLATE_DIR = ROOT / "configs" / "templates" |
| 26 | LOG_DIR = ROOT / "logs" |
| 27 | |
| 28 | def main() -> int: |
| 29 | ap = argparse.ArgumentParser(description="Extract PCI template from a frame.") |
| 30 | ap.add_argument("frame_path", help="Path to a captured frame PNG.") |
| 31 | ap.add_argument("--pad", type=int, default=10, help="Pixel margin around bbox.") |
| 32 | ap.add_argument("--center-crop-frac", type=float, default=0.6, |
| 33 | help="Restrict search to the central fraction of the frame (0..1) to avoid UI green.") |
| 34 | args = ap.parse_args() |
| 35 | |
| 36 | frame_path = Path(args.frame_path) |
| 37 | frame = cv2.imread(str(frame_path), cv2.IMREAD_COLOR) |
| 38 | if frame is None: |
| 39 | console.print(f"[red]Could not read {frame_path}[/red]") |
| 40 | return 2 |
| 41 | |
| 42 | cfg = load_config() |
| 43 | pci_cfg = cfg["cv"]["pci"] |
| 44 | hsv_low = np.array(pci_cfg["hsv_low"], dtype=np.uint8) |
| 45 | hsv_high = np.array(pci_cfg["hsv_high"], dtype=np.uint8) |
| 46 | |
| 47 | h, w = frame.shape[:2] |
| 48 | hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) |
| 49 | mask = cv2.inRange(hsv, hsv_low, hsv_high) |
| 50 | |
| 51 | cf = float(args.center_crop_frac) |
| 52 | cw = int(w * cf); ch = int(h * cf) |
| 53 | x0 = (w - cw) // 2; y0 = (h - ch) // 2 |
| 54 | center_mask = np.zeros_like(mask) |
| 55 | center_mask[y0:y0 + ch, x0:x0 + cw] = 255 |
| 56 | mask_c = cv2.bitwise_and(mask, center_mask) |
| 57 | |
| 58 | mask_c = cv2.morphologyEx(mask_c, cv2.MORPH_CLOSE, np.ones((15, 15), np.uint8), iterations=2) |
| 59 | mask_c = cv2.morphologyEx(mask_c, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8), iterations=1) |
| 60 | |
| 61 | ys, xs = np.where(mask_c > 0) |
| 62 | if len(xs) == 0: |
| 63 | console.print("[red]No green pixels found in central region - check HSV range or center-crop.[/red]") |
| 64 | mask_out = LOG_DIR / f"pci_mask_{frame_path.stem}.png" |
| 65 | cv2.imwrite(str(mask_out), mask_c) |
| 66 | console.print(f"[yellow]Saved (empty) mask -> {mask_out}[/yellow]") |
| 67 | return 2 |
| 68 | |
| 69 | x_min, x_max = int(xs.min()), int(xs.max()) |
| 70 | y_min, y_max = int(ys.min()), int(ys.max()) |
| 71 | x_min = max(0, x_min - args.pad) |
| 72 | y_min = max(0, y_min - args.pad) |
| 73 | x_max = min(w - 1, x_max + args.pad) |
| 74 | y_max = min(h - 1, y_max + args.pad) |
| 75 | |
| 76 | console.print(f"PCI bbox: x=({x_min}..{x_max}) y=({y_min}..{y_max}) " |
| 77 | f"w={x_max-x_min} h={y_max-y_min} green_px={len(xs)}") |
| 78 | |
| 79 | crop = frame[y_min:y_max + 1, x_min:x_max + 1].copy() |
| 80 | TEMPLATE_DIR.mkdir(parents=True, exist_ok=True) |
| 81 | tpl_out = TEMPLATE_DIR / "pci_circle.png" |
| 82 | cv2.imwrite(str(tpl_out), crop) |
| 83 | console.print(f"[green]Saved template -> {tpl_out} ({crop.shape[1]}x{crop.shape[0]})[/green]") |
| 84 | |
| 85 | ann = frame.copy() |
| 86 | cv2.rectangle(ann, (x_min, y_min), (x_max, y_max), (0, 255, 0), 2) |
| 87 | ann_out = LOG_DIR / f"pci_extract_{frame_path.stem}.png" |
| 88 | cv2.imwrite(str(ann_out), ann) |
| 89 | mask_out = LOG_DIR / f"pci_mask_{frame_path.stem}.png" |
| 90 | cv2.imwrite(str(mask_out), mask_c) |
| 91 | console.print(f"[green]Saved annotated -> {ann_out}[/green]") |
| 92 | console.print(f"[green]Saved mask -> {mask_out}[/green]") |
| 93 | return 0 |
| 94 | |
| 95 | if __name__ == "__main__": |
| 96 | sys.exit(main()) |