Nodes/ComfyUI CV/CV HDR Tonemap
ComfyUI Node

CV HDR Tonemap

Four operators, one radiance map, no guessing

By bmad4ever·Created 4 months ago·Updated 15 days ago· 1
CV HDR Tonemap
  • radiance
  • image
◄methodReinhard►
◄gamma2.2►
◄saturation1.00►
◄bias0.85►
◄intensity0.0►
◄light_adapt1.00►
◄color_adapt0.00►
◄scale0.70►

You have a float radiance map - linear light with values well past 1.0 - and a screen that tops out at 1.0. Something has to compress the range, and how you compress it is the difference between "HDR photo" and "mud". This node is that decision, four ways.

It's the last stage of the hdr family in bmad4ever's ComfyUI CV (bmad4ever/comfyui_cv, a fork of Gerold Meisinger's opencv-comfyui), taking the output of CV HDR Merge to Radiance and producing an ordinary 0–1 IMAGE.

The four methods

method picks the operator, and the choice matters more than the sliders:

  • linear - plain gamma correction. output = input^(1/gamma), no local adaptation, no range compression beyond the curve. Honest, fast, and it clips: whatever is above white stays white. Useful as a baseline, and as the answer when you've already graded the radiance map yourself.
  • Reinhard - the default, and the safe one. A global-plus-local adaptation operator that keeps detail in both shadows and highlights. intensity (-8 to 8) shifts overall brightness, light_adapt (1.0) sets how local the adaptation is - 1 fully local, 0 global - and color_adapt (0.0) at 1 does per-channel adaptation, which is an auto-white-balance you usually don't want.
  • Drago - built for very bright scenes. bias (0.85; 0.7–0.9 typical) sets the tone curve's bias and saturation (1.0) the colour. It's the one that makes a sunlit window look like something rather than a white rectangle.
  • Mantiuk - contrast-domain. scale (0.7; 0.6–0.9 typical) is the contrast factor and saturation again is colour. It's the punchiest of the four and the easiest to overshoot into crunchy HDR-postcard territory.

gamma (2.2) applies to every operator, not just linear - 1.0 is linear, 2.2 matches a typical display, higher brightens midtones. And to be clear about which knobs do anything: saturation, bias, intensity, light_adapt, color_adapt and scale are each read only by their own method, as the tooltips say. Dragging bias while you're on Reinhard changes nothing, and that's not a bug.

The output is a single image - an ordinary 0–1 IMAGE you can preview, save or feed into the rest of a graph.

What's actually happening, and why the operator choice is real

Tone mapping is not "brightness with extra steps." The radiance map is linear light; your viewer is not. A tone-mapping operator models how an eye or a display adapts - compartmentalising the image into regions and compressing each locally - which is why Reinhard and Mantiuk can hold window detail and shadow detail in the same frame while a plain gamma curve cannot. This is the same distinction the KB draws in the colour-management section: sRGB tensors and scene-linear float are different worlds, and moving between them is where the interesting decisions live.

A practical consequence of doing it properly: one radiance map, several tone curves. The merge is the expensive, data-dependent part; the tonemap is a cheap transformation on top of it. The pack's own playground exploits that, rendering Reinhard, Drago and Mantiuk from the same radiance so the comparison is fair - same input, three interpretations, side by side.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"

Or find ComfyUI CV in ComfyUI Manager. Python ≥ 3.12 and a recent ComfyUI on the V3 node API. workflows/15_hdr_playground.json is the one to open - four exposures, alignment, a Debevec merge and all three operators previewed.

Common issues

Black, or a flat grey frame. Almost always the input: a raw 0–1 image into radiance rather than a float radiance map. Feed it from CV HDR Merge to Radiance (or preview the radiance first with Preview CV Array in normalize mode and check the numbers actually go past 1.0).

Crunchy, haloed, "HDR from 2008". You're on Mantiuk with too much scale, or on Drago with the wrong bias. Reinhard at intensity 0 with light_adapt 1 is the neutral starting point - go back there and change one thing at a time.

The image is too dark overall. intensity (Reinhard) and gamma are different levers: intensity re-adapts the mapping, gamma moves midtones. Try gamma 1.8–2.4 before you start moving intensity around.

Colours drift. color_adapt above 0 on Reinhard, or saturation away from 1.0 on Drago/Mantiuk. Both are doing what they say; set them back to neutral and grade downstream where you can see what you're doing.

Categoryimage/CV/hdr

Inputs (9)

NameTypeDefaultDescription
radianceNPARRAYHxWx3 float32 radiance map from 'HDR Merge to Radiance'.
methodCOMBOReinhardTonemapping operator. linear = plain gamma; Reinhard is a safe default; Drago suits very bright scenes; Mantiuk is contrast-domain.
gammaFLOAT2.20.1–5Gamma applied by every operator. 1.0 = linear; 2.2 matches a typical display; higher brightens midtones.
saturationoptFLOAT1.000–3Drago/Mantiuk only: color saturation of the result.
biasoptFLOAT0.850–1Drago only: bias of the tone curve (0.7-0.9 typical).
intensityoptFLOAT0.0-8–8Reinhard only: overall brightness (- darker, + brighter).
light_adaptoptFLOAT1.000–1Reinhard only: 1 = fully local adaptation, 0 = global.
color_adaptoptFLOAT0.000–1Reinhard only: 1 = per-channel (auto white-balance) adaptation, 0 = shared across channels.
scaleoptFLOAT0.700–2Mantiuk only: contrast scale factor (0.6-0.9 typical).

Outputs (1)

NameTypeDescription
imageIMAGETonemapped display image (0..1 IMAGE).