HDR Blend Validator
Proof that your HDR blend did anything at all
- image_a
- image_b
- winner
- report_json
- ssim
- js_divergence
- dr_delta_stops
Here's the uncomfortable truth about HDR tooling: "the HDR version looks better" is almost always you comparing two different tone maps. The brighter one wins. HDR Blend Validator exists to strip that out - you give it a baseline and an HDR-blended output, and it gives you numbers on whether the blend actually expanded range or just moved the tones around.
This is an A/B check, not a processing node. Wire it in when you're tuning an HDR step and want to stop chasing your own eyes, or when you're publishing a claim (a LoRA, a workflow, a model) that your HDR pass is worth the effort. It also makes a decent CI assertion - the report_json output is machine-readable on purpose.
How it works
Three metrics, computed independently:
- SSIM - structural similarity between the two. Closer to 1.0 means the blend barely changed structure. The node's own guidance: below 0.95 you've made a significant structural change; if you wanted the blend to preserve detail, that number is a red flag, not a win.
- Jensen-Shannon divergence on the luma histogram. This is how differently the two images distribute brightness. An HDR blend that did something is expected to push this above roughly 0.01 - below that, your blend is a no-op in tonal terms.
- Dynamic range delta in stops, computed as
DR_b − DR_awhereDR = log2(p99 / (p1 + ε)). Positive means range expanded. Negative means you compressed - which is a genuinely useful thing to catch, because a "looks punchier" blend is often a compressed one.
If the two inputs differ in size, the second one is bilinearly resized to match the first, so you can compare a crop against a full frame without it erroring - but you probably shouldn't, since resampling shows up in SSIM.
Inputs and outputs
Only three inputs. image_a is the baseline (no HDR blend), image_b is the HDR-blended output, and win_metric picks which metric decides the winner: dynamic_range, ssim or js_divergence. With dynamic_range - the default - you get image_b back when the delta is positive and image_a when it isn't, which is exactly the "did the blend earn its place" test.
Five outputs: winner (IMAGE), report_json (STRING), ssim (FLOAT), js_divergence (FLOAT), dr_delta_stops (FLOAT).
The winner output is the one people miss. You can wire it straight into your save or monitor path, so a failed blend silently falls back to the baseline instead of shipping a worse frame. Set win_metric with intent though - ssim optimises for "did nothing changed", which is the opposite of what an HDR blend is supposed to do.
Install
Manager → search Radiance → Install → restart → refresh.
cd ComfyUI/custom_nodes
git clone https://github.com/fxtd-studios/radiance.git
cd radiance
python -m pip install -r requirements.txt
On Windows portable, run pip with python_embeded\python.exe. No model downloads - SSIM and histogram math are cheap local operations.
Where people get burned
Comparing things that shouldn't be compared is the fast way to a meaningless number. If image_b came from a different resolution, a different random seed, or a denoising pass, SSIM drops for reasons that have nothing to do with HDR, and you'll conclude the blend destroyed structure when it didn't. Keep everything but the HDR step identical.
Second, don't read dr_delta_stops as "how much HDR I gained." It's the spread between the 1st and 99th percentile luma - a distribution measure, not a peak. A blend that lifts highlights while crushing blacks can show a modest delta and still be a real improvement, while one that just raises overall exposure can inflate it. Read it alongside the other two numbers or not at all.
And the standard pack-wide reminder: the whole Radiance install pulls OpenImageIO, OpenColorIO, OpenEXR, diffusers and accelerate whether or not the one node you wanted needs them.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| image_a | IMAGE | Baseline (no HDR blend) | |
| image_b | IMAGE | HDR-blended output | |
| win_metric | COMBO | dynamic_range | Which metric decides the winner. |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| winner | IMAGE | — |
| report_json | STRING | — |
| ssim | FLOAT | — |
| js_divergence | FLOAT | — |
| dr_delta_stops | FLOAT | — |