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

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


参考

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

一、锚框生成

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

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

1、锚框的宽高

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

2、锚框的个数

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

因此,通常只对包含 s1 的大小与宽高比 r1 的组合感兴趣,即:
( s 1 , r 1 ) , ( s 1 , r 2 ) , . . . , ( s 1 , r m ) , ( s 2 , r 1 ) , . . . , ( s n , r 1 ) (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)
比如 s i z e s = [ 0.3 , 0.5 , 0.75 ] sizes = [0.3, 0.5, 0.75] sizes=[0.3,0.5,0.75] r a t i o s = [ 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 − 1 n + m - 1 n+m1, 对于整个输入图像,将一共生成 w h ( n + m − 1 ) wh(n+m-1) wh(n+m1)的anchor

3、注意点(★★★)

参考

  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.html
    Args:
        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转1D
    shift_y = tf.reshape(shift_y, (-1,))   #2D转1D
    shifts = 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_values
        elif not isinstance(obj, (list, tuple)):
            obj = [obj]
        return obj

    labels = _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 display

class 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) (h1,w1)的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+21)1010=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)。

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

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

假设图像中锚框分别为 A 1 , A 2 , … , A n a A_1, A_2 , … , A_{na} A1,A2,,Ana真实边界框分别为 B 1 , B 2 , … , B n b B_1,B_2,…,B_{n_b} B1,B2,,Bnb,且 n a ≥ n b n_a≥n_b nanb

定义矩阵 X ∈ R n a × n b X∈R^{n_a×n_b} XRna×nb,其中第 i 行第 j 列的元素 x i j x_{ij} xij 为锚框 A i A_i Ai与真实边界框 B j B_j Bj 的交并比。

首先,找出矩阵 X X X中最大元素,并将该元素的行索引与列索引分别记为 i 1 i_1 i1, j 1 j_1 j1 。为锚框 A i 1 A_{i1} Ai1 分配真实边界框 B j 1 B_{j1} Bj1
显然,锚框 A i 1 A_{i1} Ai1和真实边界框 B j 1 B_{j1} Bj1在所有的“锚框—真实边界框”的配对中相似度最高

接下来,将矩阵 X X X中第 i 1 i_1 i1 行和第 j 1 j_1 j1 列上的所有元素丢弃。找出矩阵 X X X中剩余的最大元素,并将该元素的行索引与列索引分别记为 i 2 , j 2 i_2,j_2 i2,j2
为锚框 A i 2 A_{i2} Ai2 分配真实边界框 B j 2 B_{j2} Bj2 ,再将矩阵 X X X中第 i 2 i_2 i2行和第 j 2 j_2 j2列上的所有元素丢弃。
此时矩阵 X X X 中已有2行2列的元素被丢弃。
依此类推,直到矩阵 X X X中所有 n b n_b nb列元素全部被丢弃

此时,已为 n b n_b nb个锚框各分配了一个真实边界框。之后,只遍历剩余的 n a − n b n_a−n_b nanb个锚框
给定其中的锚框 A i A_i Ai,根据矩阵 X X X 的第 i 行找到与 A i A_i Ai交并比最大的真实边界框 B j B_j Bj ,且只有当该交并比大于预先设定的阈值时,才为锚框 A i A_i Ai 分配真实边界框 B j B_j Bj

如上图(左)所示,假设矩阵 X X X中最大值为 x 23 x_{23} x23 ,我们将为锚框 A 2 A_2 A2分配真实边界框 B 3 B_3 B3 。然后,丢弃矩阵中第2行和第3列的所有元素,找出剩余阴影部分的最大元素 x 71 x_{71} x71 ,为锚框 A 7 A_7 A7分配真实边界框 B 1 B_1 B1

接着如上图(中)所示,丢弃矩阵中第7行和第1列的所有元素,找出剩余阴影部分的最大元素 x 54 x_{54} x54 ,为锚框 A 5 A_5 A5 分配真实边界框 B 4 B_4 B4

最后如上图(右)所示,丢弃矩阵中第5行和第4列的所有元素,找出剩余阴影部分的最大元素 x 92 x_{92} x92 ,为锚框 A 9 A_9 A9 分配真实边界框 B 2 B_2 B2

之后,只需遍历除去 A 2 , A 5 , A 7 , A 9 A_2,A_5,A_7,A_9 A2,A5,A7,A9 的剩余锚框,并根据阈值判断是否为剩余锚框分配真实边界框

3、标注锚框

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

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

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

设锚框 A 及其被分配的真实边界框 B 的中心坐标分别为 ( x a , y a ) ( x_a , y_a ) (xa,ya) ( x b , y b ) ( x_b , y_b ) (xb,yb) , A 和 B 的宽分别为 w a w_a wa w b w_b wb,高分别为 h a h_a ha h b h_b hb ,一个常用的技巧是将 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 dimensions
    lower_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 sets
    areas_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,MultiBoxPrior

class 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,346
        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_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 sets
        areas_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) - 1
    boxes = 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, :, :]}")  # dog
    print(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进行归一化,才能计算IoU

    set_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.html
    Args:
        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] = j
        jaccard_cp[i, :] = float("-inf")    # 赋值为负无穷, 相当于去掉这一行

    # 处理还未被分配的anchor, 要求满足jaccard_threshold
    for i in range(na):
        if assigned_idx[i] == -1:
            j = np.argmax(jaccard[i, :])
            if jaccard[i, j] >= jaccard_threshold:
                assigned_idx[i] = j
    return 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.py
    Args:
        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_y
              xy[:, 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.html
    Args:
        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, 正类锚框的掩码均为1
        cls_labels: 每个锚框的标注类别, 其中0表示为背景, 形状为(bn,锚框总数)
    """
    assert len(anchor.shape) == 3 and len(label.shape) == 3
    bn = label.shape[0]

    def MultiBoxTarget_one(anchor, label, eps=1e-6):
        """
        MultiBoxTarget函数的辅助函数, 处理batch中的一个
        Args:
            anchor: shape of (锚框总数, 4)
            label: shape of (真实锚框数, 5), 5代表[类别标签, 四个坐标值]
            eps: 一个极小值, 防止log0
        Returns:
            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 # 要注意加1
                assigned_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_labels

    batch_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, 正类锚框的掩码均为1
    print()
    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.09
                else:
                    temp[i] = random.randint(3, 4) * 0.09

            cls_probs.append(temp)  #背景预测准确度,猫预测准确度,狗预测准确度

    cls_probs = tf.constant(cls_probs)
    cls_probs = tf.transpose(cls_probs)  #cls_probs转置, shape=(类别数,anchor_count),注意用transpose,不是reshape
    pred_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'>
3

tf.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 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 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 0 0 0
  0 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 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 1 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 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 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 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 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 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 0 0 0 0 0 0 0 0 0 0 0
  0 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作为背景。

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

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

不考虑锚框 A 4 A_4 A4或猫的真实边界框,在剩余的“锚框—真实边界框”的配对中,最大交并比的配对为锚框 A 1 A_1 A1和狗的真实边界框,因此锚框 A 1 A_1 A1的类别标注为狗。

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

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

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

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

  • MultiBoxTarget的输出结果是:

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

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

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

三、非极大值抑制

1、NMS原理

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

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

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

非极大值抑制的工作原理

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

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

接下来,从 L 中选取置信度第二高的预测边界框 B 2 B_2 B2作为基准,将所有与 B 2 B_2 B2的交并比大于某阈值的非基准预测边界框从 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:
            break
        bb_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 in
                    range(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.html
    Args:
        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) == 3
    bn = 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)
        # 最大概率对应的id
        class_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)]
        # 正类的index
        obj_bb_idx = [bb.index for bb
                in 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_idx
                        else -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)))  #简单起见,假设预测偏移量全是0

        cls_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, 正类锚框的掩码均为1
    print()
    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.09
                else:
                    temp[i] = random.randint(3, 4) * 0.09

            cls_probs.append(temp)  # 背景预测准确度,猫预测准确度,狗预测准确度


    cls_probs = tf.constant(cls_probs)
    cls_probs = tf.transpose(cls_probs)  # cls_probs转置, shape=(类别数,anchor_count),注意用transpose,不是reshape
    offset_preds = tf.convert_to_tensor([0.0] * (4 * len(anchors_detected)))  # 简单起见,假设预测偏移量全是0
    anchors = 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:
            continue
        label = ('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.09
                else:
                    temp[i] = random.randint(3, 4) * 0.09

            cls_probs.append(temp)  # 背景预测准确度,猫预测准确度,狗预测准确度


    cls_probs = tf.constant(cls_probs)
    cls_probs = tf.transpose(cls_probs)  # cls_probs转置, shape=(类别数,anchor_count),注意用transpose,不是reshape
    offset_preds = tf.convert_to_tensor([0.0] * (4 * len(anchors_detected)))  # 简单起见,假设预测偏移量全是0
    anchors = tf.constant(anchors_detected)
    # anchors = tf.divide(anchors,bbox_scale) #anchors缩放至0,1区间上
  • 使用上面的NMS代码,anchors无需转换到[0,1]区间上,直接使用预测框真实坐标即可
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个简单的CNN训练和非极大抑制代码示例,使用了TensorFlow库: ```python import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense from tensorflow.keras.optimizers import Adam from tensorflow.keras.utils import to_categorical from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras.layers import Lambda import numpy as np import cv2 # 构建CNN模型 model = Sequential() model.add(Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1))) model.add(MaxPooling2D((2,2))) model.add(Conv2D(64, (3,3), activation='relu')) model.add(MaxPooling2D((2,2))) model.add(Conv2D(64, (3,3), activation='relu')) model.add(Flatten()) model.add(Dense(64, activation='relu')) model.add(Dense(10, activation='softmax')) # 编译模型 model.compile(optimizer=Adam(lr=0.001), loss='categorical_crossentropy', metrics=['accuracy']) # 加载数据集 (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() # 将图像转换为float类型并进行归一化 x_train = x_train.astype('float32') / 255 x_test = x_test.astype('float32') / 255 # 将标签转换为one-hot编码 y_train = to_categorical(y_train) y_test = to_categorical(y_test) # 对训练数据进行增强 datagen = ImageDataGenerator(rotation_range=10, zoom_range=0.1, width_shift_range=0.1, height_shift_range=0.1) datagen.fit(x_train) # 训练模型 model.fit(datagen.flow(x_train, y_train, batch_size=32), epochs=10, validation_data=(x_test, y_test)) # 非极大抑制 def non_max_suppression(boxes, scores, threshold): # 计算框的面积 areas = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) # 根据得分从高到低排序 order = scores.argsort()[::-1] keep = [] while order.size > 0: # 取得分最高的框 i = order[0] keep.append(i) # 计算当前框与其它框的交集面积 xx1 = np.maximum(boxes[i, 0], boxes[order[1:], 0]) yy1 = np.maximum(boxes[i, 1], boxes[order[1:], 1]) xx2 = np.minimum(boxes[i, 2], boxes[order[1:], 2]) yy2 = np.minimum(boxes[i, 3], boxes[order[1:], 3]) w = np.maximum(0, xx2 - xx1) h = np.maximum(0, yy2 - yy1) inter = w * h # 计算重叠度 overlap = inter / (areas[i] + areas[order[1:]] - inter) # 保留重叠度小于阈值的框 inds = np.where(overlap <= threshold)[0] order = order[inds + 1] return keep # 测试模型 img = cv2.imread('test.jpg') img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img_gray = cv2.resize(img_gray, (28, 28)) img_gray = img_gray.astype('float32') / 255 img_gray = np.expand_dims(img_gray, axis=2) img_gray = np.expand_dims(img_gray, axis=0) predictions = model.predict(img_gray) class_idx = np.argmax(predictions) class_output = model.output[:, class_idx] last_conv_layer = model.get_layer('conv2d_3') grads = K.gradients(class_output, last_conv_layer.output)[0] pooled_grads = K.mean(grads, axis=(0, 1, 2)) iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]]) pooled_grads_value, conv_layer_output_value = iterate([img_gray]) for i in range(64): conv_layer_output_value[:, :, i] *= pooled_grads_value[i] heatmap = np.mean(conv_layer_output_value, axis=-1) heatmap = np.maximum(heatmap, 0) heatmap /= np.max(heatmap) heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0])) heatmap = np.uint8(255 * heatmap) heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) output_image = cv2.addWeighted(img, 0.5, heatmap, 0.5, 0) cv2.imwrite('output.jpg', output_image) # 非极大抑制 boxes = np.array([[40, 40, 100, 100], [50, 50, 120, 120], [60, 60, 130, 130]]) scores = np.array([0.9, 0.75, 0.8]) threshold = 0.5 keep = non_max_suppression(boxes, scores, threshold) print(keep) ``` 在上面的代码中,我们首先定义了一个简单的CNN模型,并使用Keras库进行训练。然后,我们使用OpenCV库的函数对一张测试图片进行预测,并生成热力图。接着,我们定义了一个非极大抑制的函数,用于过滤掉重叠度高的框。最后,我们对一组测试框进行非极大抑制,并输出保留的框的索引。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值