easyVmaf
Video Multi-Method Assessment Fusion (VMAF) is one of the most widely used metrics to assess the quality of encoded video. It is a full reference metric: it compares a distorted video (e.g., an encode) with its reference frame by frame. However, computing it correctly is tricky. Both videos must have the same resolution, frame rate, scan type and pixel format, and their first frames must be aligned in time; otherwise, the score measures the mismatch instead of the encoding.
easyVmaf is a Python tool, built on FFmpeg and FFprobe, that does this preprocessing for you:
- Deinterlacing
- Upscaling and downscaling to the display resolution
- Frame-accurate synchronization
- Frame rate adaptation
- Pixel format normalization
Since 4.0, easyVmaf computes VMAF v1
by default. The VMAF v0.6 models of easyVmaf 3.x are still available with
--vmaf-version 0.6. Upgrading from 3.x? See Migrating from 3.x.
On GitHub, this README follows the master branch, which can document changes
not released yet (see the unreleased entry of the
CHANGELOG). For
the version you installed (pip show easyvmaf), read its page on
PyPI or the README of its v<version> tag.
We explain how it works in this article, written for an earlier version of easyVmaf.
Contents
- Requirements
- Installation
- Usage
- Models
- Automatic VMAF v1 parameters
- Interlaced sources
- Frame ranges
- Examples
- Summary file
- Python API
- Docker
- Sync examples explained
- Known bugs
- Versioning
- Migrating from 3.x
Requirements
- Linux / macOS
- Python >= 3.10
- FFmpeg >= 8.1 built with
--enable-libvmaf - libvmaf >= 3.2.1 built with
-Dbuilt_in_models=true - Python package:
ffmpeg-progress-yield
easyVmaf checks FFmpeg and libvmaf at startup. It exits with an error if FFmpeg
is older than 8.1, or if its libvmaf cannot compute a frame with the VMAF v1
model vmaf_v1.0.16_3d0h.
Why libvmaf 3.2.1
The VMAF v1 models ship with libvmaf 3.2.0, but 3.2.0 is not enough:
- In 3.2.0 the
speed_chromafeature extractor, used by every v1 model, is only compiled with-Denable_float=true, which is off by default. A default 3.2.0 build loads the v1 models but fails to compute them (problem during vmaf_use_features_from_model). Fixed in 3.2.1 by PR #1558. - 3.2.0 can grow memory without bound when decoding is faster than feature extraction (issue #1587). v1 is slower to extract than v0.6, so the risk is higher. Fixed in 3.2.1.
- 3.2.1 also fixes wrong CUDA scores with inputs above 8 bits.
--gpuis not affected: it computes VMAF on the GPU only at 8 bits.
libvmaf 3.2.1 still reports itself as 3.2.0: pkg-config --modversion libvmaf
and the version field of the libvmaf log both say 3.2.0, because the upstream
v3.2.1 tag kept that version string. No version check can tell the two apart.
Hence, in practice, the only reliable test is to compute a frame with a v1 model, which is what
easyVmaf does at startup (see Verifying your build).
Building FFmpeg from source
Packaged builds that pass the check are listed in Installation. Otherwise, build libvmaf 3.2.1 with meson, then build FFmpeg >= 8.1 against it:
curl -LO https://github.com/Netflix/vmaf/archive/v3.2.1.tar.gz
tar -xzf v3.2.1.tar.gz
cd vmaf-3.2.1/libvmaf
meson setup build --buildtype release -Dbuilt_in_models=true
ninja -C build
sudo ninja -C build install
# then, in the FFmpeg >= 8.1 source tree
./configure --enable-libvmaf --enable-version3 && make && sudo make install
-Denable_float=true is not needed with 3.2.1.
Verifying your build
This command computes one frame with a VMAF v1 model. It is the same probe easyVmaf runs at startup:
ffmpeg -hide_banner -loglevel error \
-f lavfi -i color=black:s=320x240:r=1:d=1 \
-f lavfi -i color=black:s=320x240:r=1:d=1 \
-lavfi libvmaf=model=version=vmaf_v1.0.16_3d0h:log_fmt=json:log_path=/dev/null \
-f null -
The build is valid when the command exits with code 0 and prints nothing. The probe frames are 320x240 because the Spatial Efficient Entropic Differencing (SpEED) feature of v1 rejects frames smaller than about 288x162. That limit does not affect normal use: easyVmaf always scales the inputs to 1920x1080 or 3840x2160 before libvmaf.
GPU requirements
For VMAF on the GPU (--gpu with VMAF v0.6 at 8 bits, see GPU):
- NVIDIA GPU with CUDA support
- FFmpeg built with
--enable-ffnvcodec --enable-libvmaf - libvmaf built with
-Denable_cuda=true - NVIDIA Container Toolkit (for Docker GPU usage)
- For the CUDA image: NVIDIA driver >= 570 on the host. Its libvmaf is built with CUDA 12.8, whose PTX (ISA 8.7) needs that driver
For GPU decoding (--gpu with any model, see
Hardware decoding):
- NVIDIA GPU with NVDEC, its hardware video decoder
- FFmpeg built with
--enable-ffnvcodec --enable-cuda-llvm. To check it,ffmpeg -hwaccelsmust listcudaandffmpeg -filtersmust listscale_cuda; otherwise every input is decoded on the CPU.--enable-cuda-nvccalso works, but it requires--enable-nonfree, which makes the FFmpeg build not redistributable - libvmaf does not need CUDA for GPU decoding, only for VMAF on the GPU
- In Docker, the
videodriver capability (NVIDIA_DRIVER_CAPABILITIES=compute,utility,video), which the CUDA image sets
Installation
easyVmaf needs Python >= 3.10 and an FFmpeg build that passes the startup check. Install both.
easyVmaf
pipx install easyvmaf
# or
uv tool install easyvmaf
Both install the easyvmaf command in its own environment and put it on PATH,
so it runs from any directory. A plain pip install easyvmaf outside a virtual
environment fails with externally-managed-environment (PEP 668) on Homebrew
Python and on Debian 12 / Ubuntu 23.04 and later.
To use the Python API, install the package in a virtual environment:
python3 -m venv .venv
. .venv/bin/activate
pip install easyvmaf
FFmpeg
easyVmaf needs FFmpeg >= 8.1 with libvmaf >= 3.2.1 and its built-in models (see Requirements). After installing it, run the probe in Verifying your build.
macOS. Homebrew's FFmpeg is built against libvmaf 3.2.1:
brew update && brew install ffmpeg
# already installed: brew update && brew upgrade libvmaf ffmpeg
brew update matters when Homebrew does not update itself, e.g. with
HOMEBREW_NO_AUTO_UPDATE set: an old Homebrew index installs libvmaf 3.2.0,
which cannot compute VMAF v1.
Linux. Distribution packages are older than 8.1 (Ubuntu 24.04 ships FFmpeg 6.1). Use a static build from BtbN/FFmpeg-Builds, which includes libvmaf with its built-in models:
# x86_64; on arm64 use ffmpeg-n8.1-latest-linuxarm64-gpl-8.1
curl -LO https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n8.1-latest-linux64-gpl-8.1.tar.xz
tar -xf ffmpeg-n8.1-latest-linux64-gpl-8.1.tar.xz
export PATH="$PWD/ffmpeg-n8.1-latest-linux64-gpl-8.1/bin:$PATH"
Docker. The Docker images bundle easyVmaf, FFmpeg 8.1 and
libvmaf 3.2.1 (linux/amd64). They are published on the GitHub Container
Registry (GHCR) from the 5.0.0
release on (ghcr.io/gdavila/easyvmaf:5.0.0); before that, clone the repository
and build them with docker build -t easyvmaf ..
Other platforms or builds. See Building FFmpeg from source.
easyVmaf runs the ffmpeg and ffprobe found on PATH, or the ones given in
environment variables:
FFMPEG=/path/to/ffmpeg FFPROBE=/path/to/ffprobe easyvmaf ...
From source (development)
git clone https://github.com/gdavila/easyVmaf.git
cd easyVmaf
python3 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]" # or: uv venv && uv pip install -e ".[dev]"
python -m pytest -q
The integration tests run the FFmpeg on PATH; they are skipped when it has no
libvmaf, and the VMAF v1 ones when its libvmaf cannot compute a v1 frame.
Usage
easyvmaf -d <distorted> -r <reference> [options]
easyvmaf --help groups the options in the same sections as the tables below.
Input
| Flag | Default | Description |
|---|---|---|
-d, --distorted |
required | Distorted video path. Accepts a glob pattern for batch processing. |
-r, --reference |
required | Reference video path. |
--fps FPS |
0 |
Force frame rate conversion. Disables auto-deinterlace when set. |
Synchronization
| Flag | Default | Description |
|---|---|---|
--sync-window SW |
0 |
Sync window size in seconds. Enables automatic sync search: the first frames of the distorted video are looked for inside this window of the reference (of the distorted video with --sync-reverse). 0 disables sync. |
--sync-start SS |
0 |
Time into the reference (into the distorted video with --sync-reverse) where the sync window begins. Requires --sync-window. |
--sync-reverse |
off | Reverse the sync search: match the first frames of the reference against the distorted video. Requires --sync-window. |
--sync-only |
off | Measure the sync offset for every input and skip VMAF. Requires an explicit, finite --sync-window greater than zero. |
--sync-offset S |
0 |
Manual sync offset in seconds, instead of a sync search: positive trims the reference, negative trims the distorted video. Same sign as the reported offset. Cannot be combined with --sync-window. |
--shortest |
off | Stop when the shorter video ends instead of repeating its last frame. Use it when the inputs have different durations. |
Frame range
| Flag | Default | Description |
|---|---|---|
--start-frame N |
0 |
First frame to measure, numbered as in the log of the full calculation. See Frame ranges. |
--frame-count N |
to the end | Number of frames to measure. The last range of a video can have fewer. |
Models
| Flag | Default | Description |
|---|---|---|
--display {hd,4k} |
hd |
Target display. Inputs are scaled to 1920x1080 (hd) or 3840x2160 (4k). |
--vmaf-version {1,0.6} [...] |
1 |
VMAF generations to compute. --vmaf-version 1 0.6 computes both in one pass. |
--bitdepth {auto,8,10} |
auto |
Measurement bit depth. auto: 10 with any v1 model, otherwise the reference bit depth. See Pixel format. |
VMAF v1 parameters
These flags apply only to VMAF v1 models. Using them without 1 in
--vmaf-version is an error, except --hfr auto and --hfr off.
| Flag | Default | Description |
|---|---|---|
--view {3h,5h,phone,1.5h} [...] |
hd: 3h 5h; 4k: 1.5h |
Viewing distance, as a multiple of the screen height (3h: the viewer sits 3 screen heights away). The farther the viewer, the fewer artifacts are visible and the higher the score. phone is another name for 5h. With --display 4k, 3h uses a model whose scores go from 0 to 110 instead of 0 to 100. |
--hfr {auto,on,off} |
auto |
High frame rate models. auto: when the effective frame rate is >= 47 fps. See High frame rate. |
--enc-size WxH |
distorted video size | Encoding resolution passed to CAMBI, the v1 banding feature. |
--enc-bitdepth {8,10,12} |
from the distorted pix_fmt |
Encoding bit depth passed to CAMBI. |
--model-option FEATURE.OPTION=VALUE |
— | Advanced override of a v1 model option, e.g. cambi.topk=0.5. Repeatable. |
Output
| Flag | Default | Description |
|---|---|---|
--output-format {json,xml,csv} |
json |
Format of the per-frame libvmaf log. The summary file is always JSON. |
--cambi-heatmap |
off | Compute and save CAMBI banding heatmaps. |
--verbose |
off | Enable verbose log level. |
--progress |
off | Show FFmpeg progress during the VMAF computation. |
Every run writes two files next to the distorted video: the per-frame libvmaf
log (<distorted>_vmaf.json) and a summary file with the
final results (<distorted>_vmaf_summary.json).
Execution
| Flag | Default | Description |
|---|---|---|
--threads N |
0 |
Parallel single-threaded sync workers and libvmaf threads (0 = CPU count). |
--subsample N |
1 |
Frame subsampling factor to speed up the computation. |
--gpu |
off | Use the NVIDIA GPU where it can: decode on the GPU (NVDEC) each input it decodes exactly, never changing a score, and compute VMAF with libvmaf_cuda when the models allow it (--vmaf-version 0.6, 8 bits), otherwise on the CPU. Requires a CUDA build of FFmpeg (such as the CUDA Docker image). See GPU. |
--disable-hw-decode |
off | With --gpu: decode every input on the CPU. |
--disable-vmaf-cuda |
off | With --gpu: compute VMAF on the CPU (libvmaf). |
Validation
Invalid arguments are usage errors (exit code 2), reported on stderr before checking FFmpeg or probing videos. They are never converted to valid values or replaced with defaults.
--sync-window,--sync-startand--fpsaccept finite numbers greater than or equal to zero. Zero keeps the defaults: no sync search, a sync window that starts at the beginning, and automatic frame rate handling.--sync-offsetaccepts any finite number.--sync-startand--sync-reverseonly configure the sync search: without--sync-windowthey are an error that names the--sync-offsetequivalent, e.g.--sync-start 1.5 --sync-reverse→use --sync-offset -1.5.--sync-offsetand--sync-windoware mutually exclusive.--subsampleaccepts integers of at least 1;--threadsaccepts integers of at least 0.- Option values are case-insensitive (
--display 4K,--output-format XML,--model-option CAMBI.TOPK=0.5work). Flag names are not: write them in lowercase. --view,--hfr on,--enc-size,--enc-bitdepthand--model-optionrequire a VMAF v1 model in--vmaf-version.- A view that does not exist for the display is an error, e.g.
--view 5hwith--display 4k. --disable-hw-decodeand--disable-vmaf-cudarequire--gpu.--model-optionmust matchfeature.option=value, with lowercase names and a value made of letters, digits,_,.and-. Nothing else can reach the FFmpeg filter graph.- A sync window that ends after the video it searches (
--sync-start+--sync-windowlonger than the reference, or than the distorted video with--sync-reverse) is an error (exit code 1), reported before running FFmpeg. --start-frameaccepts integers of at least 0 and--frame-countintegers of at least 1. A frame range cannot be combined with--sync-only, and, for now, not with--subsample.- Removed flags are rejected with the name of their replacement, e.g.
error: -sw was removed, use --sync-windoworerror: --reverse was removed, use --sync-reverse.
Models
| Score name | libvmaf model | Version | Display | View | Range | Default |
|---|---|---|---|---|---|---|
vmaf_v1_hd |
vmaf_v1.0.16_3d0h |
1 | hd | 3h | [0, 100] | yes |
vmaf_v1_phone |
vmaf_v1.0.16_5d0h |
1 | hd | 5h | [0, 100] | yes |
vmaf_v1_4k |
vmaf_v1.0.16_1d5h_2160 |
1 | 4k | 1.5h | [0, 100] | yes |
vmaf_v1_4k_3h |
vmaf_v1.0.16_3d0h_2160 |
1 | 4k | 3h | [0, 110] | no (--view 3h) |
vmaf_hd |
vmaf_v0.6.1 |
0.6 | hd | — | [0, 100] | yes |
vmaf_hd_neg |
vmaf_v0.6.1neg |
0.6 | hd | — | [0, 100] | yes |
vmaf_hd_phone |
vmaf_v0.6.1 + enable_transform=true |
0.6 | hd | — | [0, 100] | yes |
vmaf_4k |
vmaf_4k_v0.6.1 |
0.6 | 4k | — | [0, 100] | yes |
All models are libvmaf built-in models. Each v1 model also has a high frame rate
(HFR) variant with _hfr_ in its id (e.g. vmaf_v1.0.16_hfr_3d0h). The HFR variant
keeps the score name; only libvmaf_model changes.
VMAF v1 differs from v0.6 in ways that change the scores:
- v1 uses the
cambi(Contrast Aware Multiscale Banding Index),speed_chroma_uv,adm3andmotion3features. VIF is gone. v1 measures chroma and banding; v0.6 measures luma only. - The v1 phone model is a separate 5H model, not the score transform of v0.6. The v1 models already enable their own score transform.
- The 4K model at 3H (
vmaf_v1_4k_3h) ranges from 0 to 110.
v1 and v0.6 scores are not comparable. A v1 score of 85 is not a v0.6 score of 85. That is why every v1 score name starts with
vmaf_v1_and never reusesvmaf_hdorvmaf_4k. To recalibrate quality thresholds, compute both generations in one pass with--vmaf-version 1 0.6.
Automatic VMAF v1 parameters
easyVmaf resolves the v1 parameters from the stream information it already
probes, and from the effective frame rate after deinterlacing or --fps.
| Parameter | Automatic value | Override |
|---|---|---|
| Measurement pixel format | Chroma subsampling of the reference; 10 bits with any v1 model | --bitdepth |
| CAMBI encoding resolution | Width and height of the distorted video | --enc-size WxH |
| CAMBI encoding bit depth | From the distorted pix_fmt (yuv420p → 8, yuv420p10le → 10; no digits → 8) |
--enc-bitdepth |
| HFR models | Effective frame rate >= 47 fps | --hfr on/off |
| Other model options | — | --model-option |
Pixel format
libvmaf needs the same pixel format on both inputs. Without an explicit format, FFmpeg picks one and may convert the reference down, for example from 4:2:2 to 4:2:0, or from 10 to 8 bits. v1 measures chroma, so that conversion would erase exactly the chroma loss v1 is meant to measure.
easyVmaf measures both inputs in one format. We prefer to leave the reference unmodified, so the measurement format is never lower than the reference's:
- Chroma subsampling: the reference's (4:2:0, 4:2:2 or 4:4:4). An unrecognized reference format is measured as 4:2:0, with a warning in the log.
- Bit depth: 10 when any v1 model is computed (or the reference bit depth, if
higher); the reference bit depth with only v0.6 models; or
--bitdepth. Netflix recommends measuring v1 at 10 bits for SDR content, even when the encode is 8 bits, to capture banding.
Each input whose native format differs gets a format= filter as the first
filter of its chain, before scaling and deinterlacing. For example, a yuv422p
reference with v1 is measured in yuv422p10le; a yuv420p reference with only
v0.6 is measured in yuv420p, with no filter if the distorted video is already
yuv420p. The format used is reported as pix_fmt in the results. Netflix does
not document whether the v1 models are calibrated for 4:2:2 or 4:4:4, so
pix_fmt lets you identify those measurements later.
CAMBI encoding resolution
easyVmaf always scales the distorted video to the display resolution. Without
the encoding resolution, CAMBI would measure banding on the scaled picture.
easyVmaf passes the distorted video size and bit depth as cambi.enc_width,
cambi.enc_height and cambi.enc_bitdepth to every v1 model.
libvmaf 3.2.1 CAMBI rejects encoding sizes below 180x150, or with both sides below 216, and the whole v1 calculation fails. Low rungs of an adaptive bitrate (ABR) ladder, such as 256x144, 192x108 or 160x90 fall below that limit. easyVmaf raises the size to the smallest accepted one with the same aspect ratio and logs a warning:
CAMBI does not accept a 256x144 encoding size; using 267x150, the smallest accepted size with the same aspect ratio
The same adjustment applies to --enc-size.
High frame rate
The HFR variants of the v1 models use a five-frame motion window and are
calibrated for ~50/60 fps. With --hfr auto, easyVmaf uses them when the
effective frame rate of the distorted video is at least 47 fps. The effective
frame rate is the distorted frame rate after deinterlacing and --fps, not the
container value. Above 60 fps, easyVmaf still uses the HFR
variants and logs a warning, since that is outside their calibration.
Interlaced sources
easyVmaf detects interlacing by sampling the first 5 seconds of each input and
deinterlaces with yadif when only one of them is interlaced. Deinterlacing
runs before scaling, so the two fields of an SD or 720i picture are never
blended, and the sync search aligns on single fields.
In the table, 1080i25 is an interlaced video with 25 frames (50 fields) per
second; the 29.97/59.94 family behaves the same (1080i29.97 vs 59.94p).
1080i25* is the same video when ffprobe reports it at its field rate
(r_frame_rate of 50, typical of H.264 Picture-Adaptive Frame-Field (PAFF)
broadcast streams).
| Reference | Distorted | Typical case | What easyVmaf does | Compared at | Supported |
|---|---|---|---|---|---|
1080i25 |
25p |
Broadcast source, OTT rendition at frame rate | Reference: one frame per frame (yadif=0) |
25 fps | Yes |
1080i25 |
50p |
Broadcast source, OTT rendition at field rate | Reference: one frame per field (yadif=1) |
50 fps (HFR models with v1) | Yes |
25p |
1080i25 |
Progressive master, interlaced broadcast | Distorted: one frame per frame | 25 fps | Yes |
50p |
1080i25 |
50p master, interlaced broadcast | Distorted: one frame per field | 50 fps (HFR models with v1) | Yes |
1080i25* |
25p |
PAFF source, OTT rendition at frame rate | Reference: one frame per frame | 25 fps | Yes |
25p |
1080i25* |
Progressive master, PAFF broadcast | Distorted: one frame per frame | 25 fps | Yes |
1080i25 |
1080i25 |
Interlaced transcode | Nothing is deinterlaced | 25 fps, both fields woven | No |
1080i25* |
50p |
PAFF source, OTT rendition at field rate | Reference: one frame per frame, instead of per field | Reference at 25 vs distorted at 50: half of the pairs are 20 ms off | No |
50p |
1080i25* |
50p master, PAFF broadcast | Distorted: one frame per frame, instead of per field | 25 fps, only the first field (HFR models picked as if 50 fps) | No |
Any other frame rate pair with an interlaced input (for example 1080i25 vs
30p) stops with an error that suggests --fps. --fps forces a common frame
rate on both inputs but does not deinterlace, so it is a workaround, not a fix,
for the unsupported rows.
To check whether an interlaced file is reported at its field rate:
ffprobe -v error -select_streams v:0 \
-show_entries stream=r_frame_rate,avg_frame_rate,field_order -of default=nw=1 file.ts
An interlaced field_order (tt, bb, tb, bt) with r_frame_rate twice
avg_frame_rate (for example 50/1 and 25/1) is a 1080i25* input.
Frame ranges
Measuring a long video takes time, and a single calculation runs on one
machine. --start-frame and --frame-count measure a range of frames instead
of the whole video, without cutting or re-encoding the inputs. Frames are numbered as
in the log of the full calculation: after sync, deinterlacing and frame rate
conversion, at the frame rate libvmaf receives.
A range returns exactly the frames of the full calculation, with the same frame
numbers and identical scores. Consecutive ranges therefore join into the full
calculation: concatenating their logs gives its log, and its mean is the mean
of the joined frames, or the mean of each range weighted by its
vmaf.range.frames_scored. This lets a higher layer split a long video into
ranges and compute them in parallel, on one machine or on several:
# 1. Sync once
easyvmaf -d distorted.mp4 -r reference.mp4 --sync-window 3 --sync-only # sync.offset: 1.5
# 2. One range per process or instance, with the same offset and options
easyvmaf -d distorted.mp4 -r reference.mp4 --sync-offset 1.5 --start-frame 0 --frame-count 9000
easyvmaf -d distorted.mp4 -r reference.mp4 --sync-offset 1.5 --start-frame 9000 --frame-count 9000
A range can also search the sync itself with --sync-window: the search always
runs at the beginning of the videos and finds the same offset for every range,
but each range then repeats its cost.
How a range keeps the frames of the full calculation:
- Each input is read from shortly before the range (
-ss), keeping its original timestamps, so deinterlacing and frame rate conversion select the same frames. - The first frame the full calculation measures is found by running the same filters up to it, and each range counts its frames from there.
- Two extra frames are measured on each side of the range and dropped from its log: the motion features of a frame depend on the previous and the next frame.
Each range writes its own log, named after the requested frames:
<distorted>_vmaf_f<first>-<last>.{json,xml,csv} (-end without
--frame-count). Its frames keep their frame numbers in the full calculation,
and its pooled metrics cover only the range. Its summary file is named after
the log (<distorted>_vmaf_f<first>-<last>_summary.json), so ranges computed in
parallel never overwrite each other's results.
With --cambi-heatmap, a range writes its heatmaps to
<distorted>_cambi_heatmap_f<first>-<last>/, and its summary points to that
directory (vmaf.cambi_heatmap_path). Each file holds only the pictures of the
range: concatenating the files of the same name of consecutive ranges, in range
order, gives the heatmaps of the full calculation byte for byte (with more
than one thread, see Known bugs).
Limits:
- Only MP4, MOV, Matroska and WebM inputs. Other containers are an error (exit code 1): MPEG-TS seeks to the next keyframe, which would silently score other frames, and raw elementary streams have no timestamps.
- A range that starts after the last frame is an error. A range that loses frames before the end of the videos, e.g. after an inexact seek, is an error too, never a partial result.
- Without a sync offset, both videos must start at the same timestamp.
- Not yet with
--subsample.
With --gpu, ranges join into the full GPU calculation in the same way:
libvmaf_cuda gives identical values run after run, and the seek, the sync
trims and the range trim select the same frames on the CPU and, for an input
that stays on the GPU after hardware decoding, on the GPU.
Examples
Basic VMAF (no sync)
# VMAF v1 HD: vmaf_v1_hd and vmaf_v1_phone
easyvmaf -d distorted.mp4 -r reference.mp4
Model selection
# VMAF v1 4K at 1.5H
easyvmaf -d distorted.mp4 -r reference.mp4 --display 4k
# Both v1 4K models, 1.5H and 3H ([0, 110])
easyvmaf -d distorted.mp4 -r reference.mp4 --display 4k --view 1.5h 3h
# Only the v1 phone model
easyvmaf -d distorted.mp4 -r reference.mp4 --view phone
# VMAF v0.6 models, as in easyVmaf 3.x: vmaf_hd, vmaf_hd_neg, vmaf_hd_phone
easyvmaf -d distorted.mp4 -r reference.mp4 --vmaf-version 0.6
# v1 and v0.6 in one pass, to compare both generations
easyvmaf -d distorted.mp4 -r reference.mp4 --vmaf-version 1 0.6
CAMBI encoding parameters
# The distorted file was upscaled from a 1280x720 8-bit encode
easyvmaf -d distorted.mp4 -r reference.mp4 --enc-size 1280x720 --enc-bitdepth 8
# CAMBI heatmaps, written to <distorted>_cambi_heatmap/
easyvmaf -d distorted.mp4 -r reference.mp4 --cambi-heatmap
With v1 models the heatmap directory is passed inside the libvmaf model=
option, where :, |, \ and ' cannot be escaped. If the distorted file path
contains any of them, easyVmaf stops with an error before running FFmpeg; rename
or move the file.
The heatmaps are of the distorted picture: one raw 16-bit gray file per CAMBI
scale (cambi_heatmap_scale_<s>_<W>x<H>_16b.gray, 5 scales), with picture n at
n × picture size. <W>x<H> is the size of the scale computed from the
encoding size, not always the picture size: from a 1920x1080 encoding size the
v1 models compute CAMBI at half resolution (cambi_high_res_speedup), so every
scale holds pictures of half the size in its name (scale 0 of a 1080p encoding
size, ..._1920x1080_16b.gray, holds 960x540 pictures). The picture size in
bytes is the file size divided by the frames scored.
With automatic sync
# Sync window of 2 seconds starting from the beginning of reference
easyvmaf -d distorted.mp4 -r reference.mp4 --sync-window 2
# Sync window starting at 6 s into the distorted video, reverse direction
easyvmaf -d distorted.mp4 -r reference.mp4 --sync-window 3 --sync-start 6 --sync-reverse
Human-readable output of the first command (logs go to stderr):
=======================================
Results: distorted.mp4
=======================================
VMAF computed
=======================================
offset: 1.5 | psnr: 40.032121
pix_fmt: yuv420p10le | HFR: off
vmaf_v1_hd 93.256258 [vmaf_v1.0.16_3d0h, 0-100]
vmaf_v1_phone 95.393178 [vmaf_v1.0.16_5d0h, 0-100]
VMAF output file path: distorted_vmaf.json
Summary file path: distorted_vmaf_summary.json
The offset of 1.5 s means that the first frame of distorted.mp4 matches the
reference 1.5 s in. So easyVmaf trimmed the first 1.5 s of the reference before
computing VMAF; 40.03 dB is the PSNR of that best match.
Without a sync search, --sync-offset X applies a manual offset: positive to trim
the reference, negative to trim the distorted video. Automatic offsets use the
same sign convention in the summary file and the human output, so the offset reported by
--sync-only can be passed to --sync-offset as is. A zero manual offset is reported
as 0.0, including --sync-offset -0.
Sync measurement only
--sync-only requires an explicit, finite --sync-window greater than zero.
Missing, zero, negative, NaN, or infinite windows are usage errors (exit code 2),
reported before checking FFmpeg or probing videos. Every matched input is
synchronized, with one result per file. No VMAF calculation or libvmaf log is
produced: the summary file of each input is <distorted>_sync_summary.json and
contains only schema_version, distorted, reference, and sync.
easyvmaf -d distorted.mp4 -r reference.mp4 --sync-window 2 --sync-only
# Batch: one summary file per matched file
easyvmaf -d "folder/*.mp4" -r reference.mp4 --sync-window 2 --sync-only
Batch processing
# Glob pattern — one log and one summary file per file
easyvmaf -d "folder/*.mp4" -r reference.mp4
GPU
# Decode on the GPU, compute v0.6 with libvmaf_cuda
easyvmaf -d distorted.mp4 -r reference.mp4 --vmaf-version 0.6 --gpu
# Decode on the GPU, compute the default v1 models on the CPU
easyvmaf -d distorted.mp4 -r reference.mp4 --gpu
--gpu uses an NVIDIA GPU (CUDA) for every part of the calculation it can do
without changing a score, and the CPU for the rest. Each part has an opt-out:
| Flags | Decoding | VMAF |
|---|---|---|
| (none) | CPU | CPU (libvmaf) |
--gpu |
GPU (NVDEC) where the input allows it | GPU (libvmaf_cuda) where the models allow it, else CPU |
--gpu --disable-hw-decode |
CPU | as --gpu; with v0.6, the --gpu of earlier versions |
--gpu --disable-vmaf-cuda |
as --gpu |
CPU |
--disable-hw-decode and --disable-vmaf-cuda without --gpu are a usage
error. The sync search always runs on the CPU.
VMAF on the GPU. libvmaf_cuda only computes the v0.6 models at 8 bits:
libvmaf 3.2.1 has no CUDA extractors for the v1 features (cambi,
speed_chroma, adm3, motion3). With any v1 model, including the default,
or with --bitdepth 10, --gpu computes VMAF on the CPU, decodes on the GPU,
says why in the log and records "cuda": false in the summary:
VMAF runs on the CPU (libvmaf): libvmaf_cuda cannot compute VMAF v1 models
With libvmaf_cuda both inputs are measured in yuv420p. Its scores differ
from the CPU scores by thousandths, a difference of libvmaf's own CUDA
extractors (Netflix/vmaf#1644,
about 0.003 VMAF per frame in motion;
Netflix/vmaf#1564, ADM rounding),
so compare scores computed the same way. vmaf.cuda in the summary says which.
Hardware decoding
Decoding on the GPU never changes a score: a frame stays on the GPU only for the steps that give exactly the CPU's frames.
- When VMAF runs on the CPU (v1,
--bitdepth 10,--disable-vmaf-cuda), each decoded frame is copied back to the CPU right away and goes through the usual chain. - With
libvmaf_cuda, an input already at the display resolution (1920x1080 or 3840x2160), inyuv420pand not deinterlaced (progressive, or with--fps) stays on the GPU up tolibvmaf_cuda. Scaling, deinterlacing and 10 to 8 bit conversion give different frames on the GPU. For instance, in our tests, upscaling a 720p H.264 encode to 1080p withscale_cudainstead of FFmpeg'sscalelowered VMAF v1 by 2.2 points. So an input that needs any of them is copied back to the CPU after decoding: only its decoding moves to the GPU.
In practice, it helps when software decoding is what keeps the CPU busy, as with high-bitrate inputs. We measured it with the equivalent FFmpeg commands (not an easyVmaf benchmark), on a Tesla T4 with 4 vCPU, one run each, with a synthetic 1080p25 H.264 distorted video against a 268 Mb/s H.264 reference:
| Calculation | CPU decoding | NVDEC | Score |
|---|---|---|---|
v0.6 with libvmaf_cuda, 1200 frames |
55.2 s (212 s of CPU) | 12.8 s (7 s of CPU) | identical |
| v1 on the CPU, 300 frames | 22.6 s | 11.1 s | identical |
The gain depends on the codec, the bitrate and the CPU: with a light input and a fast CPU, software decoding is cheap and the gain is smaller.
Inputs decoded on the GPU: H.264 8-bit 4:2:0, HEVC 8 and 10-bit 4:2:0 and VP9
8-bit 4:2:0, the decoders validated as bit-exact. Each one is first checked by
decoding one frame, since what NVDEC supports depends on the GPU. Any other
input (MPEG-2, whose NVDEC output differs, AV1, ProRes, FFV1, H.264 High 10 or
4:2:2, ...), or one this GPU or FFmpeg build cannot decode, is decoded on the
CPU with a warning, and the summary records it as "sw":
Decoding the reference video ref.mov in software: cuda decoding of prores yuv422p10le is not supported
"hw_decode": {"api": "cuda", "distorted": "hw", "reference": "sw"}
What runs on the GPU
We split every step of the calculation into three groups, based on what we measured on a Tesla T4 with FFmpeg 8.1 and libvmaf 3.2.1.
On the GPU with --gpu:
| Step | On the GPU | Score |
|---|---|---|
| Decoding (H.264, HEVC and VP9, see above) | NVDEC | Unchanged: bit-exact |
Format conversion without resizing (nv12 → yuv420p) |
scale_cuda |
Unchanged: bit-exact |
Frame selection and timestamps (fps, trim, sync, frame ranges) |
Same filters on GPU frames | Unchanged: the same frames |
| VMAF v0.6 features (ADM, VIF, motion) | libvmaf_cuda |
Differs from the CPU by thousandths (see GPU) |
Possible on the GPU, but kept on the CPU because the score changes. The error is the typical difference per pixel between the CPU and the GPU output, in 8-bit levels (0 to 255; one level is the smallest possible step):
| Step | CUDA filter | Error per pixel | Effect on VMAF |
|---|---|---|---|
| Scaling (e.g. 720p → 1080p) | scale_cuda |
1.0 level | VMAF v1 −2.2 points in our test |
| Deinterlacing | yadif_cuda |
1.1 levels | Not measured |
| 10 to 8 bit conversion | scale_cuda |
0.6 levels | Not measured |
| MPEG-2 decoding | NVDEC | 0.13 levels | Not measured |
These differences are invisible, but they are systematic on edges and detail, which is what VMAF weighs: a scaler that differs by one level per pixel moved VMAF v1 by 2.2 points. Hence, in practice, an input that needs one of these steps is copied back to the CPU after decoding.
Not available on the GPU:
| Step | Why | Consequence |
|---|---|---|
| PSNR in the libvmaf log | Neither libvmaf nor FFmpeg has a CUDA PSNR | With libvmaf_cuda, both frames are copied to the CPU for it. It stays: without a CPU feature, libvmaf 3.2.1 crashes with n_threads set |
| PSNR of the sync search | FFmpeg's psnr filter is CPU only |
The sync search always runs on the CPU |
VMAF v1 features (cambi, speed_chroma, adm3, motion3) |
libvmaf has no CUDA version | With v1, VMAF runs on the CPU (vmaf.cuda: false) |
CAMBI heatmaps (--cambi-heatmap) |
CAMBI is CPU only | Frames are copied to the CPU for it |
| VMAF at 10 bits | easyVmaf measures in yuv420p with libvmaf_cuda |
With --bitdepth 10, VMAF runs on the CPU |
| Other codecs (ProRes, FFV1, H.264 High 10 or 4:2:2, AV1, ...) | No NVDEC support, or not validated as bit-exact | Decoded on the CPU, recorded as "sw" |
Requirements: see GPU requirements. An FFmpeg build
without libvmaf_cuda exits with code 1 only when VMAF would run on the GPU;
one without NVDEC decodes every input on the CPU, with the warning above. In
Docker, NVDEC needs the video driver capability, which the CUDA image sets.
Roadmap: an explicit option to run the whole chain on the GPU, scaling and deinterlacing included, which changes the scores; and other APIs (VideoToolbox, VAAPI, QSV, Vulkan).
Summary file
Every run writes the final results of each input to a JSON summary file, next
to the libvmaf log and named after it, with schema_version: 2:
| Run | Summary file |
|---|---|
| VMAF | <distorted>_vmaf_summary.json |
| Frame range | <distorted>_vmaf_f<first>-<last>_summary.json |
--sync-only |
<distorted>_sync_summary.json |
The path is printed at the end of each result (Summary file path:). For
easyvmaf -d distorted.mp4 -r reference.mp4 --sync-window 2,
distorted_vmaf_summary.json contains:
{
"schema_version": 2,
"distorted": "distorted.mp4",
"reference": "reference.mp4",
"sync": {"offset": 1.5, "psnr": 40.032121},
"vmaf": {
"display": "hd",
"pix_fmt": "yuv420p10le",
"hfr": false,
"cuda": false,
"scores": {"vmaf_v1_hd": 93.256258, "vmaf_v1_phone": 95.393178},
"models": [
{"name": "vmaf_v1_hd", "libvmaf_model": "vmaf_v1.0.16_3d0h",
"vmaf_version": "1", "view": "3h", "range": [0, 100]},
{"name": "vmaf_v1_phone", "libvmaf_model": "vmaf_v1.0.16_5d0h",
"vmaf_version": "1", "view": "5h", "range": [0, 100]}
],
"output_file": "distorted_vmaf.json"
}
}
| Field | Description |
|---|---|
schema_version |
2 since easyVmaf 4.0. Present in every record, including --sync-only. |
sync.offset |
Offset in seconds. Positive: the reference was trimmed; negative: the distorted video was trimmed. |
sync.psnr |
Sync PSNR, or null when sync was not calculated or is not finite. |
vmaf.display |
hd or 4k. |
vmaf.pix_fmt |
Pixel format both inputs were measured in. |
vmaf.hfr |
true when the v1 HFR variants were used. |
vmaf.cuda |
true when VMAF was computed on the GPU (libvmaf_cuda); false without --gpu, and with --gpu when the models need the CPU or with --disable-vmaf-cuda. See GPU. |
vmaf.scores |
Mean score of each model over all frames, keyed by score name. |
vmaf.models |
One entry per score: libvmaf model id (the _hfr_ variant when HFR is on), VMAF version, view (default, neg or phone for v0.6) and score range. |
vmaf.output_file |
Per-frame libvmaf log: same directory and base name as the distorted video, plus _vmaf.{json,xml,csv} (_vmaf_f<first>-<last>.{json,xml,csv} for a frame range). |
vmaf.cambi_heatmap_path |
Heatmap directory, only with --cambi-heatmap. |
vmaf.hw_decode |
Only with hardware decoding (--gpu without --disable-hw-decode): api (cuda), and "hw" or "sw" for the distorted and reference inputs. See Hardware decoding. |
vmaf.range |
Only with a frame range: start_frame, frame_count (null without --frame-count) and frames_scored, which is lower than frame_count for the last range of a video. |
vmaf.scores is the flat view for quick reads; vmaf.models carries the context
needed to avoid comparing scores of different generations.
Each summary is strict JSON. Logs, progress, diagnostics, and help associated with usage errors go to stderr; the human-readable results go to stdout. If a file fails, the command stops with a nonzero exit code and writes no summary for that file; the summaries of earlier files in a batch remain. A summary left by a previous run is not removed.
SIGINT (Ctrl-C) exits with code 130 and reports the interruption on stderr. The interrupted calculation writes no summary, and the summaries of the files already completed in a batch remain. easyVmaf stops the running VMAF process and waits a bounded time for it to exit; during an automatic sync search, it can still wait for the running PSNR workers to finish.
Finite sync PSNR values keep their numeric value, rounded to six decimal places.
When sync PSNR was not calculated, sync.psnr is null with no status field.
For nonfinite PSNR, sync.psnr is also null, and sync.psnr_status identifies
the value as "positive_infinity", "negative_infinity", or "nan". Identical
frames legitimately produce positive infinity. This representation affects only
the summary file; the Python API and sync calculation retain the numeric value.
Nonfinite offsets or VMAF scores cause an error before the summary is written.
Python API
The CLI is a client of this API: each flag is an argument of the same name
(--sync-offset is sync_offset=, --output-format is output_format=).
from easyvmaf import Vmaf
v = Vmaf('distorted.mp4', 'reference.mp4', display='hd', vmaf_versions=('1', '0.6'))
offset, psnr = v.sync(2) # optional: sync window of 2 s (SyncResult)
result = v.compute() # VmafResult
result.scores # {'vmaf_v1_hd': ..., 'vmaf_v1_phone': ..., 'vmaf_hd': ..., ...}
result.pix_fmt # 'yuv420p10le'
result.log_path # 'distorted_vmaf.json'
result.sync_offset # the offset applied, in seconds
# Frames 9000-17999 of the full calculation, with a manual offset
v = Vmaf('distorted.mp4', 'reference.mp4', sync_offset=1.5,
start_frame=9000, frame_count=9000)
result = v.compute()
result.frames_scored # 9000, or fewer for the last range
sync(window, start=0, reverse=False) is --sync-window, --sync-start and
--sync-reverse: it returns SyncResult(offset, psnr) and stores the offset in
v.sync_offset, which compute() applies. All Vmaf() arguments after the two
paths are keyword-only: display, vmaf_versions, views, hfr, bitdepth,
enc_size, enc_bitdepth, model_options, output_format, loglevel,
subsample, threads, progress, shortest, fps, cambi_heatmap, gpu,
sync_offset, start_frame, frame_count, disable_hw_decode and
disable_vmaf_cuda. result.cuda and result.hw_decode are the summary's
vmaf.cuda and vmaf.hw_decode (None without hardware decoding).
The public API is what the easyvmaf package exports: Vmaf, VmafResult,
SyncResult, validate_model_config, validate_range_config,
UnsupportedModelConfigError, UnsupportedRangeError,
UnsupportedFramerateError, FFmpegExecutionError, ModelSpec, ModelRun,
CATALOG, select_models, check_ffmpeg and __version__. The submodules
(easyvmaf.vmaf, easyvmaf.ffmpeg, ...) are internal and can change in any
release.
Docker
From the 5.0.0 release on, every release publishes two images on the GitHub
Container Registry, ghcr.io/gdavila/easyvmaf, built from this repository's
Dockerfile and Dockerfile.cuda:
| Image | Tags | Contents |
|---|---|---|
| CPU | 5.0.0, 5.0, 5, latest |
easyVmaf, FFmpeg 8.1, libvmaf 3.2.1 |
| CUDA | 5.0.0-cuda, 5.0-cuda, 5-cuda, latest-cuda, cuda |
The same, plus libvmaf_cuda, NVDEC and the CUDA filters, on the nvidia/cuda 12.8 base image (NVIDIA driver >= 570) |
A pre-release (5.1.0rc1) is tagged only with its full version (5.1.0rc1,
5.1.0rc1-cuda). Both images are linux/amd64 only: on Apple Silicon, Docker
runs them emulated, which is slower; there, install easyVmaf and FFmpeg
with Homebrew instead (see Installation).
docker pull ghcr.io/gdavila/easyvmaf:5.0.0
docker pull ghcr.io/gdavila/easyvmaf:5.0.0-cuda
Running with Docker
# CPU
docker run --rm -v /path/to/videos:/videos \
ghcr.io/gdavila/easyvmaf:5.0.0 -d /videos/distorted.mp4 -r /videos/reference.mp4
# With sync
docker run --rm -v /path/to/videos:/videos \
ghcr.io/gdavila/easyvmaf:5.0.0 -d /videos/distorted.mp4 -r /videos/reference.mp4 --sync-window 2
# GPU (requires NVIDIA Container Toolkit): GPU decoding and libvmaf_cuda (v0.6)
docker run --rm --gpus all -v /path/to/videos:/videos \
ghcr.io/gdavila/easyvmaf:5.0.0-cuda -d /videos/distorted.mp4 -r /videos/reference.mp4 --vmaf-version 0.6 --gpu
# GPU decoding, default v1 models on the CPU
docker run --rm --gpus all -v /path/to/videos:/videos \
ghcr.io/gdavila/easyvmaf:5.0.0-cuda -d /videos/distorted.mp4 -r /videos/reference.mp4 --gpu
The CUDA image sets NVIDIA_DRIVER_CAPABILITIES=compute,utility,video: NVDEC
needs the video capability. Without it every input is decoded on the CPU,
with a warning. An image built from an older Dockerfile.cuda lacks it:
rebuild it, or pass -e NVIDIA_DRIVER_CAPABILITIES=compute,utility,video to
docker run.
Building the images
Instead of pulling them, build the images from a clone of the repository, and
use these names in place of ghcr.io/gdavila/easyvmaf:5.0.0 and :5.0.0-cuda
in the examples above:
docker build -t easyvmaf . # CPU
docker build -f Dockerfile.cuda -t easyvmaf:cuda . # CUDA
The CUDA image builds FFmpeg without --enable-nonfree: its CUDA filters are
compiled with clang (--enable-cuda-llvm) instead of nvcc, and libnpp is left
out, so the image can be redistributed. FFmpeg is LGPL-3.0-or-later in both
images (--enable-version3, no --enable-gpl).
The license texts of what each image builds from source (FFmpeg, libvmaf, and
in the CUDA image dav1d and nv-codec-headers) are in
/usr/local/share/licenses. FFmpeg is unmodified; ffmpeg/SOURCE there says
where its source code is: the archive the image was built from, with its
sha256 and the commit of its tag:
docker run --rm --entrypoint cat ghcr.io/gdavila/easyvmaf:5.0.0 /usr/local/share/licenses/ffmpeg/SOURCE
Build arguments
Both Dockerfiles accept these build-time arguments:
| ARG | Default | Description |
|---|---|---|
FFMPEG_version |
8.1 |
FFmpeg release tag (>= 8.1) |
VMAF_version |
3.2.1 |
libvmaf release tag (>= 3.2.1) |
EASYVMAF_VERSION |
5.0.0 |
easyVmaf version label (the published images take it from the release tag) |
DAV1D_version |
1.4.3 |
dav1d release (CUDA image only — built from source) |
NVCODEC_version |
13.0.19.1 |
nv-codec-headers release (CUDA image only). 13.0.19.1 is the only release that builds both FFmpeg 8.1 and libvmaf 3.2.1 |
# Custom versions
docker build --build-arg FFMPEG_version=8.1 --build-arg VMAF_version=3.2.1 -t easyvmaf .
Docker Compose
A docker-compose.yml is included with pre-configured easyvmaf (CPU) and easyvmaf-cuda (GPU) services:
# CPU service
VIDEO_DIR=/path/to/videos docker compose run easyvmaf \
-d /videos/distorted.mp4 -r /videos/reference.mp4
# GPU service
VIDEO_DIR=/path/to/videos docker compose run easyvmaf-cuda \
-d /videos/distorted.mp4 -r /videos/reference.mp4 --vmaf-version 0.6 --gpu
VIDEO_DIR is required: the directory with your videos, mounted at /videos.
The services build the images from the checkout (easyvmaf:latest,
easyvmaf:cuda); they do not pull the published ones.
Sync examples explained
Reference delayed relative to distorted
reference.ts has 0.7 extra seconds at the start: the first frame of distorted-A.ts appears 0.7 seconds into reference.ts. Use --sync-window to search for the offset automatically:
easyvmaf -d distorted-A.ts -r reference.ts --sync-window 2
With a 2-second sync window, easyVmaf searches the first 2 seconds of reference.ts for the best PSNR match against the first frames of distorted-A.ts. The reported offset is 0.7: the first 0.7 seconds of reference.ts were trimmed.
Distorted delayed relative to reference
This time distorted-B.ts has the extra seconds: the first frame of reference.ts appears 8.3 seconds into distorted-B.ts. Use --sync-reverse to flip the sync direction:
easyvmaf -d distorted-B.ts -r reference.ts --sync-window 3 --sync-start 6 --sync-reverse
With --sync-reverse, the window slides over the distorted video: --sync-start 6 --sync-window 3 searches from 6 to 9 seconds into distorted-B.ts for the first frames of reference.ts. The reported offset is negative (−8.3), meaning that the distorted video was trimmed.
Known bugs
- Error messages at the end of a
libvmaf_cudarun. With--gpuand v0.6, libvmaf 3.2.1 can printproblem flushing libvmaf contextorcontext could not be synchronizedwhen it finishes. The run succeeds and its log and scores are complete. - CAMBI heatmaps with more than one thread. libvmaf 3.2.1 can leave part of
the first pictures of a
--cambi-heatmapfile as zeros when it runs on more than one thread, which is the default (Netflix/vmaf#1676). Scores are not affected. Use--threads 1if you need exact heatmaps.
Versioning
From 5.0.0, easyVmaf follows Semantic Versioning. The versioned contract is:
- the CLI flags and exit codes;
- the output file names: libvmaf log, summary file and CAMBI heatmap directory;
- the summary file format, versioned by its
schema_version; - the score names (
vmaf_v1_hd,vmaf_hd, ...); - what the
easyvmafpackage exports (easyvmaf.__all__). The submodules are internal.
A new default that changes scores, such as a different model or measurement format, is a major release. A bug fix that changes scores is a minor release, with a prominent CHANGELOG note that says which inputs change.
Deprecated flags and API names keep working, with a warning, in a minor release and are removed in the next major release.
Releases before 5.0.0 made no compatibility guarantees.
Migrating from 3.x
easyVmaf 5.0 changes the default model, the requirements, the CLI flags, the Python API and the JSON output of 3.x. Every change has a one-line migration. Coming from 4.x instead? The CHANGELOG lists the changes since 4.0.
| Contract | 3.x | 5.0 | Migration |
|---|---|---|---|
| Default model | v0.6 (vmaf_hd, vmaf_hd_neg, vmaf_hd_phone) |
v1 (vmaf_v1_hd, vmaf_v1_phone) |
--vmaf-version 0.6 |
| Python | >= 3.8 | >= 3.10 | Upgrade Python |
| FFmpeg and libvmaf | FFmpeg >= 5.0, libvmaf with built-in v0.6 models | FFmpeg >= 8.1, libvmaf >= 3.2.1 with built-in models (verified by computing a v1 frame) | The Docker images, brew upgrade libvmaf ffmpeg, a BtbN build, or rebuild libvmaf (see Installation) |
| Installation | From a clone of the repository | pipx install easyvmaf (PyPI), or the Docker images on GHCR |
See Installation |
| CLI flags | -model HD, -sw, -output_fmt, ... |
--display hd, --sync-window, --output-format, ... |
3.x flags are rejected; see Flag equivalences |
| Manual offset | -ss S without -sw, negative with -reverse |
--sync-offset S: positive trims the reference, negative the distorted video |
-ss 1.5 -reverse → --sync-offset -1.5 |
-gpu |
libvmaf_cuda with any model, CPU decoding |
--gpu: also decodes on the GPU (NVDEC), with identical scores. libvmaf_cuda only computes v0.6 at 8 bits, so with the default v1 models VMAF runs on the CPU (vmaf.cuda: false) |
Add --vmaf-version 0.6 to compute VMAF on the GPU, as in 3.x |
JSON (-json) |
Printed to stdout: vmaf.model and flat scores in vmaf |
Always written to the summary file: schema_version: 2, vmaf.display, vmaf.scores, vmaf.models |
Read scores from vmaf.scores of <distorted>_vmaf_summary.json |
| Text output | VMAF HD:, VMAF Neg:, VMAF Phone:, VMAF 4K: labels |
One line per score: name, value, libvmaf model and range | Parse score names, or read the summary file |
docker-compose.yml |
VIDEO_DIR defaults to ./video_samples |
VIDEO_DIR is required |
Set VIDEO_DIR |
vmaf(mainSrc, refSrc, output_fmt, model='HD', phone=False, ...) |
Class vmaf, output_fmt positional, model, phone |
Vmaf(distorted, reference, ...), keyword-only after the paths: display='hd', vmaf_versions=('1',), views=None, ...; output_format defaults to 'json' |
from easyvmaf import Vmaf; phone is gone (select with views) |
vmaf() arguments |
output_fmt=, print_progress=, end_sync=, manual_fps=, gpu_mode= |
output_format=, progress=, shortest=, fps=, gpu= |
Rename the arguments |
| Manual offset (API) | v.offset = 1.5, or setOffset(1.5) |
Vmaf(..., sync_offset=1.5) |
Pass it to the constructor |
syncOffset(syncWindow=3, start=0, reverse=False) |
Returns [offset, psnr] |
sync(window, start=0, reverse=False) returns SyncResult(offset, psnr); window has no default |
Rename the method; pass the window |
getVmaf(autoSync=False) |
Returns the FFmpeg process; autoSync=True runs syncOffset() first |
compute() returns a VmafResult with scores, models and paths; no autoSync |
Call sync() before compute(); read result.scores instead of parsing the log |
FFprobe, FFmpegQos, inputFFmpeg |
Exported by easyvmaf |
Not exported: the submodules are internal | Use Vmaf |
VMAF_MODELS, HD_MODEL_NAME, _4K_MODEL_NAME, *_VERSION |
In easyvmaf.ffmpeg |
Removed; the catalog is CATALOG |
from easyvmaf import CATALOG |
check_ffmpeg()['builtin_models'] |
Probe of vmaf_v0.6.1 |
libvmaf_v1 (probe of vmaf_v1.0.16_3d0h) |
from easyvmaf import check_ffmpeg; rename the key |
FFmpegQos.vmaf_cambi_heatmap_path |
In easyvmaf.ffmpeg |
vmaf.cambi_heatmap_path and VmafResult.cambi_heatmap_path |
Read it from the result |
Unchanged: the v0.6 score names, the libvmaf log path
(<distorted>_vmaf.{json,xml,csv}), the PSNR sync search for progressive
inputs at the same frame rate, duration handling and the libvmaf json/xml/csv
log formats. Interlaced inputs changed: easyVmaf now deinterlaces before
scaling, fixes two deinterlacing cases against a progressive reference, and
keeps sync field accurate. Reverse sync between inputs at different frame
rates now converts the right one, so its reported PSNR changes (see the
changelog).
With --vmaf-version 0.6, easyVmaf 5.0 produces the same v0.6 scores as 3.x on
the same libvmaf for progressive inputs; interlaced inputs can score
differently because of those fixes. Moving from libvmaf 3.0.0 to 3.2.1 changed
them by at most 0.00002 in our checks. With --vmaf-version 1 0.6, the v0.6
models are measured at 10 bits, which moved them by at most 0.028 when the
inputs are scaled. See the
CHANGELOG for
the full verification.
New since 3.x:
- VMAF v1 and its parameters:
--vmaf-version,--view,--hfr,--bitdepth,--enc-size,--enc-bitdepthand--model-option. - Frame ranges:
--start-frameand--frame-count. - GPU decoding with
--gpu, and its opt-outs--disable-hw-decodeand--disable-vmaf-cuda. - The summary file, and the Docker images on GHCR.
- In the Python API:
VmafResult,SyncResult,ModelSpec,ModelRun,CATALOG,select_models,check_ffmpeg,validate_model_config,validate_range_config, and theUnsupportedModelConfigError,UnsupportedRangeErrorandFFmpegExecutionErrorexceptions.
Flag equivalences
Only -d and -r keep their short form. Every other 3.x flag exits with code 2
and names its replacement.
| 3.x flag (removed) | Current flag |
|---|---|
-d |
-d, --distorted |
-r |
-r, --reference |
-sw |
--sync-window |
-ss |
--sync-start (sync window start) or --sync-offset (manual offset) |
-sync_only |
--sync-only |
-reverse |
--sync-reverse |
-fps |
--fps |
-subsample |
--subsample |
-threads |
--threads |
-endsync |
--shortest |
-output_fmt |
--output-format |
-cambi_heatmap |
--cambi-heatmap |
-progress |
--progress |
-verbose |
--verbose |
-json |
None: the summary file is always written |
-gpu |
--gpu |
-model HD / -model 4K |
--display hd / --display 4k |
Unique prefixes of long flags (--sync-w) are not accepted: write the full name.
Metadata
Release files for easyvmaf 5.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| easyvmaf-5.0.0.tar.gz | 119.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| easyvmaf-5.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 183.7 kB
Release files / easyvmaf-5.0.0.tar.gz
| Download URL | easyvmaf-5.0.0.tar.gz |
|---|---|
| Size | 119.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a10a379c5cafad40d7b1f26bddcf1a75eb2b29506bdc576ccdfbfb30581a7079
|
|
BLAKE2b-256 checksum How to use checksums |
3d8b318da0e14db455cd89f7ac8f247e2be83c9ca3a5a74c369055a7604b7b75
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency logRelease files / easyvmaf-5.0.0-py3-none-any.whl
| Download URL | easyvmaf-5.0.0-py3-none-any.whl |
|---|---|
| Size | 64.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
58c66ff179433659bde3ccba7f28eda928c689a77ddeb00b9809de85c5d81fc4
|
|
BLAKE2b-256 checksum How to use checksums |
489aa2d5045172570c85decaccf45bc2815e3f3c414a870e8e6afb1cd23a36d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency log