Nodes/ComfyUI CV/CV Image Moments
ComfyUI Node

CV Image Moments

Measuring a shape by how bright it is, not just its outline

By bmad4ever·Created 4 months ago·Updated 15 days ago· 1
CV Image Moments
  • images
  • centroids
  • mass
  • angles
  • eccentricities
  • axes
  • hu_moments
  • count
  • orientations
  • chirality
  • pose_confidence
◄weightingintensity, peak-normalized►

Here's a distinction that confuses people for years: a contour node and a moments node can both tell you a region's orientation, but they're not measuring the same thing. Contour moments integrate a uniform density inside an outline - the outline is all that matters. cv2.moments on a raster defaults to binaryImage=False, where a pixel's mass is its value. That's Hu's original density formulation, and it's the case the contour nodes in this pack structurally cannot reach: two regions with identical outlines but different internal shading are the same shape to CV Shape Moments and different shapes to CV Image Moments.

If that's your problem, this is the node. If it isn't, use the shape one.

The one input you'll tune

images takes frames (IMAGE/MASK/NPARRAY, one output row per frame) and reduces each to a single channel - luminance for colour, since cv2.moments rejects 3-channel data.

weighting decides the density function, and it's the interesting part:

  • intensity, peak-normalized (the default) - divide by the frame's own maximum before integrating. Hu moments are not invariant to a brightness gain, and the tooltips report the measured cost of ignoring that: a ×0.5 exposure change moved the I1 error from 0.0394 raw to 0.0146 normalized. That's a real improvement, and honestly reported as imperfect - dimming a soft-edged region also shrinks its effective support, and no normalisation fixes that.
  • intensity - the raw formulation, mass equals pixel value.
  • binary - ignores values and uses the silhouette, the closest thing to what the contour nodes measure. But every non-zero pixel counts as shape, so any background haze swallows the frame. On a mask this is fine; on a photo it usually isn't.

You get ten outputs, deliberately mirroring CV Shape Moments one-for-one so the two nodes are swappable downstream: centroids (N,1,2 - plugs into CV Draw Points and Draw Labels), mass (the m00 moment: sum of pixel values, not an area - unless you chose binary, where it is the non-zero pixel count), angles (major-axis orientation in degrees, [-90, 90), Y pointing down), eccentricities (0 = radially symmetric, →1 = a line), axes (major-axis segments, 2 standard deviations per side, ready for CV Draw Segments), hu_moments (7 log-scaled invariants), count, plus orientations, chirality and pose_confidence.

Those last three are the advanced ones and the tooltips are refreshingly blunt. orientations is a canonical 0–360 direction where the third-order moments tell head from tail - and it's meaningless when pose_confidence is near 0. chirality is the signed seventh Hu invariant, the one whose sign flips under reflection and which everyone else discards as too noisy - and on intensity fields its magnitudes are far smaller than on a hard silhouette (~1e-5 versus 0.02–0.05 for a glyph), so any downstream matcher's chirality gate needs loosening on this path.

Install

ComfyUI Manager, search comfyui_cv (repo bmad4ever/comfyui_cv). Or:

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv

Restart. Requirements: Python ≥ 3.12 plus a ComfyUI recent enough for the V3 node API - every node here is an io.ComfyNode with an io.Schema, and there's no NODE_CLASS_MAPPINGS to fall back on, so older builds just don't load the pack. Dependency:

pip install "opencv-contrib-python-headless~=5.0.0.93"

Pinned because the pack is curated against it, and contrib because the contrib submodules have to be present. All four OpenCV wheels share one site-packages/cv2, so a non-contrib install over a contrib one silently strips them and contrib nodes disappear from the menu with no error. tools/repair_opencv_contrib.py --check / --apply is included for that.

Practical notes

Feed it crops, not whole frames, if you're measuring regions of a busy scene - CV Crop by Masks and CV Crop by BBoxes exist for exactly that. A non-zero background is fatal for the intensity modes, and zero frames is a valid result rather than an error, so don't build error handling around "must return data."

And the pack-wide caveat, which the README states itself: heavy LLM assistance in development, test-driven overfitting risk in some pipelines, updates not planned, and not production-ready without your own review. The moments maths is textbook OpenCV; the failure modes above are the ones the node's own tooltips document, which is more guidance than most packs give you.

Categoryimage/CV/moments

Inputs (2)

NameTypeDefaultDescription
imagesNPARRAY,IMAGE,MASKFrames to measure, one row of output per frame. Reduced to a single channel (luminance for a colour picture) because cv2.moments rejects a 3-channel array. 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.
weightingCOMBOintensity, peak-normalizedWhich density function the moments integrate. 'intensity' is Hu's original formulation and cv2's default (mass = pixel value), so a shaded region is a different shape from a flat one. Hu moments are NOT invariant to a brightness gain, so 'peak-normalized' (divide by the frame's own maximum) is the default: it more than halved the error of a x0.5 exposure change in testing (I1 0.0394 raw -> 0.0146). It does NOT fully remove the effect - dimming also shrinks the effective support of a soft-edged region, and no normalization can fix that. 'binary' ignores the values and uses the silhouette, which is the closest thing to what the contour nodes measure - but beware, it treats EVERY non-zero pixel as part of the shape, so any background haze swallows the whole frame.

Outputs (10)

NameTypeDescription
centroidsNPARRAY(N,1,2) float32 centre of mass per frame - plugs into 'CV Draw Points' / 'Draw Labels'. This is the INTENSITY centroid, so shading moves it.
massNPARRAY(N,) float32 the m00 moment: the SUM OF THE PIXEL VALUES, not an area (with 'binary' weighting it is the count of non-zero pixels, which is an area).
anglesNPARRAY(N,) float32 orientation of the major axis in degrees, [-90, 90), from the +X axis with Y pointing DOWN. Meaningless for a radially symmetric distribution (eccentricity ~ 0).
eccentricitiesNPARRAY(N,) float32 elongation in 0-1: 0 = radially symmetric, -> 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, matching 'OpenCV Shape Moments'. Invariant to translation, scale and rotation of the intensity field - but NOT to a brightness change.
countINT—
orientationsNPARRAY(N,) float32 canonical orientation, 0-360, with the head told from the tail by the third-order moments (unlike 'angles', which is only defined mod 180). MEANINGLESS when pose_confidence is ~0.
chiralityNPARRAY(N,) float32 signed handedness from the 7th Hu invariant, the only one whose sign changes under reflection (CV HuMoments docs; Hu 1962) and the one usually discarded as too small and too noisy to trust - hence the sign(h7)*|h7|**0.25 rescaling. The sign flips under reflection and survives rotation and scale. NOTE the magnitudes here are far smaller than for a hard-edged silhouette - a smooth intensity field measures around 1e-5 where an 'F' glyph measures 0.02-0.05 - so the matcher's 'min_chirality' gate needs a much lower value on this path.
pose_confidenceNPARRAY(N,) float32 how well-defined 'orientations' is (the normalized third moment along the major axis). Judge it relative to the other frames in the batch, not against the silhouette thresholds quoted on 'OpenCV Shape Moments'.