In object detection, accurately determining how well a predicted bounding box aligns with the ground truth is essential. This alignment is quantified using the Intersection over Union (IoU), a metric derived from set theory. IoU measures the overlap between two regions by dividing the area of their intersection by the area of their union:
$$ \text{IoU} = \frac{\text{Area of Intersection}}{\text{Area of Union}} $$
For two rectangular bounding boxes, each defined by their corner coordinates (x₁, y₁, x₂, y₂), the intersection area is computed by finding the overlapping region’s boundaries:
- Left edge: max(x₁₁, x₂₁)
- Top edge: max(y₁₁, y₂₁)
- Right edge: min(x₁₂, x₂₂)
- Bottom edge: min(y₁₂, y₂₂)
If the right edge is less than the left edge, or the bottom edge is less than the top edge, the boxes do not overlap, and the intersection area is zero.
The following implementation accepts bounding boxes in xyxy format:
import numpy as np
def compute_iou_xyxy(box_a, box_b):
x1_a, y1_a, x2_a, y2_a = box_a
x1_b, y1_b, x2_b, y2_b = box_b
# Calculate areas of both boxes
area_a = (x2_a - x1_a + 1) * (y2_a - y1_a + 1)
area_b = (x2_b - x1_b + 1) * (y2_b - y1_b + 1)
# Determine intersection coordinates
inter_x1 = max(x1_a, x1_b)
inter_y1 = max(y1_a, y1_b)
inter_x2 = min(x2_a, x2_b)
inter_y2 = min(y2_a, y2_b)
# Compute intersection area
inter_width = max(0, inter_x2 - inter_x1 + 1)
inter_height = max(0, inter_y2 - inter_y1 + 1)
intersection = inter_width * inter_height
# Compute union area
union = area_a + area_b - intersection
return intersection / union if union > 0 else 0.0
# Example usage
box1 = [100, 100, 200, 200]
box2 = [120, 120, 220, 220]
iou_value = compute_iou_xyxy(box1, box2)
print(f"IoU: {iou_value:.6f}") # Output: IoU: 0.474026
For boxes defined in xywh format (center x, center y, width, height), the corners are first derived before computing IoU:
def compute_iou_xywh(box_a, box_b):
cx_a, cy_a, w_a, h_a = box_a
cx_b, cy_b, w_b, h_b = box_b
# Convert to xyxy format
x1_a, y1_a = cx_a - w_a / 2, cy_a - h_a / 2
x2_a, y2_a = cx_a + w_a / 2, cy_a + h_a / 2
x1_b, y1_b = cx_b - w_b / 2, cy_b - h_b / 2
x2_b, y2_b = cx_b + w_b / 2, cy_b + h_b / 2
area_a = w_a * h_a
area_b = w_b * h_b
inter_x1 = max(x1_a, x1_b)
inter_y1 = max(y1_a, y1_b)
inter_x2 = min(x2_a, x2_b)
inter_y2 = min(y2_a, y2_b)
inter_width = max(0, inter_x2 - inter_x1)
inter_height = max(0, inter_y2 - inter_y1)
intersection = inter_width * inter_height
union = area_a + area_b - intersection
return intersection / union if union > 0 else 0.0
# Example usage
box1 = [100, 100, 200, 200]
box2 = [120, 120, 220, 220]
iou_value = compute_iou_xywh(box1, box2)
print(f"IoU: {iou_value:.6f}") # Output: IoU: 0.690249
The IoU value ranges from 0 to 1:
- IoU = 1: The two boxes are perfectly aligned (identical in position and size).
- IoU = 0: The boxes do not overlap at all.
In real-world detection scenarios, a single object often triggers multiple overlapping predictions. To eliminate redundancy, Non-Maximum Suppression (NMS) is applied. NMS retains only the highest-scoring prediction per object and suppresses others with significant overlap.
The algorithm proceeds as follows:
- Sort all predicted boxes by confidence score in descending order.
- Select the box with the highest score and add it to the final results.
- Compute IoU between this box and all remaining boxes.
- Discard any box with IoU exceeding a predefined threshold (e.g., 0.5 in YOLOv3).
- Repeat untill all boxes are processed.
Here is a clean implementation of NMS:
def non_maximum_suppression(bounding_boxes, confidence_scores, score_threshold=0.01, iou_threshold=0.5):
# Sort indices by confidence score in descending order
sorted_indices = np.argsort(confidence_scores)[::-1]
selected = []
while len(sorted_indices) > 0:
current_idx = sorted_indices[0]
current_score = confidence_scores[current_idx]
# Skip if below confidence threshold
if current_score < score_threshold:
break
# Keep the current box
selected.append(current_idx)
# Compute IoU with all remaining boxes
remaining_indices = sorted_indices[1:]
if len(remaining_indices) == 0:
break
ious = np.array([
compute_iou_xyxy(bounding_boxes[current_idx], bounding_boxes[idx])
for idx in remaining_indices
])
# Filter out boxes with IoU above threshold
keep_mask = ious < iou_threshold
sorted_indices = remaining_indices[keep_mask]
return np.array(selected)
Applying NMS to a set of 11 detection proposals with associated confidence scores reduces redundancy, retaining only the most confident, non-overlapping predictions. This results in a clean set of final bounding boxes, each corresponding to a distinct object instance.