123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- """``scenedetect.scene_detector`` Module
- This module contains the :class:`SceneDetector` interface, from which all scene detectors in
- :mod:`scenedetect.detectors` module are derived from.
- The SceneDetector class represents the interface which detection algorithms are expected to provide
- in order to be compatible with PySceneDetect.
- .. warning::
- This API is still unstable, and changes and design improvements are planned for the v1.0
- release. Instead of just timecodes, detection algorithms will also provide a specific type of
- event (in, out, cut, etc...).
- """
- from enum import Enum
- import typing as ty
- import numpy
- from scenedetect.stats_manager import StatsManager
- class SceneDetector:
- """ Base class to inherit from when implementing a scene detection algorithm.
- This API is not yet stable and subject to change.
- This represents a "dense" scene detector, which returns a list of frames where
- the next scene/shot begins in a video.
- Also see the implemented scene detectors in the scenedetect.detectors module
- to get an idea of how a particular detector can be created.
- """
-
- stats_manager: ty.Optional[StatsManager] = None
- """Optional :class:`StatsManager <scenedetect.stats_manager.StatsManager>` to
- use for caching frame metrics to and from."""
-
- def is_processing_required(self, frame_num: int) -> bool:
- """[DEPRECATED] DO NOT USE
- Test if all calculations for a given frame are already done.
- Returns:
- False if the SceneDetector has assigned _metric_keys, and the
- stats_manager property is set to a valid StatsManager object containing
- the required frame metrics/calculations for the given frame - thus, not
- needing the frame to perform scene detection.
- True otherwise (i.e. the frame_img passed to process_frame is required
- to be passed to process_frame for the given frame_num).
- """
- metric_keys = self.get_metrics()
- return not metric_keys or not (self.stats_manager is not None
- and self.stats_manager.metrics_exist(frame_num, metric_keys))
- def stats_manager_required(self) -> bool:
- """Stats Manager Required: Prototype indicating if detector requires stats.
- Returns:
- True if a StatsManager is required for the detector, False otherwise.
- """
- return False
- def get_metrics(self) -> ty.List[str]:
- """Get Metrics: Get a list of all metric names/keys used by the detector.
- Returns:
- List of strings of frame metric key names that will be used by
- the detector when a StatsManager is passed to process_frame.
- """
- return []
- def process_frame(self, frame_num: int, frame_img: numpy.ndarray) -> ty.List[int]:
- """Process the next frame. `frame_num` is assumed to be sequential.
- Args:
- frame_num (int): Frame number of frame that is being passed. Can start from any value
- but must remain sequential.
- frame_img (numpy.ndarray or None): Video frame corresponding to `frame_img`.
- 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.
- Returns:
- List of frame numbers of cuts to be added to the cutting list.
- """
- return []
- def post_process(self, frame_num: int) -> ty.List[int]:
- """Post Process: Performs any processing after the last frame has been read.
- Prototype method, no actual detection.
- Returns:
- List of frame numbers of cuts to be added to the cutting list.
- """
- return []
- @property
- def event_buffer_length(self) -> int:
- """The amount of frames a given event can be buffered for, in time. Represents maximum
- amount any event can be behind `frame_number` in the result of :meth:`process_frame`.
- """
- return 0
- class SparseSceneDetector(SceneDetector):
- """Base class to inherit from when implementing a sparse scene detection algorithm.
- This class will be removed in v1.0 and should not be used.
- Unlike dense detectors, sparse detectors detect "events" and return a *pair* of frames,
- as opposed to just a single cut.
- An example of a SparseSceneDetector is the MotionDetector.
- """
- def process_frame(self, frame_num: int,
- frame_img: numpy.ndarray) -> ty.List[ty.Tuple[int, int]]:
- """Process Frame: Computes/stores metrics and detects any scene changes.
- Prototype method, no actual detection.
- Returns:
- List of frame pairs representing individual scenes
- to be added to the output scene list directly.
- """
- return []
- def post_process(self, frame_num: int) -> ty.List[ty.Tuple[int, int]]:
- """Post Process: Performs any processing after the last frame has been read.
- Prototype method, no actual detection.
- Returns:
- List of frame pairs representing individual scenes
- to be added to the output scene list directly.
- """
- return []
- class FlashFilter:
- class Mode(Enum):
- MERGE = 0
- """Merge consecutive cuts shorter than filter length."""
- SUPPRESS = 1
- """Suppress consecutive cuts until the filter length has passed."""
- def __init__(self, mode: Mode, length: int):
- self._mode = mode
- self._filter_length = length
- self._last_above = None
- self._merge_enabled = False
- self._merge_triggered = False
- self._merge_start = None
- def filter(self, frame_num: int, above_threshold: bool) -> ty.List[int]:
- if not self._filter_length > 0:
- return [frame_num] if above_threshold else []
- if self._last_above is None:
- self._last_above = frame_num
- if self._mode == FlashFilter.Mode.MERGE:
- return self._filter_merge(frame_num=frame_num, above_threshold=above_threshold)
- if self._mode == FlashFilter.Mode.SUPPRESS:
- return self._filter_suppress(frame_num=frame_num, above_threshold=above_threshold)
- def _filter_suppress(self, frame_num: int, above_threshold: bool) -> ty.List[int]:
- min_length_met: bool = (frame_num - self._last_above) >= self._filter_length
- if not (above_threshold and min_length_met):
- return []
-
-
- self._last_above = frame_num
- return [frame_num]
- def _filter_merge(self, frame_num: int, above_threshold: bool) -> ty.List[int]:
- min_length_met: bool = (frame_num - self._last_above) >= self._filter_length
-
- if above_threshold:
- self._last_above = frame_num
- if self._merge_triggered:
-
- num_merged_frames = self._last_above - self._merge_start
- if min_length_met and not above_threshold and num_merged_frames >= self._filter_length:
- self._merge_triggered = False
- return [self._last_above]
-
- return []
-
- if not above_threshold:
- return []
-
- if min_length_met:
-
- self._merge_enabled = True
- return [frame_num]
-
- if self._merge_enabled:
- self._merge_triggered = True
- self._merge_start = frame_num
- return []
|