DiffHDR Preprocess
The log curve and the mask that make a native VACE graph HDR
- images
- control_video
- control_masks
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) orpano. Matches the LoRA variant you applied upstream.mask_overexposed(on) andmask_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_video→WanVaceToVideo.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_masks→WanVaceToVideo.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_maskswith a mask preview before you blame the sampler.mask_overexposedoff 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_underexposedback 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_apiV3 node interface and registers nothing on older builds.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| images | IMAGE | sRGB LDR frames at the final processing size (multiples of 16). | |
| variant | COMBO | video | Mask detector: video/image (temporally stabilised) or pano. |
| mask_overexposed | BOOLEAN | true | Detect over-exposed regions (video variant). |
| mask_underexposed | BOOLEAN | false | Detect under-exposed regions and paint them mid-grey (video variant). |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| control_video | IMAGE | Log-encoded frames in [0,1]. Keep as float; do not route through 8-bit nodes. |
| control_masks | MASK | 1 = regenerate. |