Nodes/ComfyUI CV/cv2.erode
ComfyUI Node

cv2.erode

Shrink a mask the right way (the kernel is the whole story)

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
cv2.erode
  • src
  • kernel
  • anchor
  • result
◄iterations1►
◄borderTypeBORDER_DEFAULT►
◄borderValue►

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.

Categoryimage/CV/low-level/cv2 E

Inputs (6)

NameTypeDefaultDescription
srcCOMFY_MATCHTYPE_V3input 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.
kernelNPARRAYstructuring 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.
anchoroptCV_TUPLE-1,-1position 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.
iterationsoptINT1-2147483648–2147483647number of times erosion is applied. Preset to the OpenCV default (1).
borderTypeoptCOMBOBORDER_DEFAULTpixel extrapolation method, see #BorderTypes. #BORDER_WRAP is not supported.
borderValueoptSTRINGborder 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)

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