CV Region Properties
Turn a mask into a spreadsheet
- mask
- image
- features
- labels
- names
- count
Masks are great for compositing and useless for reasoning. This node is the bridge: hand it a mask, get back one row per region with the columns you ticked - position, size, shape ratios, orientation, colour, Hu moments - plus a label map indexing the same regions. It's the difference between "there are blobs in here somewhere" and "region 4 is 900 px, elongated, oriented 37°".
The mask semantics, which are the part to read twice
A single mask gets split into its connected blobs (connectivity 8 or 4 - 8 also joins diagonals, and only applies to this path). A MASK batch - the usual output of a segmentation model - is treated as one region per frame, resized to frame 0's size. So the same node means two different things depending on what you feed it, and confusing them is the number one way to get a bizarre count.
The columns
properties is the interesting widget: a position choice plus independent toggles. In the UI it renders as a dropdown plus checkboxes; the underlying value is a pipe-joined string like no position | area, which is why CV Region Property Flags exists - it authors that string once and fans it out to several of these nodes.
Two things about it are worth knowing before you tick everything:
- The position choice is exclusive (a region has one place) and everything else is an independent toggle, so
no position | areameasures size alone. - Toggles aren't one column each. The orientation pairs,
centroid offset in bboxandmin-area rect centroid clearanceemit two columns each,mean colouremits three, andhu moments (log magnitude)emits seven (hu1…hu7). Tick the Hu moments and your feature matrix just got seven columns wider - which is either what you wanted (grouping regions by shape, ignoring position and size) or a surprise.
Columns come out in the order the options are listed, not the order you ticked them, and the names output spells the final list out. Wire that into a text preview and you can read the matrix instead of guessing which column is which.
Scale, which decides whether the features mean anything
scale has three settings: raw (pixels) keeps honest pixel units; image-relative divides lengths by frame size and areas by frame area, so numbers survive a resize; z-score centres each column and divides by its standard deviation.
That last one is not cosmetic. Anything distance-based downstream - k-means, kNN - is dominated by whichever column has the biggest numbers. Raw, an area in px² outweighs a centroid in px by thousands, so your clustering is really just clustering by area. If you're feeding features into cv2.kmeans, z-score exists precisely for that.
Outputs
features is (N, D) float32, one row per region in label order - straight into CV Train Classifier to learn "what kind of region is this", into a scatter chart to eyeball the distribution, or into k-means to group them.
labels is the [H,W] int32 map: 0 is background, and label i is the region described by row i-1. That off-by-one is documented and it's the reason this pairs so naturally with CV Reduce Array By Label (which indexes rows by label value) - average the colour of each region, then paint it back with CV Take By Index. names is the comma-separated column list, count is N.
Handy extras
min_area drops regions smaller than N pixels. Specks are what make a cluster count meaningless, so this is usually the first thing to set. on_empty decides what happens when the mask has no regions: empty table (0 rows) is the honest answer, but one all-zero row exists because cv2.kmeans asserts on an empty sample set - so a clustering graph that should keep running needs the padded row. count stays 0 either way, so the filler row is never mistaken for a measurement. image is only needed for the mean-colour / mean-intensity columns, at the same size as the mask (frame 0 of a batch).
Install
Part of ComfyUI CV (bmad4ever/comfyui_cv), GPL-3.0, forked from opencv-comfyui:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
# restart ComfyUI
Manager: search the pack title. Python ≥ 3.12, V3-node-API ComfyUI.
Where to see it work
workflows/24_kmeans_clusters.json groups mask components by position and size, and by shape alone - and workflows/84_measure_and_export.json glues per-region columns into one table and formats it as CSV/TSV/JSON for export. Together they're the argument for the node: measurements that used to require a Python node now come out of a graph as a table. Note the pack is tagged NC here - the geometry is implemented in the pack, so don't expect an OpenCV function of the same name to behave identically.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| mask | MASK | A single mask is split into its connected blobs; a MASK batch clusters the masks themselves (one region per frame, resized to frame 0's size). | |
| connectivity | COMBO | 8 | 8 also connects diagonal pixels; 4 only direct neighbours. Only used when splitting a single mask. |
| properties | STRING | centroid (x, y) | Which columns to emit. The position choice is exclusive (a region has one place); everything else is an independent toggle, so 'no position | area' measures size alone. Columns come out in the order listed here, whatever order you tick them in; 'names' spells the final list out. Most toggles emit ONE column, but the two orientation pairs, 'centroid offset in bbox' and 'min-area rect centroid clearance' emit two, 'mean colour' three and 'hu moments (log magnitude)' SEVEN - that last one groups regions by SHAPE alone. Each toggle carries its own tooltip. |
| scale | COMBO | raw (pixels) | raw keeps pixel units - the honest measurement. image-relative divides lengths by the frame size and areas by its area, so the numbers survive a resize. z-score centres each column and divides by its standard deviation, which is what a distance-based consumer (k-means, kNN) needs: raw, an area in px^2 outweighs a centroid in px by thousands. |
| min_areaopt | INT | 00–2147483647 | Regions smaller than this many pixels are dropped (0 keeps every region). Specks are what make a cluster count meaningless. |
| on_emptyopt | COMBO | empty table (0 rows) | What to emit when the mask has no regions at all. The empty table is the honest answer, but a consumer that cannot take 0 rows - cv2.kmeans asserts on an empty sample set - needs the padded row instead. 'count' stays 0 either way, so the row is never mistaken for a measurement. |
| imageopt | IMAGE | Only needed for the 'mean colour' / 'mean intensity' columns: the picture the mask belongs to, at the same size. Frame 0 of a batch. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| features | NPARRAY | (N, D) float32, one row per region in label order. N = 0 for an empty mask. |
| labels | NPARRAY | [H,W] int32 label map: 0 = background, i = the region described by row i-1. Feed it to 'CV Take By Index' with a per-region table, or to 'OpenCV Labels to Masks' to get the regions back. |
| names | STRING | The column names, comma separated - wire it into 'Preview as Text' to read the matrix. |
| count | INT | N, the number of regions measured. |