File size: 10,473 Bytes
aee009f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"""
Diagnose why clock detection stops working after ~12 minutes in the Oregon video.

This script samples frames at regular intervals throughout the video and
checks both the scorebug detection and clock reading success rates over time.

Usage:
    python scripts/diagnose_clock_gap.py
"""

import json
import logging
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple

import cv2
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 diagnose_over_time(
    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,
    time_points: List[float],
    samples_per_point: int = 20,
    sample_interval: float = 0.5,
) -> Dict[str, Any]:
    """
    Diagnose clock detection at different time points throughout the video.
    """
    # Create output directory
    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,
    )

    # 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)
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    duration = total_frames / fps

    logger.info("Video: %s", video_path)
    logger.info("  FPS: %.2f, Duration: %s", fps, seconds_to_timestamp(duration))

    results = []

    for time_point in time_points:
        if time_point >= duration:
            continue

        logger.info("Analyzing time point: %s", seconds_to_timestamp(time_point))

        # Sample multiple frames around this time point
        scorebug_detections = 0
        clock_detections = 0
        clock_values = []

        for i in range(samples_per_point):
            sample_time = time_point + i * sample_interval
            frame_num = int(sample_time * fps)

            cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num)
            ret, frame = cap.read()

            if not ret or frame is None:
                continue

            # Check scorebug
            sb_result = scorebug_detector.detect(frame)
            if sb_result.detected:
                scorebug_detections += 1

            # Check clock
            clock_result = reader.read_from_fixed_location(frame, playclock_coords)
            if clock_result.detected:
                clock_detections += 1
                clock_values.append(clock_result.value)

            # Save debug image for first sample of each time point
            if i == 0:
                save_time_debug(frame, playclock_coords, scorebug_coords, sb_result, clock_result, time_point, output_path / f"time_{int(time_point):05d}.png", reader.scale_factor)

        result = {
            "time_point": time_point,
            "timestamp": seconds_to_timestamp(time_point),
            "scorebug_rate": scorebug_detections / samples_per_point,
            "clock_rate": clock_detections / samples_per_point,
            "clock_values": clock_values,
        }
        results.append(result)

        logger.info(
            "  Scorebug: %.0f%%, Clock: %.0f%%, Values: %s", result["scorebug_rate"] * 100, result["clock_rate"] * 100, sorted(set(clock_values)) if clock_values else "none"
        )

    cap.release()

    # Print summary
    logger.info("")
    logger.info("=" * 80)
    logger.info("TIME-BASED DETECTION SUMMARY")
    logger.info("=" * 80)
    logger.info("%-12s %-12s %-12s %-20s", "Time", "Scorebug", "Clock", "Values")
    logger.info("-" * 80)

    for r in results:
        values_str = str(sorted(set(r["clock_values"]))[:5]) if r["clock_values"] else "none"
        logger.info("%-12s %-12.0f%% %-12.0f%% %-20s", r["timestamp"], r["scorebug_rate"] * 100, r["clock_rate"] * 100, values_str)

    # Save results
    results_path = output_path / "time_diagnosis.json"
    with open(results_path, "w") as f:
        json.dump(results, f, indent=2)

    return {"results": results}


def save_time_debug(
    frame: np.ndarray,
    playclock_coords: Tuple[int, int, int, int],
    scorebug_coords: Tuple[int, int, int, int],
    sb_result,
    clock_result,
    timestamp: float,
    output_path: Path,
    scale_factor: int = 4,
) -> None:
    """Save a debug image for time-based analysis."""
    pc_x, pc_y, pc_w, pc_h = playclock_coords
    sb_x, sb_y, sb_w, sb_h = scorebug_coords

    # Extract the scorebug area plus some 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 (adjust coordinates)
    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 for detail view
    playclock_region = frame[pc_y : pc_y + pc_h, pc_x : pc_x + pc_w].copy()
    pc_scaled = cv2.resize(playclock_region, None, fx=10, fy=10, interpolation=cv2.INTER_NEAREST)

    # Preprocess for visualization
    preprocessed = preprocess_playclock_region(playclock_region, scale_factor)
    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 crop
    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 + 50, composite_width, 3), dtype=np.uint8)

    # Place scorebug
    composite[0 : scorebug_scaled.shape[0], 0 : scorebug_scaled.shape[1]] = scorebug_scaled

    # Place original playclock (scaled)
    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 text
    font = cv2.FONT_HERSHEY_SIMPLEX
    sb_status = f"Scorebug: {sb_result.confidence:.2f}" if sb_result.detected else "Scorebug: NOT DETECTED"

    if clock_result.detected:
        clock_status = f"Clock: {clock_result.value} (conf: {clock_result.confidence:.2f})"
        color = (0, 255, 0)
    else:
        clock_status = f"Clock: NOT DETECTED (conf: {clock_result.confidence:.2f})"
        color = (0, 0, 255)

    text = f"t={seconds_to_timestamp(timestamp)} | {sb_status} | {clock_status}"
    cv2.putText(composite, text, (10, composite_height + 30), font, 0.5, color, 1)

    cv2.imwrite(str(output_path), composite)


def main():
    """Main function to diagnose clock detection over time."""
    # Load Oregon config
    config = load_oregon_config()

    video_path = config["video_path"]
    template_path = config["template_path"]

    # Calculate absolute play clock coordinates
    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"],
    )

    # Time points to check (in seconds)
    # Focus on: early game (works), transition zone, later game (doesn't work)
    time_points = [
        # Pre-game (should not work)
        0,
        60,
        120,
        180,
        240,
        300,
        # Early game (should work - plays detected here)
        330,
        360,
        400,
        450,
        500,
        550,
        600,
        650,
        700,
        750,
        # After last detected play (~723s)
        800,
        850,
        900,
        950,
        1000,
        # Much later
        1200,
        1500,
        2000,
        2500,
        3000,
        3600,
        # Second half
        4200,
        4800,
        5400,
        6000,
        6600,
        7200,
        7800,
    ]

    logger.info("Oregon Video Clock Detection Analysis Over Time")
    logger.info("=" * 80)

    diagnose_over_time(
        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/oregon_time_analysis",
        time_points=time_points,
        samples_per_point=20,
        sample_interval=0.5,
    )


if __name__ == "__main__":
    main()