>FFmpegLab
FFmpeg Guide

Color Spaces, HDR, and Tone Mapping in FFmpeg

Convert HDR video to SDR without washed-out colors. Master zscale, tonemap, and proper color space conversion for HDR10, HLG, and Dolby Vision.

Converting HDR video to SDR is one of the most common—and most misunderstood—tasks in FFmpeg. You've probably tried something like this:

ShareRenders { } Code Config
Generated Code Logs Customize
# This will look terrible — washed out and grey
ffmpeg -i hdr_video.mp4 -c:v libx264 -crf 18 output_sdr.mp4

The result? Washed-out colors, crushed shadows, blown highlights—a video that looks worse than a VHS tape.[reference:0] This guide will show you exactly why that happens and how to fix it.

Key takeaways

The Gap: Raw Parameters vs. Cohesive Guide

The FFmpeg documentation for zscale and colorspace filters lists raw parameters—primaries, matrix, transfer, range—but fails to provide a cohesive guide on how to properly tone-map HDR10 or Dolby Vision video down to SDR without ending up with a washed-out, grey image.[reference:1]

You'll find scattered forum posts with commands that "sort of work," but they rarely explain why they work or how to adapt them to your specific source. This guide bridges that gap.

The Symptom: Washed-Out, Grey Images

The main reason HDR videos look washed out on SDR screens is improper or missing tone mapping during conversion.[reference:2] HDR footage uses wider color gamuts such as BT.2020 and higher brightness levels, while SDR is based on Rec.709 with a more limited range.[reference:3]

Without tone mapping, this difference is not correctly compressed. Bright areas become overexposed, mid-tones lose depth, and the entire image appears flat and desaturated.[reference:4]

Why You Can't Just Re-encode HDR Video

HDR video uses three things that SDR displays can't interpret correctly[reference:5]:

PropertyHDR (typical)SDR
Transfer functionPQ (SMPTE ST 2084) or HLGBT.709 gamma (~2.4)
Color primariesBT.2020 (wide gamut)BT.709 (standard gamut)
Bit depth10-bit8-bit
Peak brightness1,000–10,000 nits~100 nits

Simply changing the container or codec doesn't convert these properties.[reference:6] You need a tonemapping step that mathematically remaps the wide brightness range into the SDR range while preserving as much visual detail as possible—plus a color space conversion from BT.2020 to BT.709.[reference:7]

Check Your Input with ffprobe

Before converting, verify that your source is actually HDR and identify its characteristics[reference:8]:

ShareRenders { } Code Config
Generated Code Logs Customize
# Check HDR metadata
ffprobe -v quiet -show_streams -select_streams v:0 input_hdr.mp4 2>&1 | grep -E "color_|pix_fmt"

You should see something like[reference:9]:

pix_fmt=yuv420p10le
color_space=bt2020nc
color_transfer=smpte2084
color_primaries=bt2020

The zscale + tonemap CPU Pipeline

The most reliable CPU-based pipeline uses zscale for color space conversion and tonemap for brightness compression.[reference:13] Here's the quickest reliable command[reference:14]:

ShareRenders { } Code Config
Generated Code Logs Customize
# Full HDR to SDR conversion with zscale + tonemap
ffmpeg -i input_hdr.mp4 \
  -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
  -c:v libx264 -crf 18 -preset slow -c:a copy output_sdr.mp4

Let's break down each step[reference:15]:

  1. zscale=t=linear:npl=100 — Convert from PQ/HLG to linear light. npl=100 sets the nominal peak luminance to 100 nits (SDR reference).[reference:16]
  2. format=gbrpf32le — Convert to floating-point RGB for accurate tone mapping.[reference:17]
  3. zscale=p=bt709 — Convert color primaries from BT.2020 to BT.709.[reference:18]
  4. tonemap=hable:desat=0 — Apply the Hable tone-mapping algorithm with no desaturation.[reference:19]
  5. zscale=t=bt709:m=bt709:r=tv — Convert transfer to BT.709 gamma, matrix to BT.709, and range to TV (limited).[reference:20]
  6. format=yuv420p — Convert back to 8-bit YUV for encoding.[reference:21]

Note: The zscale filter requires FFmpeg to be built with --enable-libzimg.[reference:22][reference:23] Most pre-built binaries don't include it, so you may need to compile from source or use a build that includes it.

Tonemapping Algorithms: Which One to Use

The tonemap filter supports several algorithms[reference:24]. Here's how to choose[reference:25][reference:26]:

AlgorithmBest forTrade-offs
hablePreserving both dark and bright detailsSlightly darkens everything; best detail preservation[reference:27]
reinhardBright, punchy outputLess detail in highlights; "safe" default[reference:28]
mobiusBalance between accuracy and detailUser-adjustable threshold; default 0.3 is more accurate than reinhard[reference:29]
bt.2390ITU standard (BT.2390)Follows the official ITU recommendation for HDR to SDR
gammaSimple gamma adjustmentCan cause color shifts; not recommended[reference:30]

For most use cases, hable is the best choice—it preserves detail in both shadows and highlights better than reinhard.[reference:31]

GPU Acceleration with libplacebo

If you have a GPU and want faster conversion, the libplacebo filter provides hardware-accelerated tone mapping.[reference:32] It's significantly faster than the CPU-based zscale+tonemap pipeline, especially for 4K60 HDR video.[reference:33]

ShareRenders { } Code Config
Generated Code Logs Customize
# GPU-accelerated HDR to SDR with libplacebo
ffmpeg -i input_hdr.mp4 \
  -vf "libplacebo=tonemapping=hable:colorspace=bt709:color_primaries=bt709:color_trc=bt709" \
  -c:v libx264 -crf 18 -preset slow -c:a copy output_sdr.mp4

Note: libplacebo requires FFmpeg to be built with Vulkan support. It's available in recent FFmpeg builds but may not be included in all distributions.

Setting HDR Metadata Correctly

When creating HDR output (rather than converting to SDR), you must set the correct metadata flags[reference:34]:

ShareRenders { } Code Config
Generated Code Logs Customize
# Create HDR10 output with proper metadata
ffmpeg -i input.mp4 \
  -vf "zscale=primaries=bt2020:matrix=bt2020_ncl:transfer=smpte2084" \
  -color_trc smpte2084 -color_primaries bt2020 -colorspace bt2020nc \
  -pix_fmt yuv420p10le -c:v libx265 -crf 18 output_hdr10.mp4

Key metadata flags[reference:35]:

Practical Example: HDR10 to SDR

Let's put it all together with a real-world example. Suppose you have an HDR10 video (PQ, BT.2020) and you want to convert it to SDR (BT.709, gamma) for playback on standard displays.

ShareRenders { } Code Config
Generated Code Logs Customize
# Convert HDR10 (PQ/BT.2020) to SDR (BT.709) with hable tone mapping
ffmpeg -i hdr10_video.mp4 \
  -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
  -c:v libx264 -crf 18 -preset slow -c:a copy -movflags +faststart sdr_output.mp4

If your source is HLG instead of PQ, use transfer=arib-std-b67 instead of transfer=smpte2084 in the zscale chain.[reference:40]

Test in the FFmpegLab IDE

The FFmpegLab editor lets you preview color space conversions before you commit to a full encode. Upload an HDR video, adjust tone mapping parameters, and see the result in real time—then export the exact command you need.

Try it below. Load an HDR video, select your target color space (BT.709 for SDR), choose a tone mapping algorithm, and watch the preview update instantly. When you're happy, copy the generated FFmpeg command and run it locally—fully offline, nothing uploaded.

ShareRenders { } Code Config
Generated Code Logs Customize
# Interactive HDR to SDR conversion with tone mapping preview
ffmpeg -i hdr_input.mp4 \
  -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
  -c:v libx264 -crf 18 -preset slow -c:a copy tonemapped_output.mp4

Frequently Asked Questions (FAQ)

Why does my HDR video look washed out after conversion?

The main reason is missing or improper tone mapping. HDR uses BT.2020 primaries and PQ/HLG transfer, while SDR uses BT.709 and gamma. Simply re-encoding without converting these properties results in washed-out, grey images.[reference:41] You need to use zscale for color space conversion and tonemap for brightness compression.

What's the difference between zscale and colorspace filters?

zscale is more powerful and supports HDR conversion with proper handling of primaries, matrix, and transfer characteristics.[reference:42] colorspace is simpler but may not handle all HDR scenarios correctly.[reference:43] For HDR to SDR conversion, zscale is the recommended choice.

Which tone mapping algorithm should I use?

hable is generally the best choice—it preserves detail in both shadows and highlights better than reinhard.[reference:44] Use mobius if you want a balance between accuracy and detail preservation.[reference:45] reinhard is a safe default that produces bright, punchy output but may lose some highlight detail.[reference:46]

How do I convert HLG (Hybrid Log-Gamma) HDR to SDR?

Use transfer=arib-std-b67 instead of transfer=smpte2084 in the zscale chain.[reference:47] The rest of the pipeline (primaries conversion, tone mapping) remains the same. Check your input with ffprobe—if you see color_transfer=arib-std-b67, your source is HLG.[reference:48]

Does FFmpeg support Dolby Vision conversion?

FFmpeg can read Dolby Vision metadata but full conversion support is limited. The zscale+tonemap pipeline works for the base layer, but Dolby Vision's dynamic metadata (which adjusts tone mapping per scene) is not fully supported in open-source FFmpeg.[reference:49] For best results with Dolby Vision, use professional tools or the libplacebo filter which has better HDR handling.

Final Word

Converting HDR to SDR doesn't have to be a guessing game. With zscale for color space conversion and tonemap for brightness compression, you can produce SDR video that looks good—not washed out, not grey, not overly dark.

The key is understanding what you're converting (PQ or HLG? BT.2020 or BT.709?) and how to map it. Use ffprobe to check your input, choose the right tone mapping algorithm for your content, and always set the correct output metadata.

Next time you're faced with an HDR video that needs to play on an SDR screen, you'll know exactly what to do.

✦  Fresh from the render queue

Better FFmpeg workflows, delivered.

Get practical commands, new templates, and deep-dive guides for the edits that are usually hardest to get right.

✓  Copy-pasteable commands    ✓  Editor templates    ✓  No noise
One useful email at a time.