一张图看懂Faster R-CNN。

背景

  • Faster R-CNN采用与Fast R-CNN相同的设计,只是它用内部深层网络代替了候选区域方法
  • 新的候选区域网络(RPN)在生成ROI时效率更高,并且以每幅图像10毫秒的速度运行。

Faster R-CNN 是作者 Ross Girshick 继 Fast R-CNN 后的又一力作,同样使用 VGG16 作为
backbone,推理速度在 GPU 上达到 5fps(每秒检测五张图,包括候选区域生成),准确度也有一定的进步。核心在于 RPN
区域生成网络(Region Proposal Network)。

端到端卷积神经网络 端到端cnn_神经网络

四个部分融合在一个CNN中,实现端到端

网络架构

端到端卷积神经网络 端到端cnn_目标检测_02

输入图像首先经过Backbone得到feature map

端到端卷积神经网络 端到端cnn_pytorch_03


端到端卷积神经网络 端到端cnn_目标检测_04

特征提取网络

端到端卷积神经网络 端到端cnn_cnn_05

RPN模块

Reginal Proposal Network:区域检测网络

区域生成较好的建议框模块即Proposal,代替SS算法。实现端到端

  1. Anchor生成:RPN对输入的feature map上的每个点对应9个anchor,每个anchor的大小宽高不同
  2. RPN卷积网络:利用1x1的卷积在feature map 上得到每一个Anchor的预测得分和预测偏移值
  3. 计算RPN loss:
  4. 生成Proposal:
  5. 筛选Proposal得到RoI

RPN的输入输出:

输入:feature map 、物体标签(训练集中所有物体和标签)

输出:Proposal(生成区域给后续模块分类和回归)、分类Loss、回归Loss(两个损失用于优化网络)

端到端卷积神经网络 端到端cnn_cnn_06

1.理解Anchor

anchor box: 以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。

用法:

  1. 提出多个被称为锚框的区域(暂定维边缘框)
  2. 预测每个锚框里是否含有目标物体
  3. 如果是,预测从这个锚框到真实边缘框的偏移。

锚框之间的相似度表示:IoU:

端到端卷积神经网络 端到端cnn_端到端卷积神经网络_07


Jacquard指数的一个特殊情况

赋予锚框标号:

  • 每个锚框是一个训练样本
  • 将每个锚框,要么标注成背景,要么关联上一个真实边缘框
  • 可能导致生成大量的锚框
  • 导致大量的负类样本

端到端卷积神经网络 端到端cnn_cnn_08

端到端卷积神经网络 端到端cnn_目标检测_09

9个大小不同的锚框:类似于鱼网

端到端卷积神经网络 端到端cnn_pytorch_10


端到端卷积神经网络 端到端cnn_端到端卷积神经网络_11

#锚框的生成
def generate_anchors(base_size=16,ratios=[0.5,1,2],scales=2**np.arange(3,6)):
	#首先创建一个基本的anchor为[0,0,15,15]

重点,anchor生成:

  1. 对feature map进行3*3 的卷积,得到每一个点的维度是512(VGG)
  2. 512维对应原始图片上很多不同大小和宽高区域的特征。这些区域的中心点相同(假设下采样率是16,每一个点乘以16就可以得到原图对应的坐标)
  3. anchor有九种大小:scale=2**{8,16,32} ratio= {0.5,1,2}将9个大小的anchor反算到原图上,就可得到不同的原始proposal。
  4. VGG得出的feature map 大小为37*50 ,所有anchor总数:37x50x9 =16650个:注论文中大小是40x60
  5. 再将anchor输入分类和回归的卷积神经网络,分类网络来判断anchor是前景的概率和回归网络将预测偏移量作用到anchor上使得anchor会更接近于真实物体坐标

代码部分:

class AnchorGenerator(nn.Module):
    def __init__(self,sizes=(128,256,512),aspect_ratios=(0.5,1.0,2.0)):
        '''默认参数没有用到'''
        super(AnchorGenerator,self).__init__()

        if not isinstance(sizes[0],(list,tuple)):
            # TODO change this
            sizes = tuple((s,) for s in sizes)   #(3,)元组中一个元素必须加,
        if not isinstance(aspect_ratios[0],(list,tuple)):
            aspect_ratios = (aspect_ratios,)*len(sizes)

        assert len(sizes)  == len(aspect_ratios)

        self.sizes = sizes
        self.aspect_ratios = aspect_ratios
        self.cell_anchors = None   #存储anchor的模板
        self._cache = {}  #存储anchor的坐标信息

    def generate_anchors(self,scales,aspect_ratios,dtype=torch.float32 ,device = torch.device("cpu")):

        # type: (List[int], List[float], torch.dtype, torch.device) -> Tensor
        """
        compute anchor sizes
        Arguments:
            scales: sqrt(anchor_area)
            aspect_ratios: h/w ratios
            dtype: float32
            device: cpu/gpu
        """
        #先都转为tensor
        scales = torch.as_tensor(scales, dtype=dtype, device=device)
        aspect_ratios = torch.as_tensor(aspect_ratios, dtype=dtype, device=device)
        h_ratios = torch.sqrt(aspect_ratios)  #高度的乘法因子
        w_ratios = 1.0 / h_ratios             #宽度的乘法因子

        #计算每个anchor的宽度和高度
        # [r1, r2, r3]' * [s1, s2, s3]
        # number of elements is len(ratios)*len(scales)
        ws = (w_ratios[:, None] * scales[None, :]).view(-1)  #anchor宽度值:将每个宽度比例乘以scales得到->展成一维向量
        hs = (h_ratios[:, None] * scales[None, :]).view(-1)  #anchor高度值:将每个高度比例乘以scales得到->展成一维向量

        # left-top, right-bottom coordinate relative to anchor center(0, 0)
        # 生成的anchors模板都是以(0, 0)为中心的, shape [len(ratios)*len(scales), 4]
        base_anchors = torch.stack([-ws, -hs, ws, hs], dim=1) / 2   #输出base_anchors.shape =torch.size(15,4)  15个anchor,4个点的信息

        return base_anchors.round()  # round 四舍五入


    def set_cell_anchors(self,dtype,device) -> (torch.dtype,torch.device):
        if self.cell_anchors is not None:   #cell_anchors初始化为None
            cell_anchors = self.cell_anchors
            assert cell_anchors is not None
            # suppose that all anchors have the same device
            # which is a valid assumption in the current state of the codebase
            if cell_anchors[0].device == device:
                return

        # 根据提供的sizes和aspect_ratios生成anchors模板
        # anchors模板都是以(0, 0)为中心的anchor
        cell_anchors = [
            self.generate_anchors(sizes,aspect_ratios,dtype,device)
            for sizes ,aspect_ratios in zip(self.sizes,self.aspect_ratios)
        ]

        self.cell_anchors = cell_anchors

    def grid_anchors(self, grid_sizes, strides):
        # type: (List[List[int]], List[List[Tensor]]) -> List[Tensor]
        """
        anchors position in grid coordinate axis map into origin image
        计算预测特征图对应原始图像上的所有anchors的坐标
        Args:
            grid_sizes: 预测特征矩阵的height和width
            strides: 预测特征矩阵上一步对应原始图像上的步距
        """
        anchors = []
        cell_anchors = self.cell_anchors
        assert  cell_anchors is not None

        #遍历每个预测特征层的grid_size,strides和cell_anchors
        for size, stride, base_anchors in zip(grid_sizes, strides, cell_anchors):
            grid_height, grid_width = size
            stride_height, stride_width = stride
            device = base_anchors.device

            # For output anchor, compute [x_center, y_center, x_center, y_center]
            # shape: [grid_width] 对应原图上的x坐标(列)
            shifts_x = torch.arange(0, grid_width, dtype=torch.float32, device=device) * stride_width
            # shape: [grid_height] 对应原图上的y坐标(行)
            shifts_y = torch.arange(0, grid_height, dtype=torch.float32, device=device) * stride_height

            # 计算预测特征矩阵上每个点对应原图上的坐标(anchors模板的坐标偏移量)
            # torch.meshgrid函数分别传入行坐标和列坐标,生成网格行坐标矩阵和网格列坐标矩阵
            # shape: [grid_height, grid_width]
            shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x)
            shift_x = shift_x.reshape(-1)
            shift_y = shift_y.reshape(-1)

            # 计算anchors坐标(xmin, ymin, xmax, ymax)在原图上的坐标偏移量
            # shape: [grid_width*grid_height, 4]
            shifts = torch.stack([shift_x, shift_y, shift_x, shift_y], dim=1)

            # For every (base anchor, output anchor) pair,
            # offset each zero-centered base anchor by the center of the output anchor.
            # 将anchors模板与原图上的坐标偏移量相加得到原图上所有anchors的坐标信息(shape不同时会使用广播机制)
            shifts_anchor = shifts.view(-1, 1, 4) + base_anchors.view(1, -1, 4)
            anchors.append(shifts_anchor.reshape(-1, 4))

        return anchors  # List[Tensor(all_num_anchors, 4)]


    def cached_grid_anchors(self,grid_sizes,strides) :
        # type: (List[List[int]], List[List[Tensor]]) -> List[Tensor]
        """将计算得到的所有anchors信息进行缓存"""
        key = str(grid_sizes) + str(strides)
        #self._cache初始化时是个空字典,所有下面条件不满足
        if key in self._cache:
            return  self._cache[key]

        #得到所有预测特征层在原图上的anchors
        anchors = self.grid_anchors(grid_sizes,strides)
        self._cache[key] = anchors     #将anchors存入到字典

        return  anchors


    def forward(self,image_list,feature_maps):
        # type: (ImageList, List[Tensor]) -> List[Tensor]

        #获取每个backbone预测特征层的尺寸(height,weight)  shape的最后两个维度就是
        grid_sizes = list([ feature_map.shape[-2:] for feature_map in feature_maps])
        #获取原始输入图像的height和weight
        image_size = image_list.tensor.shape[-2:]

        # 获取变量类型和设备类型
        dtype, device = feature_maps[0].dtype, feature_maps[0].device

        ''''#计算feature map和 原始图像的比例大小  原图的高度/grid_size的高度
        # one step in feature map equate n pixel stride in origin image
        # 计算特征层上的一步等于原始图像上的步长'''
        strides = [ [ torch.Tensor(image_size[0]/g[0],dtype=torch.int64,device= device),
                        torch.Tensor(image_size[1]/g[1],dtype=torch.int64,device= device)] for g in grid_sizes]

        # 根据提供的sizes和aspect_ratios生成anchors模板
        self.set_cell_anchors(dtype, device)   #已经将anchors模板生成,

        # 计算/读取所有anchors的坐标信息(这里的anchors信息是映射到原图上的所有anchors信息,不是anchors模板)
        # 得到的是一个list列表,对应每张预测特征图映射回原图的anchors坐标信息
        anchors_over_all_feature_maps = self.cached_grid_anchors(grid_sizes,strides)

        anchors = torch.jit.annotate(list[list[torch.Tensor]], [])
        # 遍历一个batch中的每张图像
        for i, (image_height, image_width) in enumerate(image_list.image_sizes):
            anchors_in_image = []
            # 遍历每张预测特征图映射回原图的anchors坐标信息

            for anchors_per_feature_map in anchors_over_all_feature_maps:
                anchors_in_image.append(anchors_per_feature_map)
            anchors.append(anchors_in_image)
        # 将每一张图像的所有预测特征层的anchors坐标信息拼接在一起
        # anchors是个list,每个元素为一张图像的所有anchors信息
        anchors = [torch.cat(anchors_per_image) for anchors_per_image in anchors]
        # Clear the cache in case that memory leaks.内存泄露
        self._cache.clear()
        return anchors

2.RPN的真值与预测值

RPN可以预测Anchor的类别作为预测边框的类别

可以预测真实的边框相对于Anchor位置的偏移量

而不是直接预测边界框的中心坐标x和y,宽w ,高h

端到端卷积神经网络 端到端cnn_目标检测_12

**模型(类别)真值:**对应类别真值,RPN只需proposal生成,保证recall,没必要细分每个区域属于哪个类别,

只需前景(有目标物)和背景两个类别。

前景和后景判断方法:Anchor和标签框(真实框)的IoU 。算出的IoU大于阈值就是前景,否则就是背景

偏移量真值

端到端卷积神经网络 端到端cnn_神经网络_13

将偏移量的真值输入RPN网络–>输出预测偏移量

端到端卷积神经网络 端到端cnn_神经网络_14

端到端卷积神经网络 端到端cnn_pytorch_15

  • 如果没有Anchor:物体检测需要直接预测每个框的位置,由于框的坐标幅度大,会使网络模型很难收敛和预测
  • 有了Anchor:相当于一个先验阶梯,使得RPN去预测Anchor的偏移量,更好的接近真实物体

总结:

  1. 类别上Anchor根据IoU判断出是否前景,将后景会生成负样本
  2. 得到是前景的anchor,算出和标签框的偏移量,从而让RPN模型不断学习,给出一个Proposal

3.RPN卷积网络

端到端卷积神经网络 端到端cnn_cnn_16

RPNhead代码实现

class RPNhead(nn.Moduke):
    '''
    add a RPN head with classification and regression
    通过滑动窗口计算预测目标概率与bbox regression参数

    Arguments:
        in_channels: number of channels of the input feature
        num_anchors: number of anchors to be predicted
    '''
    def __init__(self,in_channels,num_anchors):
        super(RPNhead,self).__init__()
        #从backbone出来的特征图,经历3*3大小的卷积层
        self.conv = nn.Conv2d(in_channels,in_channels,kernel_size=3,stride=1,padding=1)
        #计算预测的目标目标类别的分手(前景或后景)
        self.cls_logits = nn.Conv2d(in_channels,num_anchors,kernel_size=1,stride=1)  #论文中输出的类别为2k,因为是二分类,给一种类别就可以判断
        #计算预测目标bbox regression位置偏移参数
        self.box_pred = nn.Conv2d(in_channels,num_anchors*4,kernel_size=1,stride=1)

        for layer in self.children():
            '''初始化卷积网络,初始权重'''
            if isinstance(layer,nn.Conv2d):
                torch.nn.init.normal_(layer.weight,std = 0.01)
                torch.nn.init.constant_(layer.bias,0)

    def forward(self,x) -> (list[Tensor]) :
        '''x是backbone输出的特侦层,输出预测的类别和预测框的列表'''
        logits = []
        bbox_reg = []
        for i,feature in enumerate(x):
            '''遍历输入进来的每一个特征层'''
            t  = nn.ReLU(self.conv(feature))
            logits.append(self.cls_logits(t))
            bbox_reg.append(self.box_pred)
        return logits , bbox_reg

4.RPN真值的求取

端到端卷积神经网络 端到端cnn_pytorch_17

5.损失函数设计

端到端卷积神经网络 端到端cnn_cnn_18

  • 分类损失:

端到端卷积神经网络 端到端cnn_cnn_19


端到端卷积神经网络 端到端cnn_端到端卷积神经网络_20


端到端卷积神经网络 端到端cnn_神经网络_21

6.NMS和生成Proposal

使用NMS输出:

  • 每个锚框预测一个边缘框
  • NMS可以合并相似的预测
  • 选中是非背景类的最大值预测值
  • 去掉所有其他和它IoU值大于置信度的预测
  • 重复上述过程直到所有预测要么被选中要么被去掉

过程:

端到端卷积神经网络 端到端cnn_端到端卷积神经网络_22

7.筛选Proposal得到RoI

从上一步生成的Proposal数量的2000个 ,任然还有很多背景框,需要对proposal进行再次筛选

利用标签和proposal构建IoU矩阵:通过和标签重合度选出256个正负样本


RoI Pooling层(Region of Interest)

RoI Pooling

端到端卷积神经网络 端到端cnn_pytorch_23

RoI Align

使用双线性插值获得坐标为浮点数的值,最大可能的保留了原始区域的特征,对本身较小的物体改善更为明显

端到端卷积神经网络 端到端cnn_pytorch_24

将RoI对应到特征图上,但坐标和大小都保留浮点数,大小为20.75*20.75

将20.75*20.75大小均匀分成 7x7方格大小。中间依然保留浮点数。每个方格内特定位置选取4个采样点进行特征采样

端到端卷积神经网络 端到端cnn_端到端卷积神经网络_25

RCNN模块

端到端卷积神经网络 端到端cnn_pytorch_26

预测结果映射回原尺度