Spaces:
Sleeping
Sleeping
| """ | |
| Find frames where scorebug is detected but play clock reading fails. | |
| Run OCR to confirm what the actual clock value is, then save debug images. | |
| This produces images showing: | |
| - The scorebug region (with play clock highlighted) | |
| - The extracted play clock region (scaled up) | |
| - The preprocessed play clock region | |
| - OCR result vs template matching result | |
| Usage: | |
| python scripts/find_missed_clock_readings.py | |
| """ | |
| import json | |
| import logging | |
| import sys | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Tuple | |
| import cv2 | |
| import easyocr | |
| 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 find_missed_readings( | |
| 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, | |
| target_count: int = 10, | |
| sample_interval: float = 2.0, # Sample every 2 seconds to spread out | |
| start_time: float = 900.0, # Start at 15 minutes (after early game where templates work) | |
| end_time: float = 7200.0, # End at 2 hours | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| Find frames where scorebug is detected but clock reading fails. | |
| Run OCR to confirm the actual clock value. | |
| """ | |
| 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, | |
| ) | |
| # Initialize EasyOCR | |
| logger.info("Initializing EasyOCR...") | |
| ocr_reader = easyocr.Reader(["en"], gpu=False, verbose=False) | |
| logger.info("EasyOCR ready") | |
| # 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) | |
| logger.info("Video: %s", video_path) | |
| logger.info("Searching for missed clock readings from %s to %s...", seconds_to_timestamp(start_time), seconds_to_timestamp(end_time)) | |
| missed_readings = [] | |
| current_time = start_time | |
| while current_time < end_time and len(missed_readings) < target_count: | |
| 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: | |
| current_time += sample_interval | |
| continue | |
| # Check scorebug detection | |
| sb_result = scorebug_detector.detect(frame) | |
| if not sb_result.detected: | |
| current_time += sample_interval | |
| continue | |
| # Check template-based clock reading | |
| template_result = reader.read_from_fixed_location(frame, playclock_coords) | |
| if template_result.detected: | |
| # Template matching worked - skip this frame | |
| current_time += sample_interval | |
| continue | |
| # Scorebug detected but template matching failed - run OCR to confirm actual value | |
| pc_x, pc_y, pc_w, pc_h = playclock_coords | |
| playclock_region = frame[pc_y : pc_y + pc_h, pc_x : pc_x + pc_w].copy() | |
| # Preprocess for OCR (same as template building) | |
| preprocessed = preprocess_playclock_region(playclock_region, scale_factor=4) | |
| # Run OCR | |
| try: | |
| ocr_results = ocr_reader.readtext(preprocessed, allowlist="0123456789", detail=1) | |
| if ocr_results: | |
| best = max(ocr_results, key=lambda x: x[2]) | |
| ocr_text = best[1].strip() | |
| ocr_confidence = best[2] | |
| # Only keep if OCR finds a valid clock value | |
| if ocr_text and ocr_confidence >= 0.5: | |
| try: | |
| ocr_value = int(ocr_text) | |
| if 0 <= ocr_value <= 40: | |
| # Valid reading - this is a missed detection | |
| missed = { | |
| "timestamp": current_time, | |
| "timestamp_str": seconds_to_timestamp(current_time), | |
| "ocr_value": ocr_value, | |
| "ocr_confidence": ocr_confidence, | |
| "template_confidence": template_result.confidence, | |
| "scorebug_confidence": sb_result.confidence, | |
| } | |
| missed_readings.append(missed) | |
| logger.info( | |
| "Found missed reading #%d at %s: OCR=%d (%.2f), Template conf=%.2f", | |
| len(missed_readings), | |
| seconds_to_timestamp(current_time), | |
| ocr_value, | |
| ocr_confidence, | |
| template_result.confidence, | |
| ) | |
| # Save debug image | |
| save_missed_debug_image( | |
| frame, | |
| playclock_coords, | |
| scorebug_coords, | |
| template_result, | |
| ocr_value, | |
| ocr_confidence, | |
| current_time, | |
| output_path / f"missed_{len(missed_readings):02d}_t{int(current_time)}_ocr{ocr_value}.png", | |
| ) | |
| except ValueError: | |
| pass | |
| except Exception as e: | |
| logger.debug("OCR error at %.1fs: %s", current_time, e) | |
| current_time += sample_interval | |
| cap.release() | |
| logger.info("") | |
| logger.info("Found %d missed readings", len(missed_readings)) | |
| # Save summary | |
| summary_path = output_path / "missed_readings_summary.json" | |
| with open(summary_path, "w") as f: | |
| json.dump(missed_readings, f, indent=2) | |
| logger.info("Summary saved to %s", summary_path) | |
| return missed_readings | |
| def save_missed_debug_image( | |
| frame: np.ndarray, | |
| playclock_coords: Tuple[int, int, int, int], | |
| scorebug_coords: Tuple[int, int, int, int], | |
| template_result, | |
| ocr_value: int, | |
| ocr_confidence: float, | |
| timestamp: float, | |
| output_path: Path, | |
| ) -> None: | |
| """Save a debug image showing the missed clock reading.""" | |
| 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 | |
| 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 | |
| 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 + 80, 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 | |
| # Title with timestamp | |
| title = f"t={seconds_to_timestamp(timestamp)} - MISSED READING" | |
| cv2.putText(composite, title, (10, composite_height + 25), font, 0.6, (255, 255, 255), 1) | |
| # OCR result (green - correct) | |
| ocr_text = f"OCR: {ocr_value} (conf: {ocr_confidence:.2f})" | |
| cv2.putText(composite, ocr_text, (10, composite_height + 50), font, 0.5, (0, 255, 0), 1) | |
| # Template result (red - failed) | |
| tmpl_text = f"Template: NOT DETECTED (conf: {template_result.confidence:.2f})" | |
| cv2.putText(composite, tmpl_text, (10, composite_height + 70), font, 0.5, (0, 0, 255), 1) | |
| # Add labels for the image panels | |
| cv2.putText(composite, "Raw", (x_offset, pc_scaled.shape[0] + 5), font, 0.4, (200, 200, 200), 1) | |
| cv2.putText(composite, "Preprocessed", (x_offset, y_offset + preprocessed_scaled.shape[0] + 5), font, 0.4, (200, 200, 200), 1) | |
| cv2.imwrite(str(output_path), composite) | |
| def main(): | |
| """Main function to find missed clock readings.""" | |
| 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"], | |
| ) | |
| logger.info("Finding missed clock readings in Oregon video") | |
| logger.info("=" * 60) | |
| missed = find_missed_readings( | |
| 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/missed_clock_readings", | |
| target_count=10, | |
| sample_interval=5.0, # Sample every 5 seconds to spread out examples | |
| start_time=900.0, # Start at 15 min | |
| end_time=7200.0, # End at 2 hours | |
| ) | |
| # Print summary | |
| logger.info("") | |
| logger.info("=" * 60) | |
| logger.info("MISSED READINGS SUMMARY") | |
| logger.info("=" * 60) | |
| for i, m in enumerate(missed, 1): | |
| logger.info( | |
| "%2d. t=%s | OCR: %2d (%.2f) | Template conf: %.2f", | |
| i, | |
| m["timestamp_str"], | |
| m["ocr_value"], | |
| m["ocr_confidence"], | |
| m["template_confidence"], | |
| ) | |
| logger.info("") | |
| logger.info("Debug images saved to: output/debug/missed_clock_readings/") | |
| if __name__ == "__main__": | |
| main() | |