diff --git a/vidiopy/video/VideoFileClip.py b/vidiopy/video/VideoFileClip.py index f39881f..b6f7539 100644 --- a/vidiopy/video/VideoFileClip.py +++ b/vidiopy/video/VideoFileClip.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Self, Union +from typing import Self, Union from PIL import Image import ffmpegio import numpy as np @@ -8,7 +8,53 @@ class VideoFileClip(VideoClip): - def __init__(self, filename, audio=True, ffmpeg_options=None): + """ + A class used to represent a video file clip. + + This class extends the VideoClip class and provides additional functionality for working with video files. It uses ffmpeg to read video files, extract frames, and set the properties of the video clip. It also provides methods for transforming frames, creating sub-clips, and generating frame representations. + + Attributes: + filename (str): The name of the video file. + fps (float): The frames per second of the video. + size (tuple): The width and height of the video. + start (float): The start time of the video clip. + end (float): The end time of the video clip. + duration (float): The duration of the video clip. + audio (AudioFileClip): The audio of the video clip. + clip (tuple): The frames of the video clip as PIL Images. + + Methods: + fl_frame_transform(func, *args, **kwargs): Applies a function to each frame of the video clip. + fl_clip_transform(func, *args, **kwargs): Applies a function to each frame of the video clip along with its timestamp. + sub_clip(t_start=None, t_end=None): Returns a sub-clip of the video clip. + sub_clip_copy(t_start=None, t_end=None): Returns a copy of a sub-clip of the video clip. + make_frame_array(t): Returns a numpy array representation of a specific frame in the video clip. + make_frame_pil(t): Returns a PIL Image representation of a specific frame in the video clip. + _import_video_clip(file_name, ffmpeg_options=None): Imports a video clip from a file using ffmpeg. + """ + + def __init__( + self, filename: str, audio: bool = True, ffmpeg_options: dict | None = None + ) -> None: + """ + Initializes a new instance of the VideoFileClip class. + + This method creates a new VideoFileClip from a video file. It uses ffmpeg to read the video file, extract the frames, and set the properties of the video clip. + + Args: + filename (str): The name of the video file to import. + audio (bool, optional): Whether to include audio in the video clip. Defaults to True. + ffmpeg_options (dict | None, optional): Additional options to pass to ffmpeg. Defaults to None. + + Raises: + None + + Example: + >>> video_clip = VideoFileClip("video.mp4") + + Note: + This method uses ffmpeg to read the video file. + """ super().__init__() self.filename = filename @@ -48,7 +94,7 @@ def __eq__(self, other) -> bool: return False return ( - isinstance(other, VideoClip) + isinstance(other, VideoFileClip) and self.fps == other.fps and self.size == other.size and self.start == other.start @@ -63,7 +109,32 @@ def __eq__(self, other) -> bool: ################# @requires_start_end - def fl_frame_transform(self, func, *args, **kwargs): + def fl_frame_transform(self, func, *args, **kwargs) -> Self: + """ + Applies a function to each frame of the video clip. + + This method iterates over each frame in the video clip, applies a function to it, and replaces the original frame with the result. + + Args: + func (callable): The function to apply to each frame. It should take an Image as its first argument, and return an Image. + *args: Additional positional arguments to pass to func. + **kwargs: Additional keyword arguments to pass to func. + + Returns: + Self: Returns the instance of the class with updated frames. + + Raises: + None + + Example: + >>> video_clip = VideoClip() + >>> def invert_colors(image): + ... return ImageOps.invert(image) + >>> video_clip.fl_frame_transform(invert_colors) + + Note: + This method requires the start and end of the video clip to be set. + """ clip: list[Image.Image] = [] for frame in self.clip: frame: Image.Image = func(frame, *args, **kwargs) @@ -72,7 +143,34 @@ def fl_frame_transform(self, func, *args, **kwargs): return self @requires_fps - def fl_clip_transform(self, func, *args, **kwargs): + def fl_clip_transform(self, func, *args, **kwargs) -> Self: + """ + Applies a function to each frame of the video clip along with its timestamp. + + This method iterates over each frame in the video clip, applies a function to it and its timestamp, and replaces the original frame with the result. + + Args: + func (callable): The function to apply to each frame. It should take an Image and a float (representing the timestamp) as its first two arguments, and return an Image. + *args: Additional positional arguments to pass to func. + **kwargs: Additional keyword arguments to pass to func. + + Returns: + Self: Returns the instance of the class with updated frames. + + Raises: + None + + Example: + >>> video_clip = VideoClip() + >>> def add_timestamp(image, timestamp): + ... draw = ImageDraw.Draw(image) + ... draw.text((10, 10), str(timestamp), fill="white") + ... return image + >>> video_clip.fl_clip_transform(add_timestamp) + + Note: + This method requires the fps of the video clip to be set. + """ td = 1 / self.fps frame_time = 0.0 clip: list[Image.Image] = [] @@ -83,11 +181,6 @@ def fl_clip_transform(self, func, *args, **kwargs): self.clip = tuple(clip) return self - def fx(self, func: Callable[..., Self], *args, **kwargs): - # Apply an effect function directly to the clip - self = func(self, *args, **kwargs) - return self - @requires_fps def sub_clip( self, @@ -174,7 +267,28 @@ def sub_clip_copy( return instance @requires_duration - def make_frame_array(self, t) -> np.ndarray: + def make_frame_array(self, t: int | float) -> np.ndarray: + """ + Generates a numpy array representation of a specific frame in the video clip. + + This method calculates the index of the frame for a specific time, retrieves the frame from the video clip, and converts it to a numpy array. + + Args: + t (int | float): The time of the frame to convert. + + Returns: + np.ndarray: The numpy array representation of the frame. + + Raises: + ValueError: If the duration of the video clip is not set. + + Example: + >>> video_clip = VideoClip() + >>> frame_array = video_clip.make_frame_array(10) + + Note: + This method requires the duration of the video clip to be set. + """ if self.duration is None: raise ValueError("Duration is Not Set.") time_per_frame = self.duration / len(self.clip) @@ -183,7 +297,28 @@ def make_frame_array(self, t) -> np.ndarray: return np.array(self.clip[frame_index]) @requires_duration - def make_frame_pil(self, t) -> Image.Image: + def make_frame_pil(self, t: int | float) -> Image.Image: + """ + Generates a PIL Image representation of a specific frame in the video clip. + + This method calculates the index of the frame for a specific time, retrieves the frame from the video clip, and returns it as a PIL Image. + + Args: + t (int | float): The time of the frame to convert. + + Returns: + Image.Image: The PIL Image representation of the frame. + + Raises: + ValueError: If the duration of the video clip is not set. + + Example: + >>> video_clip = VideoClip() + >>> frame_image = video_clip.make_frame_pil(10) + + Note: + This method requires the duration of the video clip to be set. + """ if self.duration is None: raise ValueError("Duration is Not Set.") time_per_frame = self.duration / len(self.clip) @@ -191,7 +326,31 @@ def make_frame_pil(self, t) -> Image.Image: frame_index = int(min(len(self.clip) - 1, max(0, frame_index))) return self.clip[frame_index] - def _import_video_clip(self, file_name, ffmpeg_options): + def _import_video_clip( + self, file_name: str, ffmpeg_options: dict | None = None + ) -> tuple: + """ + Imports a video clip from a file using ffmpeg. + + This method reads a video file using ffmpeg, converts each frame to a PIL Image, and returns a tuple of the images and the fps of the video. + + Args: + file_name (str): The name of the video file to import. + ffmpeg_options (dict | None, optional): Additional options to pass to ffmpeg. Defaults to None. + + Returns: + tuple: A tuple of the frames as PIL Images and the fps of the video. + + Raises: + None + + Example: + >>> video_clip = VideoClip() + >>> frames, fps = video_clip._import_video_clip("video.mp4") + + Note: + This method uses ffmpeg to read the video file. + """ options = {**(ffmpeg_options if ffmpeg_options else {})} fps, frames = ffmpegio.video.read(file_name, **options) return tuple(Image.fromarray(frame) for frame in frames), fps