Nodes/ComfyUI-DiffHDR/DiffHDR Preprocess
ComfyUI Node

DiffHDR Preprocess

The log curve and the mask that make a native VACE graph HDR

By claussteinmassl·Created 2 days ago·Updated a day ago· 5
DiffHDR Preprocess
  • images
  • control_video
  • control_masks
variantvideo
mask_overexposedtrue
mask_underexposedfalse

DiffHDR Preprocess is the front half of the pack's modular pipeline: it takes your plain sRGB LDR frames and produces the two things a stock WanVaceToVideo node needs - a log-encoded control video and a mask of the regions that should be regenerated.

If you're using the all-in-one DiffHDR (Image / Video) node you never touch this; it does the same work internally. You're here because you want DiffHDR inside a VACE graph you already have, next to other controls or extra LoRAs.

What it actually computes

Two things, from the source:

The control video. Each frame is clamped to 0–1, its alpha dropped, converted from sRGB to linear light (the sRGB EOTF), then pushed through DiffHDR's log-gamma curve: pow(log(2.2·x + 1) / log(2.2·65536 + 1), 1/2.2). The result is a float tensor in [0, 1] where four or five stops of highlight live in a small, well-conditioned slice of the range instead of being pinned at 1.0. That's the whole trick: the model gets handed something it can actually reason about in the bright end.

The mask. Over-exposure is detected on Rec.709 luma: a soft threshold around 0.95, weighted with a signal that fires when at least two channels are pinned at 1.0. That soft mask is closed with a 3×3 dilation/erosion, then for the video variant it goes through a temporal pass - an exponential moving average across frames plus morphology and a final threshold - so the mask doesn't flicker from frame to frame. Temporally stable masks are the difference between "regenerated" and "strobing".

There's an under-exposure path too. Crushed blacks are detected with the mirror-image threshold (0.01), and when you switch it on the node also paints those regions mid-grey before they go into the control video, so the model isn't being handed a flat black hole and asked to imagine it.

The pano variant swaps the detector for a single-image one suitable for equirectangular input (no temporal pass - there's nothing to stabilise) and applies the same mask to every frame.

Inputs and outputs

  • images - your LDR frames. The tooltip is a real constraint, not boilerplate: they must already be at the final processing size, and that size must be a multiple of 16. This node does not resize. Put a resize node in front.
  • variant - video (default) or pano. Matches the LoRA variant you applied upstream.
  • mask_overexposed (on) and mask_underexposed (off) - which detector runs. Turning both off gives you an empty mask, which means VACE is being asked to reconstruct everything; that is not the workflow.
  • control_videoWanVaceToVideo.control_video. Keep it float; the tooltip says don't route it through 8-bit nodes, and it means it - quantising the log-encoded values to 8 bits throws away the graded highlight range you just built.
  • control_masksWanVaceToVideo.control_masks. 1 means regenerate.

The rest of the graph is not this node's business, which is exactly where people trip: DiffHDR Apply LoRA on the model, then ModelSamplingSD3 at shift 8, KSampler at cfg 1, TrimVideoLatent, VAEDecode, DiffHDR Postprocess. Skip the postprocess and you'll be looking at log-encoded frames wondering why the HDR looks grey. The repo's workflows/diffhdr_modular.json is the graph to copy.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/claussteinmassl/ComfyUI-DiffHDR.git
pip install -r ComfyUI-DiffHDR/requirements.txt   # OpenEXR + huggingface_hub

Restart ComfyUI and the node appears under the DiffHDR category. Manager users: search the pack title. The OpenEXR requirement is only needed for the save node, but it comes from the same requirements.txt, so install the lot.

Common problems

  • Bizarre aspect ratio, or an error about the VACE latent shape. Your frames aren't a multiple of 16 (or you fed a 1280×720 frame into a graph configured for something else). This node passes size straight through; check the resize upstream.
  • Everything regenerated, or nothing. Look at control_masks with a mask preview before you blame the sampler. mask_overexposed off by accident looks exactly like "the model did nothing".
  • Underexposed regions look like flat grey mud in the output. That's the mid-grey fill the under-exposure path paints. It's intentional, but if you want the crushed blacks left alone, turn mask_underexposed back off - most people should.
  • The mask flickers on real footage. The video detector's EMA is tuned for clips with a consistent exposure; a shot where the camera really is sweeping exposure will legitimately move the mask. That's not a bug, but it does mean the reconstruction moves too.
  • Notice that your turbo LoRA is off. Step-distillation LoRAs wreck this pipeline by shifting the value of the log-encoded output, which the log curve then explodes. The pack measured thirteen of them; none helped and all of them changed the highlights.
  • Nodes missing from the menu after install is almost always an out-of-date ComfyUI - this pack registers through the newer comfy_api V3 node interface and registers nothing on older builds.
CategoryDiffHDR

Inputs (4)

NameTypeDefaultDescription
imagesIMAGEsRGB LDR frames at the final processing size (multiples of 16).
variantCOMBOvideoMask detector: video/image (temporally stabilised) or pano.
mask_overexposedBOOLEANtrueDetect over-exposed regions (video variant).
mask_underexposedBOOLEANfalseDetect under-exposed regions and paint them mid-grey (video variant).

Outputs (2)

NameTypeDescription
control_videoIMAGELog-encoded frames in [0,1]. Keep as float; do not route through 8-bit nodes.
control_masksMASK1 = regenerate.