cfb40 / scripts /diagnose_clock_gap.py
andytaylor-smg's picture
I think this fixes Oregon
aee009f
"""
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()