1# -*- coding: utf-8 -*- 2# 3# PySceneDetect: Python-Based Video Scene Detector 4# --------------------------------------------------------------- 5# [ Site: http://www.bcastell.com/projects/PySceneDetect/ ] 6# [ Github: https://github.com/Breakthrough/PySceneDetect/ ] 7# [ Documentation: http://pyscenedetect.readthedocs.org/ ] 8# 9# Copyright (C) 2014-2020 Brandon Castellano <http://www.bcastell.com>. 10# 11# PySceneDetect is licensed under the BSD 3-Clause License; see the included 12# LICENSE file, or visit one of the following pages for details: 13# - https://github.com/Breakthrough/PySceneDetect/ 14# - http://www.bcastell.com/projects/PySceneDetect/ 15# 16# This software uses Numpy, OpenCV, click, tqdm, simpletable, and pytest. 17# See the included LICENSE files or one of the above URLs for more information. 18# 19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22# AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 23# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25# 26 27""" ``scenedetect.video_manager`` Module 28 29This module contains the :py:class:`VideoManager` class, which provides a consistent 30interface to reading videos, specific exceptions raised upon certain error 31conditions, and some global helper functions to open/close multiple videos, 32as well as validate their parameters. 33 34The :py:class:`VideoManager` can be constructed with a path to a video (or sequence of 35videos) and a start and end time/duration, then passed to a `SceneManager` 36object for performing scene detection analysis. If the start time is modified, 37then it also needs to be reflected in the `SceneManager`. 38 39The :py:class:`VideoManager` class attempts to emulate some methods of the OpenCV 40cv2.VideoCapture object, and can be used interchangably with one with 41respect to a SceneManager object. 42""" 43 44# There also used to be an asynchronous implementation in addition to the 45# synchronous VideoManager, but the performance was poor. In the future, I may 46# consider rewriting an asynchronous frame grabber in C++ and write a C-API to 47# interface with the Python ctypes module. - B.C. 48 49 50# Standard Library Imports 51from __future__ import print_function 52import os 53import math 54 55# Third-Party Library Imports 56import cv2 57 58# PySceneDetect Library Imports 59from scenedetect.platform import STRING_TYPE 60from scenedetect.frame_timecode import FrameTimecode, MINIMUM_FRAMES_PER_SECOND_FLOAT 61 62 63## 64## VideoManager Exceptions 65## 66 67class VideoOpenFailure(Exception): 68 """ VideoOpenFailure: Raised when an OpenCV VideoCapture object fails to open (i.e. calling 69 the isOpened() method returns a non True value). """ 70 def __init__(self, file_list=None, message= 71 "OpenCV VideoCapture object failed to return True when calling isOpened()."): 72 # type: (Iterable[(str, str)], str) 73 # Pass message string to base Exception class. 74 super(VideoOpenFailure, self).__init__(message) 75 # list of (filename: str, filepath: str) 76 self.file_list = file_list 77 78 79class VideoFramerateUnavailable(Exception): 80 """ VideoFramerateUnavailable: Raised when the framerate cannot be determined from the video, 81 and the framerate has not been overriden/forced in the VideoManager. """ 82 def __init__(self, file_name=None, file_path=None, message= 83 "OpenCV VideoCapture object failed to return framerate when calling " 84 "get(cv2.CAP_PROP_FPS)."): 85 # type: (str, str, str) 86 # Pass message string to base Exception class. 87 super(VideoFramerateUnavailable, self).__init__(message) 88 # Set other exception properties. 89 self.file_name = file_name 90 self.file_path = file_path 91 92 93class VideoParameterMismatch(Exception): 94 """ VideoParameterMismatch: Raised when opening multiple videos with a VideoManager, and some 95 of the video parameters (frame height, frame width, and framerate/FPS) do not match. """ 96 def __init__(self, file_list=None, message= 97 "OpenCV VideoCapture object parameters do not match."): 98 # type: (Iterable[Tuple[int, float, float, str, str]], str) 99 # Pass message string to base Exception class. 100 super(VideoParameterMismatch, self).__init__(message) 101 # list of (param_mismatch_type: int, parameter value, expected value, 102 # filename: str, filepath: str) 103 # where param_mismatch_type is an OpenCV CAP_PROP (e.g. CAP_PROP_FPS). 104 self.file_list = file_list 105 106 107class VideoDecodingInProgress(RuntimeError): 108 """ VideoDecodingInProgress: Raised when attempting to call certain VideoManager methods that 109 must be called *before* start() has been called. """ 110 # pylint: disable=unnecessary-pass 111 pass 112 113 114class VideoDecoderNotStarted(RuntimeError): 115 """ VideoDecodingInProgress: Raised when attempting to call certain VideoManager methods that 116 must be called *after* start() has been called. """ 117 # pylint: disable=unnecessary-pass 118 pass 119 120 121class InvalidDownscaleFactor(ValueError): 122 """ InvalidDownscaleFactor: Raised when trying to set invalid downscale factor, 123 i.e. the supplied downscale factor was not a positive integer greater than zero. """ 124 # pylint: disable=unnecessary-pass 125 pass 126 127 128## 129## VideoManager Constants & Helper Functions 130## 131 132DEFAULT_DOWNSCALE_FACTORS = { 133 3200: 12, # ~4k 134 2100: 8, # ~2k 135 1700: 6, # ~1080p 136 1200: 5, 137 900: 4, # ~720p 138 600: 3, 139 400: 2 # ~480p 140} 141"""Dict[int, int]: The default downscale factor for a video of size W x H, 142which enforces the constraint that W >= 200 to ensure an adequate amount 143of pixels for scene detection while providing a speedup in processing. """ 144 145 146 147def compute_downscale_factor(frame_width): 148 # type: (int) -> int 149 """ Compute Downscale Factor: Returns the optimal default downscale factor based on 150 a video's resolution (specifically, the width parameter). 151 152 Returns: 153 int: The defalt downscale factor to use with a video of frame_height x frame_width. 154 """ 155 for width in sorted(DEFAULT_DOWNSCALE_FACTORS, reverse=True): 156 if frame_width >= width: 157 return DEFAULT_DOWNSCALE_FACTORS[width] 158 return 1 159 160 161def get_video_name(video_file): 162 # type: (str) -> Tuple[str, str] 163 """ Get Video Name: Returns a string representing the video file/device name. 164 165 Returns: 166 str: Video file name or device ID. In the case of a video, only the file 167 name is returned, not the whole path. For a device, the string format 168 is 'Device 123', where 123 is the integer ID of the capture device. 169 """ 170 if isinstance(video_file, int): 171 return ('Device %d' % video_file, video_file) 172 return (os.path.split(video_file)[1], video_file) 173 174 175def get_num_frames(cap_list): 176 # type: (List[cv2.VideoCapture]) -> int 177 """ Get Number of Frames: Returns total number of frames in the cap_list. 178 179 Calls get(CAP_PROP_FRAME_COUNT) and returns the sum for all VideoCaptures. 180 """ 181 return sum([math.trunc(cap.get(cv2.CAP_PROP_FRAME_COUNT)) for cap in cap_list]) 182 183 184def open_captures(video_files, framerate=None, validate_parameters=True): 185 # type: (Iterable[str], float, bool) -> Tuple[List[VideoCapture], float, Tuple[int, int]] 186 """ Open Captures - helper function to open all capture objects, set the framerate, 187 and ensure that all open captures have been opened and the framerates match on a list 188 of video file paths, or a list containing a single device ID. 189 190 Arguments: 191 video_files (list of str(s)/int): A list of one or more paths (str), or a list 192 of a single integer device ID, to open as an OpenCV VideoCapture object. 193 A ValueError will be raised if the list does not conform to the above. 194 framerate (float, optional): Framerate to assume when opening the video_files. 195 If not set, the first open video is used for deducing the framerate of 196 all videos in the sequence. 197 validate_parameters (bool, optional): If true, will ensure that the frame sizes 198 (width, height) and frame rate (FPS) of all passed videos is the same. 199 A VideoParameterMismatch is raised if the framerates do not match. 200 201 Returns: 202 A tuple of form (cap_list, framerate, framesize) where cap_list is a list of open 203 OpenCV VideoCapture objects in the same order as the video_files list, framerate 204 is a float of the video(s) framerate(s), and framesize is a tuple of (width, height) 205 where width and height are integers representing the frame size in pixels. 206 207 Raises: 208 ValueError: No video file(s) specified, or invalid/multiple device IDs specified. 209 TypeError: `framerate` must be type `float`. 210 IOError: Video file(s) not found. 211 VideoFramerateUnavailable: Video framerate could not be obtained and `framerate` 212 was not set manually. 213 VideoParameterMismatch: All videos in `video_files` do not have equal parameters. 214 Set `validate_parameters=False` to skip this check. 215 VideoOpenFailure: Video(s) could not be opened. 216 """ 217 is_device = False 218 if not video_files: 219 raise ValueError("Expected at least 1 video file or device ID.") 220 if isinstance(video_files[0], int): 221 if len(video_files) > 1: 222 raise ValueError("If device ID is specified, no video sources may be appended.") 223 elif video_files[0] < 0: 224 raise ValueError("Invalid/negative device ID specified.") 225 is_device = True 226 elif not all([isinstance(video_file, (str, STRING_TYPE)) for video_file in video_files]): 227 raise ValueError("Unexpected element type in video_files list (expected str(s)/int).") 228 elif framerate is not None and not isinstance(framerate, float): 229 raise TypeError("Expected type float for parameter framerate.") 230 # Check if files exist. 231 if not is_device and any([not os.path.exists(video_file) for video_file in video_files]): 232 raise IOError("Video file(s) not found.") 233 cap_list = [] 234 235 try: 236 cap_list = [cv2.VideoCapture(video_file) for video_file in video_files] 237 video_names = [get_video_name(video_file) for video_file in video_files] 238 closed_caps = [video_names[i] for i, cap in 239 enumerate(cap_list) if not cap.isOpened()] 240 if closed_caps: 241 raise VideoOpenFailure(closed_caps) 242 243 cap_framerates = [cap.get(cv2.CAP_PROP_FPS) for cap in cap_list] 244 cap_framerate, check_framerate = validate_capture_framerate( 245 video_names, cap_framerates, framerate) 246 # Store frame sizes as integers (VideoCapture.get() returns float). 247 cap_frame_sizes = [(math.trunc(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), 248 math.trunc(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))) 249 for cap in cap_list] 250 cap_frame_size = cap_frame_sizes[0] 251 252 # If we need to validate the parameters, we check that the FPS and width/height 253 # of all open captures is identical (or almost identical in the case of FPS). 254 if validate_parameters: 255 validate_capture_parameters( 256 video_names=video_names, cap_frame_sizes=cap_frame_sizes, 257 check_framerate=check_framerate, cap_framerates=cap_framerates) 258 259 except: 260 release_captures(cap_list) 261 raise 262 263 return (cap_list, cap_framerate, cap_frame_size) 264 265 266def release_captures(cap_list): 267 # type: (Iterable[VideoCapture]) -> None 268 """ Close Captures: Calls release() on every capture in cap_list. """ 269 for cap in cap_list: 270 cap.release() 271 272 273def close_captures(cap_list): 274 # type: (Iterable[VideoCapture]) -> None 275 """ Close Captures: Calls close() on every capture in cap_list. """ 276 for cap in cap_list: 277 cap.close() 278 279 280def validate_capture_framerate(video_names, cap_framerates, framerate=None): 281 # type: (List[Tuple[str, str]], List[float], Optional[float]) -> Tuple[float, bool] 282 """ Validate Capture Framerate: Ensures that the passed capture framerates are valid and equal. 283 284 Raises: 285 ValueError: Invalid framerate (must be positive non-zero value). 286 TypeError: Framerate must be of type float. 287 VideoFramerateUnavailable: Framerate for video could not be obtained, 288 and `framerate` was not set. 289 """ 290 check_framerate = True 291 cap_framerate = cap_framerates[0] 292 if framerate is not None: 293 if isinstance(framerate, float): 294 if framerate < MINIMUM_FRAMES_PER_SECOND_FLOAT: 295 raise ValueError("Invalid framerate (must be a positive non-zero value).") 296 cap_framerate = framerate 297 check_framerate = False 298 else: 299 raise TypeError("Expected float for framerate, got %s." % type(framerate).__name__) 300 else: 301 unavailable_framerates = [(video_names[i][0], video_names[i][1]) 302 for i, fps in enumerate(cap_framerates) 303 if fps < MINIMUM_FRAMES_PER_SECOND_FLOAT] 304 if unavailable_framerates: 305 raise VideoFramerateUnavailable(unavailable_framerates) 306 return (cap_framerate, check_framerate) 307 308 309def validate_capture_parameters(video_names, cap_frame_sizes, check_framerate=False, 310 cap_framerates=None): 311 # type: (List[Tuple[str, str]], List[Tuple[int, int]], Optional[bool], 312 # Optional[List[float]]) -> None 313 """ Validate Capture Parameters: Ensures that all passed capture frame sizes and (optionally) 314 framerates are equal. Raises VideoParameterMismatch if there is a mismatch. 315 316 Raises: 317 VideoParameterMismatch 318 """ 319 bad_params = [] 320 max_framerate_delta = MINIMUM_FRAMES_PER_SECOND_FLOAT 321 # Check heights/widths match. 322 bad_params += [(cv2.CAP_PROP_FRAME_WIDTH, frame_size[0], 323 cap_frame_sizes[0][0], video_names[i][0], video_names[i][1]) for 324 i, frame_size in enumerate(cap_frame_sizes) 325 if abs(frame_size[0] - cap_frame_sizes[0][0]) > 0] 326 bad_params += [(cv2.CAP_PROP_FRAME_HEIGHT, frame_size[1], 327 cap_frame_sizes[0][1], video_names[i][0], video_names[i][1]) for 328 i, frame_size in enumerate(cap_frame_sizes) 329 if abs(frame_size[1] - cap_frame_sizes[0][1]) > 0] 330 # Check framerates if required. 331 if check_framerate: 332 bad_params += [(cv2.CAP_PROP_FPS, fps, cap_framerates[0], video_names[i][0], 333 video_names[i][1]) for i, fps in enumerate(cap_framerates) 334 if math.fabs(fps - cap_framerates[0]) > max_framerate_delta] 335 336 if bad_params: 337 raise VideoParameterMismatch(bad_params) 338 339 340## 341## VideoManager Class Implementation 342## 343 344class VideoManager(object): 345 """ Provides a cv2.VideoCapture-like interface to a set of one or more video files, 346 or a single device ID. Supports seeking and setting end time/duration. """ 347 348 def __init__(self, video_files, framerate=None, logger=None): 349 # type: (List[str], Optional[float]) 350 """ VideoManager Constructor Method (__init__) 351 352 Arguments: 353 video_files (list of str(s)/int): A list of one or more paths (str), or a list 354 of a single integer device ID, to open as an OpenCV VideoCapture object. 355 framerate (float, optional): Framerate to assume when storing FrameTimecodes. 356 If not set (i.e. is None), it will be deduced from the first open capture 357 in video_files, else raises a VideoFramerateUnavailable exception. 358 359 Raises: 360 ValueError: No video file(s) specified, or invalid/multiple device IDs specified. 361 TypeError: `framerate` must be type `float`. 362 IOError: Video file(s) not found. 363 VideoFramerateUnavailable: Video framerate could not be obtained and `framerate` 364 was not set manually. 365 VideoParameterMismatch: All videos in `video_files` do not have equal parameters. 366 Set `validate_parameters=False` to skip this check. 367 VideoOpenFailure: Video(s) could not be opened. 368 """ 369 if not video_files: 370 raise ValueError("At least one string/integer must be passed in the video_files list.") 371 # These VideoCaptures are only open in this process. 372 self._cap_list, self._cap_framerate, self._cap_framesize = open_captures( 373 video_files=video_files, framerate=framerate) 374 self._end_of_video = False 375 self._start_time = self.get_base_timecode() 376 self._end_time = None 377 self._curr_time = self.get_base_timecode() 378 self._last_frame = None 379 self._curr_cap, self._curr_cap_idx = None, None 380 self._video_file_paths = video_files 381 self._logger = logger 382 if self._logger is not None: 383 self._logger.info( 384 'Loaded %d video%s, framerate: %.2f FPS, resolution: %d x %d', 385 len(self._cap_list), 's' if len(self._cap_list) > 1 else '', 386 self.get_framerate(), *self.get_framesize()) 387 self._started = False 388 self._downscale_factor = 1 389 self._frame_length = get_num_frames(self._cap_list) 390 391 392 def set_downscale_factor(self, downscale_factor=None): 393 # type: (Optional[int]) -> None 394 """ Set Downscale Factor - sets the downscale/subsample factor of returned frames. 395 396 If N is the downscale_factor, the size of the frames returned becomes 397 frame_width/N x frame_height/N via subsampling. 398 399 If downscale_factor is None, the downscale factor is computed automatically 400 based on the current video's resolution. A downscale_factor of 1 indicates 401 no downscaling. 402 """ 403 if downscale_factor is None: 404 self._downscale_factor = compute_downscale_factor(self.get_framesize()[0]) 405 else: 406 if not downscale_factor > 0: 407 raise InvalidDownscaleFactor() 408 self._downscale_factor = downscale_factor 409 if self._logger is not None: 410 effective_framesize = self.get_framesize_effective() 411 self._logger.info( 412 'Downscale factor set to %d, effective resolution: %d x %d', 413 self._downscale_factor, effective_framesize[0], effective_framesize[1]) 414 415 416 def get_num_videos(self): 417 # type: () -> int 418 """ Get Number of Videos - returns the length of the internal capture list, 419 representing the number of videos the VideoManager was constructed with. 420 421 Returns: 422 int: Number of videos, equal to length of capture list. 423 """ 424 return len(self._cap_list) 425 426 427 def get_video_paths(self): 428 # type: () -> List[str] 429 """ Get Video Paths - returns list of strings containing paths to the open video(s). 430 431 Returns: 432 List[str]: List of paths to the video files opened by the VideoManager. 433 """ 434 return list(self._video_file_paths) 435 436 437 def get_framerate(self): 438 # type: () -> float 439 """ Get Framerate - returns the framerate the VideoManager is assuming for all 440 open VideoCaptures. Obtained from either the capture itself, or the passed 441 framerate parameter when the VideoManager object was constructed. 442 443 Returns: 444 float: Framerate, in frames/sec. 445 """ 446 return self._cap_framerate 447 448 449 def get_base_timecode(self): 450 # type: () -> FrameTimecode 451 """ Get Base Timecode - returns a FrameTimecode object at frame 0 / time 00:00:00. 452 453 The timecode returned by this method can be used to perform arithmetic (e.g. 454 addition), passing the resulting values back to the VideoManager (e.g. for the 455 :py:meth:`set_duration()` method), as the framerate of the returned FrameTimecode 456 object matches that of the VideoManager. 457 458 As such, this method is equivalent to creating a FrameTimecode at frame 0 with 459 the VideoManager framerate, for example, given a VideoManager called obj, 460 the following expression will evaluate as True: 461 462 obj.get_base_timecode() == FrameTimecode(0, obj.get_framerate()) 463 464 Furthermore, the base timecode object returned by a particular VideoManager 465 should not be passed to another one, unless you first verify that their 466 framerates are the same. 467 468 Returns: 469 FrameTimecode object set to frame 0/time 00:00:00 with the video(s) framerate. 470 """ 471 return FrameTimecode(timecode=0, fps=self._cap_framerate) 472 473 474 def get_current_timecode(self): 475 # type: () -> FrameTimecode 476 """ Get Current Timecode - returns a FrameTimecode object at current VideoManager position. 477 478 Returns: 479 FrameTimecode: Timecode at the current VideoManager position. 480 """ 481 return self._curr_time 482 483 484 def get_framesize(self): 485 # type: () -> Tuple[int, int] 486 """ Get Frame Size - returns the frame size of the video(s) open in the 487 VideoManager's capture objects. 488 489 Returns: 490 Tuple[int, int]: Video frame size in the form (width, height) where width 491 and height represent the size of the video frame in pixels. 492 """ 493 return self._cap_framesize 494 495 496 def get_framesize_effective(self): 497 # type: () -> Tuple[int, int] 498 """ Get Frame Size - returns the frame size of the video(s) open in the 499 VideoManager's capture objects, divided by the current downscale factor. 500 501 Returns: 502 Tuple[int, int]: Video frame size in the form (width, height) where width 503 and height represent the size of the video frame in pixels. 504 """ 505 return [num_pixels / self._downscale_factor for num_pixels in self._cap_framesize] 506 507 508 def set_duration(self, duration=None, start_time=None, end_time=None): 509 # type: (Optional[FrameTimecode], Optional[FrameTimecode], Optional[FrameTimecode]) -> None 510 """ Set Duration - sets the duration/length of the video(s) to decode, as well as 511 the start/end times. Must be called before :py:meth:`start()` is called, otherwise 512 a VideoDecodingInProgress exception will be thrown. May be called after 513 :py:meth:`reset()` as well. 514 515 Arguments: 516 duration (Optional[FrameTimecode]): The (maximum) duration in time to 517 decode from the opened video(s). Mutually exclusive with end_time 518 (i.e. if duration is set, end_time must be None). 519 start_time (Optional[FrameTimecode]): The time/first frame at which to 520 start decoding frames from. If set, the input video(s) will be 521 seeked to when start() is called, at which point the frame at 522 start_time can be obtained by calling retrieve(). 523 end_time (Optional[FrameTimecode]): The time at which to stop decoding 524 frames from the opened video(s). Mutually exclusive with duration 525 (i.e. if end_time is set, duration must be None). 526 527 Raises: 528 VideoDecodingInProgress: Must call before start(). 529 """ 530 if self._started: 531 raise VideoDecodingInProgress() 532 533 # Ensure any passed timecodes have the proper framerate. 534 if ((duration is not None and not duration.equal_framerate(self._cap_framerate)) or 535 (start_time is not None and not start_time.equal_framerate(self._cap_framerate)) or 536 (end_time is not None and not end_time.equal_framerate(self._cap_framerate))): 537 raise ValueError("FrameTimecode framerate does not match.") 538 539 if duration is not None and end_time is not None: 540 raise TypeError("Only one of duration and end_time may be specified, not both.") 541 542 if start_time is not None: 543 self._start_time = start_time 544 545 if end_time is not None: 546 if end_time < start_time: 547 raise ValueError("end_time is before start_time in time.") 548 self._end_time = end_time 549 elif duration is not None: 550 self._end_time = self._start_time + duration 551 552 if self._end_time is not None: 553 self._frame_length = min(self._frame_length, self._end_time.get_frames() + 1) 554 self._frame_length -= self._start_time.get_frames() 555 556 if self._logger is not None: 557 self._logger.info( 558 'Duration set, start: %s, duration: %s, end: %s.', 559 start_time.get_timecode() if start_time is not None else start_time, 560 duration.get_timecode() if duration is not None else duration, 561 end_time.get_timecode() if end_time is not None else end_time) 562 563 564 def get_duration(self): 565 # type: () -> FrameTimecode 566 """ Get Duration - gets the duration/length of the video(s) to decode, 567 as well as the start/end times. 568 569 If the end time was not set by :py:meth:`set_duration()`, the end timecode 570 is calculated as the start timecode + total duration. 571 572 Returns: 573 Tuple[FrameTimecode, FrameTimecode, FrameTimecode]: The current video(s) 574 total duration, start timecode, and end timecode. 575 """ 576 frame_length = self.get_base_timecode() + self._frame_length 577 end_time = self._end_time 578 if end_time is None: 579 end_time = self.get_base_timecode() + frame_length 580 return (frame_length, self._start_time, end_time) 581 582 583 def start(self): 584 # type: () -> None 585 """ Start - starts video decoding and seeks to start time. Raises 586 exception VideoDecodingInProgress if the method is called after the 587 decoder process has already been started. 588 589 Raises: 590 VideoDecodingInProgress: Must call :py:meth:`stop()` before this 591 method if :py:meth:`start()` has already been called after 592 initial construction. 593 """ 594 if self._started: 595 raise VideoDecodingInProgress() 596 597 self._started = True 598 self._get_next_cap() 599 self.seek(self._start_time) 600 601 602 def seek(self, timecode): 603 # type: (FrameTimecode) -> bool 604 """ Seek - seeks forwards to the passed timecode. 605 606 Only supports seeking forwards (i.e. timecode must be greater than the 607 current position). Can only be used after the :py:meth:`start()` 608 method has been called. 609 610 Arguments: 611 timecode (FrameTimecode): Time in video to seek forwards to. 612 613 Returns: 614 bool: True if seeking succeeded, False if no more frames / end of video. 615 616 Raises: 617 VideoDecoderNotStarted: Must call :py:meth:`start()` before this method. 618 """ 619 if not self._started: 620 raise VideoDecoderNotStarted() 621 622 if isinstance(self._curr_cap, cv2.VideoCapture): 623 if self._curr_cap is not None and self._end_of_video is not True: 624 self._curr_cap.set(cv2.CAP_PROP_POS_FRAMES, timecode.get_frames() - 1) 625 self._curr_time = timecode - 1 626 627 while self._curr_time < timecode: 628 if not self.grab(): # raises VideoDecoderNotStarted if start() was not called 629 return False 630 return True 631 632 633 def release(self): 634 # type: () -> None 635 """ Release (cv2.VideoCapture method), releases all open capture(s). """ 636 release_captures(self._cap_list) 637 self._cap_list = [] 638 self._started = False 639 640 641 def reset(self): 642 # type: () -> None 643 """ Reset - Reopens captures passed to the constructor of the VideoManager. 644 645 Can only be called after the :py:meth:`release()` method has been called. 646 647 Raises: 648 VideoDecodingInProgress: Must call :py:meth:`release()` before this method. 649 """ 650 if self._started: 651 raise VideoDecodingInProgress() 652 653 self._started = False 654 self._end_of_video = False 655 self._curr_time = self.get_base_timecode() 656 self._cap_list, self._cap_framerate, self._cap_framesize = open_captures( 657 video_files=self._video_file_paths, framerate=self._curr_time.get_framerate()) 658 self._curr_cap, self._curr_cap_idx = None, None 659 660 661 def get(self, capture_prop, index=None): 662 # type: (int, Optional[int]) -> Union[float, int] 663 """ Get (cv2.VideoCapture method) - obtains capture properties from the current 664 VideoCapture object in use. Index represents the same index as the original 665 video_files list passed to the constructor. Getting/setting the position (POS) 666 properties has no effect; seeking is implemented using VideoDecoder methods. 667 668 Note that getting the property CAP_PROP_FRAME_COUNT will return the integer sum of 669 the frame count for all VideoCapture objects if index is not specified (or is None), 670 otherwise the frame count for the given VideoCapture index is returned instead. 671 672 Arguments: 673 capture_prop: OpenCV VideoCapture property to get (i.e. CAP_PROP_FPS). 674 index (int, optional): Index in file_list of capture to get property from (default 675 is zero). Index is not checked and will raise exception if out of bounds. 676 677 Returns: 678 float: Return value from calling get(property) on the VideoCapture object. 679 """ 680 if capture_prop == cv2.CAP_PROP_FRAME_COUNT and index is None: 681 return self._frame_length 682 elif capture_prop == cv2.CAP_PROP_POS_FRAMES: 683 return self._curr_time 684 elif index is None: 685 index = 0 686 return self._cap_list[index].get(capture_prop) 687 688 689 def grab(self): 690 # type: () -> bool 691 """ Grab (cv2.VideoCapture method) - retrieves a frame but does not return it. 692 693 Returns: 694 bool: True if a frame was grabbed, False otherwise. 695 696 Raises: 697 VideoDecoderNotStarted: Must call :py:meth:`start()` before this method. 698 """ 699 if not self._started: 700 raise VideoDecoderNotStarted() 701 702 grabbed = False 703 if self._curr_cap is not None and not self._end_of_video: 704 while not grabbed: 705 grabbed = self._curr_cap.grab() 706 if not grabbed and not self._get_next_cap(): 707 break 708 else: 709 self._curr_time += 1 710 if self._end_time is not None and self._curr_time > self._end_time: 711 grabbed = False 712 self._last_frame = None 713 return grabbed 714 715 716 def retrieve(self): 717 # type: () -> Tuple[bool, Union[None, numpy.ndarray]] 718 """ Retrieve (cv2.VideoCapture method) - retrieves and returns a frame. 719 720 Frame returned corresponds to last call to :py:meth:`grab()`. 721 722 Returns: 723 Tuple[bool, Union[None, numpy.ndarray]]: Returns tuple of 724 (True, frame_image) if a frame was grabbed during the last call 725 to grab(), and where frame_image is a numpy ndarray of the 726 decoded frame, otherwise returns (False, None). 727 728 Raises: 729 VideoDecoderNotStarted: Must call :py:meth:`start()` before this method. 730 """ 731 if not self._started: 732 raise VideoDecoderNotStarted() 733 734 retrieved = False 735 if self._curr_cap is not None and not self._end_of_video: 736 while not retrieved: 737 retrieved, self._last_frame = self._curr_cap.retrieve() 738 if not retrieved and not self._get_next_cap(): 739 break 740 if self._downscale_factor > 1: 741 self._last_frame = self._last_frame[ 742 ::self._downscale_factor, ::self._downscale_factor, :] 743 if self._end_time is not None and self._curr_time > self._end_time: 744 retrieved = False 745 self._last_frame = None 746 return (retrieved, self._last_frame) 747 748 749 def read(self): 750 # type: () -> Tuple[bool, Union[None, numpy.ndarray]] 751 """ Read (cv2.VideoCapture method) - retrieves and returns a frame. 752 753 Returns: 754 Tuple[bool, Union[None, numpy.ndarray]]: Returns tuple of 755 (True, frame_image) if a frame was grabbed, where frame_image 756 is a numpy ndarray of the decoded frame, otherwise (False, None). 757 758 Raises: 759 VideoDecoderNotStarted: Must call :py:meth:`start()` before this method. 760 """ 761 if not self._started: 762 raise VideoDecoderNotStarted() 763 764 read_frame = False 765 if self._curr_cap is not None and not self._end_of_video: 766 read_frame, self._last_frame = self._curr_cap.read() 767 768 # Switch to the next capture when the current one is over 769 if not read_frame and self._get_next_cap(): 770 read_frame, self._last_frame = self._curr_cap.read() 771 772 # Downscale frame if there was any 773 if read_frame and self._downscale_factor > 1: 774 self._last_frame = self._last_frame[ 775 ::self._downscale_factor, ::self._downscale_factor, :] 776 777 if self._end_time is not None and self._curr_time > self._end_time: 778 read_frame = False 779 self._last_frame = None 780 if read_frame: 781 self._curr_time += 1 782 return (read_frame, self._last_frame) 783 784 785 def _get_next_cap(self): 786 # type: () -> bool 787 self._curr_cap = None 788 if self._curr_cap_idx is None: 789 self._curr_cap_idx = 0 790 self._curr_cap = self._cap_list[0] 791 return True 792 else: 793 if not (self._curr_cap_idx + 1) < len(self._cap_list): 794 self._end_of_video = True 795 return False 796 self._curr_cap_idx += 1 797 self._curr_cap = self._cap_list[self._curr_cap_idx] 798 return True 799