cv2.ft.filter
A denoiser that isn't a blur, and needs no model
- image
- kernel
- result
Here's the pitch for cv2.ft.filter, and it's the same pitch the whole deterministic post-processing layer gets: not every repair needs a diffusion pass. If you want to knock sensor noise off a photo, smooth a mask edge, or suppress texture before measuring something, a model is the expensive and unpredictable answer. This one is a filter. Deterministic, instant, and it flattens texture while keeping the large-scale structure - because it's not a blur at all.
It comes from the fuzzy transform (cv2.ft), one of the more obscure contrib modules in OpenCV, exposed here as a raw wrapper by ComfyUI CV (bmad4ever). That pack wraps OpenCV 5.0 as ComfyUI nodes - around 470 generated cv2.* wrappers plus curated high-level nodes, GPL-3.0, built on the V3 node API. The raw layer gives you OpenCV's parameters and nothing else, which is why the radius field starts at 0 and why that matters.
What it does, mechanically
The fuzzy transform expresses an image as coefficients over a coarse fuzzy partition - a grid of overlapping membership functions spaced by the radius - and then reconstructs the image from those coefficients. So the output isn't "the input smeared with a kernel"; it's the image rebuilt from a coarse description. Two things follow: detail below the partition scale is gone while structure above it survives, and the scale knob is the radius itself - doubling it changes what survives in an obvious, visible way, with no subtle sigma curve to tune.
That makes it a smoother-and-approximator rather than a detail keeper. If what you need is edge-preserving smoothing - skin, denoise-with-boundaries-intact - a bilateral or guided filter is the right family; the fuzzy transform is for deliberately coarsening the description.
Inputs and outputs
image- the picture, on a match-type socket: IMAGE in, IMAGE out. It echoes your format, soresultplugs straight into Preview Image. MASK and NPARRAY work too, and an NPARRAY stays an NPARRAY - handy mid-pipeline.kernel- the fuzzy kernel from cv2.ft.createKernel (orcreateKernel1). Its radius sets the smoothing scale; the kernel's channel count must match the image's, sincecreateKernelbuilds it for 3 channels for BGR or 1 for grayscale.result- the filtered image, same format as the input.
One detail the pack's own tooltip documents: cv2's ft.filter returns CV_32F internally no matter what you gave it. The wrapper casts the result back to your input's dtype, so a uint8 image comes back as a plain uint8 image rather than a float array you'd then have to normalise. That single cast is the difference between "usable in a ComfyUI graph" and "everything looks black".
Install
ComfyUI Manager → search ComfyUI CV, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install -r comfyui_cv/requirements.txt
Needs Python ≥3.12 and a current ComfyUI (V3 node API), and a restart afterwards. The dependency is opencv-contrib-python-headless~=5.0.0.93. Contrib matters here specifically: cv2.ft is not in a plain opencv-python wheel, so this node won't exist without it - and because all four OpenCV pip distributions share one site-packages/cv2, installing a non-contrib wheel over the contrib one empties the contrib submodules silently. When whole categories (fisheye, ft, aruco, ximgproc) are missing from your node search, that's the cause:
python ComfyUI/custom_nodes/comfyui_cv/tools/repair_opencv_contrib.py --check
Common issues
The output is identical to the input. Almost always a radius of 0 in the kernel - (2·0+1) is a 1×1 kernel, i.e. no smoothing. createKernel's radius widget ports cv2's default of 0 rather than a sensible value, so you have to set it.
Everything looks grey and mushy. Radius is far past the useful range for that image size. Remember it's a fraction of the frame: a radius that's gentle at 512 px is heavy at 4K, and vice versa.
A channel-count error. The kernel was built for a different channel count than the image it's filtering - common when you build the kernel once and reuse it on a grayscale branch.
Multi-frame batches only process once. ft.filter isn't on the pack's per-frame batch list, so a multi-frame IMAGE link goes through as frame 0. Interpolate over a frame index, or use the batch bridge nodes, if you're filtering a sequence.
Want inpainting instead of smoothing? The same module has cv2.ft.inpaint, and the pack's own tooltip records a real trap: this build's ONE_STEP algorithm returns NaN at every masked pixel, so the wrapper defaults to MULTI_STEP. Its mask convention is also inverted relative to cv2.inpaint - non-zero means known, so flip a hole mask before feeding it. workflows/18_inpainting_playground.json shows it in use.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| image | COMFY_MATCHTYPE_V3 | Input image. The image output(s) echo this input's format. 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. | |
| kernel | NPARRAY,IMAGE,MASK | Final 32-bit kernel. 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. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'image' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |