一张图看懂Faster R-CNN。
背景
- Faster R-CNN采用与Fast R-CNN相同的设计,只是它用内部深层网络代替了候选区域方法
- 新的候选区域网络(RPN)在生成ROI时效率更高,并且以每幅图像10毫秒的速度运行。
Faster R-CNN 是作者 Ross Girshick 继 Fast R-CNN 后的又一力作,同样使用 VGG16 作为
backbone,推理速度在 GPU 上达到 5fps(每秒检测五张图,包括候选区域生成),准确度也有一定的进步。核心在于 RPN
区域生成网络(Region Proposal Network)。
四个部分融合在一个CNN中,实现端到端
网络架构
输入图像首先经过Backbone得到feature map
特征提取网络
RPN模块
Reginal Proposal Network:区域检测网络
区域生成较好的建议框模块即Proposal,代替SS算法。实现端到端
- Anchor生成:RPN对输入的feature map上的每个点对应9个anchor,每个anchor的大小宽高不同
- RPN卷积网络:利用1x1的卷积在feature map 上得到每一个Anchor的预测得分和预测偏移值
- 计算RPN loss:
- 生成Proposal:
- 筛选Proposal得到RoI
RPN的输入输出:
输入:feature map 、物体标签(训练集中所有物体和标签)
输出:Proposal(生成区域给后续模块分类和回归)、分类Loss、回归Loss(两个损失用于优化网络)
1.理解Anchor
anchor box: 以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。
用法:
- 提出多个被称为锚框的区域(暂定维边缘框)
- 预测每个锚框里是否含有目标物体
- 如果是,预测从这个锚框到真实边缘框的偏移。
锚框之间的相似度表示:IoU:
Jacquard指数的一个特殊情况
赋予锚框标号:
- 每个锚框是一个训练样本
- 将每个锚框,要么标注成背景,要么关联上一个真实边缘框
- 可能导致生成大量的锚框
- 导致大量的负类样本
9个大小不同的锚框:类似于鱼网
#锚框的生成
def generate_anchors(base_size=16,ratios=[0.5,1,2],scales=2**np.arange(3,6)):
#首先创建一个基本的anchor为[0,0,15,15]
重点,anchor生成:
- 对feature map进行3*3 的卷积,得到每一个点的维度是512(VGG)
- 512维对应原始图片上很多不同大小和宽高区域的特征。这些区域的中心点相同(假设下采样率是16,每一个点乘以16就可以得到原图对应的坐标)
- anchor有九种大小:scale=2**{8,16,32} ratio= {0.5,1,2}将9个大小的anchor反算到原图上,就可得到不同的原始proposal。
- VGG得出的feature map 大小为37*50 ,所有anchor总数:37x50x9 =16650个:注论文中大小是40x60
- 再将anchor输入分类和回归的卷积神经网络,分类网络来判断anchor是前景的概率和回归网络将预测偏移量作用到anchor上使得anchor会更接近于真实物体坐标
代码部分:
class AnchorGenerator(nn.Module):
def __init__(self,sizes=(128,256,512),aspect_ratios=(0.5,1.0,2.0)):
'''默认参数没有用到'''
super(AnchorGenerator,self).__init__()
if not isinstance(sizes[0],(list,tuple)):
# TODO change this
sizes = tuple((s,) for s in sizes) #(3,)元组中一个元素必须加,
if not isinstance(aspect_ratios[0],(list,tuple)):
aspect_ratios = (aspect_ratios,)*len(sizes)
assert len(sizes) == len(aspect_ratios)
self.sizes = sizes
self.aspect_ratios = aspect_ratios
self.cell_anchors = None #存储anchor的模板
self._cache = {} #存储anchor的坐标信息
def generate_anchors(self,scales,aspect_ratios,dtype=torch.float32 ,device = torch.device("cpu")):
# type: (List[int], List[float], torch.dtype, torch.device) -> Tensor
"""
compute anchor sizes
Arguments:
scales: sqrt(anchor_area)
aspect_ratios: h/w ratios
dtype: float32
device: cpu/gpu
"""
#先都转为tensor
scales = torch.as_tensor(scales, dtype=dtype, device=device)
aspect_ratios = torch.as_tensor(aspect_ratios, dtype=dtype, device=device)
h_ratios = torch.sqrt(aspect_ratios) #高度的乘法因子
w_ratios = 1.0 / h_ratios #宽度的乘法因子
#计算每个anchor的宽度和高度
# [r1, r2, r3]' * [s1, s2, s3]
# number of elements is len(ratios)*len(scales)
ws = (w_ratios[:, None] * scales[None, :]).view(-1) #anchor宽度值:将每个宽度比例乘以scales得到->展成一维向量
hs = (h_ratios[:, None] * scales[None, :]).view(-1) #anchor高度值:将每个高度比例乘以scales得到->展成一维向量
# left-top, right-bottom coordinate relative to anchor center(0, 0)
# 生成的anchors模板都是以(0, 0)为中心的, shape [len(ratios)*len(scales), 4]
base_anchors = torch.stack([-ws, -hs, ws, hs], dim=1) / 2 #输出base_anchors.shape =torch.size(15,4) 15个anchor,4个点的信息
return base_anchors.round() # round 四舍五入
def set_cell_anchors(self,dtype,device) -> (torch.dtype,torch.device):
if self.cell_anchors is not None: #cell_anchors初始化为None
cell_anchors = self.cell_anchors
assert cell_anchors is not None
# suppose that all anchors have the same device
# which is a valid assumption in the current state of the codebase
if cell_anchors[0].device == device:
return
# 根据提供的sizes和aspect_ratios生成anchors模板
# anchors模板都是以(0, 0)为中心的anchor
cell_anchors = [
self.generate_anchors(sizes,aspect_ratios,dtype,device)
for sizes ,aspect_ratios in zip(self.sizes,self.aspect_ratios)
]
self.cell_anchors = cell_anchors
def grid_anchors(self, grid_sizes, strides):
# type: (List[List[int]], List[List[Tensor]]) -> List[Tensor]
"""
anchors position in grid coordinate axis map into origin image
计算预测特征图对应原始图像上的所有anchors的坐标
Args:
grid_sizes: 预测特征矩阵的height和width
strides: 预测特征矩阵上一步对应原始图像上的步距
"""
anchors = []
cell_anchors = self.cell_anchors
assert cell_anchors is not None
#遍历每个预测特征层的grid_size,strides和cell_anchors
for size, stride, base_anchors in zip(grid_sizes, strides, cell_anchors):
grid_height, grid_width = size
stride_height, stride_width = stride
device = base_anchors.device
# For output anchor, compute [x_center, y_center, x_center, y_center]
# shape: [grid_width] 对应原图上的x坐标(列)
shifts_x = torch.arange(0, grid_width, dtype=torch.float32, device=device) * stride_width
# shape: [grid_height] 对应原图上的y坐标(行)
shifts_y = torch.arange(0, grid_height, dtype=torch.float32, device=device) * stride_height
# 计算预测特征矩阵上每个点对应原图上的坐标(anchors模板的坐标偏移量)
# torch.meshgrid函数分别传入行坐标和列坐标,生成网格行坐标矩阵和网格列坐标矩阵
# shape: [grid_height, grid_width]
shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x)
shift_x = shift_x.reshape(-1)
shift_y = shift_y.reshape(-1)
# 计算anchors坐标(xmin, ymin, xmax, ymax)在原图上的坐标偏移量
# shape: [grid_width*grid_height, 4]
shifts = torch.stack([shift_x, shift_y, shift_x, shift_y], dim=1)
# For every (base anchor, output anchor) pair,
# offset each zero-centered base anchor by the center of the output anchor.
# 将anchors模板与原图上的坐标偏移量相加得到原图上所有anchors的坐标信息(shape不同时会使用广播机制)
shifts_anchor = shifts.view(-1, 1, 4) + base_anchors.view(1, -1, 4)
anchors.append(shifts_anchor.reshape(-1, 4))
return anchors # List[Tensor(all_num_anchors, 4)]
def cached_grid_anchors(self,grid_sizes,strides) :
# type: (List[List[int]], List[List[Tensor]]) -> List[Tensor]
"""将计算得到的所有anchors信息进行缓存"""
key = str(grid_sizes) + str(strides)
#self._cache初始化时是个空字典,所有下面条件不满足
if key in self._cache:
return self._cache[key]
#得到所有预测特征层在原图上的anchors
anchors = self.grid_anchors(grid_sizes,strides)
self._cache[key] = anchors #将anchors存入到字典
return anchors
def forward(self,image_list,feature_maps):
# type: (ImageList, List[Tensor]) -> List[Tensor]
#获取每个backbone预测特征层的尺寸(height,weight) shape的最后两个维度就是
grid_sizes = list([ feature_map.shape[-2:] for feature_map in feature_maps])
#获取原始输入图像的height和weight
image_size = image_list.tensor.shape[-2:]
# 获取变量类型和设备类型
dtype, device = feature_maps[0].dtype, feature_maps[0].device
''''#计算feature map和 原始图像的比例大小 原图的高度/grid_size的高度
# one step in feature map equate n pixel stride in origin image
# 计算特征层上的一步等于原始图像上的步长'''
strides = [ [ torch.Tensor(image_size[0]/g[0],dtype=torch.int64,device= device),
torch.Tensor(image_size[1]/g[1],dtype=torch.int64,device= device)] for g in grid_sizes]
# 根据提供的sizes和aspect_ratios生成anchors模板
self.set_cell_anchors(dtype, device) #已经将anchors模板生成,
# 计算/读取所有anchors的坐标信息(这里的anchors信息是映射到原图上的所有anchors信息,不是anchors模板)
# 得到的是一个list列表,对应每张预测特征图映射回原图的anchors坐标信息
anchors_over_all_feature_maps = self.cached_grid_anchors(grid_sizes,strides)
anchors = torch.jit.annotate(list[list[torch.Tensor]], [])
# 遍历一个batch中的每张图像
for i, (image_height, image_width) in enumerate(image_list.image_sizes):
anchors_in_image = []
# 遍历每张预测特征图映射回原图的anchors坐标信息
for anchors_per_feature_map in anchors_over_all_feature_maps:
anchors_in_image.append(anchors_per_feature_map)
anchors.append(anchors_in_image)
# 将每一张图像的所有预测特征层的anchors坐标信息拼接在一起
# anchors是个list,每个元素为一张图像的所有anchors信息
anchors = [torch.cat(anchors_per_image) for anchors_per_image in anchors]
# Clear the cache in case that memory leaks.内存泄露
self._cache.clear()
return anchors
2.RPN的真值与预测值
RPN可以预测Anchor的类别作为预测边框的类别
可以预测真实的边框相对于Anchor位置的偏移量
而不是直接预测边界框的中心坐标x和y,宽w ,高h
**模型(类别)真值:**对应类别真值,RPN只需proposal生成,保证recall,没必要细分每个区域属于哪个类别,
只需前景(有目标物)和背景两个类别。
前景和后景判断方法:Anchor和标签框(真实框)的IoU 。算出的IoU大于阈值就是前景,否则就是背景
偏移量真值:
将偏移量的真值输入RPN网络–>输出预测偏移量
t
x
∗
,
t
y
∗
,
t
w
∗
,
t
h
∗
t_x^*,t_y^*,t_w^*,t_h^*
tx∗,ty∗,tw∗,th∗
- 如果没有Anchor:物体检测需要直接预测每个框的位置,由于框的坐标幅度大,会使网络模型很难收敛和预测
- 有了Anchor:相当于一个先验阶梯,使得RPN去预测Anchor的偏移量,更好的接近真实物体
总结:
- 类别上Anchor根据IoU判断出是否前景,将后景会生成负样本
- 得到是前景的anchor,算出和标签框的偏移量,从而让RPN模型不断学习,给出一个Proposal
3.RPN卷积网络
RPNhead代码实现
class RPNhead(nn.Moduke):
'''
add a RPN head with classification and regression
通过滑动窗口计算预测目标概率与bbox regression参数
Arguments:
in_channels: number of channels of the input feature
num_anchors: number of anchors to be predicted
'''
def __init__(self,in_channels,num_anchors):
super(RPNhead,self).__init__()
#从backbone出来的特征图,经历3*3大小的卷积层
self.conv = nn.Conv2d(in_channels,in_channels,kernel_size=3,stride=1,padding=1)
#计算预测的目标目标类别的分手(前景或后景)
self.cls_logits = nn.Conv2d(in_channels,num_anchors,kernel_size=1,stride=1) #论文中输出的类别为2k,因为是二分类,给一种类别就可以判断
#计算预测目标bbox regression位置偏移参数
self.box_pred = nn.Conv2d(in_channels,num_anchors*4,kernel_size=1,stride=1)
for layer in self.children():
'''初始化卷积网络,初始权重'''
if isinstance(layer,nn.Conv2d):
torch.nn.init.normal_(layer.weight,std = 0.01)
torch.nn.init.constant_(layer.bias,0)
def forward(self,x) -> (list[Tensor]) :
'''x是backbone输出的特侦层,输出预测的类别和预测框的列表'''
logits = []
bbox_reg = []
for i,feature in enumerate(x):
'''遍历输入进来的每一个特征层'''
t = nn.ReLU(self.conv(feature))
logits.append(self.cls_logits(t))
bbox_reg.append(self.box_pred)
return logits , bbox_reg
4.RPN真值的求取
5.损失函数设计
- 分类损失:
- 回归损失
6.NMS和生成Proposal
使用NMS输出:
- 每个锚框预测一个边缘框
- NMS可以合并相似的预测
- 选中是非背景类的最大值预测值
- 去掉所有其他和它IoU值大于置信度的预测
- 重复上述过程直到所有预测要么被选中要么被去掉
过程:
7.筛选Proposal得到RoI
从上一步生成的Proposal数量的2000个 ,任然还有很多背景框,需要对proposal进行再次筛选
利用标签和proposal构建IoU矩阵:通过和标签重合度选出256个正负样本。
RoI Pooling层(Region of Interest)
RoI Pooling
RoI Align
使用双线性插值获得坐标为浮点数的值,最大可能的保留了原始区域的特征,对本身较小的物体改善更为明显
将RoI对应到特征图上,但坐标和大小都保留浮点数,大小为20.75*20.75
将20.75*20.75大小均匀分成 7x7方格大小。中间依然保留浮点数。每个方格内特定位置选取4个采样点进行特征采样
RCNN模块
预测结果映射回原尺度