Nodes/ComfyUI CV/cv2.ft.filter
ComfyUI Node

cv2.ft.filter

A denoiser that isn't a blur, and needs no model

By bmad4ever·Created 4 months ago·Updated 14 days ago· 1
cv2.ft.filter
  • 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, so result plugs 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 (or createKernel1). Its radius sets the smoothing scale; the kernel's channel count must match the image's, since createKernel builds 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.

Categoryimage/CV/low-level/ft

Inputs (2)

NameTypeDefaultDescription
imageCOMFY_MATCHTYPE_V3Input 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.
kernelNPARRAY,IMAGE,MASKFinal 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)

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