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