cv2.sepFilter2D
Blur and Sharpen for O(k) Instead of O(k²)
- src
- kernelX
- kernelY
- anchor
- result
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 compositeCV_TUPLEinput.deltaandborderType(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
kernelXandkernelY. 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.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | Source 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 | Destination image depth, see "combinations" |
| kernelX | NPARRAY | Coefficients for filtering each row. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| kernelY | NPARRAY | Coefficients for filtering each column. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| anchoropt | CV_TUPLE | -1,-1 | Anchor 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. |
| deltaopt | FLOAT | 0.0000-1e+38–1e+38 | Value added to the filtered results before storing them. 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. |