还是要学习一些基本的目标检测方法。
1.非极大值抑制(NMS)
基于锚的目标检测方法,通常是通过某种方式产生锚框(比如选择性搜索等,或者用一种更简单的方式:
一种简单方式:我们预先设置一些缩放比(scale, s i ∈ ( 0 , 1 ] , m 个 s_i\in(0,1],m个 si∈(0,1],m个)和一些宽高比(aspect ratio, r i > 0 , n 个 r_i>0,n个 ri>0,n个).为了避免过高的计算复杂度,我们只考虑同一缩放比下所有不同宽高比组合,和同一宽高比下所有不同缩放比组合,例如:
( s 1 , r i ) i = 1 n 和 ( s i , r 1 ) i = 2 m (s_1,r_i)_{i=1}^n和(s_i,r_1)_{i=2}^{m} (s1,ri)i=1n和(si,r1)i=2m
因此共有 h w ( m + n − 1 ) hw(m+n-1) hw(m+n−1)个anchor,其中 h , w h,w h,w是图像的高宽。
),之后再为这些锚框产生预测的类别和偏移。
但是,许多锚框可能产生重叠,而且代表的是同一个object。这时我们为了去除这种重复,就引入了非极大值抑制(Non-maximum suppression)的方法。
NMS的操作步骤如下:
- 所有预测的非背景边界框按置信度降序排序,生成列表 L L L.
- 从 L L L中选取置信度最高的锚框 B 1 B_1 B1,然后将所有与 B 1 B_1 B1的IoU大于某个阈值 θ \theta θ的锚框全部移除,也就是说,那些具有非极大值置信度的边界框被抑制了。
- 选取下一个置信度第二高的锚框 B 2 B_2 B2,重复上述步骤,直至所有的锚框都被选取过,这说明没有任何一对锚框的相似度大于了阈值。
代码实现:(参考《动手学深度学习》)
```python
import torch
#定义计算IoU函数
def box_iou(boxes1,boxes2):
'''
input:
boxes1,boxes2:输入的锚框,每个boxes的shape(box_number,4),其中
4代表:xmin,ymin,xmax,ymax
output:boxes1和boxes2中两两框的iou torch.Tensor
注意:两两框的意思是假设boxes1有m个,boxes2有n个,则计算结果应是mn个.
'''
#计算每个box的面积
box_area = lambda boxes : ((boxes[:,2] - boxes[:,0]) *
(boxes[:,3] - boxes[:,1])) #box_area:(box_number,)
area1 , area2 = box_area(boxes1), box_area(boxes2)
#计算对应锚框的交集部分
inter_upleft = torch.max(boxes1[:,None,:2],boxes2[:,:2]) #交集部分左上角坐标,就是比较两个框的(x_min,y_min)的较大者
#特别注意:None的用法是扩充维度,也就是对每一个boxes1中的框,将所有的boxes2进行比较,下同
inter_downright = torch.min(boxes1[:,None,2:],boxes2[:,2:]) #同理 比较(x_max,y_max)的较小者
inters = (inter_downright - inter_upleft).clamp(min=0) #clamp是筛选大于0的值,小于0的输出0
inter_area = inters[:,:,0] * inters[:,:,1] #交集部分面积 shape(box_number1,box_number2) 下同
union_area = area1[:,None] + area2 - inter_area #并集面积 None扩充维度,为了维度一直相除
return inter_area / union_area
#定义NMS
def NMS(boxes,scores,threshold):
'''
input:
boxes:anchor boxes,shape(number,4)
scores:置信度 (numbers,)
threshold:阈值 float
output:筛选出的anchor box的index
'''
#先按置信度大小对所有锚框排序 降序 返回索引值
B = torch.argsort(scores,dim=-1,descending=True)
keep = [] #要保留的box的index
while B.numel() > 0: #还没筛完
i = B[0] #选定当前置信度最高的
keep.append(i) #加入
if B.numel() == 1:
break #只剩一个就完毕
iou = box_iou(boxes[i,:].reshape(-1,4),
boxes[B[1:],:].reshape(-1,4)).reshape(-1) #计算选定框和剩余框的iou,reshape(-1)相当于flatten
indexs = torch.nonzero(iou <= threshold).reshape(-1) #iou <= threshold是bool值,
# 返回的是为True的值得索引,也就是满足小于阈值的索引
B = B[indexs + 1] # 相当于是B = B[indexs],是筛选,然后将索引都加1,表示移动,删除掉已经计算完的基准框:B = B[indexs + 1]
return torch.tensor(keep)
2.R-CNN,Fast RCNN 和Faster RCNN
2.1 RCNN
RCNN的设计结合了选择性搜索和SVM,其大致分为如下几个步骤:
- 对输入图像使用选择性搜索,大概提出2k个左右的建议区域。
选择性搜索的大概思路:
1.生成区域集 R R R
2.计算区域集里每个相邻区域的相似度 S = { s 1 , . . . } S=\{s_1,...\} S={s1,...}
3.找出相似度最高的两个区域,将其合并为新集,添加进 R R R
4.删除 S S S中与新集有关的子集
5.重复2~4 直到 S S S为空.
有点NMS的思想有木有
- 经过一个pre-trained的CNN,提取出每个框的特征
- 从头开始训练一个SVM分类器,对提取出特征进行分类
- 使用线性回归,对边框偏移进行预测
2.2 Fast RCNN
由于RCNN的效率太低了,因为对每个候选框都要计算特征,很冗余.因此Fast RCNN只对整张图进行特征提取,虽然仍用选择性搜索来选择候选框,但是这些框是被投影到feature map上,进行后续的计算.
大体步骤如下:
- 选择性搜索,得到大约2k个候选框
- 用CNN提取图片特征,假设输出为 1 × c × h 1 × w 1 1\times c \times h_1\times w_1 1×c×h1×w1
- 如前所述,将候选框按比例缩放到feature map上.例如feature map的大小是原图的0.1倍,则候选框的size响应缩小到0.1倍. 但是,候选框的大小各异,为了统一输出,方便后续计算,要加入ROI Pooling. ROI Pooling将候选区域在feature map上的投影进行池化, 输出相同的尺寸 h 2 × w 2 h_2\times w_2 h2×w2 .假设候选区域有 n n n个,则输出变为 n × c × h 2 × w 2 n\times c \times h_2\times w_2 n×c×h2×w2.
- 通过全连接层将输出形状变换为 n × d n \times d n×d,其中超参数 d d d 取决于模型设计
- 分别预测每个候选框的类别和size. 分别经过两个支路,一个输出 n × q n \times q n×q, q q q是类别数,另一个输出 n × 4 n \times 4 n×4,4分别指bbox 的位置.
至于ROI Pooling 具体如何做到将不同输入化为同样输出的,日后要再学习.
2.3 Faster RCNN
选择性搜索得到的候选框还是太多.因此Faster RCNN干脆利用区域建议网络(RPN)来代替选择性搜索,来减少锚框数量.当然RPN进行额外的训练.
区域建议网络的卷积层是全卷积层,也就是用卷积层替代了普通卷积网络中的FC层.RPN的大致步骤:
- 输入的是feature map,FCN采用3x3的kernel,padding为1, 假设RPN的卷积层输出通道为 c c c,则feature map中每个单元都得到了长度为 c c c的特征.
- 以特征图的每个像素为中心,生成多个不同大小和宽高比的锚框并标注它们
- 使用锚框中心单元长度为 c c c的特征,分别预测该锚框的二元类别(目标还是背景)和边界框
- 使用NMS,从预测类别为目标的预测边界框中移除相似的结果
其余部分和Fast RCNN相同.
3. SSD(单发多框检测)
SSD的基本思想是one-stage,不再使用RPN来生成边界框. 它采用类似于金字塔结构, 提取不同尺度的feature map,在每个feature map上利用一开始讲的那个产生锚的简单方式在每个像素上生成一些锚框, 对每个锚框进行预测, 最终用NMS和置信度筛选来确定最后的候选框.
自然,大的feature map可以检测小目标,小的feature map具有更大的感受野, 可检测大目标.
基础网络块一般采用截断的VGG,或者ResNet.
4.Yolo系列
4.1 Yolo v1
1.整体思想
与之前的方法不同,yolo v1是anchor-free的.它将一幅图像均匀地分成
S
×
S
S\times S
S×S块,如果GT的目标的中心出现在某个块中,那么这个块就负责这个目标的检测.
将一幅图像均匀分块,作者认为这样可以更好地掌握目标的泛化表达,而且由于将整张图片作为输入,可以学习到类别和外观的上下文信息.
在 S × S S\times S S×S个块中,每个块预测 B B B个bbox和置信度,一个bbox用四个量 x , y , w , h x,y,w,h x,y,w,h表示,注意 x , y x,y x,y是相对于块大小进行归一化,而 w , h w,h w,h相对于整幅图像的大小进行归一化.
置信度反映的就是对于这个box内含有一个目标 (但并不分类,不区分目标) 的确信程度,记为 P r ( O b j e c t ) ∗ I O U p r e d t r u t h Pr(Object)*IOU_{pred}^{truth} Pr(Object)∗IOUpredtruth,其中 P r ( O b j e c t ) ∈ { 0 , 1 } Pr(Object)\in\{0,1\} Pr(Object)∈{0,1},含有目标时为1,不含目标时为0. I O U p r e d t r u t h ∈ [ 0 , 1 ] IOU_{pred}^{truth}\in[0,1] IOUpredtruth∈[0,1]表示预测框和GT的交并比.
此外,每个块还预测 C C C个类别的概率,即 P r ( C l a s s i ∣ O b j e c t ) , i = 1 , 2 , . . . , C Pr(Class_i|Object),i=1,2,...,C Pr(Classi∣Object),i=1,2,...,C(存在目标的情况下属于类别 C i C_i Ci的概率).
因此,每个块中存在
C
i
C_i
Ci类别目标的概率定义为:
P
r
(
C
l
a
s
s
i
∣
O
b
j
e
c
t
)
P
r
(
O
b
j
e
c
t
)
∗
I
O
U
p
r
e
d
t
r
u
t
h
=
P
r
(
C
l
a
s
s
i
)
∗
I
O
U
p
r
e
d
t
r
u
t
h
Pr(Class_i|Object)Pr(Object)*IOU_{pred}^{truth}=Pr(Class_i)*IOU_{pred}^{truth}
Pr(Classi∣Object)Pr(Object)∗IOUpredtruth=Pr(Classi)∗IOUpredtruth
最终预测结果的map的shape为
(
S
,
S
,
(
5
B
+
C
)
)
(S,S,(5B+C))
(S,S,(5B+C)),如下图所示:
2.网络结构
网络结构借鉴了GoogLeNet,为ImageNet设计,输入为
448
×
448
448\times448
448×448.
注意最后一层线性层将4096的向量映射成
7
×
7
×
30
7\times7\times30
7×7×30的tensor,即将图片分成49块,每块预测2个bbox和20个类别.
作者采用前20个卷积层(再加平均池化层和全连接层)在ImageNet上进行预训练,预训练时输入为 224 , 224 224,224 224,224,检测时再将长宽加倍,并加入额外的卷积层和线性层.
3.损失函数
对负责的再解释: 我们期望的是只有一个bbox能对预测的目标负责. 如何规定哪个bbox对目标负责呢? 与GT IoU最大的就负责.因此在计算loss时,我们只考虑对目标负责的bbox的计算结果与GT的差异.
整体的loss采用平方损失的形式:
其中: I i o b j \mathbb I_i^{obj} Iiobj表示第 i i i个块有目标的指示函数, I i , j o b j \mathbb I_{i,j}^{obj} Ii,jobj表示第 i i i个块由第 j j j个bbox负责的指示函数, λ n o o b j = 0.5 \lambda_{noobj}=0.5 λnoobj=0.5是为了减少对背景类预测的关注.
1.为什么长宽误差的计算要采用根号的形式
因为人在视觉上,假设对于同样的bbox的offset,对于小目标的误差就比较明显,对于大目标的误差就不那么明显.为了贴近这一点,不宜使用正比例函数,也就是说对小目标检测在同样偏移下赋予更大的误差有更好的效果,因此需要一个二阶导数小于0的函数.
y = x 1 / 2 y=x^{1/2} y=x1/2对于同样的变化量,大自变量对应的因变量小.2.一个疑问
求和号的上下标是不是有问题?应是 S 2 − 1 和 B − 1 S^2-1和B-1 S2−1和B−1
4.2 Yolo v2
框图来源于B站up主"霹雳吧啦Wz"
相对于Yolo v1,Yolo v2主要有几个方面改进:
1.Batch Norm
加入了BN,由于BN可以起到正则化的作用,因此移除了Dropout.
2.High Resolution Classifier
之前Yolo v1是在一半分辨率上做的分类预训练, 这次直接在 448 × 448 448 \times 448 448×448的分辨率下在ImageNet上进行预训练.
3.Conv with anchors
作者认为每一块预测中心坐标和长宽这种方式效果比较差,还是利用anchor预测偏移比较好,而且也更容易学.
4.Dimension clusters
那么怎么获得anchors呢? 与之前RCNN系列不同的是(它们有的是手工预设的,有的是学习的), Yolo v2采用k-means聚类的方式, 就是在GT的bbox上进行聚类来获得先验知识.
传统的k-means聚类方式采用欧氏距离来度量距离, 作者认为这样在大box上导致更大的误差.既然我们关注的就是IoU,那不妨用IoU来定义距离:
d
(
b
o
x
,
c
e
n
t
r
o
i
d
)
=
1
−
I
o
U
(
b
o
x
,
c
e
n
t
r
o
i
d
)
d(box,centroid)=1-IoU(box,centroid)
d(box,centroid)=1−IoU(box,centroid)
实验证明,聚的类越多越好, k = 5 k=5 k=5时是准确率和复杂度的较好权衡:
5.Direct location prediction
直接应用anchor会造成模型的不稳定,这种不稳定来源于对bbox的中心点的预测. 原因在于, 基于anchor的预测得到的中心坐标按下式计算:
x
=
(
t
x
∗
w
a
)
+
x
a
y
=
(
t
y
∗
h
a
)
+
y
a
x=(t_x*w_a)+x_a\\ y=(t_y*h_a)+y_a
x=(tx∗wa)+xay=(ty∗ha)+ya
其中
(
t
x
,
t
y
)
(t_x,t_y)
(tx,ty)是预测的偏移值,
(
w
a
,
h
a
)
(w_a,h_a)
(wa,ha)是anchor的宽和高,
(
x
a
,
y
a
)
(x_a,y_a)
(xa,ya)是anchor的中心.
这个式子是没有取值范围限制的,具体来说是 ( t x , t y ) (t_x,t_y) (tx,ty)没有限制,这就会让这个框蹦到图像中的任何一个地方去.所以要对 t t t加以限制.
具体地,对于一个块,假设它对于整幅图的左上角的offset为 c x , c y c_x,c_y cx,cy,anchor的高宽为 p w , p h p_w,p_h pw,ph,预测值为 ( t x , t y ) (t_x,t_y) (tx,ty),则预测出的bbox的中心偏移 b x , b y b_x,b_y bx,by(注意,它仍是一个offset) 以及高宽 b h , b w b_h,b_w bh,bw和置信度 b o b_o bo为:
其中
σ
\sigma
σ为sigmoid函数.
6.Fine-Grained Features
为了检测小目标,就需要低层的特征图,因此作者提出了PassThrough Layer,来将低层特征和高层特征图进行融合.
具体地,骨干网络采用的DarkNet19,中间某一层的feature map的维度为(26,26,512) ,先用1x1conv降维至(26,26,64),之后reshape成(13,13,256)
( H , W , C ) − > ( H / 2 , W / 2 , 4 C ) (H,W,C)->(H/2,W/2,4C) (H,W,C)−>(H/2,W/2,4C)
和高层的输出(13,13,1024)进行融合(在特征维concat),得到(13,13,256+1024)的feature map.
7.Multi-Scale Training
替换掉固定的输入尺寸,将输入尺寸每10个epoch随机改变一次,改变的是32的倍数(因为原来输入为416,最终网络输出为13,缩放因子为32).
为什么可以输入不同的尺寸?
观察框图可知,Yolo v2是全卷积网络,因此可以有任意输入。
4.3 Yolo v3
把DarkNet19换成了DarkNet53. 仍然使用k-means聚类来计算anchors,计算offset的方式和v2相同
整体框架如下:
引自yolov3
Predict one,two,three尺寸不同(注意经过了上采样并与低层特征进行融合),可以检测不同尺度的目标.
关于损失函数,yolo v4,v5,X,有空再补.