cfb40 / scripts /test_padded_playclock.py
andytaylor-smg's picture
I think this fixes Oregon
aee009f
"""
Test the padded region approach for shift-invariant play clock reading.
Compares clock detection rates with different padding values to determine
the optimal padding for handling translational shifts in the broadcast.
Usage:
python scripts/test_padded_playclock.py
"""
import json
import logging
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple
import cv2
# 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
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 test_padding_values(
video_path: str,
playclock_coords: Tuple[int, int, int, int],
scorebug_coords: Tuple[int, int, int, int],
template_dir: str,
template_path: str,
padding_values: List[int],
start_time: float,
end_time: float,
sample_interval: float = 5.0,
) -> Dict[int, Dict[str, Any]]:
"""
Test different padding values and compare detection rates.
Args:
video_path: Path to video file
playclock_coords: Absolute play clock coordinates (x, y, w, h)
scorebug_coords: Scorebug coordinates (x, y, w, h)
template_dir: Path to digit templates
template_path: Path to scorebug template
padding_values: List of padding values to test
start_time: Start time in seconds
end_time: End time in seconds
sample_interval: Seconds between samples
Returns:
Dictionary mapping padding value to results
"""
# 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)
# Collect sample timestamps
timestamps = []
current_time = start_time
while current_time < end_time:
timestamps.append(current_time)
current_time += sample_interval
logger.info("Testing %d padding values on %d samples from %s to %s", len(padding_values), len(timestamps), seconds_to_timestamp(start_time), seconds_to_timestamp(end_time))
# Results for each padding value
results: Dict[int, Dict[str, Any]] = {}
for padding in padding_values:
detections = 0
scorebug_present = 0
total_confidence = 0.0
sample_results: List[Dict[str, Any]] = []
for ts in 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:
continue
# Check scorebug
sb_result = scorebug_detector.detect(frame)
if not sb_result.detected:
continue
scorebug_present += 1
# Try play clock reading with this padding
pc_result = reader.read_from_fixed_location(frame, playclock_coords, padding=padding)
sample_result = {
"timestamp": ts,
"timestamp_str": seconds_to_timestamp(ts),
"detected": pc_result.detected,
"value": pc_result.value,
"confidence": pc_result.confidence,
}
sample_results.append(sample_result)
if pc_result.detected:
detections += 1
total_confidence += pc_result.confidence
detection_rate = (detections / scorebug_present * 100) if scorebug_present > 0 else 0
avg_confidence = (total_confidence / detections) if detections > 0 else 0
results[padding] = {
"padding": padding,
"samples": len(timestamps),
"scorebug_present": scorebug_present,
"detections": detections,
"detection_rate": detection_rate,
"avg_confidence": avg_confidence,
"sample_results": sample_results,
}
logger.info(
" padding=%d: %d/%d detected (%.1f%%), avg conf=%.3f",
padding,
detections,
scorebug_present,
detection_rate,
avg_confidence,
)
cap.release()
return results
def main():
"""Main function to test padded play clock reading."""
config = load_oregon_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"],
)
# Padding values to test (in pixels)
padding_values = [0, 2, 3, 4, 5, 6, 8, 10]
logger.info("=" * 70)
logger.info("PADDED PLAY CLOCK REGION TEST")
logger.info("=" * 70)
logger.info("Video: %s", video_path)
logger.info("Play clock region: %s", playclock_coords)
logger.info("")
# Test on the problematic segment (after 12 min where templates fail)
logger.info("SEGMENT 1: After 12-minute mark (known failure zone)")
logger.info("-" * 70)
results_late = test_padding_values(
video_path=video_path,
playclock_coords=playclock_coords,
scorebug_coords=scorebug_coords,
template_dir="output/debug/digit_templates",
template_path=template_path,
padding_values=padding_values,
start_time=900.0, # 15:00
end_time=2400.0, # 40:00
sample_interval=10.0, # Sample every 10 seconds
)
# Also test on early segment to ensure no regression
logger.info("")
logger.info("SEGMENT 2: Early game (should already work)")
logger.info("-" * 70)
results_early = test_padding_values(
video_path=video_path,
playclock_coords=playclock_coords,
scorebug_coords=scorebug_coords,
template_dir="output/debug/digit_templates",
template_path=template_path,
padding_values=padding_values,
start_time=330.0, # 5:30
end_time=720.0, # 12:00
sample_interval=10.0,
)
# Print summary
logger.info("")
logger.info("=" * 70)
logger.info("SUMMARY")
logger.info("=" * 70)
logger.info("")
logger.info("%-10s | %-25s | %-25s", "Padding", "Late Game (15:00-40:00)", "Early Game (5:30-12:00)")
logger.info("-" * 70)
for padding in padding_values:
late = results_late[padding]
early = results_early[padding]
logger.info(
"%-10d | %3d/%3d = %5.1f%% (conf %.2f) | %3d/%3d = %5.1f%% (conf %.2f)",
padding,
late["detections"],
late["scorebug_present"],
late["detection_rate"],
late["avg_confidence"],
early["detections"],
early["scorebug_present"],
early["detection_rate"],
early["avg_confidence"],
)
# Find best padding
best_padding = max(padding_values, key=lambda p: results_late[p]["detection_rate"])
best_late = results_late[best_padding]
best_early = results_early[best_padding]
logger.info("")
logger.info("RECOMMENDATION:")
logger.info(
" Best padding = %d pixels (late game: %.1f%%, early game: %.1f%%)",
best_padding,
best_late["detection_rate"],
best_early["detection_rate"],
)
# Check for false positive risk - confidence should stay high
if best_late["avg_confidence"] < 0.6:
logger.warning(" WARNING: Average confidence is low (%.2f) - may indicate false positives", best_late["avg_confidence"])
# Save detailed results
output_path = Path("output/debug/padding_test_results.json")
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w") as f:
json.dump(
{
"late_game": {str(k): {kk: vv for kk, vv in v.items() if kk != "sample_results"} for k, v in results_late.items()},
"early_game": {str(k): {kk: vv for kk, vv in v.items() if kk != "sample_results"} for k, v in results_early.items()},
"recommendation": {"best_padding": best_padding},
},
f,
indent=2,
)
logger.info("")
logger.info("Detailed results saved to: %s", output_path)
if __name__ == "__main__":
main()