SDR → HDR Recover
The node that would rather fail than fake it
- image
- image
- highlight_mask
- shadow_mask
- highlight_confidence
- shadow_confidence
There are two ways to turn an SDR image into HDR. One is a curve you can do in your head. The other is a trained model guessing what was in the blown-out sky. SDR → HDR Recover is the honest version of the second: it runs the RUDRA learned model and nothing else, and if the model isn't installed it raises instead of quietly falling back to the curve. That behaviour is the reason to pick it over the friendlier Universal node.
How it works
The pack's ground truth is blunt about the architecture: "No VAE needed." Radiance 3.5.0 retired the latent-space RUDRA decoders, so this node runs a direct pixel model - SDR pixels in, scene-linear out - plus a temporal residual model over 5/7/9 adjacent frames when you flip batch_mode to Video Frames and the temporal checkpoint is present.
The mask logic is what keeps it from wrecking your image. It identifies clipped luma or RGB channels above highlight_threshold and crushed shadows below shadow_threshold, then applies the learned reconstruction only inside those masks. Pixels outside them are preserved exactly. So a mid-grey wall that was never clipped does not get "enhanced" - it comes back bit-for-bit.
That's also the node's limit, and the pack documents it: learned highlight recovery restores brightness, not colour. The released checkpoints were trained on SDR rendered at -1 EV, which almost never clipped, so in genuinely blown areas the model takes only the learned luminance, keeps your source hue, and fades it in as the source goes to white. A highlight clipped in a single channel - a saturated red light - gets no learned guess at all.
Inputs you'll actually touch
inverse_oetf - the transfer curve of your source (sRGB, Rec.709, Gamma 2.2, Gamma 2.4, or None if it's already linear). The input is clamped to 0–1 first, which is a hint that this expects display-encoded SDR, not a float HDR plate.
peak_nits (default 1000, up to 10000) - the mastering peak. Recovered highlights keep the source hue and no channel exceeds it, so this is a hard ceiling, not a suggestion.
highlight_threshold (default 0.98) and shadow_threshold (default 0.05) - where recovery is considered valid. The pack's own known-issues list flags the consequence: because the mask ramps over codes 0.98 → 1.0, noisy or dithered 8-bit input makes the mask edge speckle. Drop highlight_threshold to around 0.9 and the ramp widens.
highlight_strength / shadow_strength (default 1) - dial either pass down without touching the other. Set one to 0 and that class of pixel stays at its decoded SDR level.
output_encoding - Linear (1.0 = reference_white_nits), Linear ACES2065-1 (AP0) for EXR work, or PQ (HDR10) / HLG for delivery. reference_white_nits defaults to 203, the BT.2408 HDR reference white, and it's the same convention HDR Encode and the Write nodes use - keep them equal and a Linear output re-encodes to the same PQ signal.
Outputs: image, plus highlight_mask, shadow_mask, highlight_confidence and shadow_confidence. The confidence masks are the interesting ones when a shot looks off - they show where the model was on firm ground.
Install
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
The RUDRA pixel checkpoint is small (~5 MB) and downloads on first use into ComfyUI/models/radiance/. Manual installs of the weights are documented on the pack's Hugging Face page, under the same folder. Two things to know: RADIANCE_ALLOW_DOWNLOADS=0 disables fetching entirely, and the RUDRA weights are non-commercial even though the code is GPL-3.0 - different licences, read both.
Troubleshooting
If it raises on a fresh machine, that is the design, not a bug: with no checkpoint and downloads off, only Universal can fall back. Point pixel_checkpoint at a local .pt, or let it download.
For video, remember the pixel model runs frame by frame unless the temporal checkpoint is installed - expect to deflicker afterwards. There's a real cost figure in the pack's notes too: ~19 seconds per 1080p frame on CPU. And leave pixel_tile_size at 512 or above; tiling only kicks in when a frame doesn't fit in memory, and each smaller tile sees less context, so the result shifts slightly.
Inputs (15)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | SDR still, image batch, or ordered video frames. | |
| inverse_oetf | COMBO | sRGB | Transfer curve the SDR input is encoded with, decoded to linear before recovery. None treats the input as already linear. Input is clamped to 0-1 first. |
| peak_nits | FLOAT | 1000200–10000 | Mastering display peak in nits. Recovered highlights keep the source hue and no channel exceeds it; it is also the PQ encode peak. |
| highlight_threshold | FLOAT | 0.9800.8–0.999 | SDR code-value threshold used to identify clipped luma or RGB channels. |
| shadow_threshold | FLOAT | 0.0500.001–0.5 | Linear-luma threshold used to identify crushed shadows. |
| highlight_strength | FLOAT | 1.000–1 | Scales the clipped-highlight recovery mask: 1 applies the learned highlights fully inside the mask, 0 leaves highlights at their decoded SDR level. |
| shadow_strength | FLOAT | 1.000–1 | Scales the crushed-shadow recovery mask: 1 applies the learned shadow detail fully inside the mask, 0 leaves shadows at their decoded SDR level. |
| output_encoding | COMBO | Linear | Linear: scene-linear Rec.709, 1.0 = reference_white_nits. AP0: the same in ACES2065-1. PQ (HDR10) and HLG: Rec.2020 delivery code values (BT.2100). |
| batch_modeopt | COMBO | Independent Images | Video Frames treats the batch as an ordered clip and prefers the temporal model. |
| pixel_checkpointopt | STRING | Direct-pixel .pt checkpoint. Empty searches models/radiance and RADIANCE_SDR2HDR_PIXEL, and downloads the default RUDRA model (~5 MB) on first use unless RADIANCE_ALLOW_DOWNLOADS=0. | |
| pixel_tile_sizeopt | INT | 512128–2048 | Tile size used only when the whole frame does not fit in memory. The model normalises over its input, so whole-frame inference is more accurate and is always tried first. |
| pixel_tile_overlapopt | INT | 640–512 | Overlap in pixels between tiles, feathered to hide seams. Used only when tiling kicks in; must be smaller than pixel_tile_size. |
| temporal_windowopt | COMBO | 5 | Adjacent frames used by the temporal residual model. |
| temporal_checkpointopt | STRING | Optional temporal checkpoint path. Empty uses RADIANCE_TEMPORAL_RUDRA or models/radiance. | |
| reference_white_nitsopt | FLOAT | 203100–1000 | Output convention: linear 1.0 = this many nits (BT.2408 reference white, 203). The same value HDR Encode and Write use, so Linear output re-encodes to the same PQ/HLG this node writes. Recover keeps unclipped pixels at their SDR display level (SDR 1.0 = 100 nits); only the recovered highlights and shadows change. |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| highlight_mask | MASK | — |
| shadow_mask | MASK | — |
| highlight_confidence | MASK | — |
| shadow_confidence | MASK | — |