scene_detector.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. # -*- coding: utf-8 -*-
  2. #
  3. # PySceneDetect: Python-Based Video Scene Detector
  4. # -------------------------------------------------------------------
  5. # [ Site: https://scenedetect.com ]
  6. # [ Docs: https://scenedetect.com/docs/ ]
  7. # [ Github: https://github.com/Breakthrough/PySceneDetect/ ]
  8. #
  9. # Copyright (C) 2014-2024 Brandon Castellano <http://www.bcastell.com>.
  10. # PySceneDetect is licensed under the BSD 3-Clause License; see the
  11. # included LICENSE file, or visit one of the above pages for details.
  12. #
  13. """``scenedetect.scene_detector`` Module
  14. This module contains the :class:`SceneDetector` interface, from which all scene detectors in
  15. :mod:`scenedetect.detectors` module are derived from.
  16. The SceneDetector class represents the interface which detection algorithms are expected to provide
  17. in order to be compatible with PySceneDetect.
  18. .. warning::
  19. This API is still unstable, and changes and design improvements are planned for the v1.0
  20. release. Instead of just timecodes, detection algorithms will also provide a specific type of
  21. event (in, out, cut, etc...).
  22. """
  23. from enum import Enum
  24. import typing as ty
  25. import numpy
  26. from scenedetect.stats_manager import StatsManager
  27. # pylint: disable=unused-argument, no-self-use
  28. class SceneDetector:
  29. """ Base class to inherit from when implementing a scene detection algorithm.
  30. This API is not yet stable and subject to change.
  31. This represents a "dense" scene detector, which returns a list of frames where
  32. the next scene/shot begins in a video.
  33. Also see the implemented scene detectors in the scenedetect.detectors module
  34. to get an idea of how a particular detector can be created.
  35. """
  36. # TODO(v0.7): Make this a proper abstract base class.
  37. stats_manager: ty.Optional[StatsManager] = None
  38. """Optional :class:`StatsManager <scenedetect.stats_manager.StatsManager>` to
  39. use for caching frame metrics to and from."""
  40. # TODO(v1.0): Remove - this is a rarely used case for what is now a neglegible performance gain.
  41. def is_processing_required(self, frame_num: int) -> bool:
  42. """[DEPRECATED] DO NOT USE
  43. Test if all calculations for a given frame are already done.
  44. Returns:
  45. False if the SceneDetector has assigned _metric_keys, and the
  46. stats_manager property is set to a valid StatsManager object containing
  47. the required frame metrics/calculations for the given frame - thus, not
  48. needing the frame to perform scene detection.
  49. True otherwise (i.e. the frame_img passed to process_frame is required
  50. to be passed to process_frame for the given frame_num).
  51. """
  52. metric_keys = self.get_metrics()
  53. return not metric_keys or not (self.stats_manager is not None
  54. and self.stats_manager.metrics_exist(frame_num, metric_keys))
  55. def stats_manager_required(self) -> bool:
  56. """Stats Manager Required: Prototype indicating if detector requires stats.
  57. Returns:
  58. True if a StatsManager is required for the detector, False otherwise.
  59. """
  60. return False
  61. def get_metrics(self) -> ty.List[str]:
  62. """Get Metrics: Get a list of all metric names/keys used by the detector.
  63. Returns:
  64. List of strings of frame metric key names that will be used by
  65. the detector when a StatsManager is passed to process_frame.
  66. """
  67. return []
  68. def process_frame(self, frame_num: int, frame_img: numpy.ndarray) -> ty.List[int]:
  69. """Process the next frame. `frame_num` is assumed to be sequential.
  70. Args:
  71. frame_num (int): Frame number of frame that is being passed. Can start from any value
  72. but must remain sequential.
  73. frame_img (numpy.ndarray or None): Video frame corresponding to `frame_img`.
  74. Returns:
  75. List[int]: List of frames where scene cuts have been detected. There may be 0
  76. or more frames in the list, and not necessarily the same as frame_num.
  77. Returns:
  78. List of frame numbers of cuts to be added to the cutting list.
  79. """
  80. return []
  81. def post_process(self, frame_num: int) -> ty.List[int]:
  82. """Post Process: Performs any processing after the last frame has been read.
  83. Prototype method, no actual detection.
  84. Returns:
  85. List of frame numbers of cuts to be added to the cutting list.
  86. """
  87. return []
  88. @property
  89. def event_buffer_length(self) -> int:
  90. """The amount of frames a given event can be buffered for, in time. Represents maximum
  91. amount any event can be behind `frame_number` in the result of :meth:`process_frame`.
  92. """
  93. return 0
  94. class SparseSceneDetector(SceneDetector):
  95. """Base class to inherit from when implementing a sparse scene detection algorithm.
  96. This class will be removed in v1.0 and should not be used.
  97. Unlike dense detectors, sparse detectors detect "events" and return a *pair* of frames,
  98. as opposed to just a single cut.
  99. An example of a SparseSceneDetector is the MotionDetector.
  100. """
  101. def process_frame(self, frame_num: int,
  102. frame_img: numpy.ndarray) -> ty.List[ty.Tuple[int, int]]:
  103. """Process Frame: Computes/stores metrics and detects any scene changes.
  104. Prototype method, no actual detection.
  105. Returns:
  106. List of frame pairs representing individual scenes
  107. to be added to the output scene list directly.
  108. """
  109. return []
  110. def post_process(self, frame_num: int) -> ty.List[ty.Tuple[int, int]]:
  111. """Post Process: Performs any processing after the last frame has been read.
  112. Prototype method, no actual detection.
  113. Returns:
  114. List of frame pairs representing individual scenes
  115. to be added to the output scene list directly.
  116. """
  117. return []
  118. class FlashFilter:
  119. class Mode(Enum):
  120. MERGE = 0
  121. """Merge consecutive cuts shorter than filter length."""
  122. SUPPRESS = 1
  123. """Suppress consecutive cuts until the filter length has passed."""
  124. def __init__(self, mode: Mode, length: int):
  125. self._mode = mode
  126. self._filter_length = length # Number of frames to use for activating the filter.
  127. self._last_above = None # Last frame above threshold.
  128. self._merge_enabled = False # Used to disable merging until at least one cut was found.
  129. self._merge_triggered = False # True when the merge filter is active.
  130. self._merge_start = None # Frame number where we started the merge filte.
  131. def filter(self, frame_num: int, above_threshold: bool) -> ty.List[int]:
  132. if not self._filter_length > 0:
  133. return [frame_num] if above_threshold else []
  134. if self._last_above is None:
  135. self._last_above = frame_num
  136. if self._mode == FlashFilter.Mode.MERGE:
  137. return self._filter_merge(frame_num=frame_num, above_threshold=above_threshold)
  138. if self._mode == FlashFilter.Mode.SUPPRESS:
  139. return self._filter_suppress(frame_num=frame_num, above_threshold=above_threshold)
  140. def _filter_suppress(self, frame_num: int, above_threshold: bool) -> ty.List[int]:
  141. min_length_met: bool = (frame_num - self._last_above) >= self._filter_length
  142. if not (above_threshold and min_length_met):
  143. return []
  144. # Both length and threshold requirements were satisfied. Emit the cut, and wait until both
  145. # requirements are met again.
  146. self._last_above = frame_num
  147. return [frame_num]
  148. def _filter_merge(self, frame_num: int, above_threshold: bool) -> ty.List[int]:
  149. min_length_met: bool = (frame_num - self._last_above) >= self._filter_length
  150. # Ensure last frame is always advanced to the most recent one that was above the threshold.
  151. if above_threshold:
  152. self._last_above = frame_num
  153. if self._merge_triggered:
  154. # This frame was under the threshold, see if enough frames passed to disable the filter.
  155. num_merged_frames = self._last_above - self._merge_start
  156. if min_length_met and not above_threshold and num_merged_frames >= self._filter_length:
  157. self._merge_triggered = False
  158. return [self._last_above]
  159. # Keep merging until enough frames pass below the threshold.
  160. return []
  161. # Wait for next frame above the threshold.
  162. if not above_threshold:
  163. return []
  164. # If we met the minimum length requirement, no merging is necessary.
  165. if min_length_met:
  166. # Only allow the merge filter once the first cut is emitted.
  167. self._merge_enabled = True
  168. return [frame_num]
  169. # Start merging cuts until the length requirement is met.
  170. if self._merge_enabled:
  171. self._merge_triggered = True
  172. self._merge_start = frame_num
  173. return []