CV Filter Contours By Shape
Matching a template, and the mirroring trap nobody warns you about
- contours
- reference
- contours
- count
- distances
- scale_ratios
- rotations
- mirrored
- transforms
Give it a set of candidate contours and one reference contour, and it keeps the ones that look like the reference. Under the hood it's cv2.matchShapes on Hu moments - a shape signature that's translation-, scale- and rotation-invariant by construction. Which is exactly what you want until you discover what "invariant" quietly cost you.
How the comparison works
Hu moments describe a shape's mass distribution in a way that doesn't change when you move it, resize it or spin it. matchShapes compares those signature vectors and returns a distance - 0 for identical shapes, and roughly under 0.1–0.3 for similar ones with the usual CONTOURS_MATCH_I1 norm. method picks the norm (I1 is the normal choice; I2 and I3 are alternatives), and max_distance (default 0.3) is the accept/reject line.
Make the reference with CV Select Contour - pick the largest blob and use that - or by running a second CV Find Contours over a template image. If the reference set has several contours, the first one is used. Output is sorted most-similar-first, and every per-match output is aligned to that ordering.
The part worth reading twice
Hu moments are invariant to reflection except for the seventh invariant, whose sign flips under mirroring. And here's the kicker, straight from the node's own (very long) tooltip, which reports it as measured on this build: matchShapes skips any term below its internal eps = 1e-5, and |h7| is usually below that. So an "F" glyph and its mirror image come back indistinguishable (~0 on all three norms), and the mirror can even rank above a correctly-handed but rotated match. Meanwhile a hook shape with a larger |h7| scores 0.50 for its mirror.
The conclusion the author draws is the right one: mirror sensitivity here is a cliff, not a property, and it is never a usable handedness test. So the node doesn't pretend. It gives you explicit policies instead:
scale_mode-any scale (invariant)by default, orsame scale as reference, withscale_toleranceas a fractional band (0.10 accepts roughly 0.91× to 1.10× linear size, symmetric so growing and shrinking are treated alike).rotation_mode- any rotation by default,same orientation as reference, orsame orientation or 180 deg flipped. That middle option matters for two-fold-symmetric shapes (ellipses, rectangles) whose 0–360 orientation flips arbitrarily; the tooltip also warns the whole idea is meaningless for a shape with no well-defined orientation - a rotating square reads 0, 140, 145, 155, 135 degrees, which is pure noise. Checkpose_confidenceonCV Shape Momentsfirst.rotation_tolerance_deg(default 10) - compared circularly, so 359° counts as 1° away from 0.mirror_mode-either handedness (invariant),same handedness only, ormirrored only. A mirror-symmetric shape is its own mirror, so it counts as undecided: kept by "same handedness only", dropped by "mirrored only".min_chirality(default 0.01) - how chiral a shape must be before the node will decide handedness at all. This exists because the 7th Hu invariant is the smallest and most fragile, and OpenCV's own docs admit the invariance is proved assuming infinite resolution, so raster images have slightly different invariants. A rasterised symmetric shape gets a small non-zeroh7whose sign means nothing. Measured magnitudes in the tooltip: an "F" glyph 0.020–0.048, a scalene triangle 0.0069, a rasterised symmetric shape up to about 0.005 of pure noise, a square exactly 0. Below the threshold on either side, the node reportsmirrored = 0(undecidable) rather than guessing. Set it to 0 to trust the sign always.
That's an unusual amount of intellectual honesty for a node tooltip, and it's the reason this node is more trustworthy than most shape-matching code you'll find.
Outputs, including the good one
contours (most similar first), count, distances, scale_ratios (size relative to reference, as the ratio of sqrt(m00), so 0.5 = half the linear size), rotations (0–360, clockwise on screen, Y-down; for a mirrored match it's the rotation applied after flipping), mirrored (+1 reflected, -1 same handedness, 0 undecidable), and transforms - an (N, 2, 3) similarity matrix per kept contour mapping reference coordinates onto that contour.
That last one is the payoff. Pull a row out with CV Slice Array (axis 0) and feed cv2_warpAffine to warp a whole template image onto the match - template matching with rotation and scale, without ever computing a correlation. Or feed the reference points (via CV Contour To Points) through cv2_transform to draw the reference over its match for a visual check.
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 on the V3 node API. Single dependency: opencv-contrib-python-headless~=5.0.0.93. Keep it contrib - a plain opencv-python install over it silently empties the contrib submodules and contrib nodes disappear from the menu (tools/repair_opencv_contrib.py --check / --apply).
Tuning
Raise max_distance to something huge (the tooltip suggests 1e6), run once, and read the distances output. That tells you where your matches actually sit instead of guessing 0.3 and wondering why a good match got filtered out. Then narrow. And if identical shapes are scoring 0.5, you're not comparing shapes - you're comparing scale policies with an unintended constraint switched on.
The pack is bmad4ever's rewrite of geroldmeisinger's opencv-comfyui, with the author's standing disclaimer that it's LLM-assisted and not production-grade. Given how much of this node's behaviour is documented at the level of measured numbers, it's also the piece of the pack most worth reading the tooltip on rather than skimming.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| contours | CV_CONTOURS | Candidates to test. | |
| reference | CV_CONTOURS | The shape to compare against (first contour of the set). | |
| method | COMBO | CONTOURS_MATCH_I1 | How the Hu-moment signatures are compared (I1 is the usual choice; I2/I3 are alternative norms). |
| max_distance | FLOAT | 0.300–1000000 | Largest accepted dissimilarity (0 = identical). Raise it to 1e6 and inspect the distances output to calibrate. |
| scale_modeopt | COMBO | any scale (invariant) | Hu moments ignore size. 'same scale as reference' additionally requires the candidate to be about as big as the reference - the ratio of sqrt(m00), so 0.5 means half the linear size. |
| scale_toleranceopt | FLOAT | 0.100–100 | Accepted size band as a fraction: 0.10 accepts 0.91x to 1.10x of the reference (symmetric in the ratio, so growing and shrinking are treated alike). 0 demands an exact match. Ignored unless scale is 'same scale as reference'. |
| rotation_modeopt | COMBO | any rotation (invariant) | 'same orientation as reference' keeps only candidates standing the same way up. 'same orientation or 180 deg flipped' also accepts the upside-down copy - USE IT for 2-fold symmetric shapes (ellipse, rectangle), whose 0-360 orientation flips arbitrarily. Both are meaningless for a shape with no well-defined orientation: check 'pose_confidence' first (a rotating square reads 0, 140, 145, 155, 135 degrees - pure noise). |
| rotation_tolerance_degopt | FLOAT | 100–180 | Half-width of the accepted rotation window in degrees. Compared CIRCULARLY, so 359 degrees counts as 1 degree away from 0. |
| mirror_modeopt | COMBO | either handedness (invariant) | The matchShapes distance is a poor handedness test, so decide it here instead. Only the 7th Hu invariant's SIGN changes under reflection (OpenCV: "invariants to the image scale, rotation, and reflection except the seventh one, whose sign is changed by reflection"), and matchShapes skips any term below its internal eps = 1e-5 - which |h7| usually is. Measured: an 'F' and its mirror are indistinguishable (~0), while a hook with a bigger |h7| scores 0.50. 'same handedness only' drops reflected copies; 'mirrored only' keeps just the reflected ones. A mirror-symmetric shape (square, circle, isoceles triangle) is its own mirror, so it counts as UNDECIDED: kept by 'same handedness only', dropped by 'mirrored only'. |
| min_chiralityopt | FLOAT | 0.0100–1 | How chiral a shape must be before mirroring can be decided at all - the magnitude of the 'chirality' value. This knob exists because the 7th Hu invariant is the one routinely discarded in practice: it is the smallest and the most fragile, and OpenCV's own docs note the invariance is proved "with the assumption of infinite image resolution", so "in case of raster images, the computed Hu invariants for the original and transformed images are a bit different". Rasterizing a symmetric shape therefore produces a small NON-zero h7 whose sign is meaningless. Measured magnitudes: an 'F' glyph 0.020-0.048, a scalene triangle 0.0069, a rasterized symmetric shape up to ~0.005 of pure noise, a square exactly 0. Below this on EITHER side the match is reported as mirrored = 0 (undecidable) rather than guessed. Set 0 to trust the sign always. |
Outputs (7)
| Name | Type | Description |
|---|---|---|
| contours | CV_CONTOURS | The matching contours, most similar first. |
| count | INT | — |
| distances | NPARRAY | float32 matchShapes distance per kept contour (aligned with the contour output). |
| scale_ratios | NPARRAY | (N,) float32 size of each kept contour relative to the reference - the ratio of sqrt(m00), so 0.5 means half the linear size. 0.0 when either shape has no usable area. |
| rotations | NPARRAY | (N,) float32 rotation in degrees, 0-360, taking the reference onto each kept contour (Y down, so it turns clockwise on screen). For a MIRRORED match it is the rotation applied AFTER flipping the reference about its vertical axis. Noise for a shape with no well-defined orientation - check 'pose_confidence' on 'CV Shape Moments'. |
| mirrored | NPARRAY | (N,) float32 handedness verdict per kept contour: +1 reflected, -1 same handedness, 0 undecidable (one of the two shapes is mirror-symmetric, or is less chiral than 'min_chirality'). |
| transforms | NPARRAY | (N,2,3) float32 similarity matrix per kept contour, mapping REFERENCE coordinates onto that contour (mirror, then scale, then rotate, then centroid to centroid). Pull one row out with 'CV Slice Array' (axis 0) and feed cv2_transform with the reference points from 'CV Contour To Points' to draw the reference over its match, or cv2_warpAffine to warp a whole template image onto it. |