CV Box IoU Matrix
The overlap matrix that scores and matches detections
- bboxes_a
- bboxes_b
- iou
- best_index
- best_iou
- count_a
- count_b
Intersection-over-union is the least glamorous useful number in computer vision, and this node computes all of them at once: every box in A against every box in B, as an (N, M) array. One pair of inputs, and suddenly four different jobs are the same job.
Scoring a detector against ground truth. Match predictions to annotations, threshold the IoU, count the hits. That's how you stop tuning a detector by vibes.
Frame-to-frame tracking. Boxes in frame t against boxes in frame t+1; best_index is your association, best_iou is your confidence in it. Cheap and surprisingly hard to beat when the camera is fixed and boxes don't jump.
De-duplicating two detectors. Run a fast detector and a careful one, then drop whatever they both found, or keep only the agreement. Feed the same value to both inputs and the diagonal is 1.0, which is the self-overlap case you'd want for near-duplicate suppression.
Keep-out regions. Find everything that overlaps the region you must not touch.
The pack has a CV Polygon IoU (convex) subgraph for polygons, but it does one pair at a time. This is the all-pairs box version.
Inputs and outputs
bboxes_a and bboxes_b are both core BOUNDING_BOX data - per-frame lists of {x, y, width, height} dicts, from any emitter, core or third-party. A becomes the rows, B becomes the columns, and that distinction only matters for reading best_index, which is indexed by A.
Four outputs: iou, an (N, M) float32 array in [0, 1] with 0.0 where boxes don't touch; best_index, (N,) int32 giving each A box the B box it overlaps most, -1 when it overlaps nothing at all; best_iou, (N,) float32, the classic thing to threshold for a matched/unmatched split; and count_a / count_b.
The -1 matters. It's a real distinction between "matched something badly" and "matched nothing", and if you treat 0.0 IoU as a match you'll silently glue unrelated boxes together.
The frame problem
Boxes are flattened across frames, so an (N, M) matrix from a 200-frame clip is going to be enormous, and row 47 doesn't tell you which frame it came from. For anything multi-frame, run CV BBoxes To Array first to get coordinates plus group_index, or run this per frame pair and keep the matrix small. In practice the useful call is one frame versus the next, not the whole clip versus itself.
Empty input yields an empty matrix and never raises.
Install
Manager → ComfyUI CV, or:
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, current ComfyUI (V3 node API). Pure numpy math - no OpenCV function under it, no models, nothing to download. It's one of the pack's genuinely curated compositions rather than a cv2.* wrapper.
Where people get burned
Coordinate spaces. IoU is meaningless across two different image sizes - a box in a 512px space against a box in a 1024px space gives you numerators and denominators from different worlds. Scale with CV Scale BBoxes first.
Threshold choice. 0.5 is the conventional detection threshold, 0.3 is common for tracking association where boxes legitimately shift, and near-duplicate suppression wants something high like 0.8. There's no universal value; the number you pick is the policy.
Inputs (2)
| Name | Type | Default | Description |
|---|---|---|---|
| bboxes_a | BOUNDING_BOX | [object Object] | Core BOUNDING_BOX data: per-frame lists of {x, y, width, height} dicts - compatible with Draw BBoxes, Crop By Bounding Boxes, Image Crop, etc. Becomes the ROWS of the matrix. |
| bboxes_b | BOUNDING_BOX | [object Object] | Core BOUNDING_BOX data: per-frame lists of {x, y, width, height} dicts - compatible with Draw BBoxes, Crop By Bounding Boxes, Image Crop, etc. Becomes the COLUMNS. Pass the same value on both sides to get the self-overlap matrix (its diagonal is 1.0). |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| iou | NPARRAY | (N,M) float32 in [0,1]: intersection area / union area for each pair. 0.0 where the boxes do not touch. |
| best_index | NPARRAY | (N,) int32: for each A box, the B box it overlaps most (-1 when it overlaps nothing at all). |
| best_iou | NPARRAY | (N,) float32: that best overlap. Threshold it to get the classic matched/unmatched split. |
| count_a | INT | Number of A boxes (rows). |
| count_b | INT | Number of B boxes (columns). |