Spaces:
Sleeping
Sleeping
| """ | |
| Diagnose short play detection in Texas video. | |
| Captures play clock regions around: | |
| 1. Short/filtered plays (late game, ~5900-6400s) | |
| 2. Normal plays (early game, for comparison) | |
| Usage: | |
| python scripts/diagnose_texas_short_plays.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_texas_config() -> Dict[str, Any]: | |
| """Load the saved config for Texas video.""" | |
| config_path = Path("output/OSU_vs_Texas_01_10_25_config.json") | |
| with open(config_path, "r") as f: | |
| return json.load(f) | |
| def capture_clock_at_timestamps( | |
| video_path: str, | |
| playclock_coords: Tuple[int, int, int, int], | |
| scorebug_coords: Tuple[int, int, int, int], | |
| template_dir: str, | |
| template_path: str, | |
| timestamps: List[float], | |
| output_dir: str, | |
| label: str, | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| Capture play clock regions at specific timestamps. | |
| """ | |
| output_path = Path(output_dir) | |
| output_path.mkdir(parents=True, exist_ok=True) | |
| # Load template library | |
| 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) | |
| results = [] | |
| for i, ts in enumerate(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: | |
| logger.warning("Could not read frame at %s", seconds_to_timestamp(ts)) | |
| continue | |
| # Check scorebug | |
| sb_result = scorebug_detector.detect(frame) | |
| # Try template matching with different paddings | |
| result_no_pad = reader.read_from_fixed_location(frame, playclock_coords, padding=0) | |
| result_pad4 = reader.read_from_fixed_location(frame, playclock_coords, padding=4) | |
| result_pad10 = reader.read_from_fixed_location(frame, playclock_coords, padding=10) | |
| result = { | |
| "timestamp": ts, | |
| "timestamp_str": seconds_to_timestamp(ts), | |
| "scorebug_detected": sb_result.detected, | |
| "scorebug_confidence": sb_result.confidence, | |
| "pad0_detected": result_no_pad.detected, | |
| "pad0_value": result_no_pad.value, | |
| "pad0_confidence": result_no_pad.confidence, | |
| "pad4_detected": result_pad4.detected, | |
| "pad4_value": result_pad4.value, | |
| "pad4_confidence": result_pad4.confidence, | |
| "pad10_detected": result_pad10.detected, | |
| "pad10_value": result_pad10.value, | |
| "pad10_confidence": result_pad10.confidence, | |
| } | |
| results.append(result) | |
| # Save debug image | |
| save_debug_image( | |
| frame, | |
| playclock_coords, | |
| scorebug_coords, | |
| result, | |
| output_path / f"{label}_{i+1:02d}_t{int(ts)}.png", | |
| ) | |
| logger.info( | |
| "%s #%d at %s: SB=%s, pad0=%s(%.2f), pad4=%s(%.2f), pad10=%s(%.2f)", | |
| label, | |
| i + 1, | |
| seconds_to_timestamp(ts), | |
| "Y" if sb_result.detected else "N", | |
| result_no_pad.value if result_no_pad.detected else "X", | |
| result_no_pad.confidence, | |
| result_pad4.value if result_pad4.detected else "X", | |
| result_pad4.confidence, | |
| result_pad10.value if result_pad10.detected else "X", | |
| result_pad10.confidence, | |
| ) | |
| cap.release() | |
| return results | |
| def save_debug_image( | |
| frame: np.ndarray, | |
| playclock_coords: Tuple[int, int, int, int], | |
| scorebug_coords: Tuple[int, int, int, int], | |
| result: Dict[str, Any], | |
| output_path: Path, | |
| ) -> None: | |
| """Save a debug image showing the play clock region.""" | |
| pc_x, pc_y, pc_w, pc_h = playclock_coords | |
| sb_x, sb_y, sb_w, sb_h = scorebug_coords | |
| # Extract scorebug area with 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 | |
| pc_rel_x = pc_x - crop_x1 | |
| pc_rel_y = pc_y - crop_y1 | |
| color = (0, 255, 0) if result["pad4_detected"] else (0, 0, 255) | |
| cv2.rectangle(scorebug_crop, (pc_rel_x, pc_rel_y), (pc_rel_x + pc_w, pc_rel_y + pc_h), color, 2) | |
| # Extract play clock region | |
| playclock_region = frame[pc_y : pc_y + pc_h, pc_x : pc_x + pc_w].copy() | |
| # Scale up for visibility | |
| pc_scaled = cv2.resize(playclock_region, None, fx=12, fy=12, interpolation=cv2.INTER_NEAREST) | |
| # Preprocess | |
| preprocessed = preprocess_playclock_region(playclock_region, scale_factor=4) | |
| 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 | |
| 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 + 100, composite_width, 3), dtype=np.uint8) | |
| # Place scorebug | |
| composite[0 : scorebug_scaled.shape[0], 0 : scorebug_scaled.shape[1]] = scorebug_scaled | |
| # Place original playclock | |
| 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 labels | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| y_text = composite_height + 20 | |
| # Title | |
| title = f"t={result['timestamp_str']}" | |
| cv2.putText(composite, title, (10, y_text), font, 0.6, (255, 255, 255), 1) | |
| # Results for each padding | |
| y_text += 25 | |
| pad0_text = f"pad=0: {result['pad0_value'] if result['pad0_detected'] else 'FAIL'} (conf={result['pad0_confidence']:.2f})" | |
| pad0_color = (0, 255, 0) if result["pad0_detected"] else (0, 0, 255) | |
| cv2.putText(composite, pad0_text, (10, y_text), font, 0.5, pad0_color, 1) | |
| y_text += 20 | |
| pad4_text = f"pad=4: {result['pad4_value'] if result['pad4_detected'] else 'FAIL'} (conf={result['pad4_confidence']:.2f})" | |
| pad4_color = (0, 255, 0) if result["pad4_detected"] else (0, 0, 255) | |
| cv2.putText(composite, pad4_text, (10, y_text), font, 0.5, pad4_color, 1) | |
| y_text += 20 | |
| pad10_text = f"pad=10: {result['pad10_value'] if result['pad10_detected'] else 'FAIL'} (conf={result['pad10_confidence']:.2f})" | |
| pad10_color = (0, 255, 0) if result["pad10_detected"] else (0, 0, 255) | |
| cv2.putText(composite, pad10_text, (10, y_text), font, 0.5, pad10_color, 1) | |
| cv2.imwrite(str(output_path), composite) | |
| def main(): | |
| """Main function to diagnose Texas short plays.""" | |
| config = load_texas_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"], | |
| ) | |
| logger.info("=" * 70) | |
| logger.info("TEXAS VIDEO SHORT PLAY DIAGNOSIS") | |
| logger.info("=" * 70) | |
| logger.info("Video: %s", video_path) | |
| logger.info("Play clock region: %s", playclock_coords) | |
| logger.info("") | |
| # Timestamps around the filtered short plays (from terminal output) | |
| # These are plays that were detected but filtered for being too short | |
| short_play_timestamps = [ | |
| 5933.4, # Play #193 | |
| 5935.3, | |
| 5944.6, # Play #194 | |
| 5947.4, | |
| 6001.2, # Play #197 | |
| 6003.6, | |
| 6021.0, # Play #198 | |
| 6093.1, # Play #201 | |
| 6113.9, # Play #202 | |
| 6254.7, # Play #206 | |
| ] | |
| # Timestamps from normal plays in first half | |
| normal_play_timestamps = [ | |
| 323.7, # Play #1 start | |
| 330.0, # mid-play | |
| 346.4, # Play #2 start | |
| 355.0, # mid-play | |
| 462.5, # Play #3 start | |
| 470.0, # mid-play | |
| 660.4, # Play #4 start | |
| 670.0, # mid-play | |
| 1044.6, # Play #11 start | |
| 1050.0, # mid-play | |
| ] | |
| # Capture around short plays | |
| logger.info("CAPTURING SHORT PLAY REGIONS (late game)") | |
| logger.info("-" * 70) | |
| short_results = capture_clock_at_timestamps( | |
| video_path=video_path, | |
| playclock_coords=playclock_coords, | |
| scorebug_coords=scorebug_coords, | |
| template_dir="output/debug/digit_templates", | |
| template_path=template_path, | |
| timestamps=short_play_timestamps, | |
| output_dir="output/debug/texas_short_plays", | |
| label="short", | |
| ) | |
| logger.info("") | |
| logger.info("CAPTURING NORMAL PLAY REGIONS (early game)") | |
| logger.info("-" * 70) | |
| normal_results = capture_clock_at_timestamps( | |
| video_path=video_path, | |
| playclock_coords=playclock_coords, | |
| scorebug_coords=scorebug_coords, | |
| template_dir="output/debug/digit_templates", | |
| template_path=template_path, | |
| timestamps=normal_play_timestamps, | |
| output_dir="output/debug/texas_normal_plays", | |
| label="normal", | |
| ) | |
| # Summary | |
| logger.info("") | |
| logger.info("=" * 70) | |
| logger.info("SUMMARY") | |
| logger.info("=" * 70) | |
| def calc_detection_rate(results, key): | |
| detected = sum(1 for r in results if r[key]) | |
| return 100 * detected / len(results) if results else 0 | |
| logger.info("%-20s | pad=0 | pad=4 | pad=10", "Region") | |
| logger.info("-" * 70) | |
| logger.info( | |
| "%-20s | %4.0f%% | %4.0f%% | %4.0f%%", | |
| "Short plays (late)", | |
| calc_detection_rate(short_results, "pad0_detected"), | |
| calc_detection_rate(short_results, "pad4_detected"), | |
| calc_detection_rate(short_results, "pad10_detected"), | |
| ) | |
| logger.info( | |
| "%-20s | %4.0f%% | %4.0f%% | %4.0f%%", | |
| "Normal plays (early)", | |
| calc_detection_rate(normal_results, "pad0_detected"), | |
| calc_detection_rate(normal_results, "pad4_detected"), | |
| calc_detection_rate(normal_results, "pad10_detected"), | |
| ) | |
| # Save summary | |
| summary = { | |
| "short_play_results": short_results, | |
| "normal_play_results": normal_results, | |
| } | |
| summary_path = Path("output/debug/texas_diagnosis_summary.json") | |
| with open(summary_path, "w") as f: | |
| json.dump(summary, f, indent=2) | |
| logger.info("") | |
| logger.info("Summary saved to: %s", summary_path) | |
| logger.info("Debug images saved to: output/debug/texas_short_plays/ and output/debug/texas_normal_plays/") | |
| if __name__ == "__main__": | |
| main() | |