Skip to content

Compare encoding quality with metrics and visual checks

current

Metrics narrow down settings, find bad frames and let you compare many encodes; your eyes make the final call. A fair comparison needs one reference, aligned frames, several quality levels, metrics that track perception, a summary that shows the worst parts, and a look at those parts.

Encode every candidate from the same file with the same preprocessing, and measure against that file, not the original release. Use a few 10–30 second clips that stress encoders: grain, dark gradients, foliage, fast motion, line art. Cutting them to lossless FFV1 gives every encoder and metric identical frames (-map 0:v:0 keeps just the video):

Terminal window
ffmpeg -ss 00:12:30 -i source.mkv -t 20 -map 0:v:0 -c:v ffv1 reference.mkv

A setting can win at one bitrate and lose at another, so encode each candidate at four or more levels and compare rate–quality curves (size or bitrate against score):

Terminal window
for crf in 20 25 30 35; do
ffmpeg -i reference.mkv -c:v libx264 -preset slow -crf $crf x264-crf$crf.mkv
done

CRF values aren’t comparable between encoders or even presets, so compare at equal size, not equal CRF. BD-rate condenses two curves into the average bitrate difference at equal quality; the bjontegaard Python package computes it:

import bjontegaard as bd
print(bd.bd_rate(rate_a, score_a, rate_b, score_b, method="akima"))

−10 means candidate B needs 10% fewer bits for the same score. It expects higher-is-better scores, so negate Butteraugli distances, and it only covers the quality range where the curves overlap.

Score encodes without film-grain synthesis. Synthesised grain never matches the source pixel for pixel, so full-reference metrics punish it even when it looks better. For AV1 in FFmpeg, put -export_side_data film_grain before the encode’s -i to decode it without the grain, and judge the grain by eye. If the encoder also denoised the source (SVT-AV1 --film-grain-denoise 1, aomenc’s --denoise-noise-level), the decode is still a clean picture measured against a grainy reference, and the scores stay low however good it looks.

Terminal window
ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=width,height,nb_read_frames -of csv=p=0 x264-crf30.mkv
  • Frame counts must match. FFmpeg’s metric filters silently repeat the last frame of a shorter input, and a one-frame offset ruins every score. If everything scores terribly, check alignment first; PSNR is a quick sanity check.
  • Scale a downscaled encode back to the reference resolution (bicubic) before measuring.
  • SSIMULACRA2, Butteraugli and CVVDP convert to RGB using the files’ colour tags, so missing or wrong matrix, range or transfer tags give wrong scores. FFVship guesses missing tags; FFVship --verbose prints what it used for each file.

Use more than one. They fail in different ways, and where they disagree is worth a look.

MetricRoleHow to run it
SSIMULACRA2Main perceptual scoreFFVship or Vship (GPU), vszip (CPU)
Butteraugli 3-normHigh-fidelity and near-transparent encodesFFVship or Vship
XPSNRFast, motion-aware first passFFmpeg xpsnr (reference first)
CVVDPTemporal, colour and display-aware; HDRcvvdp, FFVship
VMAF NEG or v1Optional streaming-style scoreFFmpeg libvmaf
PSNR, SSIMSanity checks only; they reward blurFFmpeg psnr, ssim
Terminal window
FFVship --source reference.mkv --encoded x264-crf30.mkv --json x264-crf30-ssimu2.json
FFVship --source reference.mkv --encoded x264-crf30.mkv -m Butteraugli --json x264-crf30-butter.json
ffmpeg -i reference.mkv -i x264-crf30.mkv -lavfi xpsnr=stats_file=x264-crf30-xpsnr.log -f null -

A short ugly section is what viewers notice, and a mean hides it. For each encode keep the mean, a low percentile (the 5th for SSIMULACRA2, the 95th for Butteraugli, where lower is better) and the worst frames by number. FFVship prints the mean and percentiles; this lists the ten worst frames from its SSIMULACRA2 JSON:

Terminal window
jq -c 'to_entries | sort_by(.value[0]) | .[:10] | map(.key)' x264-crf30-ssimu2.json

Plot curves for the low percentile as well as the mean. A setting that lifts the mean but lowers the percentile is trading consistency for average quality.

Watch the scenes around the worst frames at normal speed, at the size people will watch them. Then step frame by frame and flip between the reference and each candidate at the same frame; flipping shows differences that side-by-side views hide. A video player with frame stepping or YUView works, or extract a frame:

Terminal window
ffmpeg -i x264-crf30.mkv -vf "select=eq(n\,1234)" -frames:v 1 x264-crf30-1234.png

Look for banding, blocking, ringing, smeared texture, lost grain, colour shifts and flicker, and compare blind if you can.

Record encoder versions and commands, the source and clip timestamps, and each metric tool with its settings (VMAF model, Butteraugli norm and intensity target, CVVDP display model). Otherwise nobody, including you later, can tell whether a difference came from the encoder or from the measurement.