CV Components Touching Border
The sky is whatever runs off the edge of the frame
- image
- mask
- area
- count
- found
What this is for
"Which blob is the foreground" is a question people try to answer with size or with a probe point, and both answers are wrong often enough to be a running joke. Keep the largest component and on a washed-out driving frame you get the road, 71,344 pixels of it, not the sky you wanted. Put one probe point in the sky and a lamp post cuts that sky into 18 separate components - your probe caught one of them.
Touching the frame border is a different predicate, and for background it's the right one: sky, a table surface, water, a wall - anything unbounded runs off the edge of the image by definition. Invert it and you have the standard "drop detections the frame clipped" filter, which is what you want when a boat half out of shot is never going to be classified correctly anyway.
How it works
It runs cv2.connectedComponentsWithStats on the binarized input with your connectivity setting, then examines the label strips along the chosen edges. Any label appearing in those strips is "touching". keep touching keeps exactly those; remove touching keeps everything else. Both are one operation on the same label set, which is why they're a mode instead of two nodes.
The border dropdown is wider than just "any edge": top, bottom, left, right, top or bottom, left or right. That matters for panning shots and for frames where the ground plane, not the sky, is the thing you're isolating.
margin is the setting with the best tooltip in the pack, and it's worth reading rather than rediscovering: it's how many pixels in from the edge still count as the border. Zero sounds right and often isn't, because whatever the frame edge already carries becomes part of your mask. cv2.remap with BORDER_CONSTANT leaves a dark seam one row deep; any blur or derivative after it smears that seam a few rows further, so a mask built from gradients may only start answering the question at row 4. Not a morphology problem, either - OpenCV pads erosion with +inf, so an opening doesn't shrink a region at the image border.
Inputs and outputs
image- binary image or MASK; non-zero is foreground. It comes back in the format you put in.borderandmode- as above.any edge+keep touchingis the sky-mask case;any edge+remove touchingis the clipped-detection filter.connectivity(advanced, default 8) - 8 also connects diagonals. On noisy masks this is the difference between one sky and a hundred islands.margin(advanced, default 0) - see above.
Outputs: mask (your kept components, same format as the input), area (total kept pixels - handy as a sanity threshold, and as a "how much sky is in this frame" metric), count (how many components were kept) and found. Nothing surviving is found=false with an empty mask, not an error, so a frame with no sky doesn't blow up a batch.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
# restart ComfyUI
Manager users: ComfyUI CV (bmad4ever). Python ≥ 3.12 and a V3-node-API ComfyUI; opencv-contrib-python-headless~=5.0.0.93 comes from the pack's requirements. No models - it's connected components on a mask you already produced.
Common issues
- Nothing is kept, but the region clearly touches the edge. Raise
margin. The classic case is a mask derived from a remapped or gradient-based image where the outer rows are artefact, not signal. - The sky came back as 20 pieces. Connectivity 4 on a noisy mask, usually. Set it to 8, or clean the mask before this node rather than after.
- You kept the road instead of the sky. Both touch the border. Restrict
bordertotop, or useareaas a filter downstream - the road touching the bottom edge is whyany edgeis not always the right answer. - Contrib nodes missing from the pack. A non-contrib OpenCV wheel emptied the contrib submodules in the shared
site-packages/cv2;tools/repair_opencv_contrib.py --check/--apply.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| image | COMFY_MATCHTYPE_V3 | Binary 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. | |
| border | COMBO | any edge | Which image edge a component must reach to count. |
| mode | COMBO | keep touching | 'keep touching' isolates the background that runs off the frame; 'remove touching' drops blobs the frame cut off and keeps the fully-visible ones. |
| connectivityopt | COMBO | 8 | 8 also connects diagonal pixels; 4 only direct neighbours. |
| marginopt | INT | 00–64 | How many pixels in from the edge still count as 'the border'. It has to cover whatever artefact the frame edge already carries, or a region that visually reaches the edge is missed entirely: cv2.remap with BORDER_CONSTANT leaves a dark seam one row deep, and any blur/derivative applied after it smears that seam a few rows further, so a mask derived from gradients may only start answering at row 4. Morphology is NOT the problem - OpenCV pads erosion with +inf, so an opening does not shrink a region at the image border. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| mask | COMFY_MATCHTYPE_V3 | 0/255 mask of the kept components, in the image input's format. |
| area | INT | Total pixel area kept. |
| count | INT | How many components were kept. |
| found | BOOLEAN | False when nothing was kept (empty mask out). |