Nodes/ComfyUI CV/cv2.morphologyEx
ComfyUI Node

cv2.morphologyEx

Open and close are the mask hygiene you keep forgetting

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

Every mask you get out of a detector, a segmentation model, a background remover or a threshold is dirty in the same three ways: speckles of noise that shouldn't be there, pinholes that should be filled, and thin bridges connecting things that shouldn't touch. cv2.morphologyEx is the deterministic, millisecond fix for all three - no model, no second sampling pass. When the KB says automatic detailing is "zooming in on a mask, upscaling, and diffusing", this node belongs in the step before: cleaning the region so the re-render happens in the right place.

The operations, in the order you'll care about them

The op dropdown is OpenCV's MorphTypes. The short version:

  • MORPH_OPEN - erode then dilate. Kills isolated speckle while leaving surviving shapes roughly their original size. This is the "remove the stray dots" tool.
  • MORPH_CLOSE - dilate then erode. Fills pinholes and small gaps, keeps the outer boundary roughly where it was. This is the "heal the mask" tool.
  • MORPH_ERODE / MORPH_DILATE - the bare primitives: shrink or grow. Open is these two in sequence.
  • MORPH_GRADIENT - dilate minus erode. A clean outline of a mask, without Canny's noise sensitivity.
  • MORPH_TOPHAT - input minus its opening. Surfaces bright structures smaller than the kernel; the classic uneven-illumination flattener.
  • MORPH_BLACKHAT - closing minus input, the dark-structure twin.
  • MORPH_HITMISS - the odd one out. It's template matching for binary shapes, expecting a single-channel 8-bit input and a kernel of 1/0/−1 entries. Ignore it until you specifically need it.

iterations is the how-many-times knob, preset to OpenCV's 1. Two passes of a 3×3 ellipse is roughly one 5×5 but rounder; for organic masks (faces, hair, painted regions) an ellipse kernel respects the shape better than a square.

The input that trips people

kernel is typed NPARRAY and nothing else - the author's own note says it plainly: a data array, not an image. So you don't crop a shape out of a picture; you build a structuring element. The pack's cv2_getStructuringElement does it (shape: MORPH_ELLIPSE / MORPH_RECT / MORPH_CROSS, ksize a (w, h) CV_TUPLE), and the bundled cv2.morphology (simple) subgraph is exactly that pair wired into cv2_morphologyEx.

The rest of the inputs are set-and-forget: anchor defaults to (-1, -1), meaning the kernel center - only move it for deliberately asymmetric effects; borderType is a dropdown (BORDER_WRAP isn't supported); borderValue is a literal like (0, 255, 0) or a bare number that broadcasts across components.

result echoes src's format - IMAGE in comes back IMAGE, MASK comes back MASK, NPARRAY stays NPARRAY. That's deliberate: you can drop this node into a mask chain with no conversion bridges on either side.

One curiosity: morphologyEx is also on the pack's latent-safe list, so a LATENT link is processed in latent space (frame 0, float32, values untouched, no uint8 quantization). It runs. Whether min/max filtering of a latent is useful is a research question you'd be answering yourself.

Install

ComfyUI Manager → search ComfyUI CV → install → restart. Or:

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"

Python ≥ 3.12, a recent ComfyUI on the V3 node API. Core cv2, no contrib submodule, no model files.

When it doesn't do anything

  • The kernel won't connect. If you're trying to link an IMAGE into kernel, the socket only accepts NPARRAY. Build the element with cv2_getStructuringElement.
  • Holes survive CLOSE. They're bigger than the kernel. Size it against the feature, not against the image - rule of thumb, roughly the width of the gap you want closed, or ~1% of the mask's width if you'd rather guess.
  • Everything vanished. MORPH_ERODE with a large kernel eats a thin mask completely, and erosion of a thresholded edge map is usually the culprit. Open/close first, see what you've got, and only then push sizes.
Categoryimage/CV/low-level/cv2 M

Inputs (7)

NameTypeDefaultDescription
srcCOMFY_MATCHTYPE_V3Source image. The number of channels can be arbitrary. 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.
opCOMBOMORPH_OPENType of a morphological operation, see #MorphTypes
kernelNPARRAYStructuring element. It can be created using #getStructuringElement. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here.
anchoroptCV_TUPLE-1,-1Anchor position with the kernel. Negative values mean that the anchor is at the kernel 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 and dilation are 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. The default value has a special meaning. 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.