Videre app icon

Reading a video signal

A video file stores integers. Matrix, range and transfer decide what those integers mean, and none of them are stored in every format. What each one does, what a wrong one looks like on screen, and how to settle it with numbers instead of opinion.

Codes, not colors Numbers you can check Test-signal recipes

a primer for anyone working with video files

Codes, not colors

Open an uncompressed video file and what is on disk is a grid of integers, three per pixel at most and often fewer. There is no red in there, no brightness in nits, no white point. Those appear only when something declares how the integers should be read.

Three declarations carry nearly all of that meaning, and the same three names appear twice in a working day, in opposite roles. When a camera, an encoder or a signal generator writes a file, they are constructive: they decide which integers get written. When a player or an inspector opens that file, they are declarative: they decide what the integers are taken to mean.

Matrix
How the three numbers split into brightness and two color differences. BT.601, BT.709, BT.2020.
Range
Which integers count as black and white. Video range keeps room above and below; full range uses everything.
Transfer
How much light each value stands for. The SDR curves, PQ, HLG.

The rule everything else follows from. Changing matrix, range or transfer in a viewer never rewrites a byte. It changes every derived quantity: the RGB readout, the nits figure, the waveform, the vectorscope, gamut statistics, quality metrics, legality checks. The codes sit still and the meaning moves.

The pipeline

The order is not the one most people guess. Light is converted to a signal first, by the transfer function, and only then split into luma and chroma by the matrix. That is why the components are written R'G'B' with a prime: they are already non-linear when the matrix touches them.

WRITING from light Transfer light to signal R'G'B' Matrix luma weights Y'CbCr Range scale, quantize codes the file integers only plus tags, maybe READING to a picture Transfer signal to light R'G'B' Matrix the inverse Y'CbCr Range offset, stretch codes WRITING Transfer light to signal R'G'B' Matrix luma weights Y'CbCr Range scale, quantize codes the file integers, plus tags, maybe codes READING Range offset, stretch Y'CbCr Matrix the inverse R'G'B' Transfer signal to light to a picture
The three stages are a mirror. Match the reading side to the writing side and the value you authored comes back, give or take quantization. Set them differently and you are looking at a deliberate misinterpretation, which is a legitimate diagnostic and the reason test-signal generators exist. Chroma subsampling sits between the range stage and the file.

Codes and the 0 to 255 habit

A pixel probe in a 10-bit file typically shows something like Y 1023 Cb 512 Cr 512 → RGB 255 255 255. Those numbers live at different stages and are not two views of the same thing.

Y, Cb and Cr are the file, read straight from the plane bytes at the format's own depth, 0 to 1023 for 10-bit, with nothing applied. RGB is a derived signal on a legacy scale: the decode produces R'G'B' normalized 0 to 1, and the readout multiplies by 255 because that is the scale everyone reads at a glance. It is a display convention, not the file's precision.

The decode also clamps to 0 and 1, and that is where information disappears. At 10-bit video range the luma white point is 940, so every code from 940 to 1023 produces a signal at or above 1.0 and shows the same 255. Codes 1019 and 1023 are visibly different signals and an identical RGB triple. Measure in codes; use RGB for intuition, not for judgement.

Matrix

Video does not store R, G and B. It stores one brightness channel and two color differences, so the color channels can be sampled at lower resolution without an obvious penalty. Luma is a weighted sum of the three primaries, and those weights are the matrix. The three standards disagree about them, and that disagreement is the entire practical content of the setting.

SHARE OF LUMA BLUE BAR BT.601 R .299 · G .587 · B .114 Y 41 BT.709 R .2126 · G .7152 · B .0722 Y 32 BT.2020 R .2627 · G .6780 · B .0593 Y 29 8-bit, video BT.601 Y 41 R .299 · G .587 · B .114 BT.709 Y 32 R .2126 · G .7152 · B .0722 BT.2020 Y 29 R .2627 · G .6780 · B .0593
Green dominates luma in every standard, and blue's share shrinks as the standards move to wider gamuts. Feed the same pure blue into each and a different luma code comes out: 41, 32, 29 at 8-bit video range. Cr moves too, to 110, 118 and 119, while Cb pins at 240 in all three.

Greys are immune. Neutral means Cb and Cr sit at the centre code, which decodes neutral under every matrix, so a grey ramp is byte-for-byte the same file whichever matrix authored it. The error shows only on saturated color, as a hue and brightness shift worst on red and blue. On a vectorscope the color targets sit rotated off their boxes rather than randomly scattered.

Why the mistake is common. Nothing about a 1920×1080 file makes it BT.709. Software guesses from resolution, SD to BT.601, HD to BT.709, UHD to BT.2020, and the guess is right often enough that the wrong cases are hard to spot. A file carrying no tag gets whatever its reader assumes.

Range

In video range, black sits above the bottom of the number space and white below the top. At 8-bit that is 16 and 235 for luma, 16 to 240 for chroma; at 10-bit multiply by four, so black 64, white 940, chroma 64 to 960. Full range uses everything.

The reserved space is not waste. Filtering and resampling overshoot, and a signal clipping at the boundary would fold those overshoots back into the picture as hard artefacts; footroom and headroom give them somewhere to live. SDI also reserves the outermost codes as sync words, 0 to 3 and 1020 to 1023 at 10-bit, so they cannot carry picture at all.

0 512 1023 10-bit code codes 0 to 3 and 1020 to 1023 reserved for SDI sync video range · 64 to 940 footroom headroom full range · 0 to 1023 1023 940 512 64 0 VIDEO 64 to 940 FULL 0 to 1023 outermost codes: SDI sync
The same number space, divided two ways. Video range keeps 64 codes of footroom and 83 of headroom; full range spends everything on picture. Both are correct, and the file has to say which one it is.

Legal is not the same as present. A video-range file can hold codes outside 64 to 940, legitimately. PLUGE bars sit deliberately below black so a monitor's brightness control has a reference to disappear against; a 10-bit PLUGE in video range writes 48, 56, 72 and 80 around the 64 floor, per BT.814. A range checker writes 0 and 1023 on purpose, because a chart covering only the legal window would check nothing. The range is the coordinate system a pattern maps its levels into, not a filter applied to the finished picture.

When the reading is wrong, the two directions are not symmetrical. One washes the picture out and is fully reversible. The other crushes and clips, and destroys distinctions.

CODES IN THE FILE 0 64 511 940 1019 1023 READ AS VIDEO 0 130 255 0 and 64 collapse together 940, 1019 and 1023 all land here READ AS FULL 0 16 127 234 254 255 CODES 0 511 940 1023 AS VIDEO 0 130 255 three codes clip to 255 AS FULL 0 16 127 234 nothing clips, everything separates
Six codes, two readings, one output scale. The full-range reading is a straight pass-through, which is why its connectors are vertical. The video-range reading shifts, stretches and clips: three distinct codes arrive at 255 and can no longer be told apart, and everything from 0 to 64 arrives at 0.
10-bit luma, decoded both ways
code    R' as video    RGB     R' as full    RGB    what it is
   0      -0.0731        0        0.0000       0     full-range black
  64       0.0000        0        0.0626      16     video-range black
 255       0.2180       56        0.2493      64     quarter scale
 511       0.5103      130        0.4995     127     mid scale
 940       1.0000      255        0.9189     234     video-range white
1019       1.0902      255        0.9961     254     highest legal SDI code
1023       1.0947      255        1.0000     255     full-range white

Chroma is scaled as well as clipped, because its span is 896 codes in video range against 1023 in full. Read video-range chroma as full and saturation drops to about 88 percent of what was intended; read full-range chroma as video and it is pushed to about 114 percent, with the tips clipping. Greys are unaffected either way, since the centre code is the same in both.

Transfer

The transfer function is the only one of the three that talks about the physical world. It maps a signal value to a luminance, and the three families in use answer that question very differently. The SDR curves (sRGB, gamma 2.2, gamma 2.4 for BT.1886) are relative: full scale means diffuse white, conventionally 100 nits on a reference display. PQ (SMPTE ST 2084, the HDR10 curve) is absolute: a code means a specific number of nits, from 0 to 10 000, whether or not any display can produce it. HLG (BT.2100) is scene-referred: full scale means the display's peak, and the luminance comes from a system gamma applied at display time, quoted here against a 1000-nit nominal peak.

1000010001001010.10.01 NITS 0.000.250.500.751.00 signal value, after range is removed SDR diffuse white, 100 nits PQ HLG 2.4 10k10001001010.1.01 0.000.501.00 signal value PQ HLG gamma 2.4 SDR white = 100 nits
The same signal value, three claims about light. At half scale, gamma 2.4 says 19 nits, HLG says 51, PQ says 92. At full scale they say 100, 1000 and 10 000. Nothing here touches a code: this is entirely a statement about what the codes were supposed to mean.
one 10-bit video-range code, three claims
code    signal    gamma 2.4        HLG           PQ
 255     0.218      2.6 nits     6.9 nits      3.2 nits
 511     0.510     19.9 nits      53 nits      102 nits
 767     0.803       59 nits     280 nits     1591 nits
 940     1.000      100 nits    1000 nits    10000 nits

Why HDR test patterns can all look like the same white. A typical panel gives an app roughly twice SDR white of extended range, about 200 nits. Under PQ, code 511 is already 102 nits and code 767 is 1591, so everything from the mid-scale step upward is beyond what the panel can show and clips to the same white. The picture is not wrong, the display is out of room. The codes and the nits readout still tell the truth, which is the argument for trusting the meter over the monitor.

Chroma subsampling and siting

Because luma carries most of the detail the eye resolves, the two color-difference channels are usually stored at lower resolution. 4:4:4 keeps everything, 4:2:2 halves chroma horizontally, 4:2:0 halves it in both directions, so a 4:2:0 chroma sample stands for a 2×2 block of luma. That raises a question with no self-evident answer: where in the block does the chroma sample sit? Different standards answered differently, and the answer has to travel with the file or edges pick up a half-sample color shift.

Left siting MPEG-2, H.264, HEVC default Centre siting JPEG, MPEG-1 luma sample chroma sample, one per 2×2 block Left siting MPEG-2, H.264, HEVC Centre siting JPEG, MPEG-1 luma chroma
Same eight luma samples, same two chroma samples, half a pixel apart. Flat color fields are identical either way, which is why the error hides: it appears only on color edges, as a faint one-sided fringe. Broadcast-lineage formats site chroma to the left, JPEG-lineage formats centre it.

What the file carries

Given how much the three declarations change, you would expect every format to record them. Most raw and intermediate formats record almost none.

Headerless raw
Nothing, not even dimensions or pixel format. Everything is supplied by whoever opens it, which is why a raw file needs a parameter sheet.
y4m
Range rides in the XCOLORRANGE extension, and chroma siting only in the 8-bit 4:2:0 colorspace tags. No field exists for matrix or transfer, so a BT.2020 PQ clip written to y4m reopens as whatever the reader assumes.
MP4 and MOV
The colr box carries primaries, transfer, matrix and a full-range flag, with chroma location alongside. This is the layer metadata repair fixes without re-encoding.
Coded bitstream
H.264 and HEVC VUI, AV1 color config. A second copy that can disagree with the container, written by different tools at different times, and players do not all resolve the conflict the same way. Checking that both layers agree is a real quality-control task.

Check it yourself

None of this has to be taken on faith. A signal generator writes a file whose every code you chose, so you can predict the readout before you look at it. These use Videre's generator and pixel probe; the experiments work with any tool that authors exact codes and reports them back.

the matrix moves color, never grey
$ videre generate -o a.y4m --pattern solidField \
      --param mode=signalRGB --param red=0 --param green=0 --param blue=1 \
      --format yuv444p10 --size 640x480 --frames 1 --matrix bt709 --range video
$ videre generate -o b.y4m ... --matrix bt601
$ cmp a.y4m b.y4m          # differs

$ videre generate -o c.y4m --pattern ramp --matrix bt709 ...
$ videre generate -o d.y4m --pattern ramp --matrix bt601 ...
$ cmp c.y4m d.y4m          # identical, byte for byte

Open the blue field and flip the viewer's matrix between BT.601 and BT.709. The Y, Cb, Cr readout does not move. The RGB readout does.

the range asymmetry, on one frame
$ videre generate -o range.y4m --pattern rangeChecker \
      --format yuv420p10 --size 1920x1080 --frames 1 --range full

A full-range checker writes the cells 0, 4, 255, 511, 767, 1019 and 1023. Toggle the viewer between Full and Video and watch the readout: the codes never move, and under Video the top two cells both report RGB 255, then separate into 254 and 255 under Full. Codes 0 and 4 are the mirror case at the bottom.

transfer changes the meaning, not the file
$ videre generate -o pq.y4m --pattern pqStairs --transfer pq \
      --format yuv420p10 --size 1920x1080 --frames 1

Hover a step with the viewer set to PQ and the nits figure lands at or just below the authored value, never above, because the nits-to-code quantization floors toward the legal side. Switch to HLG and the same code reports a different luminance. Switch to an SDR curve and the nits figure disappears, because a relative curve makes no absolute claim. The generator refuses to write a nits-defined pattern under an SDR transfer at all: a file claiming 1000 nits while carrying 100 is a lie the rest of the chain would believe.

The full pattern catalogue, with what each one exposes, is on the test signals page.

Symptom to cause

Most color faults in a delivery chain are one of these, and each has a signature you can confirm with a probe rather than an opinion. The tell is usually what stays correct.

Milky blacks
Dull whites, nothing clipped, greys lifted: video-range material read as full. Confirm it by probing black, which reads as code 64 but RGB 16.
Crushed shadows
Blown highlights, contrast too strong: full-range material read as video. Confirm it where distinct high codes all report RGB 255.
Hues drift
Worst on red and blue, brightness slightly off, greys still neutral: wrong matrix. On a vectorscope the targets sit rotated off their boxes.
Too dark or bright
Whole picture off, color relationships intact: wrong transfer. The nits readout disagrees with the authored value.
Edge fringes
A faint color fringe on one side of vertical edges only, flat fields fine: wrong chroma siting.

The habit worth building. When a picture looks wrong, read the codes first and the picture second. Codes are the only part of the chain that is not an opinion. Everything else, the RGB triple, the nits figure, the scopes, the way it looks on your monitor, is downstream of three declarations that may well be the thing that is wrong.

Go deeper

Videre shows the codes, the interpretation and the derived numbers side by side, and generates the test signals to check them against.

About Videre The decode path User guide