X2HDR PU21 Decode
You can't save HDR straight off VAE Decode — this node is the missing step
- image
- hdr_image
- metrics_json
If you've run an "HDR" model through ComfyUI and saved the image straight off VAE Decode, you know exactly what this node is for. The result comes out flat, gray, washed out - nothing like the punchy examples. That's not a broken model. In the X2HDR scheme, what VAE Decode hands you is a PU21-encoded image, a perceptually uniform encoding of HDR, not linear HDR. A PNG of that is only a preview.
X2HDR PU21 Decode is the inverse. It takes that PU21-space tensor and converts it into linear float HDR RGB - the thing you can actually save as an EXR, grade, and call HDR.
Why this exists at all: the X2HDR paper (HDR Image Generation in a Perceptually Uniform Space) makes the point that HDR images are normally stored as linear RGB, while diffusion VAEs are trained mostly on display-encoded LDR images. The value statistics of linear HDR are so different that feeding them straight through an LDR-pretrained VAE wrecks reconstruction. So X2HDR encodes HDR into PU21 space for training and model output, and someone has to invert it on the way out. That someone is this node. Note this pack (facok/comfyui-x2hdr) is an independent implementation, not the official X2HDR/X2HDR repo - but it implements the same math.
The practical pairing is the Krea2 X2HDR LoRA run on Krea-2-Raw or Krea-2-Turbo. The LoRA teaches the denoiser to emit PU21 output; this node unwraps it. Without a matching PU21-trained model or LoRA upstream, decoding an ordinary LDR image gives you wrong colors and garbage luminance, not HDR. The node doesn't invent HDR - it only inverts its named encoding.
The inputs that actually matter
- input_range -
0_1(default) orminus1_1. Chooseminus1_1only when the upstream tensor is centered on [-1, 1] instead of [0, 1]. Get this wrong and everything shifts. - apply_l_peak + l_peak - applies the peak-luminance scale;
l_peakdefaults to 4000 nits, which matches the README's canonical workflow. If your LoRA was trained against a different peak, match it. - target_luminance - 16 by default. This percentile-normalizes the decoded result so it behaves sensibly inside ComfyUI. Set it to 0 to disable normalization and keep the raw decoded scale as much as possible.
- target_percentile - 99.5 by default; the luminance percentile used for that normalization.
- clamp_pu21 - clamps encoded values to [0, 1] before decoding. Leave it on.
Outputs
- hdr_image (
IMAGE, float32) - linear HDR. Values above 1.0 are normal here; that's the whole point. Wire it intoX2HDR Save EXR,X2HDR Dynamic Range QA,X2HDR Tone Map Preview, orX2HDR Color Grade. - metrics_json (
STRING) - decode statistics. Wire it to a text-display node if you want to eyeball the numbers.
Installing it
The whole pack installs the same way. ComfyUI Manager → search comfyui-x2hdr → install, restart, and the nodes appear under image/HDR/X2HDR. Or manually:
cd ComfyUI/custom_nodes
git clone https://github.com/facok/comfyui-x2hdr
then restart ComfyUI. The pack declares exactly one extra dependency, PyAV (av), in pyproject.toml rather than requirements.txt - and it imports av at load time. If you get an import error, run pip install av inside ComfyUI's Python environment and restart.
Where people get burned
- The flat preview is not a bug. Raw linear HDR looks dark and washed out until tone-mapped. Judge it through
X2HDR Tone Map PrevieworX2HDR Color Grade, never by the raw pixels. - Wrong colors after decode almost always means the model wasn't actually PU21-trained. Run the output through
X2HDR Dynamic Range QA- areviewverdict with "does_not_exceed_sdr_reference" is your tell. - This pack is brand new - a single commit, essentially zero community coverage - so the README's decode defaults are the best spec you'll get. Keep them until you know why you're changing them.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| input_range | COMBO | 0_1 | 2 options: 0_1, minus1_1 |
| apply_l_peak | BOOLEAN | true | — |
| l_peak | FLOAT | 40000–100000 | — |
| target_luminance | FLOAT | 16.00–100000 | — |
| target_percentile | FLOAT | 99.50–100 | — |
| clamp_pu21 | BOOLEAN | true | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| hdr_image | IMAGE | — |
| metrics_json | STRING | — |