How to Use FFmpeg Tonemap for HDR to SDR Conversion

This article provides a direct guide on converting High Dynamic Range (HDR) video to Standard Dynamic Range (SDR) using FFmpeg's tonemap filter. You will learn the requirements for accurate color translation, how to construct the necessary filter chain to map dynamic range and color primaries, and how to execute production-ready commands using popular tone mapping algorithms like Hable and Mobius.


The HDR to SDR Conversion Process

HDR video typically uses the BT.2020 color gamut with either the SMPTE ST 2084 (PQ) or HLG transfer function. SDR displays, by contrast, rely on the BT.709 color space and standard gamma curves.

Converting HDR to SDR requires four distinct stages:

  1. Converting the pixel format to linear light.
  2. Compressing high dynamic range highlights to fit standard range using the tonemap filter.
  3. Converting the color primaries and matrix from BT.2020 to BT.709.
  4. Encoding the output into a standard 8-bit YUV format (like yuv420p).

To handle color space and linear transformations accurately, FFmpeg should ideally be compiled with --enable-libzimg to utilize the zscale filter alongside tonemap.


The Core Filter Chain

The standard filter pipeline uses zscale to prepare the video for tone mapping, passes it through tonemap, and then standardizes the color metadata with a second zscale call.

zscale=t=linear:npl=100,tonemap=tonemap=hable:desat=0.5,zscale=p=bt709:t=bt709:m=bt709:r=tv,format=yuv420p

Breakdown of the Chain:

  • zscale=t=linear:npl=100: Converts the transfer characteristics from PQ/HLG to linear light. The npl (nominal peak luminance) value sets the target peak brightness (default is usually 100 nits for SDR).
  • tonemap=tonemap=hable:desat=0.5: Compresses the dynamic range using the Hable algorithm and desaturates extreme highlights to prevent color clipping.
  • zscale=p=bt709:t=bt709:m=bt709:r=tv: Sets the primaries (p), transfer function (t), matrix coefficients (m) to BT.709, and range (r) to limited/TV range.
  • format=yuv420p: Converts the internal 32-bit floating-point representation back to standard 8-bit SDR video for universal playback.

Available tonemap Algorithms

The tonemap filter provides several algorithms defined by the tonemap option:

  • hable: Film-like tone mapping based on the Uncharted 2 curve. It preserves detail in both darks and bright highlights and is generally considered the most balanced choice for film and television content.
  • mobius: Smoothly transitions linear colors to a logarithmic curve. It preserves high mid-tone contrast while rolling off highlights smoothly.
  • reinhard: A classic simple photographic curve that preserves midtones well, though it can result in washed-out highlights if not tuned.
  • clip: Hard clips any value above SDR peak. This produces severe highlight blowing and is generally not recommended.

Complete FFmpeg Command Examples

This command takes an HDR10 input file and outputs an SDR H.264 file with Hable tone mapping:

ffmpeg -i input_hdr.mkv -vf "zscale=t=linear:npl=100,tonemap=tonemap=hable:desat=0.5,zscale=p=bt709:t=bt709:m=bt709:r=tv,format=yuv420p" -c:v libx264 -crf 18 -preset slow -c:a copy output_sdr.mp4

2. Using the Mobius Algorithm

The mobius operator accepts a param argument representing the transition point where compression begins (default is roughly 0.3):

ffmpeg -i input_hdr.mkv -vf "zscale=t=linear:npl=100,tonemap=tonemap=mobius:param=0.3:desat=0.5,zscale=p=bt709:t=bt709:m=bt709:r=tv,format=yuv420p" -c:v libx264 -crf 18 -preset slow -c:a copy output_sdr.mp4

Alternative: All-in-One zscale Tone Mapping

If you have libzimg enabled, you can also perform tone mapping directly inside the zscale filter without calling the standalone tonemap filter. This reduces pipeline overhead:

ffmpeg -i input_hdr.mkv -vf "zscale=tin=smpte2084:min=bt2020nc:pin=bt2020:t=bt709:m=bt709:p=bt709:tonemap=hable,format=yuv420p" -c:v libx264 -crf 18 -preset slow -c:a copy output_sdr.mp4

This single-filter command explicitly sets the input metadata (PQ transfer smpte2084, BT.2020 color) and translates directly to standard BT.709 SDR video.