def test_metrics(): """ Test StatsManager metric registration/setting/getting with a set of pre-defined key-value pairs (metric_dict). """ metric_dict = {'some_metric': 1.2345, 'another_metric': 6.7890} metric_keys = list(metric_dict.keys()) stats = StatsManager() frame_key = 100 assert not stats.is_save_required() stats.register_metrics(metric_keys) assert not stats.is_save_required() with pytest.raises(FrameMetricRegistered): stats.register_metrics(metric_keys) assert not stats.metrics_exist(frame_key, metric_keys) assert stats.get_metrics(frame_key, metric_keys) == [None] * len(metric_keys) stats.set_metrics(frame_key, metric_dict) assert stats.is_save_required() assert stats.metrics_exist(frame_key, metric_keys) assert stats.metrics_exist(frame_key, metric_keys[1:]) assert stats.get_metrics(frame_key, metric_keys) == [ metric_dict[metric_key] for metric_key in metric_keys]
def test_metrics(): """ Test StatsManager metric registration/setting/getting with a set of pre-defined key-value pairs (metric_dict). """ metric_dict = {'some_metric': 1.2345, 'another_metric': 6.7890} metric_keys = list(metric_dict.keys()) stats = StatsManager() frame_key = 100 assert not stats.is_save_required() stats.register_metrics(metric_keys) assert not stats.is_save_required() with pytest.raises(FrameMetricRegistered): stats.register_metrics(metric_keys) assert not stats.metrics_exist(frame_key, metric_keys) assert stats.get_metrics(frame_key, metric_keys) == [None] * len(metric_keys) stats.set_metrics(frame_key, metric_dict) assert stats.is_save_required() assert stats.metrics_exist(frame_key, metric_keys) assert stats.metrics_exist(frame_key, metric_keys[1:]) assert stats.get_metrics(frame_key, metric_keys) == [ metric_dict[metric_key] for metric_key in metric_keys]
class SceneManager(object): """ The SceneManager facilitates detection of scenes via the :py:meth:`detect_scenes` method, given a video source (:py:class:`VideoManager <scenedetect.video_manager.VideoManager>` or cv2.VideoCapture), and SceneDetector algorithms added via the :py:meth:`add_detector` method. Can also optionally take a StatsManager instance during construction to cache intermediate scene detection calculations, making subsequent calls to :py:meth:`detect_scenes` much faster, allowing the cached values to be saved/loaded to/from disk, and also manually determining the optimal threshold values or other options for various detection algorithms. """ def __init__(self, stats_manager=None): # type: (Optional[StatsManager]) self._cutting_list = [] self._event_list = [] self._detector_list = [] self._sparse_detector_list = [] self._stats_manager = stats_manager self._num_frames = 0 self._start_frame = 0 self._base_timecode = None def add_detector(self, detector): # type: (SceneDetector) -> None """ Adds/registers a SceneDetector (e.g. ContentDetector, ThresholdDetector) to run when detect_scenes is called. The SceneManager owns the detector object, so a temporary may be passed. Arguments: detector (SceneDetector): Scene detector to add to the SceneManager. """ if self._stats_manager is None and detector.stats_manager_required(): # Make sure the lists are empty so that the detectors don't get # out of sync (require an explicit statsmanager instead) assert not self._detector_list and not self._sparse_detector_list self._stats_manager = StatsManager() detector.stats_manager = self._stats_manager if self._stats_manager is not None: # Allow multiple detection algorithms of the same type to be added # by suppressing any FrameMetricRegistered exceptions due to attempts # to re-register the same frame metric keys. try: self._stats_manager.register_metrics(detector.get_metrics()) except FrameMetricRegistered: pass if not issubclass(type(detector), SparseSceneDetector): self._detector_list.append(detector) else: self._sparse_detector_list.append(detector) def get_num_detectors(self): # type: () -> int """ Gets number of registered scene detectors added via add_detector. """ return len(self._detector_list) def clear(self): # type: () -> None """ Clears all cuts/scenes and resets the SceneManager's position. Any statistics generated are still saved in the StatsManager object passed to the SceneManager's constructor, and thus, subsequent calls to detect_scenes, using the same frame source reset at the initial time (if it is a VideoManager, use the reset() method), will use the cached frame metrics that were computed and saved in the previous call to detect_scenes. """ self._cutting_list.clear() self._event_list.clear() self._num_frames = 0 self._start_frame = 0 def clear_detectors(self): # type: () -> None """ Removes all scene detectors added to the SceneManager via add_detector(). """ self._detector_list.clear() self._sparse_detector_list.clear() def get_scene_list(self, base_timecode=None): # type: (FrameTimecode) -> List[Tuple[FrameTimecode, FrameTimecode]] """ Returns a list of tuples of start/end FrameTimecodes for each detected scene. The scene list is generated by combining the results of all sparse detectors with those from dense ones (i.e. combining the results of :py:meth:`get_cut_list` and :py:meth:`get_event_list`). Returns: List of tuples in the form (start_time, end_time), where both start_time and end_time are FrameTimecode objects representing the exact time/frame where each detected scene in the video begins and ends. """ if base_timecode is None: base_timecode = self._base_timecode if base_timecode is None: return [] return sorted( self.get_event_list(base_timecode) + get_scenes_from_cuts( self.get_cut_list(base_timecode), base_timecode, self._num_frames, self._start_frame)) def get_cut_list(self, base_timecode=None): # type: (FrameTimecode) -> List[FrameTimecode] """ Returns a list of FrameTimecodes of the detected scene changes/cuts. Unlike get_scene_list, the cutting list returns a list of FrameTimecodes representing the point in the input video(s) where a new scene was detected, and thus the frame where the input should be cut/split. The cutting list, in turn, is used to generate the scene list, noting that each scene is contiguous starting from the first frame and ending at the last frame detected. If only sparse detectors are used (e.g. MotionDetector), this will always be empty. Returns: List of FrameTimecode objects denoting the points in time where a scene change was detected in the input video(s), which can also be passed to external tools for automated splitting of the input into individual scenes. """ if base_timecode is None: base_timecode = self._base_timecode if base_timecode is None: return [] return [ FrameTimecode(cut, base_timecode) for cut in self._get_cutting_list() ] def _get_cutting_list(self): # type: () -> list """ Returns a sorted list of unique frame numbers of any detected scene cuts. """ # We remove duplicates here by creating a set then back to a list and sort it. return sorted(list(set(self._cutting_list))) def get_event_list(self, base_timecode=None): # type: (FrameTimecode) -> List[FrameTimecode] """ Returns a list of FrameTimecode pairs of the detected scenes by all sparse detectors. Unlike get_scene_list, the event list returns a list of FrameTimecodes representing the point in the input video(s) where a new scene was detected only by sparse detectors, otherwise it is the same. Returns: List of pairs of FrameTimecode objects denoting the detected scenes. """ if base_timecode is None: base_timecode = self._base_timecode if base_timecode is None: return [] return [(base_timecode + start, base_timecode + end) for start, end in self._event_list] def _process_frame(self, frame_num, frame_im, callback=None): # type(int, numpy.ndarray) -> None """ Adds any cuts detected with the current frame to the cutting list. """ for detector in self._detector_list: cuts = detector.process_frame(frame_num, frame_im) if cuts and callback: callback(frame_im, frame_num) self._cutting_list += cuts for detector in self._sparse_detector_list: events = detector.process_frame(frame_num, frame_im) if events and callback: callback(frame_im, frame_num) self._event_list += events def _is_processing_required(self, frame_num): # type(int) -> bool """ Is Processing Required: Returns True if frame metrics not in StatsManager, False otherwise. """ return all([ detector.is_processing_required(frame_num) for detector in self._detector_list ]) def _post_process(self, frame_num): # type(int, numpy.ndarray) -> None """ Adds any remaining cuts to the cutting list after processing the last frame. """ for detector in self._detector_list: self._cutting_list += detector.post_process(frame_num) def detect_scenes(self, frame_source, end_time=None, frame_skip=0, show_progress=True, callback=None): # type: (VideoManager, Union[int, FrameTimecode], # Optional[Union[int, FrameTimecode]], Optional[bool], # Optional[Callable[numpy.ndarray]) -> int """ Perform scene detection on the given frame_source using the added SceneDetectors. Blocks until all frames in the frame_source have been processed. Results can be obtained by calling either the get_scene_list() or get_cut_list() methods. Arguments: frame_source (scenedetect.video_manager.VideoManager or cv2.VideoCapture): A source of frames to process (using frame_source.read() as in VideoCapture). VideoManager is preferred as it allows concatenation of multiple videos as well as seeking, by defining start time and end time/duration. end_time (int or FrameTimecode): Maximum number of frames to detect (set to None to detect all available frames). Only needed for OpenCV VideoCapture objects; for VideoManager objects, use set_duration() instead. frame_skip (int): Not recommended except for extremely high framerate videos. Number of frames to skip (i.e. process every 1 in N+1 frames, where N is frame_skip, processing only 1/N+1 percent of the video, speeding up the detection time at the expense of accuracy). `frame_skip` **must** be 0 (the default) when using a StatsManager. show_progress (bool): If True, and the ``tqdm`` module is available, displays a progress bar with the progress, framerate, and expected time to complete processing the video frame source. callback ((image_ndarray, frame_num: int) -> None): If not None, called after each scene/event detected. Note that the signature of the callback will undergo breaking changes in v0.6 to provide more context to the callback (detector type, event type, etc... - see #177 for further details). Returns: int: Number of frames read and processed from the frame source. Raises: ValueError: `frame_skip` **must** be 0 (the default) if the SceneManager was constructed with a StatsManager object. """ if frame_skip > 0 and self._stats_manager is not None: raise ValueError('frame_skip must be 0 when using a StatsManager.') start_frame = 0 curr_frame = 0 end_frame = None self._base_timecode = FrameTimecode(timecode=0, fps=frame_source.get( cv2.CAP_PROP_FPS)) total_frames = math.trunc(frame_source.get(cv2.CAP_PROP_FRAME_COUNT)) start_time = frame_source.get(cv2.CAP_PROP_POS_FRAMES) if isinstance(start_time, FrameTimecode): start_frame = start_time.get_frames() elif start_time is not None: start_frame = int(start_time) self._start_frame = start_frame curr_frame = start_frame if isinstance(end_time, FrameTimecode): end_frame = end_time.get_frames() elif end_time is not None: end_frame = int(end_time) if end_frame is not None: total_frames = end_frame if start_frame is not None and not isinstance(start_time, FrameTimecode): total_frames -= start_frame if total_frames < 0: total_frames = 0 progress_bar = None if tqdm and show_progress: progress_bar = tqdm(total=total_frames, unit='frames', dynamic_ncols=True) try: while True: if end_frame is not None and curr_frame >= end_frame: break # We don't compensate for frame_skip here as the frame_skip option # is not allowed when using a StatsManager - thus, processing is # *always* required for *all* frames when frame_skip > 0. if (self._is_processing_required(self._num_frames + start_frame) or self._is_processing_required(self._num_frames + start_frame + 1)): ret_val, frame_im = frame_source.read() else: ret_val = frame_source.grab() frame_im = None if not ret_val: break self._process_frame(self._num_frames + start_frame, frame_im, callback) curr_frame += 1 self._num_frames += 1 if progress_bar: progress_bar.update(1) if frame_skip > 0: for _ in range(frame_skip): if not frame_source.grab(): break curr_frame += 1 self._num_frames += 1 if progress_bar: progress_bar.update(1) self._post_process(curr_frame) num_frames = curr_frame - start_frame finally: if progress_bar: progress_bar.close() return num_frames