HDR Grain Matcher
Grain that scales with exposure instead of eating your highlights
- target
- reference
- grained_image
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.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| target | IMAGE | Clean RGB image or sequence to receive grain, ideally scene-linear. Negative values are clamped to 0. | |
| reference | IMAGE | Grainy 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. | |
| intensity | FLOAT | 1.000–5 | Multiplier on the extracted grain. 0 = no grain, 1 = matched to the reference. |
| kernel_size | INT | 31–15 | Box-blur size in pixels used to split grain from the image. Larger values capture coarser grain (and more image detail); 1 extracts nothing. |
| r_gain | FLOAT | 1.000–2 | Extra grain multiplier for the red channel. 1.0 = unchanged. |
| g_gain | FLOAT | 1.000–2 | Extra grain multiplier for the green channel. 1.0 = unchanged. |
| b_gain | FLOAT | 1.000–2 | Extra grain multiplier for the blue channel. 1.0 = unchanged. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| grained_image | IMAGE | — |