CV Reduce Array By Label
One row per region — the flat-poster trick, and much more
- values
- labels
- mask
- table
- counts
- count
You have an image and you have a label map: superpixels, watershed, connected components, HFS segmentation, k-means. What you don't have is a way to say "what colour is region 47". This node collapses everything sharing a label into one row - mean, median, sum, min or max - and the row order is the label value. That last detail is what makes the obvious trick work: paint the table back through the label map and the image goes flat, region by region, like a poster print.
The round trip, concretely
values is your array - (N,), (N,C), [H,W] or [H,W,C], and its leading axes have to match labels. labels is [H,W] int (or (N,) for per-element values); negative labels are excluded, which is exactly what you want because cv2.watershed writes -1 along the boundaries between regions. reduction is mean for the palette case - the average colour of each region. median if a region straddles an edge and you want the robust version.
Out comes table, (K, C) float32, row i = reduction over label i. Feed that into CV Take By Index with the same label map and you've replaced every pixel with its region's average. Now nudge the table - brighten every row, quantise it, sort the regions - and the image follows. That's non-photorealistic rendering without a model, a stable diffusion pass, or a seed lottery.
counts is (K,) int32 telling you how many elements carried each label; a 0 there marks a row that's filler rather than measurement. count is just K.
The optional inputs earn their keep
num_labels at 0 derives the row count from the largest label present, which is fine until a region comes out empty - then your table is short and everything downstream is misaligned. Wire the count output of CV Superpixels (or whatever produced the labels) here to pin the height, and labels at or above K get ignored. That alignment matters enormously if you're feeding the table into k-means or a classifier later.
empty_value (0 by default) fills rows with no members. Zero paints such a region black - sometimes exactly right, sometimes a bold flag that a region vanished.
mask is an inclusion mask at the label resolution: nonzero elements take part, zeros don't. Drop the sky before averaging a landscape, or drop a shadow before measuring a surface.
Where it fits
Three uses, in ascending order of how much they surprise people:
- Region palette / flat render. As above, with
CV Superpixels(workflows/24_kmeans_clusters.jsonclusters the portrait's own pixel colours and paints the centres back, which is this pattern one step removed). - Region statistics into ML. Average colour, average Fourier magnitude, average whatever per label becomes a feature row per region - then cluster or classify the regions themselves.
CV Region Propertiesis the more complete version of this idea for shape features; this node is for anything array-shaped. - Counting and measuring.
sumover an all-ones input counts members, and thecountsoutput already does that for you.meanover a depth map, sampled by a segmentation mask, gives you the mean depth per object - no learning involved.
Install
Ships in ComfyUI CV (bmad4ever/comfyui_cv), GPL-3.0, a fork of 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 users: search the pack title. Needs Python ≥ 3.12 and a ComfyUI on the V3 node API.
Gotchas
Leading axes, not shapes. The tooltip says it and it still bites people: values can be [H,W,3] against an [H,W] label map (reduce colour per pixel), or (N,) against (N,) labels. What it can't be is N mismatched - that raises, and that's a wiring bug rather than bad data.
Negative labels silently vanish, which is correct for watershed boundaries and a surprise if you generated labels with a −1 sentinel of your own meaning. Fix the labels upstream.
table is always 2-D. Single-channel input gives you (K,1), not (K,). Harmless, and worth knowing before you reshape it.
The pack's docs are unambiguous that this is one of the nodes implementing its own logic rather than calling a cv2 function - the reduction is numpy. Fine for how it's used; just don't expect the OpenCV docs to describe it.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| values | NPARRAY | Array to reduce: (N,), (N,C), [H,W] or [H,W,C]. Its leading axes must match 'labels' - the trailing axis is the channel axis and is reduced independently. | |
| labels | NPARRAY | Integer label per element: (N,) or [H,W], matching the leading axes of 'values'. NEGATIVE labels are excluded (watershed writes -1 on boundaries). | |
| reduction | COMBO | mean | How the members of each label are collapsed. mean is the region average (the palette case); median is the robust version when a region straddles an edge; sum with an all-ones input counts members (the 'counts' output does that for you). |
| num_labelsopt | INT | 00–2147483647 | Number of rows K to emit. 0 = derive it from the largest label present. Wire the 'count' output of 'CV Superpixels' here to pin the table height even when a region ends up empty; labels >= K are then ignored. |
| empty_valueopt | FLOAT | 0.00-1000000000000–1000000000000 | Value written for labels with no members (and for every row when the input is empty). 0 paints such a region black. |
| maskopt | NPARRAY | Optional inclusion mask, same shape as 'labels': nonzero elements take part in the reduction, zeros are ignored (e.g. drop the sky before averaging). |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| table | NPARRAY | (K,C) float32, row i = the reduction over label i. Always 2-D, so a single-channel input gives (K,1). Feed it to 'CV Take By Index' with the same label map to paint the result back over the image. |
| counts | NPARRAY | (K,) int32: how many elements carried each label. 0 marks a row that is 'empty_value', not a measurement. |
| count | INT | K, the number of rows in the table. |