Skip to content

Commit f788f77

Browse files
committed
[api] Make sure all values are temporal
1 parent ebe45af commit f788f77

File tree

14 files changed

+168
-105
lines changed

14 files changed

+168
-105
lines changed

scenedetect.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@
227227
# Compression amount for png images (0 to 9). Only affects size, not quality.
228228
#compression = 3
229229

230-
# Number of frames to ignore around each scene cut when selecting frames.
230+
# Padding around each scene cut when selecting frames. Accepts a number of frames (1),
231+
# seconds with `s` suffix (0.1s), or timecode (00:00:00.100).
231232
#frame-margin = 1
232233

233234
# Resize by scale factor (0.5 = half, 1.0 = same, 2.0 = double).

scenedetect/_cli/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,11 +1397,11 @@ def split_video_command(
13971397
@click.option(
13981398
"-m",
13991399
"--frame-margin",
1400-
metavar="N",
1400+
metavar="DURATION",
14011401
default=None,
1402-
type=click.INT,
1403-
help="Number of frames to ignore at beginning/end of scenes when saving images. Controls temporal padding on scene boundaries.%s"
1404-
% (USER_CONFIG.get_help_string("save-images", "num-images")),
1402+
type=click.STRING,
1403+
help="Padding around the beginning/end of each scene used when selecting which frames to extract. DURATION can be specified in frames (-m 1), in seconds with `s` suffix (-m 0.1s), or timecode (-m 00:00:00.100).%s"
1404+
% (USER_CONFIG.get_help_string("save-images", "frame-margin")),
14051405
)
14061406
@click.option(
14071407
"--scale",
@@ -1441,7 +1441,7 @@ def save_images_command(
14411441
quality: ty.Optional[int] = None,
14421442
png: bool = False,
14431443
compression: ty.Optional[int] = None,
1444-
frame_margin: ty.Optional[int] = None,
1444+
frame_margin: ty.Optional[str] = None,
14451445
scale: ty.Optional[float] = None,
14461446
height: ty.Optional[int] = None,
14471447
width: ty.Optional[int] = None,

scenedetect/_cli/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ class XmlFormat(Enum):
412412
"compression": RangeValue(3, min_val=0, max_val=9),
413413
"filename": "$VIDEO_NAME-Scene-$SCENE_NUMBER-$IMAGE_NUMBER",
414414
"format": "jpeg",
415-
"frame-margin": 1,
415+
"frame-margin": TimecodeValue(1),
416416
"height": 0,
417417
"num-images": 3,
418418
"output": None,

scenedetect/detector.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
event (in, out, cut, etc...).
2525
"""
2626

27+
import math
2728
import typing as ty
2829
from abc import ABC, abstractmethod
2930
from enum import Enum
@@ -114,26 +115,48 @@ class Mode(Enum):
114115
SUPPRESS = 1
115116
"""Suppress consecutive cuts until the filter length has passed."""
116117

117-
def __init__(self, mode: Mode, length: int):
118+
def __init__(self, mode: Mode, length: ty.Union[int, float, str]):
118119
"""
119120
Arguments:
120121
mode: The mode to use when enforcing `length`.
121-
length: Number of frames to use when filtering cuts.
122+
length: Minimum scene length. Accepts an `int` (number of frames), `float` (seconds),
123+
or `str` (timecode, e.g. ``"0.6s"`` or ``"00:00:00.600"``).
122124
"""
123125
self._mode = mode
124-
self._filter_length = length # Number of frames to use for activating the filter.
125-
self._filter_secs: ty.Optional[float] = None # Threshold in seconds, computed on first use.
126+
# Frame count (int) and seconds (float) representations of `length`. Exactly one is
127+
# populated up front; the other is computed on the first frame once the framerate is
128+
# known. Temporal inputs (float/non-digit str) populate `_filter_secs`; integer inputs
129+
# (int/digit str) populate `_filter_length`.
130+
self._filter_length: int = 0
131+
self._filter_secs: ty.Optional[float] = None
132+
if isinstance(length, float):
133+
self._filter_secs = length
134+
elif isinstance(length, str) and not length.strip().isdigit():
135+
self._filter_secs = FrameTimecode(timecode=length, fps=100.0).seconds
136+
else:
137+
self._filter_length = int(length)
126138
self._last_above = None # Last frame above threshold.
127139
self._merge_enabled = False # Used to disable merging until at least one cut was found.
128140
self._merge_triggered = False # True when the merge filter is active.
129141
self._merge_start = None # Frame number where we started the merge filter.
130142

131143
@property
132144
def max_behind(self) -> int:
133-
return 0 if self._mode == FlashFilter.Mode.SUPPRESS else self._filter_length
145+
if self._mode == FlashFilter.Mode.SUPPRESS:
146+
return 0
147+
if self._filter_secs is not None:
148+
# Estimate using 240fps so the event buffer is large enough for any reasonable input.
149+
return math.ceil(self._filter_secs * 240.0)
150+
return self._filter_length
151+
152+
@property
153+
def _is_disabled(self) -> bool:
154+
if self._filter_secs is not None:
155+
return self._filter_secs <= 0.0
156+
return self._filter_length <= 0
134157

135158
def filter(self, timecode: FrameTimecode, above_threshold: bool) -> ty.List[FrameTimecode]:
136-
if not self._filter_length > 0:
159+
if self._is_disabled:
137160
return [timecode] if above_threshold else []
138161
if self._last_above is None:
139162
self._last_above = timecode

scenedetect/detectors/adaptive_detector.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class AdaptiveDetector(ContentDetector):
3838
def __init__(
3939
self,
4040
adaptive_threshold: float = 3.0,
41-
min_scene_len: int = 15,
41+
min_scene_len: ty.Union[int, float, str] = 15,
4242
window_width: int = 2,
4343
min_content_val: float = 15.0,
4444
weights: ContentDetector.Components = ContentDetector.DEFAULT_COMPONENT_WEIGHTS,
@@ -49,8 +49,9 @@ def __init__(
4949
Arguments:
5050
adaptive_threshold: Threshold (float) that score ratio must exceed to trigger a
5151
new scene (see frame metric adaptive_ratio in stats file).
52-
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
53-
be added to the scene list. Can be an int or FrameTimecode type.
52+
min_scene_len: Once a cut is detected, this much time must pass before a new one can
53+
be added to the scene list. Accepts an int (frames), float (seconds), or
54+
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
5455
window_width: Size of window (number of frames) before and after each frame to
5556
average together in order to detect deviations from the mean. Must be at least 1.
5657
min_content_val: Minimum threshold (float) that the content_val must exceed in order to

scenedetect/detectors/content_detector.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class _FrameData:
104104
def __init__(
105105
self,
106106
threshold: float = 27.0,
107-
min_scene_len: int = 15,
107+
min_scene_len: ty.Union[int, float, str] = 15,
108108
weights: "ContentDetector.Components" = DEFAULT_COMPONENT_WEIGHTS,
109109
luma_only: bool = False,
110110
kernel_size: ty.Optional[int] = None,
@@ -113,8 +113,9 @@ def __init__(
113113
"""
114114
Arguments:
115115
threshold: Threshold the average change in pixel intensity must exceed to trigger a cut.
116-
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
117-
be added to the scene list. Can be an int or FrameTimecode type.
116+
min_scene_len: Once a cut is detected, this much time must pass before a new one can
117+
be added to the scene list. Accepts an int (frames), float (seconds), or
118+
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
118119
weights: Weight to place on each component when calculating frame score
119120
(`content_val` in a statsfile, the value `threshold` is compared against).
120121
luma_only: If True, only considers changes in the luminance channel of the video.

scenedetect/detectors/hash_detector.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,17 @@ class HashDetector(SceneDetector):
4141
size: Size of square of low frequency data to use for the DCT
4242
lowpass: How much high frequency information to filter from the DCT. A value of 2 means
4343
keep lower 1/2 of the frequency data, 4 means only keep 1/4, etc...
44-
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
45-
be added to the scene list. Can be an int or FrameTimecode type.
44+
min_scene_len: Once a cut is detected, this much time must pass before a new one can
45+
be added to the scene list. Accepts an int (frames), float (seconds), or
46+
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
4647
"""
4748

4849
def __init__(
4950
self,
5051
threshold: float = 0.395,
5152
size: int = 16,
5253
lowpass: int = 2,
53-
min_scene_len: int = 15,
54+
min_scene_len: ty.Union[int, float, str] = 15,
5455
):
5556
super(HashDetector, self).__init__()
5657
self._threshold = threshold

scenedetect/detectors/histogram_detector.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,22 @@ class HistogramDetector(SceneDetector):
3030

3131
METRIC_KEYS = ["hist_diff"]
3232

33-
def __init__(self, threshold: float = 0.05, bins: int = 256, min_scene_len: int = 15):
33+
def __init__(
34+
self,
35+
threshold: float = 0.05,
36+
bins: int = 256,
37+
min_scene_len: ty.Union[int, float, str] = 15,
38+
):
3439
"""
3540
Arguments:
3641
threshold: maximum relative difference between 0.0 and 1.0 that the histograms can
3742
differ. Histograms are calculated on the Y channel after converting the frame to
3843
YUV, and normalized based on the number of bins. Higher dicfferences imply greater
3944
change in content, so larger threshold values are less sensitive to cuts.
4045
bins: Number of bins to use for the histogram.
41-
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
42-
be added to the scene list. Can be an int or FrameTimecode type.
46+
min_scene_len: Once a cut is detected, this much time must pass before a new one can
47+
be added to the scene list. Accepts an int (frames), float (seconds), or
48+
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
4349
"""
4450
super().__init__()
4551
# Internally, threshold represents the correlation between two histograms and has values

scenedetect/detectors/threshold_detector.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class Method(Enum):
4848
def __init__(
4949
self,
5050
threshold: float = 12,
51-
min_scene_len: int = 15,
51+
min_scene_len: ty.Union[int, float, str] = 15,
5252
fade_bias: float = 0.0,
5353
add_final_scene: bool = False,
5454
method: Method = Method.FLOOR,
@@ -58,8 +58,9 @@ def __init__(
5858
Arguments:
5959
threshold: 8-bit intensity value that each pixel value (R, G, and B)
6060
must be <= to in order to trigger a fade in/out.
61-
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
62-
be added to the scene list. Can be an int or FrameTimecode type.
61+
min_scene_len: Once a cut is detected, this much time must pass before a new one can
62+
be added to the scene list. Accepts an int (frames), float (seconds), or
63+
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
6364
fade_bias: Float between -1.0 and +1.0 representing the percentage of
6465
timecode skew for the start of a scene (-1.0 causing a cut at the
6566
fade-to-black, 0.0 in the middle, and +1.0 causing the cut to be

scenedetect/detectors/transnet_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def __init__(
135135
model_path: ty.Union[str, Path] = "tests/resources/transnetv2.onnx",
136136
onnx_providers: ty.Union[ty.List[str], None] = None,
137137
threshold: float = 0.5,
138-
min_scene_len: int = 15,
138+
min_scene_len: ty.Union[int, float, str] = 15,
139139
filter_mode: FlashFilter.Mode = FlashFilter.Mode.MERGE,
140140
):
141141
super().__init__()

0 commit comments

Comments
 (0)