【算法手撕代码】手撕 IOU 与 NMS(Pytorch实现)

NMS

在目标检测后处理的时候,因为网络产生的候选框太多了,通常我们都需要使用 NMS (Non-maximum suppression) 非极大值抑制来保留最大值的候选框,也就是最合适的候选框。

NMS 算法的输入与返回值

输入值有 3 项:

  • box
  • score
  • threshold

返回值:没有被抑制的 index 矩阵

其实仔细观察可以看到,目标检测的话,出了候选框的位置信息,网络还会输出候选框内部物体的分类信息,但是在 NMS 计算里却没有,这是为什么?

因为对于每个类别,都会进行一次 NMS,也就是有多少个类别就会进行多少次 NMS。

NMS 的流程

  1. 根据 score 的大小进行排序(降序)
  2. 从 score 的最大的 box 开始遍历,求其与别的 box 的 IOU 值
  3. 如果 IOU 值大于 阈值 threshold,则去除,否则则保留。
  4. 最后返回没有被抑制的 index 矩阵
def nms(self, bboxes, scores, thresh=0.5):
	# bboxe: [N, 4], scores: [N, ]
    x1, y1 = bboxes[:, 0], bboxes[:, 1]
    x2, y2 = bboxes[:, 1], bboxes[:, 1]
    areas = (x2 - x1) * (y2 - y1)  # bboxes area: [N, ]
    
    _, order = scores.sort(0, descending=True)  # 根据 score 进行降序排序,返回索引 order: [N, ]

    keep = []   # keep保留了NMS后留下的边框box
    
    while order.numel() > 0:
    
       	# 结束判断 ----------------------------------------------
        if order.numel() == 1: # 保留框只剩1个
            i = order.item()
            keep.append(i)
            break
        else: # 还有保留框没有NMS
            i = order[0].item() # 保留scores最大的那个框box[i]
            keep.append(i)
            
        # 计算 iou ----------------------------------------------------
        xx1 = x1[order[1: ]].clamp(min=x1[i])  # [N-1, ]
        yy1 = y1[order[1: ]].clamp(min=y1[i]) 
        xx2 = x2[order[1: ]].clamp(max=x2[i]) 
        yy2 = y2[order[1: ]].clamp(max=y2[i]) 
        # 求每一个框和当前框重合部分和总共叠加的面积
        inter = (xx2 - xx1).clamp(min=0) * (yy2 - yy1).clamp(min=0)  # inter: [N-1, ]
        union = areas[i] + areas[order[1: ]] - inter
        # 计算每一个框和当前框的IoU
        IoU = inter / union

        # 保留IoU小于threshold的边框索引 ---------------------------------------
        idx = (IoU <= thresh).nonzero().squeeze()
        if idx.numel() == 0:
            break
        order = order[idx+1]  # 这里+1是为了补充idx和order之间的索引差

    return torch.LongTensor(keep)  # 返回保留下的所有边框的索引

while 循环外面的计算 bbox 面积和根据 score 大小进行降序排序的部分都很好理解。

主要是在 while 循环内部,其实主要分成 3 个部分。

第一部分

        if order.numel() == 1: # 保留框只剩1个
            i = order.item()
            keep.append(i)
            break
        else: # 还有保留框没有NMS
            i = order[0].item() # 保留scores最大的那个框box[i]
            keep.append(i)

这段代码怎么理解呢?

因为是根据 score 的大小,将 bbox 从大到小遍历

  • 如果最后遍历到最后,只剩下一个候选框的话,就直接将这个候选框的 index 保留,并且结束。
  • 如果还有很多候选框没有遍历的话,就保留最大的候选框,然后遍历剩下的 order,去抑制和这个最大候选框接近的结果。

第二部分

在这里插入图片描述
这个部分是 IOU 的计算:

        # 计算 iou ----------------------------------------------------
        xx1 = x1[order[1: ]].clamp(min=x1[i])  # [N-1, ]
        yy1 = y1[order[1: ]].clamp(min=y1[i]) 
        xx2 = x2[order[1: ]].clamp(max=x2[i]) 
        yy2 = y2[order[1: ]].clamp(max=y2[i]) 
        # 求每一个框和当前框重合部分和总共叠加的面积
        inter = (xx2 - xx1).clamp(min=0) * (yy2 - yy1).clamp(min=0)  # inter: [N-1, ]
        union = areas[i] + areas[order[1: ]] - inter  # areas[i]: [1, ], union: [N-1, ]
        # 计算每一个框和当前框的IoU
        IoU = inter / union # iou: [N-1, ]

torch.clamp 是怎么个用法,就是一个截断函数(虽然 clamp 直接翻译是“夹紧”)

  • tensor_a.clamp(min=a1):tensor_a 中小于 a1 的值都会被设成 a1。
  • tensor_a.clamp(max=a2):tensor_a 中大于 a2 的值都会被设成 a2。
  • tensor_a.clamp(min=a1, max=a2):tensor_a 中小于 a1 的值都会被设成 a1,大于 a2 的值都会被设成 a2。

当然也可以写成这样的形式: torch.clamp(tensor_a, min=a1, max=a2)

  • xx1:就是所有大于 x1[i] 的 x1 值都会被保留,也就是上图中红点,小于 x1[i] 的都会被设成 x1[i]。
  • xx2:就是说所有大于 x2[i] 的都会被设成 x2[i],也就是上图的蓝点。

yy1yy2 也是同理,这样就相当于保留了中间交集部分矩形的左上角和右上角的坐标,就可以用来计算交集矩形的面积 inter。这个交集肯定是不包含与自己的交集的,所有是 [ N − 1 , ] [N-1, ] [N1,]

union 也是 [ N − 1 , ] [N-1, ] [N1,], areas[i] 是 [ 1 , ] [1, ] [1,],但是在进行矩阵运算的时候,自动变成了 [ N − 1 , ] [N-1, ] [N1,] 来进行计算。最后相除得到的 iou 也是 [ N − 1 , ] [N-1, ] [N1,]

第三部分

        # 保留IoU小于threshold的边框索引 ---------------------------------------
        idx = (IoU <= thresh).nonzero().squeeze()
        if idx.numel() == 0:
            break
        order = order[idx+1]  # 这里+1是为了补充idx和order之间的索引差, order: [len(idx)+1, ]

tensor.nonzero():会返回一个 [M, ] 的矩阵,每一行都是一个非零值的索引,说明一共有 M 个非零值。
tensor.squeeze():会将维度为1的给去掉。

  • 如果 idx 为 0,说明剩下的候选框全部抑制掉了,可以结束 NMS 了。
  • idx 不为 0,则说明还有别的候选框没有被抑制掉,继续进行 NMS。

为什么 order[idx + 1]idx + 1 呢?

因为计算 iou 的时候,会计算候选框和除了自己(第一个)别的候选框的 iou,所以得到的 iou 结果应该是 N-1 个,而 orderN 个,所以为了对齐,要加 1。

最后返回 keep

最后返回的 keep 理论上最大可能保留了所有的候选框,就是如果你的 threshold 设置得很大的时候,例如 0.99 之类的。

最少应该也能保留一个 score 值最大的候选框吧。

IOU

IOU 其实上面计算 NMS 其实已经涉及到了,原理是一样的,也是求交集和并集的比例。但是可能对于不同形式的输入,写法可能有些区别。

在求 NMS 中,是利用矩阵运算同时计算一个候选框和别的候选框的 IOU。

这里,我们也可以简单求两个候选框之间的 IOU, 也可以矩阵运算一次性求解多个候选框之间的 IOU。

矩阵运算计算 IOU 矩阵

def iou(self, box1, box2):
        N, M = box1.size(0), box2.size(0)

        lt = torch.max(  # 左上角的点
            box1[:, :2].unsqueeze(1).expand(N, M, 2),   # [N,2]->[N,1,2]->[N,M,2]
            box2[:, :2].unsqueeze(0).expand(N, M, 2),   # [M,2]->[1,M,2]->[N,M,2]
        )

        rb = torch.min(  # 右下角的点
            box1[:, 2:].unsqueeze(1).expand(N, M, 2),
            box2[:, 2:].unsqueeze(0).expand(N, M, 2),
        )

        wh = rb - lt  # [N,M,2]
        wh[wh < 0] = 0   # 两个box没有重叠区域
        inter = wh[:,:,0] * wh[:,:,1]   # [N,M]

        area1 = (box1[:, 2]-box1[:, 0]) * (box1[:, 3]-box1[:, 1])  # [N,]
        area2 = (box2[:, 2]-box2[:, 0]) * (box2[:, 3]-box2[:, 1])  # [M,]
        area1 = area1.unsqueeze(1).expand(N, M)  # [N, M]
        area2 = area2.unsqueeze(0).expand(N, M)  # [N, M]

        return inter / (area1 + area2 - inter)  # [N, M]

box1box2 分别是 [N, 4][M, 4] 的维度,最后获得的 IOU 也是 [N, M] 的维度。

两个 box 的 IOU

def iou(self, box1, box2):
    lt_x, lt_y = max(box1[0], box2[0]), max(box1[1], box2[1])  
    rb_x, rb_y = min(box1[2], box2[2]), min(box1[3], box2[3])  

    area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
    area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])

    w, h = rb_x - lt_x, rb_y - lt_y

    if w <= 0 or h <= 0:
        return 0  

    inter = w * h
    return inter / (area1 + area2 - inter)

box1box2 只有一个维度,也就是 list 的长度,都是 4,所以最后获得的 IOU 是一个数值。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用 YOLOv5 和 PyTorch 实现疲劳检测的示例代码: ```python import cv2 import torch from PIL import Image from yolov5.models.experimental import attempt_load from yolov5.utils.general import non_max_suppression from yolov5.utils.torch_utils import select_device # 加载模型 model = attempt_load('yolov5s.pt', map_location=torch.device('cpu')) # 选择设备 device = select_device('') # 加载标签 classes = ['open_eye', 'closed_eye'] # 加载图像 img = cv2.imread('test.jpg') # 将图像转换为PIL Image img = Image.fromarray(img[..., ::-1]) # 进行推理 results = model(img, size=640) # 非最大抑制 results = non_max_suppression(results, conf_thres=0.5, iou_thres=0.5) # 遍历结果 for result in results: if result is not None: # 获取预测框的坐标和类别 boxes = result[:, :4] scores = result[:, 4] labels = result[:, 5] # 遍历每个预测框 for box, score, label in zip(boxes, scores, labels): # 将坐标转换为整数 box = [int(b) for b in box] # 绘制预测框和类别 cv2.rectangle(img, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 2) cv2.putText(img, classes[int(label)], (box[0], box[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) # 如果检测到闭眼,则输出警告信息 if classes[int(label)] == 'closed_eye': print('Warning: Driver is drowsy!') # 显示结果图像 cv2.imshow('Result', img) cv2.waitKey(0) ``` 以上代码可以检测图像中的人的眼睛是否闭合,如果检测到闭眼,则输出警告信息。你可以根据自己的需求修改代码,比如改变模型的大小、调整阈值等等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值