R-CNN
现在,将目光穿越回2012年,hinton刚刚提出alexnet的时代。
此时,该如何审视目标检测任务?
当时的目标检测采用的是滑动窗口+手动特征+分类器的思路。
该方法的弱点包括
-
速度慢
-
精度差
精度差的问题是由手工特征造成的,因此可以通过CNN解决
速度慢的问题是由滑动窗口造成的,结合图像金字塔,速度会非常的慢。
如何解决速度慢的问题?
区域选择算法
如果采用金字塔+滑动窗口,同时加上不同窗口比例,目标框可能会数百万计。
对每个目标框的区域都进行图像分类,会造成极大的计算压力。
然而,绝大部分的框都是背景,只有少部分的框是目标。
一种直观的解决思路就是,从图像中筛选出可能存在目标的区域,然后再对这些目标进行仔细辨认
这种思路也是符合人类视觉习惯,例如,人们首先会定位出人的位置,再辨识出是谁。
2014年提出的RCNN就是用到这种思路。
如何筛选出可能存在目标的区域?
从视觉上讲,我们要先找出物体,再判断物体类别。
那么什么是一个物体?就是表达同一个语义的像素区域。
例如,一个雪原场景中,一些黑色的色块,就是同一个物体。
这里就涉及到一个基本假设:表达同一个语义信息的像素,在数值上存在连续性或者相关性
在RCNN中,采用了一种基于传统方法的图像分割算法(Felzenszwalb and Huttenlocher algorithm),按照像素值将图像分割成小的子块,再进行合并后得到的图像分割结果。
关于图像分割算法的样例代码如下所示,具体原理细节在图像分割部分讲授。
In [11]:
# Felzenszwalb and Huttenlocher
import matplotlib.pyplot as plt
import numpy as np
from skimage.data import astronaut
from skimage.color import rgb2gray
from skimage.filters import sobel
from skimage.segmentation import felzenszwalb, slic, quickshift, watershed
from skimage.segmentation import mark_boundaries
from skimage.util import img_as_float
img = img_as_float(astronaut()[::2, ::2])
segments_fz = felzenszwalb(img, scale=100, sigma=0.5, min_size=50)
print(f"Felzenszwalb number of segments: {len(np.unique(segments_fz))}")
fig, ax = plt.subplots(1, 2, figsize=(10, 10), sharex=True, sharey=True)
ax[0].imshow(img)
ax[0].set_title("original")
ax[1].imshow(mark_boundaries(img, segments_fz))
ax[1].set_title("Felzenszwalbs's method")
for a in ax.ravel():
a.set_axis_off()
plt.tight_layout()
plt.show()
Felzenszwalb number of segments: 194
再回顾一下我们的目标,希望生成多个包含物体的候选框。
那么如何通过分割结果得到候选框?
具体步骤如下:
- 计算所有邻域之间的相似性(像素相似性、纹理相似性、大小相似性和形状相似性)
- 两个最相似的区域被组合到一起
- 计算合并区域和相邻区域的相似度
- 重复2、3,直到整个图像变为一个区域
算法截图:
具体解释:
- 先计算各个区域间的相关性
- 找出相关性最大对应的两个区域,进行合并。
- 合并后,将合并的区域加入集合R,并将原先两个区域的相关性删除,加入新区域的相关性
- 重复上述步骤2和3,直到全部的相关性从S中删除。
- 最后根据4得到的全部区域集合,得到对应区域的全部候选框。
以上步骤被称为selective search。
计算相似度
计算相似度有四种方法:
- 颜色
- 纹理
- 大小
- 形状
颜色相似度
如何度量颜色? 直方图
对每个通道量化成25个直方图,三个通道就有75维直方图
根据直方图的重叠区域来定义颜色的相似度
纹理相似度
定义纹理也采用直方图的方式进行。
纹理直方图如何定义?统计每个像素的特征方向。
定义出这些直方图之后,通过直方图的重叠区域定义纹理相似度。
大小相似度
大小相似度并不是对同样大小的区域合并,而是希望优先合并小的区域。
形状相似度
形状相似度也不是为了将相同形状的区域合并,而是衡量两个区域是否更加吻合。(像拼图一样,如果合并后的区域更加符合矩形框,则优先级更高)
综上所述,总体相似度为
最终,selective search算法生成了大量的候选框,从上至下挑选一千至两千个框,称为region proposal.
相比于传统的框来说,region proposal的方式效率极高,避免对每个像素进行遍历。
小结:selective search的步骤为
-
用分割算法获取若干个区域
-
计算两两区域之间的相似度,原则:给小的区域与吻合的区域赋予较大的相似度
-
挑选出相似度最大的区域,进行合并;
-
合并后的区域送入备选区域集合,并删除掉合并前的相似度
-
利用步骤2计算出新区域与邻居区域的相似度,加入相似度集合
-
重复3、4、5步骤,直到相似度集合中没有可以合并的值。
从候选区域集合中获取1-2千个区域,用于后续检测。
特征提取与分类
区域选择代替了滑动窗口,下一步就需要对候选区域进行特征提取。
考虑到选择的区域存在不同的尺度和比例,此处第一步就需要对齐所有区域。
step1. resize region: 将所有的区域resize成同样大小的尺寸(经过cnn后映射到fc的参数一样)
step2. extract feature with CNN:将所有的区域送入CNN中提取特征。
还记得什么是CNN提取的特征吗?
把softmax去掉后的feature map就是特征。
采用的模型为alexnet或者vgg16
那么如何训练这个CNN?
-
step1. 用imagenet训练分类网络
-
step2. 针对检测目标进行训练。
- 数据集采用pascal voc,每张图像用selective search得到候选框,用候选框作为CNN的输入
- 定义标签:当候选框与目标检测框的重叠度超过0.5,则认为是一个正样本;否则是一个负样本
- 此处为多分类器分类,每个分类器是一个二分类,判断是不是这个目标
位置优化
图像分割获得的结果,并不一定准确。
直接将图像分割结果用作最后的检测结果,边界框可能存在较大的误差。
对此,RCNN将位置优化定义为一个线性回归问题
定义某类分类的正例结果,在原图上的位置为x,y,w,ℎ
而ground truth上的位置为 x′,y′,w′,ℎ′.
那么我们说这两个框的差距可以用两个操作完成:平移和缩放
对此,定义四个量如下
将其定义一个优化问题如下
本质上,对于确定了目标的框,需要通过上述求得的参数α∗进行位置优化
对位置进行进一步的精修
非极大值抑制
为了避免多个高度重叠的候选框,需要使用NMS来抑制那些与已有框的重叠度高于一定阈值的候选框。
NMS的原理
- 判断两两框之间是否重合,以及重合的IoU是多少;
- 当IoU大于一定阈值,保留较大的分的框,删除较小的分的框。
慢速的nms
import cv2
import numpy as np
def nms(bounding_boxes, confidence_score, threshold):
if len(bounding_boxes) == 0:
return [], []
bboxes = np.array(bounding_boxes)
score = np.array(confidence_score)
# 计算 n 个候选框的面积大小
x1 = bboxes[:, 0]
y1 = bboxes[:, 1]
x2 = bboxes[:, 2]
y2 = bboxes[:, 3]
areas =(x2 - x1 + 1) * (y2 - y1 + 1)
# 对置信度进行排序, 获取排序后的下标序号, argsort 默认从小到大排序
order = np.argsort(score)
picked_boxes = [] # 返回值
picked_score = [] # 返回值
while order.size > 0:
# 将当前置信度最大的框加入返回值列表中
index = order[-1]
picked_boxes.append(bounding_boxes[index])
picked_score.append(confidence_score[index])
# 获取当前置信度最大的候选框与其他任意候选框的相交面积
x11 = np.maximum(x1[index], x1[order[:-1]])
y11 = np.maximum(y1[index], y1[order[:-1]])
x22 = np.minimum(x2[index], x2[order[:-1]])
y22 = np.minimum(y2[index], y2[order[:-1]])
w = np.maximum(0.0, x22 - x11 + 1)
h = np.maximum(0.0, y22 - y11 + 1)
intersection = w * h
# 利用相交的面积和两个框自身的面积计算框的交并比, 将交并比大于阈值的框删除
ratio = intersection / (areas[index] + areas[order[:-1]] - intersection)
left = np.where(ratio < threshold)
order = order[left]
return picked_boxes, picked_score
总结
RCNN是真正使用深度学习进行目标检测的开山之作,为后续的faster rcnn系列提供了巨人的肩膀。
总体示意图如下图所示
优势:
- rcnn打破了传统思路滑动窗口的模式,提供了更加快速的检测
- 大幅提升了传统方法的性能,在voc2007上从35%提升到了53%
劣势:
- 速度依然很慢。gpu上的速度大概每张图像13秒
rcnn的局限性
RCNN的首要问题在于:
* 目标产生了形变
* 目标框要一一识别
- 目标框一一识别
2000个目标框,每个都需要调用CNN进行识别。这是算力上的严重浪费。(速度慢的原因之一)
如果能对整张图像进行特征提取,然后再对其对应的特征进一步的分类,就可以解决上述问题。
- 目标产生形变
从selective search中产生的目标框,想要从其中获取特征,需要先把他resize成相同大小的图像。
这样就会造成较大的误差,严重降低了识别精度。
为什么要变成相同大小的内容?
因为CNN需要变成相同大小的内容。
为什么CNN需要输入相同大小的内容?
因为CNN的卷积层到fc层限定了feature map的尺寸。
问题:如何能够令cnn接收不同尺寸的输入与输出?
问题的关键就在于令卷积层能够对不同尺寸的图像,输出相同大小的feature map(通过SPP)
即然无法改变参数,就只能改变非参数的层,即池化层。adaptivemaxpooling
SPP Net
金字塔池化层
自适应窗口的池化层
根据输入的feature map大小,将feature map划分为固定数量的块。
每块采用最大池化,将不同尺寸的feature map池化为固定尺寸的池化。
金字塔池化
分别用不同的尺度进行池化,最终得到不同尺寸的feature map池化结果。
将不同尺寸的feature拉平后 concate起来,得到最终的结果。
具体参数
-
输入任意大小的,N1通道的feature map
-
第一尺度的池化,将feature map池化为4*4
-
第二尺度的池化,将feature map池化为2*2
-
第三尺度的池化,将feature map池化为1*1
上述特征按通道flatten,分别得到16*N1, 4*N1, 1*N1的特征,连接起来,就可以得到一个21*N1的feature map
统一提取特征
通过selective search得到的目标框,坐标可以映射到feature map中。
这些feature map经过空间金字塔池化的处理,得到相同大小的faeture map
这些feature map会被进一步的送入fc层进行特征提取,最终得到目标框的特征
SPP net的总体流程
-
selective search找到若干候选框
-
整张图像进入cnn提取特征
-
根据候选框从feature map中提取对应的特征,送入SPP层
-
SPP层的内容经过FC提取,得到特征,最终存入硬盘
-
硬盘中读取特征,进入SVM进行预测和训练;
-
硬盘中读取特征,进入回归中进行预测和训练。
训练过程
- 先训练ConvNet,在imagenet的基础上fine tuning(用提取到的特征微调fc)
- 完成了conv的训练后,将提取到的特征用于svm训练
- 训练回归(L2损失)
SPPNET的实验结果
- 精度略低于RCNN(降低了不到3个点):原因:训练的时候不能微调conv(效率低下)在训练fc时无法优化conv:因为SPP到fc特征是拼接得到的,因此无法求导回传梯度
- 速度远快于RCNN(38倍预测速度,3.4倍训练速度) 原因:大图像进行了一次conv,rcnn需要运行2000次
SPPNET存在什么问题?
SPPnet较好的解决了不同尺度的图像送入cnn的问题,但是在svm和回归的分类器,使得模型的训练非常的繁琐。
具体上,我们要先把图像提取的fc特征存储到硬盘上。
然后从硬盘读取出来(比从内存读慢得多),再进行分类和回归。
时间慢、占用磁盘高。
Fast RCNN--现代目标检测的雏形
fast rcnn提出了rcnn的三个不足:
-
测试效率低
-
多阶段训练模型
-
训练计算量大
对此,fast rcnn做了以下三点改进:
-
roi(region of interest感兴趣的区域) pooling(spp 的一种特例)
-
先对全图进行特征提取,然后从feature map中提取出对应区域(spp已经解决)
-
将分类和回归合并到了一起
fast rcnn如何训练?
对于一个分类cnn网络,需要做三件事情:
-
将最后一层pool换成roi pooling
-
将模型的fc分别输送给并行的两个内容:softmax分类和回归
-
将模型的输入改变为两个:图像和proposal region
这样就把一个imagenet训练的模型载入了fast rcnn中(conv)。
之后,用voc数据集对参数进行训练。
不同于sppnet,fast rcnn这里是使用了分层的思想进行训练。
sppnet是从一个batch中均匀的采样出若干个roi框,也就是说他的batch size中必须有足够多的图像才能进行
然而,fastrcnn中,是从一张图像中提取出若干个框来训练。
这样,sppnet中会占用更多的显存。
fast rcnn的损失函数
- 第一项是:分类(softmax)[损失,u为ground truth,p是预测结果 −logpu
- 第二项是:回归损失,采用了smooth L1损失进行
关于smooth l1损失,是为了解决l2损失的不足(当x很大时,求导接近正无穷,会造成梯度爆炸)。
当x>=1时,梯度=1,解决了梯度爆炸的问题
SVD优化算法
fc层的快速优化算法:通过svd奇异值分解。
已知fc层参数W可分解为:
1层fc==>2层fc(w1=∑V^T; w2=U)提高了速度
就可以分解成两个不同的全联接层。
fast-rcnn代码可看
Faster RCNN
从RCNN网络开始,用region proposal代替了滑动窗口式的遍历检测
之后的SPP Net,开启了统一提取特征并在feature map上的特征提取
而后,Fast RCNN则是将回归与分类结合起来,形成了多任务训练模型
下面该如何改进?
Motivation
虽然Fast RCNN很优雅,但是其存在一个源生性缺陷:selective search
selective search固然是代替滑动窗口来实现目标检测的里程碑式工作,但目前已经严重阻碍了目标检测的发展。
主要的阻碍分为两个方面:
-
速度慢:selective search步骤占据了fast RCNN中的大多数运算时间
-
精度差:无论如何,selective search是一个基于手工特征的方法,本身依然存在泛化性不足的问题(迷彩服)
这样一来,如何改进selective search,则变成了实现现代目标检测算法中的最关键的一个步骤。
为了改进selective search(手工特征),一个直接的想法就是利用神经网络。
那么一个什么样的神经网络可以满足要求呢?
这样的神经网络又该如何训练呢?
基本思路:RPN网络
Region Proposal Network, 其目的是接受图像的输入,并输出可能存在目标的框。那么如何实现?
- 要找出粗略的目标的话,首先要对模型进行特征提取。如何做?
用imagenet模型参数作为初始参数,提取自然图像中的高纬特征。
将得到的高维特征作为输入,我们希望新的网络从中预测出潜在的框的位置。
- 如何找到潜在框的位置?
本质上就是针对特征层面的selective search。
这也是faster rcnn中最关键的内容。
目标区域推荐
假设我们已经训练好了一个网络,这个网络能对输入的大尺寸的feature map中得到若干个不同尺寸的框。
那么这个流程,应当可以借鉴传统的目标检测方法,即滑动窗口的方式。
用一个固定大小的窗口对feature map中的元素进行遍历,每个窗口对应了原图像中的一个区域。
把这个窗口截取的feature map进行后续的分类和回归,来判断这个窗口是否存在目标,以及应该框在哪里。
注意 这里的目标依然是固定尺寸的。针对目标可能存在比例、尺度的不同,这里对每个feature进行不同尺寸窗口的截取。
具体上,窗口中存在3种比例,3种尺度(128,256,512)。分别是
一张featuremap中可以提取多个不同尺度的proposal。
对每个proposal进行预测,(判断这里的框是/否是目标,而不是是哪一类),即可得到正例样本。
同时不要忘记进行nms,最终可以输出推荐出来的,可能存在区域的样本。
问题:如何训练这样的网络?
- 将imagenet的模型载入base net
- 输入一张图像,得到feature map
- 根据输入图像的目标框位置,得到若干正样本(包含目标)以及对应数量的负样本(不包含目标的框)
选择正负样本应当遵循一下原则:
1. Anchor boxes与ground truth框的IoU大于0.7的每个anchor box标记为正样本。因为IoU较大表示anchor box与ground truth框有较好的匹配。
2. Anchor boxes与所有的ground truth框的IoU都小于0.3的每个anchor box标记为负样本。因为IoU较小表明anchor box可能是背景或误检。
3. Anchor boxes与某个ground truth框的IoU在0.3到0.7之间的不参与训练。因为这些anchor box难以清晰判断是正样本还是负样本。
4. 每个ground truth框最多只能匹配一个anchor box。如果多个anchor box与同一ground truth框IoU都大于0.7,则选择IoU最大的那一个作为正样本,其余不参与训练。
5. 每个ground truth框至少要匹配一个anchor box作为正样本。如果没有任何anchor box可以匹配,则选择与该ground truth框IoU最接近0.7的anchor box作为正样本。(想一下为什么?当目标物体大于最大的候选框时会出现这种情况)
6. 对于未匹配到任何ground truth框的anchor boxes,标记为负样本。
7. 保证正负样本数量比例为1:1或1:3,过度不平衡会影响模型的收敛。
- 最后,将选择到的正负样本送入网络进行训练,让模型充分认识到哪些区域可能会存在特征
(负样本的选择尽量与目标有小的重叠,这样网络可以学习到“较难”的特征)
- 根据联合损失(分类+回归)得到目标框的位置
根据以上内容,我们可以大致画出rpn模型的草图如下
那么根据这个网络,我们就可以彻底甩掉selective search,用上述网络实现目标区域的粗筛选。
(为什么是粗筛选?因为不知道具体属于什么类别,只知道是否是目标)
Faster RCNN的进一步优化
在得到RPN网络后,一个直接的感受就是
- 图像先进入RPN进行区域推荐(RPN将坐标(anchor的中心点)输入给roi)
- 图像进入fast rcnn进行训练,只不过selective search的部分,由rpn得到
显然,这样做是一种极大的浪费,因为图像会被提取两次basenet,极大地降低了效率。
为何不将这两个网络进行合并?
也就是说他们的basenet应当公用。
这样的话,就可以一次提取特征,用于rpn和后续检测。
最终,faster rcnn的框图如下图所示
训练
那么这样的一个网络该如何训练?
-
将imagenet的模型载入basenet,然后训练RPN模块(basenet部分不更新)
-
带入RPN模块,三部分进行统一训练。(四个loss:RPN坐标和位置,svm和Bbox回归)
损失函数
关于损失函数部分,rpn会得到cls_rpn和reg_rpn;rcnn那部分会得到cls_rcnn和reg_rcnn,
将上述两部分特征加起来,就是最终的损失。