计算机系统应用教程网站

网站首页 > 技术文章 正文

Faster RCNN目标检测之RPN网络

btikc 2024-08-31 17:16:24 技术文章 14 ℃ 0 评论
  • rpn介绍
  • rpn(Region Proposal Network, 区域候选网络)是faster rcnn中最重要的改进。它的主要功能是生成区域候选(Region Proposal),通俗来说,区域候选可以看做是许多潜在的边界框(也叫anchor,它是包含4个坐标的矩形框)。

    那么为什么需要区域候选呢,因为在目标检测之前,网络并不知道图片中存在多少个目标物体,所以rpn通常会预先在原图上生成若干个边界框,并最终输出最有可能包含物体的anchor,也称之为区域候选,训练阶段会不断的调整区域候选的位置,使其与真实物体框的偏差最小。

    rpn的结构如下图所示,可以看到,backbone输出的特征图经过一个3 * 3卷积之后分别进入了不同的分支,对应不同的1 * 1卷积。第一个卷积为定位层,输出anchor的4个坐标偏移。第二个卷积为分类层,输出anchor的前后景概率。

    • rpn详细过程

    看完了rpn的大致结构,下面来看rpn的详细过程。上图中展示的就不细讲了,主要来看一下,rpn是如何生成以及处理anchor的。下图表示了rpn网络的详细结构

    第一步,生成基础anchor(base_anchor),基础anchor的数目 = 长宽比的数目 * anchor的缩放比例数目, 即anchors_num = len(ratios) * len(scales)。这里,设置了3种长宽比(1:1, 1:2,2:1)和3种缩放尺度(8, 16, 32),因此anchor_num = 9. 下图表示了其中一个位置对应的9个尺寸的anchor。

    第二步,根据base_anchor,对特征图上的每一个像素,都会以它为中心生成9种不同尺寸的边界框,所以总共生成60 40 9 = 21600个anchor。下图所示的为特征图上的每个像素点在原图上对应的位置。需要注意的是,所有生成的anchor都是相对于原图而言的。

    第三步,也是最后一步,进行anchor的筛选。首先将定位层输出的坐标偏移应用到所有生成的anchor(也就是图2中anchor to iou),然后将所有anchor按照前景概率/得分进行从高到低排序。如图2所示,只取前pre_nms_num个anchor(训练阶段),最后anchor通过非极大值抑制(Non-Maximum-Suppression, nms)筛选得到post_nms_num(训练阶段)个anchor,也称作roi。

    • rpn代码实现

    首先是RegionProposalNetwork类的详细代码。

    # ------------------------ rpn----------------------#
    
    import numpy as np
    from torch.nn import functional as F
    import torch as t
    from torch import nn
    
    from model.utils.bbox_tools import generate_anchor_base
    from model.utils.creator_tool import ProposalCreator
    
    class RegionProposalNetwork(nn.Module):
        
        """
        Args:
            in_channels (int): 输入的通道数
            mid_channels (int): 中间层输出的通道数
            ratios (list of floats): anchor的长宽比
            anchor_scales (list of numbers): anchor的缩放尺度
            feat_stride (int): 原图与特征图的大小比例
            proposal_creator_params (dict): 传入ProposalCreator类的参数
        """
    
        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__()
    
            # 生成数量为(len(ratios) * len(anchors_scales))的基础anchor, 基础尺寸为16 * feat_stride
            self.anchor_base = generate_anchor_base(
                anchor_scales=anchor_scales, ratios=ratios)
            self.feat_stride = feat_stride
            self.proposal_layer = ProposalCreator(self, **proposal_creator_params)
            n_anchor = self.anchor_base.shape[0]
            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.):
            """
            注释
            * :math:`N` batch size
            * :math:`C` 输入的通道数
            * :math:`H` and :math:`W` 输入特征图的高和宽
            * :math:`A` 指定每个像素的anchor数目
    
            Args:
                x (tensor): backbone输出的特征图. shape -> :math:`(N, C, H, W)`.
                img_size (tuple of ints): 元组 :obj:`height, width`, 缩放后的图片尺寸.
                scale (float): 从文件读取的图片和输入的图片的比例大小.
    
            Returns:
                * **rpn_locs**: 预测的anchor坐标位移. shape -> :math:`(N, H W A, 4)`.
                * **rpn_scores**:  预测的前景概率得分. shape -> :math:`(N, H W A, 2)`.
                * **rois**: 筛选后的anchor数组. 它包含了一个批次的所有区域候选. shape -> :math:`(R', 4)`.
                * **roi_indices**: 表示roi对应的批次,shape -> :math:`(R',)`.
                * **anchor**: 生成的所有anchor. \
                    shape -> :math:`(H W A, 4)`.
    
            """
            n, _, hh, ww = x.shape
    
            # 根据基础anchor生成所有anchors, 所有的anchor均是在原图上生成的
            # 一共生成 hh * ww * 9个anchor
            anchor = _enumerate_shifted_anchor(
                np.array(self.anchor_base),
                self.feat_stride, hh, ww)
    
            n_anchor = anchor.shape[0] // (hh * ww)
            h = F.relu(self.conv1(x))
    
            # 定位层, rpn_locs --> (batch_size, 36, hh, ww)
            rpn_locs = self.loc(h)
            
            rpn_locs = rpn_locs.permute(0, 2, 3, 1).contiguous().view(n, -1, 4)
    
            # 分类层, rpn_locs --> (batch_size, 18, hh, ww)
            rpn_scores = self.score(h)
    
            rpn_scores = rpn_scores.permute(0, 2, 3, 1).contiguous()
            rpn_softmax_scores = F.softmax(rpn_scores.view(n, hh, ww, n_anchor, 2), dim=4)
    
            # 前景概率
            rpn_fg_scores = rpn_softmax_scores[:, :, :, :, 1].contiguous()
    
            # shape --> (batch_size * hh * ww, 9)
            rpn_fg_scores = rpn_fg_scores.view(n, -1)
            
            # shape --> (batch_size * hh * ww, 9, 2)
            rpn_scores = rpn_scores.view(n, -1, 2)
    
            rois = list()
            roi_indices = list()
            for i in range(n):
            # 分批次处理
                # 根据anchors、预测的位置偏移和前景概率得分来生成候选区域
                """
                1、移除超出区域的anchor,按前景概率排序取出前pre_nums(训练阶段12000,测试阶段6000)个anchors。
                2、进行nms,取出前post_nums(训练阶段2000,测试阶段300)个anchors
                """
                roi = self.proposal_layer(
                    rpn_locs[i].cpu().data.numpy(),
                    rpn_fg_scores[i].cpu().data.numpy(),
                    anchor, img_size,
                    scale=scale)
    
                batch_index = i * np.ones((len(roi),), dtype=np.int32)
                rois.append(roi)
                roi_indices.append(batch_index)
    
            rois = np.concatenate(rois, axis=0)
            roi_indices = np.concatenate(roi_indices, axis=0)
            return rpn_locs, rpn_scores, rois, roi_indices, anchor  
            
            
    def normal_init(m, mean, stddev, truncated=False):
        """
        权重初始化
        """
        # x is a parameter
        if truncated:
            m.weight.data.normal_().fmod_(2).mul_(stddev).add_(mean)  # not a perfect approximation
        else:
            m.weight.data.normal_(mean, stddev)
            m.bias.data.zero_()
            
    # 根据基础anchor生成所有anchor
    def _enumerate_shifted_anchor(anchor_base, feat_stride, height, width):
        """
        return
            shape -> (height * width * 9, 4)
        """
        shift_y = np.arange(0, height * feat_stride, feat_stride)
        shift_x = np.arange(0, width * feat_stride, feat_stride)
        
        # 根据特征图大小,在原图上构建网格
        shift_x, shift_y = np.meshgrid(shift_x, shift_y)
        shift = np.stack((shift_y.ravel(), shift_x.ravel(),
                          shift_y.ravel(), shift_x.ravel()), axis=1)
    
        A = anchor_base.shape[0]
        K = shift.shape[0]
        anchor = anchor_base.reshape((1, A, 4)) + \
                 shift.reshape((1, K, 4)).transpose((1, 0, 2))
        anchor = anchor.reshape((K * A, 4)).astype(np.float32)
        return anchor

    然后是ProposalCreator类的代码,它负责rpn网络的anchor筛选,输出区域候选(roi)

    # -------------------- ProposalCreator ---------------#
    
    class ProposalCreator:
        """
        Args:
            nms_thresh (float): 调用nms使用的iou阈值
            n_train_pre_nms (int): 在训练阶段,调用nms之前,保留的分值最高的前多少个anchor
            n_train_post_nms (int): 在训练阶段,调用nms之后,保留的分值最高的前多少个
            n_test_pre_nms (int): 在测试阶段,调用nms之前,保留的分值最高的前多少个anchor
            n_test_post_nms (int): 在测试阶段,调用nms之后,保留的分值最高的前多少个anchor
            min_size (int): 尺寸阈值,小于该尺寸则丢弃。
        """
    
        def __init__(self,
                     parent_model,
                     nms_thresh=0.7,
                     n_train_pre_nms=12000,
                     n_train_post_nms=2000,
                     n_test_pre_nms=6000,
                     n_test_post_nms=300,
                     min_size=16
                     ):
            self.parent_model = parent_model
            self.nms_thresh = nms_thresh
            self.n_train_pre_nms = n_train_pre_nms
            self.n_train_post_nms = n_train_post_nms
            self.n_test_pre_nms = n_test_pre_nms
            self.n_test_post_nms = n_test_post_nms
            self.min_size = min_size
    
        def __call__(self, loc, score, anchor, img_size, scale=1.):
            """
            :math:`R` anchor总数目. 
            
            Args:
                loc (array): 预测的坐标偏移,shape -> :math:`(R, 4)`.
                score (array): 预测的前景概率,shape -> :math:`(R,)`.
                anchor (array): 生成的anchor,shape -> :math:`(R, 4)`.
                img_size (tuple of ints): 元组,缩放前的图片尺寸 :obj:`height, width`.
                scale (float): 
    
            Returns:
                array:
    
            """
            # NOTE: 测试时,需要
            # faster_rcnn.eval()
            # 设置 self.traing = False
            if self.parent_model.training:
                n_pre_nms = self.n_train_pre_nms
                n_post_nms = self.n_train_post_nms
            else:
                n_pre_nms = self.n_test_pre_nms
                n_post_nms = self.n_test_post_nms
    
            # 将anchor转换为候选.
            roi = loc2bbox(anchor, loc)
    
            
            roi[:, slice(0, 4, 2)] = np.clip(
                roi[:, slice(0, 4, 2)], 0, img_size[0])
            roi[:, slice(1, 4, 2)] = np.clip(
                roi[:, slice(1, 4, 2)], 0, img_size[1])
    
            # 丢弃尺寸小于最小尺寸阈值的anchor
            min_size = self.min_size * scale
            hs = roi[:, 2] - roi[:, 0]
            ws = roi[:, 3] - roi[:, 1]
            keep = np.where((hs >= min_size) & (ws >= min_size))[0]
            roi = roi[keep, :]
            score = score[keep]
    
            # 按照前景概率从大到小排序
            # Take top pre_nms_topN (e.g. 6000).
            order = score.ravel().argsort()[::-1]
            if n_pre_nms > 0:
                order = order[:n_pre_nms]
            roi = roi[order, :]
    
            keep = non_maximum_suppression(
                np.ascontiguousarray(np.asarray(roi)),
                thresh=self.nms_thresh)
            if n_post_nms > 0:
                keep = keep[:n_post_nms]
            roi = roi[keep]
            return roi
    • 细节
    1. 为什么不是直接预测anchor的中心坐标以及长宽或者4个坐标,而是预测anchor的坐标偏移(图中的px,py,pw,ph)呢?
      a. 如直接预测中心坐标以及长宽或者是预测4个坐标,则大部分预测都是无效预测,因为网络预测的输出并不太可能满足这种约束条件。
      b. 图片中的物体通常大小不一,形状也大不相同,直接预测中心坐标以及长宽或者是4个坐标,范围过大,这使得网络难以训练。
      c. 而坐标偏移一方面大小较小,同时,坐标偏移有良好的数学公式,能够方便的求导。
    2. iou的计算
    def bbox_iou(bbox_a, bbox_b):
        """
        return:
            array: shape -> (bbox_a.shape[0], bbox_b.shape[1])
        """
        if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
            raise IndexError
    
        # 上边界和左边界
        tl = np.maximum(bbox_a[:, None, :2], bbox_b[:, :2])
        # 下边界和右边界
        br = np.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:])
        area_i = np.prod(br - tl, axis=2) * (tl < br).all(axis=2)
        
        area_a = np.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)
        area_b = np.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1)
        return area_i / (area_a[:, None] + area_b - area_i)

    本文暂时没有评论,来添加一个吧(●'◡'●)

    欢迎 发表评论:

    最近发表
    标签列表