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