Skip to content

Commit 09057df

Browse files
committed
Revert "[detectors] Integrate FlashFilter with AdaptiveDetector #35"
Reason: Performs worse due to AdaptiveDetector's windowing algorithm, which already acts as a bit of a filter. Will add the filter as a detector-only option. This reverts commit e8c59ad.
1 parent e8c59ad commit 09057df

File tree

7 files changed

+126
-123
lines changed

7 files changed

+126
-123
lines changed

scenedetect/_cli/config.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,6 @@
3333

3434
VALID_PYAV_THREAD_MODES = ['NONE', 'SLICE', 'FRAME', 'AUTO']
3535

36-
DEPRECATED_CONFIG_OPTIONS = {
37-
"global": {"drop-short-scenes"},
38-
"detect-adaptive": {"min-delta-hsv"},
39-
}
40-
4136

4237
class OptionParseFailure(Exception):
4338
"""Raised when a value provided in a user config file fails validation."""
@@ -256,7 +251,8 @@ class FlashFilterMode(Enum):
256251
DEFAULT_JPG_QUALITY = 95
257252
DEFAULT_WEBP_QUALITY = 100
258253

259-
# TODO(v0.7): Remove deprecated [detect-adaptive] min-delta-hsv and [global] drop-short-scenes
254+
# TODO(v0.6.4): Warn if [detect-adaptive] min-delta-hsv and [global] drop-short-scenes are used.
255+
# TODO(v0.7): Remove [detect-adaptive] min-delta-hsv and [global] drop-short-scenes
260256
CONFIG_MAP: ConfigDict = {
261257
'backend-opencv': {
262258
'max-decode-attempts': 5,
@@ -547,14 +543,6 @@ def _load_from_disk(self, path=None):
547543
for log_str in errors:
548544
self._init_log.append((logging.ERROR, log_str))
549545
raise ConfigLoadFailure(self._init_log)
550-
for command in self._config:
551-
for option in self._config[command]:
552-
if (command in DEPRECATED_CONFIG_OPTIONS
553-
and option in DEPRECATED_CONFIG_OPTIONS[command]):
554-
self._init_log.append(
555-
(logging.WARNING, "WARNING: Config file contains deprecated option:\n "
556-
f"[{command}] {option} will be removed in a future version."))
557-
pass
558546

559547
def is_default(self, command: str, option: str) -> bool:
560548
"""True if specified config option is unset (i.e. the default), False otherwise."""

scenedetect/_cli/context.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,11 @@ def handle_options(
275275
if drop_short_scenes:
276276
logger.warning(
277277
"WARNING: --drop-short-scenes is deprecated, use --filter-mode=drop instead.")
278-
if self.config.get_value("global", "drop-short-scenes", drop_short_scenes):
279-
logger.info("drop-short-scenes set, overriding filter-mode")
280-
self.filter_mode = FlashFilterMode.DROP
278+
if filter_mode is None:
279+
self.filter_mode = FlashFilterMode.DROP
281280
else:
282281
self.filter_mode = FlashFilterMode[self.config.get_value("global", "filter-mode",
283282
filter_mode).upper()]
284-
285283
self.merge_last_scene = merge_last_scene or self.config.get_value(
286284
"global", "merge-last-scene")
287285
self.frame_skip = self.config.get_value("global", "frame-skip", frame_skip)
@@ -362,13 +360,13 @@ def get_detect_adaptive_params(
362360

363361
# TODO(v0.7): Remove these branches when removing -d/--min-delta-hsv.
364362
if min_delta_hsv is not None:
365-
logger.error("-d/--min-delta-hsv is deprecated, use -c/--min-content-val instead.")
363+
logger.error('-d/--min-delta-hsv is deprecated, use -c/--min-content-val instead.')
366364
if min_content_val is None:
367365
min_content_val = min_delta_hsv
368366
# Handle case where deprecated min-delta-hsv is set, and use it to set min-content-val.
369367
if not self.config.is_default("detect-adaptive", "min-delta-hsv"):
370-
logger.error("[detect-adaptive] config file option `min-delta-hsv` is deprecated"
371-
", use `min-delta-hsv` instead.")
368+
logger.error('[detect-adaptive] config file option `min-delta-hsv` is deprecated'
369+
', use `min-delta-hsv` instead.')
372370
if self.config.is_default("detect-adaptive", "min-content-val"):
373371
self.config.config_dict["detect-adaptive"]["min-content-val"] = (
374372
self.config.config_dict["detect-adaptive"]["min-deleta-hsv"])
@@ -381,21 +379,21 @@ def get_detect_adaptive_params(
381379
weights = ContentDetector.Components(*weights)
382380
except ValueError as ex:
383381
logger.debug(str(ex))
384-
raise click.BadParameter(str(ex), param_hint="weights")
382+
raise click.BadParameter(str(ex), param_hint='weights')
385383
return {
386-
"adaptive_threshold":
384+
'adaptive_threshold':
387385
self.config.get_value("detect-adaptive", "threshold", threshold),
388-
"flash_filter":
389-
self._init_flash_filter("detect-content", min_scene_len),
390-
"kernel_size":
386+
'weights':
387+
self.config.get_value("detect-adaptive", "weights", weights),
388+
'kernel_size':
391389
self.config.get_value("detect-adaptive", "kernel-size", kernel_size),
392-
"luma_only":
390+
'luma_only':
393391
luma_only or self.config.get_value("detect-adaptive", "luma-only"),
394-
"min_content_val":
392+
'min_content_val':
395393
self.config.get_value("detect-adaptive", "min-content-val", min_content_val),
396-
"weights":
397-
self.config.get_value("detect-adaptive", "weights", weights),
398-
"window_width":
394+
'min_scene_len':
395+
min_scene_len,
396+
'window_width':
399397
self.config.get_value("detect-adaptive", "frame-window", frame_window),
400398
}
401399

scenedetect/detectors/adaptive_detector.py

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@
1818
"""
1919

2020
from logging import getLogger
21-
import typing as ty
21+
from typing import List, Optional
2222

2323
import numpy as np
2424

2525
from scenedetect.detectors import ContentDetector
26-
from scenedetect.scene_detector import FlashFilter
2726

2827
logger = getLogger('pyscenedetect')
2928

@@ -44,19 +43,15 @@ def __init__(
4443
min_content_val: float = 15.0,
4544
weights: ContentDetector.Components = ContentDetector.DEFAULT_COMPONENT_WEIGHTS,
4645
luma_only: bool = False,
47-
kernel_size: ty.Optional[int] = None,
48-
flash_filter: ty.Optional[FlashFilter] = None,
46+
kernel_size: Optional[int] = None,
4947
video_manager=None,
50-
min_delta_hsv: ty.Optional[float] = None,
48+
min_delta_hsv: Optional[float] = None,
5149
):
5250
"""
5351
Arguments:
5452
adaptive_threshold: Threshold (float) that score ratio must exceed to trigger a
5553
new scene (see frame metric adaptive_ratio in stats file).
56-
min_scene_len: Defines the minimum length of a given scene. Sequences of consecutive
57-
cuts that occur closer than this length will be merged. Equivalent to setting
58-
`flash_filter = FlashFilter(length=min_scene_len)`.
59-
Ignored if `flash_filter` is set.
54+
min_scene_len: Minimum length of any scene.
6055
window_width: Size of window (number of frames) before and after each frame to
6156
average together in order to detect deviations from the mean. Must be at least 1.
6257
min_content_val: Minimum threshold (float) that the content_val must exceed in order to
@@ -70,10 +65,8 @@ def __init__(
7065
Overrides `weights` if both are set.
7166
kernel_size: Size of kernel to use for post edge detection filtering. If None,
7267
automatically set based on video resolution.
73-
flash_filter: Filter to use for scene length compliance. If None, initialized as
74-
`FlashFilter(length=min_scene_len)`. If set, `min_scene_length` is ignored.
75-
video_manager: [DEPRECATED] DO NOT USE.
76-
min_delta_hsv: [DEPRECATED] DO NOT USE.
68+
video_manager: [DEPRECATED] DO NOT USE. For backwards compatibility only.
69+
min_delta_hsv: [DEPRECATED] DO NOT USE. Use `min_content_val` instead.
7770
"""
7871
# TODO(v0.7): Replace with DeprecationWarning that `video_manager` and `min_delta_hsv` will
7972
# be removed in v0.8.
@@ -84,35 +77,44 @@ def __init__(
8477
min_content_val = min_delta_hsv
8578
if window_width < 1:
8679
raise ValueError('window_width must be at least 1.')
80+
8781
super().__init__(
8882
threshold=255.0,
89-
min_scene_len=min_scene_len,
83+
min_scene_len=0,
9084
weights=weights,
9185
luma_only=luma_only,
9286
kernel_size=kernel_size,
93-
flash_filter=flash_filter,
9487
)
95-
self._adaptive_threshold = adaptive_threshold
96-
self._min_content_val = min_content_val
97-
self._window_width = window_width
88+
89+
# TODO: Turn these options into properties.
90+
self.min_scene_len = min_scene_len
91+
self.adaptive_threshold = adaptive_threshold
92+
self.min_content_val = min_content_val
93+
self.window_width = window_width
94+
9895
self._adaptive_ratio_key = AdaptiveDetector.ADAPTIVE_RATIO_KEY_TEMPLATE.format(
9996
window_width=window_width, luma_only='' if not luma_only else '_lum')
97+
self._first_frame_num = None
98+
99+
# NOTE: This must be different than `self._last_scene_cut` which is used by the base class.
100+
self._last_cut: Optional[int] = None
101+
100102
self._buffer = []
101103

102104
@property
103105
def event_buffer_length(self) -> int:
104106
"""Number of frames any detected cuts will be behind the current frame due to buffering."""
105-
return self._window_width
107+
return self.window_width
106108

107-
def get_metrics(self) -> ty.List[str]:
109+
def get_metrics(self) -> List[str]:
108110
"""Combines base ContentDetector metric keys with the AdaptiveDetector one."""
109111
return super().get_metrics() + [self._adaptive_ratio_key]
110112

111113
def stats_manager_required(self) -> bool:
112114
"""Not required for AdaptiveDetector."""
113115
return False
114116

115-
def process_frame(self, frame_num: int, frame_img: ty.Optional[np.ndarray]) -> ty.List[int]:
117+
def process_frame(self, frame_num: int, frame_img: Optional[np.ndarray]) -> List[int]:
116118
"""Process the next frame. `frame_num` is assumed to be sequential.
117119
118120
Args:
@@ -124,33 +126,47 @@ def process_frame(self, frame_num: int, frame_img: ty.Optional[np.ndarray]) -> t
124126
List[int]: List of frames where scene cuts have been detected. There may be 0
125127
or more frames in the list, and not necessarily the same as frame_num.
126128
"""
127-
frame_score = self._calculate_frame_score(frame_num=frame_num, frame_img=frame_img)
128-
required_frames = 1 + (2 * self._window_width)
129-
self._buffer.append((frame_num, frame_score))
129+
130+
# TODO(#283): Merge this with ContentDetector and turn it on by default.
131+
132+
super().process_frame(frame_num=frame_num, frame_img=frame_img)
133+
134+
# Initialize last scene cut point at the beginning of the frames of interest.
135+
if self._last_cut is None:
136+
self._last_cut = frame_num
137+
138+
required_frames = 1 + (2 * self.window_width)
139+
self._buffer.append((frame_num, self._frame_score))
130140
if not len(self._buffer) >= required_frames:
131141
return []
132142
self._buffer = self._buffer[-required_frames:]
133-
target = self._buffer[self._window_width]
143+
target = self._buffer[self.window_width]
134144
average_window_score = (
135-
sum(frame[1] for i, frame in enumerate(self._buffer) if i != self._window_width) /
136-
(2.0 * self._window_width))
145+
sum(frame[1] for i, frame in enumerate(self._buffer) if i != self.window_width) /
146+
(2.0 * self.window_width))
147+
137148
average_is_zero = abs(average_window_score) < 0.00001
149+
138150
adaptive_ratio = 0.0
139151
if not average_is_zero:
140152
adaptive_ratio = min(target[1] / average_window_score, 255.0)
141-
elif average_is_zero and target[1] >= self._min_content_val:
153+
elif average_is_zero and target[1] >= self.min_content_val:
142154
# if we would have divided by zero, set adaptive_ratio to the max (255.0)
143155
adaptive_ratio = 255.0
144156
if self.stats_manager is not None:
145157
self.stats_manager.set_metrics(target[0], {self._adaptive_ratio_key: adaptive_ratio})
146158

147159
# Check to see if adaptive_ratio exceeds the adaptive_threshold as well as there
148160
# being a large enough content_val to trigger a cut
149-
found_cut: bool = (
150-
adaptive_ratio >= self._adaptive_threshold and target[1] >= self._min_content_val)
151-
return self._flash_filter.apply(frame_num=target[0], found_cut=found_cut)
152-
153-
def get_content_val(self, frame_num: int) -> ty.Optional[float]:
161+
threshold_met: bool = (
162+
adaptive_ratio >= self.adaptive_threshold and target[1] >= self.min_content_val)
163+
min_length_met: bool = (frame_num - self._last_cut) >= self.min_scene_len
164+
if threshold_met and min_length_met:
165+
self._last_cut = target[0]
166+
return [target[0]]
167+
return []
168+
169+
def get_content_val(self, frame_num: int) -> Optional[float]:
154170
"""Returns the average content change for a frame."""
155171
# TODO(v0.7): Add DeprecationWarning that `get_content_val` will be removed in v0.7.
156172
logger.error("get_content_val is deprecated and will be removed. Lookup the value"
@@ -159,10 +175,6 @@ def get_content_val(self, frame_num: int) -> ty.Optional[float]:
159175
return self.stats_manager.get_metrics(frame_num, [ContentDetector.FRAME_SCORE_KEY])[0]
160176
return 0.0
161177

162-
def post_process(self, _frame_num: int):
163-
# Already processed frame at self._window_width, process the rest. This ensures we emit any
164-
# cuts the filtering mode might require.
165-
cuts = []
166-
for (frame_num, _) in self._buffer[self._window_width + 1:]:
167-
cuts += self._flash_filter.apply(frame_num=frame_num, found_cut=False)
168-
return cuts
178+
def post_process(self, _unused_frame_num: int):
179+
"""Not required for AdaptiveDetector."""
180+
return []

scenedetect/detectors/content_detector.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,12 @@ def __init__(
122122
kernel_size: Size of kernel for expanding detected edges. Must be odd integer
123123
greater than or equal to 3. If None, automatically set using video resolution.
124124
flash_filter: Filter to use for scene length compliance. If None, initialized as
125-
`FlashFilter(length=min_scene_len)`. If set, `min_scene_length` is ignored.
125+
`FlashFilter(length=min_scene_len)`.
126126
"""
127127
super().__init__()
128128
self._threshold: float = threshold
129+
self._min_scene_len: int = min_scene_len
130+
self._last_above_threshold: ty.Optional[int] = None
129131
self._last_frame: ty.Optional[ContentDetector._FrameData] = None
130132
self._weights: ContentDetector.Components = weights
131133
if luma_only:
@@ -136,6 +138,7 @@ def __init__(
136138
if kernel_size < 3 or kernel_size % 2 == 0:
137139
raise ValueError('kernel_size must be odd integer >= 3')
138140
self._kernel = numpy.ones((kernel_size, kernel_size), numpy.uint8)
141+
self._frame_score: ty.Optional[float] = None
139142
self._flash_filter = flash_filter if not flash_filter is None else FlashFilter(
140143
length=min_scene_len)
141144

@@ -199,9 +202,11 @@ def process_frame(self, frame_num: int, frame_img: numpy.ndarray) -> ty.List[int
199202
ty.List[int]: List of frames where scene cuts have been detected. There may be 0
200203
or more frames in the list, and not necessarily the same as frame_num.
201204
"""
202-
frame_score = self._calculate_frame_score(frame_num, frame_img)
203-
found_cut = frame_score >= self._threshold
204-
return self._flash_filter.apply(frame_num=frame_num, found_cut=found_cut)
205+
self._frame_score = self._calculate_frame_score(frame_num, frame_img)
206+
if self._frame_score is None:
207+
return []
208+
return self._flash_filter.filter(
209+
frame_num=frame_num, found_cut=self._frame_score >= self._threshold)
205210

206211
def _detect_edges(self, lum: numpy.ndarray) -> numpy.ndarray:
207212
"""Detect edges using the luma channel of a frame.

0 commit comments

Comments
 (0)