SDR to HDR Tone-Mapping in FFmpeg

This article explains how to perform inverse tone-mapping to convert Standard Dynamic Range (SDR) video into High Dynamic Range (HDR) using FFmpeg. You will learn how to use native filters like zscale for color space conversion, apply mathematical luminance expansion, and leverage custom 3D LUTs to achieve high-quality up-mapping results.

Understanding SDR to HDR Up-Mapping

Converting SDR (typically Rec. 709, BT.1886, 100 nits peak brightness) to HDR (Rec. 2020, PQ/ST 2084 or HLG, 1000+ nits) requires more than just changing container metadata. To make the video look truly HDR, you must perform “inverse tone-mapping.” This process expands the highlight luminance and color gamut without introducing clipping, banding, or unnatural oversaturation.


Method 1: Mathematical Up-Mapping using Native Filters

If your FFmpeg build is compiled with --enable-libzimg, you can use the high-quality zscale filter. To manually expand SDR colors and brightness, you must convert the video to linear light, scale the luminance values mathematically, and then encode them into the PQ (ST 2084) color space.

Use the following command to perform linear luminance expansion:

ffmpeg -i input_sdr.mp4 -vf "zscale=primaries=bt709:transfer=linear,format=gbrpf32le,geq=r='r*2.0':g='g*2.0':b='b*2.0',zscale=primaries=bt2020:transfer=smpte2084:matrix=bt2020nc,format=yuv420p10le" -c:v libx265 -crf 18 -pix_fmt yuv420p10le output_hdr.mp4

How the filter chain works:

  1. zscale=primaries=bt709:transfer=linear: Converts the SDR Rec. 709 input into linear RGB light, which is necessary for accurate mathematical calculations.
  2. format=gbrpf32le: Changes the pixel format to 32-bit floating-point RGB to prevent color clipping during the math phase.
  3. geq=r='r*2.0':g='g*2.0':b='b*2.0': Uses mathematical expressions to boost the linear light values. Multiplying by 2.0 expands the highlights. You can adjust this multiplier to control the peak brightness expansion.
  4. zscale=primaries=bt2020:transfer=smpte2084:matrix=bt2020nc: Maps the expanded linear light into the BT.2020 color gamut with the SMPTE ST 2084 (PQ) transfer function.
  5. format=yuv420p10le: Converts the output to 10-bit YUV, which is the standard format for HDR10 distribution.

For precise artistic control over highlights, midtones, and shadows, using a 3D Lookup Table (LUT) is the industry standard. A custom .cube LUT can map Rec. 709 to Rec. 2020 while carefully applying a custom S-curve for smooth luminance expansion.

Execute this command to apply a custom 3D LUT:

ffmpeg -i input_sdr.mp4 -vf "lut3d=file=sdr_to_hdr_upmap.cube,zscale=primaries=bt2020:transfer=smpte2084:matrix=bt2020nc,format=yuv420p10le" -c:v libx265 -crf 18 -pix_fmt yuv420p10le output_hdr.mp4

How the filter chain works:

  1. lut3d=file=sdr_to_hdr_upmap.cube: Applies your custom 3D LUT file. The LUT should be configured to receive Rec. 709 inputs and output either linear light or PQ-mapped RGB.
  2. zscale: Ensures that the final matrix coefficients, primaries, and transfer characteristics are properly tagged as BT.2020 and SMPTE ST 2084 in the output bitstream metadata.

Injecting HDR10 Metadata

To ensure consumer HDR TVs and monitors recognize the up-mapped file and engage their HDR modes, you must pass HDR10 metadata to the x265 encoder.

Add the following parameter string to your FFmpeg command:

-x265-params "hdr10=1:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1):max-cll=1000,400"

Metadata Parameter Breakdown: