Skip to content

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]