自进入吉大,开始硕士生活之后,本人便很少有时间写一些blog,毕竟忙起来之后时间真的比较紧,不过细想想感觉自己也没忙出个什么。这次,导师给安排的项目是病变细胞检测的项目(Emmmmm,这是我的第三个项目,我研一),在深度学习领域上,属于目标检测的范畴。我主要采用的是RetinaNet,那么今天简单总结一下anchor的相关知识。
为什么会出现anchor、anchor处理之前目标检测用的是什么技术?这些问题本次一一不涉及
从学术的角度来说,anchor就是拥有多尺度、多种宽高比例的box,也就是multi-scale and different aspect ratio 。
我们知道,图片分类和实例分割分别属于image-level和pix-level,而目标检测则属于region-level。那么region的信息,我们是如何向网络提供的呢?
anchor的设置,相当于提前在图片上做预选,通常在生成anchor时,每一张图片上将会对应成千上万的anchor也就是候选框,可想而知,如此庞大的候选框数量,肯定能够满足对图上实例进行检测的需要,效果如下图所示:
可以看到,整张图片上存在许多不同形状的小框框,但是这些框框的总数也只是原anchor数量的1%。
那么有了这些框框,网络便可以学习这些框框中的事物以及框框的位置,最终可以进行分类和回归(还涉及IOU的计算等等,本篇文章不涉及)。
我们以Retinanet网络中的anchor为例,使用numpy和python生成,具体RetinaNet网络中的anchor是什么形式的,请移步ICCV2017kaiming大神的论文,可详细了解。
anchor-size:[32,64,128,256,512]
每个anchor-size对应着三种scale和三个ratio,那么每个anchor-size将对应生成9个先验框,同时生成的所有先验框均满足:
下面为代码思路,我们以32为例:
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的宽和高。
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相对于自身中心点的坐标,我们还要将anchor映射到特征金字塔的每一层的feature map上,我们以p3为例,p3的尺度为75*75。
那么映射的思路为:
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之间的数。