锚框、交并比和非极大值抑制(tf2.0源码解析)

文章目录

  • 锚框、交并比和非极大值抑制(tf2.0源码解析)
    • 一、锚框生成
      • 1、锚框的宽高
      • 2、锚框的个数
      • 3、注意点(★★★)
      • 4、tf2.0代码
    • 二、交并比
      • 1、Jaccard相似度
      • 2、交并比矩阵
      • 3、标注锚框
      • 4、注意点(★★★)
      • 5、tf2.0代码
        • 1)交并比
        • 2)绘制真实框和锚框
        • 3)交并比矩阵
        • 4)计算偏移量
    • 三、非极大值抑制
      • 1、NMS原理
      • 2、注意点(★★★)
      • 3、tf2.0代码
        • 1)NMS控制台输出
        • 2)NMS锚框绘制

参考

  • 计算机视觉 – 4 锚框
  • 9.4 锚框

Note:如果本文代码不易你阅读,可以参考上面两篇博客,下面的代码基本上摘抄于上面的。

一、锚框生成

目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含感兴趣的目标,并调整区域边缘从而更准确地预测目标的真实边界框(ground-truth bounding box)。

不同的模型使用的区域采样方法可能不同。这里介绍其中的一种:
它以每个像素为中心(锚点) 生成多个大小和宽高比(aspect ratio)不同的边界框。这些边界框被称为锚框(anchor box)。

1、锚框的宽高

假设输入图像高为H、宽为W,分别以图像的每个像素为中心生成不同形状的锚框。设大小为 s∈(0,1]s∈(0,1]s∈(0,1] 且宽高比为 r>0r>0r>0 ,则锚框的宽和高分别为 ws×rws \times \sqrt{r}ws×r​ 和 hs/rhs / \sqrt{r}hs/r​。
因为通常情况下,锚框是以特征图上的锚点进行矩形框绘制(而不是在原图上进行绘制,主要原因是可以减少冗余锚框的生成)。为了方便计算,下面用的函数中w,h是每个像素的长宽,即1,1,输出的锚框坐标是归一化的结果

2、锚框的个数

分别设定一组大小s1,...,sns_1,...,s_ns1​,...,sn​ 和一组宽高比 r1,...,rmr_1 ,... , r_mr1​,...,rm​。如果以每个像素为中心时使用所有的大小与宽高比的组合,输入图像将一共得到w×h×n×mw \times h \times n \times mw×h×n×m个锚框。虽然这些锚框可能覆盖了所有的真实边界框,但计算复杂度容易过高

因此,通常只对包含 s1 的大小与宽高比 r1 的组合感兴趣,即:
(s1,r1),(s1,r2),...,(s1,rm),(s2,r1),...,(sn,r1)(s_1,r_1),(s_1,r_2),...,(s_1,r_m),(s_2,r_1),...,(s_n,r_1) (s1​,r1​),(s1​,r2​),...,(s1​,rm​),(s2​,r1​),...,(sn​,r1​)
比如sizes=[0.3,0.5,0.75]sizes = [0.3, 0.5, 0.75]sizes=[0.3,0.5,0.75], ratios=[0.5,1,2]ratios = [0.5, 1, 2]ratios=[0.5,1,2],则有效组合为
(s,r)=(0.3,0.5),(0.3,1),(0.3,2),(0.5,0.5),(0.75,0.5)(s,r) = (0.3,0.5),(0.3,1),(0.3,2),(0.5,0.5),(0.75,0.5) (s,r)=(0.3,0.5),(0.3,1),(0.3,2),(0.5,0.5),(0.75,0.5)
也就是说,以相同像素为中心的锚框的数量为n+m−1n + m - 1n+m−1, 对于整个输入图像,将一共生成wh(n+m−1)wh(n+m-1)wh(n+m−1)的anchor

3、注意点(★★★)

参考

  • 目标检测中ANCHOR如何映射到原图

  • faster r-cnn的rpn网络中每个锚框和特征图是否存在映射关系

  1. 为特征图上的每个锚点生成相应的锚框,而不是在原始图上的每个锚点生成锚框,原因是特征图上每个像素是对原图像某个区域的信息进行的浓缩,每个像素就代表着一个语义信息,因此在特征图上生成的锚框相较于原图上生成的会更少
  2. 每个锚框是一个4元组,即表示为归一化后的左上角坐标和右下角坐标。虽然是在特征图上生成的,但是锚框再乘上原始图片的宽高比例因子之后可以得到原图片上的预测框(预测框太多时,会采用IoU阈值,非极大值抑制的方法去除掉交并比低的框,以及类别置信度低的预测框,再计算回归损失1-IoU和分类损失),在锚框的复原计算过程中,和特征图的尺寸是没有关系的。
  3. 不同迭代过程中,每个特征图锚点上生成的原始锚框是相同的,但是模型学习到的锚框中心坐标偏移量,以及宽高偏移量(区间在[0,1]上)是不同的,模型会更新原始锚框的位置,在边界框回归中进行参数更新

这里还要注意的是,下面方法生成的锚框是由归一化的左上角坐标和右上角坐标这个4元组构成。如果想复原到原始图像上的锚框大小,则需要乘上个缩放系数box_scale

4、tf2.0代码

utils.py中:

def MultiBoxPrior(feature_map, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]):"""# anchor表示成(xmin, ymin, xmax, ymax).https://zh.d2l.ai/chapter_computer-vision/anchor.htmlArgs:feature_map: torch tensor, Shape: [N, C, H, W].sizes: List of sizes (0~1) of generated MultiBoxPriores.ratios: List of aspect ratios (non-negative) of generated MultiBoxPriores.Returns:anchors of shape (1, num_anchors, 4). 由于batch里每个都一样, 所以第一维为1"""pairs = [] # pair of (size, sqrt(ratio))for r in ratios:pairs.append([sizes[0], np.sqrt(r)])for s in sizes[1:]:pairs.append([s, np.sqrt(ratios[0])])pairs = np.array(pairs)ss1 = pairs[:, 0] * pairs[:, 1] # size * sqrt(ration)  #宽ss2 = pairs[:, 0] / pairs[:, 1] # size / sqrt(retion)  #高base_anchors = tf.stack([-ss1, -ss2, ss1, ss2], axis=1) / 2  #每个锚点上关于锚框的(x1,y1,x2,y2)相对位置坐标h, w = feature_map.shape[-2:]shifts_x = tf.divide(tf.range(0, w), w)shifts_y = tf.divide(tf.range(0, h), h)shift_x, shift_y = tf.meshgrid(shifts_x, shifts_y)  #shift_x为复制h行,shift_y为转置后复制w列shift_x = tf.reshape(shift_x, (-1,))   #2D转1Dshift_y = tf.reshape(shift_y, (-1,))   #2D转1Dshifts = tf.stack((shift_x, shift_y, shift_x, shift_y), axis=1)  #4D和base_anchors[0]一致,为了在后者基础上加上偏移量,得到所有anchors的相对坐标anchors = tf.add(tf.reshape(shifts, (-1,1,4)), tf.reshape(base_anchors, (1,-1,4)))return tf.cast(tf.reshape(anchors, (1,-1,4)), tf.float32)def bbox_to_rect(bbox, color):# 将边界框(左上x, 左上y, 右下x, 右下y)格式转换成matplotlib格式:# ((左上x, 左上y), 宽, 高)return plt.Rectangle(xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],fill=False, edgecolor=color, linewidth=2)def show_bboxes(axes, bboxes, labels=None, colors=None):def _make_list(obj, default_values=None):if obj is None:obj = default_valueselif not isinstance(obj, (list, tuple)):obj = [obj]return objlabels = _make_list(labels)colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])for i, bbox in enumerate(bboxes):color = colors[i % len(colors)]rect = bbox_to_rect(bbox.numpy(), color)axes.add_patch(rect)if labels and len(labels) > i:text_color = 'k' if color == 'w' else 'w'axes.text(rect.xy[0], rect.xy[1], labels[i],va='center', ha='center', fontsize=6,color=text_color, bbox=dict(facecolor=color, lw=0))

code002_anchor_generate1.py中:

import unittest
import cv2
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from IPython import displayclass MyTest(unittest.TestCase):'''绘制该锚点上所有锚框'''def test_AnchorBoxesPlot(self):img = cv2.imread("./img/catAndDog.png")img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)h, w = img.shape[0], img.shape[1]print(h, w)x = tf.zeros((1, 3, h, w))sizes = [0.5, 0.75, 1.2]ratios = [1.5, 2]y = MultiBoxPrior(x, sizes, ratios)  # 返回锚框变量y的形状为(1,锚框个数,4),其中4个元素,分别是锚框左上角的x和y轴坐标和右下角的x和y轴坐标。boxes = tf.reshape(y,(h,w,4,4))   #将锚框变量y的形状变为(图像高,图像宽,以相同像素为中心的锚框个数,4)use_svg_display()# 设置图的尺寸plt.rcParams['figure.figsize'] = (3.5, 2.5)print(f"boxes[0, 0, :, :] = {boxes[0, 0, :, :]}")print(f"boxes[h-1, w-1, :, :] = {boxes[h - 1, w - 1, :, :]}")fig = plt.imshow(img)bbox_scale = tf.constant([[w, h, w, h]], dtype=tf.float32)  #bbox_scale放大尺寸show_bboxes(fig.axes,tf.multiply(boxes[0, 0, :, :], bbox_scale),labels=['s=0.5, r=1.5', 's=0.5, r=2', 's=0.75, r=2', 's=1.2, r=2'])show_bboxes(fig.axes,tf.multiply(boxes[h - 1, w - 1, :, :], bbox_scale),labels=['s=0.5, r=1.5', 's=0.5, r=2', 's=0.75, r=2', 's=1.2, r=2'])plt.show()

像素(0,0)(0,0)(0,0)的4个锚框和像素(h−1,w−1)(h-1,w-1)(h−1,w−1)的4个锚框如下图所示

代码小解析

  • 为了减少锚框生成的个数,这里不用原始图片的尺寸进行锚框的生成,因此这里假设x为feature_map
     x = tf.zeros((1, 3, 10, 10))sizes = [0.5, 0.75, 1.2]ratios = [1.5, 2]y = MultiBoxPrior(x, sizes, ratios)  # 返回锚框变量y的形状为(1,锚框个数,4),其中4个元素,分别是锚框左上角的x和y轴坐标和右下角的x和y轴坐标

这样生成的锚框只有 (3+2−1)∗10∗10=400(3+2-1) * 10 * 10 = 400(3+2−1)∗10∗10=400个,经过IoU阈值和NMS抑制之后,可以减少很多冗余和不符合要求的锚框,避免程序在绘制锚框时卡死(如果x = tf.zeros((1, 3, 499, 385))确实会卡死)。

  • tf.meshgrid()用于从数组a和b产生网格。生成的网格矩阵A和B大小是相同的,它也可以是更高维的。

用法: [A, B] = tf.meshgrid(a, b), 生成size(b) x size(a)大小的矩阵A和B。

它相当于a从一行重复增加到size(b)行,把b转置成一列再重复增加到size(a)列

a=[0,5,10]
b=[0,5,15,20,25]
A,B=tf.meshgrid(a,b)print (A)  # 将a重复b行,得到A,size为 b * a
print (B)  # 将b转置,再将b的每一列重复a列,得到B, size = b * a
---
tf.Tensor(
[[ 0  5 10][ 0  5 10][ 0  5 10][ 0  5 10][ 0  5 10]], shape=(5, 3), dtype=int32)
tf.Tensor(
[[ 0  0  0][ 5  5  5][15 15 15][20 20 20][25 25 25]], shape=(5, 3), dtype=int32)
  • 由此可见,返回锚框变量y的形状为**(1,锚框个数,4)
    将锚框变量y的
    形状变为(图像高,图像宽,以相同像素为中心的锚框个数,4)后,即可通过指定像素位置来获取所有以该像素为中心的锚框**了。
boxes = tf.reshape(y,(h,w,4,4))   #将锚框变量y的形状变为(图像高,图像宽,以相同像素为中心的锚框个数,4)

例如,访问以(0,0)为中心的第一个锚框。
它有4个元素,分别是锚框左上角的x和y轴坐标和右下角的x和y轴坐标。
其中,x和y轴的坐标值分别已除以图像的宽和高,因此值域均为0和1之间

  • 为了描绘图像中以某个像素为中心的所有锚框,定义show_bboxes函数以便在图像上画出多个边界框。

二、交并比

1、Jaccard相似度

上文提到不同的锚框对图像中狗的覆盖程度不同。若该目标的真实边界框已知,如何对覆盖程度进行量化

一种直观的方法是衡量锚框和真实边界框之间的相似度。Jaccard系数(Jaccard index)可以衡量两个集合的相似度。给定集合A和B,它们的Jaccard系数即为二者交集大小除以二者并集大小

实际上,可以把边界框内的像素区域看作像素的集合。由此,可以用两个边界框的像素集合的Jaccard系数衡量这两个边界框的相似度。

当衡量两个边界框的相似度时,通常将Jaccard系数称为交并比(Intersection over Union,IoU),即两个边界框相交面积与相并面积之比。如下图所示:

交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框相等。

2、交并比矩阵

在训练集中,将每个锚框视为一个训练样本。为了训练目标检测模型,需要为每个锚框标注两类标签

  • 一是锚框所含目标的类别,简称类别
  • 二是真实边界框相对锚框的偏移量,简称偏移量(offset)。

在目标检测时,首先生成多个锚框,然后为每个锚框预测类别以及偏移量,接着根据预测的偏移量调整锚框位置从而得到预测边界框,最后筛选需要输出的预测边界框。

在目标检测的训练集中,每个图像已标注了真实边界框的位置以及所含目标的类别。在生成锚框之后,主要依据与锚框相似的真实边界框的位置和类别信息为锚框标注。那么,该如何为锚框分配与其相似的真实边界框呢

假设图像中锚框分别为 A1,A2,…,AnaA_1, A_2 , … , A_{na}A1​,A2​,…,Ana​,真实边界框分别为 B1,B2,…,BnbB_1,B_2,…,B_{n_b}B1​,B2​,…,Bnb​​,且na≥nbn_a≥n_bna​≥nb​ 。

定义矩阵 X∈Rna×nbX∈R^{n_a×n_b}X∈Rna​×nb​,其中第 i 行第 j 列的元素 xijx_{ij}xij​ 为锚框 AiA_iAi​与真实边界框 BjB_jBj​ 的交并比。

首先,找出矩阵 XXX中最大元素,并将该元素的行索引与列索引分别记为i1i_1i1​,j1j_1j1​ 。为锚框 Ai1A_{i1}Ai1​ 分配真实边界框 Bj1B_{j1}Bj1​ 。
显然,锚框 Ai1A_{i1}Ai1​和真实边界框 Bj1B_{j1}Bj1​在所有的“锚框—真实边界框”的配对中相似度最高

接下来,将矩阵 XXX中第 i1i_1i1​ 行和第j1j_1j1​ 列上的所有元素丢弃。找出矩阵XXX中剩余的最大元素,并将该元素的行索引与列索引分别记为 i2,j2i_2,j_2i2​,j2​。
为锚框 Ai2A_{i2}Ai2​ 分配真实边界框 Bj2B_{j2}Bj2​ ,再将矩阵XXX中第i2i_2i2​行和第j2j_2j2​列上的所有元素丢弃。
此时矩阵 XXX 中已有2行2列的元素被丢弃。
依此类推,直到矩阵XXX中所有 nbn_bnb​列元素全部被丢弃

此时,已为 nbn_bnb​个锚框各分配了一个真实边界框。之后,只遍历剩余的 na−nbn_a−n_bna​−nb​个锚框
给定其中的锚框 AiA_iAi​,根据矩阵XXX 的第 i 行找到与AiA_iAi​交并比最大的真实边界框 BjB_jBj​ ,且只有当该交并比大于预先设定的阈值时,才为锚框 AiA_iAi​ 分配真实边界框 BjB_jBj​

如上图(左)所示,假设矩阵XXX中最大值为 x23x_{23}x23​ ,我们将为锚框A2A_2A2​分配真实边界框 B3B_3B3​ 。然后,丢弃矩阵中第2行和第3列的所有元素,找出剩余阴影部分的最大元素 x71x_{71}x71​ ,为锚框A7A_7A7​分配真实边界框 B1B_1B1​ 。

接着如上图(中)所示,丢弃矩阵中第7行和第1列的所有元素,找出剩余阴影部分的最大元素x54x_{54}x54​ ,为锚框 A5A_5A5​ 分配真实边界框B4B_4B4​ 。

最后如上图(右)所示,丢弃矩阵中第5行和第4列的所有元素,找出剩余阴影部分的最大元素 x92x_{92}x92​ ,为锚框 A9A_9A9​ 分配真实边界框B2B_2B2​ 。

之后,只需遍历除去 A2,A5,A7,A9A_2,A_5,A_7,A_9A2​,A5​,A7​,A9​ 的剩余锚框,并根据阈值判断是否为剩余锚框分配真实边界框

3、标注锚框

为锚框分配真实锚框后,现在可以标注锚框的类别和偏移量了。

如果一个锚框 A 被分配了真实边界框 B ,将锚框 A 的类别设为 B 的类别,并根据 B 和 A 的中心坐标的相对位置以及两个框的相对大小为锚框 A 标注偏移量。

由于数据集中各个框的位置和大小各异,因此这些相对位置和相对大小通常需要一些特殊变换,才能使偏移量的分布更均匀从而更容易拟合。

设锚框 A 及其被分配的真实边界框 B 的中心坐标分别为 (xa,ya)( x_a , y_a )(xa​,ya​) 和 (xb,yb)( x_b , y_b )(xb​,yb​) , A 和 B 的宽分别为 waw_awa​和wbw_bwb​,高分别为hah_aha​和 hbh_bhb​ ,一个常用的技巧是将 A 的偏移量标注为:

其中,常数的默认值为:
μx=μy=μw=μy=0σx=σy=0.1σw=σh=0.2μ_x=μ_y = μ_w = μ_y = 0 \\ \sigma_x=\sigma_y = 0.1 \\ \sigma_w=\sigma_h=0.2 μx​=μy​=μw​=μy​=0σx​=σy​=0.1σw​=σh​=0.2
如果一个锚框没有被分配真实边界框,只需将该锚框的类别设为背景。类别为背景的锚框通常被称为负类锚框,其余则被称为正类锚框。

4、注意点(★★★)

  • 下面提到的compute_jaccard方法计算的IoU要求set1,set2里的元素都是归一化的位置坐标,不是真实图片下的坐标,否则计算的并集存在问题。因此这里需要对真实框的坐标进行归一化
  • 下面代码的实现步骤如下:
    • 使用LabelImg标注出原始图片中的猫和狗,进而得到猫狗的真实框左上角坐标和右上角坐标
    • 计算每个锚框和每个真实框的交并比,得到交并比矩阵
    • 绘制预测框1:获取真实框对应交并比最大的锚框,即每个真实框对应一个锚框
    • 绘制预测框2:利用交并比阈值,去除掉和真实框交并比小于阈值的锚框(标注为背景),并根据真实框的标注类别以及交并比值的大小,将满足要求的预测框标注为指定类别

5、tf2.0代码

1)交并比

utils.py

'''计算anchors之间的交集'''
# 交并比 参考 https://blog.csdn.net/m0_38111466/article/details/109408964,https://trickygo.github.io/Dive-into-DL-TensorFlow2.0/#/chapter09_computer-vision/9.4_anchor
def compute_intersection(set_1, set_2):"""计算anchor之间的交集Args:set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)  归一化的坐标set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)Returns:intersection of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)"""# tensorflow auto-broadcasts singleton dimensionslower_bounds = tf.maximum(tf.expand_dims(set_1[:,:2], axis=1), tf.expand_dims(set_2[:,:2], axis=0)) # (n1, n2, 2)  #比较真实框和预测框,获得最大左上角坐标upper_bounds = tf.minimum(tf.expand_dims(set_1[:,2:], axis=1), tf.expand_dims(set_2[:,2:], axis=0)) # (n1, n2, 2)  #比较真实框和预测框,获得最小右下角坐标# 设置最小值intersection_dims = tf.clip_by_value(upper_bounds - lower_bounds, clip_value_min=0, clip_value_max=3) # (n1, n2, 2)  #把A中的每一个元素的值都压缩在min和max之间。小于min的让它等于min,大于max的元素的值等于max。return tf.multiply(intersection_dims[:, :, 0], intersection_dims[:, :, 1]) # (n1, n2)   #diff(x) * diff(y)计算交集'''计算anchors之间的并集'''
def compute_union(set_1,set_2,intersection):"""计算anchor之间的交集Args:set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)intersection: intersection of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)Returns:union of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)"""# Find areas of each box in both setsareas_set_1 = tf.multiply(tf.subtract(set_1[:, 2], set_1[:, 0]), tf.subtract(set_1[:, 3], set_1[:, 1]))  # (n1)areas_set_2 = tf.multiply(tf.subtract(set_2[:, 2], set_2[:, 0]), tf.subtract(set_2[:, 3], set_2[:, 1]))  # (n2)# Find the union(找并集)union = tf.add(tf.expand_dims(areas_set_1, axis=1), tf.expand_dims(areas_set_2, axis=0))  # (n1, n2)union = tf.subtract(union, intersection)  # (n1, n2)  算了两次,减去交集部分return union'''计算anchor之间的Jaccard系数(IoU)'''
def compute_jaccard(set_1, set_2):"""计算anchor之间的Jaccard系数(IoU)Args:set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)Returns:Jaccard Overlap of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)"""# Find intersections(找交集)intersection = compute_intersection(set_1, set_2)union = compute_union(set_1,set_2,intersection)return tf.divide(intersection, union) #(n1, n2)

code003_IoU_test.py中:

import tensorflow as tf
import unittest
import cv2
from anchor_test.code002_anchor_generate1 import use_svg_display,show_bboxes
import matplotlib.pyplot as plt
from anchor_test.utils import compute_union,compute_jaccard,compute_intersection,MultiBoxPriorclass MyTest(unittest.TestCase):'''这个方法计算的IoU前提是set1,set2里的元素都是归一化的位置坐标,不是真实图片下的坐标,否则计算的并集存在问题'''def test_cal_IoU(self):# set_1 = tf.constant([[1, 2, 3, 4], [5, 6, 7, 8]],dtype=tf.float32)# set_2 = tf.constant([[1, 1, 1, 1], [2, 2, 2, 2]],dtype=tf.float32)h,w = 499,346set_1 = tf.constant([[38, 88, 176, 249],[242, 79, 296, 133]],dtype=tf.float32)  #ground truthbbox_scale = tf.constant([[1/w,1/h,1/w,1/h]],dtype=tf.float32)set_1 = tf.multiply(set_1, bbox_scale)set_2 = tf.constant([[0.62436354, 0.11036278, 0.93054974, 0.31448692],[0.15615545, 0.23461127, 0.46234167, 0.43873543]], dtype=tf.float32)  #predict boxes# set_2 = tf.multiply(set_2, bbox_scale)intersection = compute_intersection(set_1,set_2)union = compute_union(set_1,set_2,intersection)IoU = compute_jaccard(set_1,set_2)print(f"intersection = {intersection}, union = {union}, IoU = {IoU}")
---
intersection = [[0.         0.0625    ][0.01688927 0.        ]], union = [[0.19118512 0.12868512][0.06249999 0.07938927]], IoU = [[0.         0.48568165][0.2702283  0.        ]]

代码解析

  • 这个方法计算的IoU前提是set1,set2里的元素都是归一化的位置坐标,不是真实图片下的坐标,否则计算的并集存在问题。因此这里需要对真实框的坐标进行归一化
set_1 = tf.constant([[38, 88, 176, 249],[242, 79, 296, 133]],dtype=tf.float32)  #ground truth
bbox_scale = tf.constant([[1/w,1/h,1/w,1/h]],dtype=tf.float32)
set_1 = tf.multiply(set_1, bbox_scle)
  • 在intersection计算中

  • 先比较真实框和预测框,获得最大左上角坐标 lower_bounds

  • 再比较真实框和预测框,获得最小右下角坐标 upper_bounds

  • 再利用clip_by_value函数把upper_bounds - lower_bounds计算的正负值利用截断函数压缩到[0,3]之间,这样下面在计算交集面积时,负数会被处理成0,求得的交集也为0

    tf.multiply(intersection_dims[:, :, 0], intersection_dims[:, :, 1]) #diff(x) * diff(y)计算交集
    
  • 在计算union时

  • 先分别计算set1,set2的面积

     # Find areas of each box in both setsareas_set_1 = tf.multiply(tf.subtract(set_1[:, 2], set_1[:, 0]), tf.subtract(set_1[:, 3], set_1[:, 1]))  # (n1)areas_set_2 = tf.multiply(tf.subtract(set_2[:, 2], set_2[:, 0]), tf.subtract(set_2[:, 3], set_2[:, 1]))  # (n2)# Find the union(找并集)union = tf.add(tf.expand_dims(areas_set_1, axis=1), tf.expand_dims(areas_set_2, axis=0))  # (n1, n2)
    
  • 再减去两面积的交集

    union = tf.subtract(union, intersection)  # (n1, n2)  算了两次,减去交集部分
    

2)绘制真实框和锚框

先通过LabelImg图片标注软件标注出图像中狗和猫的真实框位置,得到的xml文件如下:

<annotation><folder>img</folder><filename>catAndDog.png</filename><path>E:/研究生任务/fatigue_package/anchor_test/img/catAndDog.png</path><source><database>Unknown</database></source><size><width>346</width><height>499</height><depth>3</depth></size><segmented>0</segmented><object><name>Dog</name><pose>Unspecified</pose><truncated>0</truncated><difficult>0</difficult><bndbox><xmin>38</xmin><ymin>88</ymin><xmax>176</xmax><ymax>249</ymax></bndbox></object><object><name>Cat</name><pose>Unspecified</pose><truncated>0</truncated><difficult>0</difficult><bndbox><xmin>242</xmin><ymin>79</ymin><xmax>296</xmax><ymax>133</ymax></bndbox></object></annotation>

code003_IoU_test.py中:

import tensorflow as tf
import unittest
import cv2
from anchor_test.code002_anchor_generate1 import use_svg_display,show_bboxes
import matplotlib.pyplot as plt
from anchor_test.utils import compute_union,compute_jaccard,compute_intersection,MultiBoxPrior'''Step1:绘制锚框和真实框'''
def anchors_groundTruth_plot(img,w,h,ground_truth,sizes = [0.25,0.35,0.5], ratios = [1.5,2]):''':param img: 原始图片:param w: 图片的宽:param h: 图片的高:param ground_truth: 真实边界框, (count,4):param sizes: 锚框相对于原始图片的缩放比例:param ratios: 宽高比例:return:'''x = tf.zeros(shape=(1, 3, h, w))anchors = MultiBoxPrior(x, sizes, ratios)  # 返回一个归一化后的四元组,(x1,x2,y1,y2)anchors_count = len(sizes) + len(ratios) - 1boxes = tf.reshape(anchors, (h, w, anchors_count, 4))  # 将anchors转化成(1,h,w,count,4)# 设置图的大小plt.rcParams['figure.figsize'] = (3.5, 2.5)fig = plt.imshow(img)use_svg_display()  # 使用svg进行绘制bbox_scale = tf.constant([[w, h, w, h]],dtype=tf.float32)  #用于将归一化的锚框放大至原图预测框的大小# 在原始图像上绘制指定锚点上的所有锚框print(f"boxes[168, 107,:, :] = {boxes[168, 107, :, :]}")  # dogprint(f"boxes[106, 269,:, :] = {boxes[106, 269, :, :]}")  # cat# 绘制锚框show_bboxes(fig.axes, tf.multiply(boxes[168, 107, :, :], bbox_scale),labels=['s=0.25, r=1.5', 's=0.25, r=2', 's=0.35, r=2', 's=0.5, r=2'])show_bboxes(fig.axes, tf.multiply(boxes[106, 269, :, :], bbox_scale),labels=['s=0.25, r=1.5', 's=0.25, r=2', 's=0.35, r=2', 's=0.5, r=2'])# 绘制真实框show_bboxes(fig.axes, ground_truth[:, :], ['dog', 'cat'], 'k')plt.show()class MyTest(unittest.TestCase):def test_get_maxIoU_withDogCat(self):'''Dog1: (c1,c2)=(107,168)  #Dog的中心坐标Cat1: (c1,c2)=(269,106)   #Cat中心坐标'''img = cv2.imread("./img/catAndDog.png")img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)h,w = img.shape[0],img.shape[1]'''###########################   Step1:绘制锚框和真实框  ############################################'''sizes = [0.25,0.35,0.5]ratios = [1.5,2]# 真实的边界框ground_truth = tf.constant([[38, 88, 176, 249, 0.92],[242, 79, 296, 133, 0.88]], dtype=tf.float32)anchors_groundTruth_plot(img,w,h,ground_truth,sizes,ratios)#maxIoU_predBoxes_plot(img,w,h,ground_truth,sizes,ratios)

通过真实框的位置信息,得到狗和猫的中心坐标(Dog1: (c1,c2)=(107,168),Cat1: (c1,c2)=(269,106)),并以该坐标为中心,生成相应的锚框。下面是关于真实框和锚框信息的绘制。其中黑色为真实框。

3)交并比矩阵

该部分的绘制结果是保留真实框对应的IoU最大的预测框

code003_IoU_test.py中:

'''Step2: 计算真实框和锚框的交并比矩阵,并根据交并比阈值绘制预测框'''
def maxIoU_predBoxes_plot(img,w,h,ground_truth,sizes = [0.25,0.35,0.5], ratios = [1.5,2]):''':param img: 原始图片:param w: 图片的宽:param h: 图片的高:param ground_truth: 真实边界框, (count,4):param sizes: 锚框相对于原始图片的缩放比例:param ratios: 宽高比例:return:'''x = tf.zeros(shape=(1, 3, h, w))anchors = MultiBoxPrior(x, sizes, ratios)  # 返回一个归一化后的四元组,(x1,x2,y1,y2)bbox_scale1 = tf.constant([[1 / w, 1 / h, 1 / w, 1 / h]], dtype=tf.float32)  # 用于真实框坐标的归一化ground_truth1 = tf.multiply(ground_truth[:, :-1], bbox_scale1)  # (1,anchor_count,4) * bbox_scale1对ground truth进行归一化,才能计算IoUset_1 = ground_truth1  # 归一化坐标的真实框set_2 = anchors[0]  # 归一化坐标的锚框'''TEST'''# set_3 = tf.constant([[0.62436354, 0.11036278, 0.93054974, 0.31448692],#                      [0.15615545, 0.23461127, 0.46234167, 0.43873543]], dtype=tf.float32)# set_3 = tf.multiply(set_3, bbox_scale)# print(f"set_3={set_3}")jaccard = compute_jaccard(set_1, set_2)# jaccard = compute_jaccard(set_1,set_3)print(f"jaccard={jaccard}")'''利用交并比矩阵,根据真实框索引依次获得IoU最大的锚框索引'''fig = plt.imshow(img)IoU_max, anchors_index = tf.reduce_max(jaccard, axis=1), tf.argmax(jaccard, axis=1)print(f"IoU_max = {IoU_max}, anchors_index = {anchors_index}")# anchors_index_withThreshold = jaccard > IoU_threshold# 绘制IoU最大的锚框cls = ["dog", "cat"]# 预测框bbox_scale = tf.constant([[[w, h, w, h]]], dtype=tf.float32)anchors_boxes = tf.multiply(anchors,bbox_scale)  #放大锚框for index, anchor_index in enumerate(anchors_index):show_bboxes(fig.axes, [anchors_boxes[0, anchor_index, :]],labels=[cls[index]])# show_bboxes(fig.axes, [set_3[anchor_index]],#             labels=[cls[index]])# 真实框show_bboxes(fig.axes, ground_truth[:, :-1], ['dog', 'cat'], 'r')plt.show()class MyTest(unittest.TestCase):def test_get_maxIoU_withDogCat(self):'''Dog1: (c1,c2)=(107,168)Cat1: (c1,c2)=(269,106)'''img = cv2.imread("./img/catAndDog.png")img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)h,w = img.shape[0],img.shape[1]'''###########################   Step1:绘制锚框和真实框  ############################################'''sizes = [0.25,0.35,0.5]ratios = [1.5,2]# 真实的边界框ground_truth = tf.constant([[38, 88, 176, 249, 0.92],[242, 79, 296, 133, 0.88]], dtype=tf.float32)#anchors_groundTruth_plot(img,w,h,ground_truth,sizes,ratios)'''###########################   Step2: 计算真实框和锚框的交并比矩阵,并绘制最大交并比预测框  ###########################'''maxIoU_predBoxes_plot(img,w,h,ground_truth,sizes,ratios)

为每个真实框匹配一个最大IoU的预测框,并且根据IoU大小和真实框类别为预测框贴上标签,绘制结果如下图所示。

4)计算偏移量

该部分的绘制结果是根据IoU阈值,保留真实框对应的IoU > 阈值的预测框

utils.py中:

'''保留IoU阈值大于jaccard_threshold的锚框'''
def assign_anchor(bb, anchor, jaccard_threshold=0.3):"""为每个anchor分配真实的bb # anchor表示成归一化(xmin, ymin, xmax, ymax).https://zh.d2l.ai/chapter_computer-vision/anchor.htmlArgs:bb: 真实边界框(bounding box), shape:(nb, 4)anchor: 待分配的anchor, shape:(na, 4)jaccard_threshold: 预先设定的阈值Returns:assigned_idx: shape: (na, ), 每个anchor分配的真实bb对应的索引, 若未分配任何bb则为-1"""na = anchor.shape[0]nb = bb.shape[0]jaccard = compute_jaccard(anchor, bb).numpy()   # shape: (na, nb)assigned_idx = np.ones(na) * -1 # 初始全为-1# 先为每个bb分配一个anchor(不要求满足jaccard_threshold)jaccard_cp = jaccard.copy()for j in range(nb):i = np.argmax(jaccard_cp[:, j])assigned_idx[i] = jjaccard_cp[i, :] = float("-inf")    # 赋值为负无穷, 相当于去掉这一行# 处理还未被分配的anchor, 要求满足jaccard_thresholdfor i in range(na):if assigned_idx[i] == -1:j = np.argmax(jaccard[i, :])if jaccard[i, j] >= jaccard_threshold:assigned_idx[i] = jreturn tf.cast(assigned_idx, tf.int32)def xy_to_cxcy(xy):"""将(x_min, y_min, x_max, y_max)形式的anchor转换成(center_x, center_y, w, h)形式的.https://github.com/sgrvinod/a-PyTorch-Tutorial-to-Object-Detection/blob/master/utils.pyArgs:xy: bounding boxes in boundary coordinates, a tensor of size (n_boxes, 4)Returns:bounding boxes in center-size coordinates, a tensor of size (n_boxes, 4)"""return tf.concat(((xy[:, 2:] + xy[:, :2]) / 2,  #c_x, c_yxy[:, 2:] - xy[:, :2]), axis=1)'''为锚框标注类别和偏移量,该函数将背景类别设为0,并令从零开始的目标类别的整数索引自加1(1为狗,2为猫)。'''
def MultiBoxTarget(anchor, label,jaccard_threshold):"""为锚框标注类别和偏移量 # anchor表示成归一化(xmin, ymin, xmax, ymax).https://zh.d2l.ai/chapter_computer-vision/anchor.htmlArgs:anchor: torch tensor, 输入的锚框, 一般是通过MultiBoxPrior生成, shape:(1,锚框总数,4)label: 真实标签, shape为(bn, 每张图片最多的真实锚框数, 5)第二维中,如果给定图片没有这么多锚框, 可以先用-1填充空白, 最后一维中的元素为[类别标签, 四个坐标值]jaccard_threshold: 预先设定的阈值Returns:列表, [bbox_offset, bbox_mask, cls_labels]bbox_offset: 每个锚框的标注偏移量,形状为(bn,锚框总数*4)bbox_mask: 形状同bbox_offset, 每个锚框的掩码, 一一对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1cls_labels: 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)"""assert len(anchor.shape) == 3 and len(label.shape) == 3bn = label.shape[0]def MultiBoxTarget_one(anchor, label, eps=1e-6):"""MultiBoxTarget函数的辅助函数, 处理batch中的一个Args:anchor: shape of (锚框总数, 4)label: shape of (真实锚框数, 5), 5代表[类别标签, 四个坐标值]eps: 一个极小值, 防止log0Returns:offset: (锚框总数*4, )bbox_mask: (锚框总数*4, ), 0代表背景, 1代表非背景cls_labels: (锚框总数, 4), 0代表背景"""an = anchor.shape[0]assigned_idx = assign_anchor(label[:, 1:], anchor, jaccard_threshold) ## (锚框总数, )# 决定anchor留下或者舍去bbox_mask = tf.repeat(tf.expand_dims(tf.cast((assigned_idx >= 0), dtype=tf.double), axis=-1), repeats=4, axis=1)cls_labels = np.zeros(an, dtype=int) # 0表示背景assigned_bb = np.zeros((an, 4), dtype=float) # 所有anchor对应的bb坐标for i in range(an):bb_idx = assigned_idx[i]if bb_idx >= 0: # 即非背景cls_labels[i] = label.numpy()[bb_idx, 0] + 1 # 要注意加1assigned_bb[i, :] = label.numpy()[bb_idx, 1:]center_anchor = tf.cast(xy_to_cxcy(anchor), dtype=tf.double)  # (center_x, center_y, w, h)center_assigned_bb = tf.cast(xy_to_cxcy(assigned_bb), dtype=tf.double) # (center_x, center_y, w, h)offset_xy = 10.0 * (center_assigned_bb[:,:2] - center_anchor[:,:2]) / center_anchor[:,2:]offset_wh = 5.0 * tf.math.log(eps + center_assigned_bb[:, 2:] / center_anchor[:, 2:])offset = tf.multiply(tf.concat((offset_xy, offset_wh), axis=1), bbox_mask)    # (锚框总数, 4)return tf.reshape(offset, (-1,)), tf.reshape(bbox_mask, (-1,)), cls_labelsbatch_offset = []batch_mask = []batch_cls_labels = []for b in range(bn):offset, bbox_mask, cls_labels = MultiBoxTarget_one(anchor[0, :, :], label[b,:,:])batch_offset.append(offset)batch_mask.append(bbox_mask)batch_cls_labels.append(cls_labels)batch_offset = tf.convert_to_tensor(batch_offset)batch_mask = tf.convert_to_tensor(batch_mask)batch_cls_labels = tf.convert_to_tensor(batch_cls_labels)return [batch_offset, batch_mask, batch_cls_labels]

code004_cal_offset.py中:

'''将预测框和真实框进行绑定,并且计算预测框相对于真实框的偏移量'''
def anchors_with_IoU_threshold_plot(img,w,h,ground_truth,sizes = [0.25,0.35,0.5], ratios = [1.5,2],jaccard_threshold=0.4):''':param img: 原始图片:param w: 图片的宽:param h: 图片的高:param ground_truth: 真实边界框, (count,4):param sizes: 锚框相对于原始图片的缩放比例:param ratios: 宽高比例:param jaccard_threshold: 预先设定的IoU阈值:return:'''bbox_scale1 = tf.constant([[1, 1 / w, 1 / h, 1 / w, 1 / h]], dtype=tf.float32)  # 用于真实框坐标的归一化ground_truth1 = tf.multiply(ground_truth[:, :],bbox_scale1)  # (1,anchor_count,4) * bbox_scale1对ground truth进行归一化,才能计算IoU# ground_truth归一化x = tf.zeros(shape=(1, 3, 10, 10))  # 模拟利用特征图上每个锚点生成锚框,在原图上生成会存在大量高密度冗余锚框anchors = MultiBoxPrior(x, sizes, ratios)labels = MultiBoxTarget(anchors, tf.expand_dims(ground_truth1, axis=0),jaccard_threshold)print(type(labels))print(len(labels))print()print(labels[0])  # 每个锚框的标注偏移量print()print(labels[1])  # 形状同bbox_offset, 每个锚框的掩码, 一一对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1print()print(labels[2])  # 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)fig = plt.imshow(img)# 绘制IoU最大的锚框cls = ["dog", "cat"]# 预测框bbox_scale = tf.constant([[[w, h, w, h]]], dtype=tf.float32)anchors_boxes = tf.multiply(anchors, bbox_scale)  # 放大锚框#不绘制置信度# for index, anchor_label in enumerate(labels[2][0]):#     if (anchor_label != 0):#         show_bboxes(fig.axes, [anchors_boxes[0,index,:]],#                     labels=[cls[anchor_label - 1]])#绘制置信度anchors_detected = []  #IoU阈值过滤后的锚框,shape=(anchor_count,4)cls_probs = []  #类别置信度(模拟),shape=(anchor_count,类别数)for index, anchor_label in enumerate(labels[2][0]):if (anchor_label != 0):anchors_detected.append(anchors_boxes[0, index, :])temp = [0,0,0]for i in range(0,len(temp)):if(i == anchor_label): #预测类别置信度大temp[i] = random.randint(6, 9) * 0.09else:temp[i] = random.randint(3, 4) * 0.09cls_probs.append(temp)  #背景预测准确度,猫预测准确度,狗预测准确度cls_probs = tf.constant(cls_probs)cls_probs = tf.transpose(cls_probs)  #cls_probs转置, shape=(类别数,anchor_count),注意用transpose,不是reshapepred_probs,pred_labels = tf.reduce_max(cls_probs,axis=0),tf.argmax(cls_probs,axis=0)#绘制最终筛选到的锚框和置信度for index,anchor_detected in enumerate(anchors_detected):if(pred_labels[index] != 0):label = cls[pred_labels[index] - 1] + " : " + str(round(pred_probs[index].numpy(),5))show_bboxes(fig.axes, [anchor_detected],labels=[label])# 真实框show_bboxes(fig.axes, ground_truth[:, 1:], ['dog', 'cat'], 'r')plt.show()  # 可能会因为难以绘制高密度的预测框而卡死class MyTest(unittest.TestCase):#通过IoU阈值来绘制预测框,会因为预测框太多,导致matplotlib绘制失败,因此需要先进行NMS,再进行绘制def test_cal_offset_withBoxAndGT(self):'''Dog1: (c1,c2)=(107,168)Cat1: (c1,c2)=(269,106)'''img = cv2.imread("./img/catAndDog.png")img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)h, w = img.shape[0], img.shape[1]sizes = [0.25, 0.35, 0.5]ratios = [1.5, 2]# 真实的边界框  ground_truth.shape为(bn, 每张图片最多的真实锚框数, 5), 最后一维中的元素为[类别标签, 四个坐标值]ground_truth = tf.constant([[0, 38, 88, 176, 249],[1, 242, 79, 296, 133]], dtype=tf.float32)  #类别标签从0开始(MultiBoxTarget处理时会自动+1)anchors_with_IoU_threshold_plot(img,w,h,ground_truth,sizes,ratios,jaccard_threshold=0.5)
---
<class 'list'>
3tf.Tensor([[ 0.  0. -0. ... -0. -0. -0.]], shape=(1, 1600), dtype=float64)tf.Tensor([[0. 0. 0. ... 0. 0. 0.]], shape=(1, 1600), dtype=float64)tf.Tensor(
[[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 0 1 1 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0]], shape=(1, 400), dtype=int32)

代码解析

  • 这里的ground_truth和上面提到的ground_truth不同在于,前者需要在首位标记上该真实框的类别标签,标签从0开始,即
ground_truth = tf.constant([[0, 38, 88, 176, 249],[1, 242, 79, 296, 133]], dtype=tf.float32)  #类别标签从0开始(MultiBoxTarget处理时会自动+1)

MultiBoxTarget处理时会自动+1,并把0作为背景。

  • 根据锚框与真实边界框在图像中的位置来分析这些标注的类别。

首先,在所有的“锚框—真实边界框”的配对中,锚框 A4A_4A4​与猫的真实边界框的交并比最大,因此锚框 A4A_4A4​的类别标注为猫。

不考虑锚框A4A_4A4​或猫的真实边界框,在剩余的“锚框—真实边界框”的配对中,最大交并比的配对为锚框 A1A_1A1​和狗的真实边界框,因此锚框A1A_1A1​的类别标注为狗。

接下来遍历未标注的剩余398个锚框:

  • 与锚框 A0A_0A0​交并比最大的真实边界框的类别为狗,但交并比小于阈值(默认为0.5),因此类别标注为背景

  • 与锚框 A2A_2A2​交并比最大的真实边界框的类别为猫,且交并比大于阈值,因此类别标注为猫

  • 与锚框 A3A_3A3​交并比最大的真实边界框的类别为猫,但交并比小于阈值,因此类别标注为背景

  • MultiBoxTarget的输出结果是:

  • 返回的第一项是为每个锚框标注的四个偏移量,其中负类锚框的偏移量标注为0。

  • 返回值的第二项掩码(mask)变量,形状为(批量大小, 锚框个数的四倍)。掩码变量中的元素与每个锚框的4个偏移量一一对应。由于我们不关心对背景的检测,有关负类的偏移量不应影响目标函数。通过按元素乘法,掩码变量中的0可以在计算目标函数之前过滤掉负类的偏移量

  • 返回的第三项为每个锚框的标注类别, 其中0表示为背景, 形状为**(bn,锚框总数)**

三、非极大值抑制

1、NMS原理

在模型预测阶段,先为图像生成多个锚框,并为这些锚框一一预测类别和偏移量。

随后,根据锚框及其预测偏移量得到预测边界框

当锚框数量较多时,同一个目标上可能会输出较多相似的预测边界框。为了使结果更加简洁,可以移除相似的预测边界框。常用的方法叫作非极大值抑制(non-maximum suppression,NMS)。

非极大值抑制的工作原理

对于一个预测边界框 B ,模型会计算各个类别的预测概率。设其中最大的预测概率为 p ,该概率所对应的类别即 B 的预测类别,将 p 称为预测边界框 B 的置信度。

在同一图像上,将预测类别非背景的预测边界框按置信度从高到低排序,得到列表 L 。从 L 中选取置信度最高的预测边界框 B1B_1B1​作为基准,将所有与 B1B_1B1​的交并比大于某阈值(预先设定的超参数)的非基准预测边界框从 L 中移除。此时, L 保留了置信度最高的预测边界框并移除了与其相似的其他预测边界框。

接下来,从 L 中选取置信度第二高的预测边界框B2B_2B2​作为基准,将所有与B2B_2B2​的交并比大于某阈值的非基准预测边界框从 L 中移除。

重复该过程,直到 L 中所有的预测边界框都曾作为基准。此时 L 中任意一对预测边界框的交并比都小于阈值。最终,输出列表 L 中的所有预测边界框

实践中,可以在执行非极大值抑制前将置信度较低的预测边界框移除,从而减小非极大值抑制的计算量。此外,还可以筛选非极大值抑制的输出,例如,只保留其中置信度较高的结果作为最终输出。

2、注意点(★★★)

  • 非极值抑制是针对预测框的类别置信度而言的,和IoU无关。目的是为了去除掉多余的预测框,让分类和回归结果更加简洁
  • 实践中,可以在执行非极大值抑制前将置信度较低的预测边界框移除,从而减小非极大值抑制的计算量

3、tf2.0代码

1)NMS控制台输出

utils.py中:

from collections import namedtuple# Returns a new subclass of tuple with named fields.
Pred_BB_Info = namedtuple("Pred_BB_Info", ["index", "class_id", "confidence", "xyxy"])def non_max_suppression(bb_info_list, nms_threshold=0.5):"""非极大抑制处理预测的边界框Args:bb_info_list: Pred_BB_Info的列表, 包含预测类别、置信度等信息nms_threshold: 阈值Returns:output: Pred_BB_Info的列表, 只保留过滤后的边界框信息"""output = []# 现根据置信度从高到底排序sorted_bb_info_list = sorted(bb_info_list,key = lambda x: x.confidence,reverse=True)while len(sorted_bb_info_list) != 0:best = sorted_bb_info_list.pop(0)output.append(best)if len(sorted_bb_info_list) == 0:breakbb_xyxy = []for bb in sorted_bb_info_list:bb_xyxy.append(bb.xyxy)iou = compute_jaccard(tf.convert_to_tensor(best.xyxy),tf.squeeze(tf.convert_to_tensor(bb_xyxy), axis=1))[0] # shape: (len(sorted_bb_info_list), )n = len(sorted_bb_info_list)sorted_bb_info_list = [sorted_bb_info_list[i] for i inrange(n) if iou[i] <= nms_threshold]return output'''非极值抑制是针对预测框的类别置信度而言的,和IoU无关,目的是为了去除掉多余的预测框,让分类和回归结果更加简洁'''
def MultiBoxDetection(cls_prob, loc_pred, anchor, nms_threshold=0.5):"""非极大值抑制 # anchor表示成归一化(xmin, ymin, xmax, ymax).https://zh.d2l.ai/chapter_computer-vision/anchor.htmlArgs:cls_prob: 经过softmax后得到的各个锚框的预测概率, shape:(bn, 预测总类别数+1, 锚框个数)loc_pred: 预测的各个锚框的偏移量, shape:(bn, 锚框个数*4)anchor: MultiBoxPrior输出的默认锚框, shape: (1, 锚框个数, 4)nms_threshold: 非极大抑制中的阈值Returns:所有锚框的信息, shape: (bn, 锚框个数, 6)每个锚框信息由[class_id, confidence, xmin, ymin, xmax, ymax]表示class_id=-1 表示背景或在非极大值抑制中被移除了"""assert len(cls_prob.shape) == 3 and len(loc_pred.shape) == 2 and len(anchor.shape) == 3bn = cls_prob.shape[0]def MultiBoxDetection_one(c_p, l_p, anc, nms_threshold=0.5):"""MultiBoxDetection的辅助函数, 处理batch中的一个Args:c_p: (预测总类别数+1, 锚框个数)l_p: (锚框个数*4, )anc: (锚框个数, 4)nms_threshold: 非极大抑制中的阈值Return:output: (锚框个数, 6)返回的结果的形状为(批量大小, 锚框个数, 6)。其中,每一行的6个元素代表同一个预测边界框的输出信息:第一个元素是索引从0开始计数的预测类别(0为狗,1为猫),而 -1表示背景或在非极大值抑制中被移除;第二个元素是预测边界框的置信度;剩余的4个元素分别是预测边界框左上角的x和y轴坐标以及右下角的x和y轴坐标(值域在0到1之间)。"""pred_bb_num = c_p.shape[1]# 加上偏移量anc = tf.add(anc, tf.reshape(l_p, (pred_bb_num, 4))).numpy()# 最大的概率confidence = tf.reduce_max(c_p, axis=0)# 最大概率对应的idclass_id = tf.argmax(c_p, axis=0)confidence = confidence.numpy()class_id = class_id.numpy()pred_bb_info = [Pred_BB_Info(index=i,class_id=class_id[i]-1,confidence=confidence[i],xyxy=[anc[i]]) # xyxy是个列表for i in range(pred_bb_num)]# 正类的indexobj_bb_idx = [bb.index for bbin non_max_suppression(pred_bb_info,nms_threshold)]output = []for bb in pred_bb_info:output.append(np.append([(bb.class_id if bb.index in obj_bb_idxelse -1.0),bb.confidence],bb.xyxy))return tf.convert_to_tensor(output) # shape: (锚框个数, 6)batch_output = []for b in range(bn):batch_output.append(MultiBoxDetection_one(cls_prob[b],loc_pred[b], anchor[0],nms_threshold))return tf.convert_to_tensor(batch_output)

code005_cal_NMS_test.py中:

class MyTest(unittest.TestCase):def test_NMS(self):''':return:'''anchors = tf.convert_to_tensor([[0.1, 0.08, 0.52, 0.92],[0.08, 0.2, 0.56, 0.95],[0.15, 0.3, 0.62, 0.91],[0.55, 0.2, 0.9, 0.88]])  #先构造4个锚框,预测边界框即锚框offset_preds = tf.convert_to_tensor([0.0] * (4 * len(anchors)))  #简单起见,假设预测偏移量全是0cls_probs = tf.convert_to_tensor([[0., 0., 0., 0.],  # 背景的预测概率[0.9, 0.8, 0.7, 0.1],  # 狗的预测概率[0.1, 0.2, 0.3, 0.9]])  # 猫的预测概率print(f"anchors = {anchors}")print()print(f"offset_preds = {offset_preds}")print()print(f"cls_probs = {cls_probs}")output = MultiBoxDetection(tf.expand_dims(cls_probs, 0),tf.expand_dims(offset_preds, 0),tf.expand_dims(anchors, 0),nms_threshold=0.5)print(f"output.shape = {output.shape}, output = {output}")  #返回的结果的形状为(批量大小, 锚框个数, 6),第一个元素是索引从0开始计数的预测类别(0为狗,1为猫),# 而 -1表示背景或在非极大值抑制中被移除;第二个元素是预测边界框的置信度;剩余的4个元素分别是预测边界框左上角的x和y轴坐标以及右下角的x和y轴坐标(值域在0到1之间)。
---

代码解析

  • MultiBoxDetection的输出: 返回的结果的形状为**(批量大小, 锚框个数, 6)**。其中,每一行的6个元素代表同一个预测边界框的输出信息:
  • 第一个元素是索引从0开始计数的预测类别(0为狗,1为猫),而 -1表示背景或在非极大值抑制中被移除
  • 第二个元素是预测边界框的置信度;
  • 剩余的4个元素分别是预测边界框左上角的x和y轴坐标以及右下角的x和y轴坐标(值域在0到1之间)。

2)NMS锚框绘制

code005_cal_NMS_test.py中:

'''根据分类标签的置信度进行非极大值抑制'''
def anchors_with_NMS_plot(img,w,h,ground_truth,sizes = [0.25,0.35,0.5], ratios = [1.5,2], jaccard_threshold=0.4, nms_threshold=0.5):''':param img: 原始图片:param w: 图片的宽:param h: 图片的高:param ground_truth: 真实边界框, (count,4):param sizes: 锚框相对于原始图片的缩放比例:param ratios: 宽高比例:param jaccard_threshold: 预先设定的IoU阈值:param nms_threshold: 预先设定的NMS阈值:return:'''bbox_scale1 = tf.constant([[1, 1 / w, 1 / h, 1 / w, 1 / h]], dtype=tf.float32)  # 用于真实框坐标的归一化ground_truth1 = tf.multiply(ground_truth[:, :],bbox_scale1)  # (1,anchor_count,4) * bbox_scale1对ground truth进行归一化,才能计算IoU# ground_truth归一化x = tf.zeros(shape=(1, 3, 10, 10))  # 模拟利用特征图上每个锚点生成锚框,在原图上生成会存在大量高密度冗余锚框anchors = MultiBoxPrior(x, sizes, ratios)labels = MultiBoxTarget(anchors, tf.expand_dims(ground_truth1, axis=0), jaccard_threshold)print(type(labels))print(len(labels))print()print(labels[0])  # 每个锚框的标注偏移量print()print(labels[1])  # 形状同bbox_offset, 每个锚框的掩码, 一一对应上面的偏移量, 负类锚框(背景)对应的掩码均为0, 正类锚框的掩码均为1print()print(labels[2])  # 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)fig = plt.imshow(img)# 绘制IoU最大的锚框cls = ["dog", "cat"]# 预测框bbox_scale = tf.constant([[[w, h, w, h]]], dtype=tf.float32)anchors_boxes = tf.multiply(anchors, bbox_scale)  # 放大锚框# 不绘制置信度# for index, anchor_label in enumerate(labels[2][0]):#     if (anchor_label != 0):#         show_bboxes(fig.axes, [anchors_boxes[0,index,:]],#                     labels=[cls[anchor_label - 1]])# 绘制置信度anchors_detected = []  # IoU阈值过滤后的锚框,shape=(anchor_count,4)cls_probs = []  # 类别置信度(模拟),shape=(anchor_count,类别数)for index, anchor_label in enumerate(labels[2][0]):if (anchor_label != 0):anchors_detected.append(anchors_boxes[0, index, :].numpy().tolist())temp = [0, 0, 0]for i in range(0, len(temp)):if (i == anchor_label):  # 预测类别置信度大temp[i] = random.randint(6, 9) * 0.09else:temp[i] = random.randint(3, 4) * 0.09cls_probs.append(temp)  # 背景预测准确度,猫预测准确度,狗预测准确度cls_probs = tf.constant(cls_probs)cls_probs = tf.transpose(cls_probs)  # cls_probs转置, shape=(类别数,anchor_count),注意用transpose,不是reshapeoffset_preds = tf.convert_to_tensor([0.0] * (4 * len(anchors_detected)))  # 简单起见,假设预测偏移量全是0anchors = tf.constant(anchors_detected)# anchors = tf.divide(anchors,bbox_scale) #anchors缩放至0,1区间上'''NMS非极大值抑制'''output = MultiBoxDetection(tf.expand_dims(cls_probs, 0),tf.expand_dims(offset_preds, 0),tf.expand_dims(anchors, 0),nms_threshold=nms_threshold)  #返回的结果的形状为(批量大小, 锚框个数, 6),第一个元素是索引从0开始计数的预测类别(0为狗,1为猫),# 而 -1表示背景或在非极大值抑制中被移除;第二个元素是预测边界框的置信度;剩余的4个元素分别是预测边界框左上角的x和y轴坐标以及右下角的x和y轴坐标(值域在0到1之间)。'''绘制最终筛选到的锚框和类别置信度'''for i in output[0].numpy():if i[0] == -1:continuelabel = ('dog=', 'cat=')[int(i[0])] + str(round(i[1],6))   #根据索引获取元组元素box = tf.constant([i[2:].tolist()])show_bboxes(fig.axes, box, label)# 真实框show_bboxes(fig.axes, ground_truth[:, 1:], ['dog', 'cat'], 'r')plt.show()  # 可能会因为难以绘制高密度的预测框而卡死class MyTest(unittest.TestCase):def test_NMS_withBoxAndGT(self):'''Dog1: (c1,c2)=(107,168)Cat1: (c1,c2)=(269,106)'''img = cv2.imread("./img/catAndDog.png")img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)h, w = img.shape[0], img.shape[1]sizes = [0.25, 0.35, 0.5]ratios = [1.5, 2]# 真实的边界框  ground_truth.shape为(bn, 每张图片最多的真实锚框数, 5), 最后一维中的元素为[类别标签, 四个坐标值]ground_truth = tf.constant([[0, 38, 88, 176, 249],[1, 242, 79, 296, 133]], dtype=tf.float32)  # 类别标签从0开始(MultiBoxTarget处理时会自动+1)anchors_with_NMS_plot(img, w, h, ground_truth, sizes, ratios, jaccard_threshold=0.6)

代码解析

  • 由于没有模型进行锚框的类别预测,因此这里需要为每个锚框赋予与真实框IoU值最大的类别,置信度模拟生成代码如下:
 anchors_detected = []  # IoU阈值过滤后的锚框,shape=(anchor_count,4)cls_probs = []  # 类别置信度(模拟),shape=(anchor_count,类别数)for index, anchor_label in enumerate(labels[2][0]):if (anchor_label != 0):anchors_detected.append(anchors_boxes[0, index, :].numpy().tolist())temp = [0, 0, 0]for i in range(0, len(temp)):if (i == anchor_label):  # 预测类别置信度大temp[i] = random.randint(6, 9) * 0.09else:temp[i] = random.randint(3, 4) * 0.09cls_probs.append(temp)  # 背景预测准确度,猫预测准确度,狗预测准确度cls_probs = tf.constant(cls_probs)cls_probs = tf.transpose(cls_probs)  # cls_probs转置, shape=(类别数,anchor_count),注意用transpose,不是reshapeoffset_preds = tf.convert_to_tensor([0.0] * (4 * len(anchors_detected)))  # 简单起见,假设预测偏移量全是0anchors = tf.constant(anchors_detected)# anchors = tf.divide(anchors,bbox_scale) #anchors缩放至0,1区间上
  • 使用上面的NMS代码,anchors无需转换到[0,1]区间上,直接使用预测框真实坐标即可

锚框、交并比和非极大值抑制(tf2.0源码解析)相关推荐

  1. 下拉多选择框 实现方式_非极大值抑制Non-Maximum Suppression(NMS)一文搞定理论+多平台实现...

    这是独立于薰风读论文的投稿,作为目标检测模型的拓展阅读,目的是帮助读者详细了解一些模型细节的实现. 薰风说 Non-Maximum Suppression的翻译是非"极大值"抑制, ...

  2. 锚框的实现-非极大值抑制预测边界框

    大家好,我是阿林.由于各种原因,阿林的组会告吹了.所以阿林的锚框的最后一期非极大值抑制预测边界框提前发布. 我们来回顾一下前两期的内容,并和第三期做一个总结吧. 第一期 我们通过提取出整张图的高宽,利 ...

  3. 手写非极大值抑制代码(NMS)

    在物体检测领域当中,非极大值抑制应用十分广泛,目的是为了消除多余的框,找到最佳的物体检测的位置.那么具体如何操作呢?如下图所示,有三个boundingbox,其中第一个绿色boundingbox的置信 ...

  4. array python 交集_NMS原理(非极大值抑制)+python实现

    1.先解释什么叫IoU(intersection-over-union).IoU表示(A∩B)/(A∪B) 即交并比. 非极大值抑制:图一 --> 图二 ,剔除同一个目标上的重叠建议框,最终一个 ...

  5. part 4:置信度阈值化和非极大值抑制

    翻译原文:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch- ...

  6. 交并比 (IoU), mAP (mean Average Precision), 非极大值抑制 (NMS, Soft NMS, Softer NMS, IoU-Net)

    目录 目标检测的评价指标 交并比 (Intersection of Union, IoU) mAP (mean Average Precision) 其他指标 非极大值抑制 (Non-Maximum ...

  7. 目标定位和检测系列:交并比(IOU)和非极大值抑制(NMS)的python与C/C++实现

    Python实现 交并比(Intersection over Union)和非极大值抑制是(Non-Maximum Suppression)是目标检测任务中非常重要的两个概念.例如在用训练好的模型进行 ...

  8. 在Python中用cupy实现IoU(交并比)和NMS(非极大值抑制)的GPU加速

    1. 前言 IoU(交并比)和NMS(非极大值抑制)的计算在目标检测任务中可以说是必不可少的,但是当需要计算的bounding box的数量级很大的时候,cpu就吃不消了.例如在对Faster RCN ...

  9. 目标检测中的LOU(交并比)和NMS(非极大值抑制)代码实现

    1.LOU, 两个box框的交集比上并集,示意图如下所示: 代码如下所示: #假设box1的维度为[N,4] box2的维度为[M,4] def Lou(box1, box2):N = box1.si ...

最新文章

  1. SpringBoot 中 JPA 的使用
  2. Cortex-A9 UART
  3. java 32个Java面试必考点
  4. DButils数据库升级不丢失数据
  5. Azure云端部署Exchange 2016双数据中心—Part6(DAG切换测试)
  6. Excel中 插入 对号等特殊字符
  7. java堆排序图解_108-堆排序的思路图解_清华毕业老程序员亲授通俗易懂的Java数据结构和算法​​​​教程_Java视频-51CTO学院...
  8. linux执行ksh文件,关于linux:KSH shell,它对目录中的文件行进行计数
  9. 苗族php动态网页设计作业
  10. 小客车年检(年审)相关的技术参数一览
  11. 《Python金融大数据风控建模实战》 第5章 变量编码方法
  12. Eclipse快捷键、Debug调试
  13. 两组的数据平均值合并_数据平均值合并计算 合并计算求平均值
  14. Android R系统aidl文件怎么对应的java文件找不到了?
  15. Java~大厂面试八股文~强烈推荐视频
  16. 当我在ChatGPT上问重建大师,它居然这样回答我
  17. Windows彩色桌面变成灰色,怎么办?
  18. 【HTML第二个综合案例】----相亲App注册页面
  19. SQL 语法查询手册
  20. font face=微软雅黑 color=DodgerBlue*IncomesESL Analy*/font

热门文章

  1. 《奇点来临》——镜子测试与认知
  2. 学校计算机教室报损登记本,平阴县中小学功能室管理基本要求
  3. 【笔记】 数字集成电路设计(一)
  4. 华钜同创:跨境开店如何选择跨境电商平台
  5. springcloud+fastdfs在docker中设置防盗链
  6. oracle 根据已有表创建新表
  7. 八防区模块接线图_消防模块接线方法和接线图【借鉴实操】
  8. 技术人员的赚钱之道-1:开篇
  9. 三大开源社区是哪几个_3个衡量开源社区健康的指标
  10. linux xunsou_Linux下的迅搜(xunsearch)安装使用教程,并设置成开机启动服务