File size: 8,168 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
"""
Test accurate frame extraction using ffmpeg.

This script demonstrates that ffmpeg correctly handles VFR video
timestamps while OpenCV does not.

Usage:
    python scripts/test_ffmpeg_frame_extraction.py
"""

import json
import logging
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any, Dict, List, Optional, 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

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)


def load_texas_config() -> Dict[str, Any]:
    """Load the saved config for Texas video."""
    config_path = Path("output/OSU_vs_Texas_01_10_25_config.json")
    with open(config_path, "r") as f:
        return json.load(f)


def extract_frame_ffmpeg(video_path: str, timestamp: float) -> Optional[np.ndarray]:
    """
    Extract a single frame using ffmpeg for accurate VFR handling.

    Args:
        video_path: Path to video file
        timestamp: Time in seconds

    Returns:
        Frame as numpy array (BGR), or None on failure
    """
    with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
        tmp_path = tmp.name

    try:
        # Use ffmpeg with accurate seeking (-ss before -i for fast seek)
        cmd = [
            "ffmpeg",
            "-ss",
            str(timestamp),
            "-i",
            str(video_path),
            "-frames:v",
            "1",
            "-q:v",
            "2",
            "-loglevel",
            "error",
            tmp_path,
            "-y",
        ]

        result = subprocess.run(cmd, capture_output=True, timeout=30)
        if result.returncode != 0:
            logger.error("ffmpeg failed: %s", result.stderr.decode())
            return None

        frame = cv2.imread(tmp_path)
        return frame

    finally:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)


def extract_frame_opencv(video_path: str, timestamp: float) -> Tuple[Optional[np.ndarray], float]:
    """
    Extract a frame using OpenCV (for comparison).

    Returns tuple of (frame, actual_timestamp).
    """
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        return None, -1.0

    fps = cap.get(cv2.CAP_PROP_FPS)
    frame_num = int(timestamp * fps)
    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num)

    ret, frame = cap.read()
    actual_ts = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0

    cap.release()
    return (frame, actual_ts) if ret else (None, -1.0)


def extract_game_clock_region(frame: np.ndarray, config: Dict[str, Any]) -> np.ndarray:
    """Extract the game clock region (not play clock) for verification."""
    # Game clock is typically in the center of the scorebug
    # For Texas video, the game clock shows MM:SS format
    sb_x = config["scorebug_x"]
    sb_y = config["scorebug_y"]

    # Game clock is roughly at center of scorebug, adjusted based on visual inspection
    # This is around x=560-620, y=663-680 in absolute coordinates
    gc_x = sb_x + 560
    gc_y = sb_y + 16
    gc_w = 60
    gc_h = 20

    return frame[gc_y : gc_y + gc_h, gc_x : gc_x + gc_w].copy()


def main():
    """Test ffmpeg vs OpenCV frame extraction."""
    config = load_texas_config()
    video_path = config["video_path"]

    logger.info("=" * 80)
    logger.info("FFMPEG VS OPENCV FRAME EXTRACTION TEST")
    logger.info("=" * 80)
    logger.info("Video: %s", video_path)
    logger.info("")

    # Initialize play clock reader for verification
    template_dir = "output/debug/digit_templates"
    library = DigitTemplateLibrary()
    library.load(template_dir)

    pc_w, pc_h = config["playclock_width"], config["playclock_height"]
    reader = ReadPlayClock(library, region_width=pc_w, region_height=pc_h)

    playclock_coords = (
        config["scorebug_x"] + config["playclock_x_offset"],
        config["scorebug_y"] + config["playclock_y_offset"],
        config["playclock_width"],
        config["playclock_height"],
    )

    # Test timestamps in the problem area
    test_timestamps = [5928.4, 5929.4, 5930.4, 5931.4, 5932.4, 5933.4, 5934.4]

    output_dir = Path("output/debug/ffmpeg_extraction_test")
    output_dir.mkdir(parents=True, exist_ok=True)

    logger.info("Testing frame extraction at timestamps:")
    logger.info("-" * 40)

    results = []

    for ts in test_timestamps:
        # Extract with ffmpeg
        ffmpeg_frame = extract_frame_ffmpeg(video_path, ts)

        # Extract with OpenCV
        opencv_frame, opencv_actual_ts = extract_frame_opencv(video_path, ts)

        if ffmpeg_frame is None or opencv_frame is None:
            logger.warning("  %.1fs: Extraction failed", ts)
            continue

        # Read play clock from both
        ffmpeg_clock = reader.read_from_fixed_location(ffmpeg_frame, playclock_coords, padding=10)
        opencv_clock = reader.read_from_fixed_location(opencv_frame, playclock_coords, padding=10)

        # Compare
        opencv_error = opencv_actual_ts - ts

        result = {
            "target_ts": ts,
            "opencv_actual_ts": opencv_actual_ts,
            "opencv_error": opencv_error,
            "ffmpeg_clock": ffmpeg_clock.value if ffmpeg_clock.detected else None,
            "opencv_clock": opencv_clock.value if opencv_clock.detected else None,
        }
        results.append(result)

        logger.info(
            "  t=%.1fs: FFmpeg clock=%s, OpenCV clock=%s (actual=%.1fs, err=%+.1fs)",
            ts,
            ffmpeg_clock.value if ffmpeg_clock.detected else "N/A",
            opencv_clock.value if opencv_clock.detected else "N/A",
            opencv_actual_ts,
            opencv_error,
        )

        # Save frames for visual comparison
        cv2.imwrite(str(output_dir / f"ffmpeg_t{ts:.1f}_clock{ffmpeg_clock.value or 'X'}.png"), ffmpeg_frame)
        cv2.imwrite(str(output_dir / f"opencv_t{ts:.1f}_actual{opencv_actual_ts:.1f}_clock{opencv_clock.value or 'X'}.png"), opencv_frame)

    logger.info("")
    logger.info("ANALYSIS")
    logger.info("-" * 40)

    # Check if ffmpeg frames show proper clock progression
    ffmpeg_clocks = [r["ffmpeg_clock"] for r in results if r["ffmpeg_clock"] is not None]
    opencv_clocks = [r["opencv_clock"] for r in results if r["opencv_clock"] is not None]

    logger.info("FFmpeg play clock sequence: %s", ffmpeg_clocks)
    logger.info("OpenCV play clock sequence: %s", opencv_clocks)

    # Check for proper countdown (values should generally decrease or reset at 40)
    def check_monotonic_countdown(clocks):
        """Check if clock values form a reasonable countdown pattern."""
        if len(clocks) < 2:
            return True

        violations = 0
        for i in range(1, len(clocks)):
            # Allow for resets (when clock goes from low value back to 40)
            if clocks[i] > clocks[i - 1] and not (clocks[i - 1] < 10 and clocks[i] >= 35):
                violations += 1
        return violations

    ffmpeg_violations = check_monotonic_countdown(ffmpeg_clocks)
    opencv_violations = check_monotonic_countdown(opencv_clocks)

    logger.info("")
    logger.info("FFmpeg countdown violations: %d", ffmpeg_violations)
    logger.info("OpenCV countdown violations: %d", opencv_violations)

    if ffmpeg_violations < opencv_violations:
        logger.info("")
        logger.info("CONCLUSION: FFmpeg extraction produces correct chronological order!")
        logger.info("RECOMMENDATION: Use ffmpeg for frame extraction instead of OpenCV seeking")
    elif ffmpeg_violations == opencv_violations == 0:
        logger.info("")
        logger.info("CONCLUSION: Both methods appear correct in this sample")
        logger.info("(May need more samples to see the difference)")
    else:
        logger.info("")
        logger.info("CONCLUSION: Both methods show issues - may need re-encoding")

    logger.info("")
    logger.info("Debug frames saved to: %s", output_dir)


if __name__ == "__main__":
    main()