Nodes/ComfyUI CV/CV Shape Moments
ComfyUI Node

CV Shape Moments

The shape measurements Hu moments throw away

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
CV Shape Moments
  • contours
  • centroids
  • areas
  • angles
  • eccentricities
  • axes
  • hu_moments
  • count
  • orientations
  • chirality
  • pose_confidence

cv2.moments and cv2.HuMoments are already in the auto-generated wrapper set, so why does a curated node exist? Because the raw wrappers have no output socket worth the name. Getting anything out of a moments struct in a graph means parsing an OrderedDict, and the pack doesn't do dicts - it does arrays and typed sockets.

CV Shape Moments takes a contour set and emits ten aligned arrays: the numbers people actually threshold on, in shapes the rest of the pack can consume.

What comes out

Coordinates and size first: centroids (N,1,2) - plugs straight into CV Draw Points or Draw Labels. areas (N,), the m00 moment. angles (N,), the major-axis orientation in degrees over [-90, 90), measured from +X with Y pointing down because this is image space - and the tooltip is honest that it's meaningless for near-circular shapes. eccentricities (N,) from 0 to 1: 0 is a perfect circle, approaching 1 is a line, derived from the central-moment eigenvalues. axes (N,4) are the major-axis segments x1,y1,x2,y2, 2 standard deviations per side, ready for CV Draw Segments. Plus count, because every node in this pack tells you when it found nothing rather than raising.

Then the interesting three. hu_moments is (N,7), log-scaled as -sign(h)·log10(|h|) - the raw values span about forty orders of magnitude, so the raw numbers are unusable without this. It's the classic translation/scale/rotation-invariant signature, and it's what cv2.matchShapes compares when you use that instead. But Hu invariance has a specific hole in it, and the last three outputs are about that hole:

  • orientations (N,), 0–360°, resolved from the third-order moments. That's the difference from angles: angles is only defined mod 180, so a shape and its 180° rotation read identically. orientations tells head from tail. It always equals angles or angles + 180.
  • chirality (N,), signed handedness from the 7th Hu invariant - the only one that isn't reflection-invariant. OpenCV's own docs say it: invariants to "scale, rotation, and reflection except the seventh one, whose sign is changed by reflection". And h7 is the invariant everybody discards in practice, because it's the smallest and noisiest of the seven. The tooltip gives measured magnitudes, which is the part you can't guess: an F glyph reads 0.020–0.048, a scalene triangle 0.0069, and a genuinely mirror-symmetric shape - square, circle, ellipse, isoceles triangle - reads below ~0.005 where the sign is rasterization noise. So the sign says mirrored-or-not, the magnitude says whether you should believe it.
  • pose_confidence (N,), how well-defined orientations is: the normalized third moment along the major axis. Measured 0.052 for a strongly asymmetric shape, 0.00057 for an ellipse, 0.0 for a square. Test against roughly 0.005, not against zero - below that, the 0–360 orientation is a coin flip and any rotation you measure from it is noise.

That last point deserves emphasis because it's the trap: orientations is always a number, even when the shape is symmetric and the number is arbitrary. pose_confidence is how you avoid building a decision on it.

How you use it

One input - contours, from CV Find Contours - and the outputs fan out to the rest of the pack: centroids and axes to the draw nodes, areas and eccentricities to CV Filter Contours-style gating, hu_moments to a vector distance or CV Filter Contours By Shape, and chirality/orientations when you're classifying orientation-sensitive shapes (letters, arrows, hands on a clock). Zero contours is a valid result: empty arrays, count = 0.

Install

ComfyUI Manager → ComfyUI CV → install → restart. By hand:

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, recent ComfyUI on the V3 node API, no model downloads. The contrib wheel is the pack's declared dependency even where a given node doesn't need it.

Common issues

  • You compared Hu vectors raw and everything looked identical. Log-scaled or not, the seven components live on wildly different scales. Normalize per component, or compare 5 of the 7 and skip h6/h7 - they're the small, noisy pair, which is exactly the pack author's own note about which invariants get dropped in practice.
  • orientations flipped between two nearly identical frames. A near-symmetric blob with pose_confidence under ~0.005. Check the confidence, not the angle.
  • Chirality says "mirrored" for a shape that isn't. Magnitude below ~0.005 - noise. Threshold on magnitude as well as sign.
  • Areas are zero for everything. Your mask is empty or inverted. This node measures foreground; if the threshold upstream flipped, every number is an honest measure of nothing.
  • Category missing after a pip install. Non-contrib OpenCV wheel wiped the contrib submodules in the shared site-packages/cv2; tools/repair_opencv_contrib.py --check then --apply.
Categoryimage/CV/contours

Inputs (1)

NameTypeDefaultDescription
contoursCV_CONTOURSFrom 'CV Find Contours'.

Outputs (10)

NameTypeDescription
centroidsNPARRAY(N,1,2) float32 center of mass per contour - plugs into 'CV Draw Points' / 'Draw Labels'.
areasNPARRAY(N,) float32 area in px^2 (the m00 moment).
anglesNPARRAY(N,) float32 orientation of the major axis in degrees, [-90, 90), measured from the +X axis with Y pointing DOWN (image coordinates). Meaningless for near-circular shapes (eccentricity ~ 0).
eccentricitiesNPARRAY(N,) float32 elongation in 0-1: 0 = perfect circle, -> 1 = a line. From the central-moment eigenvalues.
axesNPARRAY(N,4) float32 major-axis segments (x1, y1, x2, y2) through each centroid, 2 standard deviations long per side - plugs into 'CV Draw Segments'.
hu_momentsNPARRAY(N,7) float32 log-scaled Hu invariants: -sign(h)*log10(|h|) per component (the raw values span ~40 orders of magnitude). Compare shapes with 'CV Filter Contours By Shape' or a vector distance.
countINT—
orientationsNPARRAY(N,) float32 canonical orientation in degrees, 0-360, from the +X axis with Y pointing DOWN. Unlike 'angles' (defined only mod 180) the head is told from the tail using the third-order moments, so a shape and its 180-degree rotation read differently. Always equals 'angles' or 'angles' + 180. MEANINGLESS when pose_confidence is ~0.
chiralityNPARRAY(N,) float32 signed handedness, from the 7th Hu invariant - the ONLY one that is not reflection-invariant (OpenCV: "invariants to the image scale, rotation, and reflection except the seventh one, whose sign is changed by reflection"; Hu 1962). h7 is also the one routinely DISCARDED in practice, because it is the smallest and noisiest of the seven - raw values here are ~1e-6 - so this output rescales it as sign(h7)*|h7|**0.25 to put it on a thresholdable scale. The SIGN flips when the shape is mirrored and survives any rotation or scale; the MAGNITUDE says how far to trust it: an 'F' glyph reads 0.020-0.048, a scalene triangle 0.0069, and a mirror-symmetric shape (square, circle, ellipse, isoceles triangle) reads below ~0.005, where the sign is only rasterization noise (OpenCV again: the invariance assumes "infinite image resolution"). It agrees in SIGN with hu_moments[:,6] - that column negates the sign but also takes log10 of a value far below 1, and the two flips cancel.
pose_confidenceNPARRAY(N,) float32 how well-defined 'orientations' is: the normalized third moment along the major axis. Measured 0.052 for a strongly asymmetric shape, 0.00057 for an ellipse and 0.0 for a square or circle - so test against ~0.005, not against 0. Below that the 0-360 orientation is a coin flip and any rotation measured from it is noise.