CV Shape Distance
Matching shapes when 'similar' isn't a moment
- contours_a
- contours_b
- distance
cv2.matchShapes compares two contours using Hu moments - seven numbers per shape. It's instant, it's rotation- and scale-invariant, and it's blind to almost everything else. When it can't tell your two shapes apart, you need a distance that knows where the points are.
CV Shape Distance wraps two of OpenCV's shape distances: Shape Context (cv2.ShapeContextDistanceExtractor) and Hausdorff (cv2.HausdorffDistanceExtractor). Both live in OpenCV's shape module as classes, which is exactly why no raw wrapper in the pack can reach them - the generator parses top-level functions only. Curated node, no alternative.
Smaller means more similar. Zero means identical. That's the whole output convention, and the number is only meaningful against a threshold you establish yourself.
Which method, and why the choice matters
Shape Context builds a log-polar histogram around every sample point and solves the correspondence between the two shapes. That's what makes it tolerant of bending and of scale - the author's description calls it "the strong matcher and much slower" and that's the honest trade. The sample_points count drives the cost hard, so start at the default 100 and only raise it if the discrimination isn't there.
Hausdorff is the worst-case nearest-point distance: cheap, purely geometric, and brutally sensitive to a single stray point. The author cites a measurement for this, which is worth more than most node docs: a 40° rotation costs 1.93 with rotation_invariant off and 0.010 with it on. On a 20° turn, 0.028 off versus 0.009 on. That's the entire argument for the toggle in two numbers.
And the honest limit, straight from the tooltip: it degrades past about 160°, and it is not a mirror test - a reflected shape lands among the large-rotation scores, not in a separate bucket. Hu-based matching is no better there, since Hu is reflection-blind in everything but its 7th component. If mirroring is your discrimination problem, use the mirror_mode policy on CV Filter Contours By Shape instead. That's a different node doing the thing this one can't.
The inputs that matter
- contours_a, contours_b - whole contour sets, as
CV Find Contoursemits them. You pick which member with the optional index_a / index_b (default 0, which is the largest, since Find Contours sorts by area). - method - the two-way choice above. Defaults to Shape Context.
- sample_points - INT, 4 to 1000, default 100. Both contours are resampled to this many points before comparison, which is why contours of different lengths are comparable at all.
- rotation_invariant - Shape Context only, off by default. Off is deliberate: it "keeps saved graphs' numbers stable", which is a real consideration when you've tuned a threshold.
- Then the second-order knobs: angular_bins and radial_bins for the Shape Context histogram, hausdorff_norm and rank_proportion for Hausdorff.
rank_proportionis the useful one - 0.6 uses the 60th-percentile nearest-point distance instead of the true maximum, which is precisely what you want when one noisy pixel would otherwise dominate. Set it to 1.0 for classic Hausdorff.
One output: distance, FLOAT. Unitless for Shape Context, pixels for Hausdorff - and that difference is not cosmetic. A pixel-unit distance only makes sense between shapes that are already aligned and at the same scale. Feed it into a comparison or math node to turn it into a yes/no, then into a switch for the branch.
Install
ComfyUI Manager → ComfyUI CV → install → restart, or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Python ≥ 3.12 and a recent ComfyUI on the V3 node API. No model files. Shape Context ships in OpenCV's shape module, part of the standard wheel - contrib isn't needed for this node specifically, keep the contrib build anyway.
Common issues
- Distance is huge and meaningless. You're comparing a pixel-unit Hausdorff distance between shapes at different scales. Normalize, or switch to Shape Context, which doesn't care.
- Distance is ~0 for shapes that shouldn't match. Too few
sample_points, so the resampling washed out the difference. Raise it and pay the time. - Graph got slow after you raised sample_points. Shape Context's cost climbs quickly with point count. It's the strongest matcher here and it charges accordingly.
- You were hoping it would detect mirrors. It won't, and neither will Hu moments.
CV Filter Contours By Shape'smirror_modeis the tool. - Contrib nodes vanished after a pip install. Plain OpenCV wheel over the contrib one - same shared
site-packages/cv2.tools/repair_opencv_contrib.py --check, then--apply.
Inputs (11)
| Name | Type | Default | Description |
|---|---|---|---|
| contours_a | CV_CONTOURS | First shape. A whole contour LIST is accepted; the contour selected by 'index_a' is used. | |
| contours_b | CV_CONTOURS | Second shape, compared against the first. | |
| method | COMBO | Shape Context (deformation-aware) | Shape Context handles deformation and returns a unitless matching cost; Hausdorff returns a distance in PIXELS, so it only makes sense on shapes that are already aligned and at the same scale. |
| sample_points | INT | 1004–1000 | Both contours are resampled to this many points before comparing, which is what makes contours of different lengths comparable. More points = finer and slower (Shape Context cost grows quickly). |
| index_aopt | INT | 00–10000 | Which contour of 'contours_a' to use (0 = the largest, since 'CV Find Contours' sorts by area). |
| index_bopt | INT | 00–10000 | Which contour of 'contours_b' to use. |
| angular_binsopt | INT | 124–64 | Shape Context only: angular bins of the log-polar histogram. |
| radial_binsopt | INT | 42–32 | Shape Context only: radial bins of the log-polar histogram. |
| hausdorff_normopt | COMBO | L2 (euclidean) | Hausdorff only: the point-to-point norm used. |
| rank_proportionopt | FLOAT | 0.600–1 | Hausdorff only: use this RANK instead of the true maximum (0.6 = the 60th-percentile nearest-point distance), which makes it robust to a few outliers. 1.0 is the classic worst-case Hausdorff distance. |
| rotation_invariantopt | BOOLEAN | false | Shape Context only: measure each log-polar histogram against the local tangent instead of a fixed axis, which is what actually makes it rotation tolerant (measured on a 40-degree turn: 1.930 off, 0.010 on; on a 20-degree turn 0.028 off, 0.009 on). It DEGRADES past about 160 degrees, and it is not a mirror test - a reflected shape lands among the large-rotation scores. Off by default so saved graphs keep their numbers. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| distance | FLOAT | Shape dissimilarity - 0 means identical. Unitless for Shape Context, PIXELS for Hausdorff. |