---
title: "NMS Visualization"
subtitle: "LAB16: Computer Vision with YOLO"
---
## Non-Maximum Suppression Step-by-Step
Understand how NMS filters overlapping bounding boxes in object detection.
::: {.callout-note}
## Concept from LAB16
See **Section 16.2: YOLO Detection Pipeline** in the [PDF book](../downloads/Edge-Analytics-Lab-Book-v1.0.0.pdf).
:::
```{python}
#| label: fig-nms
#| fig-cap: "Non-Maximum Suppression visualization"
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
def iou(box1, box2):
"""Calculate Intersection over Union"""
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[0] + box1[2], box2[0] + box2[2])
y2 = min(box1[1] + box1[3], box2[1] + box2[3])
intersection = max(0, x2 - x1) * max(0, y2 - y1)
area1 = box1[2] * box1[3]
area2 = box2[2] * box2[3]
union = area1 + area2 - intersection
return intersection / union if union > 0 else 0
def nms(boxes, scores, iou_threshold=0.5):
"""Non-Maximum Suppression"""
indices = np.argsort(scores)[::-1] # Sort by confidence
keep = []
while len(indices) > 0:
current = indices[0]
keep.append(current)
remaining = []
for idx in indices[1:]:
if iou(boxes[current], boxes[idx]) < iou_threshold:
remaining.append(idx)
indices = remaining
return keep
# Sample detections (x, y, w, h)
boxes = np.array([
[50, 50, 100, 120], # High confidence
[55, 48, 95, 115], # Overlapping
[60, 52, 90, 110], # Overlapping
[200, 100, 80, 100], # Separate detection
[205, 105, 75, 95], # Overlapping with above
])
scores = np.array([0.95, 0.88, 0.82, 0.90, 0.75])
labels = ['person', 'person', 'person', 'dog', 'dog']
# Apply NMS
kept_indices = nms(boxes, scores, iou_threshold=0.5)
# Visualization
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
colors = plt.cm.Set1(np.linspace(0, 1, len(boxes)))
# Before NMS
ax = axes[0]
ax.set_xlim(0, 350)
ax.set_ylim(0, 300)
ax.set_aspect('equal')
ax.set_title(f'Before NMS\n({len(boxes)} boxes)')
for i, (box, score, label) in enumerate(zip(boxes, scores, labels)):
rect = patches.Rectangle((box[0], box[1]), box[2], box[3],
linewidth=2, edgecolor=colors[i],
facecolor='none', linestyle='--')
ax.add_patch(rect)
ax.text(box[0], box[1] - 5, f'{label}: {score:.2f}',
fontsize=8, color=colors[i])
ax.invert_yaxis()
# IoU Calculation
ax = axes[1]
ax.set_xlim(0, 350)
ax.set_ylim(0, 300)
ax.set_aspect('equal')
ax.set_title('IoU Calculation')
# Show IoU between first two boxes
box1, box2 = boxes[0], boxes[1]
rect1 = patches.Rectangle((box1[0], box1[1]), box1[2], box1[3],
linewidth=2, edgecolor='blue', facecolor='blue', alpha=0.3)
rect2 = patches.Rectangle((box2[0], box2[1]), box2[2], box2[3],
linewidth=2, edgecolor='red', facecolor='red', alpha=0.3)
ax.add_patch(rect1)
ax.add_patch(rect2)
# Intersection
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[0] + box1[2], box2[0] + box2[2])
y2 = min(box1[1] + box1[3], box2[1] + box2[3])
if x2 > x1 and y2 > y1:
intersection = patches.Rectangle((x1, y1), x2-x1, y2-y1,
linewidth=2, edgecolor='green',
facecolor='green', alpha=0.5)
ax.add_patch(intersection)
iou_val = iou(box1, box2)
ax.text(150, 250, f'IoU = {iou_val:.2f}', fontsize=12, ha='center')
ax.text(150, 270, 'Suppressed (IoU > 0.5)', fontsize=10, ha='center', color='red')
ax.invert_yaxis()
# After NMS
ax = axes[2]
ax.set_xlim(0, 350)
ax.set_ylim(0, 300)
ax.set_aspect('equal')
ax.set_title(f'After NMS\n({len(kept_indices)} boxes)')
for i in kept_indices:
box = boxes[i]
rect = patches.Rectangle((box[0], box[1]), box[2], box[3],
linewidth=3, edgecolor=colors[i],
facecolor=colors[i], alpha=0.3)
ax.add_patch(rect)
ax.text(box[0], box[1] - 5, f'{labels[i]}: {scores[i]:.2f}',
fontsize=10, color=colors[i], fontweight='bold')
ax.invert_yaxis()
plt.tight_layout()
plt.show()
```
## NMS Algorithm
```python
def nms(boxes, scores, iou_threshold):
# 1. Sort boxes by confidence (descending)
indices = argsort(scores, descending=True)
keep = []
while indices not empty:
# 2. Keep highest confidence box
best = indices[0]
keep.append(best)
# 3. Remove boxes with high IoU overlap
remaining = []
for idx in indices[1:]:
if IoU(boxes[best], boxes[idx]) < threshold:
remaining.append(idx)
indices = remaining
return keep
```
## IoU Threshold Effect
```{python}
#| label: fig-iou-threshold
#| fig-cap: "Effect of IoU threshold on NMS results"
thresholds = [0.3, 0.5, 0.7, 0.9]
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
for ax, thresh in zip(axes, thresholds):
kept = nms(boxes, scores, iou_threshold=thresh)
ax.set_xlim(0, 350)
ax.set_ylim(0, 300)
ax.set_aspect('equal')
ax.set_title(f'IoU threshold = {thresh}\n{len(kept)} boxes kept')
for i in kept:
box = boxes[i]
rect = patches.Rectangle((box[0], box[1]), box[2], box[3],
linewidth=2, edgecolor=colors[i],
facecolor=colors[i], alpha=0.3)
ax.add_patch(rect)
ax.invert_yaxis()
plt.tight_layout()
plt.show()
```
| IoU Threshold | Effect |
|---------------|--------|
| Low (0.3) | Aggressive suppression, may remove valid boxes |
| Medium (0.5) | **Standard choice**, good balance |
| High (0.7) | More boxes kept, may have duplicates |
## Related Sections in PDF Book
- Section 16.2: YOLO Detection Pipeline
- Section 16.3: Post-Processing
- Exercise 16.1: Implement NMS from scratch