Contour Gap Closer
Reconnecting broken contours by their endpoints — with a direction check so it doesn't bridge the wrong things
- image
- image
- contour_mask
Broken contours are the reason a laser cutter or engraver cuts a shape twice, or not at all: the path just isn't closed. Contour Gap Closer detects those breaks - the open endpoints in a skeletonized drawing - and draws connecting lines between nearby ones to produce closed, continuous paths. It's the "repair" node in this pack's contour toolset, and it pairs with Contour Endpoint Visualizer (which shows you the endpoints first) and LaserPathTracer (which needs clean closed features to trace).
How it works
The algorithm is refreshingly mechanical. Binarize the image at threshold (0–1, default 0.5) with invert handling polarity (default on = dark lines on light). Skeletonize down to single-pixel lines. Then find endpoints - pixels with exactly one neighbor in the skeleton. For each endpoint, find the nearest other endpoint within max_gap pixels (default 20) and draw a straight line between them. Repeat for iterations (default 3) passes so multi-gap chains get closed progressively.
The part that makes it not-dumb: direction_weight (0–1, default 0.3). Before connecting two endpoints, the node estimates each line's tangent direction by walking a few pixels back along the skeleton. When it scores candidate pairs, it biases toward pairs whose directions are roughly collinear - i.e. it prefers bridging a gap in the same line over fusing the end of one line to the middle of an unrelated one. At 0 it's pure nearest-neighbor (fast, sloppy); at 1.0 it's strict collinearity (won't bridge a sharp corner). The default is a reasonable middle ground; raise it if your drawings have dense, nearby, unrelated lines.
Before the endpoint search, morph_close_radius (default 2) optionally applies a morphological closing to heal micro-gaps, so tiny breaks don't even reach the endpoint logic.
Outputs and modes
image(IMAGE) - the repaired drawing, rendered peroutput_mode:original_thickness(keep line weight),skeleton(thin to 1px), ordilated(thicken todilation_radiuspx). For feeding back into measurement nodes,skeletonis usually right.contour_mask(MASK) - a binary mask of the closed contours, handy for masking downstream or compositing.
re_skeletonize (default off) runs skeletonization on the output so the repairs stay single-pixel-wide - turn it on if you're feeding a measurement node that assumes thin lines.
Where it fits
Extract Black → ContourEndpointVisualizer (check the breaks) → ContourGapCloser → LaserPathTracer. The visualizer exists specifically to tell you what max_gap and direction_weight should be before you run the closer, and skipping that check is how you end up with paths that cut through unrelated geometry.
Install
Part of ComfyUI-HappNodeSet (mikemojen). Manager: search HappNodeSet. Manual:
cd ComfyUI/custom_nodes
git clone https://github.com/mikemojen/ComfyUI-HappNodeSet.git
pip install -r ComfyUI-HappNodeSet/requirements.txt
Restart ComfyUI. Dependencies here are the scipy ndimage convolution and KDTree for the nearest-endpoint search, plus skimage for skeletonization - all in the pack's standard requirements.
Common issues
If max_gap is too large, the closer starts bridging genuinely separate lines - especially with direction_weight low. If the repaired paths cut across the drawing at weird angles, raise direction_weight. And remember this node only knows endpoints after skeletonization: thick, blobby linework gives you fewer clean endpoints and messier repairs, so thin the input first. Preview the image output before trusting any downstream trace.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| max_gap | INT | 201–200 | Maximum pixel distance between endpoints to bridge |
| threshold | FLOAT | 0.500.01–0.99 | Binarization threshold (0-1) |
| invert | BOOLEAN | true | True = dark lines on light background |
| morph_close_radius | INT | 20–20 | Morphological closing radius to heal micro-gaps first (0=skip) |
| re_skeletonize | BOOLEAN | false | Skeletonize the output to single-pixel lines |
| direction_weight | FLOAT | 0.300–1 | Direction matching strictness (0=any, 1=strict collinear) |
| iterations | INT | 31–10 | Repeat endpoint-detection + connection N times |
| output_mode | COMBO | 3 options: original_thickness, skeleton, dilated | |
| dilation_radius | INT | 11–10 | Line thickness when output_mode='dilated' |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| contour_mask | MASK | — |