Mpi Mask Fill Holes
SAM3 keeps punching the teeth out of your face mask
- mask
- mask
Prompt SAM3 for a face or a head and you get a good mask with the lips and teeth missing. Not softened - missing, a hole in the middle of a region the detector otherwise nailed. A composite then paints straight through it, and on video it flickers, because whether the hole shows up depends on the frame. If you've been staring at a face inpaint wondering why the mouth keeps coming back wrong, this is very likely why.
The reflex fixes don't reach it, which is the part that wastes an afternoon. SAM3's threshold gates a detection score; it never changes which pixels a kept mask covers. refine_iterations can't carve anything out either, because the refinement result is unioned with the coarse mask - it can only ever add. Drop it to 0 and you get the same coarse mask back, hole included. The hole lives in the detector's own concept mask, and no widget on that node touches it.
What it does
MpiMaskFillHoles runs scipy.ndimage.binary_fill_holes on the mask. That fills exactly one set: pixels the mask already surrounds. The outer silhouette is untouched by construction - there is no radius, no dilation, no size ceiling that could weld a gap shut.
That last part is the real argument against the obvious alternative. A GrowMask +N then −N close fills a hole only as a side effect of dilating everything and then taking it back, which means (a) it's capped by the iteration count, (b) it bridges any outer concavity narrower than 2N, and (c) it's slow. Measured on one 768×1344 frame carrying a 120×60 lips-shaped hole: this node took 20.5 ms; GrowMask at +12/−12 took 126.1 ms - and didn't fill the hole at all, because ±12 reaches a gap about 24 px wide and the hole is 60 px tall. Buy the same result from the close and you need a radius wide enough to weld the chin to the hair. That's a worse mask than the one you started with.
It runs per frame, deliberately. scipy would happily take the whole (B, H, W) block, but it would read the batch axis as a connectivity dimension and plug frame 12's hole because frame 11 happened to be solid there. The result is composited with maximum, not a boolean or, so a feathered mask edge keeps its soft values and only the holes snap to 1.0.
Inputs and output
Two inputs, one output, and it's a short list.
mask - the mask to repair. Typically the output of an SAM3 detect node, before it goes anywhere near your composite or your inpaint encode.
max_hole_size - the largest hole to fill, in pixels of area, defaulting to 0 which means fill every enclosed hole. That default is right for a face. Raise it off 0 when the mask has a hole that's supposed to be there: on a person mask, the triangle between an arm and the torso is an enclosed hole, and uncapped this node will happily weld it.
mask (output) - the repaired mask, same shape and dtype as the input, ready to wire into your compositor or sampler.
Install
Part of MadPonyInteractive/ComfyUi-MpiNodes. ComfyUI Manager → search ComfyUi-MpiNodes, or:
cd ComfyUI/custom_nodes
git clone https://github.com/MadPonyInteractive/ComfyUi-MpiNodes
Restart ComfyUI, then search "Mpi Mask Fill Holes". There's no requirements.txt - the pack imports numpy, scipy and Pillow, all of which ComfyUI already installs for itself, so scipy.ndimage costs you nothing extra. The pack is AGPL-3.0 since v1.2.7 (MIT up to v1.2.6).
Where people get burned
A hole is not a bite. This node only reaches enclosed holes. An open mouth that runs to the jaw line breaks the silhouette instead of being surrounded, and no fill can see it - you'd have to change what you're asking the detector for. The fix there is vocabulary, not masking: add mouth to the SAM3 categories. With individual masks off, SAM3 unions every comma-separated category's mask, so head, hat, mouth covers the gap that head alone leaves.
Wondering why a body mask lost its arm hole. That's max_hole_size = 0 doing what you asked. Cap it.
Assuming it fixes a bad detector. It repairs a specific, known defect - enclosed gaps inside an otherwise-correct mask. If the mask is in the wrong place, filling holes in it just gives you a wrong mask with fewer holes.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| mask | MASK | — | |
| max_hole_size | INT | 00–16777216 | Largest hole to fill, in PIXELS OF AREA. 0 fills every enclosed hole. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| mask | MASK | — |