Zura · Fill Small Mask Holes
The five-second fix for a mask full of pinholes
- masks
- cleaned_mask
Segmentation masks come back with holes. A face mask cut by SAM will have stray background pixels punched through it - a nostril, a gap between glasses and cheek, a couple of noisy pixels on a jacket. Those holes are individually invisible and collectively ruinous, because in a masked video edit every hole is a place the sampler is told "do not change this."
Why you'd reach for it
The author's own description is refreshingly honest about the scope: "Fill tiny enclosed gaps in each mask frame. Border-connected areas and larger gaps stay untouched; inspect the mask before generation." That's the whole node. It is not a mask refiner, it doesn't close real holes, and it doesn't touch the subject's outline.
Where it earns its keep is the step right before a masked H3 or Wan render, after you've detected and tracked a performer. You look at the mask preview and it's 98% solid with a sprinkle of one-pixel gaps. This node cleans that up in one pass and you move on.
How it works
Per frame, it thresholds the mask at 0.5 and treats everything below that as background. It then runs cv2.connectedComponentsWithStats with four-way connectivity - four-way, not eight, so diagonal gaps count as separate regions - and collects every background component whose pixel area is at or under max_hole_area.
The second half of the trick is the border check. Any component that touches the edge of the image is discarded, because "background that reaches the frame edge" is the actual background, not a hole. Only fully enclosed components get filled. That's what makes this safe to run blind: it can't bleed your mask out to the frame boundary.
It runs frame by frame, transferring one frame to CPU at a time, so it carries no temporal memory. That's a feature for a per-frame mask - and a limitation if your holes are actually tracking failures that span 30 frames, which this will never fill.
Inputs and outputs
masks- anyMASK, but it wants a properly shaped[frames, height, width]float batch, not a single 2D mask.max_hole_area- default 128, range 0–256. The tooltip is the whole manual: "Fill enclosed gaps up to this many pixels. Set to 0 to pass the mask through." Start at 128. Push it toward 256 if you're on a high-res raster where a "tiny" gap is more pixels than it used to be.cleaned_mask- same shape, same dtype, holes filled. Feeds your sampler's mask input or the mask-check preview.
Install
One pack, one install. In ComfyUI Manager search Zura Nodes, or:
cd ComfyUI/custom_nodes
git clone https://github.com/ZURAVFX/ComfyUI_zura_nodes
pip install -r ComfyUI_zura_nodes/requirements.txt
Restart. No model weights are involved in this node at all. It does need OpenCV at runtime, which you almost certainly have already - the pack's ultralytics dependency pulls it in.
Common issues
Nothing changed. Either there genuinely are no enclosed holes under the threshold, or the gaps you're looking at touch the image border. Raise max_hole_area a notch and compare.
A real hole didn't get filled. Above 256 pixels, that's out of scope by design. Use a morphological close from KJNodes (or a proper inpaint pass) for anything bigger - this node is deliberately conservative because a mouse that fills large gaps will happily swallow the gap between an arm and a torso and hand you a blob.
"Provide a floating-point MASK batch with shape [frames, height, width]." You passed a single 2D mask. Run it through a batch helper first, or check whether your upstream node dropped a dimension.
Very long clips. There are hard ceilings - 4096 frames, 16.7M pixels per frame, 134M pixels in the batch. A 5-second 24 fps clip at 1080p is nowhere near any of them, but a full-length 4K source will trip it.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| masks | MASK | — | |
| max_hole_area | INT | 1280–256 | Fill enclosed gaps up to this many pixels. Set to 0 to pass the mask through. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| cleaned_mask | MASK | — |