simple_ffmpeg_batch_io.VideoIO
Read/write video images or batches of images from video file using FFmpeg backend.
This module defines the main VideoIO class used to open videos,
read images or batches of frames, and write processed outputs.
Authors
Dominique Vaufreydaz (From original C++ code: https://github.com/Vaufreyd/ReadWriteVideosWithOpenCV)
1""" 2Read/write video images or batches of images from video file using FFmpeg backend. 3 4This module defines the main `VideoIO` class used to open videos, 5read images or batches of frames, and write processed outputs. 6 7Authors 8------- 9Dominique Vaufreydaz (From original C++ code: https://github.com/Vaufreyd/ReadWriteVideosWithOpenCV) 10 11""" 12 13__authors__ = ("Dominique Vaufreydaz") 14 15import sys 16import subprocess as sp 17import re 18from enum import Enum 19 20import numpy as np 21 22from .FrameCounter import FrameCounter 23from .FrameContainer import FrameContainer 24from .PipeMode import PipeMode 25 26# init static_ffmpeg at import time, first time it will download ffmpeg executables 27import static_ffmpeg 28static_ffmpeg.add_paths() 29 30class VideoIO: 31 # "static" variables to ffmpeg, ffprobe executables 32 videoProgram, paramProgram = static_ffmpeg.run.get_or_fetch_platform_executables_else_raise() 33 34 class VideoIOException(Exception): 35 """ 36 Dedicated exception class for VideoIO class. 37 """ 38 def __init__(self, message="Error while reading/writing video occurs"): 39 self.message = message 40 super().__init__(self.message) 41 42 class PixelFormat(Enum): 43 """ 44 Enum class for supported input video type: GBR 24 bits or RGB 24 bis. 45 """ 46 GBR24 = 'bgr24' # default format 47 RGB24 = 'rgb24' 48 49 @classmethod 50 def reader(cls, filename, **kwargs): 51 """ 52 Create and open a VideoIO object in reader mode (read a video file) 53 54 See `VideoIO.open` for the full list 55 of accepted parameters. 56 """ 57 reader = cls() 58 reader.open(filename,**kwargs) 59 return reader 60 61 @classmethod 62 def writer(cls, filename, width, height, fps, **kwargs): 63 """ 64 Create and open a VideoIO object in writer mode (write a video file) 65 66 See `VideoIO.create` for the full list 67 of accepted parameters. 68 """ 69 writer = cls() 70 writer.create(filename, width, height, fps, **kwargs) 71 return writer 72 73 # To use with context manager "with VideoIO.reader(...) as f:' for instance 74 def __enter__(self): 75 """ 76 Method call at initialisation of a context manager like "with VideoIO.reader(...) as f:' for instance 77 """ 78 # simply return myself 79 return self 80 81 def __exit__(self, exc_type, exc_val, exc_tb): 82 """ 83 Method call when existing of a context manager like "with VideoIO.reader(...) as f:' for instance 84 """ 85 # close VideoIO 86 self.close() 87 return False 88 89 @staticmethod 90 def get_time_in_sec(filename, *, debug=False, logLevel=16): 91 """ 92 Static method to get length of a video file in seconds including milliseconds as decimal part. 93 94 Parameters 95 ---------- 96 filename : str or path 97 Video file name. 98 99 debug : bool (default False) 100 Show debug info. 101 102 log_level: int (default 16) 103 Log level to pass to the underlying ffmpeg/ffprobe command. 104 105 Returns 106 ---------- 107 float 108 Length in seconds of video file (including milliseconds as decimal part) 109 """ 110 111 cmd = [VideoIO.paramProgram, # ffprobe 112 '-hide_banner', 113 '-loglevel', str(logLevel), 114 '-show_entries', 'format=duration', 115 '-of', 'default=noprint_wrappers=1:nokey=1', 116 filename 117 ] 118 119 if debug == True: 120 print(' '.join(cmd)) 121 122 # call ffprobe and get params in one single line 123 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 124 output = lpipe.stdout.readlines() 125 lpipe.terminate() 126 # transform Bytes output to one single string 127 output = ''.join( [element.decode('utf-8') for element in output]) 128 129 try: 130 return float(output) 131 except (ValueError, TypeError): 132 return None 133 134 @staticmethod 135 def get_params(filename, *, debug=False, logLevel=16): 136 """ 137 Static method to get params (width, height, fps) from a video file. 138 139 Parameters 140 ---------- 141 filename: str or path 142 Video filename. 143 144 debug: bool (default (False) 145 Show debug info. 146 147 log_level: int (default 16) 148 Log level to pass to the underlying ffmpeg/ffprobe command. 149 150 Returns 151 ---------- 152 tuple 153 Tuple containing (width, height, fps) of the video 154 """ 155 cmd = [VideoIO.paramProgram, # ffprobe 156 '-hide_banner', 157 '-loglevel', str(logLevel), 158 '-show_entries', 'stream=width,height,r_frame_rate', 159 filename 160 ] 161 162 if debug == True: 163 print(' '.join(cmd)) 164 165 # call ffprobe and get params in one single line 166 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 167 output = lpipe.stdout.readlines() 168 lpipe.terminate() 169 # transform Bytes output to one single string 170 output = ''.join( [element.decode('utf-8') for element in output]) 171 172 pattern_width = r'width=(\d+)' 173 pattern_height = r'height=(\d+)' 174 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 175 176 # Search for values in the ffprobe output 177 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 178 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 179 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 180 181 # Extraction des valeurs 182 if match_width: 183 width = int(match_width.group(1)) 184 else: 185 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 186 187 if match_height: 188 height = int(match_height.group(1)) 189 else: 190 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 191 192 if match_fps: 193 numerator = float(match_fps.group(1)) 194 denominator = float(match_fps.group(2)) 195 fps = numerator / denominator 196 else: 197 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 198 199 return (width, height, fps) 200 201 # Attributes 202 mode: PipeMode 203 """ Pipemode of the current object (default PipeMode.UNK_MODE)""" 204 205 loglevel: int 206 """ loglevel of the underlying ffmpeg backend for this object (default 16)""" 207 208 debugModel: bool 209 """ debutMode flag for this object (print debut info, default False)""" 210 211 width: int 212 """ width of images (default -1) """ 213 214 height: int 215 """ height of images (default -1) """ 216 217 fps: float 218 """ fps of video (default -1.0) """ 219 220 pipe: sp.Popen 221 """ pipe object to ffmpeg/ffprobe (default None)""" 222 223 shape: tuple 224 """ Shape of images (default (None, None, None))""" 225 226 imageSize: int 227 """ Weight in bytes of one image (default -1)""" 228 229 filename: str 230 """ Filename of the video file (default None)""" 231 232 frame_counter: FrameCounter 233 """ `Framecounter` object to count ellapsed time (default None)""" 234 235 def __init__(self, *, logLevel = 16, debugMode = False): 236 """ 237 Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode 238 239 Parameters 240 ---------- 241 log_level: int (default 16) 242 Log level to pass to the underlying ffmpeg/ffprobe command. 243 244 debugMode: bool (default (False) 245 Show debug info. while processing video 246 """ 247 248 self.mode = PipeMode.UNK_MODE 249 self.logLevel = logLevel 250 self.debugMode = debugMode 251 252 # Call init() method 253 self.init() 254 255 def init(self): 256 """ 257 Init or reinit a VideoIO object. 258 """ 259 self.width = -1 260 self.height = -1 261 self.fps = -1.0 262 self.pipe = None 263 self.shape = (None, None, None) 264 self.imageSize = -1 265 self.filename = None 266 self.frame_counter = None 267 268 _repr_exclude = {"pipe"} 269 """ List of excluded attribute for string conversion. """ 270 271 # converting the object to a string representation 272 def __repr__(self): 273 """ 274 Convert object (excluding attributes in _repr_exclude) to string representation. 275 """ 276 attrs = ", ".join( 277 f"{k}={v!r}" 278 for k, v in self.__dict__.items() 279 if k not in self._repr_exclude 280 ) 281 return f"{self.__class__.__name__}({attrs})" 282 283 __str__ = __repr__ 284 """ String representation """ 285 286 def get_elapsed_time_as_str(self) -> str: 287 """ 288 Method to get elapsed time (float value) as str from `frame_counter` attribute. 289 290 Returns 291 ---------- 292 str or None 293 Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds 294 None if no frame counter are available. 295 """ 296 if self.frame_counter is None: 297 return None 298 return self.frame_counter.get_elapsed_time_as_str() 299 300 def get_formated_elapsed_time_as_str(self,show_ms=True) -> str: 301 """ 302 Method to get elapsed time (hour format) as str from `frame_counter` attribute. 303 304 Returns 305 ---------- 306 str or None 307 Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds 308 None if no frame counter are available. 309 """ 310 if self.frame_counter is None: 311 return None 312 return self.frame_counter.get_formated_elapsed_time_as_str() 313 314 def get_elapsed_time(self) -> float: 315 """ 316 Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute. 317 318 Returns 319 ---------- 320 float or None 321 Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds 322 None if no frame counter are available. 323 """ 324 if self.frame_counter is None: 325 return None 326 return self.frame_counter.get_elapsed_time() 327 328 def is_opened(self) -> bool: 329 """ 330 Method to get status of the underlying pipe to ffmpeg. 331 332 Returns 333 ---------- 334 bool 335 True if pipe is opened (reading or writing mode), False if not. 336 """ 337 # is the pipe opened? 338 if self.pipe is not None and self.pipe.poll() is None: 339 return True 340 341 return False 342 343 def close(self) -> None: 344 """ 345 Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods. 346 """ 347 if self.pipe is not None: 348 if self.mode == PipeMode.WRITE_MODE: 349 # killing will make ffmpeg not finish properly the job, close the pipe 350 # to let it know that no more data are comming 351 self.pipe.stdin.close() 352 else: # self.mode == PipeMode.READ_MODE 353 # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows 354 self.pipe.kill() 355 356 # wait for subprocess to end 357 self.pipe.wait() 358 359 # reinit object for later use 360 self.init() 361 362 def create(self, width, height, fps, *, writeOverExistingFile = False, 363 inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool: 364 """ 365 Method to create a video using parametrized access through ffmpeg. Importante note: calling create 366 on a VideoIO will close any former open video. 367 368 Parameters 369 ---------- 370 filename: str or path 371 filename of path to the file (mp4, avi, ...) 372 373 width: int 374 If defined as a positive value, width of output images will be set to this value. 375 376 height: int 377 If defined as a positive value, height of output images will be set to this value. 378 379 fps: 380 If defined as a positive value, fps of output video will be set to this value. 381 382 inputEncoding: PixelFormat optional (default PixelFormat.BGR24) 383 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 384 385 encodingParams: str optional (default None) 386 Parameter to pass to ffmpeg to encode video like video filters. 387 388 Returns 389 ---------- 390 bool 391 Was the creation successfull 392 """ 393 394 # Close if already opened 395 self.close() 396 397 # Set geometry/fps of the video stream from params 398 self.width = int(width) 399 self.height = int(height) 400 self.fps = float(fps) 401 402 # Check params 403 if self.width <= 0 or self.height <= 0 or self.fps <= 0.0: 404 raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps)) 405 406 # Params are ok, set shape and image size 407 self.shape = (self.height,self.width,3) 408 self.imageSize = self.height * self.width * 3 409 410 # Video params are set, open the video 411 cmd = [self.videoProgram] # ffmpeg 412 413 if writeOverExistingFile == True: 414 cmd.extend(['-y']) 415 416 cmd.extend(['-hide_banner', 417 '-nostats', 418 '-loglevel', str(self.logLevel), 419 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value, 420 '-video_size', f"{self.width}x{self.height}", 421 '-r', "{:.3f}".format(self.fps), 422 '-i', '-']) 423 424 if encodingParams is not None: 425 cmd.extend(encodingParams.split()) 426 427 cmd.extend( ['-an', filename ] ) 428 429 if self.debugMode == True: 430 print( ' '.join(cmd), file=sys.stderr ) 431 432 # store filename and set mode 433 self.filename = filename 434 self.mode = PipeMode.WRITE_MODE 435 436 # try call ffmpeg and write frames directly to pipe 437 try: 438 self.pipe = sp.Popen(cmd, stdin=sp.PIPE) 439 self.frame_counter = FrameCounter(self.fps) 440 except Exception as e: 441 # if pipe failed, reinit object and raise exception 442 self.init() 443 raise 444 445 return True 446 447 def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24, 448 decodingParams = None, start_time = 0.0 ) -> bool: 449 """ 450 Method to read video using parametrized access through ffmpeg. Importante note: calling open 451 on a VideoIO will close any former open video. 452 453 Parameters 454 ---------- 455 filename: str or path 456 filename of path to the file (mp4, avi, ...) 457 458 width: int optional (default -1) 459 If defined as a positive value, width of input images will be converted to this value. 460 461 height: int optional (default -1) 462 If defined as a positive value, height of input images will be converted to this value. 463 464 fps: float optional (default -1.0) 465 If defined as a positive value, fps of input video will be converted to this value. 466 467 outputEncoding: PixelFormat optional (default PixelFormat.BGR24) 468 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 469 470 decodingParams: str optional (default None) 471 Parameter to pass to ffmpeg to decode video like filters. 472 473 start_time: float optional (default 0.0) 474 Define the reading start time. If not set, reading at beginning of the video. 475 476 Returns 477 ---------- 478 bool 479 Was the opening successfull 480 """ 481 482 # Close if already opened 483 self.close() 484 485 # Force conversion of parameters 486 width = int(width) 487 height = int(height) 488 fps = float(fps) 489 490 # get parameters from video 491 self.width, self.height, self.fps = self.getVideoParams(filename) 492 493 # check if parameters ask to overide video parameters 494 # TODO: add support for negative value (automatic preservation of aspect ratio) 495 if width > 0: 496 self.width = width 497 if height > 0: 498 self.height = height 499 if fps > 0.0: 500 self.fps = fps 501 502 # Params are ok, set shape and image size 503 self.shape = (self.height,self.width,3) 504 self.imageSize = self.height * self.width * 3 505 506 # Video params are set, open the video 507 cmd = [self.videoProgram, # ffmpeg 508 '-hide_banner', 509 '-nostats', 510 '-loglevel', str(self.logLevel)] 511 512 if start_time < 0.0: 513 pass 514 elif start_time > 0.0: 515 cmd.extend(["-ss", f"{start_time}"]) # set start time if any 516 517 cmd.extend( ['-i', filename] ) 518 519 video_filters = '' # empty 520 if decodingParams is not None: 521 decodingParams = decodingParams.split() 522 # walk over decodingParams for specific params 523 i = 0 524 while i < len(decodingParams): 525 if decodingParams[i] == '-vf': 526 decodingParams.pop(i) # remove '-vf' 527 if i < len(decodingParams): 528 video_filters += ','+decodingParams.pop(i) # remove parameters from list too 529 # to do : add support to other option like -y 530 else: 531 i += 1 532 else: 533 decodingParams = [] 534 535 cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters 536 *(decodingParams), 537 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding 538 '-an', # no audio 539 '-r', f"{self.fps}", 540 '-' # output to stdout 541 ] ) 542 543 if self.debugMode == True: 544 print( ' '.join(cmd) ) 545 546 # store filename and set mode to READ_MODE 547 self.filename = filename 548 self.mode = PipeMode.READ_MODE 549 550 # try to call ffmpeg to get frames directly from pipe 551 try: 552 self.pipe = sp.Popen(cmd, stdout=sp.PIPE) 553 self.frame_counter = FrameCounter(self.fps) 554 if start_time > 0.0: 555 self.frame_counter += start_time # adding with float means adding time 556 except Exception as e: 557 # if pipe failed, reinit object and raise exception 558 self.init() 559 raise 560 561 return True 562 563 def read_frame(self, with_timestamps = False): 564 """ 565 Read next frame from the video 566 567 Parameters 568 ---------- 569 with_timestamps: bool optional (default False) 570 If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s) 571 572 Returns 573 ---------- 574 nparray or FrameContainer 575 An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and 576 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame). 577 """ 578 579 if self.pipe is None: 580 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram)) 581 # - pipe is in write mode 582 if self.mode != PipeMode.READ_MODE: 583 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 584 585 if with_timestamps: 586 # get elapsed time in video, it is time of next frame(s) 587 current_elapsed_time = self.get_elapsed_time() 588 589 # read rgb image from pipe 590 buffer = self.pipe.stdout.read(self.imageSize) 591 if len(buffer) != self.imageSize: 592 # not considered as an error, no more frame, no exception 593 return None 594 595 # get numpy UINT8 array from buffer 596 rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape) 597 598 # increase frame_counter 599 self.frame_counter.frame_count += 1 600 601 # say to gc that this buffer is no longer needed 602 del buffer 603 604 if with_timestamps: 605 return FrameContainer(1,rgbImage,self.fps,current_elapsed_time) 606 607 return rgbImage 608 609 def read_batch(self, number_of_frames, with_timestamps = False) -> Union[nb.array, FrameC]: 610 """ 611 Read next batch of images from the video 612 613 Parameters 614 ---------- 615 number_of_frames: int 616 Number of desired images within the batch. The last batch of the video may have less images. 617 618 with_timestamps: bool optional (default False) 619 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 620 621 Returns 622 ---------- 623 nparray or FrameContainer 624 A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and 625 the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame). 626 """ 627 628 if self.pipe is None: 629 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram)) 630 # - pipe is in write mode 631 if self.mode != PipeMode.READ_MODE: 632 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 633 634 if with_timestamps: 635 # get elapsed time in video, it is time of next frame(s) 636 current_elapsed_time = self.get_elapsed_time() 637 638 # try to read complete batch 639 buffer = self.pipe.stdout.read(self.imageSize*number_of_frames) 640 641 # check if we have at least 1 Frame 642 if len(buffer) < self.imageSize: 643 # not considered as an error, no more frame, no exception 644 return None 645 646 # compute actual number of Frames 647 actualNbFrames = len(buffer)//self.imageSize 648 649 # get and reshape batch from buffer 650 batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3)) 651 652 # increase frame_counter 653 self.frame_counter.frame_count += actualNbFrames 654 655 # say to gc that this buffer is no longer needed 656 del buffer 657 658 if with_timestamps: 659 return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time) 660 661 return batch 662 663 def write_frame(self, image) -> bool: 664 """ 665 Write an image to the video 666 667 Parameters 668 ---------- 669 image: nparray 670 The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called. 671 672 Returns 673 ---------- 674 bool 675 Writing was successful or not. 676 """ 677 678 # Check params 679 # - pipe exists 680 if self.pipe is None: 681 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 682 # - pipe is in write mode 683 if self.mode != PipeMode.WRITE_MODE: 684 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 685 # - shape of image is fine, thus we have pixels for a full compatible frame 686 if image.shape != self.shape: 687 raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape)) 688 # - type of data is UINT8 689 if image.dtype != np.uint8: 690 raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype)) 691 692 # write frame 693 buffer = image.tobytes() 694 if self.pipe.stdin.write( buffer ) < self.imageSize: 695 print( "Error writing frame to" ) 696 return False 697 698 # increase frame_counter 699 self.frame_counter.frame_count += 1 700 701 # say to gc that this buffer is no longer needed 702 del buffer 703 704 return True 705 706 def write_batch(self, batch) -> bool: 707 """ 708 Write a batch of images to the video 709 710 Parameters 711 ---------- 712 batch: nparray 713 A batch of images to write to the video file in the PixelFormat provided when create was called. 714 715 Returns 716 ---------- 717 bool 718 Writing was successful or not. 719 """ 720 721 # Check params 722 # - pipe exists 723 if self.pipe is None: 724 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 725 # - pipe is in write mode 726 if self.mode != PipeMode.WRITE_MODE: 727 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 728 # - shape of images in batch is fine 729 if batch.shape[-3:] != self.shape: 730 raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape)) 731 # - we have the right amount of pixels for the full batch 732 if batch.size != (batch.shape[0]*self.imageSize): 733 raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize)) 734 735 # write frame 736 buffer = batch.tobytes() 737 if self.pipe.stdin.write( buffer ) < batch.size: 738 # say to gc that this buffer is no longer needed 739 del buffer 740 raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename)) 741 742 # increase frame_counter 743 self.frame_counter.frame_count += batch.shape[0] 744 745 # say to gc that this buffer is no longer needed 746 del buffer 747 748 return True 749 750 def iter_frames(self, with_timestamps = False): 751 """ 752 Method to iterate on video frames using VideoIO obj. 753 for frame in obj.iter_frames(): 754 .... 755 756 Parameters 757 ---------- 758 with_timestamps: bool optional (default False) 759 If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames 760 """ 761 762 try: 763 if self.mode == PipeMode.READ_MODE: 764 while self.isOpened(): 765 frame = self.readFrame(with_timestamps) 766 if frame is not None: 767 yield frame 768 finally: 769 self.close() 770 771 def iter_batches(self, batch_size : int, with_timestamps = False): 772 """ 773 Method to iterate on batch of frames using VideoIO obj. 774 for image_batch in obj.iter_batches(): 775 .... 776 777 Parameters 778 ---------- 779 with_timestamps: bool optional (default False) 780 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 781 """ 782 try: 783 if self.mode == PipeMode.READ_MODE: 784 while self.isOpened(): 785 batch = self.readBatch(batch_size, with_timestamps) 786 if batch is not None: 787 yield batch 788 finally: 789 self.close() 790 791 # function aliases to be compliant with original C++ version 792 getVideoTimeInSec = get_time_in_sec 793 getVideoParams = get_params 794 isOpened = is_opened 795 readFrame = read_frame 796 readBatch = read_batch 797 writeFrame = write_frame 798 writeBatch = write_batch 799 get_video_time_in_sec = get_time_in_sec 800 get_video_params = get_params
31class VideoIO: 32 # "static" variables to ffmpeg, ffprobe executables 33 videoProgram, paramProgram = static_ffmpeg.run.get_or_fetch_platform_executables_else_raise() 34 35 class VideoIOException(Exception): 36 """ 37 Dedicated exception class for VideoIO class. 38 """ 39 def __init__(self, message="Error while reading/writing video occurs"): 40 self.message = message 41 super().__init__(self.message) 42 43 class PixelFormat(Enum): 44 """ 45 Enum class for supported input video type: GBR 24 bits or RGB 24 bis. 46 """ 47 GBR24 = 'bgr24' # default format 48 RGB24 = 'rgb24' 49 50 @classmethod 51 def reader(cls, filename, **kwargs): 52 """ 53 Create and open a VideoIO object in reader mode (read a video file) 54 55 See `VideoIO.open` for the full list 56 of accepted parameters. 57 """ 58 reader = cls() 59 reader.open(filename,**kwargs) 60 return reader 61 62 @classmethod 63 def writer(cls, filename, width, height, fps, **kwargs): 64 """ 65 Create and open a VideoIO object in writer mode (write a video file) 66 67 See `VideoIO.create` for the full list 68 of accepted parameters. 69 """ 70 writer = cls() 71 writer.create(filename, width, height, fps, **kwargs) 72 return writer 73 74 # To use with context manager "with VideoIO.reader(...) as f:' for instance 75 def __enter__(self): 76 """ 77 Method call at initialisation of a context manager like "with VideoIO.reader(...) as f:' for instance 78 """ 79 # simply return myself 80 return self 81 82 def __exit__(self, exc_type, exc_val, exc_tb): 83 """ 84 Method call when existing of a context manager like "with VideoIO.reader(...) as f:' for instance 85 """ 86 # close VideoIO 87 self.close() 88 return False 89 90 @staticmethod 91 def get_time_in_sec(filename, *, debug=False, logLevel=16): 92 """ 93 Static method to get length of a video file in seconds including milliseconds as decimal part. 94 95 Parameters 96 ---------- 97 filename : str or path 98 Video file name. 99 100 debug : bool (default False) 101 Show debug info. 102 103 log_level: int (default 16) 104 Log level to pass to the underlying ffmpeg/ffprobe command. 105 106 Returns 107 ---------- 108 float 109 Length in seconds of video file (including milliseconds as decimal part) 110 """ 111 112 cmd = [VideoIO.paramProgram, # ffprobe 113 '-hide_banner', 114 '-loglevel', str(logLevel), 115 '-show_entries', 'format=duration', 116 '-of', 'default=noprint_wrappers=1:nokey=1', 117 filename 118 ] 119 120 if debug == True: 121 print(' '.join(cmd)) 122 123 # call ffprobe and get params in one single line 124 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 125 output = lpipe.stdout.readlines() 126 lpipe.terminate() 127 # transform Bytes output to one single string 128 output = ''.join( [element.decode('utf-8') for element in output]) 129 130 try: 131 return float(output) 132 except (ValueError, TypeError): 133 return None 134 135 @staticmethod 136 def get_params(filename, *, debug=False, logLevel=16): 137 """ 138 Static method to get params (width, height, fps) from a video file. 139 140 Parameters 141 ---------- 142 filename: str or path 143 Video filename. 144 145 debug: bool (default (False) 146 Show debug info. 147 148 log_level: int (default 16) 149 Log level to pass to the underlying ffmpeg/ffprobe command. 150 151 Returns 152 ---------- 153 tuple 154 Tuple containing (width, height, fps) of the video 155 """ 156 cmd = [VideoIO.paramProgram, # ffprobe 157 '-hide_banner', 158 '-loglevel', str(logLevel), 159 '-show_entries', 'stream=width,height,r_frame_rate', 160 filename 161 ] 162 163 if debug == True: 164 print(' '.join(cmd)) 165 166 # call ffprobe and get params in one single line 167 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 168 output = lpipe.stdout.readlines() 169 lpipe.terminate() 170 # transform Bytes output to one single string 171 output = ''.join( [element.decode('utf-8') for element in output]) 172 173 pattern_width = r'width=(\d+)' 174 pattern_height = r'height=(\d+)' 175 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 176 177 # Search for values in the ffprobe output 178 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 179 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 180 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 181 182 # Extraction des valeurs 183 if match_width: 184 width = int(match_width.group(1)) 185 else: 186 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 187 188 if match_height: 189 height = int(match_height.group(1)) 190 else: 191 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 192 193 if match_fps: 194 numerator = float(match_fps.group(1)) 195 denominator = float(match_fps.group(2)) 196 fps = numerator / denominator 197 else: 198 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 199 200 return (width, height, fps) 201 202 # Attributes 203 mode: PipeMode 204 """ Pipemode of the current object (default PipeMode.UNK_MODE)""" 205 206 loglevel: int 207 """ loglevel of the underlying ffmpeg backend for this object (default 16)""" 208 209 debugModel: bool 210 """ debutMode flag for this object (print debut info, default False)""" 211 212 width: int 213 """ width of images (default -1) """ 214 215 height: int 216 """ height of images (default -1) """ 217 218 fps: float 219 """ fps of video (default -1.0) """ 220 221 pipe: sp.Popen 222 """ pipe object to ffmpeg/ffprobe (default None)""" 223 224 shape: tuple 225 """ Shape of images (default (None, None, None))""" 226 227 imageSize: int 228 """ Weight in bytes of one image (default -1)""" 229 230 filename: str 231 """ Filename of the video file (default None)""" 232 233 frame_counter: FrameCounter 234 """ `Framecounter` object to count ellapsed time (default None)""" 235 236 def __init__(self, *, logLevel = 16, debugMode = False): 237 """ 238 Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode 239 240 Parameters 241 ---------- 242 log_level: int (default 16) 243 Log level to pass to the underlying ffmpeg/ffprobe command. 244 245 debugMode: bool (default (False) 246 Show debug info. while processing video 247 """ 248 249 self.mode = PipeMode.UNK_MODE 250 self.logLevel = logLevel 251 self.debugMode = debugMode 252 253 # Call init() method 254 self.init() 255 256 def init(self): 257 """ 258 Init or reinit a VideoIO object. 259 """ 260 self.width = -1 261 self.height = -1 262 self.fps = -1.0 263 self.pipe = None 264 self.shape = (None, None, None) 265 self.imageSize = -1 266 self.filename = None 267 self.frame_counter = None 268 269 _repr_exclude = {"pipe"} 270 """ List of excluded attribute for string conversion. """ 271 272 # converting the object to a string representation 273 def __repr__(self): 274 """ 275 Convert object (excluding attributes in _repr_exclude) to string representation. 276 """ 277 attrs = ", ".join( 278 f"{k}={v!r}" 279 for k, v in self.__dict__.items() 280 if k not in self._repr_exclude 281 ) 282 return f"{self.__class__.__name__}({attrs})" 283 284 __str__ = __repr__ 285 """ String representation """ 286 287 def get_elapsed_time_as_str(self) -> str: 288 """ 289 Method to get elapsed time (float value) as str from `frame_counter` attribute. 290 291 Returns 292 ---------- 293 str or None 294 Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds 295 None if no frame counter are available. 296 """ 297 if self.frame_counter is None: 298 return None 299 return self.frame_counter.get_elapsed_time_as_str() 300 301 def get_formated_elapsed_time_as_str(self,show_ms=True) -> str: 302 """ 303 Method to get elapsed time (hour format) as str from `frame_counter` attribute. 304 305 Returns 306 ---------- 307 str or None 308 Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds 309 None if no frame counter are available. 310 """ 311 if self.frame_counter is None: 312 return None 313 return self.frame_counter.get_formated_elapsed_time_as_str() 314 315 def get_elapsed_time(self) -> float: 316 """ 317 Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute. 318 319 Returns 320 ---------- 321 float or None 322 Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds 323 None if no frame counter are available. 324 """ 325 if self.frame_counter is None: 326 return None 327 return self.frame_counter.get_elapsed_time() 328 329 def is_opened(self) -> bool: 330 """ 331 Method to get status of the underlying pipe to ffmpeg. 332 333 Returns 334 ---------- 335 bool 336 True if pipe is opened (reading or writing mode), False if not. 337 """ 338 # is the pipe opened? 339 if self.pipe is not None and self.pipe.poll() is None: 340 return True 341 342 return False 343 344 def close(self) -> None: 345 """ 346 Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods. 347 """ 348 if self.pipe is not None: 349 if self.mode == PipeMode.WRITE_MODE: 350 # killing will make ffmpeg not finish properly the job, close the pipe 351 # to let it know that no more data are comming 352 self.pipe.stdin.close() 353 else: # self.mode == PipeMode.READ_MODE 354 # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows 355 self.pipe.kill() 356 357 # wait for subprocess to end 358 self.pipe.wait() 359 360 # reinit object for later use 361 self.init() 362 363 def create(self, width, height, fps, *, writeOverExistingFile = False, 364 inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool: 365 """ 366 Method to create a video using parametrized access through ffmpeg. Importante note: calling create 367 on a VideoIO will close any former open video. 368 369 Parameters 370 ---------- 371 filename: str or path 372 filename of path to the file (mp4, avi, ...) 373 374 width: int 375 If defined as a positive value, width of output images will be set to this value. 376 377 height: int 378 If defined as a positive value, height of output images will be set to this value. 379 380 fps: 381 If defined as a positive value, fps of output video will be set to this value. 382 383 inputEncoding: PixelFormat optional (default PixelFormat.BGR24) 384 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 385 386 encodingParams: str optional (default None) 387 Parameter to pass to ffmpeg to encode video like video filters. 388 389 Returns 390 ---------- 391 bool 392 Was the creation successfull 393 """ 394 395 # Close if already opened 396 self.close() 397 398 # Set geometry/fps of the video stream from params 399 self.width = int(width) 400 self.height = int(height) 401 self.fps = float(fps) 402 403 # Check params 404 if self.width <= 0 or self.height <= 0 or self.fps <= 0.0: 405 raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps)) 406 407 # Params are ok, set shape and image size 408 self.shape = (self.height,self.width,3) 409 self.imageSize = self.height * self.width * 3 410 411 # Video params are set, open the video 412 cmd = [self.videoProgram] # ffmpeg 413 414 if writeOverExistingFile == True: 415 cmd.extend(['-y']) 416 417 cmd.extend(['-hide_banner', 418 '-nostats', 419 '-loglevel', str(self.logLevel), 420 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value, 421 '-video_size', f"{self.width}x{self.height}", 422 '-r', "{:.3f}".format(self.fps), 423 '-i', '-']) 424 425 if encodingParams is not None: 426 cmd.extend(encodingParams.split()) 427 428 cmd.extend( ['-an', filename ] ) 429 430 if self.debugMode == True: 431 print( ' '.join(cmd), file=sys.stderr ) 432 433 # store filename and set mode 434 self.filename = filename 435 self.mode = PipeMode.WRITE_MODE 436 437 # try call ffmpeg and write frames directly to pipe 438 try: 439 self.pipe = sp.Popen(cmd, stdin=sp.PIPE) 440 self.frame_counter = FrameCounter(self.fps) 441 except Exception as e: 442 # if pipe failed, reinit object and raise exception 443 self.init() 444 raise 445 446 return True 447 448 def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24, 449 decodingParams = None, start_time = 0.0 ) -> bool: 450 """ 451 Method to read video using parametrized access through ffmpeg. Importante note: calling open 452 on a VideoIO will close any former open video. 453 454 Parameters 455 ---------- 456 filename: str or path 457 filename of path to the file (mp4, avi, ...) 458 459 width: int optional (default -1) 460 If defined as a positive value, width of input images will be converted to this value. 461 462 height: int optional (default -1) 463 If defined as a positive value, height of input images will be converted to this value. 464 465 fps: float optional (default -1.0) 466 If defined as a positive value, fps of input video will be converted to this value. 467 468 outputEncoding: PixelFormat optional (default PixelFormat.BGR24) 469 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 470 471 decodingParams: str optional (default None) 472 Parameter to pass to ffmpeg to decode video like filters. 473 474 start_time: float optional (default 0.0) 475 Define the reading start time. If not set, reading at beginning of the video. 476 477 Returns 478 ---------- 479 bool 480 Was the opening successfull 481 """ 482 483 # Close if already opened 484 self.close() 485 486 # Force conversion of parameters 487 width = int(width) 488 height = int(height) 489 fps = float(fps) 490 491 # get parameters from video 492 self.width, self.height, self.fps = self.getVideoParams(filename) 493 494 # check if parameters ask to overide video parameters 495 # TODO: add support for negative value (automatic preservation of aspect ratio) 496 if width > 0: 497 self.width = width 498 if height > 0: 499 self.height = height 500 if fps > 0.0: 501 self.fps = fps 502 503 # Params are ok, set shape and image size 504 self.shape = (self.height,self.width,3) 505 self.imageSize = self.height * self.width * 3 506 507 # Video params are set, open the video 508 cmd = [self.videoProgram, # ffmpeg 509 '-hide_banner', 510 '-nostats', 511 '-loglevel', str(self.logLevel)] 512 513 if start_time < 0.0: 514 pass 515 elif start_time > 0.0: 516 cmd.extend(["-ss", f"{start_time}"]) # set start time if any 517 518 cmd.extend( ['-i', filename] ) 519 520 video_filters = '' # empty 521 if decodingParams is not None: 522 decodingParams = decodingParams.split() 523 # walk over decodingParams for specific params 524 i = 0 525 while i < len(decodingParams): 526 if decodingParams[i] == '-vf': 527 decodingParams.pop(i) # remove '-vf' 528 if i < len(decodingParams): 529 video_filters += ','+decodingParams.pop(i) # remove parameters from list too 530 # to do : add support to other option like -y 531 else: 532 i += 1 533 else: 534 decodingParams = [] 535 536 cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters 537 *(decodingParams), 538 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding 539 '-an', # no audio 540 '-r', f"{self.fps}", 541 '-' # output to stdout 542 ] ) 543 544 if self.debugMode == True: 545 print( ' '.join(cmd) ) 546 547 # store filename and set mode to READ_MODE 548 self.filename = filename 549 self.mode = PipeMode.READ_MODE 550 551 # try to call ffmpeg to get frames directly from pipe 552 try: 553 self.pipe = sp.Popen(cmd, stdout=sp.PIPE) 554 self.frame_counter = FrameCounter(self.fps) 555 if start_time > 0.0: 556 self.frame_counter += start_time # adding with float means adding time 557 except Exception as e: 558 # if pipe failed, reinit object and raise exception 559 self.init() 560 raise 561 562 return True 563 564 def read_frame(self, with_timestamps = False): 565 """ 566 Read next frame from the video 567 568 Parameters 569 ---------- 570 with_timestamps: bool optional (default False) 571 If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s) 572 573 Returns 574 ---------- 575 nparray or FrameContainer 576 An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and 577 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame). 578 """ 579 580 if self.pipe is None: 581 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram)) 582 # - pipe is in write mode 583 if self.mode != PipeMode.READ_MODE: 584 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 585 586 if with_timestamps: 587 # get elapsed time in video, it is time of next frame(s) 588 current_elapsed_time = self.get_elapsed_time() 589 590 # read rgb image from pipe 591 buffer = self.pipe.stdout.read(self.imageSize) 592 if len(buffer) != self.imageSize: 593 # not considered as an error, no more frame, no exception 594 return None 595 596 # get numpy UINT8 array from buffer 597 rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape) 598 599 # increase frame_counter 600 self.frame_counter.frame_count += 1 601 602 # say to gc that this buffer is no longer needed 603 del buffer 604 605 if with_timestamps: 606 return FrameContainer(1,rgbImage,self.fps,current_elapsed_time) 607 608 return rgbImage 609 610 def read_batch(self, number_of_frames, with_timestamps = False) -> Union[nb.array, FrameC]: 611 """ 612 Read next batch of images from the video 613 614 Parameters 615 ---------- 616 number_of_frames: int 617 Number of desired images within the batch. The last batch of the video may have less images. 618 619 with_timestamps: bool optional (default False) 620 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 621 622 Returns 623 ---------- 624 nparray or FrameContainer 625 A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and 626 the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame). 627 """ 628 629 if self.pipe is None: 630 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram)) 631 # - pipe is in write mode 632 if self.mode != PipeMode.READ_MODE: 633 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 634 635 if with_timestamps: 636 # get elapsed time in video, it is time of next frame(s) 637 current_elapsed_time = self.get_elapsed_time() 638 639 # try to read complete batch 640 buffer = self.pipe.stdout.read(self.imageSize*number_of_frames) 641 642 # check if we have at least 1 Frame 643 if len(buffer) < self.imageSize: 644 # not considered as an error, no more frame, no exception 645 return None 646 647 # compute actual number of Frames 648 actualNbFrames = len(buffer)//self.imageSize 649 650 # get and reshape batch from buffer 651 batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3)) 652 653 # increase frame_counter 654 self.frame_counter.frame_count += actualNbFrames 655 656 # say to gc that this buffer is no longer needed 657 del buffer 658 659 if with_timestamps: 660 return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time) 661 662 return batch 663 664 def write_frame(self, image) -> bool: 665 """ 666 Write an image to the video 667 668 Parameters 669 ---------- 670 image: nparray 671 The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called. 672 673 Returns 674 ---------- 675 bool 676 Writing was successful or not. 677 """ 678 679 # Check params 680 # - pipe exists 681 if self.pipe is None: 682 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 683 # - pipe is in write mode 684 if self.mode != PipeMode.WRITE_MODE: 685 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 686 # - shape of image is fine, thus we have pixels for a full compatible frame 687 if image.shape != self.shape: 688 raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape)) 689 # - type of data is UINT8 690 if image.dtype != np.uint8: 691 raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype)) 692 693 # write frame 694 buffer = image.tobytes() 695 if self.pipe.stdin.write( buffer ) < self.imageSize: 696 print( "Error writing frame to" ) 697 return False 698 699 # increase frame_counter 700 self.frame_counter.frame_count += 1 701 702 # say to gc that this buffer is no longer needed 703 del buffer 704 705 return True 706 707 def write_batch(self, batch) -> bool: 708 """ 709 Write a batch of images to the video 710 711 Parameters 712 ---------- 713 batch: nparray 714 A batch of images to write to the video file in the PixelFormat provided when create was called. 715 716 Returns 717 ---------- 718 bool 719 Writing was successful or not. 720 """ 721 722 # Check params 723 # - pipe exists 724 if self.pipe is None: 725 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 726 # - pipe is in write mode 727 if self.mode != PipeMode.WRITE_MODE: 728 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 729 # - shape of images in batch is fine 730 if batch.shape[-3:] != self.shape: 731 raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape)) 732 # - we have the right amount of pixels for the full batch 733 if batch.size != (batch.shape[0]*self.imageSize): 734 raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize)) 735 736 # write frame 737 buffer = batch.tobytes() 738 if self.pipe.stdin.write( buffer ) < batch.size: 739 # say to gc that this buffer is no longer needed 740 del buffer 741 raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename)) 742 743 # increase frame_counter 744 self.frame_counter.frame_count += batch.shape[0] 745 746 # say to gc that this buffer is no longer needed 747 del buffer 748 749 return True 750 751 def iter_frames(self, with_timestamps = False): 752 """ 753 Method to iterate on video frames using VideoIO obj. 754 for frame in obj.iter_frames(): 755 .... 756 757 Parameters 758 ---------- 759 with_timestamps: bool optional (default False) 760 If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames 761 """ 762 763 try: 764 if self.mode == PipeMode.READ_MODE: 765 while self.isOpened(): 766 frame = self.readFrame(with_timestamps) 767 if frame is not None: 768 yield frame 769 finally: 770 self.close() 771 772 def iter_batches(self, batch_size : int, with_timestamps = False): 773 """ 774 Method to iterate on batch of frames using VideoIO obj. 775 for image_batch in obj.iter_batches(): 776 .... 777 778 Parameters 779 ---------- 780 with_timestamps: bool optional (default False) 781 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 782 """ 783 try: 784 if self.mode == PipeMode.READ_MODE: 785 while self.isOpened(): 786 batch = self.readBatch(batch_size, with_timestamps) 787 if batch is not None: 788 yield batch 789 finally: 790 self.close() 791 792 # function aliases to be compliant with original C++ version 793 getVideoTimeInSec = get_time_in_sec 794 getVideoParams = get_params 795 isOpened = is_opened 796 readFrame = read_frame 797 readBatch = read_batch 798 writeFrame = write_frame 799 writeBatch = write_batch 800 get_video_time_in_sec = get_time_in_sec 801 get_video_params = get_params
236 def __init__(self, *, logLevel = 16, debugMode = False): 237 """ 238 Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode 239 240 Parameters 241 ---------- 242 log_level: int (default 16) 243 Log level to pass to the underlying ffmpeg/ffprobe command. 244 245 debugMode: bool (default (False) 246 Show debug info. while processing video 247 """ 248 249 self.mode = PipeMode.UNK_MODE 250 self.logLevel = logLevel 251 self.debugMode = debugMode 252 253 # Call init() method 254 self.init()
Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
Parameters
log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.
debugMode: bool (default (False) Show debug info. while processing video
50 @classmethod 51 def reader(cls, filename, **kwargs): 52 """ 53 Create and open a VideoIO object in reader mode (read a video file) 54 55 See `VideoIO.open` for the full list 56 of accepted parameters. 57 """ 58 reader = cls() 59 reader.open(filename,**kwargs) 60 return reader
Create and open a VideoIO object in reader mode (read a video file)
See VideoIO.open for the full list
of accepted parameters.
62 @classmethod 63 def writer(cls, filename, width, height, fps, **kwargs): 64 """ 65 Create and open a VideoIO object in writer mode (write a video file) 66 67 See `VideoIO.create` for the full list 68 of accepted parameters. 69 """ 70 writer = cls() 71 writer.create(filename, width, height, fps, **kwargs) 72 return writer
Create and open a VideoIO object in writer mode (write a video file)
See VideoIO.create for the full list
of accepted parameters.
90 @staticmethod 91 def get_time_in_sec(filename, *, debug=False, logLevel=16): 92 """ 93 Static method to get length of a video file in seconds including milliseconds as decimal part. 94 95 Parameters 96 ---------- 97 filename : str or path 98 Video file name. 99 100 debug : bool (default False) 101 Show debug info. 102 103 log_level: int (default 16) 104 Log level to pass to the underlying ffmpeg/ffprobe command. 105 106 Returns 107 ---------- 108 float 109 Length in seconds of video file (including milliseconds as decimal part) 110 """ 111 112 cmd = [VideoIO.paramProgram, # ffprobe 113 '-hide_banner', 114 '-loglevel', str(logLevel), 115 '-show_entries', 'format=duration', 116 '-of', 'default=noprint_wrappers=1:nokey=1', 117 filename 118 ] 119 120 if debug == True: 121 print(' '.join(cmd)) 122 123 # call ffprobe and get params in one single line 124 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 125 output = lpipe.stdout.readlines() 126 lpipe.terminate() 127 # transform Bytes output to one single string 128 output = ''.join( [element.decode('utf-8') for element in output]) 129 130 try: 131 return float(output) 132 except (ValueError, TypeError): 133 return None
Static method to get length of a video file in seconds including milliseconds as decimal part.
Parameters
filename : str or path Video file name.
debug : bool (default False) Show debug info.
log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.
Returns
float Length in seconds of video file (including milliseconds as decimal part)
135 @staticmethod 136 def get_params(filename, *, debug=False, logLevel=16): 137 """ 138 Static method to get params (width, height, fps) from a video file. 139 140 Parameters 141 ---------- 142 filename: str or path 143 Video filename. 144 145 debug: bool (default (False) 146 Show debug info. 147 148 log_level: int (default 16) 149 Log level to pass to the underlying ffmpeg/ffprobe command. 150 151 Returns 152 ---------- 153 tuple 154 Tuple containing (width, height, fps) of the video 155 """ 156 cmd = [VideoIO.paramProgram, # ffprobe 157 '-hide_banner', 158 '-loglevel', str(logLevel), 159 '-show_entries', 'stream=width,height,r_frame_rate', 160 filename 161 ] 162 163 if debug == True: 164 print(' '.join(cmd)) 165 166 # call ffprobe and get params in one single line 167 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 168 output = lpipe.stdout.readlines() 169 lpipe.terminate() 170 # transform Bytes output to one single string 171 output = ''.join( [element.decode('utf-8') for element in output]) 172 173 pattern_width = r'width=(\d+)' 174 pattern_height = r'height=(\d+)' 175 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 176 177 # Search for values in the ffprobe output 178 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 179 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 180 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 181 182 # Extraction des valeurs 183 if match_width: 184 width = int(match_width.group(1)) 185 else: 186 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 187 188 if match_height: 189 height = int(match_height.group(1)) 190 else: 191 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 192 193 if match_fps: 194 numerator = float(match_fps.group(1)) 195 denominator = float(match_fps.group(2)) 196 fps = numerator / denominator 197 else: 198 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 199 200 return (width, height, fps)
Static method to get params (width, height, fps) from a video file.
Parameters
filename: str or path Video filename.
debug: bool (default (False) Show debug info.
log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.
Returns
tuple Tuple containing (width, height, fps) of the video
Pipemode of the current object (default PipeMode.UNK_MODE)
Framecounter object to count ellapsed time (default None)
256 def init(self): 257 """ 258 Init or reinit a VideoIO object. 259 """ 260 self.width = -1 261 self.height = -1 262 self.fps = -1.0 263 self.pipe = None 264 self.shape = (None, None, None) 265 self.imageSize = -1 266 self.filename = None 267 self.frame_counter = None
Init or reinit a VideoIO object.
287 def get_elapsed_time_as_str(self) -> str: 288 """ 289 Method to get elapsed time (float value) as str from `frame_counter` attribute. 290 291 Returns 292 ---------- 293 str or None 294 Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds 295 None if no frame counter are available. 296 """ 297 if self.frame_counter is None: 298 return None 299 return self.frame_counter.get_elapsed_time_as_str()
Method to get elapsed time (float value) as str from frame_counter attribute.
Returns
str or None Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.
301 def get_formated_elapsed_time_as_str(self,show_ms=True) -> str: 302 """ 303 Method to get elapsed time (hour format) as str from `frame_counter` attribute. 304 305 Returns 306 ---------- 307 str or None 308 Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds 309 None if no frame counter are available. 310 """ 311 if self.frame_counter is None: 312 return None 313 return self.frame_counter.get_formated_elapsed_time_as_str()
Method to get elapsed time (hour format) as str from frame_counter attribute.
Returns
str or None Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds None if no frame counter are available.
315 def get_elapsed_time(self) -> float: 316 """ 317 Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute. 318 319 Returns 320 ---------- 321 float or None 322 Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds 323 None if no frame counter are available. 324 """ 325 if self.frame_counter is None: 326 return None 327 return self.frame_counter.get_elapsed_time()
Method to get elapsed time as float value rounded to 3 decimals (millisecond) from frame_counter attribute.
Returns
float or None Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds None if no frame counter are available.
329 def is_opened(self) -> bool: 330 """ 331 Method to get status of the underlying pipe to ffmpeg. 332 333 Returns 334 ---------- 335 bool 336 True if pipe is opened (reading or writing mode), False if not. 337 """ 338 # is the pipe opened? 339 if self.pipe is not None and self.pipe.poll() is None: 340 return True 341 342 return False
Method to get status of the underlying pipe to ffmpeg.
Returns
bool True if pipe is opened (reading or writing mode), False if not.
344 def close(self) -> None: 345 """ 346 Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods. 347 """ 348 if self.pipe is not None: 349 if self.mode == PipeMode.WRITE_MODE: 350 # killing will make ffmpeg not finish properly the job, close the pipe 351 # to let it know that no more data are comming 352 self.pipe.stdin.close() 353 else: # self.mode == PipeMode.READ_MODE 354 # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows 355 self.pipe.kill() 356 357 # wait for subprocess to end 358 self.pipe.wait() 359 360 # reinit object for later use 361 self.init()
Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods.
363 def create(self, width, height, fps, *, writeOverExistingFile = False, 364 inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool: 365 """ 366 Method to create a video using parametrized access through ffmpeg. Importante note: calling create 367 on a VideoIO will close any former open video. 368 369 Parameters 370 ---------- 371 filename: str or path 372 filename of path to the file (mp4, avi, ...) 373 374 width: int 375 If defined as a positive value, width of output images will be set to this value. 376 377 height: int 378 If defined as a positive value, height of output images will be set to this value. 379 380 fps: 381 If defined as a positive value, fps of output video will be set to this value. 382 383 inputEncoding: PixelFormat optional (default PixelFormat.BGR24) 384 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 385 386 encodingParams: str optional (default None) 387 Parameter to pass to ffmpeg to encode video like video filters. 388 389 Returns 390 ---------- 391 bool 392 Was the creation successfull 393 """ 394 395 # Close if already opened 396 self.close() 397 398 # Set geometry/fps of the video stream from params 399 self.width = int(width) 400 self.height = int(height) 401 self.fps = float(fps) 402 403 # Check params 404 if self.width <= 0 or self.height <= 0 or self.fps <= 0.0: 405 raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps)) 406 407 # Params are ok, set shape and image size 408 self.shape = (self.height,self.width,3) 409 self.imageSize = self.height * self.width * 3 410 411 # Video params are set, open the video 412 cmd = [self.videoProgram] # ffmpeg 413 414 if writeOverExistingFile == True: 415 cmd.extend(['-y']) 416 417 cmd.extend(['-hide_banner', 418 '-nostats', 419 '-loglevel', str(self.logLevel), 420 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value, 421 '-video_size', f"{self.width}x{self.height}", 422 '-r', "{:.3f}".format(self.fps), 423 '-i', '-']) 424 425 if encodingParams is not None: 426 cmd.extend(encodingParams.split()) 427 428 cmd.extend( ['-an', filename ] ) 429 430 if self.debugMode == True: 431 print( ' '.join(cmd), file=sys.stderr ) 432 433 # store filename and set mode 434 self.filename = filename 435 self.mode = PipeMode.WRITE_MODE 436 437 # try call ffmpeg and write frames directly to pipe 438 try: 439 self.pipe = sp.Popen(cmd, stdin=sp.PIPE) 440 self.frame_counter = FrameCounter(self.fps) 441 except Exception as e: 442 # if pipe failed, reinit object and raise exception 443 self.init() 444 raise 445 446 return True
Method to create a video using parametrized access through ffmpeg. Importante note: calling create on a VideoIO will close any former open video.
Parameters
filename: str or path filename of path to the file (mp4, avi, ...)
width: int If defined as a positive value, width of output images will be set to this value.
height: int If defined as a positive value, height of output images will be set to this value.
fps: If defined as a positive value, fps of output video will be set to this value.
inputEncoding: PixelFormat optional (default PixelFormat.BGR24) Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
encodingParams: str optional (default None) Parameter to pass to ffmpeg to encode video like video filters.
Returns
bool Was the creation successfull
448 def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24, 449 decodingParams = None, start_time = 0.0 ) -> bool: 450 """ 451 Method to read video using parametrized access through ffmpeg. Importante note: calling open 452 on a VideoIO will close any former open video. 453 454 Parameters 455 ---------- 456 filename: str or path 457 filename of path to the file (mp4, avi, ...) 458 459 width: int optional (default -1) 460 If defined as a positive value, width of input images will be converted to this value. 461 462 height: int optional (default -1) 463 If defined as a positive value, height of input images will be converted to this value. 464 465 fps: float optional (default -1.0) 466 If defined as a positive value, fps of input video will be converted to this value. 467 468 outputEncoding: PixelFormat optional (default PixelFormat.BGR24) 469 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 470 471 decodingParams: str optional (default None) 472 Parameter to pass to ffmpeg to decode video like filters. 473 474 start_time: float optional (default 0.0) 475 Define the reading start time. If not set, reading at beginning of the video. 476 477 Returns 478 ---------- 479 bool 480 Was the opening successfull 481 """ 482 483 # Close if already opened 484 self.close() 485 486 # Force conversion of parameters 487 width = int(width) 488 height = int(height) 489 fps = float(fps) 490 491 # get parameters from video 492 self.width, self.height, self.fps = self.getVideoParams(filename) 493 494 # check if parameters ask to overide video parameters 495 # TODO: add support for negative value (automatic preservation of aspect ratio) 496 if width > 0: 497 self.width = width 498 if height > 0: 499 self.height = height 500 if fps > 0.0: 501 self.fps = fps 502 503 # Params are ok, set shape and image size 504 self.shape = (self.height,self.width,3) 505 self.imageSize = self.height * self.width * 3 506 507 # Video params are set, open the video 508 cmd = [self.videoProgram, # ffmpeg 509 '-hide_banner', 510 '-nostats', 511 '-loglevel', str(self.logLevel)] 512 513 if start_time < 0.0: 514 pass 515 elif start_time > 0.0: 516 cmd.extend(["-ss", f"{start_time}"]) # set start time if any 517 518 cmd.extend( ['-i', filename] ) 519 520 video_filters = '' # empty 521 if decodingParams is not None: 522 decodingParams = decodingParams.split() 523 # walk over decodingParams for specific params 524 i = 0 525 while i < len(decodingParams): 526 if decodingParams[i] == '-vf': 527 decodingParams.pop(i) # remove '-vf' 528 if i < len(decodingParams): 529 video_filters += ','+decodingParams.pop(i) # remove parameters from list too 530 # to do : add support to other option like -y 531 else: 532 i += 1 533 else: 534 decodingParams = [] 535 536 cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters 537 *(decodingParams), 538 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding 539 '-an', # no audio 540 '-r', f"{self.fps}", 541 '-' # output to stdout 542 ] ) 543 544 if self.debugMode == True: 545 print( ' '.join(cmd) ) 546 547 # store filename and set mode to READ_MODE 548 self.filename = filename 549 self.mode = PipeMode.READ_MODE 550 551 # try to call ffmpeg to get frames directly from pipe 552 try: 553 self.pipe = sp.Popen(cmd, stdout=sp.PIPE) 554 self.frame_counter = FrameCounter(self.fps) 555 if start_time > 0.0: 556 self.frame_counter += start_time # adding with float means adding time 557 except Exception as e: 558 # if pipe failed, reinit object and raise exception 559 self.init() 560 raise 561 562 return True
Method to read video using parametrized access through ffmpeg. Importante note: calling open on a VideoIO will close any former open video.
Parameters
filename: str or path filename of path to the file (mp4, avi, ...)
width: int optional (default -1) If defined as a positive value, width of input images will be converted to this value.
height: int optional (default -1) If defined as a positive value, height of input images will be converted to this value.
fps: float optional (default -1.0) If defined as a positive value, fps of input video will be converted to this value.
outputEncoding: PixelFormat optional (default PixelFormat.BGR24) Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24.
decodingParams: str optional (default None) Parameter to pass to ffmpeg to decode video like filters.
start_time: float optional (default 0.0) Define the reading start time. If not set, reading at beginning of the video.
Returns
bool Was the opening successfull
564 def read_frame(self, with_timestamps = False): 565 """ 566 Read next frame from the video 567 568 Parameters 569 ---------- 570 with_timestamps: bool optional (default False) 571 If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s) 572 573 Returns 574 ---------- 575 nparray or FrameContainer 576 An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and 577 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame). 578 """ 579 580 if self.pipe is None: 581 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram)) 582 # - pipe is in write mode 583 if self.mode != PipeMode.READ_MODE: 584 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 585 586 if with_timestamps: 587 # get elapsed time in video, it is time of next frame(s) 588 current_elapsed_time = self.get_elapsed_time() 589 590 # read rgb image from pipe 591 buffer = self.pipe.stdout.read(self.imageSize) 592 if len(buffer) != self.imageSize: 593 # not considered as an error, no more frame, no exception 594 return None 595 596 # get numpy UINT8 array from buffer 597 rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape) 598 599 # increase frame_counter 600 self.frame_counter.frame_count += 1 601 602 # say to gc that this buffer is no longer needed 603 del buffer 604 605 if with_timestamps: 606 return FrameContainer(1,rgbImage,self.fps,current_elapsed_time) 607 608 return rgbImage
Read next frame from the video
Parameters
with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
Returns
nparray or FrameContainer
An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in FrameContainer.data and
the associated timestamp in FrameContainer.timestamps as an array (one element for one frame).
610 def read_batch(self, number_of_frames, with_timestamps = False) -> Union[nb.array, FrameC]: 611 """ 612 Read next batch of images from the video 613 614 Parameters 615 ---------- 616 number_of_frames: int 617 Number of desired images within the batch. The last batch of the video may have less images. 618 619 with_timestamps: bool optional (default False) 620 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 621 622 Returns 623 ---------- 624 nparray or FrameContainer 625 A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and 626 the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame). 627 """ 628 629 if self.pipe is None: 630 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram)) 631 # - pipe is in write mode 632 if self.mode != PipeMode.READ_MODE: 633 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 634 635 if with_timestamps: 636 # get elapsed time in video, it is time of next frame(s) 637 current_elapsed_time = self.get_elapsed_time() 638 639 # try to read complete batch 640 buffer = self.pipe.stdout.read(self.imageSize*number_of_frames) 641 642 # check if we have at least 1 Frame 643 if len(buffer) < self.imageSize: 644 # not considered as an error, no more frame, no exception 645 return None 646 647 # compute actual number of Frames 648 actualNbFrames = len(buffer)//self.imageSize 649 650 # get and reshape batch from buffer 651 batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3)) 652 653 # increase frame_counter 654 self.frame_counter.frame_count += actualNbFrames 655 656 # say to gc that this buffer is no longer needed 657 del buffer 658 659 if with_timestamps: 660 return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time) 661 662 return batch
Read next batch of images from the video
Parameters
number_of_frames: int Number of desired images within the batch. The last batch of the video may have less images.
with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
Returns
nparray or FrameContainer
A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in FrameContainer.data and
the associated timestamps in FrameContainer.timestamps as an array (one element for each frame).
664 def write_frame(self, image) -> bool: 665 """ 666 Write an image to the video 667 668 Parameters 669 ---------- 670 image: nparray 671 The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called. 672 673 Returns 674 ---------- 675 bool 676 Writing was successful or not. 677 """ 678 679 # Check params 680 # - pipe exists 681 if self.pipe is None: 682 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 683 # - pipe is in write mode 684 if self.mode != PipeMode.WRITE_MODE: 685 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 686 # - shape of image is fine, thus we have pixels for a full compatible frame 687 if image.shape != self.shape: 688 raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape)) 689 # - type of data is UINT8 690 if image.dtype != np.uint8: 691 raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype)) 692 693 # write frame 694 buffer = image.tobytes() 695 if self.pipe.stdin.write( buffer ) < self.imageSize: 696 print( "Error writing frame to" ) 697 return False 698 699 # increase frame_counter 700 self.frame_counter.frame_count += 1 701 702 # say to gc that this buffer is no longer needed 703 del buffer 704 705 return True
Write an image to the video
Parameters
image: nparray The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
Returns
bool Writing was successful or not.
707 def write_batch(self, batch) -> bool: 708 """ 709 Write a batch of images to the video 710 711 Parameters 712 ---------- 713 batch: nparray 714 A batch of images to write to the video file in the PixelFormat provided when create was called. 715 716 Returns 717 ---------- 718 bool 719 Writing was successful or not. 720 """ 721 722 # Check params 723 # - pipe exists 724 if self.pipe is None: 725 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 726 # - pipe is in write mode 727 if self.mode != PipeMode.WRITE_MODE: 728 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 729 # - shape of images in batch is fine 730 if batch.shape[-3:] != self.shape: 731 raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape)) 732 # - we have the right amount of pixels for the full batch 733 if batch.size != (batch.shape[0]*self.imageSize): 734 raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize)) 735 736 # write frame 737 buffer = batch.tobytes() 738 if self.pipe.stdin.write( buffer ) < batch.size: 739 # say to gc that this buffer is no longer needed 740 del buffer 741 raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename)) 742 743 # increase frame_counter 744 self.frame_counter.frame_count += batch.shape[0] 745 746 # say to gc that this buffer is no longer needed 747 del buffer 748 749 return True
Write a batch of images to the video
Parameters
batch: nparray A batch of images to write to the video file in the PixelFormat provided when create was called.
Returns
bool Writing was successful or not.
751 def iter_frames(self, with_timestamps = False): 752 """ 753 Method to iterate on video frames using VideoIO obj. 754 for frame in obj.iter_frames(): 755 .... 756 757 Parameters 758 ---------- 759 with_timestamps: bool optional (default False) 760 If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames 761 """ 762 763 try: 764 if self.mode == PipeMode.READ_MODE: 765 while self.isOpened(): 766 frame = self.readFrame(with_timestamps) 767 if frame is not None: 768 yield frame 769 finally: 770 self.close()
Method to iterate on video frames using VideoIO obj. for frame in obj.iter_frames(): ....
Parameters
with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames
772 def iter_batches(self, batch_size : int, with_timestamps = False): 773 """ 774 Method to iterate on batch of frames using VideoIO obj. 775 for image_batch in obj.iter_batches(): 776 .... 777 778 Parameters 779 ---------- 780 with_timestamps: bool optional (default False) 781 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 782 """ 783 try: 784 if self.mode == PipeMode.READ_MODE: 785 while self.isOpened(): 786 batch = self.readBatch(batch_size, with_timestamps) 787 if batch is not None: 788 yield batch 789 finally: 790 self.close()
Method to iterate on batch of frames using VideoIO obj. for image_batch in obj.iter_batches(): ....
Parameters
with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
90 @staticmethod 91 def get_time_in_sec(filename, *, debug=False, logLevel=16): 92 """ 93 Static method to get length of a video file in seconds including milliseconds as decimal part. 94 95 Parameters 96 ---------- 97 filename : str or path 98 Video file name. 99 100 debug : bool (default False) 101 Show debug info. 102 103 log_level: int (default 16) 104 Log level to pass to the underlying ffmpeg/ffprobe command. 105 106 Returns 107 ---------- 108 float 109 Length in seconds of video file (including milliseconds as decimal part) 110 """ 111 112 cmd = [VideoIO.paramProgram, # ffprobe 113 '-hide_banner', 114 '-loglevel', str(logLevel), 115 '-show_entries', 'format=duration', 116 '-of', 'default=noprint_wrappers=1:nokey=1', 117 filename 118 ] 119 120 if debug == True: 121 print(' '.join(cmd)) 122 123 # call ffprobe and get params in one single line 124 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 125 output = lpipe.stdout.readlines() 126 lpipe.terminate() 127 # transform Bytes output to one single string 128 output = ''.join( [element.decode('utf-8') for element in output]) 129 130 try: 131 return float(output) 132 except (ValueError, TypeError): 133 return None
Static method to get length of a video file in seconds including milliseconds as decimal part.
Parameters
filename : str or path Video file name.
debug : bool (default False) Show debug info.
log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.
Returns
float Length in seconds of video file (including milliseconds as decimal part)
135 @staticmethod 136 def get_params(filename, *, debug=False, logLevel=16): 137 """ 138 Static method to get params (width, height, fps) from a video file. 139 140 Parameters 141 ---------- 142 filename: str or path 143 Video filename. 144 145 debug: bool (default (False) 146 Show debug info. 147 148 log_level: int (default 16) 149 Log level to pass to the underlying ffmpeg/ffprobe command. 150 151 Returns 152 ---------- 153 tuple 154 Tuple containing (width, height, fps) of the video 155 """ 156 cmd = [VideoIO.paramProgram, # ffprobe 157 '-hide_banner', 158 '-loglevel', str(logLevel), 159 '-show_entries', 'stream=width,height,r_frame_rate', 160 filename 161 ] 162 163 if debug == True: 164 print(' '.join(cmd)) 165 166 # call ffprobe and get params in one single line 167 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 168 output = lpipe.stdout.readlines() 169 lpipe.terminate() 170 # transform Bytes output to one single string 171 output = ''.join( [element.decode('utf-8') for element in output]) 172 173 pattern_width = r'width=(\d+)' 174 pattern_height = r'height=(\d+)' 175 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 176 177 # Search for values in the ffprobe output 178 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 179 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 180 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 181 182 # Extraction des valeurs 183 if match_width: 184 width = int(match_width.group(1)) 185 else: 186 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 187 188 if match_height: 189 height = int(match_height.group(1)) 190 else: 191 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 192 193 if match_fps: 194 numerator = float(match_fps.group(1)) 195 denominator = float(match_fps.group(2)) 196 fps = numerator / denominator 197 else: 198 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 199 200 return (width, height, fps)
Static method to get params (width, height, fps) from a video file.
Parameters
filename: str or path Video filename.
debug: bool (default (False) Show debug info.
log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.
Returns
tuple Tuple containing (width, height, fps) of the video
329 def is_opened(self) -> bool: 330 """ 331 Method to get status of the underlying pipe to ffmpeg. 332 333 Returns 334 ---------- 335 bool 336 True if pipe is opened (reading or writing mode), False if not. 337 """ 338 # is the pipe opened? 339 if self.pipe is not None and self.pipe.poll() is None: 340 return True 341 342 return False
Method to get status of the underlying pipe to ffmpeg.
Returns
bool True if pipe is opened (reading or writing mode), False if not.
564 def read_frame(self, with_timestamps = False): 565 """ 566 Read next frame from the video 567 568 Parameters 569 ---------- 570 with_timestamps: bool optional (default False) 571 If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s) 572 573 Returns 574 ---------- 575 nparray or FrameContainer 576 An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in ``FrameContainer.data`` and 577 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame). 578 """ 579 580 if self.pipe is None: 581 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram)) 582 # - pipe is in write mode 583 if self.mode != PipeMode.READ_MODE: 584 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 585 586 if with_timestamps: 587 # get elapsed time in video, it is time of next frame(s) 588 current_elapsed_time = self.get_elapsed_time() 589 590 # read rgb image from pipe 591 buffer = self.pipe.stdout.read(self.imageSize) 592 if len(buffer) != self.imageSize: 593 # not considered as an error, no more frame, no exception 594 return None 595 596 # get numpy UINT8 array from buffer 597 rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape) 598 599 # increase frame_counter 600 self.frame_counter.frame_count += 1 601 602 # say to gc that this buffer is no longer needed 603 del buffer 604 605 if with_timestamps: 606 return FrameContainer(1,rgbImage,self.fps,current_elapsed_time) 607 608 return rgbImage
Read next frame from the video
Parameters
with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s)
Returns
nparray or FrameContainer
An image of shape (3,width,height). if with_timestamps is True, the return object is a FrameContainer with the image in FrameContainer.data and
the associated timestamp in FrameContainer.timestamps as an array (one element for one frame).
610 def read_batch(self, number_of_frames, with_timestamps = False) -> Union[nb.array, FrameC]: 611 """ 612 Read next batch of images from the video 613 614 Parameters 615 ---------- 616 number_of_frames: int 617 Number of desired images within the batch. The last batch of the video may have less images. 618 619 with_timestamps: bool optional (default False) 620 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 621 622 Returns 623 ---------- 624 nparray or FrameContainer 625 A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in ``FrameContainer.data`` and 626 the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame). 627 """ 628 629 if self.pipe is None: 630 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram)) 631 # - pipe is in write mode 632 if self.mode != PipeMode.READ_MODE: 633 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 634 635 if with_timestamps: 636 # get elapsed time in video, it is time of next frame(s) 637 current_elapsed_time = self.get_elapsed_time() 638 639 # try to read complete batch 640 buffer = self.pipe.stdout.read(self.imageSize*number_of_frames) 641 642 # check if we have at least 1 Frame 643 if len(buffer) < self.imageSize: 644 # not considered as an error, no more frame, no exception 645 return None 646 647 # compute actual number of Frames 648 actualNbFrames = len(buffer)//self.imageSize 649 650 # get and reshape batch from buffer 651 batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3)) 652 653 # increase frame_counter 654 self.frame_counter.frame_count += actualNbFrames 655 656 # say to gc that this buffer is no longer needed 657 del buffer 658 659 if with_timestamps: 660 return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time) 661 662 return batch
Read next batch of images from the video
Parameters
number_of_frames: int Number of desired images within the batch. The last batch of the video may have less images.
with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames
Returns
nparray or FrameContainer
A batch of images of shape (n,3,width,height). if with_timestamps is True, the return object is a FrameContainer with the batch in FrameContainer.data and
the associated timestamps in FrameContainer.timestamps as an array (one element for each frame).
664 def write_frame(self, image) -> bool: 665 """ 666 Write an image to the video 667 668 Parameters 669 ---------- 670 image: nparray 671 The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called. 672 673 Returns 674 ---------- 675 bool 676 Writing was successful or not. 677 """ 678 679 # Check params 680 # - pipe exists 681 if self.pipe is None: 682 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 683 # - pipe is in write mode 684 if self.mode != PipeMode.WRITE_MODE: 685 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 686 # - shape of image is fine, thus we have pixels for a full compatible frame 687 if image.shape != self.shape: 688 raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape)) 689 # - type of data is UINT8 690 if image.dtype != np.uint8: 691 raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype)) 692 693 # write frame 694 buffer = image.tobytes() 695 if self.pipe.stdin.write( buffer ) < self.imageSize: 696 print( "Error writing frame to" ) 697 return False 698 699 # increase frame_counter 700 self.frame_counter.frame_count += 1 701 702 # say to gc that this buffer is no longer needed 703 del buffer 704 705 return True
Write an image to the video
Parameters
image: nparray The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called.
Returns
bool Writing was successful or not.
707 def write_batch(self, batch) -> bool: 708 """ 709 Write a batch of images to the video 710 711 Parameters 712 ---------- 713 batch: nparray 714 A batch of images to write to the video file in the PixelFormat provided when create was called. 715 716 Returns 717 ---------- 718 bool 719 Writing was successful or not. 720 """ 721 722 # Check params 723 # - pipe exists 724 if self.pipe is None: 725 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 726 # - pipe is in write mode 727 if self.mode != PipeMode.WRITE_MODE: 728 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 729 # - shape of images in batch is fine 730 if batch.shape[-3:] != self.shape: 731 raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape)) 732 # - we have the right amount of pixels for the full batch 733 if batch.size != (batch.shape[0]*self.imageSize): 734 raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize)) 735 736 # write frame 737 buffer = batch.tobytes() 738 if self.pipe.stdin.write( buffer ) < batch.size: 739 # say to gc that this buffer is no longer needed 740 del buffer 741 raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename)) 742 743 # increase frame_counter 744 self.frame_counter.frame_count += batch.shape[0] 745 746 # say to gc that this buffer is no longer needed 747 del buffer 748 749 return True
Write a batch of images to the video
Parameters
batch: nparray A batch of images to write to the video file in the PixelFormat provided when create was called.
Returns
bool Writing was successful or not.
90 @staticmethod 91 def get_time_in_sec(filename, *, debug=False, logLevel=16): 92 """ 93 Static method to get length of a video file in seconds including milliseconds as decimal part. 94 95 Parameters 96 ---------- 97 filename : str or path 98 Video file name. 99 100 debug : bool (default False) 101 Show debug info. 102 103 log_level: int (default 16) 104 Log level to pass to the underlying ffmpeg/ffprobe command. 105 106 Returns 107 ---------- 108 float 109 Length in seconds of video file (including milliseconds as decimal part) 110 """ 111 112 cmd = [VideoIO.paramProgram, # ffprobe 113 '-hide_banner', 114 '-loglevel', str(logLevel), 115 '-show_entries', 'format=duration', 116 '-of', 'default=noprint_wrappers=1:nokey=1', 117 filename 118 ] 119 120 if debug == True: 121 print(' '.join(cmd)) 122 123 # call ffprobe and get params in one single line 124 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 125 output = lpipe.stdout.readlines() 126 lpipe.terminate() 127 # transform Bytes output to one single string 128 output = ''.join( [element.decode('utf-8') for element in output]) 129 130 try: 131 return float(output) 132 except (ValueError, TypeError): 133 return None
Static method to get length of a video file in seconds including milliseconds as decimal part.
Parameters
filename : str or path Video file name.
debug : bool (default False) Show debug info.
log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.
Returns
float Length in seconds of video file (including milliseconds as decimal part)
135 @staticmethod 136 def get_params(filename, *, debug=False, logLevel=16): 137 """ 138 Static method to get params (width, height, fps) from a video file. 139 140 Parameters 141 ---------- 142 filename: str or path 143 Video filename. 144 145 debug: bool (default (False) 146 Show debug info. 147 148 log_level: int (default 16) 149 Log level to pass to the underlying ffmpeg/ffprobe command. 150 151 Returns 152 ---------- 153 tuple 154 Tuple containing (width, height, fps) of the video 155 """ 156 cmd = [VideoIO.paramProgram, # ffprobe 157 '-hide_banner', 158 '-loglevel', str(logLevel), 159 '-show_entries', 'stream=width,height,r_frame_rate', 160 filename 161 ] 162 163 if debug == True: 164 print(' '.join(cmd)) 165 166 # call ffprobe and get params in one single line 167 lpipe = sp.Popen(cmd, stdout=sp.PIPE) 168 output = lpipe.stdout.readlines() 169 lpipe.terminate() 170 # transform Bytes output to one single string 171 output = ''.join( [element.decode('utf-8') for element in output]) 172 173 pattern_width = r'width=(\d+)' 174 pattern_height = r'height=(\d+)' 175 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 176 177 # Search for values in the ffprobe output 178 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 179 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 180 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 181 182 # Extraction des valeurs 183 if match_width: 184 width = int(match_width.group(1)) 185 else: 186 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 187 188 if match_height: 189 height = int(match_height.group(1)) 190 else: 191 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 192 193 if match_fps: 194 numerator = float(match_fps.group(1)) 195 denominator = float(match_fps.group(2)) 196 fps = numerator / denominator 197 else: 198 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 199 200 return (width, height, fps)
Static method to get params (width, height, fps) from a video file.
Parameters
filename: str or path Video filename.
debug: bool (default (False) Show debug info.
log_level: int (default 16) Log level to pass to the underlying ffmpeg/ffprobe command.
Returns
tuple Tuple containing (width, height, fps) of the video
35 class VideoIOException(Exception): 36 """ 37 Dedicated exception class for VideoIO class. 38 """ 39 def __init__(self, message="Error while reading/writing video occurs"): 40 self.message = message 41 super().__init__(self.message)
Dedicated exception class for VideoIO class.
43 class PixelFormat(Enum): 44 """ 45 Enum class for supported input video type: GBR 24 bits or RGB 24 bis. 46 """ 47 GBR24 = 'bgr24' # default format 48 RGB24 = 'rgb24'
Enum class for supported input video type: GBR 24 bits or RGB 24 bis.