simple_ffmpeg_batch_io
Reading and writing image and audio batches from/to video and audio files using an FFmpeg backend, with a simple Python API built on top of numpy.
Features
- Read batches of audio and video frames from video and audio files into
numpyarrays. - Write batches of frames from
numpyarrays to video or audio files, even compressed - Uses
static_ffmpegto provide FFmpeg binaries in a portable way. Simple-ffmpeg-batch-io provide ffmpeg and ffprobe as commands within the virtual environment - Designed for machine learning and audio/video generation pipelines.
Installation of last version
pip install simple-ffmpeg-batch-io
Documentation
Automatically generated documentation is available online.
Examples
Handling video files (i.e. images from video files)
Read video file at its own frame rate and frame shape
# Open it with VideoIO old way (C++ style)
inputVideo = VideoIO()
inputVideo.open(video_filename) # video file is a str or a path
# or open it a more pythonish way
inputVideo = VideoIO.reader(video_filename) # video file is a str or a path
# Here, one can read inputVideo.width, inputVideo.height, inputVideo.fps
print(inputVideo.width, inputVideo.height, inputVideo.fps)
# read one frame
frame = inputVideo.read_frame() # Here frame is a numpy arrays of shape (Width,height,channels). Channel is 3 as we support only 3 channels for the moment.
# Process video frame by frame
for frame in inputVideo.iter_frames():
# Process frame. Here frame is a numpy arrays of shape (Width,height,channels). Channel is 3 as we support only 3 channels for the moment.
process( frame )
# or process video using batches
n = 10
for batch in inputVideo.iter_batches(n):
# Process batch. Here batch is a numpy arrays of shape (n,Width,height,channels). Channel is 3 as we support only 3 channels for the moment.
process( batch )
inputVideo.close()
# Read video frame by frame with associated timestamps using with context, no need to close after end of context, close is aotomùatically called.
with VideoIO.reader(video_filename) as inputVideo:
for frame in inputVideo.iter_frames(with_timestamps = True):
# Here frame is encapsulated within simple-ffmpeg-batch-io.FrameContainer object
process( frame.data ) # numpy array of shape (Width,height,channels)
print( frame.timestamps ) # python list of the timestamp associated to the frame (video here, but same for audio), here only one element as one frame is used
# OR
# Read video batch by batch with associated timestamps using with context, no need to close after end of context, close is aotomùatically called.
n = 10
with VideoIO.reader(video_filename) as inputVideo:
for batch in inputVideo.iter_batches(n, with_timestamps = True):
# Here batch is encapsulated within simple-ffmpeg-batch-io.FrameContainer object
process( batch.data ) # numpy array of shape (n,Width,height,channels)
print( batch.timestamps ) # python list of timestamps associated to each frame (video here, but same for audio), here only one element as one frame is used
Read video file with more parameters
from simple_ffmpeg_batch_io import VideoIO
# Read video changing width, height, and fps
inputVideo = VideoIO.reader(video_file, width=100, height=100, fps=1.0)
# Read modifying only some of them
with VideoIO.reader(video_file, width=100, fps=2.0) as inputVideo:
...
Read video and write video file with dedicated ffmpeg parameters
from simple_ffmpeg_batch_io import VideoIO
# open file using filter:
# resizing to width=320, adapting height to keep aspect ratio while keep height pair (for some codec like H264)
# pixelising it to 5x5 pixels
# important note: one must not use decodingParams for scalling. Indeed, VideoIO class use filter to scale video, use width/height parameters
with VideoIO.reader(video_in_filename, width=320, height=320, decodingParams="-vf pixelize=w=5:h=5") as inputVideo,
VideoIO.writer(video_out_filename, width=inputVideo.width, height=inputVideo.height, fps=inputVideo.fps ) as outputVideo: # possible to add encodingParams to add filters, ...
# iter over batch of 10s and write it to the output file
batch_size = int( 10*inputVideo.fps )
for batch in inputVideo.iter_batches(batch_size):
outputVideo.write_batch(batch)
Handling audio from video or audio files
Read audio from audio file
from simple_ffmpeg_batch_io import AudioIO
# Read audio converting it to one channel, 16000 Hz, frame size of 1s as parameter is a float, start reading file at 2.0s
# default mode is plannar, i.e. samples are not interleaved, they are separated by channel after reading
# frame_size for subsequent call to read_frame, iter_frames, read_batch or iter_batches is 1.0s (16000 samples) as the value is a float, thus times in seconds.
inputAudio = AudioIO.reader(audio_filename, sample_rate=16000, channels=1, frame_size = 1.0, start_time = 2.0 )
# Read batches of 10 audio frames, each frame has 16000 sanples (1.0 second)
for audio_batch in inputAudio.iter_batches(10):
... # audio_batch is a np.array
# OR
# If the frame_size value is an int, frame_size is considered as a number of samples, for instance to have 0.5 seconds at 16 Khz (8000 samples for each frame)
with AudioIO.reader(audio_filename, sample_rate=16000, channels=1, frame_size = 8000, start_time = 2.0 ) as inputAudio:
# Read batches of 10 audio frames, each frame has 8000 sanples
for audio_batch in inputAudio.iter_batches(10, with_timestamps = True):
.... # audio_batch is encapsulated within a FrameContainer object with_timestamps = True
Copy audio stream(s) from an audio file or a video file with an audio stream to a wav file
from simple_ffmpeg_batch_io import AudioIO
# Read audio data in interleaved mode (plannard = False) to avoid useless conversion to plannar using default frame_size (1 second). Sample rate remains the same.
with AudioIO.reader(audio_or_video_filename, plannar = False) as inputAudio:
# Copy sample_rate and channels to the created file, overwrite existing output filename if any.
with AudioIO.writer(audio_filename, sample_rate=inputAudio.sample_rate, channels=inputAudio.channels, plannar = False, writeOverExistingFile = True) as outputAudio:
# Read batches of 10 audio frames (10 seconds as frame_size was by default 1 second when opening video)
for audio_batch in inputAudio.iter_batches(10):
outputAudio.write_batch(audio_batch)
# no need to close AudioIO objects as 'with' context do it automatically.
Static utility functions
VideoIO
from simple_ffmpeg_batch_io import VideoIO
# get (width, height, fps) of a video using a static function
print( VideoIO.get_params(video_filename) )
# get length of an audio stream as float (seconds.milliseconds)
print( VideoIO.get_time_in_sec(video_filename) )
AudioIO
from simple_ffmpeg_batch_io import AudioIO
# get (channels,sample_rate) of a stream using astatic function
print( AudioIO.get_params(audio_or_video_filename) )
# get length of video stream as float (seconds.milliseconds)
print( AudioIO.get_time_in_sec(audio_or_video_filename) )
Submodules
1""" 2.. include:: ../../README.md 3 :start-after: # simple-ffmpeg-batch-io 4 5# Submodules 6""" 7 8__authors__ = ("Dominique Vaufreydaz") 9 10# __init__.py 11from .VideoIO import VideoIO 12from .AudioIO import AudioIO 13from .PipeMode import PipeMode 14from .FrameCounter import FrameCounter 15from .FrameContainer import FrameContainer 16 17__all__ = [ 18 "VideoIO", 19 "AudioIO", 20 "FrameCounter", 21 "FrameContainer", 22 "PipeMode", 23]
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, *, loglevel = 16, debug = False, **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(logLevel=loglevel, debug=debug) 60 reader.open(filename,**kwargs) 61 return reader 62 63 @classmethod 64 def writer(cls, filename, width, height, fps, *, loglevel = 16, debug = False, **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(logLevel=loglevel, debug=debug) 72 writer.create(filename, width, height, fps, **kwargs) 73 return writer 74 75 # standard method 76 def get_corresponding_writer(self, filename, **kwargs): 77 """ 78 Method to get writer for a video file with same width, height, fps as the current one VideoIO object 79 80 See `VideoIO.create` for the full list 81 of accepted parameters. 82 """ 83 return VideoIO.writer(filename, self.width, self.height, self.fps, **kwargs) 84 85 # To use with context manager "with VideoIO.reader(...) as f:' for instance 86 def __enter__(self): 87 """ 88 Method call at initialisation of a context manager like "with VideoIO.reader(...) as f:' for instance 89 """ 90 # simply return myself 91 return self 92 93 def __exit__(self, exc_type, exc_val, exc_tb): 94 """ 95 Method call when existing of a context manager like "with VideoIO.reader(...) as f:' for instance 96 """ 97 # close VideoIO 98 self.close() 99 return False 100 101 @staticmethod 102 def get_time_in_sec(filename, *, debug=False, logLevel=16): 103 """ 104 Static method to get length of a video file in seconds including milliseconds as decimal part. 105 106 Parameters 107 ---------- 108 filename : str or path 109 Video file name. 110 111 debug : bool (default False) 112 Show debug info. 113 114 log_level: int (default 16) 115 Log level to pass to the underlying ffmpeg/ffprobe command. 116 117 Returns 118 ---------- 119 float 120 Length in seconds of video file (including milliseconds as decimal part) 121 """ 122 123 cmd = [VideoIO.paramProgram, # ffprobe 124 '-hide_banner', 125 '-loglevel', str(logLevel), 126 '-show_entries', 'format=duration', 127 '-of', 'default=noprint_wrappers=1:nokey=1', 128 str(filename) 129 ] 130 131 if debug == True: 132 print(' '.join(cmd)) 133 134 # call ffprobe and get params in one single line 135 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 136 output = lpipe.stdout.readlines() 137 lpipe.terminate() 138 # transform Bytes output to one single string 139 output = ''.join( [element.decode('utf-8') for element in output]) 140 141 try: 142 return float(output) 143 except (ValueError, TypeError): 144 return None 145 146 @staticmethod 147 def get_params(filename, *, debug=False, logLevel=16): 148 """ 149 Static method to get params (width, height, fps) from a video file. 150 151 Parameters 152 ---------- 153 filename: str or path 154 Video filename. 155 156 debug: bool (default (False) 157 Show debug info. 158 159 log_level: int (default 16) 160 Log level to pass to the underlying ffmpeg/ffprobe command. 161 162 Returns 163 ---------- 164 tuple 165 Tuple containing (width, height, fps) of the video 166 """ 167 cmd = [VideoIO.paramProgram, # ffprobe 168 '-hide_banner', 169 '-loglevel', str(logLevel), 170 '-show_entries', 'stream=width,height,r_frame_rate', 171 str(filename) 172 ] 173 174 if debug == True: 175 print(' '.join(cmd)) 176 177 # call ffprobe and get params in one single line 178 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 179 output = lpipe.stdout.readlines() 180 lpipe.terminate() 181 # transform Bytes output to one single string 182 output = ''.join( [element.decode('utf-8') for element in output]) 183 184 pattern_width = r'width=(\d+)' 185 pattern_height = r'height=(\d+)' 186 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 187 188 # Search for values in the ffprobe output 189 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 190 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 191 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 192 193 # Extraction des valeurs 194 if match_width: 195 width = int(match_width.group(1)) 196 else: 197 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 198 199 if match_height: 200 height = int(match_height.group(1)) 201 else: 202 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 203 204 if match_fps: 205 numerator = float(match_fps.group(1)) 206 denominator = float(match_fps.group(2)) 207 fps = numerator / denominator 208 else: 209 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 210 211 return (width, height, fps) 212 213 # Attributes 214 mode: PipeMode 215 """ Pipemode of the current object (default PipeMode.UNK_MODE)""" 216 217 loglevel: int 218 """ loglevel of the underlying ffmpeg backend for this object (default 16)""" 219 220 debug: bool 221 """ debug flag for this object (print debut info, default False)""" 222 223 width: int 224 """ width of images (default -1) """ 225 226 height: int 227 """ height of images (default -1) """ 228 229 fps: float 230 """ fps of video (default -1.0) """ 231 232 pipe: sp.Popen 233 """ pipe object to ffmpeg/ffprobe (default None)""" 234 235 shape: tuple 236 """ Shape of images (default (None, None, None))""" 237 238 imageSize: int 239 """ Weight in bytes of one image (default -1)""" 240 241 filename: str 242 """ Filename of the video file (default None)""" 243 244 frame_counter: FrameCounter 245 """ `Framecounter` object to count ellapsed time (default None)""" 246 247 def __init__(self, *, logLevel = 16, debug = False): 248 """ 249 Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode 250 251 Parameters 252 ---------- 253 log_level: int (default 16) 254 Log level to pass to the underlying ffmpeg/ffprobe command. 255 256 debug: bool (default (False) 257 Show debug info. while processing video 258 """ 259 260 self.mode = PipeMode.UNK_MODE 261 self.logLevel = logLevel 262 self.debug = debug 263 264 # Call init() method 265 self.init() 266 267 def init(self): 268 """ 269 Init or reinit a VideoIO object. 270 """ 271 self.width = -1 272 self.height = -1 273 self.fps = -1.0 274 self.pipe = None 275 self.shape = (None, None, None) 276 self.imageSize = -1 277 self.filename = None 278 self.frame_counter = None 279 280 _repr_exclude = {"pipe"} 281 """ List of excluded attribute for string conversion. """ 282 283 # converting the object to a string representation 284 def __repr__(self): 285 """ 286 Convert object (excluding attributes in _repr_exclude) to string representation. 287 """ 288 attrs = ", ".join( 289 f"{k}={v!r}" 290 for k, v in self.__dict__.items() 291 if k not in self._repr_exclude 292 ) 293 return f"{self.__class__.__name__}({attrs})" 294 295 __str__ = __repr__ 296 """ String representation """ 297 298 def get_elapsed_time_as_str(self) -> str: 299 """ 300 Method to get elapsed time (float value) as str from `frame_counter` attribute. 301 302 Returns 303 ---------- 304 str or None 305 Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds 306 None if no frame counter are available. 307 """ 308 if self.frame_counter is None: 309 return None 310 return self.frame_counter.get_elapsed_time_as_str() 311 312 def get_formated_elapsed_time_as_str(self,show_ms=True) -> str: 313 """ 314 Method to get elapsed time (hour format) as str from `frame_counter` attribute. 315 316 Returns 317 ---------- 318 str or None 319 Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds 320 None if no frame counter are available. 321 """ 322 if self.frame_counter is None: 323 return None 324 return self.frame_counter.get_formated_elapsed_time_as_str() 325 326 def get_elapsed_time(self) -> float: 327 """ 328 Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute. 329 330 Returns 331 ---------- 332 float or None 333 Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds 334 None if no frame counter are available. 335 """ 336 if self.frame_counter is None: 337 return None 338 return self.frame_counter.get_elapsed_time() 339 340 def is_opened(self) -> bool: 341 """ 342 Method to get status of the underlying pipe to ffmpeg. 343 344 Returns 345 ---------- 346 bool 347 True if pipe is opened (reading or writing mode), False if not. 348 """ 349 # is the pipe opened? 350 if self.pipe is not None and self.pipe.poll() is None: 351 return True 352 353 return False 354 355 def close(self) -> None: 356 """ 357 Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods. 358 """ 359 if self.pipe is not None: 360 if self.mode == PipeMode.WRITE_MODE: 361 # killing will make ffmpeg not finish properly the job, close the pipe 362 # to let it know that no more data are comming 363 self.pipe.stdin.close() 364 else: # self.mode == PipeMode.READ_MODE 365 # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows 366 self.pipe.kill() 367 368 # wait for subprocess to end 369 self.pipe.wait() 370 371 # reinit object for later use 372 self.init() 373 374 def create(self, filename, width, height, fps, *, writeOverExistingFile = False, 375 inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool: 376 """ 377 Method to create a video using parametrized access through ffmpeg. Importante note: calling create 378 on a VideoIO will close any former open video. 379 380 Parameters 381 ---------- 382 filename: str or path 383 filename of path to the file (mp4, avi, ...) 384 385 width: int 386 If defined as a positive value, width of output images will be set to this value. 387 388 height: int 389 If defined as a positive value, height of output images will be set to this value. 390 391 fps: 392 If defined as a positive value, fps of output video will be set to this value. 393 394 inputEncoding: PixelFormat optional (default PixelFormat.BGR24) 395 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 396 397 encodingParams: str optional (default None) 398 Parameter to pass to ffmpeg to encode video like video filters. 399 400 Returns 401 ---------- 402 bool 403 Was the creation successfull 404 """ 405 406 # Close if already opened 407 self.close() 408 409 # Set geometry/fps of the video stream from params 410 self.width = int(width) 411 self.height = int(height) 412 self.fps = float(fps) 413 414 # Check params 415 if self.width <= 0 or self.height <= 0 or self.fps <= 0.0: 416 raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps)) 417 418 # Params are ok, set shape and image size 419 self.shape = (self.height,self.width,3) 420 self.imageSize = self.height * self.width * 3 421 422 # Video params are set, open the video 423 cmd = [self.videoProgram] # ffmpeg 424 425 if writeOverExistingFile == True: 426 cmd.extend(['-y']) 427 428 cmd.extend(['-hide_banner', 429 '-nostats', 430 '-loglevel', str(self.logLevel), 431 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value, 432 '-video_size', f"{self.width}x{self.height}", 433 '-r', "{:.3f}".format(self.fps), 434 '-i', '-']) 435 436 if encodingParams is not None: 437 cmd.extend(encodingParams.split()) 438 439 # Video filename converted to str (for Path values) 440 cmd.extend( ['-an', str(filename) ] ) 441 442 if self.debug == True: 443 print( ' '.join(cmd), file=sys.stderr ) 444 445 # store filename and set mode 446 self.filename = str(filename) 447 self.mode = PipeMode.WRITE_MODE 448 449 # Call ffmpeg in write mode 450 try: 451 self.pipe = sp.Popen(cmd, stdin=sp.PIPE) 452 self.frame_counter = FrameCounter(self.fps) 453 except Exception as e: 454 # if pipe failed, reinit object and raise exception 455 self.init() 456 raise 457 458 return True 459 460 def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24, 461 decodingParams = None, start_time = 0.0 ) -> bool: 462 """ 463 Method to read video using parametrized access through ffmpeg. Importante note: calling open 464 on a VideoIO will close any former open video. 465 466 Parameters 467 ---------- 468 filename: str or path 469 filename of path to the file (mp4, avi, ...) 470 471 width: int optional (default -1) 472 If defined as a positive value, width of input images will be converted to this value. 473 474 height: int optional (default -1) 475 If defined as a positive value, height of input images will be converted to this value. 476 477 fps: float optional (default -1.0) 478 If defined as a positive value, fps of input video will be converted to this value. 479 480 outputEncoding: PixelFormat optional (default PixelFormat.BGR24) 481 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 482 483 decodingParams: str optional (default None) 484 Parameter to pass to ffmpeg to decode video like filters. 485 486 start_time: float optional (default 0.0) 487 Define the reading start time. If not set, reading at beginning of the video. 488 489 Returns 490 ---------- 491 bool 492 Was the opening successfull 493 """ 494 495 # Close if already opened 496 self.close() 497 498 # Force conversion of parameters 499 width = int(width) 500 height = int(height) 501 fps = float(fps) 502 503 # get parameters from video 504 self.width, self.height, self.fps = self.getVideoParams(str(filename)) 505 506 # check if parameters ask to overide video parameters 507 # TODO: add support for negative value (automatic preservation of aspect ratio) 508 if width > 0: 509 self.width = width 510 if height > 0: 511 self.height = height 512 if fps > 0.0: 513 self.fps = fps 514 515 # Params are ok, set shape and image size 516 self.shape = (self.height,self.width,3) 517 self.imageSize = self.height * self.width * 3 518 519 # Video params are set, open the video 520 cmd = [self.videoProgram, # ffmpeg 521 '-hide_banner', 522 '-nostats', 523 '-loglevel', str(self.logLevel)] 524 525 if start_time < 0.0: 526 pass 527 elif start_time > 0.0: 528 cmd.extend(["-ss", f"{start_time}"]) # set start time if any 529 530 cmd.extend( ['-i', str(filename)] ) 531 532 video_filters = '' # empty 533 if decodingParams is not None: 534 decodingParams = decodingParams.split() 535 # walk over decodingParams for specific params 536 i = 0 537 while i < len(decodingParams): 538 if decodingParams[i] == '-vf': 539 decodingParams.pop(i) # remove '-vf' 540 if i < len(decodingParams): 541 video_filters += ','+decodingParams.pop(i) # remove parameters from list too 542 # to do : add support to other option like -y 543 else: 544 i += 1 545 else: 546 decodingParams = [] 547 548 cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters 549 *(decodingParams), 550 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding 551 '-an', # no audio 552 '-r', f"{self.fps}", 553 '-' # output to stdout 554 ] ) 555 556 if self.debug == True: 557 print( ' '.join(cmd) ) 558 559 # store filename and set mode to READ_MODE 560 self.filename = str(filename) 561 self.mode = PipeMode.READ_MODE 562 563 # call ffmpeg in read mode 564 try: 565 self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 566 self.frame_counter = FrameCounter(self.fps) 567 if start_time > 0.0: 568 self.frame_counter += start_time # adding with float means adding time 569 except Exception as e: 570 # if pipe failed, reinit object and raise exception 571 self.init() 572 raise 573 574 return True 575 576 def read_frame(self, with_timestamps = False): 577 """ 578 Read next frame from the video 579 580 Parameters 581 ---------- 582 with_timestamps: bool optional (default False) 583 If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s) 584 585 Returns 586 ---------- 587 nparray or FrameContainer 588 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 589 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame). 590 """ 591 592 if self.pipe is None: 593 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram)) 594 # - pipe is in write mode 595 if self.mode != PipeMode.READ_MODE: 596 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 597 598 if with_timestamps: 599 # get elapsed time in video, it is time of next frame(s) 600 current_elapsed_time = self.get_elapsed_time() 601 602 # read rgb image from pipe 603 buffer = self.pipe.stdout.read(self.imageSize) 604 if len(buffer) != self.imageSize: 605 # Incomplete image, ffmpeg have been stopped or killed, do not return an incomplete frame 606 # not considered as an error, no more frame, no exception 607 return None 608 609 # get numpy UINT8 array from buffer 610 rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape) 611 612 # increase frame_counter 613 self.frame_counter.frame_count += 1 614 615 # say to gc that this buffer is no longer needed 616 del buffer 617 618 if with_timestamps: 619 return FrameContainer(1,rgbImage,self.fps,current_elapsed_time) 620 621 return rgbImage 622 623 def read_batch(self, number_of_frames, with_timestamps = False) -> Union[np.array, FrameContainer]: 624 """ 625 Read next batch of images from the video 626 627 Parameters 628 ---------- 629 number_of_frames: int 630 Number of desired images within the batch. The last batch of the video may have less images. 631 632 with_timestamps: bool optional (default False) 633 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 634 635 Returns 636 ---------- 637 nparray or FrameContainer 638 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 639 the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame). 640 """ 641 642 if self.pipe is None: 643 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram)) 644 # - pipe is in write mode 645 if self.mode != PipeMode.READ_MODE: 646 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 647 648 if with_timestamps: 649 # get elapsed time in video, it is time of next frame(s) 650 current_elapsed_time = self.get_elapsed_time() 651 652 # try to read complete batch 653 buffer = self.pipe.stdout.read(self.imageSize*number_of_frames) 654 655 # check if we have at least 1 image 656 if len(buffer) < self.imageSize: 657 # ffmpag backend have been stopped or killed 658 # not considered as an error, no more frame, no exception 659 return None 660 661 # compute actual number of Frames 662 actualNbFrames = len(buffer)//self.imageSize 663 664 # get and reshape batch from buffer 665 batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3)) 666 667 # increase frame_counter 668 self.frame_counter.frame_count += actualNbFrames 669 670 # say to gc that this buffer is no longer needed 671 del buffer 672 673 if with_timestamps: 674 return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time) 675 676 return batch 677 678 def write_frame(self, image) -> bool: 679 """ 680 Write an image to the video 681 682 Parameters 683 ---------- 684 image: nparray 685 The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called. 686 687 Returns 688 ---------- 689 bool 690 Writing was successful or not. 691 """ 692 693 # Check params 694 # - pipe exists 695 if self.pipe is None: 696 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 697 # - pipe is in write mode 698 if self.mode != PipeMode.WRITE_MODE: 699 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 700 # - shape of image is fine, thus we have pixels for a full compatible frame 701 if image.shape != self.shape: 702 raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape)) 703 # - type of data is UINT8 704 if image.dtype != np.uint8: 705 raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype)) 706 707 # write frame 708 buffer = image.tobytes() 709 if self.pipe.stdin.write( buffer ) < self.imageSize: 710 print( "Error writing frame to" ) 711 return False 712 713 # increase frame_counter 714 self.frame_counter.frame_count += 1 715 716 # say to gc that this buffer is no longer needed 717 del buffer 718 719 return True 720 721 def write_batch(self, batch) -> bool: 722 """ 723 Write a batch of images to the video 724 725 Parameters 726 ---------- 727 batch: nparray 728 A batch of images to write to the video file in the PixelFormat provided when create was called. 729 730 Returns 731 ---------- 732 bool 733 Writing was successful or not. 734 """ 735 736 # Check params 737 # - pipe exists 738 if self.pipe is None: 739 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 740 # - pipe is in write mode 741 if self.mode != PipeMode.WRITE_MODE: 742 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 743 # - shape of images in batch is fine 744 if batch.shape[-3:] != self.shape: 745 raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape)) 746 # - we have the right amount of pixels for the full batch 747 if batch.size != (batch.shape[0]*self.imageSize): 748 raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize)) 749 750 # write frame 751 buffer = batch.tobytes() 752 if self.pipe.stdin.write( buffer ) < batch.size: 753 # say to gc that this buffer is no longer needed 754 del buffer 755 raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename)) 756 757 # increase frame_counter 758 self.frame_counter.frame_count += batch.shape[0] 759 760 # say to gc that this buffer is no longer needed 761 del buffer 762 763 return True 764 765 def iter_frames(self, with_timestamps = False): 766 """ 767 Method to iterate on video frames using VideoIO obj. 768 for frame in obj.iter_frames(): 769 .... 770 771 Parameters 772 ---------- 773 with_timestamps: bool optional (default False) 774 If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames 775 """ 776 777 try: 778 if self.mode == PipeMode.READ_MODE: 779 while self.isOpened(): 780 frame = self.readFrame(with_timestamps) 781 if frame is not None: 782 yield frame 783 finally: 784 self.close() 785 786 def iter_batches(self, batch_size : int, with_timestamps = False): 787 """ 788 Method to iterate on batch of frames using VideoIO obj. 789 for image_batch in obj.iter_batches(): 790 .... 791 792 Parameters 793 ---------- 794 with_timestamps: bool optional (default False) 795 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 796 """ 797 try: 798 if self.mode == PipeMode.READ_MODE: 799 while self.isOpened(): 800 batch = self.readBatch(batch_size, with_timestamps) 801 if batch is not None: 802 yield batch 803 finally: 804 self.close() 805 806 # function aliases to be compliant with original C++ version 807 getVideoTimeInSec = get_time_in_sec 808 getVideoParams = get_params 809 get_video_time_in_sec = get_time_in_sec 810 get_video_params = get_params 811 isOpened = is_opened 812 readFrame = read_frame 813 readBatch = read_batch 814 writeFrame = write_frame 815 writeBatch = write_batch
247 def __init__(self, *, logLevel = 16, debug = False): 248 """ 249 Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode 250 251 Parameters 252 ---------- 253 log_level: int (default 16) 254 Log level to pass to the underlying ffmpeg/ffprobe command. 255 256 debug: bool (default (False) 257 Show debug info. while processing video 258 """ 259 260 self.mode = PipeMode.UNK_MODE 261 self.logLevel = logLevel 262 self.debug = debug 263 264 # Call init() method 265 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.
debug: bool (default (False) Show debug info. while processing video
51 @classmethod 52 def reader(cls, filename, *, loglevel = 16, debug = False, **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(logLevel=loglevel, debug=debug) 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, *, loglevel = 16, debug = False, **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(logLevel=loglevel, debug=debug) 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.
76 def get_corresponding_writer(self, filename, **kwargs): 77 """ 78 Method to get writer for a video file with same width, height, fps as the current one VideoIO object 79 80 See `VideoIO.create` for the full list 81 of accepted parameters. 82 """ 83 return VideoIO.writer(filename, self.width, self.height, self.fps, **kwargs)
Method to get writer for a video file with same width, height, fps as the current one VideoIO object
See VideoIO.create for the full list
of accepted parameters.
101 @staticmethod 102 def get_time_in_sec(filename, *, debug=False, logLevel=16): 103 """ 104 Static method to get length of a video file in seconds including milliseconds as decimal part. 105 106 Parameters 107 ---------- 108 filename : str or path 109 Video file name. 110 111 debug : bool (default False) 112 Show debug info. 113 114 log_level: int (default 16) 115 Log level to pass to the underlying ffmpeg/ffprobe command. 116 117 Returns 118 ---------- 119 float 120 Length in seconds of video file (including milliseconds as decimal part) 121 """ 122 123 cmd = [VideoIO.paramProgram, # ffprobe 124 '-hide_banner', 125 '-loglevel', str(logLevel), 126 '-show_entries', 'format=duration', 127 '-of', 'default=noprint_wrappers=1:nokey=1', 128 str(filename) 129 ] 130 131 if debug == True: 132 print(' '.join(cmd)) 133 134 # call ffprobe and get params in one single line 135 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 136 output = lpipe.stdout.readlines() 137 lpipe.terminate() 138 # transform Bytes output to one single string 139 output = ''.join( [element.decode('utf-8') for element in output]) 140 141 try: 142 return float(output) 143 except (ValueError, TypeError): 144 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)
146 @staticmethod 147 def get_params(filename, *, debug=False, logLevel=16): 148 """ 149 Static method to get params (width, height, fps) from a video file. 150 151 Parameters 152 ---------- 153 filename: str or path 154 Video filename. 155 156 debug: bool (default (False) 157 Show debug info. 158 159 log_level: int (default 16) 160 Log level to pass to the underlying ffmpeg/ffprobe command. 161 162 Returns 163 ---------- 164 tuple 165 Tuple containing (width, height, fps) of the video 166 """ 167 cmd = [VideoIO.paramProgram, # ffprobe 168 '-hide_banner', 169 '-loglevel', str(logLevel), 170 '-show_entries', 'stream=width,height,r_frame_rate', 171 str(filename) 172 ] 173 174 if debug == True: 175 print(' '.join(cmd)) 176 177 # call ffprobe and get params in one single line 178 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 179 output = lpipe.stdout.readlines() 180 lpipe.terminate() 181 # transform Bytes output to one single string 182 output = ''.join( [element.decode('utf-8') for element in output]) 183 184 pattern_width = r'width=(\d+)' 185 pattern_height = r'height=(\d+)' 186 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 187 188 # Search for values in the ffprobe output 189 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 190 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 191 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 192 193 # Extraction des valeurs 194 if match_width: 195 width = int(match_width.group(1)) 196 else: 197 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 198 199 if match_height: 200 height = int(match_height.group(1)) 201 else: 202 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 203 204 if match_fps: 205 numerator = float(match_fps.group(1)) 206 denominator = float(match_fps.group(2)) 207 fps = numerator / denominator 208 else: 209 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 210 211 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
267 def init(self): 268 """ 269 Init or reinit a VideoIO object. 270 """ 271 self.width = -1 272 self.height = -1 273 self.fps = -1.0 274 self.pipe = None 275 self.shape = (None, None, None) 276 self.imageSize = -1 277 self.filename = None 278 self.frame_counter = None
Init or reinit a VideoIO object.
298 def get_elapsed_time_as_str(self) -> str: 299 """ 300 Method to get elapsed time (float value) as str from `frame_counter` attribute. 301 302 Returns 303 ---------- 304 str or None 305 Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds 306 None if no frame counter are available. 307 """ 308 if self.frame_counter is None: 309 return None 310 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.
312 def get_formated_elapsed_time_as_str(self,show_ms=True) -> str: 313 """ 314 Method to get elapsed time (hour format) as str from `frame_counter` attribute. 315 316 Returns 317 ---------- 318 str or None 319 Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds 320 None if no frame counter are available. 321 """ 322 if self.frame_counter is None: 323 return None 324 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.
326 def get_elapsed_time(self) -> float: 327 """ 328 Method to get elapsed time as float value rounded to 3 decimals (millisecond) from `frame_counter` attribute. 329 330 Returns 331 ---------- 332 float or None 333 Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds 334 None if no frame counter are available. 335 """ 336 if self.frame_counter is None: 337 return None 338 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.
340 def is_opened(self) -> bool: 341 """ 342 Method to get status of the underlying pipe to ffmpeg. 343 344 Returns 345 ---------- 346 bool 347 True if pipe is opened (reading or writing mode), False if not. 348 """ 349 # is the pipe opened? 350 if self.pipe is not None and self.pipe.poll() is None: 351 return True 352 353 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.
355 def close(self) -> None: 356 """ 357 Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods. 358 """ 359 if self.pipe is not None: 360 if self.mode == PipeMode.WRITE_MODE: 361 # killing will make ffmpeg not finish properly the job, close the pipe 362 # to let it know that no more data are comming 363 self.pipe.stdin.close() 364 else: # self.mode == PipeMode.READ_MODE 365 # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows 366 self.pipe.kill() 367 368 # wait for subprocess to end 369 self.pipe.wait() 370 371 # reinit object for later use 372 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.
374 def create(self, filename, width, height, fps, *, writeOverExistingFile = False, 375 inputEncoding = PixelFormat.GBR24, encodingParams = None ) -> bool: 376 """ 377 Method to create a video using parametrized access through ffmpeg. Importante note: calling create 378 on a VideoIO will close any former open video. 379 380 Parameters 381 ---------- 382 filename: str or path 383 filename of path to the file (mp4, avi, ...) 384 385 width: int 386 If defined as a positive value, width of output images will be set to this value. 387 388 height: int 389 If defined as a positive value, height of output images will be set to this value. 390 391 fps: 392 If defined as a positive value, fps of output video will be set to this value. 393 394 inputEncoding: PixelFormat optional (default PixelFormat.BGR24) 395 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 396 397 encodingParams: str optional (default None) 398 Parameter to pass to ffmpeg to encode video like video filters. 399 400 Returns 401 ---------- 402 bool 403 Was the creation successfull 404 """ 405 406 # Close if already opened 407 self.close() 408 409 # Set geometry/fps of the video stream from params 410 self.width = int(width) 411 self.height = int(height) 412 self.fps = float(fps) 413 414 # Check params 415 if self.width <= 0 or self.height <= 0 or self.fps <= 0.0: 416 raise self.VideoIOException("Bad parameters: width={}, height={}, fps={:3f}".format(self.width,self.height,self.fps)) 417 418 # Params are ok, set shape and image size 419 self.shape = (self.height,self.width,3) 420 self.imageSize = self.height * self.width * 3 421 422 # Video params are set, open the video 423 cmd = [self.videoProgram] # ffmpeg 424 425 if writeOverExistingFile == True: 426 cmd.extend(['-y']) 427 428 cmd.extend(['-hide_banner', 429 '-nostats', 430 '-loglevel', str(self.logLevel), 431 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', inputEncoding.value, 432 '-video_size', f"{self.width}x{self.height}", 433 '-r', "{:.3f}".format(self.fps), 434 '-i', '-']) 435 436 if encodingParams is not None: 437 cmd.extend(encodingParams.split()) 438 439 # Video filename converted to str (for Path values) 440 cmd.extend( ['-an', str(filename) ] ) 441 442 if self.debug == True: 443 print( ' '.join(cmd), file=sys.stderr ) 444 445 # store filename and set mode 446 self.filename = str(filename) 447 self.mode = PipeMode.WRITE_MODE 448 449 # Call ffmpeg in write mode 450 try: 451 self.pipe = sp.Popen(cmd, stdin=sp.PIPE) 452 self.frame_counter = FrameCounter(self.fps) 453 except Exception as e: 454 # if pipe failed, reinit object and raise exception 455 self.init() 456 raise 457 458 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
460 def open( self, filename, *, width = -1, height = -1, fps = -1.0, outputEncoding = PixelFormat.GBR24, 461 decodingParams = None, start_time = 0.0 ) -> bool: 462 """ 463 Method to read video using parametrized access through ffmpeg. Importante note: calling open 464 on a VideoIO will close any former open video. 465 466 Parameters 467 ---------- 468 filename: str or path 469 filename of path to the file (mp4, avi, ...) 470 471 width: int optional (default -1) 472 If defined as a positive value, width of input images will be converted to this value. 473 474 height: int optional (default -1) 475 If defined as a positive value, height of input images will be converted to this value. 476 477 fps: float optional (default -1.0) 478 If defined as a positive value, fps of input video will be converted to this value. 479 480 outputEncoding: PixelFormat optional (default PixelFormat.BGR24) 481 Define order of channels for channels. Possible values are PixelFormat.BGR24 or PixelFormat.RGB24. 482 483 decodingParams: str optional (default None) 484 Parameter to pass to ffmpeg to decode video like filters. 485 486 start_time: float optional (default 0.0) 487 Define the reading start time. If not set, reading at beginning of the video. 488 489 Returns 490 ---------- 491 bool 492 Was the opening successfull 493 """ 494 495 # Close if already opened 496 self.close() 497 498 # Force conversion of parameters 499 width = int(width) 500 height = int(height) 501 fps = float(fps) 502 503 # get parameters from video 504 self.width, self.height, self.fps = self.getVideoParams(str(filename)) 505 506 # check if parameters ask to overide video parameters 507 # TODO: add support for negative value (automatic preservation of aspect ratio) 508 if width > 0: 509 self.width = width 510 if height > 0: 511 self.height = height 512 if fps > 0.0: 513 self.fps = fps 514 515 # Params are ok, set shape and image size 516 self.shape = (self.height,self.width,3) 517 self.imageSize = self.height * self.width * 3 518 519 # Video params are set, open the video 520 cmd = [self.videoProgram, # ffmpeg 521 '-hide_banner', 522 '-nostats', 523 '-loglevel', str(self.logLevel)] 524 525 if start_time < 0.0: 526 pass 527 elif start_time > 0.0: 528 cmd.extend(["-ss", f"{start_time}"]) # set start time if any 529 530 cmd.extend( ['-i', str(filename)] ) 531 532 video_filters = '' # empty 533 if decodingParams is not None: 534 decodingParams = decodingParams.split() 535 # walk over decodingParams for specific params 536 i = 0 537 while i < len(decodingParams): 538 if decodingParams[i] == '-vf': 539 decodingParams.pop(i) # remove '-vf' 540 if i < len(decodingParams): 541 video_filters += ','+decodingParams.pop(i) # remove parameters from list too 542 # to do : add support to other option like -y 543 else: 544 i += 1 545 else: 546 decodingParams = [] 547 548 cmd.extend( ['-vf', f'scale={self.width}:{self.height}{video_filters}', # rescale (or not if shape is original one), add specific video filters 549 *(decodingParams), 550 '-f', 'rawvideo', '-vcodec', 'rawvideo', '-pix_fmt', outputEncoding.value, # input expected coding 551 '-an', # no audio 552 '-r', f"{self.fps}", 553 '-' # output to stdout 554 ] ) 555 556 if self.debug == True: 557 print( ' '.join(cmd) ) 558 559 # store filename and set mode to READ_MODE 560 self.filename = str(filename) 561 self.mode = PipeMode.READ_MODE 562 563 # call ffmpeg in read mode 564 try: 565 self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 566 self.frame_counter = FrameCounter(self.fps) 567 if start_time > 0.0: 568 self.frame_counter += start_time # adding with float means adding time 569 except Exception as e: 570 # if pipe failed, reinit object and raise exception 571 self.init() 572 raise 573 574 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
576 def read_frame(self, with_timestamps = False): 577 """ 578 Read next frame from the video 579 580 Parameters 581 ---------- 582 with_timestamps: bool optional (default False) 583 If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s) 584 585 Returns 586 ---------- 587 nparray or FrameContainer 588 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 589 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame). 590 """ 591 592 if self.pipe is None: 593 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram)) 594 # - pipe is in write mode 595 if self.mode != PipeMode.READ_MODE: 596 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 597 598 if with_timestamps: 599 # get elapsed time in video, it is time of next frame(s) 600 current_elapsed_time = self.get_elapsed_time() 601 602 # read rgb image from pipe 603 buffer = self.pipe.stdout.read(self.imageSize) 604 if len(buffer) != self.imageSize: 605 # Incomplete image, ffmpeg have been stopped or killed, do not return an incomplete frame 606 # not considered as an error, no more frame, no exception 607 return None 608 609 # get numpy UINT8 array from buffer 610 rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape) 611 612 # increase frame_counter 613 self.frame_counter.frame_count += 1 614 615 # say to gc that this buffer is no longer needed 616 del buffer 617 618 if with_timestamps: 619 return FrameContainer(1,rgbImage,self.fps,current_elapsed_time) 620 621 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).
623 def read_batch(self, number_of_frames, with_timestamps = False) -> Union[np.array, FrameContainer]: 624 """ 625 Read next batch of images from the video 626 627 Parameters 628 ---------- 629 number_of_frames: int 630 Number of desired images within the batch. The last batch of the video may have less images. 631 632 with_timestamps: bool optional (default False) 633 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 634 635 Returns 636 ---------- 637 nparray or FrameContainer 638 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 639 the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame). 640 """ 641 642 if self.pipe is None: 643 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram)) 644 # - pipe is in write mode 645 if self.mode != PipeMode.READ_MODE: 646 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 647 648 if with_timestamps: 649 # get elapsed time in video, it is time of next frame(s) 650 current_elapsed_time = self.get_elapsed_time() 651 652 # try to read complete batch 653 buffer = self.pipe.stdout.read(self.imageSize*number_of_frames) 654 655 # check if we have at least 1 image 656 if len(buffer) < self.imageSize: 657 # ffmpag backend have been stopped or killed 658 # not considered as an error, no more frame, no exception 659 return None 660 661 # compute actual number of Frames 662 actualNbFrames = len(buffer)//self.imageSize 663 664 # get and reshape batch from buffer 665 batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3)) 666 667 # increase frame_counter 668 self.frame_counter.frame_count += actualNbFrames 669 670 # say to gc that this buffer is no longer needed 671 del buffer 672 673 if with_timestamps: 674 return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time) 675 676 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).
678 def write_frame(self, image) -> bool: 679 """ 680 Write an image to the video 681 682 Parameters 683 ---------- 684 image: nparray 685 The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called. 686 687 Returns 688 ---------- 689 bool 690 Writing was successful or not. 691 """ 692 693 # Check params 694 # - pipe exists 695 if self.pipe is None: 696 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 697 # - pipe is in write mode 698 if self.mode != PipeMode.WRITE_MODE: 699 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 700 # - shape of image is fine, thus we have pixels for a full compatible frame 701 if image.shape != self.shape: 702 raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape)) 703 # - type of data is UINT8 704 if image.dtype != np.uint8: 705 raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype)) 706 707 # write frame 708 buffer = image.tobytes() 709 if self.pipe.stdin.write( buffer ) < self.imageSize: 710 print( "Error writing frame to" ) 711 return False 712 713 # increase frame_counter 714 self.frame_counter.frame_count += 1 715 716 # say to gc that this buffer is no longer needed 717 del buffer 718 719 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.
721 def write_batch(self, batch) -> bool: 722 """ 723 Write a batch of images to the video 724 725 Parameters 726 ---------- 727 batch: nparray 728 A batch of images to write to the video file in the PixelFormat provided when create was called. 729 730 Returns 731 ---------- 732 bool 733 Writing was successful or not. 734 """ 735 736 # Check params 737 # - pipe exists 738 if self.pipe is None: 739 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 740 # - pipe is in write mode 741 if self.mode != PipeMode.WRITE_MODE: 742 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 743 # - shape of images in batch is fine 744 if batch.shape[-3:] != self.shape: 745 raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape)) 746 # - we have the right amount of pixels for the full batch 747 if batch.size != (batch.shape[0]*self.imageSize): 748 raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize)) 749 750 # write frame 751 buffer = batch.tobytes() 752 if self.pipe.stdin.write( buffer ) < batch.size: 753 # say to gc that this buffer is no longer needed 754 del buffer 755 raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename)) 756 757 # increase frame_counter 758 self.frame_counter.frame_count += batch.shape[0] 759 760 # say to gc that this buffer is no longer needed 761 del buffer 762 763 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.
765 def iter_frames(self, with_timestamps = False): 766 """ 767 Method to iterate on video frames using VideoIO obj. 768 for frame in obj.iter_frames(): 769 .... 770 771 Parameters 772 ---------- 773 with_timestamps: bool optional (default False) 774 If set to True, the method returns a FrameContainer with the batch and an array containing the associated timestamps to frames 775 """ 776 777 try: 778 if self.mode == PipeMode.READ_MODE: 779 while self.isOpened(): 780 frame = self.readFrame(with_timestamps) 781 if frame is not None: 782 yield frame 783 finally: 784 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
786 def iter_batches(self, batch_size : int, with_timestamps = False): 787 """ 788 Method to iterate on batch of frames using VideoIO obj. 789 for image_batch in obj.iter_batches(): 790 .... 791 792 Parameters 793 ---------- 794 with_timestamps: bool optional (default False) 795 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 796 """ 797 try: 798 if self.mode == PipeMode.READ_MODE: 799 while self.isOpened(): 800 batch = self.readBatch(batch_size, with_timestamps) 801 if batch is not None: 802 yield batch 803 finally: 804 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
101 @staticmethod 102 def get_time_in_sec(filename, *, debug=False, logLevel=16): 103 """ 104 Static method to get length of a video file in seconds including milliseconds as decimal part. 105 106 Parameters 107 ---------- 108 filename : str or path 109 Video file name. 110 111 debug : bool (default False) 112 Show debug info. 113 114 log_level: int (default 16) 115 Log level to pass to the underlying ffmpeg/ffprobe command. 116 117 Returns 118 ---------- 119 float 120 Length in seconds of video file (including milliseconds as decimal part) 121 """ 122 123 cmd = [VideoIO.paramProgram, # ffprobe 124 '-hide_banner', 125 '-loglevel', str(logLevel), 126 '-show_entries', 'format=duration', 127 '-of', 'default=noprint_wrappers=1:nokey=1', 128 str(filename) 129 ] 130 131 if debug == True: 132 print(' '.join(cmd)) 133 134 # call ffprobe and get params in one single line 135 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 136 output = lpipe.stdout.readlines() 137 lpipe.terminate() 138 # transform Bytes output to one single string 139 output = ''.join( [element.decode('utf-8') for element in output]) 140 141 try: 142 return float(output) 143 except (ValueError, TypeError): 144 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)
146 @staticmethod 147 def get_params(filename, *, debug=False, logLevel=16): 148 """ 149 Static method to get params (width, height, fps) from a video file. 150 151 Parameters 152 ---------- 153 filename: str or path 154 Video filename. 155 156 debug: bool (default (False) 157 Show debug info. 158 159 log_level: int (default 16) 160 Log level to pass to the underlying ffmpeg/ffprobe command. 161 162 Returns 163 ---------- 164 tuple 165 Tuple containing (width, height, fps) of the video 166 """ 167 cmd = [VideoIO.paramProgram, # ffprobe 168 '-hide_banner', 169 '-loglevel', str(logLevel), 170 '-show_entries', 'stream=width,height,r_frame_rate', 171 str(filename) 172 ] 173 174 if debug == True: 175 print(' '.join(cmd)) 176 177 # call ffprobe and get params in one single line 178 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 179 output = lpipe.stdout.readlines() 180 lpipe.terminate() 181 # transform Bytes output to one single string 182 output = ''.join( [element.decode('utf-8') for element in output]) 183 184 pattern_width = r'width=(\d+)' 185 pattern_height = r'height=(\d+)' 186 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 187 188 # Search for values in the ffprobe output 189 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 190 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 191 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 192 193 # Extraction des valeurs 194 if match_width: 195 width = int(match_width.group(1)) 196 else: 197 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 198 199 if match_height: 200 height = int(match_height.group(1)) 201 else: 202 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 203 204 if match_fps: 205 numerator = float(match_fps.group(1)) 206 denominator = float(match_fps.group(2)) 207 fps = numerator / denominator 208 else: 209 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 210 211 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
101 @staticmethod 102 def get_time_in_sec(filename, *, debug=False, logLevel=16): 103 """ 104 Static method to get length of a video file in seconds including milliseconds as decimal part. 105 106 Parameters 107 ---------- 108 filename : str or path 109 Video file name. 110 111 debug : bool (default False) 112 Show debug info. 113 114 log_level: int (default 16) 115 Log level to pass to the underlying ffmpeg/ffprobe command. 116 117 Returns 118 ---------- 119 float 120 Length in seconds of video file (including milliseconds as decimal part) 121 """ 122 123 cmd = [VideoIO.paramProgram, # ffprobe 124 '-hide_banner', 125 '-loglevel', str(logLevel), 126 '-show_entries', 'format=duration', 127 '-of', 'default=noprint_wrappers=1:nokey=1', 128 str(filename) 129 ] 130 131 if debug == True: 132 print(' '.join(cmd)) 133 134 # call ffprobe and get params in one single line 135 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 136 output = lpipe.stdout.readlines() 137 lpipe.terminate() 138 # transform Bytes output to one single string 139 output = ''.join( [element.decode('utf-8') for element in output]) 140 141 try: 142 return float(output) 143 except (ValueError, TypeError): 144 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)
146 @staticmethod 147 def get_params(filename, *, debug=False, logLevel=16): 148 """ 149 Static method to get params (width, height, fps) from a video file. 150 151 Parameters 152 ---------- 153 filename: str or path 154 Video filename. 155 156 debug: bool (default (False) 157 Show debug info. 158 159 log_level: int (default 16) 160 Log level to pass to the underlying ffmpeg/ffprobe command. 161 162 Returns 163 ---------- 164 tuple 165 Tuple containing (width, height, fps) of the video 166 """ 167 cmd = [VideoIO.paramProgram, # ffprobe 168 '-hide_banner', 169 '-loglevel', str(logLevel), 170 '-show_entries', 'stream=width,height,r_frame_rate', 171 str(filename) 172 ] 173 174 if debug == True: 175 print(' '.join(cmd)) 176 177 # call ffprobe and get params in one single line 178 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 179 output = lpipe.stdout.readlines() 180 lpipe.terminate() 181 # transform Bytes output to one single string 182 output = ''.join( [element.decode('utf-8') for element in output]) 183 184 pattern_width = r'width=(\d+)' 185 pattern_height = r'height=(\d+)' 186 pattern_fps = r'r_frame_rate=(\d+)/(\d+)' 187 188 # Search for values in the ffprobe output 189 match_width = re.search(pattern_width, output, flags=re.MULTILINE) 190 match_height = re.search(pattern_height, output, flags=re.MULTILINE) 191 match_fps = re.search(pattern_fps, output, flags=re.MULTILINE) 192 193 # Extraction des valeurs 194 if match_width: 195 width = int(match_width.group(1)) 196 else: 197 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 198 199 if match_height: 200 height = int(match_height.group(1)) 201 else: 202 raise VideoIO.VideoIOException("Unable to get geometry of '" + filename + "'") 203 204 if match_fps: 205 numerator = float(match_fps.group(1)) 206 denominator = float(match_fps.group(2)) 207 fps = numerator / denominator 208 else: 209 raise VideoIO.VideoIOException("Unable to get frame rate (fps) of '" + filename + "'") 210 211 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
340 def is_opened(self) -> bool: 341 """ 342 Method to get status of the underlying pipe to ffmpeg. 343 344 Returns 345 ---------- 346 bool 347 True if pipe is opened (reading or writing mode), False if not. 348 """ 349 # is the pipe opened? 350 if self.pipe is not None and self.pipe.poll() is None: 351 return True 352 353 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.
576 def read_frame(self, with_timestamps = False): 577 """ 578 Read next frame from the video 579 580 Parameters 581 ---------- 582 with_timestamps: bool optional (default False) 583 If set to True, the method returns a FrameContainer with the image and an array containing the associated timestamp(s) 584 585 Returns 586 ---------- 587 nparray or FrameContainer 588 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 589 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for one frame). 590 """ 591 592 if self.pipe is None: 593 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.videoProgram)) 594 # - pipe is in write mode 595 if self.mode != PipeMode.READ_MODE: 596 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 597 598 if with_timestamps: 599 # get elapsed time in video, it is time of next frame(s) 600 current_elapsed_time = self.get_elapsed_time() 601 602 # read rgb image from pipe 603 buffer = self.pipe.stdout.read(self.imageSize) 604 if len(buffer) != self.imageSize: 605 # Incomplete image, ffmpeg have been stopped or killed, do not return an incomplete frame 606 # not considered as an error, no more frame, no exception 607 return None 608 609 # get numpy UINT8 array from buffer 610 rgbImage = np.frombuffer(buffer, dtype = np.uint8).reshape(self.shape) 611 612 # increase frame_counter 613 self.frame_counter.frame_count += 1 614 615 # say to gc that this buffer is no longer needed 616 del buffer 617 618 if with_timestamps: 619 return FrameContainer(1,rgbImage,self.fps,current_elapsed_time) 620 621 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).
623 def read_batch(self, number_of_frames, with_timestamps = False) -> Union[np.array, FrameContainer]: 624 """ 625 Read next batch of images from the video 626 627 Parameters 628 ---------- 629 number_of_frames: int 630 Number of desired images within the batch. The last batch of the video may have less images. 631 632 with_timestamps: bool optional (default False) 633 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 634 635 Returns 636 ---------- 637 nparray or FrameContainer 638 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 639 the associated timestamps in ``FrameContainer.timestamps`` as an array (one element for each frame). 640 """ 641 642 if self.pipe is None: 643 raise self.VideoIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.videoProgram)) 644 # - pipe is in write mode 645 if self.mode != PipeMode.READ_MODE: 646 raise self.VideoIOException("Pipe to {} for '{}' not opened in read mode.".format(self.videoProgram, self.filename)) 647 648 if with_timestamps: 649 # get elapsed time in video, it is time of next frame(s) 650 current_elapsed_time = self.get_elapsed_time() 651 652 # try to read complete batch 653 buffer = self.pipe.stdout.read(self.imageSize*number_of_frames) 654 655 # check if we have at least 1 image 656 if len(buffer) < self.imageSize: 657 # ffmpag backend have been stopped or killed 658 # not considered as an error, no more frame, no exception 659 return None 660 661 # compute actual number of Frames 662 actualNbFrames = len(buffer)//self.imageSize 663 664 # get and reshape batch from buffer 665 batch = np.frombuffer(buffer, dtype = np.uint8).reshape((actualNbFrames, self.height, self.width, 3)) 666 667 # increase frame_counter 668 self.frame_counter.frame_count += actualNbFrames 669 670 # say to gc that this buffer is no longer needed 671 del buffer 672 673 if with_timestamps: 674 return FrameContainer(actualNbFrames, batch, self.fps, current_elapsed_time) 675 676 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).
678 def write_frame(self, image) -> bool: 679 """ 680 Write an image to the video 681 682 Parameters 683 ---------- 684 image: nparray 685 The image of shape (3, width, height) to write to the video file in the PixelFormat provided when create was called. 686 687 Returns 688 ---------- 689 bool 690 Writing was successful or not. 691 """ 692 693 # Check params 694 # - pipe exists 695 if self.pipe is None: 696 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 697 # - pipe is in write mode 698 if self.mode != PipeMode.WRITE_MODE: 699 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 700 # - shape of image is fine, thus we have pixels for a full compatible frame 701 if image.shape != self.shape: 702 raise self.VideoIOException("Wong image shape: {} expected {}.".format(image.shape,self.shape)) 703 # - type of data is UINT8 704 if image.dtype != np.uint8: 705 raise self.VideoIOException("Wong pixel type: {} expected np.uint8.".format(image.dtype)) 706 707 # write frame 708 buffer = image.tobytes() 709 if self.pipe.stdin.write( buffer ) < self.imageSize: 710 print( "Error writing frame to" ) 711 return False 712 713 # increase frame_counter 714 self.frame_counter.frame_count += 1 715 716 # say to gc that this buffer is no longer needed 717 del buffer 718 719 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.
721 def write_batch(self, batch) -> bool: 722 """ 723 Write a batch of images to the video 724 725 Parameters 726 ---------- 727 batch: nparray 728 A batch of images to write to the video file in the PixelFormat provided when create was called. 729 730 Returns 731 ---------- 732 bool 733 Writing was successful or not. 734 """ 735 736 # Check params 737 # - pipe exists 738 if self.pipe is None: 739 raise self.VideoIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.videoProgram)) 740 # - pipe is in write mode 741 if self.mode != PipeMode.WRITE_MODE: 742 raise self.VideoIOException("Pipe to {} for '{}' not opened in write mode.".format(self.videoProgram, self.filename)) 743 # - shape of images in batch is fine 744 if batch.shape[-3:] != self.shape: 745 raise self.VideoIOException("Wrong image shape in batch: {} expected {}.".format(batch.shape[-3:], self.shape)) 746 # - we have the right amount of pixels for the full batch 747 if batch.size != (batch.shape[0]*self.imageSize): 748 raise self.VideoIOException("Wrong number of pixels in batch: {} expected {}.".format(batch.shape[-3:], self.imageSize)) 749 750 # write frame 751 buffer = batch.tobytes() 752 if self.pipe.stdin.write( buffer ) < batch.size: 753 # say to gc that this buffer is no longer needed 754 del buffer 755 raise self.VideoIOException("Error writing batch to '{}'.".format(self.filename)) 756 757 # increase frame_counter 758 self.frame_counter.frame_count += batch.shape[0] 759 760 # say to gc that this buffer is no longer needed 761 del buffer 762 763 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.
34class AudioIO: 35 # "static" variables to ffmpeg, ffprobe executables 36 audioProgram, paramProgram = static_ffmpeg.run.get_or_fetch_platform_executables_else_raise() 37 38 class AudioIOException(Exception): 39 """ 40 Dedicated exception class for AudioIO class. 41 """ 42 def __init__(self, message="Error while reading/writing video occurs"): 43 self.message = message 44 super().__init__(self.message) 45 46 class AudioFormat(Enum): 47 """ 48 Enum class for supported input video type: 32-bit float is the only supported type for the moment. 49 """ 50 PCM32LE = 'pcm_f32le' # default format (unique mode for the moment) 51 52 @classmethod 53 def reader(cls, filename, *, loglevel = 16, debug = False, **kwargs): 54 """ 55 Create and open an AudioIO object in reader mode 56 57 See ``AudioIO.open`` for the full list of accepted parameters. 58 """ 59 reader = cls(logLevel=loglevel, debug=debug) 60 reader.open(filename, **kwargs) 61 return reader 62 63 @classmethod 64 def writer(cls, filename, sample_rate, channels, *, loglevel = 16, debug = False, **kwargs): 65 """ 66 Create and open an AudioIO object in writer mode 67 68 See ``AudioIO.create`` for the full list of accepted parameters. 69 """ 70 writer = cls(logLevel=loglevel, debug=debug) 71 writer.create(filename, sample_rate, channels, **kwargs) 72 return writer 73 74 # standard method 75 def get_corresponding_writer(self, filename, **kwargs): 76 """ 77 Method to get writer for an audio file with same sample_rate, channels as the current one AudioIO object 78 79 See `AudioIO.create` for the full list 80 of accepted parameters. 81 """ 82 return AudioIO.writer(filename, self.sample_rate, self.channels, **kwargs) 83 84 # To use with context manager "with AudioIO.reader(...) as f:' for instance 85 def __enter__(self): 86 """ 87 Method call at initialisation of a context manager like "with AudioIO.reader/writer(...) as f:' for instance 88 """ 89 # simply return myself 90 return self 91 92 def __exit__(self, exc_type, exc_val, exc_tb): 93 """ 94 Method call when existing of a context manager like "with AudioIO.reader/writer(...) as f:' for instance 95 """ 96 # close AudioIO 97 self.close() 98 return False 99 100 @staticmethod 101 def get_time_in_sec(filename, *, debug=False, logLevel=16): 102 """ 103 Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals). 104 105 Parameters 106 ---------- 107 filename : str or path. 108 Raw audio waveform as a 1D array. 109 110 debug : bool (default False). 111 Show debug info. 112 113 logLevel: int (default 16). 114 Log level to pass to the underlying ffmpeg/ffprobe command. 115 116 Returns 117 ---------- 118 float 119 Length in seconds of video file (including milliseconds as decimal part with 3 decimals) 120 """ 121 122 cmd = [AudioIO.paramProgram, # ffprobe 123 '-hide_banner', 124 '-loglevel', str(logLevel), 125 '-show_entries', 'format=duration', 126 '-of', 'default=noprint_wrappers=1:nokey=1', 127 str(filename) 128 ] 129 130 if debug == True: 131 print(' '.join(cmd)) 132 133 # call ffprobe and get params in one single line 134 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg 135 output = lpipe.stdout.readlines() 136 lpipe.terminate() 137 # transform Bytes output to one single string 138 output = ''.join( [element.decode('utf-8') for element in output]) 139 140 try: 141 return float(output) 142 except (ValueError, TypeError): 143 return None 144 145 @staticmethod 146 def get_params(filename, *, debug=False, logLevel=16): 147 """ 148 Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds. 149 150 Parameters 151 ---------- 152 filename : str or path. 153 Raw audio waveform as a 1D array. 154 155 debug : bool (default (False). 156 Show debug info. 157 158 log_level: int (default 16). 159 Log level to pass to the underlying ffmpeg/ffprobe command. 160 161 Returns 162 ---------- 163 tuple 164 Tuple containing (channels,sample_rate) of the file 165 """ 166 cmd = [AudioIO.paramProgram, # ffprobe 167 '-hide_banner', 168 '-loglevel', str(logLevel), 169 '-show_entries', 'stream=channels,sample_rate', 170 str(filename) 171 ] 172 173 if debug == True: 174 print(' '.join(cmd)) 175 176 # call ffprobe and get params in one single line 177 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg 178 output = lpipe.stdout.readlines() 179 lpipe.terminate() 180 # transform Bytes output to one single string 181 output = ''.join( [element.decode('utf-8') for element in output]) 182 183 pattern_sample_rate = r'sample_rate=(\d+)' 184 pattern_channels = r'channels=(\d+)' 185 186 # Search for values in the ffprobe output 187 match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE) 188 match_channels = re.search(pattern_channels, output, flags=re.MULTILINE) 189 190 # Extraction des valeurs 191 if match_sample_rate: 192 sample_rate = int(match_sample_rate.group(1)) 193 else: 194 raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'") 195 196 if match_channels: 197 channels = int(match_channels.group(1)) 198 else: 199 raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'") 200 201 return (channels,sample_rate) 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 debug: bool 211 """ debug flag for this object (print debut info, default False)""" 212 213 channels: int 214 """ Number of channels of images (default -1) """ 215 216 sample_rate: int 217 """ sample_rate of images (default -1) """ 218 219 plannar: bool 220 """ Read/write data as plannar, i.e. not interleaved (default True) """ 221 222 pipe: sp.Popen 223 """ pipe object to ffmpeg/ffprobe (default None)""" 224 225 frame_size: int 226 """ Weight in bytes of one image (default -1)""" 227 228 filename: str 229 """ Filename of the file (default None)""" 230 231 frame_counter: FrameCounter 232 """ `Framecounter` object to count ellapsed time (default None)""" 233 234 def __init__(self, *, logLevel = 16, debug = False): 235 """ 236 Create a AudioIO object giving ffmpeg/ffrobe loglevel and defining debug mode 237 238 Parameters 239 ---------- 240 log_level: int (default 16) 241 Log level to pass to the underlying ffmpeg/ffprobe command. 242 243 debug: bool (default (False) 244 Show debug info. while processing video 245 """ 246 247 self.mode = PipeMode.UNK_MODE 248 self.logLevel = logLevel 249 self.debug = debug 250 251 # Call init() method 252 self.init() 253 254 def init(self): 255 """ 256 Init or reinit a AudioIO object. 257 """ 258 self.channels = -1 259 self.sample_rate = -1 260 self.plannar = True 261 self.pipe = None 262 self.frame_size = -1 263 self.filename = None 264 self.frame_counter = None 265 self.float_size = None 266 267 _repr_exclude = {"pipe"} 268 """ List of excluded attribute for string conversion. """ 269 270 # converting the object to a string representation 271 def __repr__(self): 272 """ 273 Convert object (excluding attributes in _repr_exclude) to string representation. 274 """ 275 attrs = ", ".join( 276 f"{k}={v!r}" 277 for k, v in self.__dict__.items() 278 if k not in self._repr_exclude 279 ) 280 return f"{self.__class__.__name__}({attrs})" 281 282 __str__ = __repr__ 283 """ String representation """ 284 285 def get_elapsed_time_as_str(self) -> str: 286 """ 287 Method to get elapsed time (float value represented) as str. 288 289 Returns 290 ---------- 291 str or None 292 Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds 293 None if no frame counter are available. 294 """ 295 if self.frame_counter is None: 296 return None 297 return self.frame_counter.get_elapsed_time_as_str() 298 299 def get_formated_elapsed_time_as_str(self,show_ms=True) -> str: 300 """ 301 Method to get elapsed time (hour format) as str. 302 303 Returns 304 ---------- 305 str or None 306 Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds 307 None if no frame counter are available. 308 """ 309 if self.frame_counter is None: 310 return None 311 return self.frame_counter.get_formated_elapsed_time_as_str() 312 313 def get_elapsed_time(self) -> float: 314 """ 315 Method to get elapsed time as float value rounded to 3 decimals. 316 317 Returns 318 ---------- 319 float or None 320 Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds 321 None if no frame counter are available. 322 """ 323 if self.frame_counter is None: 324 return None 325 return self.frame_counter.get_elapsed_time() 326 327 def is_opened(self) -> bool: 328 """ 329 Method to get status of the underlying pipe to ffmpeg. 330 331 Returns 332 ---------- 333 bool 334 True if pipe is opened (reading or writing mode), False if not. 335 """ 336 # is the pip opened? 337 if self.pipe is not None and self.pipe.poll() is None: 338 return True 339 340 return False 341 342 def close(self): 343 """ 344 Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods. 345 """ 346 if self.pipe is not None: 347 if self.mode == PipeMode.WRITE_MODE: 348 # killing will make ffmpeg not finish properly the job, close the pipe 349 # to let it know that no more data are comming 350 self.pipe.stdin.close() 351 else: # self.mode == PipeMode.READ_MODE 352 # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows 353 self.pipe.kill() 354 355 # wait for subprocess to end 356 self.pipe.wait() 357 358 # reinit object for later use 359 self.init() 360 361 def create( self, filename, sample_rate, channels, *, writeOverExistingFile = False, 362 outputEncoding = AudioFormat.PCM32LE, encodingParams = None, plannar = True ): 363 """ 364 Method to create a audio file using parametrized access through ffmpeg. Importante note: calling create 365 on a AudioIO will close any former open video. 366 367 Parameters 368 ---------- 369 filename: str or path 370 filename of path to the file (mp4, avi, ...) 371 372 sample_rate: int 373 If defined as a positive value, sample_rates of the output file will be set to this value. 374 375 channels: int 376 If defined as a positive value, number of channels of output file will be set to this value. 377 378 fps: 379 If defined as a positive value, fps of input video will be set to this value. 380 381 outputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) 382 Define audio format for samples. Possible value is AudioFormat.PCM32LE. 383 384 encodingParams: str optional (default None) 385 Parameter to pass to ffmpeg to encode video like audio filters. 386 387 plannar : bool optionnal (default True) 388 Input data to write are grouped by channel if True, interleaved instead. 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.sample_rate = int(sample_rate) 401 self.channels = int(channels) 402 self.plannar = plannar 403 404 # Compute size of float 32, usefull if we want later to use other floating point convention 405 self.float_size = int(np.dtype(np.float32).itemsize) 406 407 # Check params 408 if self.sample_rate <= 0 or self.channels <= 0: 409 raise self.AudioIOException("Bad parameters: sample_rate={}, channels={}".format(self.sample_rate,self.channels)) 410 411 # To write audio, we do not need to know in advance frame size, we will write x values of n bytes 412 self.frame_size = None 413 414 # Video params are set, open the video 415 cmd = [self.audioProgram] # ffmpeg 416 417 if writeOverExistingFile == True: 418 cmd.extend(['-y']) 419 420 cmd.extend(['-hide_banner', 421 '-nostats', 422 '-loglevel', str(self.logLevel), 423 '-f', 'f32le', '-acodec', outputEncoding.value, # input expected coding 424 '-ar', f"{self.sample_rate}", 425 '-ac', f"{self.channels}", 426 '-i', '-']) 427 428 if encodingParams is not None: 429 cmd.extend(encodingParams.split()) 430 431 # Audio filename converted to str (for Path values) 432 cmd.extend( ['-vn', str(filename) ] ) 433 434 if self.debug == True: 435 print( ' '.join(cmd), file=sys.stderr ) 436 437 # store filename and set mode 438 self.filename = str(filename) 439 self.mode = PipeMode.WRITE_MODE 440 441 # call ffmpeg in write mode 442 try: 443 self.pipe = sp.Popen(cmd, stdin=sp.PIPE) 444 self.frame_counter = FrameCounter(self.sample_rate) 445 except Exception as e: 446 # if pipe failed, reinit object and raise exception 447 self.init() 448 raise 449 450 return True 451 452 def open( self, filename, *, sample_rate = -1, channels = -1, inputEncoding = AudioFormat.PCM32LE, 453 decodingParams = None, frame_size = 1.0, plannar = True, start_time = 0.0 ): 454 """ 455 Method to read (video file containing) audio using parametrized access through ffmpeg. Importante note: calling open 456 on a AudioIO will close any former open file. 457 458 Parameters 459 ---------- 460 filename: str or path 461 filename of path to the file (mp4, avi, ...) 462 463 sample_rate: int optional (default -1) 464 If defined as a positive value, sample rate of the input audio will be converted to this value. 465 466 channels: int optional (default -1) 467 If defined as a positive value, number of channels of the input audio will converted to this value. 468 469 inputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) 470 Define audio format for samples. Possible value is AudioFormat.PCM32LE. 471 472 decodingParams: str optional (default None) 473 Parameter to pass to ffmpeg to decode video like audio filters. 474 475 plannar: bool optionnal (default True) 476 Group audio samples per channel if True. Else, samples are interleaved. 477 478 frame_size: int or float (default 1.0) 479 If frame_size is an int, it is the number of expected samples in each frame, for instance 8000 for 8000 samples. 480 if frame_size is a float, it is considered as a time in seconds for each audio frame, for instance 1.0 for 1 second, 0.010 for 10 ms. 481 Number of samples in this case is computed using frame_size and sample_rate as int(frame_size * sample_rate) 482 483 start_time: float optional (default 0.0) 484 Define the reading start time. If not set, reading at beginning of the file. 485 486 Returns 487 ---------- 488 bool 489 Was the opening successfull 490 """ 491 492 # Close if already opened 493 self.close() 494 495 # Force conversion of parameters 496 channels = int(channels) 497 sample_rate = float(sample_rate) 498 499 self.plannar = plannar 500 501 # Compute size of float 32, usefull if we want later to use other floating point convention 502 self.float_size = int(np.dtype(np.float32).itemsize) 503 504 # get parameters from file if needed: 505 if sample_rate <= 0 or channels <= 0: 506 self.channels, self.sample_rate = self.getAudioParams(filename) 507 508 # check if parameters ask to overide video parameters 509 if channels > 0: 510 self.channels = channels 511 if sample_rate > 0: 512 self.sample_rate = sample_rate 513 514 # check parameters 515 516 if isinstance(frame_size,float): 517 # time in seconds 518 self.frame_size = int(frame_size*self.sample_rate) 519 elif isinstance(frame_size,int): 520 # number of samples 521 self.frame_size = frame_size 522 else: 523 # to do 524 pass 525 526 # Video params are set, open the video 527 cmd = [self.audioProgram, # ffmpeg 528 '-hide_banner', 529 '-nostats', 530 '-loglevel', str(self.logLevel)] 531 532 if decodingParams is not None: 533 cmd.extend([decodingParams.split()]) 534 535 if start_time < 0.0: 536 pass 537 elif start_time > 0.0: 538 cmd.extend(["-ss", f"{start_time}"]) 539 540 cmd.extend( ['-i', str(filename), 541 '-f', 'f32le', '-acodec', inputEncoding.value, # input expected coding 542 '-ar', f"{self.sample_rate}", 543 '-ac', f"{self.channels}", 544 '-' # output to stdout 545 ] 546 ) 547 548 if self.debug == True: 549 print( ' '.join(cmd) ) 550 551 # store filename and set mode to READ_MODE 552 self.filename = str(filename) 553 self.mode = PipeMode.READ_MODE 554 555 # call ffmpeg in read mode 556 try: 557 self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 558 self.frame_counter = FrameCounter(self.sample_rate) 559 if start_time > 0.0: 560 self.frame_counter += start_time # adding with float means adding time 561 except Exception as e: 562 # if pipe failed, reinit object and raise exception 563 self.init() 564 raise 565 566 return True 567 568 def read_frame(self, with_timestamps = False): 569 """ 570 Read next frame from the audio file 571 572 Parameters 573 ---------- 574 with_timestamps: bool optional (default False) 575 If set to True, the method returns a ``FrameContainer`` with the audio and an array containing the associated timestamp(s) 576 577 Returns 578 ---------- 579 nparray or FrameContainer 580 A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame 581 of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False. 582 if with_timestamps is True, the return object is a FrameContainer with the audio data in ``FrameContainer.data`` and 583 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element). 584 """ 585 586 if self.pipe is None: 587 raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.audioProgram)) 588 # - pipe is in write mode 589 if self.mode != PipeMode.READ_MODE: 590 raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename)) 591 592 if with_timestamps: 593 # get elapsed time in video, it is time of next frame(s) 594 current_elapsed_time = self.get_elapsed_time() 595 596 # read rgb image from pipe 597 toread = self.frame_size*self.float_size 598 buffer = self.pipe.stdout.read(toread) 599 600 if buffer == b"": 601 # not considered as an error, no more frame, no exception 602 return None 603 604 # get numpy UINT8 array from buffer 605 audio = np.frombuffer(buffer, dtype = np.float32).reshape(len(buffer)//self.float_size, self.channels) 606 607 # make it plannar (or not) 608 if self.plannar: 609 #transpose it 610 audio = audio.T 611 612 # increase frame_counter 613 self.frame_counter.frame_count += (self.frame_size * self.channels) 614 615 # say to gc that this buffer is no longer needed 616 del buffer 617 618 if with_timestamps: 619 return FrameContainer(1, audio, self.frame_size/self.sample_rate, current_elapsed_time) 620 621 return audio 622 623 def read_batch(self, numberOfFrames, with_timestamps = False): 624 """ 625 Read next batch of audio from the file 626 627 Parameters 628 ---------- 629 number_of_frames: int 630 Number of desired images within the batch. The last batch from the file may have less images. 631 632 with_timestamps: bool optional (default False) 633 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 634 635 Returns 636 ---------- 637 nparray or FrameContainer 638 A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch 639 of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False. 640 if with_timestamps is True, the return object is a FrameContainer with the audio batch in ``FrameContainer.data`` and 641 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for each audio frame). 642 """ 643 644 if self.pipe is None: 645 raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.audioProgram)) 646 # - pipe is in write mode 647 if self.mode != PipeMode.READ_MODE: 648 raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename)) 649 650 if with_timestamps: 651 # get elapsed time in video, it is time of next frame(s) 652 current_elapsed_time = self.get_elapsed_time() 653 654 # try to read complete batch 655 toread = self.frame_size*self.float_size*self.channels*numberOfFrames 656 buffer = self.pipe.stdout.read(toread) 657 658 # check if we are at the end of the buffer 659 if buffer == b"": 660 # not considered as an error, no more frame, no exception 661 return None 662 663 # compute actual number of Frames 664 # do we have a full batch ? Computer standard division 665 actualNbFrames = len(buffer)/(self.frame_size*self.float_size*self.channels) 666 667 if actualNbFrames.is_integer(): 668 actualNbFrames = int(actualNbFrames) 669 # get and reshape batch from buffer 670 batch = np.frombuffer(buffer, dtype = np.float32).reshape((actualNbFrames, self.frame_size, self.channels,)) 671 else: 672 # We do not have a full batch, we are at end of the stream or ffmpeg has been stopped/killed 673 # Compute frame size in samples knowing len of buffer, self.float_size, self.channels and numberOfFrames 674 l_frame_size = len(buffer)//(numberOfFrames*self.float_size*self.channels) 675 if l_frame_size <= 0: 676 # not considered as an error, no more frame, no exception 677 return None 678 # get and reshape batch from buffer 679 batch = np.frombuffer(buffer, dtype = np.float32).reshape((numberOfFrames, l_frame_size, self.channels,)) 680 681 if self.plannar: 682 batch = batch.transpose(0, 2, 1) 683 684 # increase frame_counter 685 self.frame_counter.frame_count += (actualNbFrames * self.frame_size * self.channels) 686 687 # say to gc that this buffer is no longer needed 688 del buffer 689 690 if with_timestamps: 691 return FrameContainer( actualNbFrames, batch, self.frame_size/self.sample_rate, current_elapsed_time) 692 693 return batch 694 695 def write_frame(self, audio) -> bool: 696 """ 697 Write an audio frame to the file 698 699 Parameters 700 ---------- 701 audio: nparray 702 The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel). 703 704 Returns 705 ---------- 706 bool 707 Writing was successful or not. 708 """ 709 # Check params 710 # - pipe exists 711 if self.pipe is None: 712 raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram)) 713 # - pipe is in write mode 714 if self.mode != PipeMode.WRITE_MODE: 715 raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename)) 716 # - shape of image is fine, thus we have pixels for a full compatible frame 717 if audio.shape[0] != self.channels: 718 raise self.AudioIOException("Wong audio shape: {} expected ({}, nb_samples_to_write).".format(audio.shape,self.channels)) 719 # - type of data is Float32 720 if audio.dtype != np.float32: 721 raise self.AudioIOException("Wong audio type: {} expected np.float32.".format(audio.dtype)) 722 723 # array must have a shape (channels, samples), reshape it it to (samples, channels) if plannar 724 if not self.plannar: 725 audio = audio.reshape(-1) 726 727 # print( audio.shape ) 728 729 # garantee to have a C continuous array 730 if not audio.flags['C_CONTIGUOUS']: 731 a = np.ascontiguousarray(a) 732 733 # write frame 734 buffer = audio.tobytes() 735 if self.pipe.stdin.write( buffer ) < len(buffer): 736 print( f"Error writing frame to {self.filename}" ) 737 return False 738 739 # increase frame_counter 740 self.frame_counter.frame_count += (audio.shape[1] * self.channels) 741 742 # say to gc that this buffer is no longer needed 743 del buffer 744 745 return True 746 747 def write_batch(self, batch): 748 """ 749 Write a batch of audio frame to the file 750 751 Parameters 752 ---------- 753 batch: nparray 754 The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data. 755 756 Returns 757 ---------- 758 bool 759 Writing was successful or not. 760 """ 761 # Check params 762 # - pipe exists 763 if self.pipe is None: 764 raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram)) 765 # - pipe is in write mode 766 if self.mode != PipeMode.WRITE_MODE: 767 raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename)) 768 # batch is 3D (n, channels, nb samples) 769 if batch.ndim !=3: 770 raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape)) 771 # - shape of images in batch is fine 772 if batch.shape[2] != self.channels: 773 raise self.AudioIOException("Wrong audio channels in batch: {} expected {} {}.".format(batch.shape[2], self.channels, batch.shape)) 774 775 # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe 776 # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False 777 if not self.plannar: 778 # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel) 779 batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels) 780 batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel) 781 782 # garantee to have a C continuous array 783 if not batch.flags['C_CONTIGUOUS']: 784 batch = np.ascontiguousarray(batch) 785 786 # write frame 787 buffer = batch.tobytes() 788 if self.pipe.stdin.write( buffer ) < len(buffer): 789 # say to gc that this buffer is no longer needed 790 del buffer 791 raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename)) 792 793 # increase frame_counter 794 self.frame_counter.frame_count += int(batch.shape[0]/self.channels) # int conversion is mandatory to avoid confusion with time as float 795 796 # say to gc that this buffer is no longer needed 797 del buffer 798 799 return True 800 801 def iter_frames(self, with_timestamps = False): 802 """ 803 Method to iterate on audio frames using AudioIO obj. 804 for audio_frame in obj.iter_frames(): 805 .... 806 807 Parameters 808 ---------- 809 with_timestamps: bool optional (default False) 810 If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames 811 812 Returns 813 ---------- 814 nparray or FrameContainer 815 A batch of images of shape () 816 """ 817 818 try: 819 if self.mode == PipeMode.READ_MODE: 820 while self.isOpened(): 821 frame = self.readFrame(with_timestamps) 822 if frame is not None: 823 yield frame 824 finally: 825 self.close() 826 827 def iter_batches(self, batch_size : int, with_timestamps = False ): 828 """ 829 Method to iterate on batch ofaudio frames using AudioIO obj. 830 for audio_batch in obj.iter_batches(): 831 .... 832 833 Parameters 834 ---------- 835 with_timestamps: bool optional (default False) 836 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 837 """ 838 try: 839 if self.mode == PipeMode.READ_MODE: 840 while self.isOpened(): 841 batch = self.readBatch(batch_size, with_timestamps) 842 if batch is not None: 843 yield batch 844 finally: 845 self.close() 846 847 # function aliases to be compliant with original C++ version 848 getAudioTimeInSec = get_time_in_sec 849 getAudioParams = get_params 850 get_audio_time_in_sec = get_time_in_sec 851 get_audio_params = get_params 852 isOpened = is_opened 853 readFrame = read_frame 854 readBatch = read_batch 855 writeFrame = write_frame 856 writeBatch = write_batch
234 def __init__(self, *, logLevel = 16, debug = False): 235 """ 236 Create a AudioIO object giving ffmpeg/ffrobe loglevel and defining debug mode 237 238 Parameters 239 ---------- 240 log_level: int (default 16) 241 Log level to pass to the underlying ffmpeg/ffprobe command. 242 243 debug: bool (default (False) 244 Show debug info. while processing video 245 """ 246 247 self.mode = PipeMode.UNK_MODE 248 self.logLevel = logLevel 249 self.debug = debug 250 251 # Call init() method 252 self.init()
Create a AudioIO 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.
debug: bool (default (False) Show debug info. while processing video
52 @classmethod 53 def reader(cls, filename, *, loglevel = 16, debug = False, **kwargs): 54 """ 55 Create and open an AudioIO object in reader mode 56 57 See ``AudioIO.open`` for the full list of accepted parameters. 58 """ 59 reader = cls(logLevel=loglevel, debug=debug) 60 reader.open(filename, **kwargs) 61 return reader
Create and open an AudioIO object in reader mode
See AudioIO.open for the full list of accepted parameters.
63 @classmethod 64 def writer(cls, filename, sample_rate, channels, *, loglevel = 16, debug = False, **kwargs): 65 """ 66 Create and open an AudioIO object in writer mode 67 68 See ``AudioIO.create`` for the full list of accepted parameters. 69 """ 70 writer = cls(logLevel=loglevel, debug=debug) 71 writer.create(filename, sample_rate, channels, **kwargs) 72 return writer
Create and open an AudioIO object in writer mode
See AudioIO.create for the full list of accepted parameters.
75 def get_corresponding_writer(self, filename, **kwargs): 76 """ 77 Method to get writer for an audio file with same sample_rate, channels as the current one AudioIO object 78 79 See `AudioIO.create` for the full list 80 of accepted parameters. 81 """ 82 return AudioIO.writer(filename, self.sample_rate, self.channels, **kwargs)
Method to get writer for an audio file with same sample_rate, channels as the current one AudioIO object
See AudioIO.create for the full list
of accepted parameters.
100 @staticmethod 101 def get_time_in_sec(filename, *, debug=False, logLevel=16): 102 """ 103 Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals). 104 105 Parameters 106 ---------- 107 filename : str or path. 108 Raw audio waveform as a 1D array. 109 110 debug : bool (default False). 111 Show debug info. 112 113 logLevel: int (default 16). 114 Log level to pass to the underlying ffmpeg/ffprobe command. 115 116 Returns 117 ---------- 118 float 119 Length in seconds of video file (including milliseconds as decimal part with 3 decimals) 120 """ 121 122 cmd = [AudioIO.paramProgram, # ffprobe 123 '-hide_banner', 124 '-loglevel', str(logLevel), 125 '-show_entries', 'format=duration', 126 '-of', 'default=noprint_wrappers=1:nokey=1', 127 str(filename) 128 ] 129 130 if debug == True: 131 print(' '.join(cmd)) 132 133 # call ffprobe and get params in one single line 134 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg 135 output = lpipe.stdout.readlines() 136 lpipe.terminate() 137 # transform Bytes output to one single string 138 output = ''.join( [element.decode('utf-8') for element in output]) 139 140 try: 141 return float(output) 142 except (ValueError, TypeError): 143 return None
Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).
Parameters
filename : str or path. Raw audio waveform as a 1D array.
debug : bool (default False). Show debug info.
logLevel: 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 with 3 decimals)
145 @staticmethod 146 def get_params(filename, *, debug=False, logLevel=16): 147 """ 148 Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds. 149 150 Parameters 151 ---------- 152 filename : str or path. 153 Raw audio waveform as a 1D array. 154 155 debug : bool (default (False). 156 Show debug info. 157 158 log_level: int (default 16). 159 Log level to pass to the underlying ffmpeg/ffprobe command. 160 161 Returns 162 ---------- 163 tuple 164 Tuple containing (channels,sample_rate) of the file 165 """ 166 cmd = [AudioIO.paramProgram, # ffprobe 167 '-hide_banner', 168 '-loglevel', str(logLevel), 169 '-show_entries', 'stream=channels,sample_rate', 170 str(filename) 171 ] 172 173 if debug == True: 174 print(' '.join(cmd)) 175 176 # call ffprobe and get params in one single line 177 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg 178 output = lpipe.stdout.readlines() 179 lpipe.terminate() 180 # transform Bytes output to one single string 181 output = ''.join( [element.decode('utf-8') for element in output]) 182 183 pattern_sample_rate = r'sample_rate=(\d+)' 184 pattern_channels = r'channels=(\d+)' 185 186 # Search for values in the ffprobe output 187 match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE) 188 match_channels = re.search(pattern_channels, output, flags=re.MULTILINE) 189 190 # Extraction des valeurs 191 if match_sample_rate: 192 sample_rate = int(match_sample_rate.group(1)) 193 else: 194 raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'") 195 196 if match_channels: 197 channels = int(match_channels.group(1)) 198 else: 199 raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'") 200 201 return (channels,sample_rate) 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 debug: bool 211 """ debug flag for this object (print debut info, default False)""" 212 213 channels: int 214 """ Number of channels of images (default -1) """ 215 216 sample_rate: int 217 """ sample_rate of images (default -1) """ 218 219 plannar: bool 220 """ Read/write data as plannar, i.e. not interleaved (default True) """ 221 222 pipe: sp.Popen 223 """ pipe object to ffmpeg/ffprobe (default None)""" 224 225 frame_size: int 226 """ Weight in bytes of one image (default -1)""" 227 228 filename: str 229 """ Filename of the file (default None)""" 230 231 frame_counter: FrameCounter 232 """ `Framecounter` object to count ellapsed time (default None)"""
Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.
Parameters
filename : str or path. Raw audio waveform as a 1D array.
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 (channels,sample_rate) of the file
254 def init(self): 255 """ 256 Init or reinit a AudioIO object. 257 """ 258 self.channels = -1 259 self.sample_rate = -1 260 self.plannar = True 261 self.pipe = None 262 self.frame_size = -1 263 self.filename = None 264 self.frame_counter = None 265 self.float_size = None
Init or reinit a AudioIO object.
285 def get_elapsed_time_as_str(self) -> str: 286 """ 287 Method to get elapsed time (float value represented) as str. 288 289 Returns 290 ---------- 291 str or None 292 Elapsed time (float value) as str, "15.500" for instance for 15 secondes and 500 milliseconds 293 None if no frame counter are available. 294 """ 295 if self.frame_counter is None: 296 return None 297 return self.frame_counter.get_elapsed_time_as_str()
Method to get elapsed time (float value represented) as str.
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.
299 def get_formated_elapsed_time_as_str(self,show_ms=True) -> str: 300 """ 301 Method to get elapsed time (hour format) as str. 302 303 Returns 304 ---------- 305 str or None 306 Elapsed time (float value) as str, "00:00:15.500" for instance for 15 secondes and 500 milliseconds 307 None if no frame counter are available. 308 """ 309 if self.frame_counter is None: 310 return None 311 return self.frame_counter.get_formated_elapsed_time_as_str()
Method to get elapsed time (hour format) as str.
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.
313 def get_elapsed_time(self) -> float: 314 """ 315 Method to get elapsed time as float value rounded to 3 decimals. 316 317 Returns 318 ---------- 319 float or None 320 Elapsed time (float value) as str, 15.500 for instance for 15 secondes and 500 milliseconds 321 None if no frame counter are available. 322 """ 323 if self.frame_counter is None: 324 return None 325 return self.frame_counter.get_elapsed_time()
Method to get elapsed time as float value rounded to 3 decimals.
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.
327 def is_opened(self) -> bool: 328 """ 329 Method to get status of the underlying pipe to ffmpeg. 330 331 Returns 332 ---------- 333 bool 334 True if pipe is opened (reading or writing mode), False if not. 335 """ 336 # is the pip opened? 337 if self.pipe is not None and self.pipe.poll() is None: 338 return True 339 340 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.
342 def close(self): 343 """ 344 Method to close current pipe to ffmpeg (if any). Ffmpeg/ffprobe will be terminated. Object can be reused using open or create methods. 345 """ 346 if self.pipe is not None: 347 if self.mode == PipeMode.WRITE_MODE: 348 # killing will make ffmpeg not finish properly the job, close the pipe 349 # to let it know that no more data are comming 350 self.pipe.stdin.close() 351 else: # self.mode == PipeMode.READ_MODE 352 # in read mode, no need to be nice, send SIGTERM on Linux,/Kill it on windows 353 self.pipe.kill() 354 355 # wait for subprocess to end 356 self.pipe.wait() 357 358 # reinit object for later use 359 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.
361 def create( self, filename, sample_rate, channels, *, writeOverExistingFile = False, 362 outputEncoding = AudioFormat.PCM32LE, encodingParams = None, plannar = True ): 363 """ 364 Method to create a audio file using parametrized access through ffmpeg. Importante note: calling create 365 on a AudioIO will close any former open video. 366 367 Parameters 368 ---------- 369 filename: str or path 370 filename of path to the file (mp4, avi, ...) 371 372 sample_rate: int 373 If defined as a positive value, sample_rates of the output file will be set to this value. 374 375 channels: int 376 If defined as a positive value, number of channels of output file will be set to this value. 377 378 fps: 379 If defined as a positive value, fps of input video will be set to this value. 380 381 outputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) 382 Define audio format for samples. Possible value is AudioFormat.PCM32LE. 383 384 encodingParams: str optional (default None) 385 Parameter to pass to ffmpeg to encode video like audio filters. 386 387 plannar : bool optionnal (default True) 388 Input data to write are grouped by channel if True, interleaved instead. 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.sample_rate = int(sample_rate) 401 self.channels = int(channels) 402 self.plannar = plannar 403 404 # Compute size of float 32, usefull if we want later to use other floating point convention 405 self.float_size = int(np.dtype(np.float32).itemsize) 406 407 # Check params 408 if self.sample_rate <= 0 or self.channels <= 0: 409 raise self.AudioIOException("Bad parameters: sample_rate={}, channels={}".format(self.sample_rate,self.channels)) 410 411 # To write audio, we do not need to know in advance frame size, we will write x values of n bytes 412 self.frame_size = None 413 414 # Video params are set, open the video 415 cmd = [self.audioProgram] # ffmpeg 416 417 if writeOverExistingFile == True: 418 cmd.extend(['-y']) 419 420 cmd.extend(['-hide_banner', 421 '-nostats', 422 '-loglevel', str(self.logLevel), 423 '-f', 'f32le', '-acodec', outputEncoding.value, # input expected coding 424 '-ar', f"{self.sample_rate}", 425 '-ac', f"{self.channels}", 426 '-i', '-']) 427 428 if encodingParams is not None: 429 cmd.extend(encodingParams.split()) 430 431 # Audio filename converted to str (for Path values) 432 cmd.extend( ['-vn', str(filename) ] ) 433 434 if self.debug == True: 435 print( ' '.join(cmd), file=sys.stderr ) 436 437 # store filename and set mode 438 self.filename = str(filename) 439 self.mode = PipeMode.WRITE_MODE 440 441 # call ffmpeg in write mode 442 try: 443 self.pipe = sp.Popen(cmd, stdin=sp.PIPE) 444 self.frame_counter = FrameCounter(self.sample_rate) 445 except Exception as e: 446 # if pipe failed, reinit object and raise exception 447 self.init() 448 raise 449 450 return True
Method to create a audio file using parametrized access through ffmpeg. Importante note: calling create on a AudioIO will close any former open video.
Parameters
filename: str or path filename of path to the file (mp4, avi, ...)
sample_rate: int If defined as a positive value, sample_rates of the output file will be set to this value.
channels: int If defined as a positive value, number of channels of output file will be set to this value.
fps: If defined as a positive value, fps of input video will be set to this value.
outputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) Define audio format for samples. Possible value is AudioFormat.PCM32LE.
encodingParams: str optional (default None) Parameter to pass to ffmpeg to encode video like audio filters.
plannar : bool optionnal (default True) Input data to write are grouped by channel if True, interleaved instead.
Returns
bool Was the creation successfull
452 def open( self, filename, *, sample_rate = -1, channels = -1, inputEncoding = AudioFormat.PCM32LE, 453 decodingParams = None, frame_size = 1.0, plannar = True, start_time = 0.0 ): 454 """ 455 Method to read (video file containing) audio using parametrized access through ffmpeg. Importante note: calling open 456 on a AudioIO will close any former open file. 457 458 Parameters 459 ---------- 460 filename: str or path 461 filename of path to the file (mp4, avi, ...) 462 463 sample_rate: int optional (default -1) 464 If defined as a positive value, sample rate of the input audio will be converted to this value. 465 466 channels: int optional (default -1) 467 If defined as a positive value, number of channels of the input audio will converted to this value. 468 469 inputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) 470 Define audio format for samples. Possible value is AudioFormat.PCM32LE. 471 472 decodingParams: str optional (default None) 473 Parameter to pass to ffmpeg to decode video like audio filters. 474 475 plannar: bool optionnal (default True) 476 Group audio samples per channel if True. Else, samples are interleaved. 477 478 frame_size: int or float (default 1.0) 479 If frame_size is an int, it is the number of expected samples in each frame, for instance 8000 for 8000 samples. 480 if frame_size is a float, it is considered as a time in seconds for each audio frame, for instance 1.0 for 1 second, 0.010 for 10 ms. 481 Number of samples in this case is computed using frame_size and sample_rate as int(frame_size * sample_rate) 482 483 start_time: float optional (default 0.0) 484 Define the reading start time. If not set, reading at beginning of the file. 485 486 Returns 487 ---------- 488 bool 489 Was the opening successfull 490 """ 491 492 # Close if already opened 493 self.close() 494 495 # Force conversion of parameters 496 channels = int(channels) 497 sample_rate = float(sample_rate) 498 499 self.plannar = plannar 500 501 # Compute size of float 32, usefull if we want later to use other floating point convention 502 self.float_size = int(np.dtype(np.float32).itemsize) 503 504 # get parameters from file if needed: 505 if sample_rate <= 0 or channels <= 0: 506 self.channels, self.sample_rate = self.getAudioParams(filename) 507 508 # check if parameters ask to overide video parameters 509 if channels > 0: 510 self.channels = channels 511 if sample_rate > 0: 512 self.sample_rate = sample_rate 513 514 # check parameters 515 516 if isinstance(frame_size,float): 517 # time in seconds 518 self.frame_size = int(frame_size*self.sample_rate) 519 elif isinstance(frame_size,int): 520 # number of samples 521 self.frame_size = frame_size 522 else: 523 # to do 524 pass 525 526 # Video params are set, open the video 527 cmd = [self.audioProgram, # ffmpeg 528 '-hide_banner', 529 '-nostats', 530 '-loglevel', str(self.logLevel)] 531 532 if decodingParams is not None: 533 cmd.extend([decodingParams.split()]) 534 535 if start_time < 0.0: 536 pass 537 elif start_time > 0.0: 538 cmd.extend(["-ss", f"{start_time}"]) 539 540 cmd.extend( ['-i', str(filename), 541 '-f', 'f32le', '-acodec', inputEncoding.value, # input expected coding 542 '-ar', f"{self.sample_rate}", 543 '-ac', f"{self.channels}", 544 '-' # output to stdout 545 ] 546 ) 547 548 if self.debug == True: 549 print( ' '.join(cmd) ) 550 551 # store filename and set mode to READ_MODE 552 self.filename = str(filename) 553 self.mode = PipeMode.READ_MODE 554 555 # call ffmpeg in read mode 556 try: 557 self.pipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg/ffprobe 558 self.frame_counter = FrameCounter(self.sample_rate) 559 if start_time > 0.0: 560 self.frame_counter += start_time # adding with float means adding time 561 except Exception as e: 562 # if pipe failed, reinit object and raise exception 563 self.init() 564 raise 565 566 return True
Method to read (video file containing) audio using parametrized access through ffmpeg. Importante note: calling open on a AudioIO will close any former open file.
Parameters
filename: str or path filename of path to the file (mp4, avi, ...)
sample_rate: int optional (default -1) If defined as a positive value, sample rate of the input audio will be converted to this value.
channels: int optional (default -1) If defined as a positive value, number of channels of the input audio will converted to this value.
inputEncoding: AudioFormat optional (default AudioFormat.PCM32LE) Define audio format for samples. Possible value is AudioFormat.PCM32LE.
decodingParams: str optional (default None) Parameter to pass to ffmpeg to decode video like audio filters.
plannar: bool optionnal (default True) Group audio samples per channel if True. Else, samples are interleaved.
frame_size: int or float (default 1.0) If frame_size is an int, it is the number of expected samples in each frame, for instance 8000 for 8000 samples. if frame_size is a float, it is considered as a time in seconds for each audio frame, for instance 1.0 for 1 second, 0.010 for 10 ms. Number of samples in this case is computed using frame_size and sample_rate as int(frame_size * sample_rate)
start_time: float optional (default 0.0) Define the reading start time. If not set, reading at beginning of the file.
Returns
bool Was the opening successfull
568 def read_frame(self, with_timestamps = False): 569 """ 570 Read next frame from the audio file 571 572 Parameters 573 ---------- 574 with_timestamps: bool optional (default False) 575 If set to True, the method returns a ``FrameContainer`` with the audio and an array containing the associated timestamp(s) 576 577 Returns 578 ---------- 579 nparray or FrameContainer 580 A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame 581 of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False. 582 if with_timestamps is True, the return object is a FrameContainer with the audio data in ``FrameContainer.data`` and 583 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element). 584 """ 585 586 if self.pipe is None: 587 raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.audioProgram)) 588 # - pipe is in write mode 589 if self.mode != PipeMode.READ_MODE: 590 raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename)) 591 592 if with_timestamps: 593 # get elapsed time in video, it is time of next frame(s) 594 current_elapsed_time = self.get_elapsed_time() 595 596 # read rgb image from pipe 597 toread = self.frame_size*self.float_size 598 buffer = self.pipe.stdout.read(toread) 599 600 if buffer == b"": 601 # not considered as an error, no more frame, no exception 602 return None 603 604 # get numpy UINT8 array from buffer 605 audio = np.frombuffer(buffer, dtype = np.float32).reshape(len(buffer)//self.float_size, self.channels) 606 607 # make it plannar (or not) 608 if self.plannar: 609 #transpose it 610 audio = audio.T 611 612 # increase frame_counter 613 self.frame_counter.frame_count += (self.frame_size * self.channels) 614 615 # say to gc that this buffer is no longer needed 616 del buffer 617 618 if with_timestamps: 619 return FrameContainer(1, audio, self.frame_size/self.sample_rate, current_elapsed_time) 620 621 return audio
Read next frame from the audio file
Parameters
with_timestamps: bool optional (default False)
If set to True, the method returns a FrameContainer with the audio and an array containing the associated timestamp(s)
Returns
nparray or FrameContainer
A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame
of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False.
if with_timestamps is True, the return object is a FrameContainer with the audio data in FrameContainer.data and
the associated timestamp in FrameContainer.timestamps as an array (one element).
623 def read_batch(self, numberOfFrames, with_timestamps = False): 624 """ 625 Read next batch of audio from the file 626 627 Parameters 628 ---------- 629 number_of_frames: int 630 Number of desired images within the batch. The last batch from the file may have less images. 631 632 with_timestamps: bool optional (default False) 633 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 634 635 Returns 636 ---------- 637 nparray or FrameContainer 638 A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch 639 of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False. 640 if with_timestamps is True, the return object is a FrameContainer with the audio batch in ``FrameContainer.data`` and 641 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for each audio frame). 642 """ 643 644 if self.pipe is None: 645 raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.audioProgram)) 646 # - pipe is in write mode 647 if self.mode != PipeMode.READ_MODE: 648 raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename)) 649 650 if with_timestamps: 651 # get elapsed time in video, it is time of next frame(s) 652 current_elapsed_time = self.get_elapsed_time() 653 654 # try to read complete batch 655 toread = self.frame_size*self.float_size*self.channels*numberOfFrames 656 buffer = self.pipe.stdout.read(toread) 657 658 # check if we are at the end of the buffer 659 if buffer == b"": 660 # not considered as an error, no more frame, no exception 661 return None 662 663 # compute actual number of Frames 664 # do we have a full batch ? Computer standard division 665 actualNbFrames = len(buffer)/(self.frame_size*self.float_size*self.channels) 666 667 if actualNbFrames.is_integer(): 668 actualNbFrames = int(actualNbFrames) 669 # get and reshape batch from buffer 670 batch = np.frombuffer(buffer, dtype = np.float32).reshape((actualNbFrames, self.frame_size, self.channels,)) 671 else: 672 # We do not have a full batch, we are at end of the stream or ffmpeg has been stopped/killed 673 # Compute frame size in samples knowing len of buffer, self.float_size, self.channels and numberOfFrames 674 l_frame_size = len(buffer)//(numberOfFrames*self.float_size*self.channels) 675 if l_frame_size <= 0: 676 # not considered as an error, no more frame, no exception 677 return None 678 # get and reshape batch from buffer 679 batch = np.frombuffer(buffer, dtype = np.float32).reshape((numberOfFrames, l_frame_size, self.channels,)) 680 681 if self.plannar: 682 batch = batch.transpose(0, 2, 1) 683 684 # increase frame_counter 685 self.frame_counter.frame_count += (actualNbFrames * self.frame_size * self.channels) 686 687 # say to gc that this buffer is no longer needed 688 del buffer 689 690 if with_timestamps: 691 return FrameContainer( actualNbFrames, batch, self.frame_size/self.sample_rate, current_elapsed_time) 692 693 return batch
Read next batch of audio from the file
Parameters
number_of_frames: int Number of desired images within the batch. The last batch from the file 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 shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch
of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False.
if with_timestamps is True, the return object is a FrameContainer with the audio batch in FrameContainer.data and
the associated timestamp in FrameContainer.timestamps as an array (one element for each audio frame).
695 def write_frame(self, audio) -> bool: 696 """ 697 Write an audio frame to the file 698 699 Parameters 700 ---------- 701 audio: nparray 702 The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel). 703 704 Returns 705 ---------- 706 bool 707 Writing was successful or not. 708 """ 709 # Check params 710 # - pipe exists 711 if self.pipe is None: 712 raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram)) 713 # - pipe is in write mode 714 if self.mode != PipeMode.WRITE_MODE: 715 raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename)) 716 # - shape of image is fine, thus we have pixels for a full compatible frame 717 if audio.shape[0] != self.channels: 718 raise self.AudioIOException("Wong audio shape: {} expected ({}, nb_samples_to_write).".format(audio.shape,self.channels)) 719 # - type of data is Float32 720 if audio.dtype != np.float32: 721 raise self.AudioIOException("Wong audio type: {} expected np.float32.".format(audio.dtype)) 722 723 # array must have a shape (channels, samples), reshape it it to (samples, channels) if plannar 724 if not self.plannar: 725 audio = audio.reshape(-1) 726 727 # print( audio.shape ) 728 729 # garantee to have a C continuous array 730 if not audio.flags['C_CONTIGUOUS']: 731 a = np.ascontiguousarray(a) 732 733 # write frame 734 buffer = audio.tobytes() 735 if self.pipe.stdin.write( buffer ) < len(buffer): 736 print( f"Error writing frame to {self.filename}" ) 737 return False 738 739 # increase frame_counter 740 self.frame_counter.frame_count += (audio.shape[1] * self.channels) 741 742 # say to gc that this buffer is no longer needed 743 del buffer 744 745 return True
Write an audio frame to the file
Parameters
audio: nparray The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel).
Returns
bool Writing was successful or not.
747 def write_batch(self, batch): 748 """ 749 Write a batch of audio frame to the file 750 751 Parameters 752 ---------- 753 batch: nparray 754 The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data. 755 756 Returns 757 ---------- 758 bool 759 Writing was successful or not. 760 """ 761 # Check params 762 # - pipe exists 763 if self.pipe is None: 764 raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram)) 765 # - pipe is in write mode 766 if self.mode != PipeMode.WRITE_MODE: 767 raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename)) 768 # batch is 3D (n, channels, nb samples) 769 if batch.ndim !=3: 770 raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape)) 771 # - shape of images in batch is fine 772 if batch.shape[2] != self.channels: 773 raise self.AudioIOException("Wrong audio channels in batch: {} expected {} {}.".format(batch.shape[2], self.channels, batch.shape)) 774 775 # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe 776 # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False 777 if not self.plannar: 778 # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel) 779 batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels) 780 batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel) 781 782 # garantee to have a C continuous array 783 if not batch.flags['C_CONTIGUOUS']: 784 batch = np.ascontiguousarray(batch) 785 786 # write frame 787 buffer = batch.tobytes() 788 if self.pipe.stdin.write( buffer ) < len(buffer): 789 # say to gc that this buffer is no longer needed 790 del buffer 791 raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename)) 792 793 # increase frame_counter 794 self.frame_counter.frame_count += int(batch.shape[0]/self.channels) # int conversion is mandatory to avoid confusion with time as float 795 796 # say to gc that this buffer is no longer needed 797 del buffer 798 799 return True
Write a batch of audio frame to the file
Parameters
batch: nparray The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data.
Returns
bool Writing was successful or not.
801 def iter_frames(self, with_timestamps = False): 802 """ 803 Method to iterate on audio frames using AudioIO obj. 804 for audio_frame in obj.iter_frames(): 805 .... 806 807 Parameters 808 ---------- 809 with_timestamps: bool optional (default False) 810 If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames 811 812 Returns 813 ---------- 814 nparray or FrameContainer 815 A batch of images of shape () 816 """ 817 818 try: 819 if self.mode == PipeMode.READ_MODE: 820 while self.isOpened(): 821 frame = self.readFrame(with_timestamps) 822 if frame is not None: 823 yield frame 824 finally: 825 self.close()
Method to iterate on audio frames using AudioIO obj. for audio_frame in obj.iter_frames(): ....
Parameters
with_timestamps: bool optional (default False) If set to True, the method returns a FrameContainer object with the batch and an array containing the associated timestamps to frames
Returns
nparray or FrameContainer A batch of images of shape ()
827 def iter_batches(self, batch_size : int, with_timestamps = False ): 828 """ 829 Method to iterate on batch ofaudio frames using AudioIO obj. 830 for audio_batch in obj.iter_batches(): 831 .... 832 833 Parameters 834 ---------- 835 with_timestamps: bool optional (default False) 836 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 837 """ 838 try: 839 if self.mode == PipeMode.READ_MODE: 840 while self.isOpened(): 841 batch = self.readBatch(batch_size, with_timestamps) 842 if batch is not None: 843 yield batch 844 finally: 845 self.close()
Method to iterate on batch ofaudio frames using AudioIO obj. for audio_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
100 @staticmethod 101 def get_time_in_sec(filename, *, debug=False, logLevel=16): 102 """ 103 Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals). 104 105 Parameters 106 ---------- 107 filename : str or path. 108 Raw audio waveform as a 1D array. 109 110 debug : bool (default False). 111 Show debug info. 112 113 logLevel: int (default 16). 114 Log level to pass to the underlying ffmpeg/ffprobe command. 115 116 Returns 117 ---------- 118 float 119 Length in seconds of video file (including milliseconds as decimal part with 3 decimals) 120 """ 121 122 cmd = [AudioIO.paramProgram, # ffprobe 123 '-hide_banner', 124 '-loglevel', str(logLevel), 125 '-show_entries', 'format=duration', 126 '-of', 'default=noprint_wrappers=1:nokey=1', 127 str(filename) 128 ] 129 130 if debug == True: 131 print(' '.join(cmd)) 132 133 # call ffprobe and get params in one single line 134 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg 135 output = lpipe.stdout.readlines() 136 lpipe.terminate() 137 # transform Bytes output to one single string 138 output = ''.join( [element.decode('utf-8') for element in output]) 139 140 try: 141 return float(output) 142 except (ValueError, TypeError): 143 return None
Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).
Parameters
filename : str or path. Raw audio waveform as a 1D array.
debug : bool (default False). Show debug info.
logLevel: 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 with 3 decimals)
145 @staticmethod 146 def get_params(filename, *, debug=False, logLevel=16): 147 """ 148 Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds. 149 150 Parameters 151 ---------- 152 filename : str or path. 153 Raw audio waveform as a 1D array. 154 155 debug : bool (default (False). 156 Show debug info. 157 158 log_level: int (default 16). 159 Log level to pass to the underlying ffmpeg/ffprobe command. 160 161 Returns 162 ---------- 163 tuple 164 Tuple containing (channels,sample_rate) of the file 165 """ 166 cmd = [AudioIO.paramProgram, # ffprobe 167 '-hide_banner', 168 '-loglevel', str(logLevel), 169 '-show_entries', 'stream=channels,sample_rate', 170 str(filename) 171 ] 172 173 if debug == True: 174 print(' '.join(cmd)) 175 176 # call ffprobe and get params in one single line 177 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg 178 output = lpipe.stdout.readlines() 179 lpipe.terminate() 180 # transform Bytes output to one single string 181 output = ''.join( [element.decode('utf-8') for element in output]) 182 183 pattern_sample_rate = r'sample_rate=(\d+)' 184 pattern_channels = r'channels=(\d+)' 185 186 # Search for values in the ffprobe output 187 match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE) 188 match_channels = re.search(pattern_channels, output, flags=re.MULTILINE) 189 190 # Extraction des valeurs 191 if match_sample_rate: 192 sample_rate = int(match_sample_rate.group(1)) 193 else: 194 raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'") 195 196 if match_channels: 197 channels = int(match_channels.group(1)) 198 else: 199 raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'") 200 201 return (channels,sample_rate) 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 debug: bool 211 """ debug flag for this object (print debut info, default False)""" 212 213 channels: int 214 """ Number of channels of images (default -1) """ 215 216 sample_rate: int 217 """ sample_rate of images (default -1) """ 218 219 plannar: bool 220 """ Read/write data as plannar, i.e. not interleaved (default True) """ 221 222 pipe: sp.Popen 223 """ pipe object to ffmpeg/ffprobe (default None)""" 224 225 frame_size: int 226 """ Weight in bytes of one image (default -1)""" 227 228 filename: str 229 """ Filename of the file (default None)""" 230 231 frame_counter: FrameCounter 232 """ `Framecounter` object to count ellapsed time (default None)"""
Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.
Parameters
filename : str or path. Raw audio waveform as a 1D array.
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 (channels,sample_rate) of the file
100 @staticmethod 101 def get_time_in_sec(filename, *, debug=False, logLevel=16): 102 """ 103 Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals). 104 105 Parameters 106 ---------- 107 filename : str or path. 108 Raw audio waveform as a 1D array. 109 110 debug : bool (default False). 111 Show debug info. 112 113 logLevel: int (default 16). 114 Log level to pass to the underlying ffmpeg/ffprobe command. 115 116 Returns 117 ---------- 118 float 119 Length in seconds of video file (including milliseconds as decimal part with 3 decimals) 120 """ 121 122 cmd = [AudioIO.paramProgram, # ffprobe 123 '-hide_banner', 124 '-loglevel', str(logLevel), 125 '-show_entries', 'format=duration', 126 '-of', 'default=noprint_wrappers=1:nokey=1', 127 str(filename) 128 ] 129 130 if debug == True: 131 print(' '.join(cmd)) 132 133 # call ffprobe and get params in one single line 134 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg 135 output = lpipe.stdout.readlines() 136 lpipe.terminate() 137 # transform Bytes output to one single string 138 output = ''.join( [element.decode('utf-8') for element in output]) 139 140 try: 141 return float(output) 142 except (ValueError, TypeError): 143 return None
Static method to get length of an audio file (or video file containing audio) in seconds including milliseconds as decimal part (3 decimals).
Parameters
filename : str or path. Raw audio waveform as a 1D array.
debug : bool (default False). Show debug info.
logLevel: 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 with 3 decimals)
145 @staticmethod 146 def get_params(filename, *, debug=False, logLevel=16): 147 """ 148 Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds. 149 150 Parameters 151 ---------- 152 filename : str or path. 153 Raw audio waveform as a 1D array. 154 155 debug : bool (default (False). 156 Show debug info. 157 158 log_level: int (default 16). 159 Log level to pass to the underlying ffmpeg/ffprobe command. 160 161 Returns 162 ---------- 163 tuple 164 Tuple containing (channels,sample_rate) of the file 165 """ 166 cmd = [AudioIO.paramProgram, # ffprobe 167 '-hide_banner', 168 '-loglevel', str(logLevel), 169 '-show_entries', 'stream=channels,sample_rate', 170 str(filename) 171 ] 172 173 if debug == True: 174 print(' '.join(cmd)) 175 176 # call ffprobe and get params in one single line 177 lpipe = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE) # stdin=sp.PIPE to prevent manipulation of shell echo mode by ffmpeg 178 output = lpipe.stdout.readlines() 179 lpipe.terminate() 180 # transform Bytes output to one single string 181 output = ''.join( [element.decode('utf-8') for element in output]) 182 183 pattern_sample_rate = r'sample_rate=(\d+)' 184 pattern_channels = r'channels=(\d+)' 185 186 # Search for values in the ffprobe output 187 match_sample_rate = re.search(pattern_sample_rate, output, flags=re.MULTILINE) 188 match_channels = re.search(pattern_channels, output, flags=re.MULTILINE) 189 190 # Extraction des valeurs 191 if match_sample_rate: 192 sample_rate = int(match_sample_rate.group(1)) 193 else: 194 raise AudioIO.AudioIOException("Unable to get audio sample_rate of '" + str(filename) + "'") 195 196 if match_channels: 197 channels = int(match_channels.group(1)) 198 else: 199 raise AudioIO.AudioIOException("Unable to get audio channels of '" + str(filename) + "'") 200 201 return (channels,sample_rate) 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 debug: bool 211 """ debug flag for this object (print debut info, default False)""" 212 213 channels: int 214 """ Number of channels of images (default -1) """ 215 216 sample_rate: int 217 """ sample_rate of images (default -1) """ 218 219 plannar: bool 220 """ Read/write data as plannar, i.e. not interleaved (default True) """ 221 222 pipe: sp.Popen 223 """ pipe object to ffmpeg/ffprobe (default None)""" 224 225 frame_size: int 226 """ Weight in bytes of one image (default -1)""" 227 228 filename: str 229 """ Filename of the file (default None)""" 230 231 frame_counter: FrameCounter 232 """ `Framecounter` object to count ellapsed time (default None)"""
Static method to get params (channels,sample_rate) of a (video containing) audio file in seconds.
Parameters
filename : str or path. Raw audio waveform as a 1D array.
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 (channels,sample_rate) of the file
327 def is_opened(self) -> bool: 328 """ 329 Method to get status of the underlying pipe to ffmpeg. 330 331 Returns 332 ---------- 333 bool 334 True if pipe is opened (reading or writing mode), False if not. 335 """ 336 # is the pip opened? 337 if self.pipe is not None and self.pipe.poll() is None: 338 return True 339 340 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.
568 def read_frame(self, with_timestamps = False): 569 """ 570 Read next frame from the audio file 571 572 Parameters 573 ---------- 574 with_timestamps: bool optional (default False) 575 If set to True, the method returns a ``FrameContainer`` with the audio and an array containing the associated timestamp(s) 576 577 Returns 578 ---------- 579 nparray or FrameContainer 580 A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame 581 of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False. 582 if with_timestamps is True, the return object is a FrameContainer with the audio data in ``FrameContainer.data`` and 583 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element). 584 """ 585 586 if self.pipe is None: 587 raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading a frame.".format(self.audioProgram)) 588 # - pipe is in write mode 589 if self.mode != PipeMode.READ_MODE: 590 raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename)) 591 592 if with_timestamps: 593 # get elapsed time in video, it is time of next frame(s) 594 current_elapsed_time = self.get_elapsed_time() 595 596 # read rgb image from pipe 597 toread = self.frame_size*self.float_size 598 buffer = self.pipe.stdout.read(toread) 599 600 if buffer == b"": 601 # not considered as an error, no more frame, no exception 602 return None 603 604 # get numpy UINT8 array from buffer 605 audio = np.frombuffer(buffer, dtype = np.float32).reshape(len(buffer)//self.float_size, self.channels) 606 607 # make it plannar (or not) 608 if self.plannar: 609 #transpose it 610 audio = audio.T 611 612 # increase frame_counter 613 self.frame_counter.frame_count += (self.frame_size * self.channels) 614 615 # say to gc that this buffer is no longer needed 616 del buffer 617 618 if with_timestamps: 619 return FrameContainer(1, audio, self.frame_size/self.sample_rate, current_elapsed_time) 620 621 return audio
Read next frame from the audio file
Parameters
with_timestamps: bool optional (default False)
If set to True, the method returns a FrameContainer with the audio and an array containing the associated timestamp(s)
Returns
nparray or FrameContainer
A frame of shape (self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A frame
of shape (self.channels*self.frame_size) with interleaved data if self.plannar is False.
if with_timestamps is True, the return object is a FrameContainer with the audio data in FrameContainer.data and
the associated timestamp in FrameContainer.timestamps as an array (one element).
623 def read_batch(self, numberOfFrames, with_timestamps = False): 624 """ 625 Read next batch of audio from the file 626 627 Parameters 628 ---------- 629 number_of_frames: int 630 Number of desired images within the batch. The last batch from the file may have less images. 631 632 with_timestamps: bool optional (default False) 633 If set to True, the method returns a FrameContainer with the batch and the an array containing the associated timestamps to frames 634 635 Returns 636 ---------- 637 nparray or FrameContainer 638 A batch of shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch 639 of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False. 640 if with_timestamps is True, the return object is a FrameContainer with the audio batch in ``FrameContainer.data`` and 641 the associated timestamp in ``FrameContainer.timestamps`` as an array (one element for each audio frame). 642 """ 643 644 if self.pipe is None: 645 raise self.AudioIOException("No pipe opened to {}. Call open(...) before reading frames.".format(self.audioProgram)) 646 # - pipe is in write mode 647 if self.mode != PipeMode.READ_MODE: 648 raise self.AudioIOException("Pipe to {} for '{}' not opened in read mode.".format(self.audioProgram, self.filename)) 649 650 if with_timestamps: 651 # get elapsed time in video, it is time of next frame(s) 652 current_elapsed_time = self.get_elapsed_time() 653 654 # try to read complete batch 655 toread = self.frame_size*self.float_size*self.channels*numberOfFrames 656 buffer = self.pipe.stdout.read(toread) 657 658 # check if we are at the end of the buffer 659 if buffer == b"": 660 # not considered as an error, no more frame, no exception 661 return None 662 663 # compute actual number of Frames 664 # do we have a full batch ? Computer standard division 665 actualNbFrames = len(buffer)/(self.frame_size*self.float_size*self.channels) 666 667 if actualNbFrames.is_integer(): 668 actualNbFrames = int(actualNbFrames) 669 # get and reshape batch from buffer 670 batch = np.frombuffer(buffer, dtype = np.float32).reshape((actualNbFrames, self.frame_size, self.channels,)) 671 else: 672 # We do not have a full batch, we are at end of the stream or ffmpeg has been stopped/killed 673 # Compute frame size in samples knowing len of buffer, self.float_size, self.channels and numberOfFrames 674 l_frame_size = len(buffer)//(numberOfFrames*self.float_size*self.channels) 675 if l_frame_size <= 0: 676 # not considered as an error, no more frame, no exception 677 return None 678 # get and reshape batch from buffer 679 batch = np.frombuffer(buffer, dtype = np.float32).reshape((numberOfFrames, l_frame_size, self.channels,)) 680 681 if self.plannar: 682 batch = batch.transpose(0, 2, 1) 683 684 # increase frame_counter 685 self.frame_counter.frame_count += (actualNbFrames * self.frame_size * self.channels) 686 687 # say to gc that this buffer is no longer needed 688 del buffer 689 690 if with_timestamps: 691 return FrameContainer( actualNbFrames, batch, self.frame_size/self.sample_rate, current_elapsed_time) 692 693 return batch
Read next batch of audio from the file
Parameters
number_of_frames: int Number of desired images within the batch. The last batch from the file 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 shape (n, self.channels,self.frame_size) as defined in the reader/open call if self.plannar is True. A batch
of shape (n, self.channels*self.frame_size) with interleaved data if self.plannar is False.
if with_timestamps is True, the return object is a FrameContainer with the audio batch in FrameContainer.data and
the associated timestamp in FrameContainer.timestamps as an array (one element for each audio frame).
695 def write_frame(self, audio) -> bool: 696 """ 697 Write an audio frame to the file 698 699 Parameters 700 ---------- 701 audio: nparray 702 The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel). 703 704 Returns 705 ---------- 706 bool 707 Writing was successful or not. 708 """ 709 # Check params 710 # - pipe exists 711 if self.pipe is None: 712 raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram)) 713 # - pipe is in write mode 714 if self.mode != PipeMode.WRITE_MODE: 715 raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename)) 716 # - shape of image is fine, thus we have pixels for a full compatible frame 717 if audio.shape[0] != self.channels: 718 raise self.AudioIOException("Wong audio shape: {} expected ({}, nb_samples_to_write).".format(audio.shape,self.channels)) 719 # - type of data is Float32 720 if audio.dtype != np.float32: 721 raise self.AudioIOException("Wong audio type: {} expected np.float32.".format(audio.dtype)) 722 723 # array must have a shape (channels, samples), reshape it it to (samples, channels) if plannar 724 if not self.plannar: 725 audio = audio.reshape(-1) 726 727 # print( audio.shape ) 728 729 # garantee to have a C continuous array 730 if not audio.flags['C_CONTIGUOUS']: 731 a = np.ascontiguousarray(a) 732 733 # write frame 734 buffer = audio.tobytes() 735 if self.pipe.stdin.write( buffer ) < len(buffer): 736 print( f"Error writing frame to {self.filename}" ) 737 return False 738 739 # increase frame_counter 740 self.frame_counter.frame_count += (audio.shape[1] * self.channels) 741 742 # say to gc that this buffer is no longer needed 743 del buffer 744 745 return True
Write an audio frame to the file
Parameters
audio: nparray The audio frame to write to the video file of shape (self.channels,nb_samples_per_channel) if plannar is True else (self.channels*nb_samples_per_channel).
Returns
bool Writing was successful or not.
747 def write_batch(self, batch): 748 """ 749 Write a batch of audio frame to the file 750 751 Parameters 752 ---------- 753 batch: nparray 754 The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data. 755 756 Returns 757 ---------- 758 bool 759 Writing was successful or not. 760 """ 761 # Check params 762 # - pipe exists 763 if self.pipe is None: 764 raise self.AudioIOException("No pipe opened to {}. Call create(...) before writing frames.".format(self.audioProgram)) 765 # - pipe is in write mode 766 if self.mode != PipeMode.WRITE_MODE: 767 raise self.AudioIOException("Pipe to {} for '{}' not opened in write mode.".format(self.audioProgram, self.filename)) 768 # batch is 3D (n, channels, nb samples) 769 if batch.ndim !=3: 770 raise self.AudioIOException("Wrong batch shape: {} expected 3 dimensions (n, n_channels, n_samples_per_channel).".format(batch.shape)) 771 # - shape of images in batch is fine 772 if batch.shape[2] != self.channels: 773 raise self.AudioIOException("Wrong audio channels in batch: {} expected {} {}.".format(batch.shape[2], self.channels, batch.shape)) 774 775 # array must have a shape (n * n_channels * n_samples_per_channel) before writing them to pipe 776 # reshape it it to (n * n_channels * n_samples_per_channel) if plannar is False 777 if not self.plannar: 778 # goes from (n, n_channels, n_samples_per_channel) to (n * n_channels * n_samples_per_channel) 779 batch = batch.transpose(0, 2, 1) # first go to (n, n_samples_per_channel, n_channels) 780 batch = batch.reshape(-1) # then to 1D array (n * n_channels * n_samples_per_channel) 781 782 # garantee to have a C continuous array 783 if not batch.flags['C_CONTIGUOUS']: 784 batch = np.ascontiguousarray(batch) 785 786 # write frame 787 buffer = batch.tobytes() 788 if self.pipe.stdin.write( buffer ) < len(buffer): 789 # say to gc that this buffer is no longer needed 790 del buffer 791 raise self.AudioIOException("Error writing batch to '{}'.".format(self.filename)) 792 793 # increase frame_counter 794 self.frame_counter.frame_count += int(batch.shape[0]/self.channels) # int conversion is mandatory to avoid confusion with time as float 795 796 # say to gc that this buffer is no longer needed 797 del buffer 798 799 return True
Write a batch of audio frame to the file
Parameters
batch: nparray The batch of audio frames to write to the video file of shape (n,self.channels,nb_samples_per_channel) if plannar is True else (n,self.channels*nb_samples_per_channel) of interleaved audio data.
Returns
bool Writing was successful or not.
38 class AudioIOException(Exception): 39 """ 40 Dedicated exception class for AudioIO class. 41 """ 42 def __init__(self, message="Error while reading/writing video occurs"): 43 self.message = message 44 super().__init__(self.message)
Dedicated exception class for AudioIO class.
46 class AudioFormat(Enum): 47 """ 48 Enum class for supported input video type: 32-bit float is the only supported type for the moment. 49 """ 50 PCM32LE = 'pcm_f32le' # default format (unique mode for the moment)
Enum class for supported input video type: 32-bit float is the only supported type for the moment.
16class FrameCounter: 17 """ 18 Create a ``FrameCounter`` to follow elapsed time in audio/video file in read or write mode. Static utility functions allow to format elapsed time. 19 """ 20 21 class FrameCounterException(Exception): 22 """ 23 Dedicated exception class for FrameCounter class. 24 """ 25 def __init__(self, message="Error while setting FrameCounter parameters."): 26 self.message = message 27 super().__init__(self.message) 28 29 _fps: float # (private) 30 """ Fps of current stream """ 31 32 _frame_count: int # (private) 33 """ Frame count in the current stream """ 34 35 def __init__(self, fps: Union[int, float]): 36 """ 37 Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode 38 39 Parameters 40 ---------- 41 fps: int or float. 42 Frames per second of the associated stream. 43 """ 44 # check init fps value 45 self._fps = float(fps) 46 if self._fps <= 0.0: 47 raise FrameCounterException("fps must be > 0.0.") 48 49 # 2 modes 50 self._frame_count = 0 # at 00:00:00.000 51 52 # support += 53 def __iadd__(self, other: Union[int, float]): 54 """ 55 Support += operator for FrameCounter. 56 57 Parameters 58 ---------- 59 other: int or float. 60 If other is a float, add the number of frame to add 'other' seconds (thus other * self._fps samples). 61 If other is an int, add the value as a number of samples in the stream. 62 """ 63 if isinstance(other,float): 64 # float means adding time 65 self._frame_count += int(other * self._fps) # number of second * Nb of elements per seconds 66 else: 67 # for int, add number of element 68 self._frame_count += other 69 return self 70 71 @property 72 def frame_count(self): 73 """ 74 Property to get underlying self._frame_count. Idea is to control setter to valid setting values. 75 """ 76 return self._frame_count 77 78 @frame_count.setter 79 def frame_count(self, value: int): 80 """ 81 Setter for underlying self._frame_count controlling setting value. 82 """ 83 if value < 0: 84 raise FrameCounterException("frame_count must be >= 0") 85 self._frame_count = value 86 87 @property 88 def fps(self): 89 """ 90 Property to get underlying self._fps. 91 """ 92 return self._fps 93 94 @staticmethod 95 def format_time(nb_frames: int, fps: float, show_ms : bool = True, show_days : bool = False) -> str: 96 """ 97 Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds, 98 show_days to show days instead of cummulative hour count. 99 100 Parameters 101 ---------- 102 nb_frames: int. 103 Number of samples already present in the stream. 104 105 fps: float. 106 Fps of the associated stream. 107 108 show_ms: bool. 109 Flag to say if we want to show milliseconds in the output str. 110 111 show_days: bool. 112 Flag to say if we want to show says instead of cumulative hours in the output str. 113 114 Returns 115 ------- 116 str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False) 117 or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True) 118 """ 119 # exact time in seconds (float) 120 exact_seconds = nb_frames / fps 121 122 # integer part for days/hours/minutes/seconds 123 total_seconds = int(exact_seconds) 124 125 # milliseconds = decimal part * 1000 126 millis = int(round((exact_seconds - total_seconds) * 1000)) 127 128 # handle the case where rounding results in 1000 ms 129 if millis == 1000: 130 millis = 0 131 total_seconds += 1 132 133 if show_days: 134 # compute number of days, hours, minutes, seconds 135 days, mod = divmod(total_seconds, 24 * 3600) 136 hours, mod = divmod(mod, 3600) 137 minutes, seconds = divmod(mod, 60) 138 139 if days > 0: 140 days = f"{days} days(s) " 141 else: 142 days = "" 143 else: 144 days = "" # no day as show_days = False 145 hours, mod = divmod(total_seconds, 3600) 146 minutes, seconds = divmod(mod, 60) 147 148 if show_ms == True: 149 millis = f".{millis:03d}" 150 else: 151 millis = "" 152 153 return f"{days}{hours:02d}:{minutes:02d}:{seconds:02d}{millis}" 154 155 def get_elapsed_time_as_str(self) -> str: 156 """ 157 Get elapsed time as string representing a float value rounded to 3 decimals. 158 159 Returns 160 ------- 161 str representing corresponding time in float format rounded to 3 decimals. 162 """ 163 return f"{float(self._frame_count)/self._fps:.3f}" 164 165 def get_formated_elapsed_time_as_str(self, show_ms : bool = True, show_days : bool = False) -> str: 166 """ 167 Get elapsed time as string representing time with different mode (see ``FrameCounter.format_time`` for parameter explanation). 168 Returns 169 ------- 170 str representing corresponding time in float format rounded to 3 decimals. 171 """ 172 # frame count to time correction is done in format_time 173 return FrameCounter.format_time(self._frame_count, self._fps, show_ms, show_days) 174 175 def get_elapsed_time(self) -> float: 176 """ 177 Get elapsed time as float value rounded to 3 decimals. 178 179 Returns 180 ------- 181 str representing corresponding time in float format rounded to 3 decimals. 182 """ 183 return round(float(self._frame_count)/self._fps,3)
Create a FrameCounter to follow elapsed time in audio/video file in read or write mode. Static utility functions allow to format elapsed time.
35 def __init__(self, fps: Union[int, float]): 36 """ 37 Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode 38 39 Parameters 40 ---------- 41 fps: int or float. 42 Frames per second of the associated stream. 43 """ 44 # check init fps value 45 self._fps = float(fps) 46 if self._fps <= 0.0: 47 raise FrameCounterException("fps must be > 0.0.") 48 49 # 2 modes 50 self._frame_count = 0 # at 00:00:00.000
Create a VideoIO object giving ffmpeg/ffrobe loglevel and defining debug mode
Parameters
fps: int or float. Frames per second of the associated stream.
71 @property 72 def frame_count(self): 73 """ 74 Property to get underlying self._frame_count. Idea is to control setter to valid setting values. 75 """ 76 return self._frame_count
Property to get underlying self._frame_count. Idea is to control setter to valid setting values.
87 @property 88 def fps(self): 89 """ 90 Property to get underlying self._fps. 91 """ 92 return self._fps
Property to get underlying self._fps.
94 @staticmethod 95 def format_time(nb_frames: int, fps: float, show_ms : bool = True, show_days : bool = False) -> str: 96 """ 97 Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds, 98 show_days to show days instead of cummulative hour count. 99 100 Parameters 101 ---------- 102 nb_frames: int. 103 Number of samples already present in the stream. 104 105 fps: float. 106 Fps of the associated stream. 107 108 show_ms: bool. 109 Flag to say if we want to show milliseconds in the output str. 110 111 show_days: bool. 112 Flag to say if we want to show says instead of cumulative hours in the output str. 113 114 Returns 115 ------- 116 str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False) 117 or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True) 118 """ 119 # exact time in seconds (float) 120 exact_seconds = nb_frames / fps 121 122 # integer part for days/hours/minutes/seconds 123 total_seconds = int(exact_seconds) 124 125 # milliseconds = decimal part * 1000 126 millis = int(round((exact_seconds - total_seconds) * 1000)) 127 128 # handle the case where rounding results in 1000 ms 129 if millis == 1000: 130 millis = 0 131 total_seconds += 1 132 133 if show_days: 134 # compute number of days, hours, minutes, seconds 135 days, mod = divmod(total_seconds, 24 * 3600) 136 hours, mod = divmod(mod, 3600) 137 minutes, seconds = divmod(mod, 60) 138 139 if days > 0: 140 days = f"{days} days(s) " 141 else: 142 days = "" 143 else: 144 days = "" # no day as show_days = False 145 hours, mod = divmod(total_seconds, 3600) 146 minutes, seconds = divmod(mod, 60) 147 148 if show_ms == True: 149 millis = f".{millis:03d}" 150 else: 151 millis = "" 152 153 return f"{days}{hours:02d}:{minutes:02d}:{seconds:02d}{millis}"
Static function to format time given by a number of frames and an fps. show_ms value defines if we show milliseconds, show_days to show days instead of cummulative hour count.
Parameters
nb_frames: int. Number of samples already present in the stream.
fps: float. Fps of the associated stream.
show_ms: bool. Flag to say if we want to show milliseconds in the output str.
show_days: bool. Flag to say if we want to show says instead of cumulative hours in the output str.
Returns
str representing corresponding time. Either 26:15:00 (show_ms=False, show_days=False), 26:15:00.500 (show_ms=True, show_days=False)
or 1 day(s) 02:15:00.500 (show_ms=True, show_days=True)
155 def get_elapsed_time_as_str(self) -> str: 156 """ 157 Get elapsed time as string representing a float value rounded to 3 decimals. 158 159 Returns 160 ------- 161 str representing corresponding time in float format rounded to 3 decimals. 162 """ 163 return f"{float(self._frame_count)/self._fps:.3f}"
Get elapsed time as string representing a float value rounded to 3 decimals.
Returns
str representing corresponding time in float format rounded to 3 decimals.
165 def get_formated_elapsed_time_as_str(self, show_ms : bool = True, show_days : bool = False) -> str: 166 """ 167 Get elapsed time as string representing time with different mode (see ``FrameCounter.format_time`` for parameter explanation). 168 Returns 169 ------- 170 str representing corresponding time in float format rounded to 3 decimals. 171 """ 172 # frame count to time correction is done in format_time 173 return FrameCounter.format_time(self._frame_count, self._fps, show_ms, show_days)
Get elapsed time as string representing time with different mode (see FrameCounter.format_time for parameter explanation).
Returns
str representing corresponding time in float format rounded to 3 decimals.
175 def get_elapsed_time(self) -> float: 176 """ 177 Get elapsed time as float value rounded to 3 decimals. 178 179 Returns 180 ------- 181 str representing corresponding time in float format rounded to 3 decimals. 182 """ 183 return round(float(self._frame_count)/self._fps,3)
Get elapsed time as float value rounded to 3 decimals.
Returns
str representing corresponding time in float format rounded to 3 decimals.
21 class FrameCounterException(Exception): 22 """ 23 Dedicated exception class for FrameCounter class. 24 """ 25 def __init__(self, message="Error while setting FrameCounter parameters."): 26 self.message = message 27 super().__init__(self.message)
Dedicated exception class for FrameCounter class.
16class FrameContainer: 17 """ 18 Create a Container with audio or image data and associated timestamp(s). 19 """ 20 21 class FrameContainerException(Exception): 22 """ 23 Dedicated exception class for FrameContainer class. 24 """ 25 def __init__(self, message="Error while setting FrameCounter parameters."): 26 self.message = message 27 super().__init__(self.message) 28 29 nb_frames: int 30 """ number of frames in the frame container. 1 for audio frame or images, n for a batch. """ 31 32 data: np.array 33 """ Audio frame or image, or batch of images or audio frames. """ 34 35 timestamps: np.array 36 """ an np.array of timestamp(s) associated to the data frame(s). """ 37 38 def __init__(self, nb_frames : int, frames : np.array, fps : float, start_time : float = 0.0 ): 39 """ 40 Create a FrameContainer object with data and associated timestamps 41 42 Parameters 43 ---------- 44 nb_frames: int. 45 Number of frame(s) in the frame container. Either 1, either number of element in the batch. 46 47 frames: np.array 48 Data for image, audio frame or batch of them. 49 50 fps : float. 51 fps of the read stream. 52 53 start_time: float (default = 0.0). 54 Start time of the current FrameContainer, i.e. time of the (first) frame. 55 """ 56 57 # check params 58 if nb_frames <= 0: 59 raise self.FrameContainerException("nb_frames must be > 0.") 60 if nb_frames > 1 and frames.shape[0] != nb_frames: 61 raise self.FrameContainerException("nb_frames and batch size (frames.shape[0]) differs.") 62 if fps <= 0.0: 63 raise self.FrameContainerException("fps must be > 0.0.") 64 if start_time < 0.0: 65 raise self.FrameContainerException("start_time must be >= 0.0.") 66 67 self.nb_frames = nb_frames 68 self.data = frames 69 # first frame is at time start_time 70 self.timestamps = np.linspace(start_time, start_time+(self.nb_frames-1)*fps, self.nb_frames).tolist() 71 72 def totorch(self): 73 """ 74 Convert the numy array into a pytorch tensor. 75 76 Returns 77 ---------- 78 pytorch tensor. 79 """ 80 # import torch late as it is not in the standard requirements for simple-ffmpeg-batch-io 81 import torch 82 return torch.from_numpy(self.data)
Create a Container with audio or image data and associated timestamp(s).
38 def __init__(self, nb_frames : int, frames : np.array, fps : float, start_time : float = 0.0 ): 39 """ 40 Create a FrameContainer object with data and associated timestamps 41 42 Parameters 43 ---------- 44 nb_frames: int. 45 Number of frame(s) in the frame container. Either 1, either number of element in the batch. 46 47 frames: np.array 48 Data for image, audio frame or batch of them. 49 50 fps : float. 51 fps of the read stream. 52 53 start_time: float (default = 0.0). 54 Start time of the current FrameContainer, i.e. time of the (first) frame. 55 """ 56 57 # check params 58 if nb_frames <= 0: 59 raise self.FrameContainerException("nb_frames must be > 0.") 60 if nb_frames > 1 and frames.shape[0] != nb_frames: 61 raise self.FrameContainerException("nb_frames and batch size (frames.shape[0]) differs.") 62 if fps <= 0.0: 63 raise self.FrameContainerException("fps must be > 0.0.") 64 if start_time < 0.0: 65 raise self.FrameContainerException("start_time must be >= 0.0.") 66 67 self.nb_frames = nb_frames 68 self.data = frames 69 # first frame is at time start_time 70 self.timestamps = np.linspace(start_time, start_time+(self.nb_frames-1)*fps, self.nb_frames).tolist()
Create a FrameContainer object with data and associated timestamps
Parameters
nb_frames: int. Number of frame(s) in the frame container. Either 1, either number of element in the batch.
frames: np.array Data for image, audio frame or batch of them.
fps : float. fps of the read stream.
start_time: float (default = 0.0). Start time of the current FrameContainer, i.e. time of the (first) frame.
72 def totorch(self): 73 """ 74 Convert the numy array into a pytorch tensor. 75 76 Returns 77 ---------- 78 pytorch tensor. 79 """ 80 # import torch late as it is not in the standard requirements for simple-ffmpeg-batch-io 81 import torch 82 return torch.from_numpy(self.data)
Convert the numy array into a pytorch tensor.
Returns
pytorch tensor.
21 class FrameContainerException(Exception): 22 """ 23 Dedicated exception class for FrameContainer class. 24 """ 25 def __init__(self, message="Error while setting FrameCounter parameters."): 26 self.message = message 27 super().__init__(self.message)
Dedicated exception class for FrameContainer class.
15class PipeMode(Enum): 16 """ 17 Enum class for pipe opening type. 18 """ 19 UNK_MODE = 0 20 """ No pipe mode defined """ 21 22 READ_MODE = 1 23 """ Read mode (get data from pipe) """ 24 25 WRITE_MODE = 2 26 """ Write mode (write data to pipe) """
Enum class for pipe opening type.