andytaylor-smg commited on
Commit
4fa2541
·
0 Parent(s):

Initial commit: scorebug

Browse files
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Video files (too large for git)
2
+ full_videos/
3
+
4
+ # Local configuration
5
+ AGENTS.md
6
+
7
+ # Generated output
8
+ output/
9
+
10
+ # Ground truth annotations (local testing data)
11
+ tests/test_data/ground_truth_plays.json
12
+
13
+ # Python
14
+ .venv/
15
+ __pycache__/
16
+ *.pyc
17
+ *.egg-info/
18
+ dist/
19
+ build/
20
+
21
+ # IDE
22
+ .vscode/
23
+ .idea/
24
+ *.swp
25
+
README.md ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CFB40 - College Football Play Detection System
2
+
3
+ Automatically detect and extract live plays from college football game videos, producing a condensed highlight reel.
4
+
5
+ ## Project Status
6
+
7
+ **Status:** Fresh start with scorebug detection foundation
8
+
9
+ ## Project Structure
10
+
11
+ ```
12
+ cfb40/
13
+ ├── src/
14
+ │ └── detectors/
15
+ │ ├── scorebug_detector.py # Scorebug detection via template matching
16
+ │ └── __init__.py
17
+ ├── data/
18
+ │ └── templates/ # Scorebug template images (17 templates)
19
+ ├── tests/
20
+ │ └── test_data/
21
+ │ └── ground_truth_plays.json # 12 annotated plays (gitignored)
22
+ ├── full_videos/ # Source video files (gitignored)
23
+ ├── output/ # Generated output (gitignored)
24
+ ├── .gitignore
25
+ ├── pyproject.toml
26
+ └── README.md
27
+ ```
28
+
29
+ ## Setup
30
+
31
+ ### Prerequisites
32
+ - Python 3.13+
33
+ - Virtual environment (`.venv`)
34
+
35
+ ### Installation
36
+
37
+ 1. Clone the repository
38
+
39
+ 2. Create and activate virtual environment:
40
+ ```bash
41
+ python -m venv .venv
42
+ source .venv/bin/activate
43
+ ```
44
+
45
+ 3. Install dependencies:
46
+ ```bash
47
+ pip install opencv-python numpy pillow
48
+ ```
49
+
50
+ ## Scorebug Detector
51
+
52
+ The scorebug detector identifies the score overlay graphic in video frames using template matching.
53
+
54
+ ### Usage
55
+
56
+ ```python
57
+ from src.detectors import ScorebugDetector
58
+
59
+ # Initialize with a template
60
+ detector = ScorebugDetector(template_path="data/templates/scorebug_template_main.png")
61
+
62
+ # Detect scorebug in a frame
63
+ detection = detector.detect(frame)
64
+
65
+ if detection.detected:
66
+ print(f"Scorebug found with confidence {detection.confidence:.2f}")
67
+ print(f"Location: {detection.bbox}")
68
+ ```
69
+
70
+ ### Detection Result
71
+
72
+ The `ScorebugDetection` dataclass contains:
73
+ - `detected`: Whether scorebug was found
74
+ - `confidence`: Match confidence (0.0 to 1.0)
75
+ - `bbox`: Bounding box (x, y, width, height)
76
+ - `method`: Detection method used
77
+
78
+ ## Test Data
79
+
80
+ ### Video Reference
81
+ - **File**: `full_videos/OSU vs Tenn 12.21.24.mkv`
82
+ - **Resolution**: 1920×1080 @ 59.94fps
83
+
84
+ ### Test Segments
85
+ - **Quick test**: 38:40 - 41:40 (3 minutes, ~5 plays)
86
+ - **Extended test**: 38:40 - 48:40 (10 minutes, ~12 plays)
87
+
88
+ ### Ground Truth
89
+ - 12 manually annotated plays in the 10-minute segment
90
+ - Average play duration: ~7 seconds
91
+
92
+ ## Development
93
+
94
+ ### Code Style
95
+ - Use Black formatter with `line-length=180`
96
+ - Add descriptive inline comments
97
+ - Include logging for debugging
98
+
99
+ ```bash
100
+ python -m black src/ --line-length=180
101
+ ```
102
+
103
+ ## License
104
+
105
+ Private project - All rights reserved
data/templates/region_0_124_966_1816_1033.png ADDED

Git LFS Details

  • SHA256: 920917eac5f36b9645e68e66b7de9c6434449af8d432c21f041fd37d496d9787
  • Pointer size: 131 Bytes
  • Size of remote file: 163 kB
data/templates/region_0_61_968_1880_1026.png ADDED

Git LFS Details

  • SHA256: 2791c1960eeb17df79fb128dec9f178881f423cc12520cc9407b845ae2ffead8
  • Pointer size: 131 Bytes
  • Size of remote file: 161 kB
data/templates/region_10_89_951_1824_1030.png ADDED

Git LFS Details

  • SHA256: 5f24619d79764ae2ef6c4763fae87c2f76c15f4575926ebb11e2eb1cf5ccc696
  • Pointer size: 131 Bytes
  • Size of remote file: 181 kB
data/templates/region_11_112_963_1820_1027.png ADDED

Git LFS Details

  • SHA256: 484df6c86a3cd0dade6ceed917efe599b4f3af6b2de01ac508dbec9636e51487
  • Pointer size: 131 Bytes
  • Size of remote file: 170 kB
data/templates/region_12_84_958_1818_1036.png ADDED

Git LFS Details

  • SHA256: 735e014dfe409fcb338be4112e14157a70853dea4a4d404227aa6720d8173875
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB
data/templates/region_16_99_954_1822_1030.png ADDED

Git LFS Details

  • SHA256: b57004eebe7b77f991488345f0b79e9404e3c4252bc4b4f4090524187bf5aad1
  • Pointer size: 131 Bytes
  • Size of remote file: 176 kB
data/templates/region_17_97_965_1836_1028.png ADDED

Git LFS Details

  • SHA256: a6dbda22f813428f63edd60517bf07764a410270eb021a4622d1909dba015b33
  • Pointer size: 131 Bytes
  • Size of remote file: 158 kB
data/templates/region_18_99_960_1836_1028.png ADDED

Git LFS Details

  • SHA256: 1d4d18631f045f13b3b60b79401f6a62a6a96d56c5852caefac6d63f8d58c7fe
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB
data/templates/region_19_113_967_1831_1027.png ADDED

Git LFS Details

  • SHA256: e21000a7dbc3357397b6d24832aeab4d6de87b5f5899750b5e5c3b0fec9fb65a
  • Pointer size: 131 Bytes
  • Size of remote file: 153 kB
data/templates/region_1_96_961_1826_1027.png ADDED

Git LFS Details

  • SHA256: a449d608922a2827007eef5e5d030b69c48c9180c8e1451edef47af79d0970e6
  • Pointer size: 131 Bytes
  • Size of remote file: 162 kB
data/templates/region_2_92_966_1822_1030.png ADDED

Git LFS Details

  • SHA256: 568a97bae476653b32e14bde1db514e567fb33af50ac4ab87ffaea342d57942c
  • Pointer size: 131 Bytes
  • Size of remote file: 157 kB
data/templates/region_3_63_962_1834_1025.png ADDED

Git LFS Details

  • SHA256: e43adaaf0ac39f52687cec982e73b6c5e23bb2e9317069a38e0410db4ac9c7db
  • Pointer size: 131 Bytes
  • Size of remote file: 160 kB
data/templates/region_4_81_958_1828_1030.png ADDED

Git LFS Details

  • SHA256: c4850d8d08db4dcafeed7fcf6eb68be764cb7a0aff3730d18d0f59e7ce90ca81
  • Pointer size: 131 Bytes
  • Size of remote file: 162 kB
data/templates/region_7_58_963_1838_1028.png ADDED

Git LFS Details

  • SHA256: 939b53f55ad909e91bc126e1b3075fd002d91c978ab44f9d1d5c289cb23c47a2
  • Pointer size: 131 Bytes
  • Size of remote file: 164 kB
data/templates/region_8_82_964_1908_1021.png ADDED

Git LFS Details

  • SHA256: 27ae9b4c7ed61c2647c281d4e8801039eb2f9066e5aa91a9792ac66581dc4ac6
  • Pointer size: 131 Bytes
  • Size of remote file: 154 kB
data/templates/region_9_77_973_1855_1027.png ADDED

Git LFS Details

  • SHA256: e705e9b8e37296b387cdfcf365c84826172443adbffae73cb0430e2e981d12d0
  • Pointer size: 131 Bytes
  • Size of remote file: 142 kB
data/templates/scorebug_template_main.png ADDED

Git LFS Details

  • SHA256: a449d608922a2827007eef5e5d030b69c48c9180c8e1451edef47af79d0970e6
  • Pointer size: 131 Bytes
  • Size of remote file: 162 kB
pyproject.toml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ [project]
3
+ name = "cfb40"
4
+ version = "0.1.0"
5
+ description = "Process to condense a college football game into a 40-ish minute highlight video."
6
+ readme = "README.md"
7
+ authors = [{ email = "andy.taylor@smg.com" }]
8
+ requires-python = ">=3.13"
9
+ dependencies = [
10
+ "opencv-python>=4.8.0",
11
+ "numpy>=1.24.0",
12
+ "pillow>=10.0.0",
13
+ ]
14
+
15
+ [dependency-groups]
16
+ dev = [
17
+ "black>=24.0.0",
18
+ ]
19
+
20
+ [build-system]
21
+ requires = ["setuptools>=61.0", "wheel"]
22
+ build-backend = "setuptools.build_meta"
23
+
24
+ [tool.mypy]
25
+ python_version = "3.13"
26
+ strict = true
27
+ ignore_missing_imports = true
28
+ exclude = ["tests", "scripts", "examples", "static"]
29
+
30
+ [tool.black]
31
+ line-length = 180
32
+ fast = true
33
+
34
+ [tool.isort]
35
+ profile = "black"
36
+
37
+ [tool.pylint.FORMAT]
38
+ ignore-long-lines = "^\\s*(# )?.*['\"]?<?https?://\\S+>?"
39
+ indent-after-paren = 4
40
+ max-line-length = 180
41
+ max-args = 8
42
+ max-positional-arguments = 8
43
+ max-attributes = 15
44
+ max-locals = 20
45
+
46
+ [tool.pylint.BASIC]
47
+ argument-naming-style = "snake_case"
48
+ attr-naming-style = "snake_case"
49
+ class-attribute-naming-style = "any"
50
+ class-naming-style = "PascalCase"
51
+ const-naming-style = "UPPER_CASE"
52
+ docstring-min-length = -1
53
+ function-naming-style = "snake_case"
54
+ include-naming-hint = "yes"
55
+
56
+ [tool.pylint.TYPECHECK]
57
+ ignore-none = "yes"
src/detectors/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Detector modules for identifying game elements."""
2
+
3
+ from .scorebug_detector import ScorebugDetector, ScorebugDetection, create_template_from_frame
4
+
5
+ __all__ = ["ScorebugDetector", "ScorebugDetection", "create_template_from_frame"]
src/detectors/scorebug_detector.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Scorebug detector module.
3
+
4
+ This module provides functions to detect the presence and location of the scorebug
5
+ (score overlay) in video frames.
6
+ """
7
+
8
+ import cv2
9
+ import numpy as np
10
+ import logging
11
+ from typing import Optional, Tuple, Dict
12
+ from dataclasses import dataclass
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class ScorebugDetection:
19
+ """Results from scorebug detection."""
20
+
21
+ detected: bool # Whether scorebug was detected
22
+ confidence: float # Confidence score (0.0 to 1.0)
23
+ bbox: Optional[Tuple[int, int, int, int]] = None # Bounding box (x, y, width, height)
24
+ method: str = "unknown" # Detection method used
25
+
26
+
27
+ class ScorebugDetector:
28
+ """
29
+ Detects the scorebug in video frames.
30
+
31
+ The detector uses multiple strategies to identify the scorebug:
32
+ 1. Template matching
33
+ 2. Color-based detection
34
+ 3. Position-based heuristics
35
+ """
36
+
37
+ def __init__(self, template_path: Optional[str] = None, expected_region: Optional[Tuple[int, int, int, int]] = None):
38
+ """
39
+ Initialize the scorebug detector.
40
+
41
+ Args:
42
+ template_path: Path to a template image of the scorebug (optional)
43
+ expected_region: Expected region where scorebug appears (x, y, w, h) (optional)
44
+ """
45
+ self.template = None
46
+ self.template_path = template_path
47
+ self.expected_region = expected_region
48
+
49
+ if template_path:
50
+ self.load_template(template_path)
51
+
52
+ logger.info(f"ScorebugDetector initialized (template: {template_path is not None}, region: {expected_region is not None})")
53
+
54
+ def load_template(self, template_path: str) -> None:
55
+ """
56
+ Load a template image for matching.
57
+
58
+ Args:
59
+ template_path: Path to the template image
60
+ """
61
+ self.template = cv2.imread(template_path)
62
+ if self.template is None:
63
+ raise ValueError(f"Could not load template image: {template_path}")
64
+
65
+ self.template_path = template_path
66
+ logger.info(f"Loaded template: {template_path} (size: {self.template.shape[1]}x{self.template.shape[0]})")
67
+
68
+ def detect(self, frame: np.ndarray) -> ScorebugDetection:
69
+ """
70
+ Detect scorebug in a frame.
71
+
72
+ Args:
73
+ frame: Input frame (BGR format)
74
+
75
+ Returns:
76
+ ScorebugDetection object with detection results
77
+ """
78
+ # Only use template matching - position alone is not sufficient
79
+ # The scorebug is NOT present during replays/timeouts even though
80
+ # the position may have other graphics
81
+
82
+ if self.template is not None:
83
+ detection = self._detect_by_template(frame)
84
+ if detection.detected:
85
+ logger.debug(f"Scorebug detected with confidence {detection.confidence:.2f} using {detection.method}")
86
+ return detection
87
+
88
+ # If template matching fails, scorebug is NOT present
89
+ logger.debug("No scorebug detected")
90
+ return ScorebugDetection(detected=False, confidence=0.0, method="none")
91
+
92
+ def _detect_by_template(self, frame: np.ndarray) -> ScorebugDetection:
93
+ """
94
+ Detect scorebug using template matching.
95
+
96
+ Args:
97
+ frame: Input frame
98
+
99
+ Returns:
100
+ Detection result
101
+ """
102
+ if self.template is None:
103
+ return ScorebugDetection(detected=False, confidence=0.0, method="template")
104
+
105
+ # Perform template matching
106
+ result = cv2.matchTemplate(frame, self.template, cv2.TM_CCOEFF_NORMED)
107
+ min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
108
+
109
+ # Use strict threshold - only detect when scorebug graphic is actually present
110
+ # Low matches indicate replays, timeouts, or other non-live-play moments
111
+ threshold = 0.8 # High threshold to avoid false positives
112
+
113
+ if max_val >= threshold:
114
+ # Get bounding box
115
+ h, w = self.template.shape[:2]
116
+ bbox = (max_loc[0], max_loc[1], w, h)
117
+
118
+ return ScorebugDetection(detected=True, confidence=float(max_val), bbox=bbox, method="template")
119
+ else:
120
+ return ScorebugDetection(detected=False, confidence=float(max_val), method="template")
121
+
122
+ def _detect_by_position(self, frame: np.ndarray) -> ScorebugDetection:
123
+ """
124
+ Detect scorebug by checking expected position.
125
+
126
+ This method assumes the scorebug is at a known location and verifies
127
+ that something is present there (e.g., by checking for non-zero content).
128
+
129
+ Args:
130
+ frame: Input frame
131
+
132
+ Returns:
133
+ Detection result
134
+ """
135
+ if self.expected_region is None:
136
+ return ScorebugDetection(detected=False, confidence=0.0, method="position")
137
+
138
+ x, y, w, h = self.expected_region
139
+
140
+ # Extract region
141
+ region = frame[y : y + h, x : x + w]
142
+
143
+ # TODO: Implement actual verification logic after analysis
144
+ # For now, just check if region is not completely black
145
+ mean_intensity = np.mean(region)
146
+
147
+ # Simple heuristic: if mean intensity is above threshold, assume scorebug is present
148
+ threshold = 30 # TODO: Tune this after analysis
149
+
150
+ if mean_intensity >= threshold:
151
+ confidence = min(mean_intensity / 255.0, 1.0)
152
+ return ScorebugDetection(detected=True, confidence=confidence, bbox=(x, y, w, h), method="position")
153
+ else:
154
+ return ScorebugDetection(detected=False, confidence=0.0, method="position")
155
+
156
+ def _detect_by_color(self, frame: np.ndarray) -> ScorebugDetection:
157
+ """
158
+ Detect scorebug by color characteristics.
159
+
160
+ This method looks for unique color patterns in the scorebug.
161
+
162
+ Args:
163
+ frame: Input frame
164
+
165
+ Returns:
166
+ Detection result
167
+
168
+ TODO: Implement after manual analysis identifies unique colors
169
+ """
170
+ # Placeholder for color-based detection
171
+ return ScorebugDetection(detected=False, confidence=0.0, method="color")
172
+
173
+ def visualize_detection(self, frame: np.ndarray, detection: ScorebugDetection) -> np.ndarray:
174
+ """
175
+ Draw detection results on frame for visualization.
176
+
177
+ Args:
178
+ frame: Input frame
179
+ detection: Detection result
180
+
181
+ Returns:
182
+ Frame with visualization overlay
183
+ """
184
+ vis_frame = frame.copy()
185
+
186
+ if detection.detected and detection.bbox:
187
+ x, y, w, h = detection.bbox
188
+
189
+ # Draw bounding box
190
+ color = (0, 255, 0) # Green for detected
191
+ cv2.rectangle(vis_frame, (x, y), (x + w, y + h), color, 2)
192
+
193
+ # Add confidence text
194
+ text = f"{detection.method}: {detection.confidence:.2f}"
195
+ cv2.putText(vis_frame, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
196
+
197
+ return vis_frame
198
+
199
+
200
+ def create_template_from_frame(frame: np.ndarray, bbox: Tuple[int, int, int, int], output_path: str) -> None:
201
+ """
202
+ Extract a region from a frame to use as a template.
203
+
204
+ Args:
205
+ frame: Source frame
206
+ bbox: Bounding box (x, y, width, height)
207
+ output_path: Path to save the template image
208
+ """
209
+ x, y, w, h = bbox
210
+ template = frame[y : y + h, x : x + w]
211
+ cv2.imwrite(output_path, template)
212
+ logger.info(f"Created template: {output_path} (size: {w}x{h})")