π¬ CYH Post Process | Film Grain
Film grain that clumps like the real thing, not static
- image
- IMAGE
Most "film grain" filters in image editors are just uniform white noise - it reads as static on a screen, not grain on film. Real grain clumps: it's spatially correlated, it's worse in the shadows, and it scales with how sensitive your "film" was. This node does all three, which puts it a cut above the default noise overlay.
It's one of four post-process nodes in the Chye ComfyUI Toolset, sitting right after VAE Decode in a classic workflow: KSampler β VAE Decode β CYH Post Process | Film Grain β Save. And it's the one people in the film-emulation corner of the community tend to respect, because it's actually modeling grain rather than slapping a texture on.
How it works
The grain isn't generated per-pixel - it's filtered noise. The pack generates a random noise field, then runs it through a Gaussian filter (scipy.ndimage.gaussian_filter) so the particles clump into visible grain structure instead of staying as independent pixels. Then three things modulate it:
- ISO scales grain logarithmically -
log10(iso / 100). ISO 400 is modest, 6400 is heavy. That's how real film responds: grain grows with sensitivity, but not linearly. - shadow_boost multiplies grain in dark areas, because shadows on real film show grain first.
- strength is the master 0β1 dial that sets how visible the whole thing is.
There's also a luminance-based pass so grain lands on the shadows and highlights appropriately rather than uniformly across the frame.
The inputs that matter
Four inputs, one IMAGE:
- strength (0β1, default 0.5) - overall intensity. Start at 0.3β0.4 and creep up.
- iso (100β6400, default 400) - the character knob. 400 is subtle, 1600+ starts to look like pushed film.
- grain_size (1β10, default 2) - particle size. 1β2 is fine grain, 8β10 is chunky and dreamy.
- colored (default off) - off gives monochrome grain (the classic look); on generates a separate grain pattern per RGB channel, which reads as color noise / chroma grain.
Output is IMAGE, straight through to a save node or further processing.
Where it bites
Two things, both real:
First, only the first image in a batch gets processed. The code grabs image_np[0] and processes one frame. Feed it a batch of 4 frames expecting a grainy grid and you'll get one grainy frame plus three untouched ones. For single images this is a non-issue; for latent-video or batch pipelines it will silently do the wrong thing.
Second, it's a CPU numpy pass, not a GPU one - the node runs its math in numpy after moving the tensor off the GPU. On a 1024Γ1024 image that's a noticeable-but-fine hiccup (a second or two); on a 4K frame it adds up. It's not a real-time filter.
Install
ComfyUI Manager (search "Chye ComfyUI Toolset"), or:
cd ComfyUI/custom_nodes
git clone https://github.com/chyer/Chye-ComfyUI-Toolset
cd Chye-ComfyUI-Toolset
pip install -r requirements.txt
Restart ComfyUI. This node is why scipy is in the pack's requirements.txt - the Gaussian-filtered noise needs it. opencv-python also installs with the pack. Zip installs need .git/.cnr-id (Chye-ComfyUI-Toolset) per the README.
Verdict
For a "vibe" filter this is a surprisingly good one - correlated noise is the difference between "looks like a filter" and "looks shot on film." The single-frame limitation is the only real complaint. If you're batch-generating, process images one at a time or pick a different tool.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | β | |
| strength | FLOAT | 0.500β1 | β |
| iso | INT | 400100β6400 | β |
| grain_size | FLOAT | 2.01β10 | β |
| colored | BOOLEAN | false | β |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| IMAGE | IMAGE | β |