Non-Maximum Suppression

An object detector scanning an image with sliding windows or anchor boxes rarely fires just once per object. A car in a street scene might trigger a hundred overlapping bounding boxes. Each claims "car" with a slightly different position, scale, and confidence score. Left unfiltered, a detector's raw output would report a hundred cars where one exists. This makes downstream tasks like counting, tracking, or drawing clean bounding boxes impossible. Non-Maximum Suppression (NMS) is the classical, deceptively simple algorithm that solves this. It turns this redundant haystack of candidate boxes into a clean set of one-box-per-object detections. NMS matters because it sits at the very last stage of nearly every object detection pipeline ever built. This spans from the sliding-window detectors of the 2000s through the two-stage and single-shot deep networks that dominate the field today. Whether a system uses region proposals or the dense anchor grids of single-shot detectors, the raw output is always the same. It is an over-complete set of overlapping boxes with confidence scores. NMS is almost universally the post-processing step that cleans it up. Despite decades of architectural revolutions in how boxes and scores are produced, the suppression step itself has remained remarkably persistent. Only modest refinements have displaced the original 1970s-era idea. The big picture is a greedy, rank-then-eliminate procedure. Sort candidate boxes by confidence, then repeatedly accept the best remaining box. Discard any box that overlaps it "too much," measured by Intersection-over-Union (IoU). This article explains the mechanics precisely. It proves why the greedy strategy is both efficient and, under natural assumptions, essentially the right thing to do. It walks through a full numeric trace. It also surveys the pitfalls that trip up nearly every from-scratch implementation, particularly the classic bug of comparing the wrong pair of boxes. By the end you will be able to implement NMS correctly from bare pseudocode. You will be able to reason about its O(n2)O(n^2) worst-case cost and where that cost comes from. You will be able to diagnose why NMS sometimes deletes a genuinely separate object standing next to another one. You will also understand why modern detectors still ship with NMS, or a differentiable relative of it, baked into their inference code.

Key takeaways
  • NMS removes duplicate detections of the same object by keeping the highest-confidence box and suppressing others that overlap it heavily.
  • Overlap is measured with Intersection-over-Union (IoU); boxes with IoU above a threshold (commonly 0.5) are considered duplicates.
  • The algorithm is greedy: sort by score, repeatedly pop the top box, suppress its high-IoU neighbors, and repeat on what remains.
  • Naive implementation is O(n2)O(n^2) in the number of boxes per class; sorting adds O(nlog⁡n)O(n \log n), which is dominated by the pairwise comparisons.
  • A single fixed IoU threshold is a known weak point: it can wrongly delete adjacent true objects or fail to remove near-duplicate boxes, motivating variants like Soft-NMS.
  • NMS is not learned and not (by default) differentiable, which is why some modern pipelines replace or relax it during end-to-end training.
The core insight

Non-Maximum Suppression greedily keeps the highest-confidence detection in each cluster of overlapping boxes and discards the rest, using IoU as the similarity measure that defines "the same object."

The idea

A detection is a tuple (b,s,c)(b, s, c): a bounding box bb, a confidence score s∈[0,1]s \in [0,1], and (in a multi-class setting) a class label cc. A raw detector output is a large set D={d1,…,dn}D = \{d_1, \dots, d_n\} of such tuples, typically thousands per image before filtering, because dense anchor grids or sliding windows generate many boxes near every true object. The goal of NMS is to reduce DD to a much smaller set D∗D^* that contains, ideally, exactly one box per real object, discarding the near-duplicates. The key notion of "near-duplicate" is Intersection-over-Union, also called the Jaccard index applied to boxes:

IoU(b1,b2)=area(b1∩b2)area(b1∪b2)\mathrm{IoU}(b_1, b_2) = \frac{\mathrm{area}(b_1 \cap b_2)}{\mathrm{area}(b_1 \cup b_2)}

IoU is 11 for identical boxes, 00 for disjoint boxes, and smoothly interpolates in between. It is scale-invariant (doubling both boxes' sizes proportionally does not change IoU) and symmetric, which makes it a natural similarity measure independent of object size. NMS declares two boxes "duplicates of the same object" when IoU(b1,b2)\mathrm{IoU}(b_1, b_2) exceeds a chosen threshold τ\tau (commonly 0.30.3–0.70.7 depending on the dataset and task). The algorithm itself is a greedy set cover by confidence rank: always trust the most confident remaining box as evidence of a real object, then remove everything that overlaps it enough to plausibly be describing the same object. This is applied independently per class in multi-class detection, since two boxes of different classes (say "person" and "skateboard") are allowed to overlap heavily without being duplicates of each other.

How it works

NMS operates on one class at a time (run it once per class, or once globally if class-agnostic suppression is desired). The steps are: 1. Collect all detections for the class, each with a box and a score. 2. Sort them by descending confidence score. 3. Take the highest-scoring box, add it to the output (kept) list, and remove it from the pending list. 4. Compute IoU between this kept box and every remaining pending box. 5. Discard (suppress) any pending box whose IoU with the kept box exceeds threshold τ\tau. 6. Repeat from step 3 with whatever remains, until the pending list is empty. ``` function NMS(boxes, scores, iou_threshold):

# boxes: list of n bounding boxes (x1, y1, x2, y2)

# scores: list of n confidence scores, same order as boxes

# returns: indices of boxes to KEEP order = argsort(scores, descending=True) # O(n log n) keep = [] while order is not empty: i = order[0] # index of current highest-scoring box keep.append(i) rest = order[1:] survivors = [] for j in rest: if IoU(boxes[i], boxes[j]) <= iou_threshold: survivors.append(j) # box j is NOT a duplicate of i, keep pending

# else: box j overlaps too much with i, suppress it permanently order = survivors return keep

function IoU(boxA, boxB): xi1 = max(boxA.x1, boxB.x1) yi1 = max(boxA.y1, boxB.y1) xi2 = min(boxA.x2, boxB.x2) yi2 = min(boxA.y2, boxB.y2) inter_w = max(0, xi2 - xi1) inter_h = max(0, yi2 - yi1) intersection = inter_w inter_h areaA = (boxA.x2 - boxA.x1) (boxA.y2 - boxA.y1) areaB = (boxB.x2 - boxB.x1) * (boxB.y2 - boxB.y1) union = areaA + areaB - intersection if union <= 0: return 0.0 return intersection / union ``` Two implementation details deserve emphasis because they are frequent sources of bugs. First, IoU must be computed against the just-kept box, never against a box that has already been suppressed — a suppressed box carries no further authority. Second, the clamp `max(0, ...)` on intersection width and height is essential: when boxes do not overlap, `xi2 - xi1` or `yi2 - yi1` can be negative, and without clamping, "negative area" would corrupt the intersection computation. For multi-class detection, the whole procedure is wrapped in a loop over classes, and for large box counts, a per-class limit (e.g., only the top 1000 boxes by score) is applied before sorting to control cost. ``` function MultiClassNMS(all_boxes, all_scores, all_classes, iou_threshold): final_keep = [] for each class c in distinct(all_classes): idx = indices where all_classes == c kept_local = NMS(all_boxes[idx], all_scores[idx], iou_threshold) final_keep.extend(idx[kept_local]) return final_keep ```

Figure
Greedy suppression of overlapping detections around a single car
Step 1: raw detections (sorted by score) A s=0.95 B s=0.88 C s=0.81 D s=0.77 A, B, C heavily overlap (same car). D is a separate object (different region). Step 2: keep A (top score), suppress B, C (IoU > τ); D survives (IoU ≈ 0) KEEP: A KEEP: D B, C removed

Complexity & correctness

Time complexity. Let nn be the number of candidate boxes for a given class. Sorting by score costs O(nlog⁡n)O(n \log n). The main loop, in the worst case, keeps one box per iteration while comparing it against all remaining boxes: iteration kk compares against roughly n−kn-k boxes, giving

∑k=1nO(n−k)=O(n2)\sum_{k=1}^{n} O(n-k) = O(n^2)

in the worst case, which occurs when almost no boxes overlap (nothing gets suppressed early, so the pending list shrinks by only one per round). In the best case — when the first box suppresses nearly everything — the loop terminates in O(n)O(n) comparisons after one round. In practice, detectors cap the number of pre-NMS boxes per class (e.g., top-kk by score, k≈1000k \approx 1000) precisely to bound this worst case, since raw anchor counts can reach 10410^4–10510^5 per image. Overall cost is therefore O(nlog⁡n+n2)O(n \log n + n^2), dominated by the quadratic term for large nn; with a top-kk cap it becomes O(nlog⁡n+k2)O(n \log n + k^2).

Space complexity. O(n)O(n) to store boxes, scores, and the sorted order; no auxiliary structure grows the space beyond this, since IoU is computed on the fly.

Correctness argument. NMS is not solving an explicitly stated optimization problem with a proven-optimal solution — it is a heuristic — but its greedy design is justifiable via an exchange argument under a natural model of detector behavior. Assume: (1) every true object generates a cluster of detections whose pairwise IoUs (and IoU with the true box) are high, (2) detections from different objects have IoU near 00 unless the objects are physically close and heavily occluding each other, and (3) within a cluster, the detector's highest-confidence box is (probabilistically) the best localization of the true object. Under these assumptions, consider any cluster of detections belonging to one object. Because IoU within the cluster exceeds τ\tau by assumption (1), once the highest-scoring member of the cluster is chosen as "kept" (which happens because sorting processes it before any weaker member of the same cluster, and no earlier unrelated kept box can suppress it if assumption (2) holds), every other member of the cluster has IoU >τ> \tau with it and is therefore suppressed in that same round. Hence each cluster contributes exactly one kept box, and it is the best-localized one by assumption (3). Boxes belonging to different, well-separated objects have IoU near 00 with each other, so assumption (2) guarantees they never suppress one another, and each survives to be kept in its own right. The two failure modes of the algorithm map exactly onto violations of these assumptions: if two true objects are close enough that assumption (2) fails (crowded scenes, occlusion), NMS can wrongly suppress a correct detection of the second object because its IoU with the first object's box exceeds τ\tau. If assumption (1) fails — the detector produces two disjoint clusters for what is truly one irregularly-shaped or elongated object — NMS can leave duplicate boxes standing. These are inherent to the hard IoU-threshold rule, not implementation bugs, and they motivate the soft variants discussed below.

Honest tradeoff. NMS trades a small, bounded risk of over-suppression (crowded scenes) for a large, reliable reduction in duplicate detections, and it does so with no learned parameters beyond the single threshold τ\tau, making it cheap, interpretable, and dataset-agnostic — but also brittle to τ\tau's exact value and non-differentiable, which complicates end-to-end training of detection heads.

Worked example

Suppose a detector outputs five boxes for the class "car" in one image, given as (x1,y1,x2,y2)(x_1, y_1, x_2, y_2) with confidence score, and we run NMS with τ=0.5\tau = 0.5.

BoxCoordinatesScore
A(100, 100, 260, 200)0.95
B(120, 110, 270, 205)0.88
C(95, 90, 250, 190)0.81
D(400, 100, 550, 200)0.77
E(410, 105, 555, 205)0.65

Round 1. Sorted order by score: A, B, C, D, E. Pop A (score 0.95), add to `keep`. Compute IoU(A, X) for remaining boxes:

  • IoU(A, B): intersection xx-overlap =min⁡(260,270)−max⁡(100,120)=140= \min(260,270)-\max(100,120)=140, yy-overlap =min⁡(200,205)−max⁡(100,110)=90=\min(200,205)-\max(100,110)=90, intersection =12,600=12{,}600. Area(A) =160×100=16,000=160\times100=16{,}000; Area(B) =150×95=14,250=150\times95=14{,}250. Union =16,000+14,250−12,600=17,650=16{,}000+14{,}250-12{,}600=17{,}650. IoU =12,600/17,650≈0.714=12{,}600/17{,}650 \approx 0.714. This exceeds τ=0.5\tau=0.5 → suppress B.
  • IoU(A, C): xx-overlap =min⁡(260,250)−max⁡(100,95)=155=\min(260,250)-\max(100,95)=155, yy-overlap =min⁡(200,190)−max⁡(100,90)=90=\min(200,190)-\max(100,90)=90, intersection =13,950=13{,}950. Area(C) =155×100=15,500=155\times100=15{,}500. Union =16,000+15,500−13,950=17,550=16{,}000+15{,}500-13{,}950=17{,}550. IoU =13,950/17,550≈0.795=13{,}950/17{,}550\approx 0.795 → suppress C.
  • IoU(A, D): boxes occupy disjoint xx-ranges (260<400260 < 400) → intersection =0=0 → IoU =0=0 → D survives.
  • IoU(A, E): disjoint xx-ranges similarly → IoU =0=0 → E survives.

Pending list after round 1: `{D, E}`. Kept so far: `{A}`.

Round 2. Pop D (score 0.77, the top of what remains), add to `keep`.

  • IoU(D, E): xx-overlap =min⁡(550,555)−max⁡(400,410)=140=\min(550,555)-\max(400,410)=140, yy-overlap =min⁡(200,205)−max⁡(100,105)=95=\min(200,205)-\max(100,105)=95, intersection =13,300=13{,}300. Area(D)=150×100=15,000=150\times100=15{,}000; Area(E)=145×100=14,500=145\times100=14{,}500. Union =15,000+14,500−13,300=16,200=15{,}000+14{,}500-13{,}300=16{,}200. IoU =13,300/16,200≈0.821=13{,}300/16{,}200\approx0.821 → exceeds τ\tau → suppress E.

Pending list empty. Final `keep = {A, D}` — exactly the two real cars, correctly recovered from five raw boxes, matching the two well-separated clusters {A, B, C} and {D, E}.

A harder case (crowded scene). Now suppose box D actually belongs to a second, distinct car standing close to the first car, but a detector artifact makes IoU(A, D) =0.55= 0.55 instead of 00 (e.g., the two cars visually overlap due to camera angle). With τ=0.5\tau = 0.5, D would be wrongly suppressed by A in round 1, and the detector would report only one car where two exist. This is the textbook failure mode of hard-threshold NMS in crowded or occluded scenes, and it is exactly why Soft-NMS (which decays D's score rather than deleting it outright) or class-specific/learned suppression is used in pedestrian-detection and crowd-counting pipelines.

Common pitfalls & traps

PitfallWhy it happensFix
Comparing suppressed boxes to each otherIterating over the original list instead of the shrinking pending listOnly compare the newly kept box against boxes still pending; never resurrect a suppressed box
Negative intersection areaForgetting to clamp `xi2 - xi1` and `yi2 - yi1` at zero when boxes don't overlapAlways `max(0, ...)` before multiplying width and height
Running NMS across classes togetherSuppresses a correct "cat" box because it overlaps a "dog" box at the same locationPartition detections by class (or run class-agnostic NMS only when intentional)
Fixed IoU threshold tuned on one datasetτ=0.5\tau=0.5 works for sparse scenes but destroys detections in crowds (pedestrians, cell counting)Validate τ\tau per domain; consider Soft-NMS or DIoU-NMS for dense scenes
Treating NMS as differentiableHard argmax/threshold breaks gradient flow, silently stalling end-to-end training experimentsUse surrogate losses during training (e.g., IoU loss on matched boxes) and reserve NMS for inference only
Ignoring score calibration across classesIn multi-class NMS run jointly, a well-calibrated class's boxes are systematically out-ranked by an overconfident classCalibrate per-class score distributions, or keep NMS strictly per-class

Pioneers & history

Non-Maximum Suppression predates deep learning by decades: it originates in classical edge detection and template matching, where multiple pixel-level responses cluster around a single true edge or feature, and the same greedy peak-picking logic is needed to collapse them. The bounding-box version used in modern object detection is a direct descendant of this signal-processing idea, popularized in vision through the Viola–Jones face detector and later formalized as a standard component of the R-CNN line of detectors and every subsequent single-shot architecture.

Origins · 1 minJohn Canny and how this was discovered
JC
John Canny
1958– · Computer vision and edge detection

Canny formalized non-maximum suppression as a rigorous step within his edge-detection framework, using it to thin gradient-magnitude ridges down to single-pixel-wide edges by keeping only local maxima along the gradient direction. This precise, criterion-driven use of "suppress everything but the local peak" became the conceptual ancestor of the bounding-box NMS used throughout modern object detection.

History
- 1986 — John Canny formalizes non-maximum suppression as a core step of the Canny edge detector, thinning gradient ridges to single-pixel edges.
- 2001 — Viola and Jones popularize cascade-based sliding-window detection, whose overlapping window outputs require a suppression step.
- 2014 — R-CNN and successors adopt greedy box-level NMS with IoU thresholding as a standard post-processing stage for CNN-based detectors.
- 2017 — Soft-NMS is introduced, replacing hard box deletion with a continuous score-decay function to address crowded-scene failures.
- 2019 onward — Learned and differentiable alternatives (e.g., learned NMS networks, set-prediction detectors that avoid NMS entirely) emerge alongside continued dominance of classical NMS in deployed systems.

Where this matters

Every production object detector that outputs bounding boxes — from smartphone camera face detection, to warehouse robotics picking systems, to autonomous-vehicle perception stacks, to video surveillance analytics — runs some form of NMS as the final inference step before boxes reach a downstream consumer. Frameworks such as torchvision, Detectron2, the TensorFlow Object Detection API, and YOLO's reference implementations all ship a fused, hardware-accelerated NMS kernel, since it runs once per image on real-time video streams and its cost must stay negligible next to the network's forward pass. Medical imaging pipelines that detect tumors or cells, retail analytics that count shelf products, and traffic-monitoring systems that detect and track vehicles all depend on NMS to avoid double-counting. Its continued ubiquity, despite being older and simpler than nearly everything else in a modern detection pipeline, is a reminder that a correct, cheap, interpretable post-processing step can outlive many generations of the learned models that feed it.

Ask the tutor: How would you modify the pseudocode above to implement Soft-NMS with a Gaussian score-decay penalty?

Why not just keep the single highest-scoring box per image and discard all others?

Because a typical image contains many objects, not one. NMS must operate per cluster of overlapping boxes, not globally: it keeps the top-scoring box within each group of mutually overlapping detections, then moves on to the next unclaimed cluster and repeats. Keeping only the single globally highest-scoring box across the whole image would discard every other genuine object in the scene. The per-class, per-cluster design is exactly what lets NMS output one box per real object rather than one box per image.

What IoU threshold should I use, and why isn't there one universal value?

Common choices are 0.3, 0.5, or 0.7, but the "right" value depends on how tightly objects can cluster in your domain. A high threshold (e.g., 0.7) suppresses only near-identical boxes, preserving closely spaced distinct objects at the cost of leaving more duplicates behind. A low threshold (e.g., 0.3) aggressively merges anything even moderately overlapping, risking deletion of true adjacent objects in crowded scenes like pedestrian crowds or cell micrographs. Benchmarks like COCO typically evaluate detectors across a range of IoU thresholds precisely because no single value is universally correct.

What is Soft-NMS and how does it fix the crowded-scene problem?

Soft-NMS replaces the hard "delete if IoU > τ" rule with a continuous decay: a nearby box's score is multiplied by a function that decreases smoothly as its IoU with the kept box increases, rather than being zeroed out abruptly. A Gaussian-weighted decay is common: sj←sj⋅e−IoU(i,j)2/σs_j \leftarrow s_j \cdot e^{-\mathrm{IoU}(i,j)^2/\sigma}. Boxes are never instantly discarded; they simply become less likely to survive a final score threshold. This preserves genuinely separate, heavily overlapping objects (e.g., people standing shoulder to shoulder) that hard NMS would have wrongly deleted, at the cost of slightly more tuning and computation.

Is NMS run once per image or once per class?

Almost always once per class, run independently on that class's subset of detections, then the per-class kept sets are concatenated into the final output. This is essential because two boxes of different classes are allowed to overlap heavily and legitimately (e.g., a "person" box and the "surfboard" box they are holding), and running NMS across classes together would wrongly delete one of them. Some systems additionally support a class-agnostic mode for cases where suppressing across classes is actually desired, such as merging redundant "vehicle" subtype predictions.

Does NMS guarantee exactly one box per true object?

No — it is a heuristic, not a guarantee, and its correctness rests on the assumption that same-object detections have high mutual IoU while different-object detections have low mutual IoU. When that assumption holds (well-separated, roughly convex objects), NMS reliably produces one box per object. When it fails — heavily occluded crowds, elongated or L-shaped objects producing disjoint detection clusters — NMS can either wrongly merge two objects into one kept box or wrongly leave duplicate boxes for one object. These known limits are why Soft-NMS and learned alternatives exist.

How does NMS interact with training versus inference in a modern detector?

NMS is applied only at inference time, after the network has produced its raw box and score predictions; during training, the loss functions (classification loss plus a box-regression loss such as IoU loss) operate on the full, unsuppressed set of predictions matched to ground truth. Because NMS's sort-and-threshold logic is not differentiable in the usual sense, it plays no role in backpropagation. Some end-to-end architectures (e.g., set-prediction detectors) are explicitly designed to make NMS unnecessary at inference by training the network to directly output a de-duplicated set of boxes.

Why does the R-CNN family and single-shot detectors both still rely on NMS despite very different architectures?

Both families produce a dense, over-complete set of candidate boxes before final output — two-stage detectors from region proposals refined per class, single-shot detectors from a dense anchor or grid-cell prediction — and both therefore face the identical redundancy problem of multiple boxes per true object. NMS is architecture-agnostic: it only needs a list of (box, score) pairs, regardless of how they were produced, which is exactly why it survived essentially unchanged across the transition from two-stage region-based detectors to dense single-shot detectors.

Before this

Where this leads