目标检测系列一:RetinaNet之anchor

前言

自进入吉大,开始硕士生活之后,本人便很少有时间写一些blog,毕竟忙起来之后时间真的比较紧,不过细想想感觉自己也没忙出个什么。这次,导师给安排的项目是病变细胞检测的项目(Emmmmm,这是我的第三个项目,我研一),在深度学习领域上,属于目标检测的范畴。我主要采用的是RetinaNet,那么今天简单总结一下anchor的相关知识。


什么是Anchor

为什么会出现anchor、anchor处理之前目标检测用的是什么技术?这些问题本次一一不涉及
从学术的角度来说,anchor就是拥有多尺度、多种宽高比例的box,也就是multi-scale and different aspect ratio 。
我们知道,图片分类和实例分割分别属于image-level和pix-level,而目标检测则属于region-level。那么region的信息,我们是如何向网络提供的呢?
anchor的设置,相当于提前在图片上做预选,通常在生成anchor时,每一张图片上将会对应成千上万的anchor也就是候选框,可想而知,如此庞大的候选框数量,肯定能够满足对图上实例进行检测的需要,效果如下图所示:目标检测系列一:RetinaNet之anchor_第1张图片
可以看到,整张图片上存在许多不同形状的小框框,但是这些框框的总数也只是原anchor数量的1%。
那么有了这些框框,网络便可以学习这些框框中的事物以及框框的位置,最终可以进行分类和回归(还涉及IOU的计算等等,本篇文章不涉及)。


numpy + python 实现anchor

我们以Retinanet网络中的anchor为例,使用numpy和python生成,具体RetinaNet网络中的anchor是什么形式的,请移步ICCV2017kaiming大神的论文,可详细了解。
anchor-size:[32,64,128,256,512]
每个anchor-size对应着三种scale和三个ratio,那么每个anchor-size将对应生成9个先验框,同时生成的所有先验框均满足:

  • 同一种scale,面积相同,形状不同
  • 同一种ratio,形状相同,面积不同
  • 对于scale和ratio的理解:scale指anchor的大小(可以当做宽)、ratio即为宽高比

下面为代码思路,我们以32为例:

  • 首先,由于存在三种scale和三种ratio,则对于w=h=32的基本anchor,则有存在9种anchor。
  • 其次对于每一个anchor,我么要保存其左上角和右下角两个点的坐标,需要四个位置进行保存。
  • 综上,我们生成形状为 [9,4]的数组
	sizes   = [32, 64, 128, 256, 512]
    strides = [8, 16, 32, 64, 128]
    ratios  = np.array([0.5, 1, 2], keras.backend.floatx())
    scales  = np.array([2 ** 0, 2 ** (1.0 / 3.0), 2 ** (2.0 / 3.0)],keras.backend.floatx())
    num_anchors = len(ratios) * len(scales)
	anchors = np.zeros((num_anchors, 4))

生成9*4的numpy数组

[[ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]

接下来,需要求解以w=h=32为基础的9种anchor的宽和高。

  • 首先,我们需要求出三种scale的anchor对应的宽(W),此时只是做scale变换,不涉及ratios,自然所求的anchor都是大小不一的正方形。
	anchors[:, 2:] = base_size * np.tile(scales, (2, len(ratios))).T

这条语句将生成anchor_size=32在三种scale下的9个宽、高。

[[ 32.          32.        ]
 [ 40.31747437  40.31747437]
 [ 50.79683304  50.79683304]
 [ 32.          32.        ]
 [ 40.31747437  40.31747437]
 [ 50.79683304  50.79683304]
 [ 32.          32.        ]
 [ 40.31747437  40.31747437]
 [ 50.79683304  50.79683304]]

在这里,我们就可以简单理解:第一列即为宽、第二列即为高。这样我们便得到了9个大小不一的正方形anchor。
接下来将做ratio变换:
ratio变换的实质:将以上生成的正方形anchor按照一定比例缩放,求得缩放后的宽,保持scale不变,求得高。
求出每个anchor的面积:

	areas = anchors[:, 2] * anchors[:, 3]

结果:

[ 1024. 1625.49873919  2580.31824672  1024. 1625.49873919
  2580.31824672  1024. 1625.49873919  2580.31824672]

求出每个anchor按照比例缩放后,得到的宽和高:

    anchors[:, 2] = np.sqrt(areas / np.repeat(ratios, len(scales)))
    anchors[:, 3] = anchors[:, 2] * np.repeat(ratios, len(scales))

结果:

[[ 45.254834    22.627417  ]
 [ 57.01751905  28.50875952]
 [ 71.83757021  35.9187851 ]
 [ 32.          32.        ]
 [ 40.31747437  40.31747437]
 [ 50.79683304  50.79683304]
 [ 22.627417    45.254834  ]
 [ 28.50875952  57.01751905]
 [ 35.9187851   71.83757021]]

截止至此,我们得到anchor的数组情况为:

[[  0.           0.          45.254834    22.627417  ]
 [  0.           0.          57.01751905  28.50875952]
 [  0.           0.          71.83757021  35.9187851 ]
 [  0.           0.          32.          32.        ]
 [  0.           0.          40.31747437  40.31747437]
 [  0.           0.          50.79683304  50.79683304]
 [  0.           0.          22.627417    45.254834  ]
 [  0.           0.          28.50875952  57.01751905]
 [  0.           0.          35.9187851   71.83757021]]

这种形式的坐标,并不是我们想要的,因为它只记录了当前anchor的宽和高。并没有给出坐标信息。
那么我们采取的办法是,以当前anchor的中心点为坐标原点建立直角坐标系,求出左上角坐标和右下角坐标,存入当前数组,格式为(x1,y1,x2,y2)。

 	anchors[:, 0::2] -= np.tile(anchors[:, 2] * 0.5, (2, 1)).T
    anchors[:, 1::2] -= np.tile(anchors[:, 3] * 0.5, (2, 1)).T

此时的anchor数组为:

[[-22.627417   -11.3137085   22.627417    11.3137085 ]
 [-28.50875952 -14.25437976  28.50875952  14.25437976]
 [-35.9187851  -17.95939255  35.9187851   17.95939255]
 [-16.         -16.          16.          16.        ]
 [-20.15873718 -20.15873718  20.15873718  20.15873718]
 [-25.39841652 -25.39841652  25.39841652  25.39841652]
 [-11.3137085  -22.627417    11.3137085   22.627417  ]
 [-14.25437976 -28.50875952  14.25437976  28.50875952]
 [-17.95939255 -35.9187851   17.95939255  35.9187851 ]]

anchor-size=32的anchor生成过程如上所示,其他同理。


将anchor映射到FPN的每一层

以上我们得到的只是当前anchor相对于自身中心点的坐标,我们还要将anchor映射到特征金字塔的每一层的feature map上,我们以p3为例,p3的尺度为75*75。
那么映射的思路为:

  • 首先生成75*75的网格坐标
  • 对于每一个网格,将9个anchor分别嵌入进去,得到基于网格的anchor坐标
    shift_x = (np.arange(0, shape[1], dtype=keras.backend.floatx()) + 0.5) * stride
    shift_y = (np.arange(0, shape[0], dtype=keras.backend.floatx()) + 0.5) * stride
    shift_x, shift_y = np.meshgrid(shift_x, shift_y)
    shift_x = np.reshape(shift_x, [-1])
    shift_y = np.reshape(shift_y, [-1])
    shifts = np.stack([
        shift_x,
        shift_y,
        shift_x,
        shift_y
    ], axis=0)
    shifts            = np.transpose(shifts)

此处,我们要讲清楚,为什么要加0.5,其实我们把75*75看成网格,那么我们所求的网格点坐标其实都是每个小网格中心点的坐标,网格的边长是1。中心点肯定是要乘上0.5的。
最终我们得到的结果为:

[[   4.    4.    4.    4.]
 [  12.    4.   12.    4.]
 [  20.    4.   20.    4.]
 ...,
 [ 580.  596.  580.  596.]
 [ 588.  596.  588.  596.]
 [ 596.  596.  596.  596.]]

此处我们构造了和anchor相同的结构,网格上第一列和第三列、第二列和第四列都是相同的。第一列是网格上7575个点的横坐标,第二列是7575个点的纵坐标。接下来,我们便可以直接做映射。
在上述中,我们得到的anchor坐标实际上可以看做是以anchor本身的中心点为坐标原点得到的,那么坐标值实际上可以看做是左上角和右下角两个点对中心点的偏移量。那么如果我们将中心点换做p3的feature map的网格点,然后将偏移量叠加至上面,不就完成了anchor到feature map的映射嘛。

	number_of_anchors = np.shape(anchors)[0]
    k = np.shape(shifts)[0]
    shifted_anchors = np.reshape(anchors, [1, number_of_anchors, 4]) + np.array(np.reshape(shifts, [k, 1, 4]), keras.backend.floatx())
    shifted_anchors = np.reshape(shifted_anchors, [k * number_of_anchors, 4])

得到的映射结果为:

[[ -18.627417     -7.3137085    26.627417     15.3137085 ]
 [ -24.50875952  -10.25437976   32.50875952   18.25437976]
 [ -31.9187851   -13.95939255   39.9187851    21.95939255]
 ...,
 [ 584.6862915   573.372583    607.3137085   618.627417  ]
 [ 581.74562024  567.49124048  610.25437976  624.50875952]
 [ 578.04060745  560.0812149   613.95939255  631.9187851 ]]

一般我们都会对anchor做归一化,我们的代码里直接除以600(图片的宽度),并只保留0-1之间的数。

参考

  • pytorch实现RetinaNet

你可能感兴趣的:(目标检测系列一:RetinaNet之anchor)