cv2.ximgproc.bilateralTextureFilter
Smooth the texture, keep the shape
- src
- result
Every edge-preserving filter faces the same ambiguity: a bumpy surface has bumps and an outline, and a bilateral filter can't tell them apart - both are gradients. Telling the two apart isn't a question of magnitude, it's a question of scale. A bilateral texture filter is built specifically for that distinction: it estimates how texture-like a local patch is and blurs it, while leaving the strong shape boundaries alone.
Concretely, it's the filter to reach for when you want skin without pores, fabric without weave, or a wall without grain, and you do not want the silhouette or the creases to move. A plain bilateral at the settings needed to kill texture will also round off every edge; this one is meant to not do that.
Inputs
src- the source image. The tooltip is explicit about what it accepts: "depth is 8-bit UINT or 32-bit FLOAT". Colour or grayscale.IMAGE,MASKorNPARRAY.fr(default 3) - "radius of kernel to be used for filtering. It should be positive integer". Larger radius = larger neighbourhood = slower and stronger texture averaging.numIter(default 1) - iteration count. The tooltip says positive integer; 1–3 is the useful range. Like every iterative smoother, more iterations creep further across boundaries.sigmaAlpha(default −1) - controls how sharp the weight transition between edge and texture regions is; bigger means a sharper transition. Negative means "automatically calculated", which is the default and usually a good idea.sigmaAvg(default −1) - the range-blur parameter for the texture blurring; bigger is more blurred. Also auto when negative.
One output, echoing the input's format. All four optional parameters arrive pre-filled with OpenCV's defaults, so you can leave everything but src alone and still get a sensible result - which, for a first experiment, is exactly what you should do.
How to tune it without guessing
Change one thing at a time, and have the pack tell you what happened. CV Quality Compare scores two images with SSIM and returns a per-pixel quality map, so you can see where the filter acted - if the map lights up along the silhouette, you've gone too far; if it lights up only in the flat areas, you've found your setting.
Two things worth internalising about the auto-sigmas: they're derived from local statistics, so a picture with wildly varying texture density will get wildly varying treatment - which is the feature, right up until it isn't. And both sigmas are in the units of the data, so if you hand it a float NPARRAY in 0–1 range, the numbers you'd want are nothing like the ones for 8-bit 0–255 data. A ComfyUI IMAGE normally reaches cv2 as 8-bit, but if you've converted explicitly with Image → CV Array, the scale is whatever you chose there. Check with Inspect CV Data if the result seems inert or nuclear.
Where it belongs in a workflow
Texture filtering is prep work, not the destination. Three places it earns its keep:
- Before an upscale. Amplifying noise is what makes upscales look plastic; remove the micro-texture first and the upscaler has less garbage to scale.
- Clean plates and composites. Smoothing the plate before a composite hides the seam, and smoothing the texture rather than the edges is exactly what a clean plate wants.
- Structure/detail separation. The pack's example filter sheet does the standard trick - subtract the smoothed version from the original (
cv2_subtract) to isolate the detail layer, then recombine withcv2_addWeighted(it uses amount 1, offset −1, bias 128 in the sheet). Once they're separate, you can sharpen the detail layer, boost it, or drop it entirely.
Installing it
Contrib module, part of ComfyUI CV. 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 ComfyUI
Python ≥3.12 and a recent, V3-API ComfyUI.
What goes wrong
- "Whatever I set, nothing changes" with both sigmas negative: the auto-calculation has decided your image is all texture or all structure. Set
sigmaAvgexplicitly and watch what happens. - Edges moved. Too many iterations.
numIter1, then 2, then stop. - Wrong dtype. Give it 8U or 32F; an int16 array from some upstream op is asking for trouble.
- It's slow on large frames - a radius-3 kernel over a 4K image, times iterations. Preview first.
- Missing node. That's the contrib wheel problem:
ximgprocimports as an empty stub if a non-contrib OpenCV overwrote the contrib binary.tools/repair_opencv_contrib.py --checkin the pack repo diagnoses it.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | Source image whose depth is 8-bit UINT or 32-bit FLOAT 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. | |
| fropt | INT | 3-2147483648–2147483647 | Radius of kernel to be used for filtering. It should be positive integer Preset to the OpenCV default (3). |
| numIteropt | INT | 1-2147483648–2147483647 | Number of iterations of algorithm, It should be positive integer Preset to the OpenCV default (1). |
| sigmaAlphaopt | FLOAT | -1.0000-1e+38–1e+38 | Controls the sharpness of the weight transition from edges to smooth/texture regions, where a bigger value means sharper transition. When the value is negative, it is automatically calculated. Preset to the OpenCV default (-1.0). |
| sigmaAvgopt | FLOAT | -1.0000-1e+38–1e+38 | Range blur parameter for texture blurring. Larger value makes result to be more blurred. When the value is negative, it is automatically calculated as described in the paper. Preset to the OpenCV default (-1.0). |
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. |