Nodes/ComfyUI CV/CV Fill Holes
ComfyUI Node

CV Fill Holes

Turn a ring into a disk before you erode it

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
CV Fill Holes
  • image
  • mask

Background pixels fully enclosed by foreground become foreground. That's it - and yet it's the difference between a usable mask and a leaky one. Motion masks have holes wherever the object is low-texture or matches the background. Thresholds leave rings instead of disks. Both cases break the next step in the chain, which is almost always "erode this a bit and use it as a seed".

Why the classic failure happens

Erode a hollow ring and you get a thinner ring, or nothing. Erode a mask with holes punched through it and the seed you hand to GrabCut (or to an inpainting mask, or to a mask-based crop) has the interiors missing - so the part you're trying to preserve gets repainted. The usual fix is a morphological close, which is blunt: it grows the object as well as filling it, and it needs a kernel size you now have to tune per image. CV FillHoles fills only enclosed background and leaves the outer boundary exactly where it was.

How it works

The mechanism is the textbook one, and the source is short enough to state precisely: threshold the input to 0/255, pad the mask with a one-pixel zero border, flood-fill from (0, 0) on a copy, invert the flood result and crop the padding back off. Everything the flood couldn't reach is interior background - those pixels are the holes - and the output is the original binary OR'd with them.

The padding is the detail that makes it robust. Without it, an object touching the image edge has its background connected to the frame border only diagonally, the flood seed can leak, and the "hole" that is really the rest of the world gets filled. With the zero border, the seed at (0, 0) is always background regardless, so objects that run off the edge of frame are handled correctly.

An empty mask stays empty. Nothing raises.

Inputs and outputs

One input, one output - this is one of the simplest nodes in the pack:

  • image - a binary image or MASK; any non-zero pixel is foreground. Colour input is handled (it's thresholded to "non-zero"), so you can feed a mask preview without converting first.
  • output mask - a 0/255 mask with the interior holes filled, returned in the input's format. Feeding a MASK gets you a MASK; feeding an IMAGE gets you an IMAGE-shaped thing. That keeps it drop-in.

There's no kernel size, no iteration count, no connectivity flag. It fills what's enclosed and nothing else. Which is why it's the right tool rather than a morphological close for this particular job.

Where it goes in a graph

The canonical chain in this pack's own docs: motion or threshold mask → CV Fill Holes → erode → seed → GrabCut or a mask-based crop. Also common: a segmentation mask with speckle holes, where filling them stops the "bumpy interior" look when composited; a ring detected as a contour, where filling gives you the disk you actually meant; and a sky or background mask, where holes in a low-contrast gradient sky would otherwise let the foreground leak through.

If what you have is a contour rather than a mask, note that filling a ring's contour is a different operation - rectangles and polygons drawn from contours are a separate set of nodes. CV Fill Holes works on rasters, not on point sets.

Install

# ComfyUI Manager → search "ComfyUI CV" → install → restart
# or manually:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
cd comfyui_cv && pip install -r requirements.txt

Python ≥ 3.12 and a recent ComfyUI (the pack is built on the V3 node API; on an older build you'll see no nodes at all). The single dependency is opencv-contrib-python-headless~=5.0.0.93. Keep it contrib - installing plain opencv-python over it silently empties the shared cv2 package's contrib submodules and makes contrib-category nodes vanish; the pack ships tools/repair_opencv_contrib.py (--check, then --apply) for that. This node is a plain cv2 call, so a clobbered contrib install doesn't touch it.

Gotchas

  • It ate the background. Your flood leaked, which usually means the input wasn't actually binary - a grayscale mask with values above zero everywhere is all-foreground, and "enclosed" becomes "everything". Threshold upstream.
  • The hole is still there. The hole isn't enclosed - there's a one-pixel gap somewhere linking it to the outside. Run CV Fill Holes after a closing or a small blur, or fill the contour instead.
  • Mask conventions. A MASK input and an IMAGE input both work, but they don't necessarily mean the same thing about which channel is foreground; the output comes back in the input's format so downstream nodes keep working either way.
  • Order matters. Fill before you erode, not after. Eroding first can disconnect the ring from itself and the fill no longer has an enclosure to find.

bmad4ever's pack is a fork of geroldmeisinger's opencv-comfyui, rewritten on the modern node API, and the author is explicit that it's LLM-assisted and not production-grade without review. This node is a short, readable algorithm - the sort of thing you can verify in a couple of minutes, which is more than you can say for the DNN parts of the same pack.

Categoryimage/CV/contours

Inputs (1)

NameTypeDefaultDescription
imageCOMFY_MATCHTYPE_V3Binary image (or MASK); non-zero = foreground. The mask comes back in this input's format. 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.

Outputs (1)

NameTypeDescription
maskCOMFY_MATCHTYPE_V30/255 mask with interior holes filled, in the image input's format.