Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Diagnose special play end timing issue. | |
| Issue 2 from oregon_plan.md: | |
| Special plays always end after 10 seconds (MAX_SPECIAL_PLAY_DURATION), even when | |
| the scorebug disappears earlier. This causes clips to include unnecessary footage. | |
| This script investigates specific timestamps where special plays should have ended | |
| earlier based on scorebug disappearance. | |
| Examples to investigate: | |
| - 42:00 (2520s) | |
| - 1:02:05 (3725s) | |
| - 1:16:28 (4588s) | |
| - 1:25:47 (5147s) | |
| - 1:56:06 (6966s) | |
| """ | |
| import argparse | |
| import json | |
| import logging | |
| import os | |
| import sys | |
| from pathlib import Path | |
| from typing import Dict, Any, List, Optional | |
| import cv2 | |
| import numpy as np | |
| # Add src to path | |
| sys.path.insert(0, str(Path(__file__).parent.parent / "src")) | |
| from detection.scorebug import DetectScoreBug | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Timestamps from Issue 2 (special plays running too long) | |
| ISSUE_TIMESTAMPS = [ | |
| {"time": 2520, "label": "42:00 - special play runs too long"}, | |
| {"time": 3725, "label": "1:02:05 - special play runs too long"}, | |
| {"time": 4588, "label": "1:16:28 - special play runs too long"}, | |
| {"time": 5147, "label": "1:25:47 - special play runs too long"}, | |
| {"time": 6966, "label": "1:56:06 - special play runs too long"}, | |
| ] | |
| def analyze_scorebug_around_timestamp( | |
| video_path: str, | |
| template_path: str, | |
| scorebug_bbox: tuple, | |
| timestamp: float, | |
| window_before: float = 5.0, | |
| window_after: float = 15.0, | |
| frame_interval: float = 0.5, | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| Analyze scorebug detection around a specific timestamp. | |
| Args: | |
| video_path: Path to video file | |
| template_path: Path to scorebug template | |
| scorebug_bbox: Fixed scorebug region (x, y, w, h) | |
| timestamp: Center timestamp to analyze | |
| window_before: Seconds before timestamp to analyze | |
| window_after: Seconds after timestamp to analyze | |
| frame_interval: Interval between frames to analyze | |
| Returns: | |
| List of frame analysis results | |
| """ | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| raise RuntimeError(f"Could not open video: {video_path}") | |
| fps = cap.get(cv2.CAP_PROP_FPS) | |
| # Initialize scorebug detector with template | |
| detector = DetectScoreBug(template_path=template_path, fixed_region=scorebug_bbox, use_split_detection=True) | |
| results = [] | |
| start_time = timestamp - window_before | |
| end_time = timestamp + window_after | |
| current_time = start_time | |
| while current_time <= end_time: | |
| frame_num = int(current_time * 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 %.1fs", current_time) | |
| current_time += frame_interval | |
| continue | |
| # Detect scorebug | |
| detection = detector.detect(frame) | |
| # Relative time from the timestamp marker | |
| rel_time = current_time - timestamp | |
| results.append( | |
| { | |
| "timestamp": current_time, | |
| "relative_time": rel_time, | |
| "scorebug_detected": detection.detected, | |
| "confidence": detection.confidence, | |
| "method": detection.method, | |
| "left_confidence": getattr(detection, "left_confidence", None), | |
| "right_confidence": getattr(detection, "right_confidence", None), | |
| } | |
| ) | |
| current_time += frame_interval | |
| cap.release() | |
| return results | |
| def find_scorebug_disappearance(results: List[Dict[str, Any]]) -> Optional[float]: | |
| """ | |
| Find when scorebug first disappears (goes from detected to not detected). | |
| Returns: | |
| Timestamp of first disappearance, or None if never disappears | |
| """ | |
| prev_detected = None | |
| for r in results: | |
| if prev_detected is True and r["scorebug_detected"] is False: | |
| return r["timestamp"] | |
| prev_detected = r["scorebug_detected"] | |
| return None | |
| def print_analysis_table(results: List[Dict[str, Any]], label: str, timestamp: float) -> None: | |
| """Print formatted analysis table.""" | |
| print(f"\n{'='*80}") | |
| print(f" {label}") | |
| print(f" Marker timestamp: {timestamp:.1f}s") | |
| print(f"{'='*80}") | |
| print(f"{'RelTime':>8} | {'Time':>8} | {'Detected':>8} | {'Conf':>6} | {'Left':>6} | {'Right':>6} | Method") | |
| print("-" * 80) | |
| # Track state changes | |
| prev_detected = None | |
| for r in results: | |
| detected_str = "✓" if r["scorebug_detected"] else "✗" | |
| left_str = f"{r['left_confidence']:.3f}" if r["left_confidence"] is not None else "-" | |
| right_str = f"{r['right_confidence']:.3f}" if r["right_confidence"] is not None else "-" | |
| # Highlight state changes | |
| state_change = "" | |
| if prev_detected is not None and prev_detected != r["scorebug_detected"]: | |
| if r["scorebug_detected"]: | |
| state_change = " <-- APPEARED" | |
| else: | |
| state_change = " <-- DISAPPEARED" | |
| print(f"{r['relative_time']:>+8.1f} | {r['timestamp']:>8.1f} | {detected_str:>8} | {r['confidence']:>6.3f} | {left_str:>6} | {right_str:>6} | {r['method']}{state_change}") | |
| prev_detected = r["scorebug_detected"] | |
| # Summary | |
| disappear_time = find_scorebug_disappearance(results) | |
| if disappear_time: | |
| print(f"\nFirst scorebug disappearance: {disappear_time:.1f}s (relative: {disappear_time - timestamp:+.1f}s)") | |
| # Calculate when special play SHOULD end vs when it does end | |
| special_play_should_end = disappear_time | |
| special_play_actual_end = timestamp + 10.0 # MAX_SPECIAL_PLAY_DURATION | |
| print(f"Special play SHOULD end at: {special_play_should_end:.1f}s") | |
| print(f"Special play ACTUALLY ends at: {special_play_actual_end:.1f}s (timestamp + 10s)") | |
| print(f"Excess footage: {special_play_actual_end - special_play_should_end:.1f}s") | |
| else: | |
| print("\nScorybug never disappears in analysis window - still detected at end") | |
| def load_session_config(config_path: str) -> Dict[str, Any]: | |
| """Load session config to get scorebug bbox.""" | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Diagnose special play end timing") | |
| parser.add_argument("--video", required=True, help="Path to video file") | |
| parser.add_argument("--config", required=True, help="Path to session config JSON") | |
| parser.add_argument("--timestamp", type=float, help="Single timestamp to analyze (overrides defaults)") | |
| parser.add_argument("--window-before", type=float, default=5.0, help="Seconds before timestamp to analyze") | |
| parser.add_argument("--window-after", type=float, default=15.0, help="Seconds after timestamp to analyze") | |
| parser.add_argument("--frame-interval", type=float, default=0.5, help="Frame interval for analysis") | |
| parser.add_argument("--output-dir", default="output/debug/special_play_end", help="Output directory for debug images") | |
| args = parser.parse_args() | |
| # Load config to get scorebug region | |
| config = load_session_config(args.config) | |
| scorebug_bbox = ( | |
| config["scorebug_x"], | |
| config["scorebug_y"], | |
| config["scorebug_width"], | |
| config["scorebug_height"], | |
| ) | |
| template_path = config["template_path"] | |
| logger.info("Video: %s", args.video) | |
| logger.info("Template: %s", template_path) | |
| logger.info("Scorebug bbox: %s", scorebug_bbox) | |
| # Determine timestamps to analyze | |
| if args.timestamp: | |
| timestamps = [{"time": args.timestamp, "label": f"Custom timestamp {args.timestamp:.1f}s"}] | |
| else: | |
| timestamps = ISSUE_TIMESTAMPS | |
| # Analyze each timestamp | |
| for ts_info in timestamps: | |
| ts = ts_info["time"] | |
| label = ts_info["label"] | |
| logger.info("Analyzing %s...", label) | |
| results = analyze_scorebug_around_timestamp( | |
| video_path=args.video, | |
| template_path=template_path, | |
| scorebug_bbox=scorebug_bbox, | |
| timestamp=ts, | |
| window_before=args.window_before, | |
| window_after=args.window_after, | |
| frame_interval=args.frame_interval, | |
| ) | |
| print_analysis_table(results, label, ts) | |
| print("\n" + "=" * 80) | |
| print("SUMMARY") | |
| print("=" * 80) | |
| print(""" | |
| Key observations from this analysis: | |
| 1. If scorebug confidence stays high (>0.5) for the full 10 seconds, the template | |
| matcher may be too lenient or the scorebug actually stays visible. | |
| 2. If scorebug disappears but special play still runs to 10s, there may be a bug | |
| in how scorebug_detected is being passed to the special play tracker. | |
| 3. Check if the special play tracker's 'last_scorebug_timestamp' is being updated. | |
| The fix may require: | |
| - Adjusting template matching thresholds | |
| - Adding explicit scorebug disappearance tracking | |
| - Using a "grace period" before declaring scorebug disappeared (debounce) | |
| """) | |
| if __name__ == "__main__": | |
| main() | |