18
18
"""
19
19
20
20
from logging import getLogger
21
- from typing import List , Optional
21
+ import typing as ty
22
22
23
23
import numpy as np
24
24
25
25
from scenedetect .detectors import ContentDetector
26
+ from scenedetect .scene_detector import FlashFilter
26
27
27
28
logger = getLogger ('pyscenedetect' )
28
29
@@ -43,15 +44,19 @@ def __init__(
43
44
min_content_val : float = 15.0 ,
44
45
weights : ContentDetector .Components = ContentDetector .DEFAULT_COMPONENT_WEIGHTS ,
45
46
luma_only : bool = False ,
46
- kernel_size : Optional [int ] = None ,
47
+ kernel_size : ty .Optional [int ] = None ,
48
+ flash_filter : ty .Optional [FlashFilter ] = None ,
47
49
video_manager = None ,
48
- min_delta_hsv : Optional [float ] = None ,
50
+ min_delta_hsv : ty . Optional [float ] = None ,
49
51
):
50
52
"""
51
53
Arguments:
52
54
adaptive_threshold: Threshold (float) that score ratio must exceed to trigger a
53
55
new scene (see frame metric adaptive_ratio in stats file).
54
- min_scene_len: Minimum length of any scene.
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.
55
60
window_width: Size of window (number of frames) before and after each frame to
56
61
average together in order to detect deviations from the mean. Must be at least 1.
57
62
min_content_val: Minimum threshold (float) that the content_val must exceed in order to
@@ -65,8 +70,10 @@ def __init__(
65
70
Overrides `weights` if both are set.
66
71
kernel_size: Size of kernel to use for post edge detection filtering. If None,
67
72
automatically set based on video resolution.
68
- video_manager: [DEPRECATED] DO NOT USE. For backwards compatibility only.
69
- min_delta_hsv: [DEPRECATED] DO NOT USE. Use `min_content_val` instead.
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.
70
77
"""
71
78
# TODO(v0.7): Replace with DeprecationWarning that `video_manager` and `min_delta_hsv` will
72
79
# be removed in v0.8.
@@ -77,44 +84,35 @@ def __init__(
77
84
min_content_val = min_delta_hsv
78
85
if window_width < 1 :
79
86
raise ValueError ('window_width must be at least 1.' )
80
-
81
87
super ().__init__ (
82
88
threshold = 255.0 ,
83
- min_scene_len = 0 ,
89
+ min_scene_len = min_scene_len ,
84
90
weights = weights ,
85
91
luma_only = luma_only ,
86
92
kernel_size = kernel_size ,
93
+ flash_filter = flash_filter ,
87
94
)
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
-
95
+ self ._adaptive_threshold = adaptive_threshold
96
+ self ._min_content_val = min_content_val
97
+ self ._window_width = window_width
95
98
self ._adaptive_ratio_key = AdaptiveDetector .ADAPTIVE_RATIO_KEY_TEMPLATE .format (
96
99
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
-
102
100
self ._buffer = []
103
101
104
102
@property
105
103
def event_buffer_length (self ) -> int :
106
104
"""Number of frames any detected cuts will be behind the current frame due to buffering."""
107
- return self .window_width
105
+ return self ._window_width
108
106
109
- def get_metrics (self ) -> List [str ]:
107
+ def get_metrics (self ) -> ty . List [str ]:
110
108
"""Combines base ContentDetector metric keys with the AdaptiveDetector one."""
111
109
return super ().get_metrics () + [self ._adaptive_ratio_key ]
112
110
113
111
def stats_manager_required (self ) -> bool :
114
112
"""Not required for AdaptiveDetector."""
115
113
return False
116
114
117
- def process_frame (self , frame_num : int , frame_img : Optional [np .ndarray ]) -> List [int ]:
115
+ def process_frame (self , frame_num : int , frame_img : ty . Optional [np .ndarray ]) -> ty . List [int ]:
118
116
"""Process the next frame. `frame_num` is assumed to be sequential.
119
117
120
118
Args:
@@ -126,47 +124,33 @@ def process_frame(self, frame_num: int, frame_img: Optional[np.ndarray]) -> List
126
124
List[int]: List of frames where scene cuts have been detected. There may be 0
127
125
or more frames in the list, and not necessarily the same as frame_num.
128
126
"""
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 ))
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 ))
140
130
if not len (self ._buffer ) >= required_frames :
141
131
return []
142
132
self ._buffer = self ._buffer [- required_frames :]
143
- target = self ._buffer [self .window_width ]
133
+ target = self ._buffer [self ._window_width ]
144
134
average_window_score = (
145
- sum (frame [1 ] for i , frame in enumerate (self ._buffer ) if i != self .window_width ) /
146
- (2.0 * self .window_width ))
147
-
135
+ sum (frame [1 ] for i , frame in enumerate (self ._buffer ) if i != self ._window_width ) /
136
+ (2.0 * self ._window_width ))
148
137
average_is_zero = abs (average_window_score ) < 0.00001
149
-
150
138
adaptive_ratio = 0.0
151
139
if not average_is_zero :
152
140
adaptive_ratio = min (target [1 ] / average_window_score , 255.0 )
153
- elif average_is_zero and target [1 ] >= self .min_content_val :
141
+ elif average_is_zero and target [1 ] >= self ._min_content_val :
154
142
# if we would have divided by zero, set adaptive_ratio to the max (255.0)
155
143
adaptive_ratio = 255.0
156
144
if self .stats_manager is not None :
157
145
self .stats_manager .set_metrics (target [0 ], {self ._adaptive_ratio_key : adaptive_ratio })
158
146
159
147
# Check to see if adaptive_ratio exceeds the adaptive_threshold as well as there
160
148
# being a large enough content_val to trigger a cut
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 ]:
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 ]:
170
154
"""Returns the average content change for a frame."""
171
155
# TODO(v0.7): Add DeprecationWarning that `get_content_val` will be removed in v0.7.
172
156
logger .error ("get_content_val is deprecated and will be removed. Lookup the value"
@@ -175,6 +159,10 @@ def get_content_val(self, frame_num: int) -> Optional[float]:
175
159
return self .stats_manager .get_metrics (frame_num , [ContentDetector .FRAME_SCORE_KEY ])[0 ]
176
160
return 0.0
177
161
178
- def post_process (self , _unused_frame_num : int ):
179
- """Not required for AdaptiveDetector."""
180
- return []
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
0 commit comments