faster r-cnn 是第一个完全可微分的检测模型 。也就是从数据到模型输出的整条路径既可以 前行传播 forward ,也可以反向传播 backword 。是一个 end -to- end 模型。
faster r-cnn 发展进程
1. R-CNN :
特点:
候选区域 + 卷积特征提取 + (SVM)特征分类 和 边界回归
候选区域:select serch 方法; 提取个数大约:1k~2k
特征提取:CNN 网络
分类和边框回归: 事先训练的SVM 分类器分出类别后 进行线性边框回归得到精准位置。
致命缺陷:
- 事先大量候选区域提取:(1)无法做到端到端训练;(2)占用大量磁盘空间
- 输入CNN 的图像大小固定尺寸,resize 破坏原有特征
- 每个region proposal 都经过CNN 网络计算,导致过多重复特征提取
2. fast R-CNN
特点
候选区域 + 特征提取 + softmax分类和边框回归
改进
- 特征提取后最后一个卷积层后加了一个ROI pooling layer,参考ssp,不再对图像进行resize。
- 损失函数使用了multi-task loss(多任务损失)函数,将边框回归直接加到CNN网络中训练。分类直接用softmax替代SVM进行分类。
3 faster R-CNN
faster rcnn 的亮点是它不要事先进行select serch 获取候选框后再进行训练 ,因此提出了 RPN 网络。
1. RPN
2. fast R-CNN
三者对比
FASTER R-CNN 结构
1.用anchor 解决边框个数不定的问题
2.用ROI Pooling 解决不同大小边框输出特征向量一致的问题
数据框图
整体结构
1.DataSet 作为数据输入
2. BackBone 提取特征,ResNet , VGG16 等,获取特征图
3. 在特征图上对生成的 Anchor 进行 0-1 分类(区分前景和背景)和 边框回归。输入 BBOX 标签得到RPN loss
4. 对每一个anchor 边框(剔除超边界,低sore)经过 NMS 后按分数排序续输出 ROIS
5. ROIS 进入 ROIHead 前经过 ProposalTargetCreator 层,主要是根据 ROIS 与 BBOX 的 IOU 确定 正样本 和 负样本
6. ROIHead 层根据 ROIS 在feature map 上的映射结合 label 进行 边框回归 和 分类。
数据流图
FASTER R-CNN 核心技术
1. bounding box 回归
bbox 回归时要转换为:中心点(x,y) 与 宽高 (w,h) 。
原理:
对 bbox 的预测结果 P 和 标记 gt_box Q 之间可以 通过学习一组参数并进行线性变换得到。变换参数:
这四个参数都是特征的函数,前两个体现为bbox的中心尺度不变性,后两个体现为体现为bbox宽高的对数空间转换。学到这四个参数(函数)后,就可以将P映射到G’, 使得G’尽量逼近G。
也就是先将中心点进行平移,在对宽高尺度进行缩放,为约束宽高为正,取exp变换,除以宽高使得网络具有尺度不变性,网络输出特征自动适应这种变换。变换是人为设定,一切为了让网络更好学习。
在proposal 层,可以理解原始proposal为anchor预设框。
这里的变换参数:
是由特征图进过一层网络学习后得到,也就是是关于候选框 P 的特征的函数。所以变换关系可以定义为:
真值 G 与 预测 P 之间的关系如下:
所以边框回归的目标参数就是:
其中P为原始框,在proposal 层为anchor初始预设框,其中P值已知, G值已知, G^ 值为经过网络学习后映射得到。
由P, G 构建学习目标真值:
最后由学习输出特征值:
与P,G 构建的真值作loss,使得输出特征尽可能往真值上靠拢。
faster rcnn 的bounding box 回归如下:
测试阶段需要根据变换公式还原得到bounding box 的中心点和宽高:
我们的目的是使得每一个边框的变换参数 d 尽可能接近 t 。d 与 t 产生LOSS ,以此学习更新 边框特征的 W 权重。 使得测试时bbox经过W运算后可以得到一个较好的offsets与scales,利用这个offsets与scales可在原预测bbox上微调,得到更好的预测结果。还有就是这个回归数据对(P,G)不是随便选的,预测的P应该离至少一个ground truth G很近,这样学出来的参数才有意义。近的度量是P、G的IOU>0.6。
FASTER R-CRNN 训练
RPN network
上面分支用于分类前景和背景,需要进行softmax 操作**。loss 用二分类loss,正负样本均参与。
下面的分支用于计算anchor的回归量,获得精准的proposal。loss 用smoothL1 ,只有正样本参与计算。
最后的ProposalLayer 综合前景anchor和回归量获取Proposals。经过score 排序,边缘过滤,NMS等筛选出Proposals。
1、RPN 训练正负样本挑选:
满足以下条件的Anchor是正样本:
与Ground Truth Box的IOU(Intersection-Over-Union) 的重叠区域最大的Anchor;
与Gound Truth Box的IOU的重叠区域>0.7;
满足以下条件的Anchor是负样本:
与Gound Truth Box的IOU的重叠区域 <0.3;
既不属于正样本又不属于负样本的Anchor不参与训练。
问题:如果大多数anchor与GT 的IOU 俊不大于0.7会怎么样?这样的正样本是不是纯正的正样本?
2、RPN LOSS:
R是Smooth L1函数;
带超参的smoothL1:
Smooth L1完美地避开了 L1 和 L2 损失的缺陷,在 x 较小时,对 x 的梯度也会变小; 而在 x 很大时,对 x 的梯度的绝对值达到上限1,不会因预测值的梯度十分大导致训练不稳定。
表示只有在正样本时才回归Bounding Box
两个类别(前景和背景)的对数损失:
Anchors Generator
假设经过16倍下采样后,输出 feature map 大小为50 * 38 。
预设3 种 anchor scale(8, 16, 32), 预设 3种宽高比(0.5, 1, 2)两两组合为9种尺度anchor。更高效的anchor生成需要对数据集进行 N 聚类。
3、ProposalCreator: ProposalLayer
RPN 最后一层为proposal layer,用于前景anchors。
backbone feature 输出信息 :im_info=[M, N, scale_factor],
anchor对应的边框回归微调参数$[d_{x}(A),d_{y}(A),d_{w}(A),d_{h}(A)]
结合产生Proposal 的位置,此时的Proposal 位置坐标对应原图尺度坐标。
Proposal Layer forward
1、生成anchors:利用 [ d x ( A ) , d y ( A ) , d w ( A ) , d h ( A ) ] [d_{x}(A),d_{y}(A),d_{w}(A),d_{h}(A)] [dx(A),dy(A),dw(A),dh(A)]对所有的anchors做bbox regression回归(这里的anchors生成和训练时完全一致)
2、按照输入的foreground softmax scores由大到小排序anchors,提取前pre_nms_topN(e.g. 6000)个anchors,即提取修正位置后的foreground anchors
3、过滤:限定超出图像边界的foreground anchors为图像边界(防止后续roi pooling时proposal超出图像边界)- 剔除非常小(width<threshold or height<threshold)的foreground anchors。
4、NMS (nonmaximum suppression)
5、再次按照nms后的foreground softmax scores由大到小排序fg anchors,提取前post_nms_topN(e.g. 300)结果作为proposal = [x1, y1, x2, y2]输出。输出的proposal=[x1, y1, x2, y2],由在第三步中将anchors映射回原图判断是否超出边界,这里输出的proposal是对应MxN输入图像尺度的
POI pooling
由于proposal是对应原图 M x N 尺度的,所以首先使用spatial_scale参数将其映射回 (M/16) x (N/16) feature map尺度。
POI pooling 希望不同大小的proposal 经过pooling 后输出大小一致。实现长度固定输出。
1、获取 proposal 在feature map 上的 roi 区域。(存在精度损失)
2、切割roi 区域。 (存在精度损失)
此时切割并不是等间隔切割,例如:
57的特征图划分成22的时候不是等分的,行是5/2,第一行得到2,剩下的那一行是3,列是7/2,第一列得到3,剩下那一列是4
3、对切割区域进行maxpooling。
更加精准要使用ROI Align。
Roi pooling 经过两次取整,一次在将proposal映射到feature map 上取整;另一次为在feature map上作等大小块区域划分取整。
例如一个 665的bbox, backbone scale 为32,那么在feature map上为 665/32 = 20.78, 取整 = 20 ,误差 = 0.8 * 32 = 25。做回归时,如果对于大物体影响较小,如果对于小物体目标,影响就很大。
roi align
第一步:将proposal 映射到feature map 上时不取整,直接用浮点数进行下一步运算。
第二步:将映射到feature map 的浮点 region proposal 等块数划分,划分间距也是浮点数。
第三步:对每一划分块,按采样间隔平分,每一份取其中心点位置。采用双线性插值计算,得到中心采样点的像素坐标。
第四步:所有份中,取中心采样点中最大像素值作为该块的值。最后输出块个数固定。
例如 :scale = 32 , pooled_h = 7
665 / 32 = 20.78。feature map 大小
20.78 / 7 = 2.97。划分间距。总分为77 = 49 块。
将2.972.97 按 22 平分。取每一份中心。对每一个中心坐标双线性插值,得到像素值(在 feature map 上得到的是特征值)。
最后得到77块中每一块的特征值。组成该region proposal 的feature。
ROI 分类回归
将pooling 后输出(每一个roi 大小一致)作为输入,同样经过几层卷积后分两个分支:
一个用于分类,这里是多分类。
另一个用于更加精准的边框回归,在proposal 的基础上再次bounding box regression。
LOSS 计算
1、 RPN层的两个损失:1.是否为前景背景的二分类交叉熵损失 2. bbox的第一次修正损失。上面已经提到。
值得注意的是rpn_cls_score的shape=(1,h,w,18),也就是(1, h,w,92),作为真值监督信号。
因此在将anchor score predict 作loss 的时候需要将其reshape 成(N,9h, w, 2),对最后 2 的维度进行 softmax ,得到概率值,最后 reshape 回(N, H, W , 18)与 rpn_cls_score 作loss。整个过程没有改变数据的位置,reshape 的目的是为了在每一个predict 上作softmax。
2、最后分类回归的两个损失:1.num_classes分类损失 2.第二次bbox修正损失
二阶段网络训练样本生成
1、两次生成并挑选训练样本:
第一次生成并挑选:proposal 阶段,给anchor打标签,anchor 与 bbox 生成正负样本和回归量。主要用来do loss 用。为了正负样本平衡,不是所有的anchor都参与训练,IOU 处于过渡区域的anchor label = -1,作为忽略样本 。
proposalLayer : 需要用到 anchor, loc2bbox + clip + kick-minsize + NMS 生成Head阶段需要的ProposalROI
第二次生成并挑选:head阶段,给ProposalROI 打标签。ProposalROI 与 bbox 生成label和回归量。并将ProposalROI 与 任意 bbox 的IOU小于某阈值的类别置为0背景类。 为了正负样本平衡,不是所有的ProposalROI都参与训练,正样本和背景类都选一些,尽可能避免过渡样本(IOU处于中间)参与训练。
总结:
第一阶段,给anchor打标签,
第二阶段,给ProposalROI 打标签,
两个阶段,都必须考虑样本均衡。
所以不管是哪个阶段,生成训练样本总结起来就是两个问题:
1、什么是正样本,什么是负样本或背景样本(对于多分类)
2、如何选正样本,如何选负样本或背景样本,选哪些,选多少。
搞清楚以上两个问题,对整个二阶段训练极为关键,弄懂了也就理解了。
代码流程
forword 过程:
1、特征抽取
特征抽取主要将图像经过backbone 运算得到一个feature map:
features = self.faster_rcnn.extractor(imgs)
2、RPN 网络
初始化预先构建base_anchor。
(1)、RPN CONV
就是简单的将feature map 经过几层卷积得到 loc 分支和 score fen分支。有网络参数,作为一阶段的终结点。
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)
由于做RPN loss 时候需要将预测和gt 维度对齐。且score 的值需要经过softmax 概率化参能计算loss。因此需要如下的维度转换。
rpn_locs = rpn_locs.permute(0, 2, 3, 1).contiguous().view(n, -1, 4) #do rpn loss
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() #get positive score value
rpn_fg_scores = rpn_fg_scores.view(n, -1)
rpn_scores = rpn_scores.view(n, -1, 2) #do rpn loss
(2)、RPN Proposal
结合score 和 loc 经过过滤筛选(NMS等)输出 proposal ROIs 的网络。无网络参数。
RPN CONV 输出的 LOC 预测的是(dx, dy, dh, dw), proposal ROIs 需要还原回真实框。
if is_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
roi = loc2bbox(anchor, loc)
# Clip predicted boxes to image.
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])
# Remove predicted boxes with either height or width < threshold.
min_size = self.min_size * scale
#print "roi ize = ", roi.shape
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]
order = score.ravel().argsort()[::-1]
if n_pre_nms > 0:
order = order[:n_pre_nms]
roi = roi[order, :]
# Apply nms (e.g. threshold = 0.7).
# Take after_nms_topN (e.g. 300).
keep = non_maximum_suppression(
cp.ascontiguousarray(cp.asarray(roi)),
thresh=self.nms_thresh)
if n_post_nms > 0:
keep = keep[:n_post_nms]
#print "keep.shape1 = " ,keep.shape
roi = roi[keep]
score =score[keep]
return roi , score
(3)、RPN LOSS
给anchor 打cls标签,和计算预测真值。
由于RPN CONV 输出的 LOC 预测的是(dx, dy, dh, dw), 在做IOU 运算
gt_rpn_loc, gt_rpn_label = self.anchor_target_creator aytool.tonumpy(bbox),anchor,img_size)
rpn_loc_loss = _fast_rcnn_loc_loss(rpn_loc,gt_rpn_loc,gt_rpn_label.data,self.rpn_sigma)
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_label.cuda(), ignore_index=-1)
(4)、HEAD
ROI Pooling
HEAD conv + linear
pool = self.roi(x, indices_and_rois)
pool = pool.view(pool.size(0), -1)
fc7 = self.classifier(pool)
roi_cls_locs = self.cls_loc(fc7)
roi_scores = self.score(fc7)
return roi_cls_locs, roi_scores
(4)、HEAD LOSS
还是分类和回归。不过这回是多分类。
roi_loc_loss = _fast_rcnn_loc_loss(roi_loc.contiguous(),gt_roi_loc,gt_roi_label.data,self.roi_sigam)
roi_cls_loss = nn.CrossEntropyLoss()(roi_score, gt_roi_label.cuda())