Skip to content

Commit f8b9c5a

Browse files
committed
[backends] Land initial VFR support for PyAV
This required extensive API changes to the SceneDetector interface and SceneManager.
1 parent 4d8b0bc commit f8b9c5a

20 files changed

+89
-64
lines changed

scenedetect/_cli/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import scenedetect
3030
from scenedetect._cli.config import XmlFormat
3131
from scenedetect._cli.context import CliContext
32-
from scenedetect.frame_timecode import FrameTimecode
32+
from scenedetect.common import FrameTimecode
3333
from scenedetect.platform import get_and_create_path
3434
from scenedetect.scene_manager import (
3535
CutList,

scenedetect/_cli/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525

2626
from platformdirs import user_config_dir
2727

28+
from scenedetect.common import FrameTimecode
2829
from scenedetect.detector import FlashFilter
2930
from scenedetect.detectors import ContentDetector
30-
from scenedetect.frame_timecode import FrameTimecode
3131
from scenedetect.scene_manager import Interpolation
3232
from scenedetect.video_splitter import DEFAULT_FFMPEG_ARGS
3333

scenedetect/_cli/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
ConfigRegistry,
2525
CropValue,
2626
)
27+
from scenedetect.common import MAX_FPS_DELTA, FrameTimecode
2728
from scenedetect.detector import FlashFilter, SceneDetector
2829
from scenedetect.detectors import (
2930
AdaptiveDetector,
@@ -32,7 +33,6 @@
3233
HistogramDetector,
3334
ThresholdDetector,
3435
)
35-
from scenedetect.frame_timecode import MAX_FPS_DELTA, FrameTimecode
3636
from scenedetect.platform import init_logger
3737
from scenedetect.scene_manager import Interpolation, SceneManager
3838
from scenedetect.stats_manager import StatsManager

scenedetect/_cli/controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from scenedetect._cli.context import CliContext
2222
from scenedetect.backends import VideoStreamCv2, VideoStreamMoviePy
23-
from scenedetect.frame_timecode import FrameTimecode
23+
from scenedetect.common import FrameTimecode
2424
from scenedetect.platform import get_and_create_path
2525
from scenedetect.scene_manager import CutList, SceneList, get_scenes_from_cuts
2626
from scenedetect.video_stream import SeekError

scenedetect/backends/moviepy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from moviepy.video.io.ffmpeg_reader import FFMPEG_VideoReader
2525

2626
from scenedetect.backends.opencv import VideoStreamCv2
27-
from scenedetect.frame_timecode import FrameTimecode
27+
from scenedetect.common import FrameTimecode
2828
from scenedetect.platform import get_file_name
2929
from scenedetect.video_stream import SeekError, VideoOpenFailure, VideoStream
3030

scenedetect/backends/opencv.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
import cv2
2727
import numpy as np
2828

29-
from scenedetect.common import Timecode
30-
from scenedetect.frame_timecode import MAX_FPS_DELTA, FrameTimecode
29+
from scenedetect.common import MAX_FPS_DELTA, FrameTimecode, Timecode, _USE_PTS_IN_DEVELOPMENT
3130
from scenedetect.platform import get_file_name
3231
from scenedetect.video_stream import (
3332
FrameRateUnavailable,
@@ -46,8 +45,6 @@
4645
" ! ", # gstreamer pipe
4746
)
4847

49-
_USE_PTS_IN_DEVELOPMENT = False
50-
5148

5249
def _get_aspect_ratio(cap: cv2.VideoCapture, epsilon: float = 0.0001) -> float:
5350
"""Display/pixel aspect ratio of the VideoCapture as a float (1.0 represents square pixels)."""

scenedetect/backends/pyav.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,14 @@
1717
import av
1818
import numpy as np
1919

20-
from scenedetect.common import Timecode
21-
from scenedetect.frame_timecode import MAX_FPS_DELTA, FrameTimecode
20+
from scenedetect.common import MAX_FPS_DELTA, FrameTimecode, Timecode, _USE_PTS_IN_DEVELOPMENT
2221
from scenedetect.platform import get_file_name
2322
from scenedetect.video_stream import FrameRateUnavailable, VideoOpenFailure, VideoStream
2423

2524
logger = getLogger("pyscenedetect")
2625

2726
VALID_THREAD_MODES = ["NONE", "SLICE", "FRAME", "AUTO"]
2827

29-
_USE_PTS_IN_DEVELOPMENT = False
30-
3128

3229
class VideoStreamAv(VideoStream):
3330
"""PyAV `av.InputContainer` backend."""
@@ -205,7 +202,12 @@ def frame_number(self) -> int:
205202
"""Current position within stream as the frame number.
206203
207204
Will return 0 until the first frame is `read`."""
205+
208206
if self._frame:
207+
if _USE_PTS_IN_DEVELOPMENT:
208+
return FrameTimecode(
209+
round(self._frame.time * self.frame_rate), self.frame_rate
210+
).frame_num
209211
return self.position.frame_num + 1
210212
return 0
211213

scenedetect/common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@
6666
from dataclasses import dataclass
6767
from fractions import Fraction
6868

69+
70+
_USE_PTS_IN_DEVELOPMENT = False
71+
6972
##
7073
## Type Aliases
7174
##

scenedetect/detector.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import numpy
3131

32+
from scenedetect.common import FrameTimecode, _USE_PTS_IN_DEVELOPMENT
3233
from scenedetect.stats_manager import StatsManager
3334

3435

@@ -46,6 +47,7 @@ class SceneDetector:
4647

4748
# TODO(v0.7): Make this a proper abstract base class.
4849

50+
# TODO(v0.7): This should be a property.
4951
stats_manager: ty.Optional[StatsManager] = None
5052
"""Optional :class:`StatsManager <scenedetect.stats_manager.StatsManager>` to
5153
use for caching frame metrics to and from."""
@@ -67,7 +69,9 @@ def get_metrics(self) -> ty.List[str]:
6769
"""
6870
return []
6971

70-
def process_frame(self, frame_num: int, frame_img: numpy.ndarray) -> ty.List[int]:
72+
def process_frame(
73+
self, timecode: FrameTimecode, frame_img: numpy.ndarray
74+
) -> ty.List[FrameTimecode]:
7175
"""Process the next frame. `frame_num` is assumed to be sequential.
7276
7377
Args:
@@ -84,7 +88,7 @@ def process_frame(self, frame_num: int, frame_img: numpy.ndarray) -> ty.List[int
8488
"""
8589
return []
8690

87-
def post_process(self, frame_num: int) -> ty.List[int]:
91+
def post_process(self, timecode: int) -> ty.List[FrameTimecode]:
8892
"""Post Process: Performs any processing after the last frame has been read.
8993
9094
Prototype method, no actual detection.
@@ -130,15 +134,17 @@ def __init__(self, mode: Mode, length: int):
130134
def max_behind(self) -> int:
131135
return 0 if self._mode == FlashFilter.Mode.SUPPRESS else self._filter_length
132136

133-
def filter(self, frame_num: int, above_threshold: bool) -> ty.List[int]:
137+
def filter(self, timecode: FrameTimecode, above_threshold: bool) -> ty.List[FrameTimecode]:
134138
if not self._filter_length > 0:
135-
return [frame_num] if above_threshold else []
139+
return [timecode] if above_threshold else []
140+
if _USE_PTS_IN_DEVELOPMENT:
141+
raise NotImplementedError("TODO: Change filter to use units of time instead of frames.")
136142
if self._last_above is None:
137-
self._last_above = frame_num
143+
self._last_above = timecode
138144
if self._mode == FlashFilter.Mode.MERGE:
139-
return self._filter_merge(frame_num=frame_num, above_threshold=above_threshold)
145+
return self._filter_merge(frame_num=timecode, above_threshold=above_threshold)
140146
elif self._mode == FlashFilter.Mode.SUPPRESS:
141-
return self._filter_suppress(frame_num=frame_num, above_threshold=above_threshold)
147+
return self._filter_suppress(frame_num=timecode, above_threshold=above_threshold)
142148
raise RuntimeError("Unhandled FlashFilter mode.")
143149

144150
def _filter_suppress(self, frame_num: int, above_threshold: bool) -> ty.List[int]:

scenedetect/detectors/adaptive_detector.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import numpy as np
2323

24+
from scenedetect.common import FrameTimecode
2425
from scenedetect.detectors import ContentDetector
2526

2627
logger = getLogger("pyscenedetect")
@@ -109,7 +110,9 @@ def stats_manager_required(self) -> bool:
109110
"""Not required for AdaptiveDetector."""
110111
return False
111112

112-
def process_frame(self, frame_num: int, frame_img: ty.Optional[np.ndarray]) -> ty.List[int]:
113+
def process_frame(
114+
self, timecode: FrameTimecode, frame_img: ty.Optional[np.ndarray]
115+
) -> ty.List[int]:
113116
"""Process the next frame. `frame_num` is assumed to be sequential.
114117
115118
Args:
@@ -124,14 +127,14 @@ def process_frame(self, frame_num: int, frame_img: ty.Optional[np.ndarray]) -> t
124127

125128
# TODO(#283): Merge this with ContentDetector and turn it on by default.
126129

127-
super().process_frame(frame_num=frame_num, frame_img=frame_img)
130+
super().process_frame(timecode=timecode, frame_img=frame_img)
128131

129132
# Initialize last scene cut point at the beginning of the frames of interest.
130133
if self._last_cut is None:
131-
self._last_cut = frame_num
134+
self._last_cut = timecode
132135

133136
required_frames = 1 + (2 * self.window_width)
134-
self._buffer.append((frame_num, self._frame_score))
137+
self._buffer.append((timecode, self._frame_score))
135138
if not len(self._buffer) >= required_frames:
136139
return []
137140
self._buffer = self._buffer[-required_frames:]
@@ -156,7 +159,7 @@ def process_frame(self, frame_num: int, frame_img: ty.Optional[np.ndarray]) -> t
156159
threshold_met: bool = (
157160
adaptive_ratio >= self.adaptive_threshold and target_score >= self.min_content_val
158161
)
159-
min_length_met: bool = (frame_num - self._last_cut) >= self.min_scene_len
162+
min_length_met: bool = (timecode - self._last_cut) >= self.min_scene_len
160163
if threshold_met and min_length_met:
161164
self._last_cut = target_frame
162165
return [target_frame]

0 commit comments

Comments
 (0)