Skip to main content

AIND Video Utils

CI License ruff uv

Tools for working with video files using ffmpeg.

Prerequisites

This package requires ffmpeg and ffprobe to be installed and available on your PATH.

  • Linux: sudo apt install ffmpeg (Debian/Ubuntu) or sudo dnf install ffmpeg (Fedora)
  • macOS: brew install ffmpeg
  • Windows: Download from ffmpeg.org and add to PATH

Installation

# uv
uv add aind-video-utils                        # core only
uv add "aind-video-utils[transcode]"            # with transcode CLI
uv add "aind-video-utils[plotting]"             # with QC plotting

# pip
pip install aind-video-utils
pip install "aind-video-utils[transcode]"
pip install "aind-video-utils[plotting]"
Extra Adds
transcode pydantic-settings, rich — required for aind-transcode CLI
plotting matplotlib, opencv — required for aind-video-qc CLI

Usage

from aind_video_utils import probe, extract_luma_frame, extract_srgb_frame

# Probe video metadata
info = probe("video.mp4")
print(info["streams"][0]["pix_fmt"])  # e.g. "yuv420p", "gbrp"

# Extract luma (Y) plane at t=1.0s
luma, color_range, bit_depth = extract_luma_frame("video.mp4", 1.0)

# Extract an sRGB frame at t=1.0s
srgb = extract_srgb_frame("video.mp4", 1.0)

Encoding Profiles

This package is the canonical Python source for the AIND behavior video file standard encoding profiles. Four profiles are provided as frozen dataclass constants:

Constant Codec Pixel Format Container Use Case
OFFLINE_8BIT libx264 yuv420p mp4 Long-term storage (default)
OFFLINE_10BIT libx264 yuv420p10le mp4 Long-term storage, 10-bit
ONLINE_8BIT h264_nvenc yuv420p mkv Real-time acquisition
ONLINE_10BIT hevc_nvenc p010le mkv Real-time acquisition, 10-bit
from aind_video_utils import OFFLINE_8BIT, ONLINE_8BIT

# Inspect the exact ffmpeg args
OFFLINE_8BIT.ffmpeg_output_args()
ONLINE_8BIT.ffmpeg_input_args()

# Customize with replace()
fast = OFFLINE_8BIT.replace(codec_params=("-preset", "veryfast", "-crf", "18"))

For online acquisition pipelines that build their own ffmpeg command:

from aind_video_utils import ONLINE_8BIT

profile = ONLINE_8BIT
cmd = [
    "ffmpeg",
    *profile.ffmpeg_input_args(),
    "-f", "rawvideo", "-pix_fmt", "bgr24",
    "-s", f"{w}x{h}", "-r", str(fps),
    "-i", "pipe:0",
    *profile.ffmpeg_output_args(),
    str(output_path),
]

Transcoding Python API

from aind_video_utils import transcode_video, OFFLINE_8BIT

# Simplest form — offline 8-bit with automatic colorspace fix
transcode_video(input_path, output_path)

# Explicit profile
transcode_video(input_path, output_path, profile=OFFLINE_8BIT)

# Custom profile with speed override
fast = OFFLINE_8BIT.replace(codec_params=("-preset", "veryfast", "-crf", "18"))
transcode_video(input_path, output_path, profile=fast)

# Skip automatic setparams probing
transcode_video(input_path, output_path, auto_fix_colorspace=False)

# Legacy h264-in-AVI sources: re-stamp timestamps so ffmpeg keeps every frame
transcode_video(input_path, output_path, normalize_cfr=True)

transcode_video() raises RuntimeError when the output holds a different number of frames than ffmpeg decoded from the source; pass fail_on_frame_drop=False to accept such an output.

Preview and Poster Derivatives

transcode_video() can also write a browser-playable preview and a JPEG poster, as extra outputs of the same ffmpeg process:

from aind_video_utils import transcode_video

transcode_video(
    input_path,
    output_path,            # clip.mp4          archival, frame-exact
    preview_fps=30.0,       # clip_preview.mp4  every N-th source frame
    poster_at_seconds=1.0,  # clip_poster.jpg   sRGB still
)

N puts the preview between 25 and 35 fps, preferring a whole-number rate:

Source N Preview
1000 fps 40 25 fps
500 fps 20 25 fps
300 fps 10 30 fps
240 fps 8 30 fps
120 fps 4 30 fps
29.97 fps 1 29.97 fps

The poster branches off the source ahead of the BT.709 chain, because browsers read JPEG as sRGB. The behavior video standard gives the rationale for both.

To build the command yourself, attach derivatives to a conditioned profile. Without with_setparams, the poster's zscale has no transfer function to resolve, and its failure takes the archival encode down with it:

from aind_video_utils import (
    OFFLINE_8BIT, probe, with_poster, with_preview, with_setparams,
)

probe_json = probe(input_path)
conditioned = with_setparams(OFFLINE_8BIT, probe_json)
profile = with_poster(with_preview(conditioned, probe_json), probe_json)

profile.output_paths(output_path)  # [clip.mp4, clip_preview.mp4, clip_poster.jpg]
profile.ffmpeg_graph_args()        # ["-filter_complex", "[0:v]setparams=...,split=2[chain][d1];..."]
profile.ffmpeg_output_groups()     # one argument list per output, in the same order

ffmpeg_output_args() raises for a profile with derivatives, since one -vf cannot branch. The aind-transcode CLI does not expose derivatives yet.

Transcode CLI

With the transcode extra installed, the aind-transcode command is available:

aind-transcode videos/                                # defaults: offline-8bit, auto-fix
aind-transcode videos/ --profile offline-10bit        # explicit profile
aind-transcode videos/ --preset veryfast              # override speed
aind-transcode videos/ --crf 20 --preset veryfast     # override quality + speed
aind-transcode videos/ --no_auto_fix_colorspace true  # skip setparams probing
aind-transcode videos/ --normalize_cfr true           # legacy h264-in-AVI sources
aind-transcode videos/ --jobs 4                       # parallel workers

Settings can also be stored in aind-transcode.toml in the working directory:

input = ["videos/"]
output_dir = "transcoded"
profile = "offline-8bit"
overwrite = false

QC CLI

With the plotting extra installed, the aind-video-qc command is available:

# Compare linear-light input against BT.709-encoded output
aind-video-qc linear-to-bt709 input.mp4 output.mp4

# Compare ffmpeg luma extraction with OpenCV decode
aind-video-qc opencv input.mp4

# Options
aind-video-qc linear-to-bt709 input.mp4 output.mp4 --frame-time 1.5 --dpi 300 -o qc.png

QC Python API

from aind_video_utils.video_qc import compare_linear_to_bt709

fig = compare_linear_to_bt709("input.mp4", "output.mp4", frame_time=0)
fig.savefig("qc.png")

Development

uv sync                              # install all dev dependencies
./scripts/run_linters_and_checks.sh -c  # run full lint + test suite

Contributing

We use Conventional Commits:

<type>(<scope>): <short summary>

Types: feat, fix, docs, ci, build, perf, refactor, test

For internal members, please create a branch. For external members, please fork the repository and open a pull request from the fork.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Metadata

Release files for aind-video-utils 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aind-video-utils 0.7.0
File Size Uploaded
aind_video_utils-0.7.0.tar.gz 240.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aind-video-utils 0.7.0
File Interpreter ABI Platform
aind_video_utils-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 306.9 kB

Release files / aind_video_utils-0.7.0.tar.gz

Download URL aind_video_utils-0.7.0.tar.gz
Size 240.5 kB
Tags Source
SHA-256 checksum
How to use checksums
d36ec4036dc7c4ae08b519e23687cf430b342283b5be27a2e579fa3cebd4e14e
BLAKE2b-256 checksum
How to use checksums
d45af3aa6ed43566d638980f4749a58cbf46e8b49bc97bf69caccbb8e2e0cac6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / aind_video_utils-0.7.0-py3-none-any.whl

Download URL aind_video_utils-0.7.0-py3-none-any.whl
Size 66.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
768780577499ee2b2b537592e4c30afd058e2260a28bc0e2220789f35070ab0d
BLAKE2b-256 checksum
How to use checksums
3bb425dbd36b01d15a5af69280ac6fcbfd9b4f38e508e4a4fe03b18875a97a4c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page