Log in

View Full Version : Converting HDR to SDR: libplacebo and other methods


GeoffreyA
23rd December 2024, 11:03
For many months this year, I experimented with converting HDR material to SDR, learning it from scratch, thanks, largely, to Doom9. It was the attempt to convert the 4K theatrical cut of The Lord of the Rings that inspired this quest, and after much headache, I finally found the solution that did the job well. So, I thought I would add my scripts here in the hope they may be useful to others.

I first started off with FFmpeg and a script from 2020 put together with the help of tonemap's documentation. From there, this year, I worked out more zscale settings, adjusting them over the months.

FFmpeg version, using zscale and tonemap:
ffmpeg -i "%src%" [...] -vf zscale=min=bt2020nc:tin=smpte2084:pin=2020:rin=limited:m=gbr:t=linear:p=2020:dither=none:npl=203:f=spline36,
format=gbrpf32le,tonemap=reinhard:desat=0,
zscale=1920:-1:f=spline36,crop=%crop%:exact=true,
zscale=m=709:t=709:p=709:r=limited:dither=error_diffusion:f=spline36,format=yuv420p,sidedata=delete [...]

ffmpeg -i "%src%" [...] -vf zscale=m=gbr:t=linear:dither=none:npl=203:f=spline36,
format=gbrpf32le,tonemap=reinhard:desat=0,
zscale=1920:-1:f=spline36,crop=%crop%:exact=true,
zscale=m=709:t=709:p=709:r=limited:dither=error_diffusion:f=spline36,format=yuv420p,sidedata=delete [...]

I also experimented with VapourSynth, finding that it gave, arguably, better results, if somewhat more desaturated. (Thanks to Selur for the zimg version.)

VapourSynth, using fmtc:
from vapoursynth import core

#clip = core.bs.VideoSource(filename)
clip = core.lsmas.LWLibavSource(filename)

clip = clip.fmtc.bitdepth(bits=32)
clip = clip.fmtc.resample(css="444")
clip = clip.fmtc.matrix(mats="2020", matd="rgb")
clip = clip.fmtc.transfer(transs="2084", transd="linear", match=1)

clip = clip.tonemap.Reinhard(exposure=1, peak=100)
#clip = clip.tonemap.Mobius(exposure=0.7, transition=0.3, peak=100)
#clip = clip.tonemap.Hable(exposure=2)

clip = clip.resize.Spline36(1920, 1080)

clip = clip.fmtc.primaries(prims="2020", primd="709")
clip = clip.fmtc.transfer(transs="linear", transd="709")
clip = clip.fmtc.matrix(mats="rgb", matd="709")
clip = clip.fmtc.resample(css="420")
clip = clip.fmtc.bitdepth(bits=8)

clip.set_output()

VapourSynth, using zimg:
import vapoursynth as vs

#clip = vs.core.bs.VideoSource(filename)
clip = vs.core.lsmas.LWLibavSource(filename)

clip = clip.resize.Spline36(format=vs.RGBS, matrix_in_s="2020ncl", transfer_in_s="st2084", primaries_in_s="2020", range_in_s="limited",
matrix_s="rgb", transfer_s="linear", primaries_s="2020", dither_type="none", nominal_luminance=203)

clip = clip.tonemap.Reinhard(exposure=1, peak=100)
#clip = clip.tonemap.Mobius(exposure=0.7, transition=0.3, peak=100)
#clip = clip.tonemap.Hable(exposure=2)

clip = clip.resize.Spline36(1920, 1080)

clip = clip.resize.Spline36(format=vs.YUV420P8, matrix_in_s="rgb", transfer_in_s="linear", primaries_in_s="2020",
matrix_s="709", transfer_s="709", primaries_s="709", range_s="limited", dither_type="error_diffusion")

clip.set_output()

Now, we come to the turn of the tide. Earlier, I had tried libplacebo in FFmpeg but found strange varying of brightness and put this method in the bin. As fate would have it, less than a fortnight ago, brushing up all my HDR scripts to destroy the Ring once and for all, casting it back into the fires from whence it came, I tried libplacebo at the last minute and was struck down to the floor. Without a doubt, this library beats most other methods; it computes peak brightness frame by frame, boasts a range of tone- and gamut-mapping functions, performs black-point compensation, and most importantly, is high quality. Long story short, if one wants to convert HDR to SDR today, libplacebo is the way to go.

Final FFmpeg-libplacebo version:
ffmpeg -init_hw_device vulkan -i "%src%" [...] -vf libplacebo=upscaler=spline36:downscaler=spline36:w=1920:h=-1:
peak_detect=true:tonemapping=spline:gamut_mode=perceptual:
colorspace=bt709:color_trc=bt709:color_primaries=bt709:range=limited:dithering=blue:format=yuv420p,
crop=%crop%:exact=true,sidedata=delete [...]

Z2697
23rd December 2024, 15:48
You can use libplacebo in VapourSynth as well, and I'd personally prefer that way. I found FFmpeg's libplacebo filter not very stable, although it could be because I for recent couple of years have been using m-ab-s to build "the most recent codes" which is not guaranteed to be stable.

(MPV itself which is the reason behind libplacebo have transcoding feature as well LOL)

GeoffreyA
23rd December 2024, 16:36
You can use libplacebo in VapourSynth as well, and I'd personally prefer that way. I found FFmpeg's libplacebo filter not very stable, although it could be because I for recent couple of years have been using m-ab-s to build "the most recent codes" which is not guaranteed to be stable.

(MPV itself which is the reason behind libplacebo have transcoding feature as well LOL)

I tried using VS but it didn't work. Do you have to call some function to initialise Vulkan?

(By the way, Niklas Haas made many commits today, reworking aspects of FFmpeg's colour-space and management infrastructure, some of which he ported straight from libplacebo. The commit remarks are quite interesting and extensive.)