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