RoI Pooling层是个池化层,负责收集proposal,把不同大小的proposal resize到相同的尺寸(例如7x7),并计算出proposal feature maps,送入后续网络。
从下图中可以看到Rol pooling层有2个输入:
- 原始的feature maps
- RPN输出的proposal boxes(大小各不相同)
caffe中faster_rcnn_test.pt生成的结构图为(网址http://ethereon.github.io/netscope/#/editor)
在常见的两级检测框架(比如Fast-RCNN,Faster-RCNN,RFCN)中,ROI Pooling 的作用是根据预选框的位置坐标在特征图中将相应区域池化为固定尺寸的特征图,以便进行后续的分类和bbox回归操作。由于预选框的位置通常是由模型回归得到的,一般来讲是浮点数,而池化后的特征图要求尺寸固定。故ROI Pooling这一操作存在两次量化的过程。
例如,Faster RCNN(VGG)中的ROI Pooling要求把随机大小的bbox resize到固定的7x7大小。首先候选框是M*N大小,prpposal是MxN尺度的,对应到feature map上要缩小16倍变成 (M/16)x(N/16)大小,再将每个proposal对应的feature map区域水平分为7x7的网格,对网格的每一份都进行max pooling处理。这样处理后,即使大小不同的proposal输出结果都是 7x7 固定大小,实现了固定长度输出。
举一个具体的例子,针对上图(引用自别人的blog)
1)Conv layers使用的是VGG16,feat_stride=32(即表示,经过网络层后图片缩小为原图的1/32),原图800800,最后一层特征图feature map大小:2525
2)假定原图中有一region proposal,大小为665665,这样,映射到特征图中的大小:665/32=20.78,即20.7820.78,如果你看过Caffe的Roi Pooling的C++源码,在计算的时候会进行取整操作,于是,进行所谓的第一次量化,即映射的特征图大小为20*20
3)假定pooled_w=7,pooled_h=7,即pooling后固定成77大小的特征图,所以,将上面在 feature map上映射的2020的 region proposal划分成49个同等大小的小区域,每个小区域的大小20/7=2.86,即2.862.86,此时,进行第二次量化,故小区域大小变成22
4)每个22的小区域里,取出其中最大的像素值,作为这一个区域的‘代表’,这样,49个小区域就输出49个像素值,组成77大小的feature map
总结,所以,通过上面可以看出,经过两次量化,即将浮点数取整,原本在特征图上映射的20* 20大小的region proposal,偏差成大小为14*14的,这样的像素偏差势必会对后层的回归定位产生影响
所以在Faster RCNN中,有两次整数化的过程:
- region proposal的xywh通常是小数,但是为了方便操作会把它整数化(取离得最近的边界点)。
- 将整数化后的边界区域平均分割成 k x k 个单元,对每一个单元的边界进行整数化。两次整数化的过程如下图所示:
事实上,经过上述两次整数化,此时的候选框已经和最开始回归出来的位置有一定的偏差,这个偏差会影响检测或者分割的准确度。在论文里,作者把它总结为“不匹配问题”(misalignment)。
为了解决这个问题,Mask RCNN(https://arxiv.org/pdf/1703.06870.pdf )提出了ROI Align方法,取消整数化操作,保留了小数,使用双线性插值的方法获得坐标为浮点数的像素点上的图像数值。
但在实际操作中,ROI Align并不是简单地补充出候选区域边界上的坐标点,然后进行池化,而是重新进行设计。
下面通过一个例子来讲解ROI Align操作。这里将ROI切分成2x2的单元格(实线框)。如果采样点数是4,那我们首先将每个单元格子均分成四个小方格(虚线框),每个小方格中心就是采样点。这些采样点的坐标通常是浮点数,所以需要对采样点像素进行双线性插值(这里有疑问),就可以得到该像素点的值了。然后对每个单元格内的四个采样点进行maxpooling,就可以得到最终的ROIAlign的结果。
需要说明的是,在相关实验中,作者发现将采样点设为4会获得最佳性能,甚至直接设为1在性能上也相差无几。事实上,ROI Align 在遍历取样点的数量上没有ROIPooling那么多,但却可以获得更好的性能,这主要归功于解决了misalignment的问题。
在faster_rcnn.py代码中的定义和引用为
self.RCNN_roi_pool = _RoIPooling(cfg.POOLING_SIZE, cfg.POOLING_SIZE, 1.0/16.0)
self.RCNN_roi_align = RoIAlignAvg(cfg.POOLING_SIZE, cfg.POOLING_SIZE, 1.0/16.0)
self.grid_size = cfg.POOLING_SIZE * 2 if cfg.CROP_RESIZE_WITH_MAX_POOL else cfg.POOLING_SIZE
self.RCNN_roi_crop = _RoICrop()
if cfg.POOLING_MODE == 'crop':
# pdb.set_trace()
# pooled_feat_anchor = _crop_pool_layer(base_feat, rois.view(-1, 5))
grid_xy = _affine_grid_gen(rois.view(-1, 5), base_feat.size()[2:], self.grid_size)
grid_yx = torch.stack([grid_xy.data[:,:,:,1], grid_xy.data[:,:,:,0]], 3).contiguous()
pooled_feat = self.RCNN_roi_crop(base_feat, Variable(grid_yx).detach())
if cfg.CROP_RESIZE_WITH_MAX_POOL:
pooled_feat = F.max_pool2d(pooled_feat, 2, 2)
elif cfg.POOLING_MODE == 'align':
pooled_feat = self.RCNN_roi_align(base_feat, rois.view(-1, 5))
elif cfg.POOLING_MODE == 'pool':
pooled_feat = self.RCNN_roi_pool(base_feat, rois.view(-1,5))