RCNN

首先,使用Selective Search算法,从图片中裁出来N个小区域图像

然后将者N个小区域图像分别使用CNN提取得到D维的特征向量。至此得到了NxD的矩阵,该矩阵包含每个小区域对应的D维特征向量

接着,将NxD的矩阵分别使用k个SVM分类器(Dxk)进行分类,得到Nxk的矩阵,即每个小区域图像的分类结果(k个类别,取概率最大的作为预测类别)。在得到每个小区域图像的预测类别后,可能存在同一个目标被多个小区域图片同时预测到的情况,因此还需要针对每个类别对应的小区域图像做一个NMS后处理。

最后,对于NMS后保留下来M(M<=N)个的小区域图像,先按照这些建议框(小区域图像)与GT之间的IoU做一个过滤,保留IoU大于阈值的建议框,然后将每个建议框对应的D维特征向量输入回归器,得到修正后的建议框位置。这里,和分类器个数一样,总共设置了k个回归器,各自负责不同类别(上一步预测得到的类别)的建议框位置修正。

RCNN存在的问题:

  • 测试速度慢(SS算法提取候选框很慢)
  • 训练速度慢(CNN,SVM分类器,回归器)
  • 训练所需空间大(需要将目标框的小区域提取特征并写入磁盘)

FastRCNN

首先,使用Selective Search算法,从图片中定位得到N个小区域图像,不同于RCNN,这里只需要记录这些建议框的位置信息,不需要单独裁剪出来

然后,将整张图片输入CNN中得到特征图,并根据原图和特征图之间的映射关系(等比例缩放),得到每个建议框对应的特征图(不需要将每个建议框对应的小区域图片单独裁剪出来并分别使用CNN提取特征,因而速度快了很多)

接着,将每个建议框的特征图使用ROI pooling层映射到7x7的小特征图,然后加入全连接层做进一步的特征提取,得到ROI特征向量

最后,每个建议框的ROI特征向量分别输入到分类层和回归层(都是全连接层),得到预测的类别以及边界框回归参数。 这里,对于每一个建议框的ROI特征向量,分类层会输出k+1个节点,k是总类别数,1对应背景类别;边界框参数回归器输出(k+1)x4个节点,即针对每个类别的边界框回归参数,其中的边界框回归参数是$(d_x,d_y,d_w,d_h)$,分别对应将通过SS得到建议框转换到预测的边界框时,建议框的中心点坐标和建议框的宽高的变换尺度,具体计算公式如下:

上述所讲内容,用一张整体网络架构图表示如下:

FastRCNN的损失函数包含分类损失和定位损失两部分:

其中,p是预测的类别向量,假设k类,那么p就是k+1维的类别概率向量,u是真实类别,$t^u=(t^u_x,t^u_y,t^u_w,t^u_h)$是预测的边界框回归参数,$v=(v_x,v_y,v_w,v_h)$是真实的边界框回归参数(数据标定可以得到中心点和宽高,于是可以根据尺度变换公式反推$v$)

分类损失是交叉熵,定位损失如下:

只有预测的正样本边界框才有对应的真实边界框,那些被预测为背景类别的建议框是负样本,负样本是没有真实的边界框与之对应的。所以,只有满足$[u\gt=1]$,即类别不被预测为背景(u=0)的边界框才会计算定位损失,这里的$[u\gt=1]$可以看作是一个逻辑函数,满足$u\gt=1$的条件则函数值为1,否则为0。

Faster R_CNN

首先,将图像输入网络得到相应的特征图

接着,使用RPN生成候选框,并将这些候选框映射到特征图上以获得相应的特征矩阵

最后,将每个特征矩阵通过roi pooling层缩放到7x7大小的特征图,并展平,后面通过一系列全连接层得到预测结果。

因此,Faster R_CNN相当于在Fast R_CNN的基础上引入了RPN,用来替代Fast R_CNN中的SS算法。

现在来介绍RPN。

对于通过主干网络提取得到的卷积特征图,RPN会设置一个3x3(stride=paddding=1,因此可以覆盖特征图上的每一个像素点)的滑动窗口,在整张特征图上进行滑动,并记录下每个时刻滑动窗口的中心点对应原始图像上的中心点,从原图的这个中心点出发可以设置不同比例和尺度的anchor(论文中设置了3个比例,包括1:1,1:2,2:1,和3种尺度,包括128x128,256x256,512x512,因此每个中心点对应有9个anchor)。

以下是一个RPN的简易代码实现,在实际应用时,还会对得到的所有anchor进行后处理,比如去掉超出图像边界的,基于cls得分采用NMS进行过滤等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch
import torch.nn as nn
import torch.nn.functional as F

class RPN(nn.Module):
def __init__(self, in_channels, mid_channels, num_anchors):
super(RPN, self).__init__()
self.conv = nn.Conv2d(in_channels, mid_channels, kernel_size=3, stride=1, padding=1)
self.cls_logits = nn.Conv2d(mid_channels, num_anchors * 2, kernel_size=1, stride=1)
self.bbox_pred = nn.Conv2d(mid_channels, num_anchors * 4, kernel_size=1, stride=1)
self._initialize_weights()

def _initialize_weights(self):
for layer in [self.conv, self.cls_logits, self.bbox_pred]:
nn.init.normal_(layer.weight, std=0.01)
nn.init.constant_(layer.bias, 0)

def forward(self, x):
x = F.relu(self.conv(x))# 1x512x50x50
logits = self.cls_logits(x)
bbox_pred = self.bbox_pred(x)

N, C, H, W = logits.shape
logits = logits.permute(0, 2, 3, 1).contiguous().view(N, -1, 2)
bbox_pred = bbox_pred.permute(0, 2, 3, 1).contiguous().view(N, -1, 4)

return logits, bbox_pred

# input_tensor是骨干网络提取的特征图
input_tensor = torch.randn(1, 512, 50, 50)

# 依照论文,每个点对应设置3x3=9个anchor
rpn = RPN(in_channels=512, mid_channels=512, num_anchors=9)

# 前向传播
logits, bbox_pred = rpn(input_tensor)

print("Logits shape:", logits.shape) # (1, 22500, 2) ,即 (50*50*9, 2 classes)
print("BBox pred shape:", bbox_pred.shape) # (1, 22500, 4),即 (50*50*9, 4 coordinates)

可以看到,RPN有两个head,分别用于分类和bbox参数回归。

其中,分类是一个二分类,只用于区分对应原图上的anchor是前景还是背景,若是前景,则对其进行进一步的分类(目标类别)和bbox参数回归。

通过生成anchor的顺序,可以知道每个anchor的具体尺度和纵横比。生成的anchor是按照尺度和纵横比的组合顺序排列的,因此根据索引可以直接确定每个anchor的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def generate_anchors(base_size=16, ratios=[0.5, 1, 2], scales=[8, 16, 32]):
"""
Generate anchor (reference) windows by enumerating aspect ratios X scales w.r.t. a reference window.
"""
num_anchors = len(ratios) * len(scales)
anchors = np.zeros((num_anchors, 4))
index = 0

for scale in scales:
for ratio in ratios:
h = base_size * scale * np.sqrt(ratio)
w = base_size * scale / np.sqrt(ratio)
anchors[index, :] = [-w/2, -h/2, w/2, h/2]
index += 1

return anchors

将图片的特征图经过RPN之后,会得到许多anchor,需要从这些anchor中确定正负样本。

正样本:anchor与GT框的IoU大于0.7(多对一),或者当没有一个anchor满足IoU与GT框大于0.7时,选择IoU最大的anchor(s)

负样本:anchor与GT框的IoU小于0.3

其他的anchor则直接不对训练做贡献。

RPN的损失函数包含两部分,分类损失和边界框参数回归损失:

分类损失$L_{cls}$是二值交叉熵,$N_{cls}$是一张图片(或一个batch)筛选出来的anchor的数量,原论文中为256,即正负样本的总数。

边界框参数回归损失$L_{reg}$是Smooth L1损失,$N_{reg}$是anchor位置的个数(记得特征图上滑动窗口每一个中心点对应一个anchor位置,每个位置对应9个不同大小比例的anchor),因此也就是特征图的高x宽得到的结果,原论文中大概为2400。负样本不参与边界框参数回归损失的计算,因此$P^*_i$起到了过滤作用,因为只有当anchor为正样本时,$P^*_i=1$,否则取0。