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