Pytorch版Faster R-CNN的RPN网络解释

Pytorch源码:https://github.com/chenyuntc/simple-faster-rcnn-pytorch
首先说一下几个提前准备的函数:
(1)loc2bbox:这个函数接受bbox源框和偏差量loc来计算最终的回归框位置。

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

(2)bbox2loc:这个函数接受源框src_bbox和目标框dst_bbox,来输出目标框相对于源框的偏差量loc

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

(3)bbox_iou:这个函数用于计算两个框的交并比

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

(4)generate_anchor_base:这个函数接受锚框基准值base_size,长宽比ratios,锚框缩放比anchor_scale作为参数,返回这些组合下一个像素点生成的几种锚框的左上角右下角坐标值anchor_base(这里就是9种)

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
    return anchor_base 

最先经过generate_anchor_base生成9种形状的锚框anchor_base,然后利用下面的算法得到所有像素点的锚框,大概20000个。

self.anchor_base = generate_anchor_base(
            anchor_scales=anchor_scales, ratios=ratios)  #首先生成上述以(0,0)为中心的9个base anchor

n, _, hh, ww = x.shape    # x为feature map,n为batch_size,此版本代码为1. hh,ww即为宽高
anchor = _enumerate_shifted_anchor( np.array(self.anchor_base), self.feat_stride, hh, ww)  # feat_stride=16 ,因为是经4次pool后提到的特征,故feature map较原图缩小了16倍
            
def _enumerate_shifted_anchor(anchor_base, feat_stride, height, width):  ##利用anchor_base生成所有对应feature map的anchor
    shift_y = xp.arange(0, height * feat_stride, feat_stride)  #纵向偏移量(0,16,32,...)
    shift_x = xp.arange(0, width * feat_stride, feat_stride)  # 横向偏移量(0,16,32,...)
    shift_x, shift_y = xp.meshgrid(shift_x, shift_y)   #shift_x = [[0,16,32,..],[0,16,32,..],[0,16,32,..]...],shift_x = [[0,0,0,..],[16,16,16,..],[32,32,32,..]...],就是形成了一个纵横向偏移量的矩阵,也就是特征图的每一点都能够通过这个矩阵找到映射在原图中的具体位置!
#产生的大X 以x的行为行,以y的元素个数为列构成矩阵,同样的产生的Y以y的行作为列,以x的元素个数作为列数产生矩阵
    shift = xp.stack((shift_y.ravel(), shift_x.ravel(),
                      shift_y.ravel(), shift_x.ravel()), axis=1)  #经过刚才的变化,其实大X,Y的元素个数已经相同,看矩阵的结构也能看出,矩阵大小是相同的,X.ravel()之后变成一行,此时shift_x,shift_y的元素个数是相同的,都等于特征图的长宽的乘积(像素点个数),不同的是此时的shift_x里面装得是横向看的x的一行一行的偏移坐标,而此时的y里面装得是对应的纵向的偏移坐标!下图显示xp.meshgrid(),shift_y.ravel()操作示例
    A = anchor_base.shape[0] #A=9
    K = shift.shape[0] #读取特征图中元素的总个数
    anchor = anchor_base.reshape((1, A, 4)) + \
             shift.reshape((1, K, 4)).transpose((1, 0, 2))    #用基础的9个anchor的坐标分别和偏移量相加,最后得出了所有的anchor的坐标,四列可以堪称是左上角的坐标和右下角的坐标加偏移量的同步执行,飞速的从上往下捋一遍,所有的anchor就都出来了!一共K个特征点,每一个有A(9)个基本的anchor,所以最后reshape((K*A),4)的形式,也就得到了最后的所有的anchor左下角和右上角坐标.          
    anchor = anchor.reshape((K * A, 4)).astype(np.float32)
    return anchor

把上一步的anchor以及真实框坐标bbox以及图片大小img_size送给AnchorTargetCreator处理器,具体操作就是会把超出边界的锚框anchor给标记为-1(就是不要了),并根据n_sample(参加训练的总样本数)以及正负标签比选出一些框,并把其他框丢弃(就算是正负标签也标记为-1),从而得到训练RPN网络的正确值组合loc,label,它们会在损失函数与网络计算出来的rpn_loc和rpn_scores进行运算来训练模型
这里说的不要其实就是把标签记为-1,记为-1的锚框数据不参与训练
注意:模型算出来的rpn_loc与rpn_scores的形状分别是(n,20000,4)和(n,20000,2),我想着这算法不是用_get_inside_index()函数把不在边界内的去掉了吗?所以,这里要注意_unmap这个函数,它接受了一个处理过的数组(label \ loc) 以及原始长度 (这里是n_anchor) ,还有inside_index这个留存下来的成员在原始数组的索引位置,根据 fill 的值来把这个数组形状还原成原来样子,对于那些空缺的地方就用 fill 的值补充,比如label就补充-1,表示这处的框被抛弃,loc就是补充0,偏差值直接设置为0,反正都被抛弃了。


#下面是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

首先说一下max_iou这个变量,它是_calc_iou函数算出来的每个锚框与之交并比最大的ground truth的之间的交并比具体大小,这里的label是在_create_label这一函数中利用max_iou来和阈值对比来看付给这个框什么标签,再根据样本数和比例进行再标记。就得到了最终的label。

还有argmax_ious这个变量,他也是_calc_iou算出来的,它指的是每个框对应最大交并比ground truth索引值,就是每个锚框和哪个ground truth相交最大。并从bbox选出第argmax_ious 处的ground truth利用bbox2loc来计算每一个anchor与交并比最大ground truth的偏移量。

_create_label这个函数就是得到一个去除了超边的锚框的label集合,以及去除了那些框的剩下框所对应的最大交并比的真实框的索引值数组argmax_ious
这个函数接受anchor和bbox参数不做解释,这个inside_index参数是之前的去除超出边框的剩下的边框索引。

#下面为调用的_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这个函数就是返回一个每个锚框(已经除去超出边的框)所对应最大交并比的ground_truth的索引argmax_ious,还有每个锚框对应交并比最大的ground_truth的具体交并比大小的数组max_ious,最后一个就是每个ground_truth对应交并比最大的锚框anchor的索引。

#下面为调用的_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

以下是RPN网络框架,输出结果就包括之前说的rpn_scores和rpn_loc以及锚框的信息anchor,只是具体的话,它还会输出继续送往后方faster_rcnn网络的rois以及对应rois所处批量的序号集合rois_indices。

def __init__(
            self, in_channels=512, mid_channels=512, ratios=[0.5, 1, 2],
            anchor_scales=[8, 16, 32], feat_stride=16,
            proposal_creator_params=dict(),
    ):
        super(RegionProposalNetwork, self).__init__()
        self.anchor_base = generate_anchor_base(
            anchor_scales=anchor_scales, ratios=ratios) #调用generate_anchor_base()函数,生成左上角9个anchor_base
        self.feat_stride = feat_stride
        self.proposal_layer = ProposalCreator(self, **proposal_creator_params)
        n_anchor = self.anchor_base.shape[0] #9
        self.conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
        self.score = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0)
        self.loc = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0)
        normal_init(self.conv1, 0, 0.01) 归一化
        normal_init(self.score, 0, 0.01)
        normal_init(self.loc, 0, 0.01)
 def forward(self, x, img_size, scale=1.):
        n, _, hh, ww = x.shape  #(batch_size,512,H/16,W/16),其中H,W分别为原图的高和宽
        anchor = _enumerate_shifted_anchor(
            np.array(self.anchor_base),
            self.feat_stride, hh, ww)  #在9个base_anchor基础上生成hh*ww*9个anchor,对应到原图坐标

        n_anchor = anchor.shape[0] // (hh * ww)  #hh*ww*9/hh*ww=9
        h = F.relu(self.conv1(x)) #512个3x3卷积(512, H/16,W/16),后面都不写batch_size了
        rpn_locs = self.loc(h) #n_anchor(9)*4个1x1卷积,回归坐标偏移量。(9*4,hh,ww)

        rpn_locs = rpn_locs.permute(0, 2, 3, 1).contiguous().view(n, -1, 4) #转换为(n,hh,ww,9*4)后变为(n,hh*ww*9,4)
        rpn_scores = self.score(h) #n_anchor(9)*2个1x1卷积,回归类别。(9*2,hh,ww)
        rpn_scores = rpn_scores.permute(0, 2, 3, 1).contiguous()#转换为(n,hh,ww,9*2)
        rpn_softmax_scores = F.softmax(rpn_scores.view(n, hh, ww, n_anchor, 2), dim=4) #计算{Softmax}(x_{i}) = \{exp(x_i)}{\sum_j exp(x_j)}
        rpn_fg_scores = rpn_softmax_scores[:, :, :, :, 1].contiguous()#得到前景的分类概率
        rpn_fg_scores = rpn_fg_scores.view(n, -1)#得到所有anchor的前景分类概率
        rpn_scores = rpn_scores.view(n, -1, 2)#得到每一张feature map上所有anchor的网络输出值

        rois = list()
        roi_indices = list()
        for i in range(n): #n为batch_size数
            roi = self.proposal_layer(
                rpn_locs[i].cpu().data.numpy(),
                rpn_fg_scores[i].cpu().data.numpy(),
                anchor, img_size,
                scale=scale) # 调用ProposalCreator函数, rpn_locs维度(hh*ww*9,4),rpn_fg_scores维度为(hh*ww*9),anchor的维度为(hh*ww*9,4), img_size的维度为(3,H,W),H和W是经过数据预处理后的。计算(H/16)x(W/16)x9(大概20000)个anchor属于前景的概率,取前12000个并经过NMS得到2000个近似目标框G^的坐标。roi的维度为(2000,4)
            batch_index = i * np.ones((len(roi),), dtype=np.int32)
            rois.append(roi) #rois为所有batch_size的roi
            roi_indices.append(batch_index)

        rois = np.concatenate(rois, axis=0)#按行拼接(即没有batch_size的区分,每一个[]里都是一个anchor的四个坐标)
        roi_indices = np.concatenate(roi_indices, axis=0)#这个 roi_indices在此代码中是多余的,因为我们实现的是batch_siae=1的网络,一个batch只会输入一张图象。如果多张图象的话就需要存储索引以找到对应图像的roi
        return rpn_locs, rpn_scores, rois, roi_indices, anchor #rpn_locs的维度(hh*ww*9,4),rpn_scores维度为(hh*ww*9,2), rois的维度为(2000,4),roi_indices用不到,anchor的维度为(hh*ww*9,4)
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值