# Flag Detection Implementation Plan ## Overview This document outlines the implementation plan for detecting penalty flags ("FLAG" indicator) on the scorebug and using this information to enhance play capture. When a flag is thrown, the system should persist video capture beyond normal play boundaries to include the referee's penalty announcement. --- ## Requirements ### Core Requirements 1. **FLAG Region Selection**: User selects the "FLAG" region during initialization (same location as "1st and 10" marker on scorebug) 2. **Yellow Detection**: Search for strong yellow presence in the FLAG region throughout the video to detect when flags are thrown 3. **Decision Override**: FLAG detection should override any other play-capturing decisions: - If a flag is detected but it looks like "weird clock behavior" (normally rejected), capture it anyway - If a flag is detected during enforced quiet time, capture it anyway - **All FLAG events should be captured, no matter what** 4. **Extended Capture Duration**: When a flag is thrown, persist video capture until EITHER: - The yellow "FLAG" indicator disappears (scorebug is present but FLAG is gone) - **OR** a configurable timeout (default: 15 seconds) after the play normally ends - Note: If FLAG appears → replay → FLAG still visible when scorebug returns, continue capturing 5. **Pre-Play Penalties**: Capture penalties that occur before a play (false start, delay of game) even though no "real football play" occurred 6. **Single-Pass Detection**: FLAG detection must be done in the same pass as normal play detection (no second video pass) ### Expected FLAG Scenarios | Scenario | Expected Behavior | |----------|------------------| | Flag during play | Extend capture until FLAG clears or timeout | | Flag before play (false start, delay of game) | Capture as play even without snap | | Flag on replay | Ignore FLAG during replay (no scorebug) | | Multiple flags on same play | Capture extends to last FLAG clearing | | Flag during quiet time | Override quiet time and capture | | Flag with weird clock behavior | Override rejection and capture | --- ## Implementation Steps ### Step 1: Test Script for FLAG Region Selection ✅ COMPLETE **Goal**: Create a short standalone script to interactively select the FLAG region and save it. **File**: `scripts/test_flag_region_selection.py` **Tasks**: - [x] Load sample frames from the test video - [x] Display frame with existing scorebug region highlighted for reference - [x] Prompt user to click/drag to select the FLAG region (typically where "1st & 10" appears) - [x] Save the selected region to `data/config/flag_region.json` - [x] Display a cropped preview of the selected region for verification **Output**: `data/config/flag_region.json` ```json { "flag_region": { "x_offset": 701, "y_offset": -27, "width": 260, "height": 35 } } ``` ### Step 2: Integrate FLAG Region into Main Pipeline **Goal**: Add FLAG region selection to the `main.py` interactive setup flow. **File Changes**: - `src/config/models.py` - Add FLAG region fields to `SessionConfig` - `src/ui/__init__.py` - Export new `select_flag_region` function - `src/ui/api.py` - Add `select_flag_region` function (similar to `select_playclock_region`) - `main.py` - Add FLAG region selection after timeout region selection (respect `--use-saved-regions`) **SessionConfig Additions**: ```python # FLAG region (relative to scorebug, like play clock) flag_x_offset: int = 0 flag_y_offset: int = 0 flag_width: int = 0 flag_height: int = 0 ``` ### Step 3: FLAG Detection Ground Truth Script ✅ COMPLETE **Goal**: Create a script to scan the video and identify potential FLAG timestamps for manual verification. **File**: `scripts/find_flag_ground_truth.py` **Tasks**: - [x] Load FLAG region from config - [x] Scan video at 2 FPS - [x] For each frame, extract FLAG region and compute "yellow-ness" score - [x] Apply section-level filters (duration, peak yellow, avg yellow, mean hue) - [x] Merge sections into FLAG events with gap tolerance for replays - [x] Output candidates to `output/cache/flag_candidates.json` **Results** (from `docs/flag_ground_truth.md`): - **Precision**: 100% (8/8 true positives, 0 false positives) - **Recall**: 100% - **Key filters**: min 3s duration, peak>=70%, avg>=60%, hue>=22 (rejects orange) ### Step 4: Create FlagReader Module **Goal**: Implement the core FLAG reading logic. **File**: `src/readers/flags.py` (NOT detection/flags.py - FlagReader goes in readers) **Class**: `FlagReader` **Key Methods**: ```python class FlagReader: """Reads FLAG indicator from scorebug using yellow color detection.""" # Detection parameters (from ground truth analysis) YELLOW_HUE_MIN = 15 YELLOW_HUE_MAX = 35 YELLOW_SAT_MIN = 100 YELLOW_VAL_MIN = 100 YELLOW_RATIO_THRESHOLD = 0.25 MIN_MEAN_HUE = 22 # Distinguishes yellow from orange def __init__(self, flag_region_offset: Tuple[int, int, int, int]): """Initialize with configured FLAG region offset (relative to scorebug).""" def read(self, frame: np.ndarray, scorebug_bbox: Tuple[int, int, int, int]) -> FlagReading: """ Read FLAG status from a frame. Args: frame: Full video frame (BGR) scorebug_bbox: Current scorebug bounding box (x, y, w, h) Returns: FlagReading with detected status, yellow_ratio, and mean_hue """ ``` **Reading Model**: ```python class FlagReading(BaseModel): """Result of reading FLAG region.""" detected: bool yellow_ratio: float mean_hue: float is_valid_yellow: bool # True if mean_hue >= MIN_MEAN_HUE (not orange) ``` ### Step 5: Integrate FLAG Detection into Play Tracking **Goal**: Modify the play tracker to use FLAG detection for extended capture. **File Changes**: - `src/tracking/models.py` - Add `FlagInfo` model and flag-related fields to state - `src/tracking/play_tracker.py` - Add FLAG state tracking - `src/tracking/normal_play_tracker.py` - Modify end-of-play logic for FLAG override - `src/pipeline/play_extractor.py` - Integrate FlagReader into frame processing **Key Logic Changes**: 1. **FlagInfo Model**: ```python class FlagInfo(BaseModel): """Flag information for a frame.""" detected: bool = False yellow_ratio: float = 0.0 mean_hue: float = 0.0 is_valid_yellow: bool = False ``` 2. **State Tracking Additions**: ```python # In NormalTrackerState flag_detected_at: Optional[float] = None # When FLAG first appeared flag_last_seen_at: Optional[float] = None # Last frame with FLAG visible flag_cleared_at: Optional[float] = None # When FLAG disappeared (scorebug present, no FLAG) ``` 3. **Play End Override Logic**: ```python def _should_extend_for_flag(self, timestamp: float, scorebug_visible: bool, flag_detected: bool) -> bool: """ Determine if play capture should extend due to FLAG. Returns True if: - FLAG was seen during this play AND - Either FLAG is still visible OR we're within the flag timeout window """ ``` 4. **Override quiet time and weird clock behavior**: - If FLAG detected, ignore quiet_time restrictions - If FLAG detected, capture even if clock behavior looks weird ### Step 6: Add Configuration Options **Goal**: Make FLAG-related parameters configurable. **File Changes**: - `src/tracking/models.py` - Add to `TrackPlayStateConfig` **Configuration Additions**: ```python class TrackPlayStateConfig(BaseModel): # ... existing fields ... # FLAG detection settings flag_extension_timeout: float = Field(15.0, description="Max seconds to extend capture after FLAG detected") flag_yellow_threshold: float = Field(0.25, description="Yellow pixel ratio threshold for FLAG detection") flag_min_mean_hue: float = Field(22.0, description="Min mean hue to distinguish yellow from orange") capture_pre_play_penalties: bool = Field(True, description="Capture penalties without plays (false start, etc.)") ``` ### Step 7: Test FLAG Detection **Goal**: Validate FLAG detection against ground truth. **File**: `scripts/test_flag_detection.py` **Note**: Ground truth already validated in Step 3 with 100% precision/recall. ### Step 8: Integration Testing **Goal**: Test full pipeline with FLAG detection enabled. **Test Cases**: 1. Standard play with flag → extended capture includes penalty announcement 2. Pre-play penalty (false start) → captured as play 3. Multiple flags in quick succession → handles correctly 4. Flag during replay → not double-counted 5. Plays without flags → unchanged behavior --- ## File Structure After Implementation ``` src/ ├── config/ │ └── models.py # + flag region fields in SessionConfig ├── readers/ │ ├── __init__.py # + export FlagReader │ ├── flags.py # NEW: FlagReader, FlagReading │ └── models.py # + FlagReading model ├── tracking/ │ ├── models.py # + FlagInfo, flag state fields │ ├── play_tracker.py # + FLAG coordination │ └── normal_play_tracker.py # + FLAG override logic ├── ui/ │ ├── __init__.py # + export select_flag_region │ └── api.py # + select_flag_region function └── pipeline/ └── play_extractor.py # + FlagReader integration data/config/ └── flag_region.json # Default FLAG region config scripts/ ├── test_flag_region_selection.py # Step 1 ✅ └── find_flag_ground_truth.py # Step 3 ✅ ``` --- ## Implementation Order | Step | Description | Dependencies | Status | |------|-------------|--------------|--------| | 1 | Test FLAG region selection script | None | ✅ Complete | | 2 | Integrate into main.py | Step 1 | In Progress | | 3 | Ground truth scanning script | Step 1 | ✅ Complete | | 4 | FlagReader module | Step 1 | In Progress | | 5 | Play tracker integration | Steps 3, 4 | In Progress | | 6 | Configuration options | Step 5 | In Progress | | 7 | Detection testing | Steps 3, 4 | Skipped (100% in Step 3) | | 8 | Integration testing | Steps 5, 6 | Pending | --- ## Success Criteria 1. **Region Selection**: User can select FLAG region in <30 seconds during setup ✅ 2. **Detection Accuracy**: ≥90% recall on verified ground truth FLAGS ✅ (100%) 3. **False Positive Rate**: <10% of detected FLAGS are false positives ✅ (0%) 4. **Extended Capture**: FLAG plays include penalty announcement in 90%+ of cases 5. **Pre-Play Penalties**: False starts and delay of game penalties captured 6. **No Regressions**: Existing play detection performance unchanged for non-FLAG plays 7. **Single Pass**: No second video pass required ✅ (integrated into main processing) --- ## Validated Detection Parameters From ground truth analysis (`docs/flag_ground_truth.md`): ```python # Yellow detection (HSV color space) YELLOW_HUE_MIN = 15 YELLOW_HUE_MAX = 35 YELLOW_SAT_MIN = 100 YELLOW_VAL_MIN = 100 # Detection threshold YELLOW_RATIO_THRESHOLD = 0.25 # 25% of region must be yellow # Orange discrimination MIN_MEAN_HUE = 22 # Rejects orange (hue ~16-17), accepts yellow (hue ~28-29) ``` ### Orange vs Yellow Discrimination | Metric | True FLAG Yellow | False Positive Orange | |--------|-----------------|----------------------| | Mean Hue | 27-29 | 16-17 | | Mean Value | 205-207 (bright) | 161-162 (darker) | The MIN_MEAN_HUE filter is essential for distinguishing true FLAG yellow from team colors (e.g., Tennessee orange).