2. IoU 计算详解:定义、方法与代码实现
聊 NMS 调优,绕不开一个核心指标——IoU(Intersection over Union)。
说白了,它就是两个框的「交集面积 ÷ 并集面积」。值越大,两个框重叠得越厉害。NMS 就是靠这个值来决定要不要干掉某个框的。
我见过不少新手,调 NMS 阈值调了半天没效果,回头一看,IoU 算错了。嗯,基础不牢,地动山摇。今天咱们就把 IoU 彻底掰开揉碎。
2.1 IoU 的数学定义
先看公式,很简单:
IoU = Area of Overlap / Area of Union
其中:
- Overlap:两个框的交集区域面积
- Union:两个框的并集区域面积 = A面积 + B面积 - 交集面积
举个例子。假设框 A 和框 B 完全重合,交集 = 并集,IoU = 1.0。如果完全不挨着,交集 = 0,IoU = 0.0。
实际场景里,IoU 通常在 0.3 ~ 0.9 之间晃悠。我在项目中遇到过最极端的情况——两个框只重叠了一个像素,IoU 只有 0.0001,但那个像素恰好是目标的中心点。这种时候,光靠 IoU 是不够的,后面我们会聊怎么用其他指标补救。
2.2 计算方法:一步步拆解
计算 IoU 分三步走:
- 求交集矩形:找到两个框在 x 轴和 y 轴上的重叠区间
- 算交集面积:重叠宽度 × 重叠高度
- 算并集面积:A面积 + B面积 - 交集面积
这里有个坑——如果两个框不相交,重叠宽度或高度会是负数。我曾经在早期版本里忘了处理这个,结果 IoU 算出来是负的,NMS 直接把所有框都保留了,模型输出一堆假阳性。嗯,血的教训。
正确的做法是:overlap_w = max(0, x2 - x1),同理高度。这样不相交时交集面积就是 0。
2.3 代码实现:从零手写
我个人习惯在项目里保留一个纯 NumPy 版本的 IoU 实现,方便调试和验证。下面是最经典的写法:
import numpy as np
def compute_iou(box1, box2):
"""
计算两个矩形框的 IoU
输入格式: [x1, y1, x2, y2] (左上角 + 右下角)
"""
# 提取坐标
x1_min, y1_min, x1_max, y1_max = box1
x2_min, y2_min, x2_max, y2_max = box2
# 计算交集矩形的坐标
inter_xmin = max(x1_min, x2_min)
inter_ymin = max(y1_min, y2_min)
inter_xmax = min(x1_max, x2_max)
inter_ymax = min(y1_max, y2_max)
# 计算交集面积(不相交时为0)
inter_w = max(0, inter_xmax - inter_xmin)
inter_h = max(0, inter_ymax - inter_ymin)
inter_area = inter_w * inter_h
# 计算各自面积
area1 = (x1_max - x1_min) * (y1_max - y1_min)
area2 = (x2_max - x2_min) * (y2_max - y2_min)
# 并集面积
union_area = area1 + area2 - inter_area
# 防止除零
if union_area == 0:
return 0.0
iou = inter_area / union_area
return iou
小技巧:如果你用的是 YOLOv5/v8 的格式(中心点 + 宽高),记得先转成 [x1,y1,x2,y2] 再算 IoU。我一般写个 xywh2xyxy 工具函数,一行搞定。
2.4 批量 IoU 计算:加速 NMS
实际 NMS 里,我们不是算两个框,而是算一堆框两两之间的 IoU。用循环写会慢到怀疑人生。
我建议用向量化操作:
def batch_iou(boxes1, boxes2):
"""
计算两组框之间的 IoU 矩阵
boxes1: [N, 4], boxes2: [M, 4]
返回: [N, M] IoU 矩阵
"""
# 提取坐标
x1 = boxes1[:, 0][:, None] # [N, 1]
y1 = boxes1[:, 1][:, None]
x2 = boxes1[:, 2][:, None]
y2 = boxes1[:, 3][:, None]
x1_2 = boxes2[:, 0][None, :] # [1, M]
y1_2 = boxes2[:, 1][None, :]
x2_2 = boxes2[:, 2][None, :]
y2_2 = boxes2[:, 3][None, :]
# 交集
inter_xmin = np.maximum(x1, x1_2)
inter_ymin = np.maximum(y1, y1_2)
inter_xmax = np.minimum(x2, x2_2)
inter_ymax = np.minimum(y2, y2_2)
inter_w = np.maximum(0, inter_xmax - inter_xmin)
inter_h = np.maximum(0, inter_ymax - inter_ymin)
inter_area = inter_w * inter_h
# 面积
area1 = (x2 - x1) * (y2 - y1) # [N, 1]
area2 = (x2_2 - x1_2) * (y2_2 - y1_2) # [1, M]
union = area1 + area2 - inter_area
union = np.maximum(union, 1e-6) # 防除零
return inter_area / union
这段代码跑起来比循环快 10 倍以上。你想想看,一张图里几百个框,两两算 IoU,循环得 O(n²) 时间,向量化一步到位。
2.5 IoU 的变体:不止一种
标准 IoU 有个硬伤——两个框不相交时,IoU 恒为 0,梯度传不回去。所以在一些场景下,我们会用它的变体:
| 名称 | 公式 | 特点 | 适用场景 |
|---|---|---|---|
| IoU | 交集 / 并集 | 标准,不相交时为 0 | NMS、评估指标 |
| GIoU | IoU - (最小外接框面积 - 并集) / 最小外接框面积 | 不相交时也有梯度 | 目标检测损失函数 |
| DIoU | IoU - (中心点距离² / 最小外接框对角线²) | 考虑中心点距离 | 回归损失、NMS 变体 |
| CIoU | DIoU + 长宽比惩罚项 | 最全面,收敛快 | YOLOv5/v8 默认损失 |
注意:NMS 里通常还是用标准 IoU。GIoU/DIoU/CIoU 主要用于训练时的回归损失。我曾经试过用 DIoU 做 NMS,结果发现对小目标反而更差了——因为中心点距离对小框太敏感。
2.6 知识结构图:IoU 全景
下面这张图帮你把 IoU 相关的知识点串起来:
2.7 避坑指南
- 坐标格式混淆:YOLO 用 [x_center, y_center, w, h],VOC 用 [x1, y1, x2, y2],COCO 用 [x1, y1, w, h]。算 IoU 前务必统一格式。我曾经在项目里混用了两种格式,结果 IoU 全乱套,排查了整整一下午。
- 整数 vs 浮点:像素坐标用整数没问题,但归一化坐标(0~1)一定要用浮点。整数除法会直接截断,IoU 精度全丢。
- 除零保护:并集面积可能为 0(两个框面积都是 0 时),一定要加
max(union, 1e-6)或类似保护。 - 批量计算的内存:如果一张图有 1000 个框,IoU 矩阵就是 1000×1000 ≈ 1M 个元素,还好。但如果视频流里做全图 NMS,注意别爆内存。
好了,IoU 这块就聊到这儿。下个知识点,咱们会基于 IoU 展开 NMS 的核心流程——怎么用 IoU 矩阵高效地干掉冗余框。