Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion scenedetect.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@
# Compression amount for png images (0 to 9). Only affects size, not quality.
#compression = 3

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

# Resize by scale factor (0.5 = half, 1.0 = same, 2.0 = double).
Expand Down
10 changes: 5 additions & 5 deletions scenedetect/_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,11 +1397,11 @@ def split_video_command(
@click.option(
"-m",
"--frame-margin",
metavar="N",
metavar="DURATION",
default=None,
type=click.INT,
help="Number of frames to ignore at beginning/end of scenes when saving images. Controls temporal padding on scene boundaries.%s"
% (USER_CONFIG.get_help_string("save-images", "num-images")),
type=click.STRING,
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"
% (USER_CONFIG.get_help_string("save-images", "frame-margin")),
)
@click.option(
"--scale",
Expand Down Expand Up @@ -1441,7 +1441,7 @@ def save_images_command(
quality: ty.Optional[int] = None,
png: bool = False,
compression: ty.Optional[int] = None,
frame_margin: ty.Optional[int] = None,
frame_margin: ty.Optional[str] = None,
scale: ty.Optional[float] = None,
height: ty.Optional[int] = None,
width: ty.Optional[int] = None,
Expand Down
2 changes: 1 addition & 1 deletion scenedetect/_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ class XmlFormat(Enum):
"compression": RangeValue(3, min_val=0, max_val=9),
"filename": "$VIDEO_NAME-Scene-$SCENE_NUMBER-$IMAGE_NUMBER",
"format": "jpeg",
"frame-margin": 1,
"frame-margin": TimecodeValue(1),
"height": 0,
"num-images": 3,
"output": None,
Expand Down
35 changes: 29 additions & 6 deletions scenedetect/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
event (in, out, cut, etc...).
"""

import math
import typing as ty
from abc import ABC, abstractmethod
from enum import Enum
Expand Down Expand Up @@ -114,26 +115,48 @@ class Mode(Enum):
SUPPRESS = 1
"""Suppress consecutive cuts until the filter length has passed."""

def __init__(self, mode: Mode, length: int):
def __init__(self, mode: Mode, length: ty.Union[int, float, str]):
"""
Arguments:
mode: The mode to use when enforcing `length`.
length: Number of frames to use when filtering cuts.
length: Minimum scene length. Accepts an `int` (number of frames), `float` (seconds),
or `str` (timecode, e.g. ``"0.6s"`` or ``"00:00:00.600"``).
"""
self._mode = mode
self._filter_length = length # Number of frames to use for activating the filter.
self._filter_secs: ty.Optional[float] = None # Threshold in seconds, computed on first use.
# Frame count (int) and seconds (float) representations of `length`. Exactly one is
# populated up front; the other is computed on the first frame once the framerate is
# known. Temporal inputs (float/non-digit str) populate `_filter_secs`; integer inputs
# (int/digit str) populate `_filter_length`.
self._filter_length: int = 0
self._filter_secs: ty.Optional[float] = None
if isinstance(length, float):
self._filter_secs = length
elif isinstance(length, str) and not length.strip().isdigit():
self._filter_secs = FrameTimecode(timecode=length, fps=100.0).seconds
else:
self._filter_length = int(length)
self._last_above = None # Last frame above threshold.
self._merge_enabled = False # Used to disable merging until at least one cut was found.
self._merge_triggered = False # True when the merge filter is active.
self._merge_start = None # Frame number where we started the merge filter.

@property
def max_behind(self) -> int:
return 0 if self._mode == FlashFilter.Mode.SUPPRESS else self._filter_length
if self._mode == FlashFilter.Mode.SUPPRESS:
return 0
if self._filter_secs is not None:
# Estimate using 240fps so the event buffer is large enough for any reasonable input.
return math.ceil(self._filter_secs * 240.0)
return self._filter_length

@property
def _is_disabled(self) -> bool:
if self._filter_secs is not None:
return self._filter_secs <= 0.0
return self._filter_length <= 0

def filter(self, timecode: FrameTimecode, above_threshold: bool) -> ty.List[FrameTimecode]:
if not self._filter_length > 0:
if self._is_disabled:
return [timecode] if above_threshold else []
if self._last_above is None:
self._last_above = timecode
Expand Down
7 changes: 4 additions & 3 deletions scenedetect/detectors/adaptive_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AdaptiveDetector(ContentDetector):
def __init__(
self,
adaptive_threshold: float = 3.0,
min_scene_len: int = 15,
min_scene_len: ty.Union[int, float, str] = 15,
window_width: int = 2,
min_content_val: float = 15.0,
weights: ContentDetector.Components = ContentDetector.DEFAULT_COMPONENT_WEIGHTS,
Expand All @@ -49,8 +49,9 @@ def __init__(
Arguments:
adaptive_threshold: Threshold (float) that score ratio must exceed to trigger a
new scene (see frame metric adaptive_ratio in stats file).
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
be added to the scene list. Can be an int or FrameTimecode type.
min_scene_len: Once a cut is detected, this much time must pass before a new one can
be added to the scene list. Accepts an int (frames), float (seconds), or
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
window_width: Size of window (number of frames) before and after each frame to
average together in order to detect deviations from the mean. Must be at least 1.
min_content_val: Minimum threshold (float) that the content_val must exceed in order to
Expand Down
7 changes: 4 additions & 3 deletions scenedetect/detectors/content_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class _FrameData:
def __init__(
self,
threshold: float = 27.0,
min_scene_len: int = 15,
min_scene_len: ty.Union[int, float, str] = 15,
weights: "ContentDetector.Components" = DEFAULT_COMPONENT_WEIGHTS,
luma_only: bool = False,
kernel_size: ty.Optional[int] = None,
Expand All @@ -113,8 +113,9 @@ def __init__(
"""
Arguments:
threshold: Threshold the average change in pixel intensity must exceed to trigger a cut.
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
be added to the scene list. Can be an int or FrameTimecode type.
min_scene_len: Once a cut is detected, this much time must pass before a new one can
be added to the scene list. Accepts an int (frames), float (seconds), or
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
weights: Weight to place on each component when calculating frame score
(`content_val` in a statsfile, the value `threshold` is compared against).
luma_only: If True, only considers changes in the luminance channel of the video.
Expand Down
7 changes: 4 additions & 3 deletions scenedetect/detectors/hash_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,17 @@ class HashDetector(SceneDetector):
size: Size of square of low frequency data to use for the DCT
lowpass: How much high frequency information to filter from the DCT. A value of 2 means
keep lower 1/2 of the frequency data, 4 means only keep 1/4, etc...
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
be added to the scene list. Can be an int or FrameTimecode type.
min_scene_len: Once a cut is detected, this much time must pass before a new one can
be added to the scene list. Accepts an int (frames), float (seconds), or
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
"""

def __init__(
self,
threshold: float = 0.395,
size: int = 16,
lowpass: int = 2,
min_scene_len: int = 15,
min_scene_len: ty.Union[int, float, str] = 15,
):
super(HashDetector, self).__init__()
self._threshold = threshold
Expand Down
12 changes: 9 additions & 3 deletions scenedetect/detectors/histogram_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,22 @@ class HistogramDetector(SceneDetector):

METRIC_KEYS = ["hist_diff"]

def __init__(self, threshold: float = 0.05, bins: int = 256, min_scene_len: int = 15):
def __init__(
self,
threshold: float = 0.05,
bins: int = 256,
min_scene_len: ty.Union[int, float, str] = 15,
):
"""
Arguments:
threshold: maximum relative difference between 0.0 and 1.0 that the histograms can
differ. Histograms are calculated on the Y channel after converting the frame to
YUV, and normalized based on the number of bins. Higher dicfferences imply greater
change in content, so larger threshold values are less sensitive to cuts.
bins: Number of bins to use for the histogram.
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
be added to the scene list. Can be an int or FrameTimecode type.
min_scene_len: Once a cut is detected, this much time must pass before a new one can
be added to the scene list. Accepts an int (frames), float (seconds), or
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
"""
super().__init__()
# Internally, threshold represents the correlation between two histograms and has values
Expand Down
7 changes: 4 additions & 3 deletions scenedetect/detectors/threshold_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class Method(Enum):
def __init__(
self,
threshold: float = 12,
min_scene_len: int = 15,
min_scene_len: ty.Union[int, float, str] = 15,
fade_bias: float = 0.0,
add_final_scene: bool = False,
method: Method = Method.FLOOR,
Expand All @@ -58,8 +58,9 @@ def __init__(
Arguments:
threshold: 8-bit intensity value that each pixel value (R, G, and B)
must be <= to in order to trigger a fade in/out.
min_scene_len: Once a cut is detected, this many frames must pass before a new one can
be added to the scene list. Can be an int or FrameTimecode type.
min_scene_len: Once a cut is detected, this much time must pass before a new one can
be added to the scene list. Accepts an int (frames), float (seconds), or
str (e.g. ``"0.6s"``, ``"00:00:00.600"``).
fade_bias: Float between -1.0 and +1.0 representing the percentage of
timecode skew for the start of a scene (-1.0 causing a cut at the
fade-to-black, 0.0 in the middle, and +1.0 causing the cut to be
Expand Down
2 changes: 1 addition & 1 deletion scenedetect/detectors/transnet_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def __init__(
model_path: ty.Union[str, Path] = "tests/resources/transnetv2.onnx",
onnx_providers: ty.Union[ty.List[str], None] = None,
threshold: float = 0.5,
min_scene_len: int = 15,
min_scene_len: ty.Union[int, float, str] = 15,
filter_mode: FlashFilter.Mode = FlashFilter.Mode.MERGE,
):
super().__init__()
Expand Down
Loading
Loading