video_manager.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  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.video_manager`` Module
  14. [DEPRECATED] DO NOT USE. Use `open_video` from `scenedetect.backends` or create a
  15. VideoStreamCv2 object (`scenedetect.backends.opencv`) instead.
  16. This module exists for *some* backwards compatibility with v0.5, and will be removed
  17. in a future release.
  18. """
  19. import os
  20. import math
  21. from logging import getLogger
  22. from typing import Iterable, List, Optional, Tuple, Union
  23. import numpy as np
  24. import cv2
  25. from scenedetect.platform import get_file_name
  26. from scenedetect.frame_timecode import FrameTimecode, MAX_FPS_DELTA
  27. from scenedetect.video_stream import VideoStream, VideoOpenFailure, FrameRateUnavailable
  28. from scenedetect.backends.opencv import _get_aspect_ratio
  29. ##
  30. ## VideoManager Exceptions
  31. ##
  32. class VideoParameterMismatch(Exception):
  33. """ VideoParameterMismatch: Raised when opening multiple videos with a VideoManager, and some
  34. of the video parameters (frame height, frame width, and framerate/FPS) do not match. """
  35. def __init__(self,
  36. file_list=None,
  37. message="OpenCV VideoCapture object parameters do not match."):
  38. # type: (Iterable[Tuple[int, float, float, str, str]], str) -> None
  39. # Pass message string to base Exception class.
  40. super(VideoParameterMismatch, self).__init__(message)
  41. # list of (param_mismatch_type: int, parameter value, expected value,
  42. # filename: str, filepath: str)
  43. # where param_mismatch_type is an OpenCV CAP_PROP (e.g. CAP_PROP_FPS).
  44. self.file_list = file_list
  45. class VideoDecodingInProgress(RuntimeError):
  46. """ VideoDecodingInProgress: Raised when attempting to call certain VideoManager methods that
  47. must be called *before* start() has been called. """
  48. class InvalidDownscaleFactor(ValueError):
  49. """ InvalidDownscaleFactor: Raised when trying to set invalid downscale factor,
  50. i.e. the supplied downscale factor was not a positive integer greater than zero. """
  51. ##
  52. ## VideoManager Helper Functions
  53. ##
  54. def get_video_name(video_file: str) -> Tuple[str, str]:
  55. """Get the video file/device name.
  56. Returns:
  57. Tuple of the form [name, video_file].
  58. """
  59. if isinstance(video_file, int):
  60. return ('Device %d' % video_file, video_file)
  61. return (os.path.split(video_file)[1], video_file)
  62. def get_num_frames(cap_list: Iterable[cv2.VideoCapture]) -> int:
  63. """ Get Number of Frames: Returns total number of frames in the cap_list.
  64. Calls get(CAP_PROP_FRAME_COUNT) and returns the sum for all VideoCaptures.
  65. """
  66. return sum([math.trunc(cap.get(cv2.CAP_PROP_FRAME_COUNT)) for cap in cap_list])
  67. def open_captures(
  68. video_files: Iterable[str],
  69. framerate: Optional[float] = None,
  70. validate_parameters: bool = True,
  71. ) -> Tuple[List[cv2.VideoCapture], float, Tuple[int, int]]:
  72. """ Open Captures - helper function to open all capture objects, set the framerate,
  73. and ensure that all open captures have been opened and the framerates match on a list
  74. of video file paths, or a list containing a single device ID.
  75. Arguments:
  76. video_files: List of one or more paths (str), or a list
  77. of a single integer device ID, to open as an OpenCV VideoCapture object.
  78. A ValueError will be raised if the list does not conform to the above.
  79. framerate: Framerate to assume when opening the video_files.
  80. If not set, the first open video is used for deducing the framerate of
  81. all videos in the sequence.
  82. validate_parameters (bool, optional): If true, will ensure that the frame sizes
  83. (width, height) and frame rate (FPS) of all passed videos is the same.
  84. A VideoParameterMismatch is raised if the framerates do not match.
  85. Returns:
  86. A tuple of form (cap_list, framerate, framesize) where cap_list is a list of open
  87. OpenCV VideoCapture objects in the same order as the video_files list, framerate
  88. is a float of the video(s) framerate(s), and framesize is a tuple of (width, height)
  89. where width and height are integers representing the frame size in pixels.
  90. Raises:
  91. ValueError: No video file(s) specified, or invalid/multiple device IDs specified.
  92. TypeError: `framerate` must be type `float`.
  93. IOError: Video file(s) not found.
  94. FrameRateUnavailable: Video framerate could not be obtained and `framerate`
  95. was not set manually.
  96. VideoParameterMismatch: All videos in `video_files` do not have equal parameters.
  97. Set `validate_parameters=False` to skip this check.
  98. VideoOpenFailure: Video(s) could not be opened.
  99. """
  100. is_device = False
  101. if not video_files:
  102. raise ValueError("Expected at least 1 video file or device ID.")
  103. if isinstance(video_files[0], int):
  104. if len(video_files) > 1:
  105. raise ValueError("If device ID is specified, no video sources may be appended.")
  106. elif video_files[0] < 0:
  107. raise ValueError("Invalid/negative device ID specified.")
  108. is_device = True
  109. elif not all([isinstance(video_file, (str, bytes)) for video_file in video_files]):
  110. print(video_files)
  111. raise ValueError("Unexpected element type in video_files list (expected str(s)/int).")
  112. elif framerate is not None and not isinstance(framerate, float):
  113. raise TypeError("Expected type float for parameter framerate.")
  114. # Check if files exist if passed video file is not an image sequence
  115. # (checked with presence of % in filename) or not a URL (://).
  116. if not is_device and any([
  117. not os.path.exists(video_file)
  118. for video_file in video_files
  119. if not ('%' in video_file or '://' in video_file)
  120. ]):
  121. raise IOError("Video file(s) not found.")
  122. cap_list = []
  123. try:
  124. cap_list = [cv2.VideoCapture(video_file) for video_file in video_files]
  125. video_names = [get_video_name(video_file) for video_file in video_files]
  126. closed_caps = [video_names[i] for i, cap in enumerate(cap_list) if not cap.isOpened()]
  127. if closed_caps:
  128. raise VideoOpenFailure(str(closed_caps))
  129. cap_framerates = [cap.get(cv2.CAP_PROP_FPS) for cap in cap_list]
  130. cap_framerate, check_framerate = validate_capture_framerate(video_names, cap_framerates,
  131. framerate)
  132. # Store frame sizes as integers (VideoCapture.get() returns float).
  133. cap_frame_sizes = [(math.trunc(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
  134. math.trunc(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))) for cap in cap_list]
  135. cap_frame_size = cap_frame_sizes[0]
  136. # If we need to validate the parameters, we check that the FPS and width/height
  137. # of all open captures is identical (or almost identical in the case of FPS).
  138. if validate_parameters:
  139. validate_capture_parameters(
  140. video_names=video_names,
  141. cap_frame_sizes=cap_frame_sizes,
  142. check_framerate=check_framerate,
  143. cap_framerates=cap_framerates)
  144. except:
  145. for cap in cap_list:
  146. cap.release()
  147. raise
  148. return (cap_list, cap_framerate, cap_frame_size)
  149. def validate_capture_framerate(
  150. video_names: Iterable[Tuple[str, str]],
  151. cap_framerates: List[float],
  152. framerate: Optional[float] = None,
  153. ) -> Tuple[float, bool]:
  154. """Ensure the passed capture framerates are valid and equal.
  155. Raises:
  156. ValueError: Invalid framerate (must be positive non-zero value).
  157. TypeError: Framerate must be of type float.
  158. FrameRateUnavailable: Framerate for video could not be obtained,
  159. and `framerate` was not set.
  160. """
  161. check_framerate = True
  162. cap_framerate = cap_framerates[0]
  163. if framerate is not None:
  164. if isinstance(framerate, float):
  165. if framerate < MAX_FPS_DELTA:
  166. raise ValueError("Invalid framerate (must be a positive non-zero value).")
  167. cap_framerate = framerate
  168. check_framerate = False
  169. else:
  170. raise TypeError("Expected float for framerate, got %s." % type(framerate).__name__)
  171. else:
  172. unavailable_framerates = [(video_names[i][0], video_names[i][1])
  173. for i, fps in enumerate(cap_framerates)
  174. if fps < MAX_FPS_DELTA]
  175. if unavailable_framerates:
  176. raise FrameRateUnavailable()
  177. return (cap_framerate, check_framerate)
  178. def validate_capture_parameters(
  179. video_names: List[Tuple[str, str]],
  180. cap_frame_sizes: List[Tuple[int, int]],
  181. check_framerate: bool = False,
  182. cap_framerates: Optional[List[float]] = None,
  183. ) -> None:
  184. """ Validate Capture Parameters: Ensures that all passed capture frame sizes and (optionally)
  185. framerates are equal. Raises VideoParameterMismatch if there is a mismatch.
  186. Raises:
  187. VideoParameterMismatch
  188. """
  189. bad_params = []
  190. max_framerate_delta = MAX_FPS_DELTA
  191. # Check heights/widths match.
  192. bad_params += [(cv2.CAP_PROP_FRAME_WIDTH, frame_size[0], cap_frame_sizes[0][0],
  193. video_names[i][0], video_names[i][1])
  194. for i, frame_size in enumerate(cap_frame_sizes)
  195. if abs(frame_size[0] - cap_frame_sizes[0][0]) > 0]
  196. bad_params += [(cv2.CAP_PROP_FRAME_HEIGHT, frame_size[1], cap_frame_sizes[0][1],
  197. video_names[i][0], video_names[i][1])
  198. for i, frame_size in enumerate(cap_frame_sizes)
  199. if abs(frame_size[1] - cap_frame_sizes[0][1]) > 0]
  200. # Check framerates if required.
  201. if check_framerate:
  202. bad_params += [(cv2.CAP_PROP_FPS, fps, cap_framerates[0], video_names[i][0],
  203. video_names[i][1])
  204. for i, fps in enumerate(cap_framerates)
  205. if math.fabs(fps - cap_framerates[0]) > max_framerate_delta]
  206. if bad_params:
  207. raise VideoParameterMismatch(bad_params)
  208. ##
  209. ## VideoManager Class Implementation
  210. ##
  211. class VideoManager(VideoStream):
  212. """[DEPRECATED] DO NOT USE.
  213. Provides a cv2.VideoCapture-like interface to a set of one or more video files,
  214. or a single device ID. Supports seeking and setting end time/duration."""
  215. BACKEND_NAME = 'video_manager_do_not_use'
  216. def __init__(self,
  217. video_files: List[str],
  218. framerate: Optional[float] = None,
  219. logger=getLogger('pyscenedetect')):
  220. """[DEPRECATED] DO NOT USE.
  221. Arguments:
  222. video_files (list of str(s)/int): A list of one or more paths (str), or a list
  223. of a single integer device ID, to open as an OpenCV VideoCapture object.
  224. framerate (float, optional): Framerate to assume when storing FrameTimecodes.
  225. If not set (i.e. is None), it will be deduced from the first open capture
  226. in video_files, else raises a FrameRateUnavailable exception.
  227. Raises:
  228. ValueError: No video file(s) specified, or invalid/multiple device IDs specified.
  229. TypeError: `framerate` must be type `float`.
  230. IOError: Video file(s) not found.
  231. FrameRateUnavailable: Video framerate could not be obtained and `framerate`
  232. was not set manually.
  233. VideoParameterMismatch: All videos in `video_files` do not have equal parameters.
  234. Set `validate_parameters=False` to skip this check.
  235. VideoOpenFailure: Video(s) could not be opened.
  236. """
  237. # TODO(v0.7): Add DeprecationWarning that this class will be removed in v0.8: 'VideoManager
  238. # will be removed in PySceneDetect v0.8. Use VideoStreamCv2 or VideoCaptureAdapter instead.'
  239. logger.error("VideoManager is deprecated and will be removed.")
  240. if not video_files:
  241. raise ValueError("At least one string/integer must be passed in the video_files list.")
  242. # Need to support video_files as a single str too for compatibility.
  243. if isinstance(video_files, str):
  244. video_files = [video_files]
  245. # These VideoCaptures are only open in this process.
  246. self._is_device = isinstance(video_files[0], int)
  247. self._cap_list, self._cap_framerate, self._cap_framesize = open_captures(
  248. video_files=video_files, framerate=framerate)
  249. self._path = video_files[0] if not self._is_device else video_files
  250. self._end_of_video = False
  251. self._start_time = self.get_base_timecode()
  252. self._end_time = None
  253. self._curr_time = self.get_base_timecode()
  254. self._last_frame = None
  255. self._curr_cap, self._curr_cap_idx = None, None
  256. self._video_file_paths = video_files
  257. self._logger = logger
  258. if self._logger is not None:
  259. self._logger.info('Loaded %d video%s, framerate: %.3f FPS, resolution: %d x %d',
  260. len(self._cap_list), 's' if len(self._cap_list) > 1 else '',
  261. self.get_framerate(), *self.get_framesize())
  262. self._started = False
  263. self._frame_length = self.get_base_timecode() + get_num_frames(self._cap_list)
  264. self._first_cap_len = self.get_base_timecode() + get_num_frames([self._cap_list[0]])
  265. self._aspect_ratio = _get_aspect_ratio(self._cap_list[0])
  266. def set_downscale_factor(self, downscale_factor=None):
  267. """No-op. Set downscale_factor in `SceneManager` instead."""
  268. _ = downscale_factor
  269. def get_num_videos(self) -> int:
  270. """Get the length of the internal capture list,
  271. representing the number of videos the VideoManager was constructed with.
  272. Returns:
  273. int: Number of videos, equal to length of capture list.
  274. """
  275. return len(self._cap_list)
  276. def get_video_paths(self) -> List[str]:
  277. """Get list of strings containing paths to the open video(s).
  278. Returns:
  279. List[str]: List of paths to the video files opened by the VideoManager.
  280. """
  281. return list(self._video_file_paths)
  282. def get_video_name(self) -> str:
  283. """Get name of the video based on the first video path.
  284. Returns:
  285. The base name of the video file, without extension.
  286. """
  287. video_paths = self.get_video_paths()
  288. if not video_paths:
  289. return ''
  290. video_name = os.path.basename(video_paths[0])
  291. if video_name.rfind('.') >= 0:
  292. video_name = video_name[:video_name.rfind('.')]
  293. return video_name
  294. def get_framerate(self) -> float:
  295. """Get the framerate the VideoManager is assuming for all
  296. open VideoCaptures. Obtained from either the capture itself, or the passed
  297. framerate parameter when the VideoManager object was constructed.
  298. Returns:
  299. Framerate, in frames/sec.
  300. """
  301. return self._cap_framerate
  302. def get_base_timecode(self) -> FrameTimecode:
  303. """Get a FrameTimecode object at frame 0 / time 00:00:00.
  304. The timecode returned by this method can be used to perform arithmetic (e.g.
  305. addition), passing the resulting values back to the VideoManager (e.g. for the
  306. :meth:`set_duration()` method), as the framerate of the returned FrameTimecode
  307. object matches that of the VideoManager.
  308. As such, this method is equivalent to creating a FrameTimecode at frame 0 with
  309. the VideoManager framerate, for example, given a VideoManager called obj,
  310. the following expression will evaluate as True:
  311. obj.get_base_timecode() == FrameTimecode(0, obj.get_framerate())
  312. Furthermore, the base timecode object returned by a particular VideoManager
  313. should not be passed to another one, unless you first verify that their
  314. framerates are the same.
  315. Returns:
  316. FrameTimecode at frame 0/time 00:00:00 with the video(s) framerate.
  317. """
  318. return FrameTimecode(timecode=0, fps=self._cap_framerate)
  319. def get_current_timecode(self) -> FrameTimecode:
  320. """ Get Current Timecode - returns a FrameTimecode object at current VideoManager position.
  321. Returns:
  322. Timecode at the current VideoManager position.
  323. """
  324. return self._curr_time
  325. def get_framesize(self) -> Tuple[int, int]:
  326. """Get frame size of the video(s) open in the VideoManager's capture objects.
  327. Returns:
  328. Video frame size, in pixels, in the form (width, height).
  329. """
  330. return self._cap_framesize
  331. def get_framesize_effective(self) -> Tuple[int, int]:
  332. """ Get Frame Size - returns the frame size of the video(s) open in the
  333. VideoManager's capture objects.
  334. Returns:
  335. Video frame size, in pixels, in the form (width, height).
  336. """
  337. return self._cap_framesize
  338. def set_duration(self,
  339. duration: Optional[FrameTimecode] = None,
  340. start_time: Optional[FrameTimecode] = None,
  341. end_time: Optional[FrameTimecode] = None) -> None:
  342. """ Set Duration - sets the duration/length of the video(s) to decode, as well as
  343. the start/end times. Must be called before :meth:`start()` is called, otherwise
  344. a VideoDecodingInProgress exception will be thrown. May be called after
  345. :meth:`reset()` as well.
  346. Arguments:
  347. duration (Optional[FrameTimecode]): The (maximum) duration in time to
  348. decode from the opened video(s). Mutually exclusive with end_time
  349. (i.e. if duration is set, end_time must be None).
  350. start_time (Optional[FrameTimecode]): The time/first frame at which to
  351. start decoding frames from. If set, the input video(s) will be
  352. seeked to when start() is called, at which point the frame at
  353. start_time can be obtained by calling retrieve().
  354. end_time (Optional[FrameTimecode]): The time at which to stop decoding
  355. frames from the opened video(s). Mutually exclusive with duration
  356. (i.e. if end_time is set, duration must be None).
  357. Raises:
  358. VideoDecodingInProgress: Must call before start().
  359. """
  360. if self._started:
  361. raise VideoDecodingInProgress()
  362. # Ensure any passed timecodes have the proper framerate.
  363. if ((duration is not None and not duration.equal_framerate(self._cap_framerate))
  364. or (start_time is not None and not start_time.equal_framerate(self._cap_framerate))
  365. or (end_time is not None and not end_time.equal_framerate(self._cap_framerate))):
  366. raise ValueError("FrameTimecode framerate does not match.")
  367. if duration is not None and end_time is not None:
  368. raise TypeError("Only one of duration and end_time may be specified, not both.")
  369. if start_time is not None:
  370. self._start_time = start_time
  371. if end_time is not None:
  372. if end_time < self._start_time:
  373. raise ValueError("end_time is before start_time in time.")
  374. self._end_time = end_time
  375. elif duration is not None:
  376. self._end_time = self._start_time + duration
  377. if self._end_time is not None:
  378. self._frame_length = min(self._frame_length, self._end_time + 1)
  379. self._frame_length -= self._start_time
  380. if self._logger is not None:
  381. self._logger.info('Duration set, start: %s, duration: %s, end: %s.',
  382. start_time.get_timecode() if start_time is not None else start_time,
  383. duration.get_timecode() if duration is not None else duration,
  384. end_time.get_timecode() if end_time is not None else end_time)
  385. def get_duration(self) -> FrameTimecode:
  386. """ Get Duration - gets the duration/length of the video(s) to decode,
  387. as well as the start/end times.
  388. If the end time was not set by :meth:`set_duration()`, the end timecode
  389. is calculated as the start timecode + total duration.
  390. Returns:
  391. Tuple[FrameTimecode, FrameTimecode, FrameTimecode]: The current video(s)
  392. total duration, start timecode, and end timecode.
  393. """
  394. end_time = self._end_time
  395. if end_time is None:
  396. end_time = self.get_base_timecode() + self._frame_length
  397. return (self._frame_length, self._start_time, end_time)
  398. def start(self) -> None:
  399. """ Start - starts video decoding and seeks to start time. Raises
  400. exception VideoDecodingInProgress if the method is called after the
  401. decoder process has already been started.
  402. Raises:
  403. VideoDecodingInProgress: Must call :meth:`stop()` before this
  404. method if :meth:`start()` has already been called after
  405. initial construction.
  406. """
  407. if self._started:
  408. raise VideoDecodingInProgress()
  409. self._started = True
  410. self._get_next_cap()
  411. if self._start_time != 0:
  412. self.seek(self._start_time)
  413. # This overrides the seek method from the VideoStream interface, but the name was changed
  414. # from `timecode` to `target`. For compatibility, we allow calling seek with the form
  415. # seek(0), seek(timecode=0), and seek(target=0). Specifying both arguments is an error.
  416. # pylint: disable=arguments-differ
  417. def seek(self, timecode: FrameTimecode = None, target: FrameTimecode = None) -> bool:
  418. """Seek forwards to the passed timecode.
  419. Only supports seeking forwards (i.e. timecode must be greater than the
  420. current position). Can only be used after the :meth:`start()`
  421. method has been called.
  422. Arguments:
  423. timecode: Time in video to seek forwards to. Only one of timecode or target can be set.
  424. target: Same as timecode. Only one of timecode or target can be set.
  425. Returns:
  426. bool: True if seeking succeeded, False if no more frames / end of video.
  427. Raises:
  428. ValueError: Either none or both `timecode` and `target` were set.
  429. """
  430. if timecode is None and target is None:
  431. raise ValueError('`target` must be set.')
  432. if timecode is not None and target is not None:
  433. raise ValueError('Only one of `timecode` or `target` can be set.')
  434. if target is not None:
  435. timecode = target
  436. assert timecode is not None
  437. if timecode < 0:
  438. raise ValueError("Target seek position cannot be negative!")
  439. if not self._started:
  440. self.start()
  441. timecode = self.base_timecode + timecode
  442. if self._end_time is not None and timecode > self._end_time:
  443. timecode = self._end_time
  444. # TODO: Seeking only works for the first (or current) video in the VideoManager.
  445. # Warn the user there are multiple videos in the VideoManager, and the requested
  446. # seek time exceeds the length of the first video.
  447. if len(self._cap_list) > 1 and timecode > self._first_cap_len:
  448. # TODO: This should throw an exception instead of potentially failing silently
  449. # if no logger was provided.
  450. if self._logger is not None:
  451. self._logger.error('Seeking past the first input video is not currently supported.')
  452. self._logger.warning('Seeking to end of first input.')
  453. timecode = self._first_cap_len
  454. if self._curr_cap is not None and self._end_of_video is not True:
  455. self._curr_cap.set(cv2.CAP_PROP_POS_FRAMES, timecode.get_frames() - 1)
  456. self._curr_time = timecode - 1
  457. while self._curr_time < timecode:
  458. if not self.grab():
  459. return False
  460. return True
  461. # pylint: enable=arguments-differ
  462. def release(self) -> None:
  463. """ Release (cv2.VideoCapture method), releases all open capture(s). """
  464. for cap in self._cap_list:
  465. cap.release()
  466. self._cap_list = []
  467. self._started = False
  468. def reset(self) -> None:
  469. """ Reset - Reopens captures passed to the constructor of the VideoManager.
  470. Can only be called after the :meth:`release()` method has been called.
  471. Raises:
  472. VideoDecodingInProgress: Must call :meth:`release()` before this method.
  473. """
  474. if self._started:
  475. self.release()
  476. self._started = False
  477. self._end_of_video = False
  478. self._curr_time = self.get_base_timecode()
  479. self._cap_list, self._cap_framerate, self._cap_framesize = open_captures(
  480. video_files=self._video_file_paths, framerate=self._curr_time.get_framerate())
  481. self._curr_cap, self._curr_cap_idx = None, None
  482. def get(self, capture_prop: int, index: Optional[int] = None) -> Union[float, int]:
  483. """ Get (cv2.VideoCapture method) - obtains capture properties from the current
  484. VideoCapture object in use. Index represents the same index as the original
  485. video_files list passed to the constructor. Getting/setting the position (POS)
  486. properties has no effect; seeking is implemented using VideoDecoder methods.
  487. Note that getting the property CAP_PROP_FRAME_COUNT will return the integer sum of
  488. the frame count for all VideoCapture objects if index is not specified (or is None),
  489. otherwise the frame count for the given VideoCapture index is returned instead.
  490. Arguments:
  491. capture_prop: OpenCV VideoCapture property to get (i.e. CAP_PROP_FPS).
  492. index (int, optional): Index in file_list of capture to get property from (default
  493. is zero). Index is not checked and will raise exception if out of bounds.
  494. Returns:
  495. float: Return value from calling get(property) on the VideoCapture object.
  496. """
  497. if capture_prop == cv2.CAP_PROP_FRAME_COUNT and index is None:
  498. return self._frame_length.get_frames()
  499. elif capture_prop == cv2.CAP_PROP_POS_FRAMES:
  500. return self._curr_time
  501. elif capture_prop == cv2.CAP_PROP_FPS:
  502. return self._cap_framerate
  503. elif index is None:
  504. index = 0
  505. return self._cap_list[index].get(capture_prop)
  506. def grab(self) -> bool:
  507. """ Grab (cv2.VideoCapture method) - retrieves a frame but does not return it.
  508. Returns:
  509. bool: True if a frame was grabbed, False otherwise.
  510. """
  511. if not self._started:
  512. self.start()
  513. grabbed = False
  514. if self._curr_cap is not None and not self._end_of_video:
  515. while not grabbed:
  516. grabbed = self._curr_cap.grab()
  517. if not grabbed and not self._get_next_cap():
  518. break
  519. if self._end_time is not None and self._curr_time > self._end_time:
  520. grabbed = False
  521. self._last_frame = None
  522. if grabbed:
  523. self._curr_time += 1
  524. else:
  525. self._correct_frame_length()
  526. return grabbed
  527. def retrieve(self) -> Tuple[bool, Optional[np.ndarray]]:
  528. """ Retrieve (cv2.VideoCapture method) - retrieves and returns a frame.
  529. Frame returned corresponds to last call to :meth:`grab()`.
  530. Returns:
  531. Tuple of (True, frame_image) if a frame was grabbed during the last call to grab(),
  532. and where frame_image is a numpy np.ndarray of the decoded frame. Otherwise (False, None).
  533. """
  534. if not self._started:
  535. self.start()
  536. retrieved = False
  537. if self._curr_cap is not None and not self._end_of_video:
  538. while not retrieved:
  539. retrieved, self._last_frame = self._curr_cap.retrieve()
  540. if not retrieved and not self._get_next_cap():
  541. break
  542. if self._end_time is not None and self._curr_time > self._end_time:
  543. retrieved = False
  544. self._last_frame = None
  545. return (retrieved, self._last_frame)
  546. def read(self, decode: bool = True, advance: bool = True) -> Union[np.ndarray, bool]:
  547. """ Return next frame (or current if advance = False), or False if end of video.
  548. Arguments:
  549. decode: Decode and return the frame.
  550. advance: Seek to the next frame. If False, will remain on the current frame.
  551. Returns:
  552. If decode = True, returns either the decoded frame, or False if end of video.
  553. If decode = False, a boolean indicating if the next frame was advanced to or not is
  554. returned.
  555. """
  556. if not self._started:
  557. self.start()
  558. has_grabbed = False
  559. if advance:
  560. has_grabbed = self.grab()
  561. if decode:
  562. retrieved, frame = self.retrieve()
  563. return frame if retrieved else False
  564. return has_grabbed
  565. def _get_next_cap(self) -> bool:
  566. self._curr_cap = None
  567. if self._curr_cap_idx is None:
  568. self._curr_cap_idx = 0
  569. self._curr_cap = self._cap_list[0]
  570. return True
  571. else:
  572. if not (self._curr_cap_idx + 1) < len(self._cap_list):
  573. self._end_of_video = True
  574. return False
  575. self._curr_cap_idx += 1
  576. self._curr_cap = self._cap_list[self._curr_cap_idx]
  577. return True
  578. def _correct_frame_length(self) -> None:
  579. """ Checks if the current frame position exceeds that originally calculated,
  580. and adjusts the internally calculated frame length accordingly. Called after
  581. exhausting all input frames from the video source(s).
  582. """
  583. self._end_time = self._curr_time
  584. self._frame_length = self._curr_time - self._start_time
  585. # VideoStream Interface (Some Covered Above)
  586. @property
  587. def aspect_ratio(self) -> float:
  588. """Display/pixel aspect ratio as a float (1.0 represents square pixels)."""
  589. return self._aspect_ratio
  590. @property
  591. def duration(self) -> Optional[FrameTimecode]:
  592. """Duration of the stream as a FrameTimecode, or None if non terminating."""
  593. return self.get_duration()[0]
  594. @property
  595. def position(self) -> FrameTimecode:
  596. """Current position within stream as FrameTimecode.
  597. This can be interpreted as presentation time stamp of the last frame which was
  598. decoded by calling `read` with advance=True.
  599. This method will always return 0 (e.g. be equal to `base_timecode`) if no frames
  600. have been `read`."""
  601. frames = self._curr_time.get_frames()
  602. if frames < 1:
  603. return self.base_timecode
  604. return self.base_timecode + (frames - 1)
  605. @property
  606. def position_ms(self) -> float:
  607. """Current position within stream as a float of the presentation time in milliseconds.
  608. The first frame has a time of 0.0 ms.
  609. This method will always return 0.0 if no frames have been `read`."""
  610. return self.position.get_seconds() * 1000.0
  611. @property
  612. def frame_number(self) -> int:
  613. """Current position within stream in frames as an int.
  614. 1 indicates the first frame was just decoded by the last call to `read` with advance=True,
  615. whereas 0 indicates that no frames have been `read`.
  616. This method will always return 0 if no frames have been `read`."""
  617. return self._curr_time.get_frames()
  618. @property
  619. def frame_rate(self) -> float:
  620. """Framerate in frames/sec."""
  621. return self._cap_framerate
  622. @property
  623. def frame_size(self) -> Tuple[int, int]:
  624. """Size of each video frame in pixels as a tuple of (width, height)."""
  625. return (math.trunc(self._cap_list[0].get(cv2.CAP_PROP_FRAME_WIDTH)),
  626. math.trunc(self._cap_list[0].get(cv2.CAP_PROP_FRAME_HEIGHT)))
  627. @property
  628. def is_seekable(self) -> bool:
  629. """Just returns True."""
  630. return True
  631. @property
  632. def path(self) -> Union[bytes, str]:
  633. """Video or device path."""
  634. if self._is_device:
  635. return "Device %d" % self._path
  636. return self._path
  637. @property
  638. def name(self) -> Union[bytes, str]:
  639. """Name of the video, without extension, or device."""
  640. if self._is_device:
  641. return self.path
  642. return get_file_name(self.path, include_extension=False)