Spaces:
Sleeping
Sleeping
| """ | |
| Public API functions for the UI module. | |
| This module provides the high-level functions for interactive region selection | |
| that are intended to be called by external code. | |
| """ | |
| import logging | |
| from pathlib import Path | |
| from typing import Any, List, Optional, Tuple | |
| import cv2 | |
| import numpy as np | |
| from video.frame_extractor import extract_sample_frames | |
| from .models import BBox | |
| from .sessions import FlagRegionSelectionSession, PlayClockSelectionSession, ScorebugSelectionSession, TimeoutSelectionSession | |
| logger = logging.getLogger(__name__) | |
| def print_banner() -> None: | |
| """Print the welcome banner for CFB40.""" | |
| print("\n" + "=" * 60) | |
| print(" CFB40 - College Football Play Detection Pipeline") | |
| print("=" * 60 + "\n") | |
| def extract_sample_frames_for_selection(video_path: str, start_time: float, num_frames: int = 5, interval: float = 2.0) -> List[Tuple[float, np.ndarray[Any, Any]]]: | |
| """Extract sample frames from the video for region selection.""" | |
| return extract_sample_frames(video_path, start_time, num_frames, interval) | |
| def select_scorebug_region( | |
| frames: List[Tuple[float, np.ndarray[Any, Any]]], video_path: str | None = None | |
| ) -> Tuple[Optional[Tuple[int, int, int, int]], Optional[Tuple[float, np.ndarray[Any, Any]]]]: | |
| """ | |
| Interactive selection of scorebug region. | |
| Args: | |
| frames: List of (timestamp, frame) tuples to choose from. | |
| video_path: Path to video file (for loading additional frames if needed). | |
| Returns: | |
| Tuple of (scorebug_bbox, selected_frame) where: | |
| - scorebug_bbox: (x, y, width, height) or None if cancelled | |
| - selected_frame: The specific (timestamp, frame) tuple that was selected | |
| """ | |
| if not frames: | |
| logger.error("No frames provided for selection") | |
| return None, None | |
| print("\n[Phase 2] Region Setup - Scorebug Selection") | |
| print("-" * 50) | |
| print("Instructions:") | |
| print(" 1. Click on the TOP-LEFT corner of the scorebug") | |
| print(" 2. Click on the BOTTOM-RIGHT corner of the scorebug") | |
| print(" 'n' = next frame, 'p' = previous frame") | |
| print(" 's' = skip forward 30s, 'S' = skip forward 5min") | |
| print(" 'r' = reset selection, 'q' = quit") | |
| print("-" * 50) | |
| session = ScorebugSelectionSession(frames, video_path) | |
| bbox = session.run() | |
| if bbox is None: | |
| return None, None | |
| selected_frame = session.state.current_frame | |
| logger.info("Selected frame %d/%d @ %.1fs for scorebug region", session.state.frame_idx + 1, len(session.state.frames), selected_frame[0]) | |
| return bbox.to_tuple(), selected_frame | |
| def select_playclock_region(selected_frame: Tuple[float, np.ndarray[Any, Any]], scorebug_bbox: Tuple[int, int, int, int]) -> Optional[Tuple[int, int, int, int]]: | |
| """ | |
| Interactive selection of play clock region within the scorebug. | |
| Args: | |
| selected_frame: The (timestamp, frame) tuple selected during scorebug selection. | |
| scorebug_bbox: Scorebug bounding box (x, y, width, height). | |
| Returns: | |
| Play clock region as (x_offset, y_offset, width, height) relative to scorebug, | |
| or None if cancelled. | |
| """ | |
| if selected_frame is None: | |
| logger.error("No frame provided for selection") | |
| return None | |
| timestamp, frame = selected_frame | |
| sb_bbox = BBox.from_tuple(scorebug_bbox) | |
| print("\n[Phase 2] Region Setup - Play Clock Selection") | |
| print("-" * 50) | |
| print("Instructions:") | |
| print(" 1. Click on the TOP-LEFT corner of the play clock digits") | |
| print(" 2. Click on the BOTTOM-RIGHT corner of the play clock digits") | |
| print(" Press 'r' to reset, 'q' to quit") | |
| print("-" * 50) | |
| logger.info("Using frame @ %.1fs for play clock selection (same as scorebug selection)", timestamp) | |
| session = PlayClockSelectionSession(frame, sb_bbox, scale_factor=3) | |
| bbox = session.get_unscaled_bbox() if session.run() else None | |
| return bbox.to_tuple() if bbox else None | |
| def select_timeout_region(selected_frame: Tuple[float, np.ndarray[Any, Any]], scorebug_bbox: Tuple[int, int, int, int], team: str) -> Optional[Tuple[int, int, int, int]]: | |
| """ | |
| Interactive selection of timeout indicator region for a team. | |
| Args: | |
| selected_frame: The (timestamp, frame) tuple selected during scorebug selection. | |
| scorebug_bbox: Scorebug bounding box (x, y, width, height). | |
| team: "home" or "away". | |
| Returns: | |
| Timeout region as (x, y, width, height) in absolute frame coordinates, | |
| or None if cancelled. | |
| """ | |
| if selected_frame is None: | |
| logger.error("No frame provided for selection") | |
| return None | |
| timestamp, frame = selected_frame | |
| sb_bbox = BBox.from_tuple(scorebug_bbox) | |
| print(f"\n[Phase 2] Region Setup - {team.upper()} Team Timeout Selection") | |
| print("-" * 50) | |
| print("Instructions:") | |
| print(f" Select the 3 timeout indicator ovals for the {team.upper()} team") | |
| print(" (They are vertically stacked - white = available, dark = used)") | |
| print(" 1. Click on the TOP-LEFT corner of the timeout region") | |
| print(" 2. Click on the BOTTOM-RIGHT corner of the timeout region") | |
| print(" Press 'r' to reset, 'q' to quit") | |
| print("-" * 50) | |
| logger.info("Using frame @ %.1fs for %s timeout selection", timestamp, team) | |
| session = TimeoutSelectionSession(frame, sb_bbox, team, scale_factor=3, padding=50) | |
| session.run() | |
| bbox = session.get_absolute_bbox() | |
| return bbox.to_tuple() if bbox else None | |
| def select_flag_region(selected_frame: Tuple[float, np.ndarray[Any, Any]], scorebug_bbox: Tuple[int, int, int, int]) -> Optional[Tuple[int, int, int, int]]: | |
| """ | |
| Interactive selection of FLAG indicator region (where '1st & 10' / 'FLAG' appears). | |
| The FLAG region is selected relative to the scorebug, similar to how the | |
| play clock region is defined. This allows the same relative position to | |
| work even when the scorebug template matcher finds the scorebug at slightly | |
| different positions. | |
| Args: | |
| selected_frame: The (timestamp, frame) tuple selected during scorebug selection. | |
| scorebug_bbox: Scorebug bounding box (x, y, width, height). | |
| Returns: | |
| FLAG region as (x_offset, y_offset, width, height) relative to scorebug, | |
| or None if cancelled. | |
| """ | |
| if selected_frame is None: | |
| logger.error("No frame provided for selection") | |
| return None | |
| timestamp, frame = selected_frame | |
| sb_bbox = BBox.from_tuple(scorebug_bbox) | |
| print("\n[Phase 2] Region Setup - FLAG Region Selection") | |
| print("-" * 50) | |
| print("Instructions:") | |
| print(" Select the region where '1st & 10' / 'FLAG' appears") | |
| print(" This is typically to the right of the scorebug") | |
| print(" 1. Click on the TOP-LEFT corner of the region") | |
| print(" 2. Click on the BOTTOM-RIGHT corner of the region") | |
| print(" Press 'r' to reset, 'q' to quit") | |
| print("-" * 50) | |
| logger.info("Using frame @ %.1fs for FLAG region selection", timestamp) | |
| session = FlagRegionSelectionSession(frame, sb_bbox, scale_factor=3, padding=100) | |
| session.run() | |
| bbox = session.get_bbox_relative_to_scorebug() | |
| return bbox.to_tuple() if bbox else None | |
| def get_video_path_from_user(project_root: Path) -> Optional[str]: | |
| """ | |
| Prompt user to enter video file path. | |
| Args: | |
| project_root: Root directory of the project. | |
| Returns: | |
| Path to selected video file, or None if no valid selection. | |
| """ | |
| print("\nAvailable videos in full_videos/:") | |
| videos_dir = project_root / "full_videos" | |
| videos = [] | |
| if videos_dir.exists(): | |
| videos = list(videos_dir.glob("*.mp4")) + list(videos_dir.glob("*.mkv")) | |
| for i, v in enumerate(videos, 1): | |
| print(f" {i}. {v.name}") | |
| print("\nEnter video path (or number from list above):") | |
| user_input = input("> ").strip() | |
| if not user_input: | |
| return None | |
| # Check if user entered a number | |
| try: | |
| idx = int(user_input) - 1 | |
| if 0 <= idx < len(videos): | |
| return str(videos[idx]) | |
| except ValueError: | |
| pass | |
| # Check if it's a valid path | |
| if Path(user_input).exists(): | |
| return user_input | |
| # Check if it's relative to full_videos | |
| relative_path = videos_dir / user_input | |
| if relative_path.exists(): | |
| return str(relative_path) | |
| logger.error("Video not found: %s", user_input) | |
| return None | |