Nodes/ComfyUI CV/cv2.randn
ComfyUI Node

cv2.randn

Gaussian noise you can actually reproduce — if you seed it right

By bmad4ever·Created 4 months ago·Updated 14 days ago· 1
cv2.randn
  • dst
  • mean
  • stddev
  • result

Adding noise is one of the few post-processing tricks with a measurable payoff: real cameras are noisy, diffusion output isn't, and a light grain pass is the cheapest way to make a frame read as captured rather than generated. photorealism.md covers the aesthetic argument and post-processing.md covers the recipe - the mechanical half is this node. It fills an array with Gaussian random values; you decide the mean, the standard deviation, and (through the shape of the array you hand it) the size and data type.

The one part that isn't obvious

cv2.randn writes into a pre-allocated array, and it draws from cv2's single global random number generator. Two consequences:

  • dst defines the output. The node's first input is dst - and despite the name, the numbers that come back are not derived from its pixels. What matters is its shape, channel count and dtype: an [H, W, 3] uint8 image in gives you an [H, W, 3] uint8 field of random numbers out. The wrapper hands cv2 a private copy, so your input image is never modified - you're using it as a canvas spec, not as a destination.
  • The seed is global, and that's a trap. Because the RNG is shared, the same graph can produce different noise on consecutive runs depending on what else in the graph drew from it, and execution order follows the links, not the node's position on the canvas. The pack ships CV Random Seed for exactly this: it seeds the global RNG and passes a wildcard value through, so you put it in the data path immediately before the randn node and the same seed gives the same noise every run. A seed widget on its own won't do it - nothing forces it to run first.

Inputs and outputs

mean and stddev are NPARRAY sockets, not numbers - they're vectors, one entry per channel. Type them with the pack's CV Scalar node: (128, 128, 128) or a bare 128 (which broadcasts to all channels), and 25 for a mild grain. That's the recipe the pack's own CV Add Gaussian noise subgraph uses on a uint8 pipeline: fill a same-shape canvas at mean 128, sigma 25, then add it and subtract 128 so the noise can go both ways without clipping the shadows.

The output result echoes the dst input's format - IMAGE in, IMAGE out, so a preview node will show you the noise field directly. This node is also in the pack's per-frame batch set, so a batch is filled frame by frame.

Data type matters. On a uint8 canvas, values are integers in 0–255 and anything that would exceed those limits saturates; the mean-128 trick exists to keep noise centred in the middle of the range. On a float32 NPARRAY (0–1 range, via Image → CV Array with the float dtype), mean 0.5 and stddev 0.02 gives you a much finer grain without quantisation. Pick one convention and stay in it.

Downstream, the usual pattern is an additive pass with cv2.add / cv2.addWeighted (or the pack's noise subgraph, which does the offset dance for you), then straight to your save node.

Install

From ComfyUI CV by bmad4ever. ComfyUI Manager, search comfyui_cv, or:

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"

Restart. Python ≥ 3.12, recent V3-API ComfyUI. No models.

Common issues

"I can't type a number into mean/stddev." They're array sockets by design. Use CV Scalar - it's the pack's answer to every vector-valued cv2 parameter.

"Different noise every run." The global RNG. Put CV Random Seed in the wire feeding this node.

"The noise is grayscale-independent per channel / looks weird." Both are true of how the fill works. For chroma noise you want different per-channel stddev values; for luminance-style grain, identical values across channels (a bare scalar broadcast) is what you want.

"My shadows crushed to pure black." You added noise directly to a dark image without offsetting. Use the mean-128 canvas pattern (or float 0.5) and combine with an add/subtract, so noise is symmetric around the midpoint.

Sanity check when nothing appears to happen. Look at the noise field itself in a preview before you blend it - if the preview is flat, your stddev is zero.

Categoryimage/CV/low-level/cv2 R

Inputs (3)

NameTypeDefaultDescription
dstCOMFY_MATCHTYPE_V3output array of random numbers; the array must be pre-allocated and have 1 to 4 channels. The image output(s) echo this input's format. The low-level cv2 function writes its result into this array in place, but this wrapper passes cv2 a private copy, so your input array is never modified. Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size.
meanNPARRAYmean value (expectation) of the generated random numbers. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
stddevNPARRAYstandard deviation of the generated random numbers; it can be either a vector (in which case a diagonal standard deviation matrix is assumed) or a square matrix. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.

Outputs (1)

NameTypeDescription
resultCOMFY_MATCHTYPE_V3Echoes the 'dst' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY.