Examples
The IO examples below (except PyTorch examples) use NumPy arrays with shape
(height, width, channels) and uint8 dtype.
Reading video metadata
from pvio.io import get_video_metadata, check_num_frames
# Number of frames in a video
n_frames = check_num_frames("example.mp4")
print(n_frames) # integer
# Full metadata — results are cached to a JSON file alongside the video. The cache
# stores the video's (size, mtime) signature and is invalidated automatically if
# the video changes, so using it is always safe.
# Control caching with the `cache_metadata` and `use_cached_metadata` arguments.
meta = get_video_metadata("example.mp4")
print(meta.n_frames, meta.frame_size, meta.fps) # VideoMetadata named tuple
Reading video frames
from pvio.io import read_frames_from_video
# Read all frames
frames, fps = read_frames_from_video("example.mp4")
# ... or just specific frames
frames, fps = read_frames_from_video("example.mp4", frame_indices=[0, 5])
Frames are returned at the source's native bit depth and channel layout: an
ordinary 8-bit video gives uint8 (H, W, 3) arrays, while a high-bit-depth
lossless source (e.g. 16-bit FFV1) gives uint16 arrays — and a source with an
alpha channel keeps it ((H, W, 4)). The real pixel values are preserved rather
than being truncated to 8 bits.
For sequential-only workloads on large videos, use iter_frames_from_video
instead: it yields the same per-frame arrays one at a time, without buffering
the whole video into memory.
from pvio.io import iter_frames_from_video
for frame in iter_frames_from_video("example.mp4"):
... # process one frame at a time
Writing a video
import numpy as np
from pvio.io import write_frames_to_video
# Create dummy 32×32 RGB frames (H, W, C)
frames = [np.full((32, 32, 3), fill_value=i, dtype=np.uint8) for i in range(10)]
write_frames_to_video("example.mp4", frames, fps=25.0)
# Override encoding quality — lower value = higher quality, larger file.
# `quality` is on the 0–51 H.264 scale and applies to both the CPU (libx264 CRF)
# and GPU (NVENC QP) paths. Default is 20 (conservative vs FFmpeg's 23); use 18
# for near-lossless output.
write_frames_to_video("example_hq.mp4", frames, fps=25.0, quality=18)
# Force the CPU encoder, or pass raw FFmpeg flags as an escape hatch.
write_frames_to_video(
"example_cpu.mp4", frames, fps=25.0,
mode="cpu", preset="slow",
extra_ffmpeg_params=["-level", "4.0"],
)
FFmpeg macroblock alignment
The writer verifies that all frames share the same (height, width). Some FFmpeg
builds require frame dimensions to be divisible by 16; use such dimensions to avoid
unexpected automatic resizing.
Combining image files into a video
When your frames already live on disk as individual image files, use
write_image_paths_to_video instead of loading them into arrays yourself. It takes
the same encoding options as write_frames_to_video (mode, quality, preset, …)
and reads the images lazily, one at a time, so an arbitrarily long sequence encodes
without holding every frame in memory.
from pathlib import Path
from pvio.io import write_image_paths_to_video
# Frames are encoded in the order given — sort the paths if order matters.
image_paths = sorted(Path("frames_dir").glob("frame*.png"))
write_image_paths_to_video("example.mp4", image_paths, fps=25.0)
The first image's (height, width) defines the output size; every other image must
match it. For combining frames straight from the command line, see the
command-line interface.
Using the PyTorch dataset and dataloader
VideoCollectionDataset iterates frames from video files or directories of image frames.
Wrap it in VideoCollectionDataLoader to load in parallel across workers — useful for
inference pipelines that process every frame independently.
TorchCodec is used for decoding.
Channels-height-width frame layout
Unlike other parts of this library, pvio.torch_tools yields frames in
(channels, height, width) (CHW) layout, float32 normalised to [0, 1],
as is standard in PyTorch.
from pvio.video import EncodedVideo # for encoded video files (e.g. MP4)
from pvio.video import ImageDirVideo # for directories of individual frame images
from pvio.torch_tools import VideoCollectionDataset, VideoCollectionDataLoader
# From video files
video1 = EncodedVideo("path/to/video1.mp4")
video2 = EncodedVideo("path/to/video2.mp4")
ds = VideoCollectionDataset([video1, video2])
# ... or from image-frame directories
video3 = ImageDirVideo("path/to/frames_dir1")
# Use a custom regex to control how frame indices are parsed from filenames
video4 = ImageDirVideo("path/to/frames_dir2", frame_id_regex=r"frame\D*(\d+)(?!\d)")
ds = VideoCollectionDataset([video3, video4])
# Apply a transform to each frame after loading
# (frames arrive as CHW float32 tensors in [0, 1])
def my_transform(frame):
return frame * 2.0
ds = VideoCollectionDataset([video1, video2], transform=my_transform)
# Larger buffer_size = faster decoding at the cost of memory (default: 64)
video_with_buffer = EncodedVideo("path/to/video.mp4", buffer_size=128)
ds = VideoCollectionDataset([video_with_buffer])
# Reading a single video from a single process (i.e. NOT in parallel DataLoader
# workers)? num_ffmpeg_threads=0 enables frame-parallel decoding — much faster.
# The default of 1 avoids core oversubscription across DataLoader workers.
video_solo = EncodedVideo("path/to/video.mp4", num_ffmpeg_threads=0)
# Wrap in a DataLoader — other DataLoader kwargs are forwarded as usual
loader = VideoCollectionDataLoader(ds, batch_size=8, num_workers=4)
for batch in loader:
frames = batch["frames"] # torch.Tensor: (B, C, H, W)
video_indices = batch["video_indices"] # list[int] — index into the videos list
frame_indices = batch["frame_indices"] # list[int] — virtual frame index
Using SimpleVideoCollectionLoader
SimpleVideoCollectionLoader combines dataset and dataloader creation in one call and
automatically constructs the right Video object from a path:
from pvio.torch_tools import SimpleVideoCollectionLoader
from pvio.video import EncodedVideo
# Mix paths to video files, image directories, and pre-built Video objects freely
videos = ["path/to/video1.mp4", "path/to/frames_dir/", EncodedVideo("path/to/video2.mp4")]
loader = SimpleVideoCollectionLoader(
videos,
batch_size=8,
num_workers=4,
transform=my_transform, # optional
buffer_size=64, # optional (for video files)
frame_id_regex=r"frame\D*(\d+)(?!\d)", # optional (for image directories)
)
for batch in loader:
frames = batch["frames"] # torch.Tensor: (B, C, H, W)
video_indices = batch["video_indices"] # list[int]
frame_indices = batch["frame_indices"] # list[int]