File size: 8,426 Bytes
07392e1
 
 
 
 
 
 
 
 
719b8f7
07392e1
46f8ebc
07392e1
 
 
 
 
fbeda03
07392e1
 
 
 
1f3bac1
07392e1
 
 
 
 
 
719b8f7
07392e1
 
 
 
719b8f7
 
 
07392e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719b8f7
07392e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719b8f7
07392e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fbeda03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
07392e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
"""
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