""" Test the padded region approach for shift-invariant play clock reading. Compares clock detection rates with different padding values to determine the optimal padding for handling translational shifts in the broadcast. Usage: python scripts/test_padded_playclock.py """ import json import logging import sys from pathlib import Path from typing import Any, Dict, List, Tuple import cv2 # Add src to path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from readers import ReadPlayClock from setup import DigitTemplateLibrary from detection.scorebug import DetectScoreBug logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) def seconds_to_timestamp(seconds: float) -> str: """Convert seconds to timestamp string (H:MM:SS).""" hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) if hours > 0: return f"{hours}:{minutes:02d}:{secs:02d}" return f"{minutes}:{secs:02d}" def load_oregon_config() -> Dict[str, Any]: """Load the saved config for Oregon video.""" config_path = Path("output/OSU_vs_Oregon_01_01_25_config.json") with open(config_path, "r") as f: return json.load(f) def test_padding_values( video_path: str, playclock_coords: Tuple[int, int, int, int], scorebug_coords: Tuple[int, int, int, int], template_dir: str, template_path: str, padding_values: List[int], start_time: float, end_time: float, sample_interval: float = 5.0, ) -> Dict[int, Dict[str, Any]]: """ Test different padding values and compare detection rates. Args: video_path: Path to video file playclock_coords: Absolute play clock coordinates (x, y, w, h) scorebug_coords: Scorebug coordinates (x, y, w, h) template_dir: Path to digit templates template_path: Path to scorebug template padding_values: List of padding values to test start_time: Start time in seconds end_time: End time in seconds sample_interval: Seconds between samples Returns: Dictionary mapping padding value to results """ # Load template library logger.info("Loading template library from %s", template_dir) library = DigitTemplateLibrary() if not library.load(template_dir): raise RuntimeError(f"Failed to load templates from {template_dir}") # Create play clock reader pc_w, pc_h = playclock_coords[2], playclock_coords[3] reader = ReadPlayClock(library, region_width=pc_w, region_height=pc_h) # Create scorebug detector scorebug_detector = DetectScoreBug( template_path=template_path, fixed_region=scorebug_coords, use_split_detection=True, ) # Open video cap = cv2.VideoCapture(video_path) if not cap.isOpened(): raise RuntimeError(f"Failed to open video: {video_path}") fps = cap.get(cv2.CAP_PROP_FPS) # Collect sample timestamps timestamps = [] current_time = start_time while current_time < end_time: timestamps.append(current_time) current_time += sample_interval logger.info("Testing %d padding values on %d samples from %s to %s", len(padding_values), len(timestamps), seconds_to_timestamp(start_time), seconds_to_timestamp(end_time)) # Results for each padding value results: Dict[int, Dict[str, Any]] = {} for padding in padding_values: detections = 0 scorebug_present = 0 total_confidence = 0.0 sample_results: List[Dict[str, Any]] = [] for ts in timestamps: frame_num = int(ts * fps) cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num) ret, frame = cap.read() if not ret or frame is None: continue # Check scorebug sb_result = scorebug_detector.detect(frame) if not sb_result.detected: continue scorebug_present += 1 # Try play clock reading with this padding pc_result = reader.read_from_fixed_location(frame, playclock_coords, padding=padding) sample_result = { "timestamp": ts, "timestamp_str": seconds_to_timestamp(ts), "detected": pc_result.detected, "value": pc_result.value, "confidence": pc_result.confidence, } sample_results.append(sample_result) if pc_result.detected: detections += 1 total_confidence += pc_result.confidence detection_rate = (detections / scorebug_present * 100) if scorebug_present > 0 else 0 avg_confidence = (total_confidence / detections) if detections > 0 else 0 results[padding] = { "padding": padding, "samples": len(timestamps), "scorebug_present": scorebug_present, "detections": detections, "detection_rate": detection_rate, "avg_confidence": avg_confidence, "sample_results": sample_results, } logger.info( " padding=%d: %d/%d detected (%.1f%%), avg conf=%.3f", padding, detections, scorebug_present, detection_rate, avg_confidence, ) cap.release() return results def main(): """Main function to test padded play clock reading.""" config = load_oregon_config() video_path = config["video_path"] template_path = config["template_path"] playclock_coords = ( config["scorebug_x"] + config["playclock_x_offset"], config["scorebug_y"] + config["playclock_y_offset"], config["playclock_width"], config["playclock_height"], ) scorebug_coords = ( config["scorebug_x"], config["scorebug_y"], config["scorebug_width"], config["scorebug_height"], ) # Padding values to test (in pixels) padding_values = [0, 2, 3, 4, 5, 6, 8, 10] logger.info("=" * 70) logger.info("PADDED PLAY CLOCK REGION TEST") logger.info("=" * 70) logger.info("Video: %s", video_path) logger.info("Play clock region: %s", playclock_coords) logger.info("") # Test on the problematic segment (after 12 min where templates fail) logger.info("SEGMENT 1: After 12-minute mark (known failure zone)") logger.info("-" * 70) results_late = test_padding_values( video_path=video_path, playclock_coords=playclock_coords, scorebug_coords=scorebug_coords, template_dir="output/debug/digit_templates", template_path=template_path, padding_values=padding_values, start_time=900.0, # 15:00 end_time=2400.0, # 40:00 sample_interval=10.0, # Sample every 10 seconds ) # Also test on early segment to ensure no regression logger.info("") logger.info("SEGMENT 2: Early game (should already work)") logger.info("-" * 70) results_early = test_padding_values( video_path=video_path, playclock_coords=playclock_coords, scorebug_coords=scorebug_coords, template_dir="output/debug/digit_templates", template_path=template_path, padding_values=padding_values, start_time=330.0, # 5:30 end_time=720.0, # 12:00 sample_interval=10.0, ) # Print summary logger.info("") logger.info("=" * 70) logger.info("SUMMARY") logger.info("=" * 70) logger.info("") logger.info("%-10s | %-25s | %-25s", "Padding", "Late Game (15:00-40:00)", "Early Game (5:30-12:00)") logger.info("-" * 70) for padding in padding_values: late = results_late[padding] early = results_early[padding] logger.info( "%-10d | %3d/%3d = %5.1f%% (conf %.2f) | %3d/%3d = %5.1f%% (conf %.2f)", padding, late["detections"], late["scorebug_present"], late["detection_rate"], late["avg_confidence"], early["detections"], early["scorebug_present"], early["detection_rate"], early["avg_confidence"], ) # Find best padding best_padding = max(padding_values, key=lambda p: results_late[p]["detection_rate"]) best_late = results_late[best_padding] best_early = results_early[best_padding] logger.info("") logger.info("RECOMMENDATION:") logger.info( " Best padding = %d pixels (late game: %.1f%%, early game: %.1f%%)", best_padding, best_late["detection_rate"], best_early["detection_rate"], ) # Check for false positive risk - confidence should stay high if best_late["avg_confidence"] < 0.6: logger.warning(" WARNING: Average confidence is low (%.2f) - may indicate false positives", best_late["avg_confidence"]) # Save detailed results output_path = Path("output/debug/padding_test_results.json") output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w") as f: json.dump( { "late_game": {str(k): {kk: vv for kk, vv in v.items() if kk != "sample_results"} for k, v in results_late.items()}, "early_game": {str(k): {kk: vv for kk, vv in v.items() if kk != "sample_results"} for k, v in results_early.items()}, "recommendation": {"best_padding": best_padding}, }, f, indent=2, ) logger.info("") logger.info("Detailed results saved to: %s", output_path) if __name__ == "__main__": main()