Faster R-CNN最全讲解

一:Faster R-CNN的改进

想要更好地了解Faster R-CNN,需先了解传统R-CNN和Fast R-CNN原理,可参考本人呕心撰写的两篇博文R-CNN史上最全讲解和Fast R-CNN讲解。

回到正题,经过R-CNN和Fast RCNN的积淀,Ross B. Girshick在2016年提出了新的Faster RCNN。从网络命名上看就很直白,那么相较于Faster R-CNN到底Faster在哪儿里呢?答案就是:region proposal的提取方式的改变。

Fast R-CNN虽然提出了ROI Pooling的特征提取方式,很好地解决了传统R-CNN中将Region Proposal区域分别输入CNN网络中的弊端。但是!!!始终都是用的传统Selective Search搜索方式确定Region Proposal,训练和测试时消耗了大量时间在RP搜索上。而Faster R-CNN突破性地使用了RPN网络直接提取出RP,并将其融入进整体网络中,使得综合性能有较大提高,在检测速度方面尤为明显。

二:网络架构

Faster R-CNN最全讲解
上图展示了python版本中的VGG16模型中的faster_rcnn_test.pt的网络结构,可以清晰的看到该网络架构分为以下几个模块:

  • Conv layers
    该Backbone层主要用来提取输入图像中的特征,生成Feature Map以供后两个模块使用。
  • Region Proposal Networks(RPN)
    RPN模块用来训练提取出原图中的Region Proposal区域,是整个网络模型中最重要的一个模块。
  • Semi-Fast R-CNN
    Semi-Fast R-CNN是我自创的命名,因为和Fast R-CNN的head层几乎一模一样,更多叫法是RoiHead层。当通过RPN模块确定了RP后,就可以训练Fast R-CNN网络了,完成对RP区域的分类与bbox框的微调。

综上述可见,细心的人会发现,Conv layers+Semi-Fast R-CNN不就是Fast R-CNN嘛!所以,Faster R-CNN网络实际上就是RPN + Fast R-CNN,也就是two-stage,训练时也是对两个模块分开训练,测试时先由RPN生成RP,再将带有RP的Feature Map输入进Fast R-CNN中完成分类和预测框回归任务。下面,我将依次对三个模块进行详细讲解。

三:Conv layers模块

Faster R-CNN最全讲解

Conv layers包含了conv,pooling,relu三种层。以python版本中的VGG16模型中faster_rcnn_test.pt的网络结构为例。Conv layers部分共有13个conv层,13个relu层,4个pooling层。不太熟悉VGG16的小伙伴们要注意一下两个细节:

  • 所有的conv层都是:kernel_size=3,pad=1,stride=1
  • 所有的pooling层都是:kernel_size=2,pad=0,stride=2

经过Conv layers模块后,一个MxN(800×600)大小的输入图像就变为了(M/16)x(N/16)的Feature Map!这样Conv layers生成的feature map中都可以和原图对应起来。

四:Region Proposal Networks(RPN)模块

Faster R-CNN最全讲解
终于迎来了最重要的RPN模块,大家提起精神,和我一起分析!RPN说到底就是两个功能模块,第一个功能模块是利用二分类给每个anchor打出前景得分,并利用回归计算出每个anchor与其对应的GT间的四个微调参数。第二个功能模块则是根据第一个功能模块输出的得分和四个微调参数,得出ROI并选出合适的RP。
其实RPN只有第一个功能模块需要训练,第二个模块都是基本的选择运算,没有参数需要训练,只是为RoiHead选出训练和测试要用到的region proposal。下面我从这两个功能模块依次讲解:

【Module 1】

在第一模块中,将讲解如何在原图中生成anchors并进行标注,又是如何利用标注好的anchors训练RPN对每个anchor进行前景与背景的二分类与anchor位置的回归微调。下面分步骤进行讲解:

step1: generate_anchor_base

首先,我们需要使用generate_anchor_base函数生成anchors。代码的主要实现思想:首先以特征图feature map的左上角点为基准产生9个anchor,三种尺度,每个尺度再对应三种比例。接着,将特征图左上角的9个anchors乘上原图的缩放比例base_size,也就是经过4个池化层后的16倍。锚点由特征图中的(0.5,0.5)变为了原图上的(8,8),原图上的9个anchors的w和h也变为16倍。然后以原图左上角的anchors为基准,每隔base_size个像素,画9个anchors。画完之后,原图上大概有20000个anchors。
具体实现方法见以下代码及附手写:

def generate_anchor_base(base_size=16, ratios=[0.5, 1, 2], #
                         anchor_scales=[8, 16, 32]):   #对特征图features以基准长度为16、选择合适的ratios和scales取基准锚点anchor_base。(选择长度为16的原因是图片大小为600*800左右,基准长度16对应的原图区域是256*256,考虑放缩后的大小有128*128,512*512比较合适)
#根据基准点生成9个基本的anchor的功能,ratios=[0.5,1,2],anchor_scales=[8,16,32]是长宽比和缩放比例,anchor_scales也就是在base_size的基础上再增加的量,本代码中对应着三种面积的大小(16*8)^2 ,(16*16)^2  (16*32)^2  也就是128,256,512的平方大小
    py = base_size / 2.
    px = base_size / 2.   

    anchor_base = np.zeros((len(ratios) * len(anchor_scales), 4),     
                           dtype=np.float32)  #(9,4),注意:这里只是以特征图的左上角点为基准产生的9个anchor,
    for i in six.moves.range(len(ratios)): #six.moves 是用来处理那些在python2 和 3里面函数的位置有变化的,直接用six.moves就可以屏蔽掉这些变化
        for j in six.moves.range(len(anchor_scales)):
            h = base_size * anchor_scales[j] * np.sqrt(ratios[i])
            w = base_size * anchor_scales[j] * np.sqrt(1. / ratios[i]) #生成9种不同比例的h和w
 '''
这9个anchor形状应为:
90.50967 *181.01933    = 128^2
181.01933 * 362.03867 = 256^2
362.03867 * 724.07733 = 512^2
128.0 * 128.0 = 128^2
256.0 * 256.0 = 256^2
512.0 * 512.0 = 512^2
181.01933 * 90.50967   = 128^2
362.03867 * 181.01933 = 256^2
724.07733 * 362.03867 = 512^2
该函数返回值为anchor_base,形状9*4,是9个anchor的左上右下坐标:
-37.2548 -82.5097 53.2548 98.5097
-82.5097	-173.019	98.5097	189.019
-173.019	-354.039	189.019	370.039
-56	-56	72	72
-120	-120	136	136
-248	-248	264	264
-82.5097	-37.2548	98.5097	53.2548
-173.019	-82.5097	189.019	98.5097
-354.039	-173.019	370.039	189.019
'''
            index = i * len(anchor_scales) + j
            anchor_base[index, 0] = py - h / 2.
            anchor_base[index, 1] = px - w / 2.
            anchor_base[index, 2] = py + h / 2.
            anchor_base[index, 3] = px + w / 2.  #计算出anchor_base画的9个框的左下角和右上角的4个anchor坐标值
    return anchor_base 

Faster R-CNN最全讲解

由于后面的解释中有一些转换函数的应用,所以我先贴在这里,大家可以根据注释理解:

def loc2bbox(src_bbox, loc): #已知源bbox 和位置偏差dx,dy,dh,dw,求目标框G
    if src_bbox.shape[0] == 0:
        return xp.zeros((0, 4), dtype=loc.dtype)        #src_bbox:(R,4),R为bbox个数,4为左下角和右上角四个坐标(这里有误,按照标准坐标系中y轴向下,应该为左上和右下角坐标)
    src_bbox = src_bbox.astype(src_bbox.dtype, copy=False) 
    src_height = src_bbox[:, 2] - src_bbox[:, 0]      #ymax-ymin
    src_width = src_bbox[:, 3] - src_bbox[:, 1]     #xmax-xmin
    src_ctr_y = src_bbox[:, 0] + 0.5 * src_height    y0+0.5h
    src_ctr_x = src_bbox[:, 1] + 0.5 * src_width   #x0+0.5w,计算出中心点坐标
#src_height为Ph,src_width为Pw,src_ctr_y为Py,src_ctr_x为Px
    dy = loc[:, 0::4]      #python [start:stop:step] 
    dx = loc[:, 1::4]
    dh = loc[:, 2::4]
    dw = loc[:, 3::4]
RCNN中提出的边框回归:寻找原始proposal与近似目标框G之间的映射关系,公式在上面
    ctr_y = dy * src_height[:, xp.newaxis] + src_ctr_y[:, xp.newaxis]  #ctr_y为Gy
    ctr_x = dx * src_width[:, xp.newaxis] + src_ctr_x[:, xp.newaxis] # ctr_x为Gx
    h = xp.exp(dh) * src_height[:, xp.newaxis] #h为Gh
    w = xp.exp(dw) * src_width[:, xp.newaxis] #w为Gw
#上面四行得到了回归后的目标框(Gx,Gy,Gh,Gw)
    dst_bbox = xp.zeros(loc.shape, dtype=loc.dtype)  #loc.shape:(R,4),同src_bbox
    dst_bbox[:, 0::4] = ctr_y - 0.5 * h
    dst_bbox[:, 1::4] = ctr_x - 0.5 * w
    dst_bbox[:, 2::4] = ctr_y + 0.5 * h
    dst_bbox[:, 3::4] = ctr_x + 0.5 * w   #由中心点转换为左上角和右下角坐标
    return dst_bbox
    
def bbox2loc(src_bbox, dst_bbox): #已知源框和目标框求出其位置偏差
    height = src_bbox[:, 2] - src_bbox[:, 0]
    width = src_bbox[:, 3] - src_bbox[:, 1]
    ctr_y = src_bbox[:, 0] + 0.5 * height
    ctr_x = src_bbox[:, 1] + 0.5 * width #计算出源框中心点坐标

    base_height = dst_bbox[:, 2] - dst_bbox[:, 0]
    base_width = dst_bbox[:, 3] - dst_bbox[:, 1]
    base_ctr_y = dst_bbox[:, 0] + 0.5 * base_height
    base_ctr_x = dst_bbox[:, 1] + 0.5 * base_width ##计算出目标框中心点坐标

    eps = xp.finfo(height.dtype).eps  #求出最小的正数
    height = xp.maximum(height, eps) 
    width = xp.maximum(width, eps)  #将height,width与其比较保证全部是非负

    dy = (base_ctr_y - ctr_y) / height
    dx = (base_ctr_x - ctr_x) / width
    dh = xp.log(base_height / height)
    dw = xp.log(base_width / width)  #根据上面的公式二计算dx,dy,dh,dw

    loc = xp.vstack((dy, dx, dh, dw)).transpose()    #np.vstack按照行的顺序把数组给堆叠起来
    return loc

def bbox_iou(bbox_a, bbox_b):  #求两个bbox的相交的交并比
    if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
        raise IndexError  #确保bbox第二维为bbox的四个坐标(ymin,xmin,ymax,xmax)
    tl = xp.maximum(bbox_a[:, None, :2], bbox_b[:, :2])  #tl为交叉部分框左上角坐标最大值,为了利用numpy的广播性质,bbox_a[:, None, :2]的shape是(N,1,2),bbox_b[:, :2]shape是(K,2),由numpy的广播性质,两个数组shape都变成(N,K,2),也就是对a里每个bbox都分别和b里的每个bbox求左上角点坐标最大值
    br = xp.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:]) #br为交叉部分框右下角坐标最小值
    area_i = xp.prod(br - tl, axis=2) * (tl < br).all(axis=2) #所有坐标轴上tl<br时,返回数组元素的乘积(y1max-yimin)X(x1max-x1min),bboxa与bboxb相交区域的面积
    area_a = xp.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)  #计算bboxa的面积
    area_b = xp.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1) #计算bboxb的面积
    return area_i / (area_a[:, None] + area_b - area_i) #计算IOU

step2: AnchorTargetCreator

在原图中生成了近乎20000个anchors后,使用AnchorTargetCreator函数对它们进行标注以用于训练。代码的主要实现思想:针对于label的标注,首先剔除掉超过原图边界的anchors,剩下将近15000个。接着,计算每一个anchor与哪个bbox的iou最大以及这个iou值,IOU>0.7的anchor为pos_anchor,IOU<0.3的anchor为neg_anchor。同时,还需计算每个bbox与哪个anchor的iou最大(其实就是矩阵中行最大和列最大的区别),每个bbox对应的最大IOU的anchors也直接设为pos_anchor。但是最后还需要在pos和neg中各随机选128个,也就是128个正样本和128个负样本,将128个正样本的label设置为1,将128个负样本的label设置为0,剩下的(20000-256)个anchors的labels都设为0。针对4个回归框的参数标注,首先对超框的都设为(0,0,0,0),框内的近乎15000个anchors的4个参数就是它们与最大IOU对应的bbox的实际偏移量。具体代码见下:

# 下面是AnchorTargetCreator()代码,作用是生成训练要用的anchor(与对应框iou值最大或者最小的各128个框的坐标和256个label(0或者1))
class AnchorTargetCreator(object):  # 利用每张图中bbox的真实标签来为所有任务分配ground truth!
    # 为Faster-RCNN专有的RPN网络提供自我训练的样本,RPN网络正是利用AnchorTargetCreator产生的样本作为数据进行网络的训练和学习的,这样产生的预测anchor的类别和位置才更加精确,anchor变成真正的ROIS需要进行位置修正,而AnchorTargetCreator产生的带标签的样本就是给RPN网络进行训练学习用哒
    def __call__(self, bbox, anchor, img_size):  # anchor:(S,4),S为anchor数
        img_H, img_W = img_size
        n_anchor = len(anchor)  # 一般对应20000个左右anchor
        inside_index = _get_inside_index(anchor, img_H, img_W)  # 将那些超出图片范围的anchor全部去掉,只保留位于图片内部的序号
        anchor = anchor[inside_index]  # 保留位于图片内部的anchor
        argmax_ious, label = self._create_label(inside_index, anchor, bbox)  # 筛选出符合条件的正例128个负例128并给它们附上相应的label
        loc = bbox2loc(anchor, bbox[argmax_ious])  # 计算每一个anchor与对应bbox求得iou最大的bbox计算偏移量(注意这里是位于图片内部的每一个)
        label = _unmap(label, n_anchor, inside_index, fill=-1)  # 将位于图片内部的框的label对应到所有生成的20000个框中(label原本为所有在图片中的框的)
        loc = _unmap(loc, n_anchor, inside_index, fill=0)  # 将回归的框对应到所有生成的20000个框中(label原本为所有在图片中的框的)
        return loc, label

        # 下面为调用的_creat_label() 函数

    def _create_label(self, inside_index, anchor, bbox):
        label = np.empty((len(inside_index),), dtype=np.int32)  # inside_index为所有在图片范围内的anchor序号
        label.fill(-1)  # 全部填充-1
        argmax_ious, max_ious, gt_argmax_ious = self._calc_ious(anchor, bbox, inside_index)
        调用_calc_ious()函数得到每个anchor与哪个bbox的iou最大以及这个iou值、每个bbox与哪个anchor的iou最大(需要体会从行和列取最大值的区别)
        label[
            max_ious < self.neg_iou_thresh] = 0  # 把每个anchor与对应的框求得的iou值与负样本阈值比较,若小于负样本阈值,则label设为0,pos_iou_thresh=0.7, neg_iou_thresh=0.3
        label[gt_argmax_ious] = 1  # 把与每个bbox求得iou值最大的anchor的label设为1
        label[max_ious >= self.pos_iou_thresh] = 1  ##把每个anchor与对应的框求得的iou值与正样本阈值比较,若大于正样本阈值,则label设为1
        n_pos = int(self.pos_ratio * self.n_sample)  # 按照比例计算出正样本数量,pos_ratio=0.5,n_sample=256
        pos_index = np.where(label == 1)[0]  # 得到所有正样本的索引
        if len(pos_index) > n_pos:  # 如果选取出来的正样本数多于预设定的正样本数,则随机抛弃,将那些抛弃的样本的label设为-1
            disable_index = np.random.choice(
                pos_index, size=(len(pos_index) - n_pos), replace=False)
            label[disable_index] = -1
        n_neg = self.n_sample - np.sum(label == 1)  # 设定的负样本的数量
        neg_index = np.where(label == 0)[0]  # 负样本的索引
        if len(neg_index) > n_neg:
            disable_index = np.random.choice(
                neg_index, size=(len(neg_index) - n_neg),
                replace=False)  # 随机选择不要的负样本,个数为len(neg_index)-neg_index,label值设为-1
            label[disable_index] = -1
        return argmax_ious, label

    # 下面为调用的_calc_ious()函数
    def _calc_ious(self, anchor, bbox, inside_index):
        ious = bbox_iou(anchor, bbox)  # 调用bbox_iou函数计算anchor与bbox的IOU, ious:(N,K),N为anchor中第N个,K为bbox中第K个,N大概有15000个
        argmax_ious = ious.argmax(axis=1)  # 1代表行,0代表列
        max_ious = ious[np.arange(len(inside_index)), argmax_ious]  # 求出每个anchor与哪个bbox的iou最大,以及最大值,max_ious:[1,N]
        gt_argmax_ious = ious.argmax(axis=0)
        gt_max_ious = ious[gt_argmax_ious, np.arange(ious.shape[1])]  # 求出每个bbox与哪个anchor的iou最大,以及最大值,gt_max_ious:[1,K]
        gt_argmax_ious = np.where(ious == gt_max_ious)[0]  # 然后返回最大iou的索引(每个bbox与哪个anchor的iou最大),有K个
        return argmax_ious, max_ious, gt_argmax_ious

step3:训练RPN

生成并标注完了训练样本,终于来到了第一功能模块的训练环节。首先对Feature Map进行3×3卷积操作,而后分为两个分支,每一分支都先进行1×1卷积操作,目的是压缩channel。第一个分支的通道数压缩成9×2,9代表每一个锚点的9个anchors,2代表每一个anchor是前景或后景的概率。第二个分支的通道数压缩成9×4,9代表每一个锚点的9个anchors,4代表每一个anchor的4个位置参数预测值。每一个min-batch,只对128个负样本和128个正样本计算分类损失和回归损失( 实际上只对正样本进行回归损失计算)。损失函数如下:
Faster R-CNN最全讲解
分类损失函数选择的是传统的交叉熵损失函数,分类损失函数选择的是Smooth L1 Loss回归损失函数,如下:
Faster R-CNN最全讲解
由于在实际过程中,Faster R-CNN最全讲解= min_batch ,Faster R-CNN最全讲解= feature map的大小,两者差距过大,用参数λ平衡二者,使总的网络Loss计算过程中能够均匀考虑2种Loss。

【Module 2】

第二个模块则是根据第一个功能模块输出的得分和四个位置参数,得出ROI并选出合适的RP。该模块在ProposalCreator函数中完成,代码的核心思想:通过训练好的第一个模块输出的约20000个anchor的4个位置参数,微调原图中所有anchor,生成20000个ROI。接着,对ROI进行裁剪,并且剔除掉裁剪后长和宽小于设定阈值的ROI。然后,根据前景score对剩余ROI进行由大到小的排序,若用于RoiHead训练,则取前12000个ROI,经过NMS二次筛选后只取前2000个ROI作为最终的region proposals。若用于RoiHead测试,则取前2000个ROI,经过NMS二次筛选后只取前300个ROI作为最终的region proposals。具体代码实现如下:

# 下面是ProposalCreator的代码: 这部分的操作不需要进行反向传播,因此可以利用numpy/tensor实现
class ProposalCreator:  # 对于每张图片,利用它的feature map,计算(H/16)x(W/16)x9(大概20000)个anchor属于前景的概率,然后从中选取概率较大的12000张,利用位置回归参数,修正这12000个anchor的位置, 利用非极大值抑制,选出2000个ROIS以及对应的位置参数。
    def __call__(self, loc, score, anchor, img_size,
                 scale=1.):  # 这里的loc和score是经过region_proposal_network中经过1x1卷积分类和回归得到的
        if self.parent_model.training:
            n_pre_nms = self.n_train_pre_nms  # 12000
            n_post_nms = self.n_train_post_nms  # 经过NMS后有2000个

        else:
            n_pre_nms = self.n_test_pre_nms  # 6000
            n_post_nms = self.n_test_post_nms  # 经过NMS后有300个


        roi = loc2bbox(anchor, loc)  # 将bbox转换为近似groudtruth的anchor(即rois)
        roi[:, slice(0, 4, 2)] = np.clip(roi[:, slice(0, 4, 2)], 0, img_size[0])  # 裁剪将rois的ymin,ymax限定在[0,H]
        roi[:, slice(1, 4, 2)] = np.clip(roi[:, slice(1, 4, 2)], 0, img_size[1])  # 裁剪将rois的xmin,xmax限定在[0,W]

        min_size = self.min_size * scale  # 16
        hs = roi[:, 2] - roi[:, 0]  # rois的宽
        ws = roi[:, 3] - roi[:, 1]  # rois的长
        keep = np.where((hs >= min_size) & (ws >= min_size))[0]  # 确保rois的长宽大于最小阈值
        roi = roi[keep, :]

        score = score[keep]  # 对剩下的ROIs进行打分(根据region_proposal_network中rois的预测前景概率)
        order = score.ravel().argsort()[::-1]  # 将score拉伸并逆序(从高到低)排序
        if n_pre_nms > 0:
            order = order[:n_pre_nms]  # train时从20000中取前12000个rois,test取前6000个
        roi = roi[order, :]

        keep = non_maximum_suppression(
        cp.ascontiguousarray(cp.asarray(roi)),
            thresh=self.nms_thresh)  # (具体需要看NMS的原理以及输入参数的作用)调用非极大值抑制函数,将重复的抑制掉,就可以将筛选后ROIS进行返回。经过NMS处理后Train数据集得到2000个框,Test数据集得到300个框
        if n_post_nms > 0:
            keep = keep[:n_post_nms]
        roi = roi[keep]
        return roi

五:Semi-Fast R-CNN(RoiHead)

介绍完了RPN模块后,最重要的RP提取任务已经完成。接下来RoiHead只要将RPN输出的RP结果作为输入,来训练和测试。我将训练阶段和测试模块分开讲解:

【训练阶段】

step1:RP中标注训练样本

如果是在训练阶段,RPN会输出大约2000个region proposals。那么如何从中选取样本并标注呢?ProposalTargetCreator函数实现了这一任务,代码的核心思想是:首先,将2000个RP和M个Ground Truth拼接起来,也就是把所有的GT也都作为RP。为什么呢?

前方核能:其实答案很简单,现在是RoiHead的训练阶段,训练RoiHead的分类和二次回归能力。也就是说,需要给该网络输入带有类别标注和实际位置参数的训练数据,经过RPN选出的大范围包括实物的ROI可以作为训练数据,说实话也都是歪歪扭扭的,拿它们训练RoiHead其实主要是出于测试阶段的实际情况出发的,毕竟要从任务实际需要适应的情况出发,实际测试的时候都是RPN找出RP,RoiHead再对它们进行分类和bbox修正的,这些RP都是歪歪扭扭的。所以也就是说,拿这些样本进行分类本身就是不严谨的,不是说完完全全包裹住实物合格的类别样本。毕竟现在是训练阶段嘛,稍微偷偷喂给网络一点”优质碳水“,未尝不可,直接把最优质的GT给它训练去,货真价实的分类样本,舒不舒服,白用白不用。

回到正题,拼接好了RP和GT后,计算它们的最大IOU所对应的那个GT的label,将(label+1)作为每一个RP的类别标注(1~20)。然后将IOU>0.5的RP中选64个作为正样本,将IOU<0.5的RP中选出64个作为负样本并将负样本的label设为0,最后将共128个正负样本打包出来,作为RoiHead的训练输入。具体的实现代码如下:

# 下面是ProposalTargetCreator代码:ProposalCreator产生2000个ROIS,但是这些ROIS并不都用于训练,经过本ProposalTargetCreator的筛选产生128个用于自身的训练
    class ProposalTargetCreator(object):  # 为2000个rois赋予ground truth!(严格讲挑出128个赋予ground truth!)
        # 输入:2000个rois、一个batch(一张图)中所有的bbox ground truth(R,4)、对应bbox所包含的label(R,1)(VOC2007来说20类0-19)
        # 输出:128个sample roi(128,4)、128个gt_roi_loc(128,4)、128个gt_roi_label(128,1)
        def __call__(self, roi, bbox, label, loc_normalize_mean=(0., 0., 0., 0.),
                     loc_normalize_std=(0.1, 0.1, 0.2, 0.2)):  # 因为这些数据是要放入到整个大网络里进行训练的,比如说位置数据,所以要对其位置坐标进行数据增强处理(归一化处理)
            n_bbox, _ = bbox.shape
            roi = np.concatenate((roi, bbox), axis=0)  # 首先将2000个roi和m个bbox给concatenate了一下成为新的roi(2000+m,4)。
            pos_roi_per_image = np.round(
                self.n_sample * self.pos_ratio)  # n_sample = 128,pos_ratio=0.5,round 对传入的数据进行四舍五入
            iou = bbox_iou(roi, bbox)  # 计算每一个roi与每一个bbox的iou  (2000+m,m)
            gt_assignment = iou.argmax(axis=1)  # 按行找到最大值,返回最大值对应的序号以及其真正的IOU。返回的是每个roi与**哪个**bbox的最大,以及最大的iou值
            max_iou = iou.max(axis=1)  # 每个roi与对应bbox最大的iou
            gt_roi_label = label[gt_assignment] + 1  # 从1开始的类别序号,给每个类得到真正的label(将0-19变为1-20)
            pos_index = np.where(max_iou >= self.pos_iou_thresh)[0]  # 同样的根据iou的最大值将正负样本找出来,pos_iou_thresh=0.5
            pos_roi_per_this_image = int(
                min(pos_roi_per_image, pos_index.size))  # 需要保留的roi个数(满足大于pos_iou_thresh条件的roi与64之间较小的一个)
            if pos_index.size > 0:
                pos_index = np.random.choice(
                    pos_index, size=pos_roi_per_this_image, replace=False)  # 找出的样本数目过多就随机丢掉一些

            neg_index = np.where((max_iou < self.neg_iou_thresh_hi) &
                                 (max_iou >= self.neg_iou_thresh_lo))[0]  # neg_iou_thresh_hi=0.5,neg_iou_thresh_lo=0.0
            neg_roi_per_this_image = self.n_sample - pos_roi_per_this_image  # #需要保留的roi个数(满足大于0小于neg_iou_thresh_hi条件的roi与64之间较小的一个)
            neg_roi_per_this_image = int(min(neg_roi_per_this_image,
                                             neg_index.size))
            if neg_index.size > 0:
                neg_index = np.random.choice(
                    neg_index, size=neg_roi_per_this_image, replace=False)  # 找出的样本数目过多就随机丢掉一些

            keep_index = np.append(pos_index, neg_index)
            gt_roi_label = gt_roi_label[keep_index]
            gt_roi_label[pos_roi_per_this_image:] = 0  # 负样本label 设为0
            sample_roi = roi[keep_index]
            # 那么此时输出的128*4的sample_roi就可以去扔到 RoIHead网络里去进行分类与回归了。同样, RoIHead网络利用这sample_roi+featue为输入,输出是分类(21类)和回归(进一步微调bbox)的预测值,那么分类回归的groud truth就是ProposalTargetCreator输出的gt_roi_label和gt_roi_loc。
            gt_roi_loc = bbox2loc(sample_roi, bbox[gt_assignment[keep_index]])  # 求这128个样本的groundtruth
            gt_roi_loc = ((gt_roi_loc - np.array(loc_normalize_mean, np.float32)
                           ) / np.array(loc_normalize_std,
                                        np.float32))  # ProposalTargetCreator首次用到了真实的21个类的label,且该类最后对loc进行了归一化处理,所以预测时要进行均值方差处理
            return sample_roi, gt_roi_loc, gt_roi_label

step2: 正式训练

将这128个标注好的训练样本,从原图中投影到Feature Map中对应的ROI区域中,然后进入RoiPooling层,将这些大小不一的ROI区域变为同一长度的向量,再经过两层4096FC层,分别得到softmax21分类打分和bbox的84个参数(21 * 4)的预测结果,放入损失函数中进行反向传播更新网络权重,其中只计算正样本的回归框损失。损失函数和RPN的类似,这里就不再赘述了,贴上损失函数核心代码:

def _fast_rcnn_loc_loss(pred_loc, gt_loc, gt_label, sigma): #输入分别为rpn回归框的偏移量与anchor与bbox的偏移量以及label
    in_weight = t.zeros(gt_loc.shape).cuda()
    # Localization loss is calculated only for positive rois.
    # NOTE:  unlike origin implementation, 
    # we don't need inside_weight and outside_weight, they can calculate by gt_label
    in_weight[(gt_label > 0).view(-1, 1).expand_as(in_weight).cuda()] = 1
    loc_loss = _smooth_l1_loss(pred_loc, gt_loc, in_weight.detach(), sigma) #sigma设置为1
    # Normalize by total number of negtive and positive rois.
    loc_loss /= ((gt_label >= 0).sum().float()) # ignore gt_label==-1 for rpn_loss #除去背景类
    return loc_loss
roi_cls_loss = nn.CrossEntropyLoss()(roi_score, gt_roi_label.cuda())#求交叉熵损失

【测试阶段】

RoiHead的测试阶段就是将RPN中输出的300个RP,输入进网络中,最后会输出每个RP的类和4个回归框微调参数。剔除掉高于背景(0)阈值和最大类别(1~20)得分低于阈值的RP,最后根据回归参数,对筛选后剩下的RP框进行微调,得到最终的bounding box!至此,大功告成。

六:Faster R-CNN训练方法

Faster-RCNN有两种训练方式:四步交替迭代训练和联合训练。本文主要讲解四步交替迭代的训练方式,如下所示:

1、训练RPN,使用大型数据集预训练模型初始化共享卷积和RPN权重,端到端训练RPN,用于生成Region Proposals;
2、训练Fast R-CNN,使用相同的预训练模型初始化共享卷积【注意此处是初始化一个新的与第1步结构相同的共享卷积网络,而不是第1步中训练得到的】,锁住第1步训练好的RPN权重,结合RPN得到的Proposals训练RCNN网络;
3、调优RPN,使用第2步训练好的共享卷积和RCNN,固定共享卷积层,继续训练RPN,我认为这一步相当于对第1步训练好的RPN进行微调;
4、调优Fast R-CNN,使用第3步训练好的共享卷积和RPN(固定住共享卷积层),继续对RCNN进行训练微调
5、重复上述步骤3、4,进行迭代。(一般到步骤四其实已经够了,后面迭代训练后的效果几乎无提升)

下面是训练过程的流程图,应该比较清楚:
Faster R-CNN最全讲解

七:Faster R-CNN测试方法

接下来讲解全网的测试流程,大功告成!

step1:输入图像经过卷积层得到feature map

step2:feature map经过RPN得到300个RP

step3:将RP输入到RoiHead网络中

step4:得出每个RP的类别得分和bbox位置参数

step5:由得分阈值选出最终的ROI

step6:结合位置参数微调ROI的bbox框

step7:经过NMS后画出最终检测框

八:总结

Fast R-CNN尽管速度和精度上都有了很大的提升,但仍然未能实现端到端(end-to-end)的目标检测,比如候选区域的获得不能同步进行,速度上还有提升空间。

最后附上一张超级原理流程图,供大家参考:

在这里插入图片描述

至此我对Faster R-CNN全部流程与细节,进行了深度讲解,希望对大家有所帮助,有不懂的地方或者建议,欢迎大家在下方留言评论。(码字不易,各位看官点个赞,手留余香~谢谢!)

我是努力在CV泥潭中摸爬滚打的江南咸鱼,我们一起努力,不留遗憾!

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(1)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2022年4月10日 下午5:01
下一篇 2022年4月10日 下午5:18

相关推荐