Nodes/radiance/HDR Expand Dynamic Range
ComfyUI Node

HDR Expand Dynamic Range

Stretching an SDR image into HDR, honestly

By FXTD-Studios·Created 8 months ago·Updated about 18 hours ago· 246
HDR Expand Dynamic Range
  • image
  • image
◄source_gamma2.2►
◄highlight_recovery1.0►
◄black_point0.000►
◄target_stops14.0►
◄highlight_rolloff1.5►

An SDR JPEG has everything above what the sensor clipped crammed into the top of its 0–1 range. There's no information up there to recover - but there is latitude to redistribute, and that's what HDR Expand Dynamic Range does. It decodes the SDR gamma to linear, pushes SDR white up to a much higher linear value, and rolls the highlight shoulder off so something downstream has real headroom to grade into.

Let's be clear about what this is and isn't. It's a curve. It does not invent highlight detail; nothing here is generated. If you need plausible content above the clip point, that's the pack's learned SDR→HDR path (SDR → HDR Universal), which runs the RUDRA model. This node is the deterministic half - and it's genuinely useful, because it's stable, instant, and won't hallucinate a window where there wasn't one.

How it works

The pipeline is: decode → black point → expand → roll off.

source_gamma drives the decode. There's a nice bit of care here that the tooltip explains: values between 2.1 and 2.3 get the exact sRGB piecewise curve rather than a naive power, 1.0 means the input is already linear, and anything else is treated as a pure power (2.4, 2.6). That's why the default of 2.2 doesn't technically mis-decode sRGB - it just tells the node which family you're in.

target_stops is the headline control. It sets where SDR white lands: 2^(stops − 8) in linear. At the default of 14 that's 2^6 = 64.0. At 8 you get no expansion at all. So the numbers are doing exactly what the name says, and the default is a deliberately aggressive 64× headroom.

highlight_rolloff (default 1.5) decides where expansion starts: a linear luma threshold of 1 − (rolloff − 1) × 0.2, capped at 0.95. Default puts that at 0.9; 3.0 puts it at 0.6. Higher values start expanding earlier and gentler, which is what you want on a scene with lots of mid-bright texture.

highlight_recovery mixes between the plain linearised input (0) and the full expansion (1) - and the tooltip notes that values above 1 push past the expansion curve, which is a straightforward way to overshoot on purpose. black_point subtracts a linear value before expansion and clips at 0, so positive crushes and negative lifts.

One output: image, in linear light. Alpha passes through untouched.

Install

Manager → search Radiance → Install → restart → refresh your browser.

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

Windows portable users: use python_embeded\python.exe for pip. No models, no downloads - unlike the pack's SDR→HDR nodes, which do pull RUDRA weights on first use.

Where people get burned

The output is linear and peaks around 64 with the defaults, so a normal preview of it looks blown out and wrong. It isn't - you're looking at linear values through a display that expects gamma-encoded ones. Put HDR Monitor after it to see what you're actually doing, or wire it into a pipeline that expects scene-linear.

Second, target_stops at 14 is a big stretch for a phone photo that was already clipped. If your highlights were gone in the source, expansion just makes a bigger, softer white blob out of them. Drop the stops, or use the learned SDR→HDR path if the frame matters.

Third: because nothing is clamped here, whatever you do gets amplified downstream. Feed this into a VAE without a compression step and you'll get banding and weird colour casts. That's what HDR Color Pipeline's compression_ratio and the pack's per-channel normalisation are for.

CategoryFXTD STUDIOS/Radiance/HDR

Inputs (6)

NameTypeDefaultDescription
imageIMAGEDisplay-encoded SDR image. Output is linear light; alpha passes through untouched.
source_gammaFLOAT2.21–3Input decoding. 2.1 to 2.3 uses the exact sRGB curve, 1.0 means already linear, any other value is a pure power (e.g. 2.4, 2.6).
highlight_recoveryFLOAT1.00–2Mix between the linearised input (0) and the full expansion (1). Values above 1 push past the expansion curve.
black_pointFLOAT0.000-0.1–0.1Linear value subtracted before expansion, then clipped at 0. Positive crushes blacks, negative lifts them.
target_stopsFLOAT14.08–20Sets the peak that SDR white is mapped to: 2^(stops - 8) in linear, so 14 gives 64.0 and 8 gives no expansion.
highlight_rolloffFLOAT1.51–3Where expansion starts: linear luma threshold 1 - (rolloff - 1) x 0.2, capped at 0.95 (0.9 at the default, 0.6 at 3.0). Higher starts earlier with a gentler curve.

Outputs (1)

NameTypeDescription
imageIMAGE—