cv2.erode
Shrink a mask the right way (the kernel is the whole story)
- src
- kernel
- anchor
- result
Erosion is the mask-editing primitive that people keep reinventing badly. If your seg mask has speckles, if two blobs are fused where they shouldn't be, if a detector's box-shaped mask needs trimming before you paste a re-rendered region back - this is the node. What it is not is a GrowMask substitute, and half of the friction with it is that the kernel socket is not a widget.
What erosion does
For every pixel, take the minimum over the neighbourhood defined by the structuring element. A white pixel survives only if all of its neighbours inside that element are white. So white regions shrink, thin structures disappear, and isolated bright specks get eaten - which is exactly what you want when you're cleaning a mask rather than a picture.
Two details bite. First, the element's shape matters as much as its size: a rectangular element erodes corners differently from an ellipse or a cross, and for masks, a cross or ellipse is usually what you meant. Second, iterations is not the same as one big kernel - repeated 3×3 cross erosions approximate a diamond, not a square, and the tooltip is explicit that the default is OpenCV's 1. Since this pack's wrapper has borderType (with BORDER_WRAP unsupported) and a borderValue literal, edges are yours to control too.
The inputs that matter
src accepts an IMAGE, a MASK or an NPARRAY - and, unusually, a LATENT. The pack treats erosion as channel-agnostic and value-range-agnostic, so a LATENT gets processed in latent space as a float32 array, frame 0, values untouched, no 8-bit round trip. That's a legitimate trick if you're experimenting with latent-space masks; it is also the kind of thing you should try once on purpose rather than discover by accident.
kernel is the one that stops people. It is NPARRAY only - you cannot wire a MASK or an IMAGE into it, and you cannot leave it empty, even though OpenCV itself would happily default to a 3×3 rectangle. Build it from cv2.getStructuringElement (another auto-generated wrapper in the same pack, category image/CV/low-level/cv2 G, with a shape dropdown and ksize_x/ksize_y widgets), or hand-write a small matrix with Parse Matrix for something custom. Then anchor (-1, -1 = centre, a CV_TUPLE you can wire from CV Tuple), iterations, borderType and borderValue are the optional knobs - the last one takes a literal like "255" or "(0, 0, 0)".
The single output, result, echoes the input's format: IMAGE in, IMAGE out; MASK in, MASK out. Masks are the money case here, and they stay masks, so cv2.erode drops straight into a masking chain without a bridge node on either side.
Where it fits
Masks, mostly. Erode to kill specks and separate touching blobs before a connected-components split; erode to trim an over-generous inpaint mask so the re-render doesn't eat the boundary you wanted to keep; erode a copy as the "core" of a mask you're about to distance-transform. In detailing pipelines the pressure goes the other way - masks generally need growing for seam-free pasting, and inpainting.md is blunt that generous overlap plus mask blur is what stops the "pasted on" look. If your goal is to grow, this is the wrong direction; cv2.dilate is the same node shape with the max instead of the min.
Also worth saying: for "grow or shrink this mask by N pixels", core ComfyUI's GrowMask and FeatherMask are simpler and instant. Reach for cv2.erode when you want a shaped element, multiple iterations, an explicit border mode, a raw array, or erosion on a latent.
Installing it
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
ComfyUI Manager: search comfyui_cv, install, restart. Python ≥ 3.12, recent V3-API ComfyUI. The node lives under image/CV/low-level/cv2 E - there are ~470 of these wrappers, so the category subfolder is the fastest way to find it.
Traps
The kernel link refusing to connect is not a bug; it's the NPARRAY socket doing its job. A Mat() empty kernel means "3×3 rectangle" in C++ and has no equivalent here, so build the element explicitly. Odd, non-zero sizes keep you out of trouble. And if erosion seems to do nothing, check what you actually fed it: erosion of a soft 0–1 float mask will shave sub-threshold values you may not see, so preview the mask after it rather than assuming the graph is broken.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | input image; the number of channels can be arbitrary, but the depth should be one of CV_8U, CV_16U, CV_16S, CV_32F or CV_64F. 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. | |
| kernel | NPARRAY | structuring element used for erosion; if `kernel=Mat()`, a `3 x 3` rectangular structuring element is used. Kernel can be created using #getStructuringElement. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| anchoropt | CV_TUPLE | -1,-1 | position of the anchor within the element; default value (-1, -1) means that the anchor is at the element center. One value with 2 components (x, y) - it travels as a whole, so it cannot arrive half-connected. Wire it from 'CV Tuple' or type the components in place. |
| iterationsopt | INT | 1-2147483648–2147483647 | number of times erosion is applied. Preset to the OpenCV default (1). |
| borderTypeopt | COMBO | BORDER_DEFAULT | pixel extrapolation method, see #BorderTypes. #BORDER_WRAP is not supported. |
| borderValueopt | STRING | border value in case of a constant border cv2 Scalar as a literal, e.g. "(0, 255, 0)" (BGR) or "(0, 255, 0, 64)" (BGRA). A bare number broadcasts to every component, so "255" means (255, 255, 255, 255). Components past the target's channel count are ignored by OpenCV. Leave blank for the OpenCV default. |
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. |