HDR Expand Dynamic Range
Stretching an SDR image into HDR, honestly
- image
- image
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.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | Display-encoded SDR image. Output is linear light; alpha passes through untouched. | |
| source_gamma | FLOAT | 2.21–3 | Input 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_recovery | FLOAT | 1.00–2 | Mix between the linearised input (0) and the full expansion (1). Values above 1 push past the expansion curve. |
| black_point | FLOAT | 0.000-0.1–0.1 | Linear value subtracted before expansion, then clipped at 0. Positive crushes blacks, negative lifts them. |
| target_stops | FLOAT | 14.08–20 | Sets the peak that SDR white is mapped to: 2^(stops - 8) in linear, so 14 gives 64.0 and 8 gives no expansion. |
| highlight_rolloff | FLOAT | 1.51–3 | Where 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)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |