Nodes/ComfyUI CV/cv2.sepFilter2D
ComfyUI Node

cv2.sepFilter2D

Blur and Sharpen for O(k) Instead of O(k²)

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

Any 2-D convolution kernel that can be written as the outer product of two 1-D kernels is separable, and filtering with it in two passes costs O(k) per pixel instead of O(k²). That's the entire idea behind cv2.sepFilter2D. Gaussians are separable, box blurs are separable, derivative operators are separable, the classic sharpen kernels are separable - and this node is how you exploit that and how you control each axis independently.

You'd reach for it over cv2.filter2D in two situations. One: the kernel is large enough that k² hurts - a wide Gaussian via a 31×31 dense kernel is ~960 multiply-adds per pixel, via two 31-tap passes it's 62. Two: you want anisotropy - different behaviour horizontally and vertically, which a single 2-D kernel expresses awkwardly and two 1-D kernels express directly. The pack's own CV Sobel 2D subgraph and the Gaussian blur nodes are the same trick under the hood.

Inputs

  • src - the primary input; IMAGE, MASK or NPARRAY. It's type-preserving, so an IMAGE link comes back as an IMAGE, and - unusually - this one also accepts a LATENT link, processed in latent space as a float32 [H, W, C] array with the values left alone. Filtering latents directly is a real use: a cheap smooth or sharpen on the latent grid with no decode/encode round trip through pixels.
  • ddepth - the output depth; same as input is the default and is right for most filters, but switch to CV_32F if the result can go negative (derivative kernels, unsharp masking with a negative lobe).
  • kernelX - the coefficients for filtering each row (the horizontal pass).
  • kernelY - the coefficients for filtering each column (the vertical pass).
  • anchor (optional) - where the kernel's origin sits inside the kernel; (-1, -1) means the centre, which is what you want. It's a composite CV_TUPLE input.
  • delta and borderType (optional) - a value added to every filtered result before storing, and the edge-extrapolation mode.

Both kernels are NPARRAY only - no IMAGE link accepted - and both are 1-D: a single row or a single column. The node's own nparray output echoes src's format, so the chain stays clean.

Where the kernels come from

You don't have to hand-type them. The pack ships the generators:

  • cv2.getGaussianKernel(ksize, sigma) → one Gaussian kernel that you feed to both kernelX and kernelY. That's a true separable Gaussian, which is what you want instead of the dense-kernel route.
  • cv2.getDerivKernels(dx, dy, ksize) → the row/column pair for a derivative, which is exactly the input this node expects after a transpose. Useful when you want a custom-order derivative with sepFilter2D's per-axis control.
  • cv2.getStructuringElement for morphology-shaped kernels, and Parse Matrix if you'd rather just type [1, 4, 6, 4, 1] as a literal.

If cv2 complains about a kernel's shape, the failing pair isn't oriented the way it expects - transpose the offending one with cv2.transpose (or CV Permute Axes) and it will go through. A 2-D matrix handed in as a kernel is the other common miss; that's filter2D's job, not this node's.

Install

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 afterwards. Needs Python ≥ 3.12 and a recent V3-API ComfyUI. No models, no downloads - this is spare, fast, CPU arithmetic.

Where people get burned

Kernel orientation and dimension. See above. Feeding an H×W matrix where a 1-D kernel belongs is the most common error and the message is not friendly.

Negative values clipped. With ddepth left at same as input on a uint8 source, an unsharp/derivative kernel's negative lobe truncates to zero and you get a lopsided filter that looks like a bug in the kernel. Use CV_32F when the coefficients can produce negatives.

This won't reproduce core's "Blur" node exactly. Core's blur is a Gaussian-ish blur; sepFilter2D with a getGaussianKernel pair is a stricter Gaussian, and the difference is visible on high-contrast edges. Which is fine - it's just worth knowing you changed the operator, not the amount.

Contrib wheel collisions. The four OpenCV distributions share one site-packages/cv2; installing a non-contrib wheel over a contrib one leaves contrib nodes missing from the menu with no error. tools/repair_opencv_contrib.py --check finds it.

Latent-space expectations. The LATENT path is element-wise on the latent array - it is not aware of VAE scaling or channel semantics. It's a filter on numbers, and it's on you to decide the numbers make sense.

Categoryimage/CV/low-level/cv2 S

Inputs (7)

NameTypeDefaultDescription
srcCOMFY_MATCHTYPE_V3Source 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 inputDestination image depth, see "combinations"
kernelXNPARRAYCoefficients for filtering each row. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
kernelYNPARRAYCoefficients for filtering each column. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
anchoroptCV_TUPLE-1,-1Anchor position within the kernel. The 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+38Value added to the filtered results before storing them. 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.