Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |