Nodes/ComfyUI CV/CV NMS Boxes
ComfyUI Node

CV NMS Boxes

De-duplicate boxes from anything, not just YOLO

By bmad4ever·Created 4 months ago·Updated 14 days ago· 1
CV NMS Boxes
  • bboxes
  • scores
  • class_ids
  • bboxes
  • kept_indices
  • count
  • removed
◄nms_threshold0.45►
◄score_threshold0.00►
◄top_k0►
◄eta1.00►

Non-maximum suppression is the step that turns a detector's scatter of overlapping guesses into one box per object. In ComfyUI it's usually buried inside a decode node - you get it free with YOLO and nothing else. So the moment your boxes come from anywhere else, you're stuck: two detectors merged, a confidence threshold dropped so far that every object rings like a bell, CV Masks to BBoxes run over a noisy segmentation, a multi-scale template match returning the same object at four scales.

This node applies NMS to any BOUNDING_BOX value. It's the kind of utility that doesn't get written about and gets installed by everybody.

How it works

It flattens the boxes, pulls a score per box, and runs cv2.dnn.NMSBoxes (or NMSBoxesBatched when you wire class ids). Boxes overlapping a higher-scoring box by more than nms_threshold get dropped; eta is cv2's adaptive-threshold coefficient (leave it at 1.0 - the IoU threshold stays constant, which is what you want).

Two structural details matter more than the parameters:

  • Suppression runs inside each per-frame group, never across frames. So a batch of frames is treated as a batch of independent images, which is nearly always what you mean and would be a nasty bug the other way.
  • Wiring class_ids switches on per-class suppression. There's no boolean for it - that is the switch. With it wired, boxes of different classes never suppress each other, which is what you want when a person box and a face box legitimately overlap.

Inputs: bboxes (required), nms_threshold (default 0.45 - lower suppresses harder, 1.0 keeps everything), score_threshold (default 0.0, a pre-filter), and optional scores, class_ids, top_k, eta.

scores deserves a note. Omitted, each box's own score key is used, defaulting to 1.0 - and with no scores at all, every box ties, so NMS keeps the first box of each overlapping cluster. That's a footgun dressed as a default: it works, and it isn't a ranking. CV BBoxes To Array emits a flattened score array that fits this input exactly, and a matching label_ids for class_ids.

Outputs: bboxes (the survivors, keeping the original per-frame nesting and every extra key on the boxes), kept_indices (int32 indices into the flattened input order - wire to CV Take By Index to keep parallel arrays like masks or areas in step), count, and removed. removed = 0 means NMS changed nothing, which is a quick way to confirm it's actually running.

Install

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

Manager → ComfyUI CV (bmad4ever). Then restart ComfyUI and reload the browser tab. Requires Python ≥ 3.12 and a ComfyUI that supports the V3 node API - the whole pack builds its schemas with it. Single dependency: opencv-contrib-python-headless~=5.0.0.93. No models.

Common issues

Choosing nms_threshold by vibes. The pack ships CV Box IoU Matrix for exactly this - look at the real overlaps between your boxes and set a threshold that separates "same object" from "adjacent object". For faces in a crowd the right value is surprisingly high; for a detector that fires three boxes per object, low.

Everything suppressed. With no scores wired, the tie-breaking is arbitrary-but-stable (first box wins), so a wide-association bug looks like a good result. Wire real scores.

Boxes that need to overlap and can't. A person and their own face box, a car and its wheel. Wire class_ids - per-class suppression is the fix, and the fact that it's an input rather than a widget is the kind of thing that costs people twenty minutes.

Empty input, empty output, no exception. By design, like the rest of this pack. If you were relying on NMS to error when something upstream broke, gate on count instead.

For some context on where this fits: in the standard detect → mask → crop → refine loop, this is a hygiene step at the front, cleaning detections before anything expensive happens. It's cheap, deterministic, and one of maybe five nodes in this pack you'd keep installed even if you never touch the 3-D or calibration side.

Categoryimage/CV

Inputs (7)

NameTypeDefaultDescription
bboxesBOUNDING_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. Boxes are suppressed within each per-frame group independently.
nms_thresholdFLOAT0.450–1Maximum IoU a kept box may have with a higher-scoring one. Lower suppresses more aggressively; 1.0 keeps everything. Measure the actual overlaps with 'CV Box IoU Matrix' if you are unsure what to set.
score_thresholdFLOAT0.000–1Boxes scoring below this are dropped before suppression. 0.0 keeps them all.
scoresoptNPARRAYOptional (N,) confidences, in the flattened box order ('CV BBoxes To Array' emits exactly this). Omitted, each box's own 'score' key is used, defaulting to 1.0 - with no scores at all NMS keeps the first box of each overlapping cluster.
class_idsoptNPARRAYOptional (N,) class indices. WIRED, suppression runs per class (boxes of different classes never suppress each other) - that is the switch, there is no boolean widget. 'CV BBoxes To Array' emits 'label_ids' for this.
top_koptINT00–100000Keep at most this many boxes per group after suppression (0 = no limit), highest score first.
etaoptFLOAT1.000.01–10Adaptive-threshold coefficient of cv2's NMS: the IoU threshold is multiplied by this after each iteration. 1.0 (the default) keeps it constant.

Outputs (4)

NameTypeDescription
bboxesBOUNDING_BOXCore BOUNDING_BOX data: per-frame lists of {x, y, width, height} dicts - compatible with Draw BBoxes, Crop By Bounding Boxes, Image Crop, etc. The survivors, keeping the original per-frame nesting and every extra key.
kept_indicesNPARRAY(K,) int32 indices of the survivors into the FLATTENED input order - feed it to 'CV Take By Index' to keep any parallel array (masks, embeddings, areas) in step.
countINTNumber of boxes kept.
removedINTHow many boxes were dropped (suppressed or below 'score_threshold'). 0 means NMS changed nothing.