Mask To Convex Mask
Fill Every Concavity in One Node
- mask
- MASK
You know that moment - you've got a mask that's all jagged. Gaps between an arm and a torso, notches where your brush strokes didn't quite connect, a shoulder that looks like a ziggurat. You feed it to an inpaint pass and the model either invents content in the notches or bleeds color around them. This node is the one-sentence fix: it takes any mask and returns the tightest solid shape that still contains it - the convex hull. Think of a rubber band snapped around every white pixel. Everything inside the band goes white; everything outside stays black.
What it does
So it doesn't grow your mask outward. That's the part people get wrong. A convex hull fills the dents, not the area around the shape - if you want expansion, you want a grow/blur node. This is for turning a wobbly hand-painted or auto-segmented shape into a clean, predictable blob. That matters more than it sounds. Mask-based inpainting is still the only route that leaves unmasked pixels bit-identical, which is the whole reason anyone still bothers, and the quality of that pass lives or dies on how clean your mask boundary is before the VAE ever sees it. A convex mask feathers evenly, composites predictably, and doesn't hand the model a concave pocket to fill on its own.
How it works
Under the hood it's a few lines of OpenCV, which is why it's instant and needs no models. The mask is squeezed to a single frame, cast to uint8, and run through cv2.findContours with RETR_EXTERNAL - outside boundary only, interior holes ignored. Every contour's points get stacked into one point cloud, cv2.convexHull finds the rubber band, and cv2.fillPoly paints it solid at 1.0. Out comes a fresh float32 mask, same size as what went in.
The inputs and outputs
One input, one output, that's the whole UI. mask (MASK) in - wire it from whatever makes masks in your graph: SAM, GroundingDINO, the mask editor, an invert. Out comes MASK, which plugs straight into anything downstream that eats a mask: mask blur/feather, InpaintModelConditioning, ImageCompositeMasked, or the VAE encode for an inpaint pass.
Installing it
Installing is the boring part. ComfyUI Manager → search "ComfyUI-My-Mask" → Install, restart, done. Or the manual route:
cd ComfyUI/custom_nodes
git clone https://github.com/jerrylongyan/ComfyUI-My-Mask
Restart ComfyUI. The pack declares numpy, opencv-python, and torch (>= 2.0) in its requirement file; Manager grabs opencv-python if it's missing and you already have the other two. No model files, no downloads, nothing to configure.
Where people get burned
All grounded in the actual code:
- Disconnected blobs get bridged. The hull is computed over all white pixels at once, so two separate mask islands become one solid shape connecting them. A U-shaped mask comes back as a disc. Sometimes that's exactly the point; sometimes it's a disaster. Keep a copy of the original if you might want it back.
- It's single-frame and CPU-only. The code does
squeeze(0).numpy(), so feed it one mask at a time. A batched mask (batch > 1) throws, and a CUDA tensor will too. ComfyUI masks are normally CPU tensors of shape (B, H, W) with B=1, so it works out of the box - just don't hand it a video batch. - The output is hard-edged. It fills at a flat 1.0, so any feather you had is gone. Blur after this node, not before - the uint8 cast also truncates soft anti-aliased edges when detecting contours, so a pre-feathered mask loses its gradient before the hull is even computed.
If your mask is already a clean convex blob, this node does nothing useful and you should skip it. If it's a jagged mess you need to make solid before an inpaint, it's the whole fix in one node - two, if you count its sibling that only convexifies the bottom half.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| mask | MASK | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| MASK | MASK | — |