Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Test script to validate play clock reading accuracy. | |
| This script extracts frames with varying play clock values from a video segment, | |
| runs the PlayClockReader on each, and outputs a comparison grid for visual inspection. | |
| Usage: | |
| python scripts/test_play_clock_reader.py | |
| Output: | |
| - Console output with reading results | |
| - Visual grid saved to output/play_clock_test_results.png | |
| """ | |
| import logging | |
| import sys | |
| from pathlib import Path | |
| from typing import List, Tuple, Any | |
| import cv2 | |
| import numpy as np | |
| from detection import DetectScoreBug | |
| from readers import PlayClockReading | |
| from setup import PlayClockRegionExtractor | |
| # Path reference for constants | |
| PROJECT_ROOT = Path(__file__).parent.parent.parent | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") | |
| logger = logging.getLogger(__name__) | |
| # Constants | |
| VIDEO_PATH = PROJECT_ROOT / "full_videos" / "OSU vs Tenn 12.21.24.mkv" | |
| TEMPLATE_PATH = PROJECT_ROOT / "data" / "templates" / "scorebug_template_main.png" | |
| CONFIG_PATH = PROJECT_ROOT / "data" / "config" / "play_clock_region.json" | |
| OUTPUT_DIR = PROJECT_ROOT / "output" | |
| # Test segment: 38:40 to 41:40 (3 minutes = ~5 plays) | |
| START_TIME_SECONDS = 38 * 60 + 40 # 38:40 | |
| END_TIME_SECONDS = 41 * 60 + 40 # 41:40 | |
| SAMPLE_INTERVAL_SECONDS = 0.5 # Sample every 0.5 seconds for detailed analysis | |
| def extract_test_frames( | |
| video_path: Path, detector: DetectScoreBug, start_time: float, end_time: float, interval: float, max_frames: int = 50 | |
| ) -> List[Tuple[float, Any, Tuple[int, int, int, int]]]: | |
| """ | |
| Extract frames from video where scorebug is detected for testing. | |
| Args: | |
| video_path: Path to video file | |
| detector: DetectScoreBug instance | |
| start_time: Start time in seconds | |
| end_time: End time in seconds | |
| interval: Sampling interval in seconds | |
| max_frames: Maximum number of frames to extract | |
| Returns: | |
| List of (timestamp, frame, scorebug_bbox) tuples | |
| """ | |
| cap = cv2.VideoCapture(str(video_path)) | |
| if not cap.isOpened(): | |
| raise ValueError("Could not open video: %s" % video_path) | |
| fps = cap.get(cv2.CAP_PROP_FPS) | |
| logger.info("Video FPS: %.2f", fps) | |
| frames = [] | |
| current_time = start_time | |
| while current_time < end_time and len(frames) < max_frames: | |
| # Seek to current time | |
| frame_number = int(current_time * fps) | |
| cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number) | |
| ret, frame = cap.read() | |
| if not ret: | |
| current_time += interval | |
| continue | |
| # Detect scorebug | |
| detection = detector.detect(frame) | |
| if detection.detected and detection.bbox: | |
| frames.append((current_time, frame, detection.bbox)) | |
| current_time += interval | |
| cap.release() | |
| logger.info("Extracted %d frames with scorebug", len(frames)) | |
| return frames | |
| def run_reading_tests(frames: List[Tuple[float, Any, Tuple[int, int, int, int]]], reader: PlayClockRegionExtractor) -> List[Tuple[float, PlayClockReading, Any]]: | |
| """ | |
| Run play clock reader on all extracted frames. | |
| Args: | |
| frames: List of (timestamp, frame, scorebug_bbox) tuples | |
| reader: PlayClockRegionExtractor instance | |
| Returns: | |
| List of (timestamp, reading, frame) tuples | |
| """ | |
| results = [] | |
| for timestamp, frame, scorebug_bbox in frames: | |
| reading = reader.read(frame, scorebug_bbox) | |
| results.append((timestamp, reading, frame)) | |
| # Log each reading | |
| if reading.detected: | |
| logger.info("%.1fs: Clock = %d (conf: %.0f%%)", timestamp, reading.value, reading.confidence * 100) | |
| else: | |
| logger.warning("%.1fs: Failed to read (raw: '%s')", timestamp, reading.raw_text) | |
| return results | |
| def create_visualization_grid( | |
| results: List[Tuple[float, PlayClockReading, Any]], | |
| reader: PlayClockRegionExtractor, | |
| scorebug_bboxes: List[Tuple[int, int, int, int]], | |
| output_path: Path, | |
| grid_cols: int = 5, | |
| ) -> None: | |
| """ | |
| Create a visual grid of test results for easy inspection. | |
| Args: | |
| results: List of (timestamp, reading, frame) tuples | |
| reader: PlayClockRegionExtractor for visualization | |
| scorebug_bboxes: List of scorebug bounding boxes | |
| output_path: Path to save the visualization | |
| grid_cols: Number of columns in the grid | |
| """ | |
| if not results: | |
| logger.warning("No results to visualize") | |
| return | |
| # Calculate grid dimensions | |
| n_images = len(results) | |
| grid_rows = (n_images + grid_cols - 1) // grid_cols | |
| # Get thumbnail size (scale down for grid) | |
| thumbnail_width = 384 | |
| thumbnail_height = 216 | |
| # Create grid canvas | |
| grid_width = grid_cols * thumbnail_width | |
| grid_height = grid_rows * thumbnail_height | |
| grid_image = np.zeros((grid_height, grid_width, 3), dtype=np.uint8) | |
| # Fill grid with thumbnails | |
| for idx, (timestamp, reading, frame) in enumerate(results): | |
| row = idx // grid_cols | |
| col = idx % grid_cols | |
| # Get corresponding scorebug bbox | |
| scorebug_bbox = scorebug_bboxes[idx] | |
| # Create visualization with play clock region highlighted | |
| vis_frame = reader.visualize_region(frame, scorebug_bbox, reading) | |
| # Add timestamp and reading to frame | |
| status_color = (0, 255, 0) if reading.detected else (0, 0, 255) | |
| cv2.putText(vis_frame, "%.1fs" % timestamp, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, status_color, 2) | |
| if reading.detected: | |
| cv2.putText(vis_frame, "Clock: %d" % reading.value, (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 1.0, status_color, 2) | |
| else: | |
| cv2.putText(vis_frame, "FAILED", (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 1.0, status_color, 2) | |
| # Resize to thumbnail | |
| thumbnail = cv2.resize(vis_frame, (thumbnail_width, thumbnail_height)) | |
| # Place in grid | |
| y1 = row * thumbnail_height | |
| y2 = y1 + thumbnail_height | |
| x1 = col * thumbnail_width | |
| x2 = x1 + thumbnail_width | |
| grid_image[y1:y2, x1:x2] = thumbnail | |
| # Save grid | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| cv2.imwrite(str(output_path), grid_image) | |
| logger.info("Saved visualization grid to %s", output_path) | |
| def print_summary(results: List[Tuple[float, PlayClockReading, Any]]) -> None: | |
| """Print summary statistics of the test results.""" | |
| total = len(results) | |
| detected = sum(1 for _, r, _ in results if r.detected) | |
| failed = total - detected | |
| logger.info("=" * 50) | |
| logger.info("SUMMARY") | |
| logger.info("=" * 50) | |
| logger.info("Total frames tested: %d", total) | |
| logger.info("Successfully read: %d (%.1f%%)", detected, 100 * detected / total if total > 0 else 0) | |
| logger.info("Failed to read: %d (%.1f%%)", failed, 100 * failed / total if total > 0 else 0) | |
| if detected > 0: | |
| confidences = [r.confidence for _, r, _ in results if r.detected] | |
| avg_conf = sum(confidences) / len(confidences) | |
| min_conf = min(confidences) | |
| max_conf = max(confidences) | |
| logger.info("Confidence: avg=%.1f%%, min=%.1f%%, max=%.1f%%", avg_conf * 100, min_conf * 100, max_conf * 100) | |
| values = [r.value for _, r, _ in results if r.detected and r.value is not None] | |
| if values: | |
| logger.info("Clock values: min=%d, max=%d", min(values), max(values)) | |
| # Print sequence of readings for pattern analysis | |
| logger.info("-" * 50) | |
| logger.info("Reading sequence (for pattern analysis):") | |
| sequence = [] | |
| for _, reading, _ in results: | |
| if reading.detected: | |
| sequence.append("%d" % reading.value) | |
| else: | |
| sequence.append("?") | |
| logger.info(" -> ".join(sequence)) | |
| def main(): | |
| """Main entry point for play clock reader testing.""" | |
| logger.info("Play Clock Reader Test") | |
| logger.info("=" * 50) | |
| # Verify paths exist | |
| if not VIDEO_PATH.exists(): | |
| logger.error("Video not found: %s", VIDEO_PATH) | |
| return 1 | |
| if not TEMPLATE_PATH.exists(): | |
| logger.error("Template not found: %s", TEMPLATE_PATH) | |
| return 1 | |
| if not CONFIG_PATH.exists(): | |
| logger.error("Play clock region config not found: %s", CONFIG_PATH) | |
| logger.info("Run identify_play_clock_region.py first to create the config") | |
| return 1 | |
| # Initialize detectors | |
| logger.info("Initializing detectors...") | |
| scorebug_detector = DetectScoreBug(template_path=str(TEMPLATE_PATH)) | |
| play_clock_reader = PlayClockRegionExtractor(region_config_path=str(CONFIG_PATH)) | |
| # Extract test frames | |
| logger.info("Extracting test frames from %.1fs to %.1fs...", START_TIME_SECONDS, END_TIME_SECONDS) | |
| frames = extract_test_frames(VIDEO_PATH, scorebug_detector, START_TIME_SECONDS, END_TIME_SECONDS, SAMPLE_INTERVAL_SECONDS) | |
| if not frames: | |
| logger.error("No frames with scorebug detected!") | |
| return 1 | |
| # Run reading tests | |
| logger.info("Running play clock reader on %d frames...", len(frames)) | |
| results = run_reading_tests(frames, play_clock_reader) | |
| # Print summary | |
| print_summary(results) | |
| # Create visualization | |
| scorebug_bboxes = [bbox for _, _, bbox in frames] | |
| output_path = OUTPUT_DIR / "play_clock_test_results.png" | |
| create_visualization_grid(results, play_clock_reader, scorebug_bboxes, output_path) | |
| logger.info("Test complete!") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |