""" Diagnose why clock detection stops working after ~12 minutes in the Oregon video. This script samples frames at regular intervals throughout the video and checks both the scorebug detection and clock reading success rates over time. Usage: python scripts/diagnose_clock_gap.py """ import json import logging import sys from pathlib import Path from typing import Any, Dict, List, Tuple import cv2 import numpy as np # 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 from utils.regions import preprocess_playclock_region 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 diagnose_over_time( video_path: str, playclock_coords: Tuple[int, int, int, int], scorebug_coords: Tuple[int, int, int, int], template_dir: str, template_path: str, output_dir: str, time_points: List[float], samples_per_point: int = 20, sample_interval: float = 0.5, ) -> Dict[str, Any]: """ Diagnose clock detection at different time points throughout the video. """ # Create output directory output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) # 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) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) duration = total_frames / fps logger.info("Video: %s", video_path) logger.info(" FPS: %.2f, Duration: %s", fps, seconds_to_timestamp(duration)) results = [] for time_point in time_points: if time_point >= duration: continue logger.info("Analyzing time point: %s", seconds_to_timestamp(time_point)) # Sample multiple frames around this time point scorebug_detections = 0 clock_detections = 0 clock_values = [] for i in range(samples_per_point): sample_time = time_point + i * sample_interval frame_num = int(sample_time * 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 sb_result.detected: scorebug_detections += 1 # Check clock clock_result = reader.read_from_fixed_location(frame, playclock_coords) if clock_result.detected: clock_detections += 1 clock_values.append(clock_result.value) # Save debug image for first sample of each time point if i == 0: save_time_debug(frame, playclock_coords, scorebug_coords, sb_result, clock_result, time_point, output_path / f"time_{int(time_point):05d}.png", reader.scale_factor) result = { "time_point": time_point, "timestamp": seconds_to_timestamp(time_point), "scorebug_rate": scorebug_detections / samples_per_point, "clock_rate": clock_detections / samples_per_point, "clock_values": clock_values, } results.append(result) logger.info( " Scorebug: %.0f%%, Clock: %.0f%%, Values: %s", result["scorebug_rate"] * 100, result["clock_rate"] * 100, sorted(set(clock_values)) if clock_values else "none" ) cap.release() # Print summary logger.info("") logger.info("=" * 80) logger.info("TIME-BASED DETECTION SUMMARY") logger.info("=" * 80) logger.info("%-12s %-12s %-12s %-20s", "Time", "Scorebug", "Clock", "Values") logger.info("-" * 80) for r in results: values_str = str(sorted(set(r["clock_values"]))[:5]) if r["clock_values"] else "none" logger.info("%-12s %-12.0f%% %-12.0f%% %-20s", r["timestamp"], r["scorebug_rate"] * 100, r["clock_rate"] * 100, values_str) # Save results results_path = output_path / "time_diagnosis.json" with open(results_path, "w") as f: json.dump(results, f, indent=2) return {"results": results} def save_time_debug( frame: np.ndarray, playclock_coords: Tuple[int, int, int, int], scorebug_coords: Tuple[int, int, int, int], sb_result, clock_result, timestamp: float, output_path: Path, scale_factor: int = 4, ) -> None: """Save a debug image for time-based analysis.""" pc_x, pc_y, pc_w, pc_h = playclock_coords sb_x, sb_y, sb_w, sb_h = scorebug_coords # Extract the scorebug area plus some context margin = 30 crop_y1 = max(0, sb_y - margin) crop_y2 = min(frame.shape[0], sb_y + sb_h + margin) crop_x1 = max(0, sb_x - margin) crop_x2 = min(frame.shape[1], sb_x + sb_w + margin) scorebug_crop = frame[crop_y1:crop_y2, crop_x1:crop_x2].copy() # Draw play clock region on crop (adjust coordinates) pc_rel_x = pc_x - crop_x1 pc_rel_y = pc_y - crop_y1 cv2.rectangle(scorebug_crop, (pc_rel_x, pc_rel_y), (pc_rel_x + pc_w, pc_rel_y + pc_h), (0, 0, 255), 2) # Extract play clock region for detail view playclock_region = frame[pc_y : pc_y + pc_h, pc_x : pc_x + pc_w].copy() pc_scaled = cv2.resize(playclock_region, None, fx=10, fy=10, interpolation=cv2.INTER_NEAREST) # Preprocess for visualization preprocessed = preprocess_playclock_region(playclock_region, scale_factor) preprocessed_bgr = cv2.cvtColor(preprocessed, cv2.COLOR_GRAY2BGR) preprocessed_scaled = cv2.resize(preprocessed_bgr, (pc_scaled.shape[1], pc_scaled.shape[0]), interpolation=cv2.INTER_NEAREST) # Scale up scorebug crop scorebug_scaled = cv2.resize(scorebug_crop, None, fx=2, fy=2) # Create composite composite_height = max(scorebug_scaled.shape[0], pc_scaled.shape[0] + preprocessed_scaled.shape[0] + 20) composite_width = scorebug_scaled.shape[1] + max(pc_scaled.shape[1], preprocessed_scaled.shape[1]) + 40 composite = np.zeros((composite_height + 50, composite_width, 3), dtype=np.uint8) # Place scorebug composite[0 : scorebug_scaled.shape[0], 0 : scorebug_scaled.shape[1]] = scorebug_scaled # Place original playclock (scaled) x_offset = scorebug_scaled.shape[1] + 20 composite[0 : pc_scaled.shape[0], x_offset : x_offset + pc_scaled.shape[1]] = pc_scaled # Place preprocessed below y_offset = pc_scaled.shape[0] + 10 composite[y_offset : y_offset + preprocessed_scaled.shape[0], x_offset : x_offset + preprocessed_scaled.shape[1]] = preprocessed_scaled # Add text font = cv2.FONT_HERSHEY_SIMPLEX sb_status = f"Scorebug: {sb_result.confidence:.2f}" if sb_result.detected else "Scorebug: NOT DETECTED" if clock_result.detected: clock_status = f"Clock: {clock_result.value} (conf: {clock_result.confidence:.2f})" color = (0, 255, 0) else: clock_status = f"Clock: NOT DETECTED (conf: {clock_result.confidence:.2f})" color = (0, 0, 255) text = f"t={seconds_to_timestamp(timestamp)} | {sb_status} | {clock_status}" cv2.putText(composite, text, (10, composite_height + 30), font, 0.5, color, 1) cv2.imwrite(str(output_path), composite) def main(): """Main function to diagnose clock detection over time.""" # Load Oregon config config = load_oregon_config() video_path = config["video_path"] template_path = config["template_path"] # Calculate absolute play clock coordinates 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"], ) # Time points to check (in seconds) # Focus on: early game (works), transition zone, later game (doesn't work) time_points = [ # Pre-game (should not work) 0, 60, 120, 180, 240, 300, # Early game (should work - plays detected here) 330, 360, 400, 450, 500, 550, 600, 650, 700, 750, # After last detected play (~723s) 800, 850, 900, 950, 1000, # Much later 1200, 1500, 2000, 2500, 3000, 3600, # Second half 4200, 4800, 5400, 6000, 6600, 7200, 7800, ] logger.info("Oregon Video Clock Detection Analysis Over Time") logger.info("=" * 80) diagnose_over_time( video_path=video_path, playclock_coords=playclock_coords, scorebug_coords=scorebug_coords, template_dir="output/debug/digit_templates", template_path=template_path, output_dir="output/debug/oregon_time_analysis", time_points=time_points, samples_per_point=20, sample_interval=0.5, ) if __name__ == "__main__": main()