Nodes/radiance/HDR Grain Matcher
ComfyUI Node

HDR Grain Matcher

Grain that scales with exposure instead of eating your highlights

By FXTD-Studios·Created 8 months ago·Updated about 18 hours ago· 246
HDR Grain Matcher
  • target
  • reference
  • grained_image
◄intensity1.00►
◄kernel_size3►
◄r_gain1.00►
◄g_gain1.00►
◄b_gain1.00►

Grain is the cheapest realism upgrade there is, and the community has been saying it for years - as one commenter put it in a thread about making AI images read as photographs, "I also add grain and it does a lot for realism," right next to "motion blur + camera shake + grain + red channel aberration + vignetting." The catch is that grain added the obvious way - additive noise on a 0–1 image - looks like video noise, and it destroys highlights, because noise added in display space is enormous relative to a bright pixel's value and invisible on a dark one.

HDR Grain Matcher does it the way a film pipeline does. It extracts grain from a reference plate, works in log2 exposure space, and transfers it so the grain scales with exposure - dense in the shadows, subtle in the highlights - which is what real film does.

How it works

The reference plate is pushed into log2 space, box-blurred, and the blur is subtracted from the original. Whatever survives is high-frequency detail: grain, texture, sensor noise, all of it. That residual is then added to the target in log2, which is what makes the magnitude exposure-relative rather than absolute - adding a fixed amount in log space is a multiplicative change in linear light. Same trick as exposure itself.

The blur is F.avg_pool2d with your kernel_size, so it's a fixed box filter, not an edge-aware one. That has a consequence: anything high-frequency in the reference comes with it.

Then each channel gets its own multiplier (r_gain, g_gain, b_gain) so you can match the reference plate's colour grain - real stocks are not neutral.

Practically: kernel_size (odd, 1–15, default 3) is the main knob. Small captures fine grain and little else; large starts dragging coarser texture across. intensity is a straight multiplier on the extracted grain, where 1 means "matched to the reference." One output: grained_image.

Install

Manager → search Radiance → Install → restart → refresh your browser.

cd ComfyUI/custom_nodes
git clone https://github.com/fxtd-studios/radiance.git
cd radiance
python -m pip install -r requirements.txt

Windows portable users should use ComfyUI's bundled python_embeded\python.exe for the pip step. No models to download - this is a pure tensor operation.

Where people get burned

Pick a flat reference. Because the node transfers all high-frequency detail, a busy reference plate doesn't give you its grain, it gives you its content - foliage, fabric, skin texture - stamped onto your target. The tooltip says it outright: use a flat, defocused area if possible. This is the single biggest quality difference between a good and a bad result here.

The reference must match the target's width and height. No resizing happens for you. A 4K plate against a 1080p target will error or misbehave; the frames do cycle if the reference is shorter than the target batch, so a short clip is fine, a differently-sized one isn't.

kernel_size of 1 extracts nothing - the tooltip is honest that the box blur degenerates and you get a no-op. Use odd numbers; the widget steps by 2 anyway.

And the practical one on long shots: process a few test frames before queueing 240. Grain extraction is cheap, but you'll want to eyeball intensity against a real frame rather than a still.

CategoryFXTD STUDIOS/Radiance/VFX

Inputs (7)

NameTypeDefaultDescription
targetIMAGEClean RGB image or sequence to receive grain, ideally scene-linear. Negative values are clamped to 0.
referenceIMAGEGrainy plate to take grain from. Must match the target's width and height; frames are reused in a cycle if it is shorter than the target. All high-frequency detail is transferred, so use a flat, defocused area if possible.
intensityFLOAT1.000–5Multiplier on the extracted grain. 0 = no grain, 1 = matched to the reference.
kernel_sizeINT31–15Box-blur size in pixels used to split grain from the image. Larger values capture coarser grain (and more image detail); 1 extracts nothing.
r_gainFLOAT1.000–2Extra grain multiplier for the red channel. 1.0 = unchanged.
g_gainFLOAT1.000–2Extra grain multiplier for the green channel. 1.0 = unchanged.
b_gainFLOAT1.000–2Extra grain multiplier for the blue channel. 1.0 = unchanged.

Outputs (1)

NameTypeDescription
grained_imageIMAGE—