-
Notifications
You must be signed in to change notification settings - Fork 387
/
content_detector.py
153 lines (124 loc) · 6.66 KB
/
content_detector.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# -*- coding: utf-8 -*-
#
# PySceneDetect: Python-Based Video Scene Detector
# ---------------------------------------------------------------
# [ Site: http://www.bcastell.com/projects/PySceneDetect/ ]
# [ Github: https://github.com/Breakthrough/PySceneDetect/ ]
# [ Documentation: http://pyscenedetect.readthedocs.org/ ]
#
# Copyright (C) 2014-2019 Brandon Castellano <http://www.bcastell.com>.
#
# PySceneDetect is licensed under the BSD 3-Clause License; see the included
# LICENSE file, or visit one of the following pages for details:
# - https://github.com/Breakthrough/PySceneDetect/
# - http://www.bcastell.com/projects/PySceneDetect/
#
# This software uses Numpy, OpenCV, click, tqdm, simpletable, and pytest.
# See the included LICENSE files or one of the above URLs for more information.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
""" Module: ``scenedetect.detectors.content_detector``
This module implements the :py:class:`ContentDetector`, which compares the
difference in content between adjacent frames against a set threshold/score,
which if exceeded, triggers a scene cut.
This detector is available from the command-line interface by using the
`detect-content` command.
"""
# Third-Party Library Imports
import numpy
import cv2
# PySceneDetect Library Imports
from scenedetect.scene_detector import SceneDetector
class ContentDetector(SceneDetector):
"""Detects fast cuts using changes in colour and intensity between frames.
Since the difference between frames is used, unlike the ThresholdDetector,
only fast cuts are detected with this method. To detect slow fades between
content scenes still using HSV information, use the DissolveDetector.
"""
def __init__(self, threshold=30.0, min_scene_len=15):
super(ContentDetector, self).__init__()
self.threshold = threshold
self.min_scene_len = min_scene_len # minimum length of any given scene, in frames (int) or FrameTimecode
self.last_frame = None
self.last_scene_cut = None
self.last_hsv = None
self._metric_keys = ['content_val', 'delta_hue', 'delta_sat', 'delta_lum']
self.cli_name = 'detect-content'
def process_frame(self, frame_num, frame_img):
# type: (int, numpy.ndarray) -> List[int]
""" Similar to ThresholdDetector, but using the HSV colour space DIFFERENCE instead
of single-frame RGB/grayscale intensity (thus cannot detect slow fades with this method).
Arguments:
frame_num (int): Frame number of frame that is being passed.
frame_img (Optional[int]): Decoded frame image (numpy.ndarray) to perform scene
detection on. Can be None *only* if the self.is_processing_required() method
(inhereted from the base SceneDetector class) returns True.
Returns:
List[int]: List of frames where scene cuts have been detected. There may be 0
or more frames in the list, and not necessarily the same as frame_num.
"""
cut_list = []
metric_keys = self._metric_keys
_unused = ''
# Initialize last scene cut point at the beginning of the frames of interest.
if self.last_scene_cut is None:
self.last_scene_cut = frame_num
# We can only start detecting once we have a frame to compare with.
if self.last_frame is not None:
# Change in average of HSV (hsv), (h)ue only, (s)aturation only, (l)uminance only.
# These are refered to in a statsfile as their respective self._metric_keys string.
delta_hsv_avg, delta_h, delta_s, delta_v = 0.0, 0.0, 0.0, 0.0
if (self.stats_manager is not None and
self.stats_manager.metrics_exist(frame_num, metric_keys)):
delta_hsv_avg, delta_h, delta_s, delta_v = self.stats_manager.get_metrics(
frame_num, metric_keys)
else:
num_pixels = frame_img.shape[0] * frame_img.shape[1]
curr_hsv = cv2.split(cv2.cvtColor(frame_img, cv2.COLOR_BGR2HSV))
last_hsv = self.last_hsv
if not last_hsv:
last_hsv = cv2.split(cv2.cvtColor(self.last_frame, cv2.COLOR_BGR2HSV))
delta_hsv = [0, 0, 0, 0]
for i in range(3):
num_pixels = curr_hsv[i].shape[0] * curr_hsv[i].shape[1]
curr_hsv[i] = curr_hsv[i].astype(numpy.int32)
last_hsv[i] = last_hsv[i].astype(numpy.int32)
delta_hsv[i] = numpy.sum(
numpy.abs(curr_hsv[i] - last_hsv[i])) / float(num_pixels)
delta_hsv[3] = sum(delta_hsv[0:3]) / 3.0
delta_h, delta_s, delta_v, delta_hsv_avg = delta_hsv
if self.stats_manager is not None:
self.stats_manager.set_metrics(frame_num, {
metric_keys[0]: delta_hsv_avg,
metric_keys[1]: delta_h,
metric_keys[2]: delta_s,
metric_keys[3]: delta_v})
self.last_hsv = curr_hsv
# We consider any frame over the threshold a new scene, but only if
# the minimum scene length has been reached (otherwise it is ignored).
if delta_hsv_avg >= self.threshold and (
(frame_num - self.last_scene_cut) >= self.min_scene_len):
cut_list.append(frame_num)
self.last_scene_cut = frame_num
if self.last_frame is not None and self.last_frame is not _unused:
del self.last_frame
# If we have the next frame computed, don't copy the current frame
# into last_frame since we won't use it on the next call anyways.
if (self.stats_manager is not None and
self.stats_manager.metrics_exist(frame_num+1, metric_keys)):
self.last_frame = _unused
else:
self.last_frame = frame_img.copy()
return cut_list
#def post_process(self, frame_num):
# """ TODO: Based on the parameters passed to the ContentDetector constructor,
# ensure that the last scene meets the minimum length requirement,
# otherwise it should be merged with the previous scene.
# """
# return []