Object-Detection(四)

Fast RCNN的问题

Fast RCNN存在的问题:
存在瓶颈:由于选择性搜索,找出所有的候选框,这个非常耗时,能否找出一个更加高效的方法来求出这些候选框?

解决:加入一个提取边缘的神经网络,也就说找到候选框的工作也交给神经网络来做了.做这样的任务的神经网络叫做Region Proposal Network(RPN).

具体做法:

  • 将RPN放在最后一个卷积层之后
  • RPN直接训练得到候选区域

pic1
RPN简介:

  • 在feature map上滑动窗口
  • 建一个神经网络用于物体分类+框位置的回归
  • 滑动窗口的位置提供了物体的大体位置信息
  • 框的回归提供了框更精确的位置
    pic2

一种网络,四个损失函数:

  • RPN classification(anchor good vs bad)
  • RPN regresssion(anchor -> Proposal)
  • Fast RCNN classification
  • Fast RCNN regression(proposal ->box)
    速度对比:
    pic3

Faster RCNN的主要贡献就是设计了提取候选区域的网络RPN,代替了费时的选择性搜索,使得检测速度大幅提高.

Faster RCNN

整体框架

pic1
我们先整体介绍上图各层的主要功能:

  1. Conv layers:用于提取特征图,Faster RCNN使用一组基础的conv+relu+pooling 层提取input image的feature maps,该feature会用于后续的RPN层和全连接层.(特征提取网络)
  2. RPN:主要用于生成region proposals,首先生成一堆Anchor box,对其进行裁剪过滤后通过softmax 判断anchor属于前景或者后景,即是物体or不是物体,所以这是一个二分类;同时,另一个分支bounding box regression修正anchor box,形成较精确的proposal(注:这里的较精确是相对于后面全连接层的再一次box regression而言)(生成ROI)
  3. ROI POOLing:该层利用RPN生成的proposal 和VGG16最后一层得到的feature map,得到固定大小的proposal feature map,进入到后面可利用全连接操作来进行目标识别和定位
  4. classifier:
    会将ROI Pooling层形成固定大小的feature map进行全连接操作,利用softmax进行具体类别的分类,同时,利用L1 Loss 完成bounding box regression回归操作获得物体的精确位置

其总体流程如下所示:

  • 首先对输入图片进行裁剪操作,并将裁剪后的图片送入预训练好的分类网络中获取图像对应的特征图
  • 首先在特征图上的 每一个点上取9个候选的ROI ,并根据相应的比例将其映射到原始图像中(因为特征提取网络一般有conv和pool组成,但是只有pool会改变特征图的大小,因此最终的特征图大小和pool的个数相关)
  • 接着讲这些候选的ROI输入到RPN网络中,RPN网络对这些ROI进行分类(即确定这些ROI是前景还是背景)同时对其进行初步回归(即计算这些前景ROI与真实目标之间的Boundingbox的偏差,包括$\Delta x,\Delta y,\Delta w,\Delta h$),然后做NMS
  • 接着对这些不同大小的ROI进行ROI Pooling(即映射为特定大小的feature map,文中是7×7),输出固定大小的feature map
  • 最后将其输入简单的检测网络中,然后利用1×1的卷积进行分类(区分不同的类别,N+1类,多余的一类是背景,用于删除不准确的ROI),同时进行Bounding Box回归(精确的调整预测的ROI和ground truth的ROI之间的偏差值),从而输出一个Bounding box集合

网络结构

pic4

  1. 图像预处理
    preprocess
  2. Conv layers
    Faster RCNN首先是支持输入任意大小的图片的,比如上图中输入的P*Q,进入网络之前对图片进行了规整化尺度的设定,如可设定图像短边不超过600,图像长边不超过1000,我们可以假定M×N=1000×600(如果图片少于该尺寸可以边缘补0,即图像会有黑色边缘)
  • 13个conv层:kernel_size=3,pad=1,stride=1;conv层不改变图片大小
  • 13个relu:激活函数不改变图片大小
  • 4个pooling层:kernel_size=2,stride=2;pooling层会让输出图片是输入图片的1/2
    经过上面的几层后,图片大小变成了(M/16)×(N/16);即60×40;feature map是60×40×512
  1. RPN(Region Proposal Networks):
    Feature map进入RPN后,先经过一次3×3的卷积,同样,特征图大小是60×40×512,这样的目的是为了进一步集中特征信息,接着两个全卷积:kernel_size=1×1,padding=0,stride=1;
    pic5
    如上图中标识:
  • rpn_cls:60×40×512 —-> 60×40×9×2:逐像素对9个Anchor box进行分类
  • rpn_bbox:60×40×512—->60×40×9×4逐像素得到其9个Anchor box 四个坐标信息(其实是偏移量)

在一些blog里会出现锚点这个概念,大致的意思是:原图经过Conv layers以后宽高都缩小为原图的1/16倍,这里特征图里的每一个点就表示原图上16×16的区域里的汇总,卷积层讲重要的信息一般存在channel里.换句话说,特征图里的每一个点表示原图的一个区域.

如下图
pic6
解释一下上面这张图的数字:

  • 经过conv_layer后生成的是512张特征图
  • 假设feature map中每个点上都有k个anchor(默认k=9),而每个anchor要分foreground和background,所以每个点由512feature 转化为cls=2k scores;而每个anchor 都有[x,y,w,h]对应4个偏移量,所以reg=4k coordinates
  • 全部anchor拿去训练太多了,训练程序会选取256个合适的anchor进行训练

仍有一些问题:

  1. RPN的input 特征图指的是哪个特征图?
  2. 为什么用sliding window?文中不是说用CNN么
  3. 256维特征向量如何获得

回答第1个问题:RPN的输入特征图就是Faster RCNN的Feature Map,也称共享feature map,主要用以RPN和Rol Pooling共享
回答第2个问题:我们可以把3×3的sliding window看作是对特征图做了一次3×3的卷积操作,zuihou得到了一个channle数目是256的特征图,尺寸和公共特征图相同,我们假设是256×(H×W)
回答第3个问题:我们可以近似的把这个特征图看作是H×W个向量,每个向量256维,那么图中的256维指的就是其中一个向量,然后我们要对每个特征向量做两次全连接操作,一个得到2个分数,一个得到4个坐标,由于我们要对每个向量做同样的全连接操作,等同于对整个特征图做两次1×1的卷积,得到一个2×H×W和一个4×H×W大小的特征图,换句话说,有H×W个结果,每个结果包含2个分数和4个坐标.
pic12

这里2个分数分别是前景(物体)的分数和背景的分数.4个坐标指针对原图坐标的偏移,首先一定要记住是原图.
其次我们知道,特征图有H×W个结果,我们随机取一点,他跟原图啃地瓜是有个一一映射关系的,由于原图和特征图大小不同,所以特征图上的一个点对应原图肯定是一个框,然而这个框很小,比如说8×8,这里8是指原图和特征图的比例,所以这个并不是我们想要的框,那我们不妨把框的左上角或者框的中心作为锚点,然后想象出一堆框,具体多少,聪明的读者肯定已经猜到,K个,这也就是图中所说的K anchor boxes(由锚点产生的K个框);换句话说,H×W个点,每个点对应原图有K个框,那么就有H×W×K个框默默的在原图上,那RPN的结果其实就是判断这些框是不是物体以及他们的偏倚;那么K个框到底有多大,长宽比是多少?这里是预先设定好的,共有9种组合,所以K等于9,最后我们的结果是针对这9种组合的,所以有H×W×9个结果,也就是18个分数和36个坐标.


(Anchors box 用于产生正样例与样例)
Anchors的生成规则:
前面提到经过Conv layers后,图片大小变为原来的1/16.令feat_stride=16,在生成anchor时,我们先定义一个base_anchor,大小为16×16的box(因为特征图(60×40)上的一个点,可以对应到原图1000×600上16×16的区域),源码中转换为[0,0,15,15]的数组,参数 ratio=[0.5,1,2] scale=[8,16,32]
先看[0,0,15,15],面积保持不变,长、宽比分别为[0.5,1,2]是产生的Anchors box
pic7
如果经过scale 变化,即长宽分别为:(16×8=128)、(16×16=256)、(16×32=512),对应anchor box如图所示
pic8
综上两种变换,最后生成9个Anchor box
pic9

特征图大小:60×40,所以会生成60×40×9=21600个Anchor box


pic11

RPN-data:
这一层主要是为特征图60×40上的每个像素生成9个Anchor box,并且对生成的Anchor box进行过滤和标记,参照源码,过滤和标记规则如下:

  1. 去除掉超过1000×600这原图的边界的anchor box
  2. 如果anchor box 与ground truth 的IOU值最大,标记为正样例,label=1
  3. 如果anchor box 与ground truth的IOU>0.7,标记为正样本,label=1
  4. 如果anchor box与ground truth的IOU<0.3,标记为负样本,label=0

剩下的既不是正样本也不是负样本,不用于最终训练,label=-1

除了对anchor box进行标记外,另一件事情就是计算anchor box与ground truth之间的偏移量
令:ground truth:标定的框也对应一个中心点位置坐标$x^* ,y^* 和宽高w^* ,h^* $
anchor box:中心点位置坐标$x_a,y_a和宽高w_a,h_a$
所以偏移量:

通过ground truth box与anchor box之间的差异来进行学习,从而是RPN网络中的权重能够学习到预测box的能力

RPN_loss,RPN_cls,RPN_bbox,RPN_cls_prob:其中RPN_loss_cls,RPN_loss_bbox分别是softmax,smooth L1计算损失函数,RPN_cls_prob计算概率值(可用于下一层的非最大值抑制操作)
RPN训练设置:在训练RPN时,一个Mini-batch是由一幅图像中任意选取的256个proposal组成的,其中正负样本的比例为1:1。如果正样本不足128,则多用一些负样本以满足有256个proposal可以用于训练,反之亦然

RPN训练

RPN网络训练,那么就涉及ground truth和loss function的问题.对于左支路,ground truth为anchor是否为目标,用0/1表示.那么怎么判定一个anchor内是否有目标呢?论文中采用了这样的规则:1)假如某anchor与任一目标区域的IOU最大,则该anchor判定为有目标;2)加入某anchor与任一目标区域的IOU>0.7,则判定为有目标;3)假如某anchor与任一目标区域的IOU<0.3,则判定为背景.所谓IOU,就是预测box和真实box的覆盖率,其值等于两个box的交集除以两个box的并集.其他的anchor不参与训练.于是有了上面的代价函数.
上面的$p_i$为anchor预测为目标的概率,$p_i^{*}$是ground truth标签.$t_i=\{t_x,t_y,t_w,t_h\}$是一个向量,表示预测的bounding box包围盒的4个参数化坐标;
$t_i^* $是与positive anchor对应的ground truth包围盒的坐标向量;

$L_{cls}(p_i,p_i^* )$是两个类别(目标 vs 非目标)的对数损失:

$L_{reg}(t_i,t_i^*)$是回归损失,用 $L_{reg}(t_i,t_i^*)=R(t_i-t_i^*)$ 来计算,R是smooth L1函数
$p_i^* L_{reg}$ 这一项意味着只有前景$anchor(p_i^* =1)$才有回归损失,其他情况没有($p_i^*=0$).cls层盒reg层的输出分别由${p_i}$和$u_i$组成,这两项分别由$N_{cls}$和$N_{reg}$以及一个平衡权重$\lambda$归一化(早期实现及公开的代码中,$\lambda$=10,cls项的归一化值为mini-batch的大小,即$N_{cls}=256$,reg项的归一化值为anchor位置的数量,即$N_{reg}~2400$(40×60),这样的cls和reg差不多是等权重的)

RPN之后

全连接层:
经过ROI Pooling层之后,proposal feature map的大小是7×7×512,对特征图进行全连接,参照下图,最后同样利用Softmax Loss和L1 Loss完成分类和定位
pic10
通过full connect层与softmax计算每个region proposal 具体属于哪个类别(如人,马,车等),输出cls_prob概率向量;同时再次利用bounding box regression获得每个region proposal的位置偏移量bbox_pred,用于回归获得更加精确的目标检测框,即从ROI Pooling获取到7×7大小的proposal feature maps后,通过全连接主要做了:
1)通过全连接和softmax对region proposals进行具体类别的分类
2)再次对region proposals进行bounding box regression,获取更高精度的rectangle box

总结

从Faster RCNN与Fast RCNN的比较可以看出,作者提出了RPN网络来代替原来的selective search,而这个RPN有单独的loss,用于粗略的训练,用于获得Region Proposal.然后再投入另一个网络中(这就类似于Fast RCNN),这部分算是精细的训练.所以说Faster RCNN是有两次训练,一次是粗训练,一次是精训练.

RPN的形式很简单:一个3×3的卷积层,然后分成两个分支分别是两个1×1的卷积;


总体框架:
model

代码

Faster RCNN的网络和前面几个版本的网络一样,网络结构并没有什么复杂的,重点是数据的处理方面

先放上网络的代码

网络结构

如论文中所述,先要将图片送入一个特征提取网络,一般是已经预训练过了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CNN(nn.Module):
def __init__(self):
super(CNN,self).__init__()
vggnet=models.vgg16(pretrained=True)
modules=list(vggnet.children())[:-1] ##去除fc layer
modules=list(modules[0])[:-1]##去除 最后一个pooling layer

self.vggnet=nn.Sequential(*modules)
for module in list(self.vggnet.children())[:10]:
for param in module.parameters():
param.requires_grad=False

def forward(self,images):
features=self.vggnet(images)
return features

然后是将上述得到的特征,送入RPN网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class RPN(nn.Module):
def __init__(self):
super(RPN,self).__init__()
##先经过一个3×3的卷积,且不变特征图的大小
self.conv==nn.Sequential(nn.Conv2d(512,512,kernel_size=3,stride=1,padding=(1,1)),nn.ReLU())
#再通过两个分支分别都是1×1的卷积
self.conv1=nn.Conv2d(512,2*9,kernel_size=1,stride=1)
self.conv2=nn.Conv2d(512,4*9,kernel_size=1,stride=1)
self.softmax=nn.Softmax()

def forward(self,features):
features=self.conv(features)
logits,rpn_bbox_pred=self.conv1(features),self.conv2(features)##logits是分类,rpn_bbox_pred是预测

height,width=features.size()[-2:]

logits=logits.squeeze(0).permute(1,2,0).contiguous() #(1,18,H/16,W/16)=>(H/16,W/16,18)
logits=logits.view(-1,2)#(H/16,W/16,18)=>(H/16*W/16*9,2)
###上述变化是为了好使用softmax
rpn_cls_prob=self.softmax(logits)
rpn_cls_prob=rpn_cls_prob.view(height,width,18)#(H/16,W/16,18)
rpn_cls_prob=rpn_cls_prob.permute(2,0,1).continguous().unsqueeze(0)#(H/16,W/16,18)=>(1,18,H/16,W/16)

return rpn_bbox_pred,rpn_cls_prob,logits

上面要进行第一次训练,训练得到的box才是我们需要的proposals,然后再将proposals送入后续网络

之后就是一些Fast RCNN里的网络,先通过ROI Pooling layer,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ROIpooling(nn.Module):
def __init__(self,size=(7,7),spatial_scale=1.0/16.0):
super(ROIpooling,self).__init__()
self.adapmax2d=nn.AdaptiveMaxPool2d(size)##一个自适应pooling
self.spatial_scale=spatial_scale
def forward(self,features,rois_boxes):##这里feature应该来自于第一个特征提取网络的输出
#rois_boxes : [x, y, x`, y`]
if type(rois_boxes)==np.ndarray:##因为在经过RPN之后还要选取一下proposals(nms),所以输入的可能是ndarray
rois_boxes=to_var(torch.from_numpy(rois_boxes))
rois_boxes=rois_boxes.data.float().clone()
rois_boxes.mul_(self.spatial_scale)
rois_boxes=rois_boxes.long()

output=[]

for i in range(rois_boxes.size(0)):
roi=rois_boxes[i]
try:

roi_feature=features[:,:,roi[1]:(roi[3]+1),roi[0]:(roi[2]+1)]
except Exception as e:
print(e,roi)

pool_feature=self.adapmax2d(roi_feature)###对那么些proposal 框内的特征做 adapmaxpool
output.append(pool_feature)
return torch.cat(output,0)

如果说RPN是粗调的话,下面这步就是精调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FasterRcnn(nn.Module):###本质上就是一些全连接层
def __init__(self):
super(FasterRcnn,self).__init__()
self.fc1=nn.Sequential(nn.Linear(512*7*7,4096),nn.ReLU(),nn.Dropout())

self.fc2=nn.Sequential(nn.Linear(4096,4096),nn.ReLU(),nn.Dropout())

self.classifier=nn.Linear(4096,21)
self.softmax=nn.Softmax()

self.regressor=nn.Linear(4096,21*4)

def forward(self,features):##这里的features是经过ROI pooling 的feature
features=features.view(-1,512*7*7)
##两个分支
features=self.fc1(features)
features=self.fc2(features)

try:
logits=self.classifier(features)
scores=self.softmax(logits)
bbox_delta=self.regressor(features)
except Exception as e:
print(e,logits)
return bbox_delta,scores,logits

loss

上面说过,整个faster RCNN有两次训练.一次是为了学习得到proposals 和前景还是背景的分类,一次是为了得到目标bbox和21个类的分类.

先讲RPN的loss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def rpn_loss(rpn_cls_prob,rpn_logits,rpn_bbox_pred,rpn_labels,rpn_bbox_targets,rpn_bbox_inside_weights):
'''
Arguments:
rpn_cls_prob(Tensor):(1,2*9,H/16,W/16)
rpn_logits(Tensor):(H/16*W/16,2) object or non-object rpn_logits
rpn_bbox_pred(Tensor):(1,4*9,H/16,W/16) predicted boxes
rpn_labels(Ndarray):(H/16*W/16*9,)
rpn_bbox_targets(Ndarray):(H/16*W/16*9,4)
rpn_bbox_inside_weights(Ndarray):(H/16*W/16*9,4) masking for only positve box loss
Return:
cls_loss(scalar):classfication loss
reg_loss*10(scalar):regression loss
log(tuple): for logging

'''

height,width=rpn_cls_prob.size()[-2:]#(H/16,W/16)

rpn_cls_prob=rpn_cls_prob.squeeze(0).permute(1,2,0).continguous()#(1,18,H/16,W/16)=>(H/16,W/16,18)
rpn_cls_prob=rpn_cls_prob.view(-1,2)##(H/16,W/16,18)=>(H/16*W/16*9,2)

rpn_labels=to_tensor(rpn_labels).long()

##index where not -1 下面这一步应该是为了剔出负数得下标,这里得ge(0)是可以选取大于等于0得元素
idx=rpn_labels.ge(0).nonzero()[:,0] ##torch.ge(0) 将各个元素与0比较,.nonzero()返回的是非零值得坐标[:,0]表示非零值得行号
rpn_cls_prob=rpn_cls_prob.index_select(0,to_var(idx)) ##0代表维度0,即行,idx是所要筛选的索引序号
rpn_labels=rpn_labels.index_select(0,idx)
rpn_logits=rpn_logits.squeeze().index_select(0,to_var(idx))

po_cnt=torch.sum(rpn_labels.eq(1))##RPN_label为1的求和, target_label中正例个数
ne_cnt=torch.sum(rpn_labels.eq(0))##RPN_label为0的求和, target_label中负例个数

maxv,predict=rpn_cls_prob.data.max(1)

po_idx=predict.eq(1).nonzero()
ne_idx=predict.eq(0).nonzero()

po_idx=po_idx.view(-1) if po_idx.dim()>0 else None
ne_idx=ne_idx.view(-1) if ne_idx.dim()>0 else None

##正例分对的数目tp,负例分对的数目tn
try:
tp=torch.sum(predict.index_select(0,po_idx).eq(rpn_labels.index_select(0,po_idx))) if po_cnt>0 and po_idx is not None else 0
tn=torch.sum(predict.index_select(0,ne_idx).eq(rpn_labels.index_select(0,ne_idx))) if ne_cnt>0 and ne_idx is not None else 0
except Exception as e:
print(e)
tp=0
tn=0


rpn_labels=to_var(rpn_labels)

cls_crit=nn.CrossEntropyLoss()
cls_loss=cls_crit(rpn_logits,rpn_labels)###对预测的标签和目标标签做交叉熵



##rpn_bbox_targets与rpn_bbox_inside_weights的形式要相同因为要按位×
rpn_bbox_targets=torch.from_numpy(rpn_bbox_targets)
rpn_bbox_targets=rpn_bbox_targets.view(height,width,36)##(H/16,W/16,36)
rpn_bbox_targets=rpn_bbox_targets.permute(2,0,1).contiguous().unsqueeze(0)
rpn_bbox_targets=to_var(rpn_bbox_targets)

rpn_bbox_inside_weights=torch.from_numpy(rpn_bbox_inside_weights)
rpn_bbox_inside_weights=rpn_bbox_inside_weights.view(height,width,36)
rpn_bbox_inside_weight=rpn_bbox_inside_weights.permute(2,0,1).contiguous().unsqueeze(0)

rpn_bbox_inside_weights=rpn_bbox_inside_weights.cuda() if torch.cuda.is_available() else rpn_bbox_inside_weights


rpn_bbox_pred=to_var(torch.mul(rpn_bbox_pred.data,rpn_bbox_inside_weights))
rpn_bbox_targets=to_var(torch.mul(rpn_bbox_targets.data,rpn_bbox_inside_weights))

reg_loss=F.smooth_l1_loss(rpn_bbox_pred,rpn_bbox_targets,size_average=True)

if po_cnt==0:
po_cnt=0.001
if ne_cnt==0:
ne_cnt=0.001

log=(po_cnt,ne_cnt,tp,tn)

return cls_loss,reg_loss*10,log

上述代码的rpn_bbox_inside_weights是什么?
pic13
用原文的话来说就是只有前景才能提供给regression loss,如果不是前景则不能提供regression loss.因此这里的rpn_bbox_inside_weights是与target_bbox同时产生的用以标记是不是前景,如果是前景,它的4个值都为1,如果不是则都为0.
对应代码:
bbox_inside_weights=np.zeros((len(inds_inside),4),dtype=np.float32)
bbox_inside_weights[labels==1,:]=np.array(cfg.TRAIN.RPN_BBOX_INSIDE_WEIGHTS)
即先将inside_weights 统一初始化为0,再将labels=1的正样本对应的值置为配置文件中的TRAIN.RPN BBOX INSIDE WEIGHTS
对应论文中的公式:

其中bbox_inside_weights就是与$p_i^* $一致的.

有些代码中还包含了bbox_outside_weights,这个应该是上式中的$N_{reg}$也可以说是前景anchor的数量

下面是faster RCNN的loss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def frcnn_loss(frcnn_cls_prob,frcnn_logits,frcnn_bbox_pred,frcnn_labels,frcnn_bbox_targets,frcnn_bbox_inside_weights):
'''
Arguments:
frcnn_cls_prob(Tensor):(256,21) 21类概率
frcnn_logits(Tensor):(256,21) 21 类logits
frcnn_bbox_pred(Tensor):(256,84) predicted boxes for 21 class
frcnn_labels(Ndarray):(256,)
frcnn_bbox_targets(Ndarray):(256,84)
frcnn_bbox_inside_weights(Ndarray):(256,84) masking for only foreground box loss
Return :
cls_loss(scalar):classification loss
reg_loss*10(scalar):regression loss
log(tuple):for logging
'''

frcnn_labels=to_tensor(frcnn_labels).long()
fg_cnt=torch.sum(frcnn_labels.ne(0))##前景数目
bg_cnt=frcnn_labels.numel()-fg_cnt##后景数目
##torch.gt 大于; torch.lt 小于 torch.eq等于 torch.nonzero 非零 torch.ne 非
try:
maxv,predict=frcnn_cls_prob.data.max(1)
tp=torch.sum(predict[:fg_cnt].eq(frcnn_labels[:fg_cnt])) if fg_cnt>0 else 0 ##true positive
tn=torch.sum(predict[fg_cnt:].eq(frcnn_labels[fg_cnt:])) if bg_cnt>0 else 0 ##true negative
except Exception as e:
print(e)
print(fg_cnt,frcnn_labels)
tp=0
tn=0

frcnn_labels=to_var(frcn_labels)

ce_weights=torch.ones(frcnn_cls_prob.size()[1])#21个1
ce_weights[0]=float(fg_cnt)/bg_cnt if bg_cnt!=0 else 1 #前景和背景的比例
if torch.cuda.is_available():
ce_weights=ce_weights.cuda()

cls_crit=nn.CrossEntropyLoss(weight=ce_weights)#带权重的损失

cls_loss=cls_crit(frcnn_logits,frcnn_labels)
frcnn_bbox_inside_weights=to_tensor(frcnn_bbox_inside_weights)
frcnn_bbox_targets=to_tensor(frcnn_bbox_targets)

frcnn_bbox_pred=to_var(torch.mul(frcnn_box_pred.data,frcnn_bbox_inside_weights))
##这里的bbox_inside_weight 与前面的一样,因为regression只回归前景.
frcnn_bbox_targets=to_var(torch.mul(frcnn_bbox_targets,frcnn_bbox_inside_weights))


reg_loss=F.smooth_l1_loss(frcnn_bbox_pred,frcnn_bbox_targets,size_average=True)


if fg_cnt==0:
fg_cnt=0.001

if bg_cnt==0:
bg_cnt=0.001


log=(fg_cnt,bg_cnt,tp,tn)

return cls_loss,reg_loss,log

box transform

之前提到过在图片经过特征提取网络后,我们会得到一张特征图,它的宽和高分别为W/16和H/16,我们希望H/16×W/16这些个点能表示一定区域的方框.这就需要从特征图映射回去,这也是需要我们写代码的

下面是一个正向过程,就是已知一个框bbox如何转换成,feature map里存储的信息,feature map里存储的信息是相对信息,一个相对的中心点的偏差值,以及相对的宽高值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def bbox_transform(boxes,gt_boxes):
'''
boxes:(x1,y1,x2,y2)
'''
ex_widths=boxes[:,2]-boxes[:,0]+1.0
ex_heights=boxes[:,3]-boxes[:,1]+1.0
ex_ctr_x=boxes[:,0]+0.5*ex_widths ##中心点的x
ex_ctr_y=boxes[:.1]+0.5*ex_heights##中心点的y
# x : predicted box, x_a : anchor box, x* : ground truth box

# faster R-Cnn paper
# t_x = (x - x_a)/w_a
# t_y = (y - y_a)/h_a
# t_w = log(w/w_a)
# t_h = log(h/h_a)

# t_x* = (x* - x_a)/w_a
# t_y* = (y* - y_a)/h_a
# t_w* = log(w*/w_a)
# t_h* = log(h*/h_a)
targets_dx = (gt_ctr_x - ex_ctr_x) / ex_widths
targets_dy = (gt_ctr_y - ex_ctr_y) / ex_heights
targets_dw = np.log(gt_widths / ex_widths)
targets_dh = np.log(gt_heights / ex_heights)

targets = np.vstack(
(targets_dx, targets_dy, targets_dw, targets_dh)).transpose()
return targets

另外一个函数是上述过程的逆过程,如何从feature map里的特征信息,转换为框信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def bbox_transform_inv(boxes,deltas):
if boxes.shape[0]==0:
return np.zeros((0,deltas.shape[1]),dtype=deltas.dtype)
##这里boxes应该是anchor box
boxes=boxes.astype(deltas.dtype,copy=False)

widths=boxes[:,2]-boxes[:,0]+1.0
heights=box[:,3]-boxes[:,1]+1.0
ctr_x=boxes[:,0]+0.5*widths
ctr_y=boxes[:,1]+0.5*heights
##设置切片 slice(start,stop,step)
col_0=(slice(0,None,4))
col_1=(slice(1,None,4))
col_2=(slice(2,None,4))
col_3=(slice(3,None,4))

##deltas里存储的是相对值
dx=deltas[:,col_0]
dy=deltas[:,col_1]
dw=deltas[:,col_2]
dh=deltas[:,col_3]

# x : predicted box, x_a : anchor box, x* : ground truth box

# faster R-Cnn paper
# t_x = (x - x_a)/w_a
# t_y = (y - y_a)/h_a
# t_w = log(w/w_a)
# t_h = log(h/h_a)

# t_x* = (x* - x_a)/w_a
# t_y* = (y* - y_a)/h_a
# t_w* = log(w*/w_a)
# t_h* = log(h*/h_a)



# (H/16 * W/16, 1 ) * (H/16 * W/16, 1) + (H/16 * W/16, 1) = > (H/16 * W/16, 1)
##求中心点的值和宽高
pred_ctr_x=dx*widths[:,np.newaxis]+ctr_x[:,np.newaxis]
pred_ctr_y=dy*heights[:,np.newaxis]+ctr_y[:,np.newaxis]
pred_w=np.exp(dw)*widths[:,np.newaxis]
pred_h=np.exp(dh)*heights[:,np.newaxis]


pred_boxes=np.zeros(deltas.shape,dtype=deltas.dtype)

pred_boxes[:,col_0]=pred_ctr_x-0.5*pred_w
pred_boxes[:,col_1]=pred_ctr_y-0.5*pred_h
pred_boxes[:,col_2]=pred_ctr_x+0.5*pred_w
pred_boxes[:,col_3]=pred_ctr_y+0.5*pred_h
#pred_boxes:(x1,y1,x2,y2)
return pred_boxes

从上述代码可以帮助我们更好的理解锚点(anchor)的这个概念,即他就是原图中的一个点或是标记点,且两个锚点之间有一定的间隔,由于是二维平面所以从这个锚点沿着x正方向和y正方到下一个锚点有一定距离,这是一个框,我们要预测的就是落在这个框里的相对位置,这是目标框的中心点,根据这个中心点可以产生9个bbox.(可以结合YOLOv2继续理解这里的思想)

其他

还有一些其他操作,比如clip_boxes:为了防止boxes超出image的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def clip_boxes(boxes,im_shape):
'''
clip boxes to image boundaries
im_shape:(H,W)
'''
#x1>=0
boxes[:,0::4]=np.maximum(np.minimum(boxes[:,0::4],im_shape[1]-1),0)
#y1>=0
boxes[:,1::4]=np.maximum(np.minimum(boxes[:,1::4],im_shape[0]-1),0)
#x2<im_shape[1](w)
boxes[:,2::4]=np.maximum(np.minimum(boxes[:,2::4],im_shape[1]-1),0)
#y2<im_shape[0](H)
boxes[:,3::4]=np.maximum(np.minimum(boxes[:,3::4],im_shape[0]-1),0)

return boxes

filter_boxes是为了过滤掉一些宽高不够大的框

1
2
3
4
5
def filter_boxes(boxes,min_size):
ws=boxes[:,2]-boxes[:,0]+1
hs=boxes[:,3]-boxes[:,1]+1
keep=np.where((ws>=min_size)&(hs>=min_size))[0]
return keep

py_cpu_nms是为了进行NMS,去除一些冗余框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def py_cpu_nms(proposals_boxes_c,thresh):
scores=proposals_boxes_c[:,-1]
x1=proposals_boxes_c[:,0]
y1=proposals_boxes_c[:,1]
x2=proposals_boxes_c[:,2]
y2=proposals_boxes_c[:,3]

areas=(x2-x1+1)*(y2-y1+1)
order=scores.argsort()[::-1]
keep=[]
while order.size >0:
i=order[0]
keep.append(i)
xx1=np.maximum(x1[i],y1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])


w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)

inds=np.where(ovr<=thresh)[0]
order=order[inds+1]

return keep

ProposalLayer

虽然写在其他的后面但这也是很重要的一个模块,之所以写在其他后面,因为这里会用到其他里的一些函数.ProposalLayer是放在RPN之后,ROIpooling之前的.前面说过,RPN相当于RCNN和FastRCNN里的selective search操作,而在这里经过RPN之后我们会得到一些列proposal仍是需要经过精挑细选。所以我们需要通过ProposalLayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class ProposalLayer:
def __init__(self,args):
self.args=args

def _get_pos_score(self,rpn_cls_prob):###得到各个位置的分数

pos_scores=rpn_cls_prob[:,:9]#(1,9,H/16,W/16)
pos_scores=pos_scores.squeeze(0).permute(1,2,0).contiguous()#(H/16,W/16,9)
pos_scores=pos_scores.view(-1,1)#(H/16*W/16*9,1)

return pos_scores

def _get_bbox_deltas(self,rpn_bbox_pred):###获得各个相对值
bbox_deltas=rpn_bbox_pred.squeeze(0).permute(1,2,0).contiguous()
bbox_deltas=bbox_deltas.view(-1,4)
return bbox_deltas

def proposal(self,rpn_bbox_pred,rpn_cls_prob,all_anchors_boxes,im_info,test,args):
'''
Arguments:
rpn_bbox_pred(Tensor):(1,4*9,H/16,W/16)
rpn_cls_prob(Tensor):(1,2*9,H/16,W/16)
all_anchors_boxes(Ndarray):(H/16*W/16*9,4) predicted boxes
im_info(tuple):(Height,width,Channel,Scale)
test(Bool)
args(argparse.Namespace):global arguments
Return:
in each minibatch number of proposal boxes is variable
proposals_boxes(Ndarray):(# proposal boxes,4)
scores(Ndarray):(#proposal boxes,)

'''

#if test==False,using training args else using testing args
pre_nms_topn=args.pre_nms_topn if test==False else args.test_pre_nms_topn
nms_thresh=args.nms_thresh if test==False else args.test.nms_thresh
post_nms_topn=args.post_nms_topn if test==False else args.test_post_nms_topn


bbox_deltas=self._get_bbox_deltas(rpn_bbox_pred).data.cou().numpy()

#1.Convert anchors into proposal via bbox transformation
proposals_boxes=bbox_transform_inv(all_anchors_boxes,bbox_deltas)
pos_score=self._get_pos_score(rpn_cls_prob).data.cpu().numpy()

height,width=im_info[0:2]

if args.include_inside_anchor==False and test==False:
_allowed_border=0
inds_inside=np.where(
(all_anchors_boxes[:,0]>=-_allowed_border)&
(all_anchors_boxes[:,1]>=-_allowed_border)&
(all_anchors_boxes[:,2]<width+_allowed_border)&
(all_anchors_boxes[:,3]<height+_allowed_border))[0]

mask=np.zeros(proposals_boxes.shape[0],dtype=bool)
mask[inds_inside]=True

proposals_boxes=proposals_boxes[mask]
pos_score=pos_score[mask]

#2.clip proposal boxes to image
proposals_boxes=clip_boxes(proposals_boxes,im_info[0:2])
# 3.remove predicted boxes with either height or width< threshold
#Note:convert min_size to input image scale stored in im_info[3]

filter_indices=filter_boxes(proposals_boxes,self.args.min_size*max(im_info[3]))
#delete filter_indices
mask=np.zeros(proposals_boxes.shape[0],dtype=bool)
mask[filter_indices]=True

proposals_boxes=proposals_boxes[mask]
pos_score=pos_score[mask]

#4 sort all (proposal,score) pairs by score from highest to lowest
indices=np.argsort(pos_score.squeeze())[::-1]#descent order
# 5 take topn score proposal
topn_indices=indices[:pre_nms_topn]
# 6.apply nms
proposals_boxes_c=np.hstack((proposals_boxes[topn_indices],pos_score[topn_indices]))
keep=py_cpu_nms(proposals_boxes_c,nms_thresh)

#7. take after nms_topn

if post_nms_topn>0:
keep=keep[:post_nms_topn]

#8.return the top proposals(->RoIs top)
proposals_boxes=proposals_boxes_c[keep,:-1]
scores=proposals_boxes_c[keep,-1]
return proposals_boxes,scores

参考文献

  1. 一个较好的blog:http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/#ITEM-1455-2
-------------本文结束感谢您的阅读-------------

本文标题:Object-Detection(四)

文章作者:Yif Du

发布时间:2019年02月18日 - 10:02

最后更新:2019年02月26日 - 14:02

原始链接:http://yifdu.github.io/2019/02/18/Object-Detection(四)/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。