ComfyUI Node

Mask Grain

Your soft matte is banding because it's 8-bit — dither it

By Code2Collapse·Created 7 months ago·Updated 2 days ago· 56
Mask Grain
  • mask
  • mask
  • report
amount6
seed0
grain_size4
invertfalse

The problem this solves

You've got a soft, feathered matte - a gradient, a blurred edge, a feathered selection - and you're using it to drive an effect at partial strength. The result shows steps: concentric bands where the mask crosses from 0.4 to 0.45 to 0.5. That's quantisation. The mask is being carried through an 8-bit-ish path, so it only has 255 distinct values, and when you stretch those across a large smooth transition the eye finds the breaks instantly.

The classic fix in graphics is dithering, and film grain is dithering with better branding. Add a small amount of random noise to the low-precision field before it gets thresholded or stretched, and the bands dissolve into a texture the eye reads as natural.

That's the entire job of Mask Grain. It is not a stylistic grain node - if you want grain on the picture, use Film Grain (MEC) in the same pack, which operates on IMAGE and is a Lightroom-style Amount/Size/Roughness implementation. This one operates on MASK and exists to hide banding.

How it works

Two random fields are generated at reduced resolution, averaged, upsampled bilinearly to the mask's size, and added to the mask symmetrically around zero. amount scales the amplitude (amount / 127, so 127 is the full half-range), grain_size sets how coarse the field is - larger means the noise starts smaller and gets stretched further, so you get coarser, less speckly grain.

The output is clamped back to 0–1 at the end, so a mask that was solid white stays solid white and a matte's interior doesn't develop holes.

Inputs and outputs

Required, all of them:

  • mask - the matte to dither.
  • amount - default 6. The tooltip is the calibration guide: "enough to hide banding, not enough to read as texture". If you can see the grain in the mask, you'll see it in the effect.
  • seed - and the default of 0 is doing real work here. Upstream reseeded per frame, so the noise field changed every frame of a batch and the matte crawled visibly in motion. A fixed seed holds the grain still through a sequence, which is what you want for an effect overlay that isn't supposed to move.
  • grain_size - default 4, range 1–64.
  • invert - flips the mask before the grain is added.

Outputs: mask (same shape as the input) and report.

Where it goes

Mask Gradient (MEC) or any feathered mask → Mask Grain → whatever consumes the matte. The one order-of-operations rule: dither before anything that thresholds, clamps or quantises the mask further. Dithering after a threshold does nothing, because the hard edges you'd be trying to soften are no longer a gradient - that's a feathering job.

If you're driving an inpaint strength or an alpha ramp with the mask, this is the node that turns a visible staircase into a clean falloff. If you're feeding a binary region selector, you're using the wrong tool; use amount 0 and go home.

Install

ComfyUI Manager → search "CustomNodePacks", or:

cd ComfyUI/custom_nodes
git clone https://github.com/Code2Collapse/ComfyUI-CustomNodePacks

Restart ComfyUI. Pack dependencies are opencv-python, scipy, safetensors; this node needs none of them - tier 1, no models, no VRAM, deterministic (the RNG is a seeded CPU generator). The README's standing warning applies: don't run pip install -r requirements.txt blind or you may overwrite ComfyUI's own torch/numpy/Pillow.

Common issues

  • Visible speckle in the output. amount too high for the size of the mask features. Try 4–8 with grain_size 3–6. Dither should be invisible at 100% and only show up as an absence of banding.
  • The matte now shimmers in video. You changed seed between frames, or something upstream is reseeding. Keep the seed fixed - temporal stability is the reason this node exists.
  • Banding is still there. Then your banding isn't coming from the mask. Check the picture side of the pipeline: a smooth sky in the image bands on its own, and no mask dither will fix that - that's Film Grain (MEC) on the IMAGE socket, or a 16-bit round trip.
CategoryMEC/Mask

Inputs (5)

NameTypeDefaultDescription
maskMASKMatte to dither — breaks 8-bit banding when stretched.
amountINT60–127Noise strength — enough to hide banding, not enough to read as texture.
seedINT00–2147483647Fixed seed holds the grain still through a sequence; upstream reseeded every frame and the matte crawled.
grain_sizeINT41–64Noise frequency — larger values give coarser, less speckly grain.
invertBOOLEANfalseFlip mask polarity before adding grain.

Outputs (2)

NameTypeDescription
maskMASK
reportSTRING