
1. Basic Comparison
- What they are
- sRGB: A standard “web”/computer-display RGB color space defined by IEC 61966-2-1. It’s used for most monitors, cameras, printers, and the vast majority of images on the Internet.
- Rec. 709: An HD-video color space defined by ITU-R BT.709. It’s the go-to standard for HDTV broadcasts, Blu-ray discs, and professional video pipelines.
- Why they exist
- sRGB: Ensures consistent colors across different consumer devices (PCs, phones, webcams).
- Rec. 709: Ensures consistent colors across video production and playback chains (cameras → editing → broadcast → TV).
- What you’ll see
- On your desktop or phone, images tagged sRGB will look “right” without extra tweaking.
- On an HDTV or video-editing timeline, footage tagged Rec. 709 will display accurate contrast and hue on broadcast-grade monitors.
2. Digging Deeper
Feature | sRGB | Rec. 709 |
---|---|---|
White point | D65 (6504 K), same for both | D65 (6504 K) |
Primaries (x,y) | R: (0.640, 0.330) G: (0.300, 0.600) B: (0.150, 0.060) | R: (0.640, 0.330) G: (0.300, 0.600) B: (0.150, 0.060) |
Gamut size | Identical triangle on CIE 1931 chart | Identical to sRGB |
Gamma / transfer | Piecewise curve: approximate 2.2 with linear toe | Pure power-law γ≈2.4 (often approximated as 2.2 in practice) |
Matrix coefficients | N/A (pure RGB usage) | Y = 0.2126 R + 0.7152 G + 0.0722 B (Rec. 709 matrix) |
Typical bit-depth | 8-bit/channel (with 16-bit variants) | 8-bit/channel (10-bit for professional video) |
Usage metadata | Tagged as “sRGB” in image files (PNG, JPEG, etc.) | Tagged as “bt709” in video containers (MP4, MOV) |
Color range | Full-range RGB (0–255) | Studio-range Y′CbCr (Y′ [16–235], Cb/Cr [16–240]) |
Why the Small Differences Matter
- Gamma curve shape
- sRGB’s piecewise transfer gives slightly more precision in the dark-shadow region (the “toe”), helping preserve detail in shadows on consumer displays.
- Rec. 709’s pure power curve is simpler for video hardware and aligns with CRT behavior, at the cost of a bit less nuanced shadow detail.
- Workflow implications
- If you render 3D or do photo editing, you’ll work in linear-light sRGB (or even wider) and then convert to “display” sRGB.
- In video editing, cameras record linear or log, then you apply a Rec. 709 output transform (OETF) to get the correct brightness curve and matrix for broadcast.
- Interchangeability
- Because their primaries and white points are effectively identical, you can treat sRGB and Rec. 709 as the same gamut for most intents—just be mindful of the transfer curve and metadata tag when moving between image and video pipelines.
Bottom line:
For everyday use they behave almost identically in color coverage. The real distinctions lie in their gamma/transfer curves and how they’re tagged and handled downstream in imaging (sRGB) vs. video workflows (REC709).
MP4 as a container doesn’t support embedding a full ICC profile the way PNG or JPEG do. Instead, video streams use Y′CbCr with metadata tags for color primaries, transfer characteristics (gamma), and matrix coefficients. You can’t truly “save” an MP4 in sRGB, but you can:
- Approximate sRGB by using Rec. 709 primaries
– sRGB’s chromaticities match Rec. 709 almost exactly, so you should tag your video as Rec. 709 rather than the older Rec. 601 standard. Rec. 709 is the HDTV standard and comes very close to sRGB in terms of color gamut - Use an sRGB (IEC 61966-2-1) transfer function
– sRGB’s gamma curve is slightly different from Rec. 709’s, so set the transfer characteristic toiec61966-2-1
. In FFmpeg you can either use the built-in filter:
ffmpeg -i input.exr \
-vf "zscale=transferin=linear:transfer=iec61966-2-1" \
-c:v libx264 \
-pix_fmt yuv444p \
-color_primaries bt709 \
-color_trc iec61966-2-1 \
-colorspace bt709 \
output.mp4
... or the more general colorspace filter:
ffmpeg -i input_%05d.png \
-c:v libx264 \
-pix_fmt yuv420p \
-colorspace rgb \
-color_trc iec61966-2-1 \
-y output.mp4
Both methods convert your source into the Rec. 709 gamut with an sRGB gamma curve Super User+1.
The MP4s you generate with this code are encoded in the Y′CbCr (YUV) color space with 4:2:0 chroma subsampling.
Concretely:
– Pixel format: yuv420p → 4:2:0 subsampling, 8 bits per channel (studio swing, Y=[16–235], Cb/Cr=[16–240]).
– Color primaries & transfer: FFmpeg’s defaults (for HD resolutions) are Rec. 709 primaries with the matching OETF/EOTF.
For SD resolutions (< 720 px), it would default to Rec. 601.
– Input handling: You feed linear-mapped 8-bit RGB frames (from EXR→float→uint8 or PIL’s .convert(‘RGB’)), so FFmpeg treats them as video-range RGB and applies its standard RGB→YUV conversion (Rec. 709 or Rec. 601 matrix).
If you need a different gamut (e.g. Rec. 2020) or full-range YUV/RGB, you’ll have to pass explicit FFmpeg flags such as
-color_primaries, -color_trc, -colorspace, or -color_range.
When you encode at HD resolutions (≥720 px height) with pix_fmt=yuv420p, FFmpeg will use BT.709 primaries, transfer curve, and matrix by default. A quick summary:
– Color primaries & transfer: BT.709 (for HD); BT.601 only if you drop below 720 px height.
– Matrix coefficients: the Rec.709 Y′CbCr conversion matrix.
– Range: “tv” (studio) swing (Y 16–235, Cb/Cr 16–240).
If you ever need to override or explicitly tag Rec.709, add these to your ffmpeg_params:
‘-color_primaries’, ‘bt709’,
‘-color_trc’, ‘bt709’,
‘-colorspace’, ‘bt709’,
‘-color_range’, ‘tv’
Note that with the defaults above you will notice a light shift in greens. In particular the greens under the MP4 will look a bit washed out. This is because the default RGB→Y′CbCr path ends up
1) subsampling chroma (yuv420p) and
2) often using limited (TV) range and/or the wrong matrix (BT.601) unless you pin it. That combo can make greens look a bit washed.
Here are minimal, surgical fixes you can paste into your existing code.
- Force the correct matrix and range during conversion (avoid implicit guesses).
- Tag the stream/container so players know how to interpret it.
- If you don’t need maximum compatibility, avoid chroma subsampling (use yuv444p).
# Note this will not display in older mp4 players, but will open fine in Premier or After Effects
writer = imageio.get_writer(
str(output_mp4),
fps=24,
codec='libx264',
ffmpeg_log_level='error',
pixelformat='yuv444p', # 4:4:4 avoids chroma loss
macro_block_size=1, # prevent auto 16x upscaling
ffmpeg_params=[
'-preset','slow',
'-crf','18',
# 1) force 8-bit RGB (drop alpha), 2) full->full with 709 matrix, 3) keep 4:4:4
'-vf','format=rgb24,scale=in_range=full:out_range=pc:in_color_matrix=bt709:out_color_matrix=bt709,format=yuv444p',
'-sws_flags','+accurate_rnd+full_chroma_int+full_chroma_inp',
'-profile:v','high444', # required for H.264 4:4:4 on many builds
'-x264-params','colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709:range=pc',
'-color_primaries','bt709',
'-color_trc','iec61966-2-1',
'-colorspace','bt709',
'-color_range','pc'
]
)
Otherwise keep yuv420p but do a clean full→TV conversion:
writer = imageio.get_writer(
str(output_mp4),
fps=24,
codec='libx264',
ffmpeg_log_level='error',
pixelformat='yuv420p', # broadly supported (requires even width/height)
ffmpeg_params=[
'-preset','slow', # slower → better compression
'-crf','18', # lower → higher quality (visually lossless)
# Full-range sRGB PNGs → TV-range 709 video, then pad to even dims, then enforce 4:2:0
'-vf','scale=in_range=full:out_range=tv:in_color_matrix=bt709:out_color_matrix=bt709,pad=ceil(iw/2)*2:ceil(ih/2)*2,format=yuv420p',
'-sws_flags','+accurate_rnd+full_chroma_int+full_chroma_inp',
# Proper signaling (709 gamut + sRGB-ish TRC) and TV range
'-x264-params','colorprim=bt709:transfer=iec61966-2-1:colormatrix=bt709:range=tv',
'-color_primaries','bt709',
'-color_trc','iec61966-2-1',
'-colorspace','bt709',
'-color_range','tv'
]
)
In short, PNGs are full-range. Unsignaled conversion to limited can flatten saturation/contrast. We either keep full all the way (yuv444p) or explicitly remap to TV with proper dithering.
yuv420p discards color detail; greens are sensitive. yuv444p avoids that entirely. If you must stay 420, the scaler flags reduce artifacts.
3. (Optional) Embed a true ICC profile
– If you really need a full ICC profile in the MP4 (rather than just metadata tags), FFmpeg’s support is limited. You can either:
- Use FFmpeg’s
iccgen
filter (requires an FFmpeg build with lcms2) to generate and attach an ICC profile automatically:
ffmpeg -i input.mp4 -vf iccgen -c:v copy output_icc.mp4
By default iccgen
infers BT. 709/sRGB profiles ayosec.github.io.
Or use MP4Box to inject an extracted ICC file as side‐data:
a. Extract the ICC from a PNG source:
magick icc_image.png profile.icc
b. Mux it into your MP4:
MP4Box -add video.mp4#video:colr=prof,profile.icc -new icc_video.mp4
–– In practice, most workflows simply rely on Rec. 709 primaries + sRGB transfer tags, which are widely supported by players and browsers and visually match true sRGB closely.
Regarding MP4 compression
Specifying codec=’libx264′ with pixelformat=’yuv420p’ is the de facto standard for widest MP4/H.264 compatibility. By default, FFmpeg (and thus ImageIO) will use a Constant Rate Factor (CRF) of 23 and “medium” preset, which trades off quality vs. speed/file-size.
If we want visually lossless output (at the cost of larger files), you can tweak two FFmpeg params:
Lower the CRF (quality factor):
Default is 23.
CRF 18 is generally considered “visually lossless.”
CRF 0 is mathematically lossless (huge files).
Use a slower preset to improve compression efficiency:
Defaults: –preset medium
Slower options: slow, slower, or veryslow