Nodes/radiance/HDR Synthesis Engine
ComfyUI Node

HDR Synthesis Engine

Real headroom on top of an SDR image, and the limits of that

By FXTD-Studios·Created 8 months ago·Updated about 18 hours ago· 246
HDR Synthesis Engine
  • image
  • guidance_mask
  • image
  • highlight_mask
◄energy_target10►
◄recovery_iters3►
◄chroma_preservation0.80►
◄guidance_nits0►

Your image is an 8-bit PNG. The highlights sit at 1.0 because that's where 8-bit puts them, and now you want a version that behaves like an HDR plate - something that reacts properly to a tonemapper, or exports as EXR with a meaningful range. Nothing in the file actually holds that information.

HDR Synthesis Engine makes a defensible guess at it. The important word is guess, and the author is unusually honest about which kind: this is a heuristic expansion, not a model, and it does not reconstruct clipped detail.

How it works

The image is split into a Laplacian pyramid. The low-pass base - the coarse brightness structure - gets lifted with a luma-weighted curve aimed at energy_target, gated so the lift mostly lands on highlights (found where the low-pass luma passes about 0.7). Then the detail bands are added back untouched.

That last part defines the whole character of the node. Detail finer than roughly 2^recovery_iters pixels keeps its original contrast, so your textures don't get pulverised by the lift. But a flat clipped white sky is still a flat clipped white sky - it comes back brighter, not resolved. Blown highlights stay blown, they just get louder.

chroma_preservation decides how highlight colour behaves as brightness climbs. At 0 you add neutral energy, so bright areas desaturate toward white as they get hotter. At 1 you keep the original low-pass RGB ratios, so a warm sun stays warm. Both use the same lifted luminance; only the chroma treatment differs. Default 0.8 is a sensible "keeps colour, but rolls off a little" setting.

There's also a guidance path. Feed it a mask from Luminance Guidance plus a guidance_nits value and the masked region is driven toward that nit target instead of the global multiplier. Radiance's nit scale treats 203 nits as 1.0 (the BT.2408 SDR reference), so guidance_nits=1000 puts the masked highlights near 4.93 in the output, not 1000. Gotcha worth stating twice: guidance_nits defaults to 0, which means ignore the mask entirely. Wiring the mask and leaving the value alone does nothing at all.

Inputs and outputs

Set three things: energy_target (default 10 - SDR white comes out around 9.6, roughly 3.3 stops up; 1.0 means no lift), recovery_iters (pyramid depth, 0 lifts the whole image and pulverises detail), and chroma_preservation. Then the optional guidance pair.

Outputs: image (scene-linear, values well above 1.0), and highlight_mask - the gate the lift actually used. That second output is your sanity check; if it's empty, nothing was lifted and you're about to spend an hour debugging a downstream node that isn't broken.

Two things to do before this node, not after. Linearise first - the tooltip says so outright, the node removes no transfer curve, and running it on sRGB values makes energy_target mean something different. And do your retouching first: everything you fix at 1.0 is cleaner than fixing it at 9.6.

Installing Radiance

Manager → search Radiance → install → restart → refresh. Or:

cd ComfyUI/custom_nodes
git clone https://github.com/fxtd-studios/radiance.git
cd radiance
python -m pip install -r requirements.txt

With the Windows portable build, run that pip line with python_embeded\python.exe. This node needs no weights, but the pack installs OpenEXR, OpenImageIO, OpenColorIO, diffusers and accelerate into ComfyUI's Python environment regardless - that's the cost of entry to Radiance.

Troubleshooting

Output is white mush. recovery_iters too low (you're lifting detail too) or energy_target beyond what the image can carry. Try 3 iterations and an energy target of 4–10 before going bigger.

Colours look pastel and washed. chroma_preservation is low. Raise it.

The bright areas flicker in video. Expected - this is a per-frame operation with no temporal term, like every other per-frame heuristic in the pack. For video, prefer the learned path.

I want the clipped detail back. Then use SDR → HDR Universal with its recovery path, which runs the RUDRA pixel model and actually predicts highlights. Note the trade: RUDRA weights are licensed for non-commercial use only, separate from Radiance's GPL-3.0 code - check that before you ship anything. Different tool, different licence, different job.

CategoryFXTD STUDIOS/Radiance/HDR

Inputs (6)

NameTypeDefaultDescription
imageIMAGESDR image. Highlights are found where the low-pass luma passes about 0.7; no transfer curve is removed, so linearise first for a linear result.
energy_targetFLOAT101–100Approximate brightness multiplier reached at SDR white (10.0 lifts 1.0 to about 9.6, roughly 3.3 stops). 1.0 = no lift.
recovery_itersINT30–8Pyramid depth. The lift is applied to the 1/2^N low-pass, so detail finer than about 2^N px keeps its original contrast. 0 lifts the whole image. It does not reconstruct clipped detail.
chroma_preservationFLOAT0.800–10 adds neutral highlight energy, reducing saturation as brightness rises. 1 preserves the original low-pass RGB ratios. Both use the same lifted luminance.
guidance_maskoptMASKPer-pixel guidance mask from Radiance Luminance Guidance.
guidance_nitsoptFLOAT00–10000Target peak in nits inside guidance_mask, on the package's scale (203 nits = 1.0). 0 = ignore the mask and use energy_target everywhere.

Outputs (2)

NameTypeDescription
imageIMAGE—
highlight_maskIMAGE—