cv2.medianBlur
The salt-and-pepper killer, and the blur that doesn't smear your edges
- src
- result
There are three blur families and people treat them as interchangeable, which is how you lose the detail you were trying to keep. Gaussian smears everything, edges included - great for softening, useless when edges are the point. Bilateral smooths flat areas while protecting boundaries. Median is the third one, and it's the specialist: it takes the middle value of each neighbourhood instead of an average, so a single crazy pixel surrounded by sane ones is simply outvoted and vanishes.
That makes it the right tool for impulse noise - salt-and-pepper, sensor hot pixels, a stray white dot from a bad JPEG, dust on a scan, a holey mask full of isolated specks. It's also the standard pre-filter before a Laplacian or any other second-derivative edge detector, because those amplify exactly the noise a median kills while leaving edges intact.
How it works
Each output pixel is the median of the ksize × ksize neighbourhood. Averaging blends an outlier into its neighbours; the median discards it. The payoff is that hard edges stay hard - a median filter barely touches a step edge, which is exactly why it's described as edge-preserving without any of bilateral's tuning.
ksize is the one parameter and it's required: odd, greater than 1 - 3, 5, 7. Larger windows kill larger specks and cost time quadratically, and there's a depth limit worth knowing about, straight from OpenCV's own docs: for ksize 3 or 5 the image can be 8-bit, 16-bit or float; above 5, only 8-bit.
The sockets
src echoes the format - a MASK in gives you a MASK back (very handy, since median-filtering a speckled mask is a genuine cleanup step), and an IMAGE comes back as an IMAGE. IMAGE inputs arrive as uint8 BGR 0–255, and this function is on the pack's per-frame list, so a whole IMAGE batch is filtered frame by frame rather than only frame 0.
One output, echoing src's format. No optional widgets, no kernel size that defaults to something sensible - the pack's default is 0, which cv2 rejects, so this is a node where you must set the widget before it will run at all.
Installing the pack
ComfyUI CV (bmad4ever/comfyui_cv) - search "comfyui_cv" in ComfyUI Manager, 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 V3-API ComfyUI build; the single dependency is that pinned contrib OpenCV wheel, and there's nothing to download. Being a core OpenCV function on a single image, this is a CPU operation that finishes in milliseconds - unlike the pack's DNN nodes, which want external ONNX models and are a different story entirely.
Where people get burned
Leaving ksize at 0. That's the widget default and it isn't a valid aperture; cv2 errors immediately. Also: even numbers are rejected. Odd numbers only, and the pack won't round for you.
Using it on Gaussian noise. Median is a minority-vote filter - it removes outliers, not continuous grain. For a noisy ISO-3200 photo you want bilateral (or a Gaussian if you don't care about edges), and a median filter will just leave the noise and eat your texture.
A 9×9 median on 16-bit data. Above 5, the aperture only supports 8-bit. If your pipeline is float32, convert back with CV Cast Array or keep the radius small.
Expecting it to fix a fuzzy edge. It won't; it's designed not to touch edges. If an edge is mushy, that's a different job.
Contrib nodes gone from the menu. The recurring pack-wide failure: all four opencv-* wheels share one site-packages/cv2, so a non-contrib wheel installed over the contrib one empties the contrib submodules and those nodes stop registering silently. python tools/repair_opencv_contrib.py --check diagnoses; --apply repairs. And since the pack's author openly says the codebase was written with heavy LLM assistance and warns against production use without review, it's worth repeating that these low-level wrappers are otherwise exactly what they say on the tin - thin calls into OpenCV.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | input 1-, 3-, or 4-channel image; when ksize is 3 or 5, the image depth should be CV_8U, CV_16U, or CV_32F, for larger aperture sizes, it can only be CV_8U. 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. | |
| ksize | INT | 0-2147483648–2147483647 | aperture linear size; it must be odd and greater than 1, for example: 3, 5, 7 ... |
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. |