cv2.ximgproc.jointBilateralFilter
Denoise one image using another image's edges
- joint
- src
- result
A bilateral filter smooths flat areas and keeps edges - but which edges? Its own. That's a problem the moment you have a cleaner view of the same scene than the image you're cleaning up. That's the joint (or "cross") bilateral filter: the smoothing radii and colour tolerances are evaluated on a second image, the joint, and the result is applied to your noisy source. The canonical example is a flash/no-flash pair - the flash frame is sharp but flat and harsh; the no-flash frame has the ambient light you want but is full of sensor noise. Filter the no-flash frame through the flash frame's edges and you get the flash's structure with the ambient's look. The pack's demo workflow points at exactly that example (and admits it ships no such photo pair, because licensing).
It's useful well beyond that trick: any "I have two versions of the same scene and one has better structure" problem.
How it works
For each output pixel, the filter takes a weighted average of src neighbourhood values, where the weights come from joint - the guide supplies both the spatial reach (whose edges to respect) and the range weighting (which pixels are "similar"). Size and depth must match between joint and src; if one is uint8 and the other float, expect an OpenCV overload error and a cv2.ximgproc.jointBilateralFilter failed: … message naming both inputs.
The parameters are the same family as plain bilateralFilter, and the tooltips spell them out:
- d - the pixel-neighbourhood diameter. Leave it ≤ 0 and it's derived from
sigmaSpace, which is usually the cleaner way to think about it. - sigmaColor - how far apart two colours can be and still get mixed. Bigger means larger areas of semi-equal colour: more smoothing, more risk of bleeding across a weak edge.
- sigmaSpace - how far the filter reaches in pixels.
- borderType (
BORDER_DEFAULT) - how pixels outside the frame are synthesised. Leave it unless you're matching another pass's border behaviour.
Output is one result, which echoes the joint input's format. That's a nice detail in practice: wire an IMAGE as the joint image and the result is an IMAGE you can preview. It also means if you want a MASK or NPARRAY back, the joint image is what decides.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Restart ComfyUI, or install ComfyUI CV from ComfyUI Manager. Python ≥3.12, a recent ComfyUI on the V3 node API, and opencv-contrib-python-headless~=5.0.0.93. No downloads. Contrib-only function: a non-contrib OpenCV wheel sharing that site-packages/cv2 will silently remove the node from your menu, and tools/repair_opencv_contrib.py --check is the diagnostic.
Common issues
The one-second stall. This node is in the pack's always-offload set, so the cv2 call runs in an interruptible subprocess - that's why cancelling a slow run actually works, and why there's a visible pause before output. It's not frozen.
Colour halo or ghosting. sigmaColor too large for the guide's contrast; the filter starts averaging across genuinely different regions. Drop it before you drop sigmaSpace, since it's the range term that decides what counts as an edge.
Wrong-type errors. src and joint must agree on depth. Bridging one through Image → CV Array and the other straight from a MASK is a classic way to hand cv2 two different depths; run both through CV Cast Array if you need to be explicit.
Use it as a refiner, not as a hole-filler. It has no concept of invalid pixels. Feeding it a disparity map full of −1 "no match" markers just averages those markers into their neighbours - the same caveat as the other edge-aware smoothers in this module.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| joint | COMFY_MATCHTYPE_V3 | Joint 8-bit or floating-point, 1-channel or 3-channel 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. | |
| src | NPARRAY,IMAGE,MASK | Source 8-bit or floating-point, 1-channel or 3-channel image with the same depth as joint image. 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. | |
| d | INT | 0-2147483648–2147483647 | Diameter of each pixel neighborhood that is used during filtering. If it is non-positive, it is computed from sigmaSpace . |
| sigmaColor | FLOAT | 0.0000-1e+38–1e+38 | Filter sigma in the color space. A larger value of the parameter means that farther colors within the pixel neighborhood (see sigmaSpace ) will be mixed together, resulting in larger areas of semi-equal color. |
| sigmaSpace | FLOAT | 0.0000-1e+38–1e+38 | Filter sigma in the coordinate space. A larger value of the parameter means that farther pixels will influence each other as long as their colors are close enough (see sigmaColor ). When d>0 , it specifies the neighborhood size regardless of sigmaSpace . Otherwise, d is proportional to sigmaSpace . |
| borderTypeopt | COMBO | BORDER_DEFAULT | - - - |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| result | COMFY_MATCHTYPE_V3 | Echoes the 'joint' input's format: an IMAGE link comes back as IMAGE, MASK as MASK, NPARRAY stays NPARRAY. |