| 1 | """Burst snapshot: capture N frames over T seconds, save each as PNG. |
| 2 | |
| 3 | Useful for catching the ball in flight and getting a clean PCI frame for |
| 4 | tuning / template extraction. Saves to logs/burst_NN_tXXXXXXms.png. |
| 5 | """ |
| 6 | from __future__ import annotations |
| 7 | |
| 8 | import argparse |
| 9 | import shutil |
| 10 | import sys |
| 11 | import time |
| 12 | from pathlib import Path |
| 13 | |
| 14 | import cv2 |
| 15 | from rich.console import Console |
| 16 | |
| 17 | sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
| 18 | from capture.ingest import open_capture |
| 19 | from cv._common import load_config |
| 20 | |
| 21 | console = Console() |
| 22 | OUT_DIR = Path(__file__).resolve().parents[1] / "logs" / "burst" |
| 23 | |
| 24 | def main() -> int: |
| 25 | ap = argparse.ArgumentParser(description="Burst frame capture.") |
| 26 | ap.add_argument("--count", type=int, default=30, help="Total frames to save.") |
| 27 | ap.add_argument("--duration", type=float, default=10.0, help="Spread frames over this many seconds.") |
| 28 | ap.add_argument("--clear", action="store_true", help="Wipe logs/burst before starting.") |
| 29 | args = ap.parse_args() |
| 30 | |
| 31 | if args.clear and OUT_DIR.exists(): |
| 32 | shutil.rmtree(OUT_DIR) |
| 33 | OUT_DIR.mkdir(parents=True, exist_ok=True) |
| 34 | |
| 35 | cfg = load_config() |
| 36 | cap = open_capture(cfg) |
| 37 | if cap is None or not cap.isOpened(): |
| 38 | console.print("[red]Could not open capture card.[/red]") |
| 39 | return 2 |
| 40 | |
| 41 | for _ in range(5): |
| 42 | cap.read() |
| 43 | |
| 44 | interval = args.duration / max(args.count - 1, 1) |
| 45 | t0 = time.perf_counter() |
| 46 | console.print( |
| 47 | f"[bold cyan]Burst: {args.count} frames over {args.duration:.1f}s " |
| 48 | f"(every {interval*1000:.0f}ms) -> {OUT_DIR}[/bold cyan]" |
| 49 | ) |
| 50 | |
| 51 | saved = 0 |
| 52 | for i in range(args.count): |
| 53 | target = t0 + i * interval |
| 54 | |
| 55 | while time.perf_counter() < target - 0.002: |
| 56 | cap.read() |
| 57 | ok, frame = cap.read() |
| 58 | if not ok or frame is None: |
| 59 | continue |
| 60 | elapsed_ms = int((time.perf_counter() - t0) * 1000) |
| 61 | out = OUT_DIR / f"burst_{i:02d}_t{elapsed_ms:05d}ms.png" |
| 62 | cv2.imwrite(str(out), frame) |
| 63 | saved += 1 |
| 64 | if i % 5 == 0: |
| 65 | console.print(f" [{i+1}/{args.count}] t={elapsed_ms}ms -> {out.name}") |
| 66 | |
| 67 | cap.release() |
| 68 | console.print(f"[green]Saved {saved}/{args.count} frames to {OUT_DIR}[/green]") |
| 69 | return 0 |
| 70 | |
| 71 | if __name__ == "__main__": |
| 72 | sys.exit(main()) |