Source code for fmusvid.operations.basic

"""
Basic video operations for FMUS-VID.

This module provides fundamental video manipulation functions.
"""

import os
import time
from pathlib import Path
from typing import List, Union, Optional, Tuple, Dict

import cv2
import numpy as np

from ..core.video import Video
from ..core.frame import Frame


[docs] def video_to_images( video: Union[str, Path, Video], output_dir: Union[str, Path], start: Optional[float] = None, end: Optional[float] = None, interval: Optional[float] = None, frame_count: Optional[int] = None, with_timestamp: bool = False, with_frame_number: bool = False, format: str = "png", prefix: str = "frame_", **kwargs ) -> List[Path]: """ Extract frames from a video to image files. Args: video: Video object or path to video file output_dir: Directory to save the images start: Start time in seconds (None for beginning) end: End time in seconds (None for end of video) interval: Time interval between frames in seconds frame_count: Number of frames to extract (evenly distributed) with_timestamp: Add timestamp text overlay to images with_frame_number: Add frame number text overlay to images format: Image format ('png', 'jpg', etc.) prefix: Filename prefix for the saved images **kwargs: Additional options Returns: List of paths to the saved images Example: >>> # Extract all frames >>> frames = fmusvid.video_to_images("input.mp4", "frames_dir") >>> # Extract 10 evenly distributed frames >>> frames = fmusvid.video_to_images("input.mp4", "frames_dir", frame_count=10) >>> # Extract frames at 1-second intervals with timestamps >>> frames = fmusvid.video_to_images("input.mp4", "frames_dir", interval=1.0, with_timestamp=True) """ # Create output directory output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) # Get video path if isinstance(video, Video): video_path = video._path else: video_path = Path(video) # Open the video cap = cv2.VideoCapture(str(video_path)) # Check if video opened successfully if not cap.isOpened(): raise ValueError(f"Could not open video: {video_path}") # Get video properties fps = cap.get(cv2.CAP_PROP_FPS) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) duration = total_frames / fps # Convert time to frame numbers start_frame = 0 if start is not None: start_frame = int(start * fps) end_frame = total_frames - 1 if end is not None: end_frame = min(int(end * fps), total_frames - 1) # Determine frames to extract if frame_count is not None: # Evenly distributed frames if frame_count <= 1: raise ValueError("frame_count must be greater than 1") frames_to_extract = [] if frame_count == 1: # Just the middle frame frames_to_extract = [start_frame + (end_frame - start_frame) // 2] else: # Calculate step step = (end_frame - start_frame) / (frame_count - 1) frames_to_extract = [int(start_frame + i * step) for i in range(frame_count)] elif interval is not None: # Frames at regular intervals frame_interval = int(interval * fps) if frame_interval < 1: frame_interval = 1 frames_to_extract = list(range(start_frame, end_frame + 1, frame_interval)) else: # All frames in range frames_to_extract = list(range(start_frame, end_frame + 1)) # Set up font for text overlay font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 1.0 font_color = (255, 255, 255) # White thickness = 2 # Extract and save frames saved_paths = [] for i, frame_num in enumerate(frames_to_extract): # Set frame position cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num) # Read the frame ret, frame = cap.read() if not ret: continue # Add timestamp if requested if with_timestamp or with_frame_number: # Calculate time frame_time = frame_num / fps hours, remainder = divmod(frame_time, 3600) minutes, seconds = divmod(remainder, 60) milliseconds = int((seconds - int(seconds)) * 1000) seconds = int(seconds) # Position for text (bottom left) position = (10, frame.shape[0] - 10) if with_timestamp and with_frame_number: # Both timestamp and frame number text = f"Frame: {frame_num} - Time: {int(hours):02d}:{int(minutes):02d}:{seconds:02d}.{milliseconds:03d}" elif with_timestamp: # Only timestamp text = f"Time: {int(hours):02d}:{int(minutes):02d}:{seconds:02d}.{milliseconds:03d}" else: # Only frame number text = f"Frame: {frame_num}" # Add text to frame frame = cv2.putText(frame, text, position, font, font_scale, font_color, thickness, cv2.LINE_AA) # Create filename with padding for sequential order if len(frames_to_extract) > 1: padding = len(str(len(frames_to_extract))) filename = f"{prefix}{i:0{padding}d}.{format}" else: filename = f"{prefix}single.{format}" # Save the frame output_file = output_path / filename cv2.imwrite(str(output_file), frame) saved_paths.append(output_file) # Release the video capture cap.release() return saved_paths
[docs] def reverse_video( video: Union[str, Path, Video], output: Optional[Union[str, Path]] = None, **kwargs ) -> Video: """ Create a reversed version of a video. Args: video: Video object or path to video file output: Path to save the reversed video (None for temporary file) **kwargs: Additional options Returns: Video object for the reversed video Example: >>> # Create reversed video >>> reversed_video = fmusvid.reverse_video("input.mp4", "reversed.mp4") """ # Get video path if isinstance(video, Video): video_path = video._path else: video_path = Path(video) # Create output path if not provided if output is None: # Create a temporary file import tempfile temp_file = tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) output_path = Path(temp_file.name) temp_file.close() else: output_path = Path(output) # Open the video cap = cv2.VideoCapture(str(video_path)) # Check if video opened successfully if not cap.isOpened(): raise ValueError(f"Could not open video: {video_path}") # Get video properties fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # Set up video writer fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter(str(output_path), fourcc, fps, (width, height)) # Start from the last frame for frame_idx in range(total_frames - 1, -1, -1): # Set position cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) # Read the frame ret, frame = cap.read() if not ret: continue # Write the frame out.write(frame) # Release resources cap.release() out.release() # Return Video object return Video(output_path)
[docs] def create_thumbnails( video: Union[str, Path, Video], count: int = 9, output_path: Optional[Union[str, Path]] = None, grid: bool = True, scale: float = 0.25, with_timestamp: bool = True, **kwargs ) -> Union[List[Path], Path]: """ Generate thumbnail images from a video. Args: video: Video object or path to video file count: Number of thumbnails to generate output_path: Path to save the grid image or directory for individual thumbnails grid: Whether to combine thumbnails into a grid image scale: Scale factor for the thumbnails with_timestamp: Add timestamp to the thumbnails **kwargs: Additional options Returns: Path to grid image if grid=True, otherwise list of paths to individual thumbnails Example: >>> # Create a 3x3 grid of thumbnails >>> grid_path = fmusvid.create_thumbnails("input.mp4", count=9, output_path="thumbs.jpg") >>> # Create individual thumbnails >>> thumb_paths = fmusvid.create_thumbnails("input.mp4", count=5, grid=False, output_path="thumbs_dir") """ # Get video path if isinstance(video, Video): video_path = video._path else: video_path = Path(video) # Open the video cap = cv2.VideoCapture(str(video_path)) # Check if video opened successfully if not cap.isOpened(): raise ValueError(f"Could not open video: {video_path}") # Get video properties fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) duration = total_frames / fps # Calculate frames to capture interval = duration / (count + 1) # +1 to avoid capturing the first and last frames frame_times = [interval * (i + 1) for i in range(count)] # Set up font for timestamps font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.7 * scale font_color = (255, 255, 255) # White thickness = 1 # Capture thumbnails thumbnails = [] for time_pos in frame_times: # Set position frame_pos = int(time_pos * fps) cap.set(cv2.CAP_PROP_POS_FRAMES, frame_pos) # Read the frame ret, frame = cap.read() if not ret: continue # Add timestamp if requested if with_timestamp: # Format timestamp hours, remainder = divmod(time_pos, 3600) minutes, seconds = divmod(remainder, 60) time_str = f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}" # Add timestamp position = (10, frame.shape[0] - 10) frame = cv2.putText(frame, time_str, position, font, font_scale, font_color, thickness, cv2.LINE_AA) # Resize thumbnail if scale != 1.0: new_width = int(width * scale) new_height = int(height * scale) thumbnail = cv2.resize(frame, (new_width, new_height)) else: thumbnail = frame thumbnails.append(thumbnail) # Release the video capture cap.release() # Process thumbnails based on output mode if grid: # Calculate grid dimensions grid_size = int(np.ceil(np.sqrt(len(thumbnails)))) rows = grid_size cols = grid_size # Create black canvas thumb_height, thumb_width = thumbnails[0].shape[:2] grid_img = np.zeros((thumb_height * rows, thumb_width * cols, 3), dtype=np.uint8) # Place thumbnails on grid for i, thumbnail in enumerate(thumbnails): if i >= rows * cols: break row = i // cols col = i % cols y_start = row * thumb_height y_end = y_start + thumb_height x_start = col * thumb_width x_end = x_start + thumb_width grid_img[y_start:y_end, x_start:x_end] = thumbnail # Save the grid if output_path is None: # Create default output path output_path = Path(f"{video_path.stem}_thumbnails.jpg") else: output_path = Path(output_path) cv2.imwrite(str(output_path), grid_img) return output_path else: # Save individual thumbnails if output_path is None: # Create default output directory output_path = Path(f"{video_path.stem}_thumbnails") output_path = Path(output_path) output_path.mkdir(parents=True, exist_ok=True) # Save each thumbnail saved_paths = [] for i, thumbnail in enumerate(thumbnails): thumb_path = output_path / f"thumb_{i:03d}.jpg" cv2.imwrite(str(thumb_path), thumbnail) saved_paths.append(thumb_path) return saved_paths