"""
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