首页IT科技fasterrcnn代码复现(Faster R-CNN最全讲解)

fasterrcnn代码复现(Faster R-CNN最全讲解)

时间2025-05-05 14:57:42分类IT科技浏览3636
导读:一: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                  ,并将其融入进整体网络中            ,使得综合性能有较大提高   ,在检测速度方面尤为明显         。

二:网络架构

上图展示了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模块

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)模块

终于迎来了最重要的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

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

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个正样本计算分类损失和回归损失

( 实际上只对正样本进行回归损失计算)         。损失函数如下:

分类损失函数选择的是传统的交叉熵损失函数   ,分类损失函数选择的是Smooth L1 Loss回归损失函数            ,如下:

由于在实际过程中                  ,

N

c

N_c

Nc
= min_batch       ,

N

r

N_r

Nr
= 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区域变为同一长度的向量         ,再经过两层 4096 FC层                  ,分别得到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 dont 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测试方法

接下面讲解整个网络的测试过程            ,即将大功告成啦!

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泥潭中摸爬滚打的江南咸鱼               ,我们一起努力,不留遗憾!

声明:本站所有文章               ,如无特殊说明或标注                  ,均为本站原创发布         。任何个人或组织   ,在未征得本站同意时            ,禁止复制、盗用               、采集                  、发布本站内容到任何网站   、书籍等各类媒体平台                  。如若本站内容侵犯了原著者的合法权益                  ,可联系我们进行处理         。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
平板无法显示全部网页界面(网页显示不全怎么办?快速解决方法)