cv2.morphologyEx
Open and close are the mask hygiene you keep forgetting
- src
- kernel
- anchor
- result
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 acceptsNPARRAY. Build the element withcv2_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_ERODEwith 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.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| src | COMFY_MATCHTYPE_V3 | Source 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. | |
| op | COMBO | MORPH_OPEN | Type of a morphological operation, see #MorphTypes |
| kernel | NPARRAY | Structuring element. It 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 | Anchor 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. |
| iterationsopt | INT | 1-2147483648–2147483647 | Number of times erosion and dilation are 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. 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)
| 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. |