Source code for ssapy_toolkit.Plots.write_videos

from .write_gifs import _sort_frames
import numpy as np
import warnings
import cv2
import glob
from pathlib import Path


[docs] def write_video( video_name, frames, fps: int = 30, *, sort_frames: bool = True, warn_on_ambiguous: bool = True, target_size: tuple = None, # (width, height); if None, use first frame freeze_last_seconds: float = 0.0, # extra time to hold last frame ): """ Write frames to an MP4 video file, with natural sorting and uniform size. Parameters ---------- video_name : str Output path ending with .mp4 frames : iterable of str, str, or Path - Iterable of image file paths, OR - Path to a folder (all files in it are used), OR - Wildcard pattern string (e.g. 'frames_*.png', 'root/**/frame_*.png'). fps : int Frame rate. sort_frames : bool If True, apply natural sort to the input paths. warn_on_ambiguous : bool Emit warnings when sorting is ambiguous. target_size : (W, H) or None Output frame size in pixels. If None, use the size of the first frame. freeze_last_seconds : float Extra number of seconds to “freeze” the last frame by repeating it. """ # Normalize frames into a list of file paths if isinstance(frames, (str, Path)): pattern = str(frames) p = Path(pattern) if p.is_dir(): # All files directly under this directory paths = [str(x) for x in p.iterdir() if x.is_file()] else: # Treat as a glob pattern (supports *, ?, **) recursive = "**" in pattern paths = glob.glob(pattern, recursive=recursive) else: paths = list(frames) if not paths: raise ValueError("frames list is empty (no files found)") if sort_frames: paths = _sort_frames(paths, warn_on_ambiguous=warn_on_ambiguous) print(f"Writing video: {video_name}") # Read first frame to determine output size first = cv2.imread(paths[0]) if first is None: raise ValueError(f"Could not read first frame: {paths[0]}") if target_size is None: h, w = first.shape[:2] W, H = int(w), int(h) else: W, H = int(target_size[0]), int(target_size[1]) # Resize first frame if needed if (first.shape[1], first.shape[0]) != (W, H): first = cv2.resize(first, (W, H)) # Ensure 3-channel BGR if first.ndim == 2: first = cv2.cvtColor(first, cv2.COLOR_GRAY2BGR) fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v') writer = cv2.VideoWriter(video_name, fourcc, float(fps), (W, H)) if not writer.isOpened(): raise RuntimeError( f"Failed to open VideoWriter for '{video_name}'. " "Your OpenCV/FFmpeg build may not support this codec/container." ) last_frame = first writer.write(first) # Write remaining frames, always resizing to (W, H) for pth in paths[1:]: img = cv2.imread(pth) if img is None: warnings.warn(f"Could not read frame: {pth}; skipping.") continue if (img.shape[1], img.shape[0]) != (W, H): img = cv2.resize(img, (W, H)) if img.ndim == 2: img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) writer.write(img) last_frame = img # Freeze on last frame if freeze_last_seconds > 0 and last_frame is not None: extra_frames = int(np.round(freeze_last_seconds * float(fps))) for _ in range(extra_frames): writer.write(last_frame) writer.release() print(f"Wrote: {video_name}")