cv2.filter2D
Write the kernel nobody has a node for
- src
- kernel
- anchor
- result
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.convertScaleAbsexists for exactly that).kernel- NPARRAY only, floating point, single channel. Build it withParse Matrix(the easiest way to type a kernel as a small text block), orCV Numbers To ArrayplusCV Reshape Arrayif 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.deltaadds 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.borderTypecontrols edge extrapolation (BORDER_WRAPisn'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 seeddepth. - Box blur: a 3×3 of
1/9s. Works, and if that's all you want, the pack hascv2.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
deltaaround 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.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | input 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. | |
| ddepth | COMBO | same as input | desired depth of the destination image, see "combinations" |
| kernel | NPARRAY | convolution 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. | |
| anchoropt | CV_TUPLE | -1,-1 | anchor 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. |
| deltaopt | FLOAT | 0.0000-1e+38–1e+38 | optional value added to the filtered pixels before storing them in dst. Preset to the OpenCV default (0.0). |
| borderTypeopt | COMBO | BORDER_DEFAULT | pixel extrapolation method, see #BorderTypes. #BORDER_WRAP is not supported. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'src' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |