Nodes/ComfyUI CV/cv2.filter2D
ComfyUI Node

cv2.filter2D

Write the kernel nobody has a node for

By bmad4ever·Created 4 months ago·Updated 15 days ago· 1
cv2.filter2D
  • src
  • kernel
  • anchor
  • result
◄ddepthsame as input►
◄delta0.0000►
◄borderTypeBORDER_DEFAULT►

Blur, sharpen, emboss, edge-detect, ridge-detect: they're all the same operation with a different 3×3 (or 5×5) matrix, and cv2.filter2D is the node where you supply that matrix yourself. It's the escape hatch for the filter that no pack shipped. The friction is entirely in the kernel - and in what ddepth does to a result with negative numbers in it.

The mechanism

For every pixel, multiply the surrounding neighbourhood by the kernel, sum, and store. OpenCV labels it honestly: it's correlation, not true convolution (the kernel isn't flipped), which only matters if you're building an asymmetric kernel and care about the orientation of the result. One kernel is applied to every channel identically - the tooltip says so and offers the workaround (split channels, filter separately) if you need per-channel behaviour, which is where cv2.extractChannel and CV Concat Arrays come in.

What makes it useful is that you're not limited to hand-written kernels either. Any array works, so a filter you computed - a PSF, a weighting map derived from a distance field, a kernel from another node - is a legal input. That's a build-your-own operation.

The inputs

  • src - IMAGE, MASK, NPARRAY, and even a LATENT: the pack treats this op as channel-agnostic and value-range-agnostic, so a latent goes through in float, untouched values, no 8-bit round trip. Convolving a latent directly is a legitimate experiment (smoothing the latent is one way to soften structure), and it's worth knowing that the wrapper allows it on purpose.
  • ddepth - default "same as input", with the obvious alternatives (8U/16S/32F/64F). This is the parameter that bites. A sharpening kernel has negative coefficients, so on an 8-bit destination the negative lobes clip to 0 and bright halos clip to 255 - you get a crunchy, broken-looking result that is technically the arithmetic working as specified. If you want negatives carried, pick a signed or float depth and convert back after (cv2.convertScaleAbs exists for exactly that).
  • kernel - NPARRAY only, floating point, single channel. Build it with Parse Matrix (the easiest way to type a kernel as a small text block), or CV Numbers To Array plus CV Reshape Array if you're computing it. anchor (-1, -1 = centre) matters for asymmetric kernels - an emboss or a directional derivative is wrong if the anchor isn't where you think. delta adds a constant to every result pixel after filtering, which is the standard way to recentre a kernel whose output would otherwise sit in the negatives. borderType controls edge extrapolation (BORDER_WRAP isn't supported).

Output result echoes the input's format - IMAGE in, IMAGE out - so this doesn't force you into raw-array land.

Kernels worth keeping in a text file

  • Sharpen: 0 -1 0 / -1 5 -1 / 0 -1 0. Sums to 1, so brightness is preserved - but see ddepth.
  • Box blur: a 3×3 of 1/9s. Works, and if that's all you want, the pack has cv2.blur, which does it without the homework.
  • Unsharp-ish: kernel = identity minus a scaled blur kernel, i.e. edge enhancement with a tunable amount. This is the honest answer to "how does sharpen work" - see post-processing.md, which makes the point that unsharp masking is blur-and-difference, not magic.
  • Emboss / directional: asymmetric kernels with a zero sum. Expect to want delta around 128 to bring it back into range.

If your kernel doesn't sum to 1, you're also changing exposure - either normalize the kernel, or use delta deliberately and know that you did.

Installing it

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

Or install comfyui_cv through ComfyUI Manager and restart. Python ≥ 3.12 and a recent V3-API ComfyUI. Node path: image/CV/low-level/cv2 F.

Traps

Signed results on an unsigned destination is the big one, and it doesn't throw - it just clips. Non-float kernels are the second: OpenCV wants a floating-point matrix, so an integer array from a literal is a coin flip. A one-element or even-sized kernel is invalid. And if your live in-graph filter looks nothing like the matrix you typed, check that the Parse Matrix output has the shape you expect with CV Inspect CV Data - a transposed kernel gives you a transposed filter, which is subtle on a symmetric one and glaring on an emboss.

Categoryimage/CV/low-level/cv2 F

Inputs (6)

NameTypeDefaultDescription
srcCOMFY_MATCHTYPE_V3input image. The image output(s) echo this input's format. A LATENT link is processed in latent space: frame 0 becomes a float32 [H,W,C] array (any channel count), values untouched. Arithmetic ops (add, multiply, etc.) also accept a full LATENT batch ({samples: [B,C,H,W]}) — the whole batch flows through when both inputs have the same batch size. 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.
ddepthCOMBOsame as inputdesired depth of the destination image, see "combinations"
kernelNPARRAYconvolution kernel (or rather a correlation kernel), a single-channel floating point matrix; if you want to apply different kernels to different channels, split the image into separate color planes using split and process them individually. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
anchoroptCV_TUPLE-1,-1anchor of the kernel that indicates the relative position of a filtered point within the kernel; the anchor should lie within the kernel; default value (-1,-1) means that the anchor is at the kernel center. One value with 2 components (x, y) - it travels as a whole, so it cannot arrive half-connected. Wire it from 'CV Tuple' or type the components in place.
deltaoptFLOAT0.0000-1e+38–1e+38optional value added to the filtered pixels before storing them in dst. Preset to the OpenCV default (0.0).
borderTypeoptCOMBOBORDER_DEFAULTpixel extrapolation method, see #BorderTypes. #BORDER_WRAP is not supported.

Outputs (1)

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