Skip to content

Colorimetry: inspect before encoding

currentMigrated from docs/colorimetry/intro.mdx at 37c3317fd058; modified for Encode Wiki · CC BY-SA 4.0

A decoded video frame is just numbers. Four pieces of metadata tell the player how to turn those numbers into light, and a fifth tells it where the chroma samples sit. If any of them is missing or wrong, the encode still “works” but the colors shift, blacks lift or crush, or HDR looks flat — which is why color mistakes so often go unnoticed until someone watches the result on another player.

PropertyWhat it answersffprobe fieldTypical HD SDRTypical HDR10
PrimariesWhich red, green, blue and white the RGB values refer tocolor_primariesbt709bt2020
TransferHow code values map to lightcolor_transferbt709smpte2084 (PQ)
MatrixHow Y′CbCr is converted back to R′G′B′color_spacebt709bt2020nc
RangeWhich code values are black and whitecolor_rangetv (limited)tv (limited)
Chroma locationWhere subsampled chroma sits relative to lumachroma_locationleftleft or topleft

The values are the integer code points from ITU-T H.273, which AVC, HEVC, VVC, AV1, Matroska and MP4 all reuse. Tools give them different names — FFmpeg says bt2020nc, SVT-AV1 says bt2020-ncl, VapourSynth says 2020ncl — but the number underneath is the same. How the pixel format stores the samples (bit depth, subsampling, planar or packed) is a separate question.

Terminal window
ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt,color_range,color_space,color_transfer,color_primaries,chroma_location -of default=nw=1 input.mkv

For an HD Blu-ray remux this prints something like:

pix_fmt=yuv420p
color_range=tv
color_space=bt709
color_transfer=bt709
color_primaries=bt709
chroma_location=left

unknown means the file does not say. HDR mastering metadata and Dolby Vision or HDR10+ data are usually carried per frame in the bitstream, so inspect the first frame as well:

Terminal window
ffprobe -v error -select_streams v:0 -read_intervals "%+#1" -show_frames input.mkv

Look for side_data_type=Mastering display metadata, Content light level metadata, Dolby Vision Metadata or HDR Dynamic Metadata SMPTE2094-40 (HDR10+). See HDR formats for what each means.

When a field is unknown the player guesses, and players guess differently. mpv 0.41, for example:

  • Matrix: BT.709 if the width is at least 1280 or the height is above 576, otherwise BT.601.
  • Primaries: BT.2020 if the matrix is BT.2020, BT.709 if the matrix is BT.709; otherwise by height (576 → PAL, 480/486 → NTSC, anything else → BT.709).
  • Transfer: BT.1886 for limited-range video, sRGB for full-range.

Browsers, TVs and other players use their own rules. An untagged 720×480 file therefore looks right in one player and slightly green-shifted in another. Tag every encode, and never assume an untagged source is BT.709 just because most HD material is — check where it came from.

There are three different operations, and confusing them is the most common color mistake:

  • Preserve: keep the samples and carry the same tags to the output. This is what you want for almost every encode.
  • Retag: change the metadata, leave the samples alone. Correct only when the source was tagged wrongly or not at all.
  • Convert: change the samples from one color system to another (matrix, range, primaries, transfer, tone mapping), then tag the result as the new system.

Tagging BT.601 pixels as BT.709 does not convert them; it makes the colors wrong. Tagging PQ video as bt709 does not tone-map it; it makes it look washed out. Scaling SD to HD does not change the matrix by itself — if you want an HD BT.709 output, convert the matrix while you scale.

Preserve color and HDR metadata has the commands for each case in FFmpeg, VapourSynth and the standalone encoders.