4.1 目标检测概述
- 了解目标检测的任务
- 了解目标检测的常用数据集
- 了解目标检测算法的评价指标
- 掌握非极大值NMS算法的应用
- 了解常用的目标检测算法分类
1. 目标检测
目标检测(Object Detection)任务是找出图像中所有感兴趣的目标,并确定它们的类别和位置。
目标检测中可检测到的对象取决于当前任务(数据集)需要检测的对象。假设我们的目标检测模型被定位为检测动物(牛、羊、猪、狗、猫),那么该模型不会输出鸭子、书籍和其他类型的结果。
目标检测的位置信息一般有两种格式(图片左上角为原点(0,0):
1、极坐标表示:(xmin, ymin, xmax, ymax)
- xmin,ymin:x,y坐标的最小值
- xmin,ymin:x,y坐标的最大值
2、中心点坐标:(x_center, y_center, w, h)
- x_center, y_center:目标检测框的中心点坐标
- w,h:目标检测框的宽度和高度
假设在以下图像中进行检测:
目标检测结果的中心点表示如下:
2.常用的开源数据集
有两种经典的目标检测数据集, 和 。
2.1
PASCAL VOC是目标检测领域的经典数据集。PASCAL VOC训练和验证大约有1万张带边界框的图片。PASCAL VOC数据集是目标检测问题的基准数据集,在这个数据集中获得了许多模型。常用的是VOC2007和VOC2012年有20个版本数据类别:
也就是:
1.人: 人
2.动物: 鸟,猫,牛,狗,马,羊
三、交通工具: 飞机、自行车、船舶、公共汽车、汽车、摩托车、火车
:https://pjreddie.com/projects/pascal-voc-dataset-mirror/
整个数据的目录结构如下所示:
其中:
- JPEGImages存储图片文件
- Annotations下存放的是xml文件描述了图片信息,如下图所示,节点下的数据,特别是bndbox下的数据.xmin,ymin构成了boundingbox的左上角,xmax,ymax构成了boundingbox右下角,即图像中的目标位置信息
- ImageSets包括以下四个文件夹:
- Action人的动作存放在下面(比如running、jumping等等)
- Layout存储在人体部位的数据(人的head、hand、feet等等)
- Segmentation可用于分割的数据存储在下面。
- Main存储在图像物体识别下的数据分为20类,是目标检测的重点。文件夹中的数据描述了负样本文件。
2.2
MS COCO的全称是Microsoft Common Objects in Context,2014年微软出资标注Microsoft COCO数据集,与ImageNet就像比赛一样,它被认为是计算机视觉领域最受关注和权威的比赛之一。
COCO数据集是一种大型、丰富的物体检测、分割和字幕数据集。该数据集以场景理解为目标,主要从复杂的日常场景中截取,图像中的目标通过精确的分割来校准位置。图像包括91类目标,328,000图像,2,500,000label。到目前为止,目标检测的最大数据集有80个类别 类,有超过33 其中20万张图片 一万张有标记,整个数据集中的个人数量超过150 万个。
图像示例:
coco标记每个数据集的标签文件segmentation bounding box精确坐标的精度如下:
{“segmentation”:[[392.87, 275.77, 402.24, 284.2, 382.54, 342.36, 375.99, 356.43, 372.23, 357.37, 372.23, 397.7, 383.48, 419.27,407.87, 439.91, 427.57, 389.25, 447.26, 346.11, 447.26, 328.29, 468.84, 290.77,472.59, 266.38], [429.44,465.23, 453.83, 473.67, 636.73, 474.61, 636.73, 392.07, 571.07, 364.88, 546.69,363.0]], “area”: 28458.996150000003, “iscrowd”: 0,“image_id”: 503837, , “category_id”: 4, “id”: 151109},
3.常用的评价指标
3.1 IOU
在目标检测算法中,IoU(intersection over union,交并比)是目标检测算法中用估两个矩形框相似度的指标:
,
如下图所示:
看看目标检测中的应用:
上图蓝框为检测结果,红框为真实标注。
然后我们可以通过预测结果和真实结果之间的结合来衡量两者之间的相似性。一般来说,测试框的判断会有一个阈值,即IoU
当IoU
的值大于0.5
当目标物体被认为是检测到的时候。
实现方法:
import numpy as np # 计算定义方法IOU def Iou(box1, box2, wh=False): # 判断bbox的表示形式 if wh == False: # 使用极坐标表示:直接获得两个bbox的坐标 xmin1, ymin1, xmax1, ymax1 = box1 xmin2, ymin2, xmax2, ymax2 = box2 else: # 使用中心点表示: 两个两个bbox的极坐标表示形式 # 左上角第一框坐标 xmin1, ymin1 = int(box1[0]-box1[2]/2.0), int(box1[1]-box1[3]/2.0) # 第一框右下角坐标 xmax1, ymax1 = int(box1[0] box1[2]/2.0), int(box1[1] box1[3]/2.0) # 第二框左上角坐标 xmin2, ymin2 = int(box2[0]-box2[2]/2.0), int(box2[1]-box2[3]/2.0) # 第二框右下角坐标 xmax2, ymax2 = int(box2[0] box2[2]/2.0), int(box2[1] box2[3]/2.0) # 获取矩形框交集对应的左上角和右下角的坐标(intersection) xx1 = np.max([xmin1, xmin2]) yy1 = np.max([ymin1, ymin2]) xx2 = np.min([xmax1, xmax2]) yy2 = np.min([ymax1, ymax2]) # 计算两个矩形框面积 area1 = (xmax1-xmin1) * (ymax1-ymin1) area2 = (xmax2-xmin2) * (ymax2-ymin2) #计算交集面积 inter_area = (np.max([0, xx2-xx1])) * (np.max([0, yy2-yy1])) #计算交并比 iou = inter_area / (area1 area2-inter_area 1e-6) return iou
假设检测结果如下所示,并显示在图像上:
import matplotlib.pyplot as plt import matplotlib.patches as patches # 真实框与预测框 True_bbox, predict_bbox = [100, 35, 398, 400], [40, 150, 355, 398] # bbox是bounding box的缩写 img = plt.imread('dog.jpeg') fig = plt.imshow(img) # 将边界框(左上x, 左上y, 右下x, 右下y)格式转换成matplotlib格式:((左上x, 左上y), 宽, 高) # 真实框绘制 fig.axes.add_patch(plt.Rectangle( xy=(True_bbox[0], True_bbox[1]), width=True_bbox[2]-True_bbox[0], height=True_bbox[3]-True_bbox[1], fill=False, edgecolor="blue", linewidth=2)) # 预测框绘制 fig.axes.add_patch(plt.Rectangle( xy=(predict_bbox[0], predict_bbox[1]), width=predict_bbox[2]-predict_bbox[0], height=predict_bbox[3]-predict_bbox[1], fill=False, edgecolor="red", linewidth=2))
计算IoU:
Iou(True_bbox,predict_bbox)
结果为:
0.5114435907762924
3.2 mAP(Mean Average Precision)
目标检测问题中的每个图片都可能包含一些不同类别的物体,需要评估模型的物体分类和定位性能。因此,用于图像分类问题的标准指标precision不能直接应用于此。 在目标检测中,mAP是主要的衡量指标。
mAP是多个分类任务的AP的平均值,而AP(average precision)是PR曲线下的面积,所以在介绍mAP之前我们要先得到PR曲线。
- True Positive (TP): IoU>IOUthreshold
- 一般取 0.5 ) 的检测框数量(同一 Ground Truth 只计算一次)
- False Positive (FP): IoU<=IOUthreshold的检测框数量,或者是检测到同一个 GT 的多余检测框的数量
- False Negative (FN): 没有检测到的 GT 的数量
- True Negative (TN): 在 mAP 评价指标中不会使用到
- 查准率(Precision): TP/(TP + FP)
- 查全率(Recall): TP/(TP + FN)
二者绘制的曲线称为 P-R 曲线
先定义两个公式,一个是 Precision,一个是 Recall,与上面的公式相同,扩展开来,用另外一种形式进行展示,其中 all detctions
代表所有预测框的数量, all ground truths
代表所有 GT 的数量。
AP 是计算某一类 P-R 曲线下的面积,mAP 则是计算所有类别 P-R 曲线下面积的平均值。
假设我们有 7 张图片(Images1-Image7),这些图片有 15 个目标(绿色的框,GT 的数量,上文提及的 all ground truths
)以及 24 个预测边框(红色的框,A-Y 编号表示,并且有一个置信度值):
根据上图以及说明,我们可以列出以下表格,其中 Images 代表图片的编号,Detections 代表预测边框的编号,Confidences 代表预测边框的置信度,TP or FP 代表预测的边框是标记为 TP 还是 FP(认为预测边框与 GT 的 IOU 值大于等于 0.3 就标记为 TP;若一个 GT 有多个预测边框,则认为 IOU 最大且大于等于 0.3 的预测框标记为 TP,其他的标记为 FP,即一个 GT 只能有一个预测框标记为 TP),这里的 0.3 是随机取的一个值。
通过上表,我们可以绘制出 P-R 曲线(因为 AP 就是 P-R 曲线下面的面积),但是在此之前我们需要计算出 P-R 曲线上各个点的坐标,根据置信度从大到小排序所有的预测框,然后就可以计算 Precision 和 Recall 的值,见下表。(需要记住一个叫累加的概念,就是下图的 ACC TP 和 ACC FP)
- 标号为 1 的 Precision 和 Recall 的计算方式:Precision=TP/(TP+FP)=1/(1+0)=1,Recall=TP/(TP+FN)=TP/(
all ground truths
)=1/15=0.0666 (all ground truths 上面有定义过了
) - 标号 2:Precision=TP/(TP+FP)=1/(1+1)=0.5,Recall=TP/(TP+FN)=TP/(
all ground truths
)=1/15=0.0666 - 标号 3:Precision=TP/(TP+FP)=2/(2+1)=0.6666,Recall=TP/(TP+FN)=TP/(
all ground truths
)=2/15=0.1333 - 其他的依次类推
然后就可以绘制出 P-R 曲线
得到 P-R 曲线就可以计算 AP(P-R 曲线下的面积),要计算 P-R 下方的面积,有两种方法:
- 在VOC2010以前,只需要选取当Recall >= 0, 0.1, 0.2, …, 1共11个点时的Precision最大值,然后AP就是这11个Precision的平均值,取 11 个点 [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] 的插值所得
得到一个类别的 AP 结果如下:
要计算 mAP,就把所有类别的 AP 计算出来,然后求取平均即可。
- 在VOC2010及以后,需要针对每一个不同的Recall值(包括0和1),选取其大于等于这些Recall值时的Precision最大值,如下图所示:
然后计算PR曲线下面积作为AP值:
计算方法如下所示:
4.NMS(非极大值抑制)
非极大值抑制(Non-Maximum Suppression,NMS),顾名思义就是抑制不是极大值的元素。例如在行人检测中,滑动窗口经提取特征,经分类器分类识别后,每个窗口都会得到一个分数。但是滑动窗口会导致很多窗口与其他窗口存在包含或者大部分交叉的情况。这时就需要用到NMS来选取那些邻域里分数最高(是行人的概率最大),并且抑制那些分数低的窗口。 NMS在计算机视觉领域有着非常重要的应用,如视频目标跟踪、数据挖掘、3D重建、目标识别以及纹理分析等 。
在目标检测中,NMS的目的就是要去除冗余的检测框,保留最好的一个,如下图所示:
NMS的原理是对于预测框的列表B及其对应的置信度S,选择具有最大score的检测框M,将其从B集合中移除并加入到最终的检测结果D中.通常将B中剩余检测框中与M的IoU大于阈值Nt的框从B中移除.重复这个过程,直到B为空。
使用流程如下图所示:
- 首先是检测出一系列的检测框
- 将检测框按照类别进行分类
- 对同一类别的检测框应用NMS获取最终的检测结果
通过一个例子看些NMS的使用方法,假设定位车辆,算法就找出了一系列的矩形框,我们需要判别哪些矩形框是没用的,需要使用NMS的方法来实现。
假设现在检测窗口有:A、B、C、D、E 5个候选框,接下来进行迭代计算:
- 第一轮:因为B是得分最高的,与B的IoU>0.5删除。A,CDE中现在与B计算IoU,DE结果>0.5,剔除DE,B作为一个预测结果,有个检测框留下B,放入集合
- 第二轮:A的得分最高,与A计算IoU,C的结果>0.5,剔除C,A作为一个结果
最终结果为在这个5个中检测出了两个目标为A和B。
单类别的NMS的实现方法如下所示:
import numpy as np
def nms(bboxes, confidence_score, threshold):
"""非极大抑制过程
:param bboxes: 同类别候选框坐标
:param confidence: 同类别候选框分数
:param threshold: iou阈值
:return:
"""
# 1、传入无候选框返回空
if len(bboxes) == 0:
return [], []
# 强转数组
bboxes = np.array(bboxes)
score = np.array(confidence_score)
# 取出n个的极坐标点
x1 = bboxes[:, 0]
y1 = bboxes[:, 1]
x2 = bboxes[:, 2]
y2 = bboxes[:, 3]
# 2、对候选框进行NMS筛选
# 返回的框坐标和分数
picked_boxes = []
picked_score = []
# 对置信度进行排序, 获取排序后的下标序号, argsort默认从小到大排序
order = np.argsort(score)
areas = (x2 - x1) * (y2 - y1)
while order.size > 0:
# 将当前置信度最大的框加入返回值列表中
index = order[-1]
#保留该类剩余box中得分最高的一个
picked_boxes.append(bboxes[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]])
# 计算相交的面积,不重叠时面积为0
w = np.maximum(0.0, x22 - x11)
h = np.maximum(0.0, y22 - y11)
intersection = w * h
# 利用相交的面积和两个框自身的面积计算框的交并比
ratio = intersection / (areas[index] + areas[order[:-1]] - intersection)
# 保留IoU小于阈值的box
keep_boxes_indics = np.where(ratio < threshold)
# 保留剩余的框
order = order[keep_boxes_indics]
# 返回NMS后的框及分类结果
return picked_boxes, picked_score
假设有检测结果如下:
bounding = [(187, 82, 337, 317), (150, 67, 305, 282), (246, 121, 368, 304)]
confidence_score = [0.9, 0.65, 0.8]
threshold = 0.3
picked_boxes, picked_score = nms(bounding, confidence_score, threshold)
print('阈值threshold为:', threshold)
print('NMS后得到的bbox是:', picked_boxes)
print('NMS后得到的bbox的confidences是:', picked_score)
返回结果:
阈值threshold为: 0.3
NMS后得到的bbox是: [array([187, 82, 337, 317])]
NMS后得到的bbox的confidences是: [0.9]
5.目标检测方法分类
目标检测算法主要分为two-stage(两阶段)和one-stage(单阶段)两类:
- two-stage的算法
先由算法生成一系列作为样本的候选框,再通过卷积神经网络进行样本分类。如下图所示,主要通过一个卷积神经网络来完成目标检测过程,其提取的是CNN卷积特征,进行候选区域的筛选和目标检测两部分。网络的准确度高、速度相对较慢。
two-stages算法的代表是RCNN系列:R-CNN到Faster R-CNN网络
- One-stage的算法
直接通过主干网络给出目标的类别和位置信息,没有使用候选区域的筛选网路,这种算法速度快,但是精度相对Two-stage目标检测网络降低了很多。
one-stage算法的代表是: YOLO系列:YOLOv1、YOLOv2、YOLOv3、 SSD等
- 了解目标检测的任务
找出图像中所有感兴趣的目标,并确定它们的类别和位置
- 知道目标检测的常用数据集
和
- 知道目标检测算法的评价指标
IOU和mAP
- 掌握非极大值NMS算法的应用
要去除冗余的检测框,保留最好的一个
- 了解常用的目标检测算法分类
two-stage(两阶段)和one-stage(单阶段)
4.2 R-CNN系列网络
- 了解Overfeat模型的移动窗口方法
- 了解RCNN目标检测的思想
- 了解fastRCNN目标检测的思想
- 熟悉FasterRCNN目标检测的思想
- 知道anchor的思想
- 掌握RPN网络是如何进行候选区域的生成的
- 掌握ROIPooling的使用方法
- 知道fasterRCNN的训练方法
1.Overfeat模型
Overfeat方法使用滑动窗口进行目标检测,也就是使用滑动窗口和神经网络来检测目标。滑动窗口使用固定宽度和高度的矩形区域,在图像上“滑动”,并将扫描结果送入到神经网络中进行分类和回归。
例如要检测汽车,就使用下图中红色滑动窗口进行扫描,将所有的扫描结果送入网络中进行分类和回归,得到最终的汽车的检测结果。
这种方法类似一种暴力穷举的方式,会消耗大量的计算力,并且由于窗口大小问题可能会造成效果不准确。
2.RCNN模型
在CVPR 2014年中Ross Girshick提出R-CNN网络,该网络不在使用暴力穷举的方法,而是使用候选区域方法(region proposal method),创建目标检测的区域来完成目标检测的任务,R-CNN是以深度神经网络为基础的目标检测的模型 ,以R-CNN为基点,后续的Fast R-CNN、Faster R-CNN模型都延续了这种目标检测思路。
2.1 算法流程
RCNN的检测流程如下图所示:
步骤是:
- :使用选择性搜索的方法找出图片中可能存在目标的侯选区域region proposal
- :选取预训练卷积神经网网络(AlexNet)用于进行特征提取。
- :训练支持向量机(SVM)来辨别目标物体和背景。对每个类别,都要训练一个二元SVM。
- :训练一个线性回归模型,为每个辨识到的物体生成更精确的边界框
我们通过一个具体的例子来展示这个流程:
- 选择一个图片进行目标检测:
- 利用选择性搜索获取候选区域
- 将这些候选区域进行变形,若是AlexNet将图片resize成227*227后送入到CNN网络中进行特征提取。
- 将CNN网络提取的特征结果送入到SVM中进行分类:
- 用线性回归的方法预测每个目标的边界框位置
这就是整个RCNN算法的流程。
,使用语义分割的方法,它通过在像素级的标注,把颜色、边界、纹理等信息作为合并条件,多尺度的综合采样方法,划分出一系列的区域,这些区域要远远少于传统的滑动窗口的穷举法产生的候选区域。
SelectiveSearch在一张图片上提取出来约2000个侯选区域,。 而使用CNN提取候选区域的特征向量,需要接受固定长度的输入,所以需要对候选区域做一些尺寸上的修改。
2.2 算法总结
1、训练阶段多:步骤繁琐: 微调网络+训练SVM+训练边框回归器。
2、训练耗时:占用磁盘空间大:5000张图像产生几百G的特征文件。
3、处理速度慢: 使用GPU, 。
4、图片形状变化:候选区域要经过crop/warp进行固定大小,无法保证图片不变形
3. Fast RCNN模型
考虑到R-CNN速度很慢, 提出了一个改善模型:Fast R-CNN。 相比R-CNN, Fast R-CNN的优点在于加快了selective search的步骤和同时训练分类和回归过程, 从整体上加快了速度。
Fast R-CNN对R-CNN的改进部分:
- 将R-CNN中三个模块(CNN, SVM, Regression)整合, 极大了减少了计算量和加快了速度
- 不对原始图像进行selective search提取, 而是先经过一次CNN, 在feature map上使用selective search生成候选区域进行映射, 并进行分类回归
- 为了兼容不同图片尺度, 使用了ROI Pooling 算法, 将特征图池化到固定维度的特征向量。
fastRCNN的工作流程描述如下:
- 输入图像:
- 图像被送入到卷积网络进行特征提取,将通过选择性搜索获取的候选区域映射到特征图中:
- 在特征图上Rol中应用RoIPooling,获取尺寸相同的特征向量
- 将这些区域传递到全连接的网络中进行分类和回归,得到目标检测的结果。
4.FasterRCNN模型
在R-CNN和Fast RCNN的基础上,Ross B. Girshick在2016年提出了Faster RCNN,在结构上,Faster RCNN已经将特征抽取(feature extraction),proposal提取,bounding box regression(rect refine),classification都整合在了一个网络中,使得综合性能有较大提高,在检测速度方面尤为明显。接下来我们给大家详细介绍fasterRCNN网络模型。网络基本结构如下图所示:
该网络主要可分为四部分:
- :backbone由一组卷积神经网络构成,Faster RCNN首先使用一组基础的conv+relu+pooling层提取图像中的特征,获取图像的特征图featuremaps。该feature maps被共享用于后续RPN层和全连接层。
- :RPN网络用于生成候选区域region proposals。该部分通过softmax判断anchors属于positive或者negative,即是否包含目标,再利用bounding box regression修正anchors获得精确的proposals。
- : 该部分收集输入图像的feature maps和proposals,综合信息后提取proposal的特征向量,送入后续全连接层判定目标类别和确定目标位置。
- : 该部分利用特征向量计算proposal的类别,并通过bounding box regression获得检测框最终的精确位置
将上述结构展开后如下所示,下图中特征提取网络是VGG16:
从上图中可以看出,对于一副任意大小PxQ的图像:
- 首先将图像缩放至固定大小MxN,然后将MxN图像送入网络;
- 而Conv layers中包含了13个conv层+13个relu层+4个pooling层,在这里使用VGG16网络进行特征提取,将最后的全连接层舍弃。在整个Conv layers中,conv和relu层不改变输入输出大小,只有pooling层使输出长宽都变为输入的½,一共有4个池化层,所以:
一个MxN大小的矩阵经过Conv layers固定变为(M/16)x(N/16);
- RPN网络首先经过3x3卷积,再分别生成positive anchors和对应bounding box regression偏移量,然后计算出proposals;
- 而Roi Pooling层则利用proposals从feature maps中提取proposal feature送入后续全连接网络中进行分类和回归。
接下来我们就从这四个方面来详细fasterRCNN网络并结合源码分析其实现过程。
4.1backbone
backbone一般为VGG,ResNet等网络构成,主要进行特征提取,将最后的全连接层舍弃,得到特征图进行后续处理。
在源码中我们使用ResNet + FPN 结构,来提取特征。普通的 FasterRCNN 只需要将 feature_map 输入到 rpn 网络生成 proposals 即可。但是由于加入 FPN,需要将多个 feature_map 逐个输入到 rpn 网络和检测网络中:
在这里ResNet和FPN的完整结构如下图所示,RPN输入的feature map是[p2,p3,p4,p5,p6] ,而作为后续目标检测网络FastRCNN的输入则是 [p2,p3,p4,p5] 。
那网络的整体架构表示成:
接下来我们分析下相关内容及源码:
4.1.1 ResNet
源码位置:fasterRCNN/detection/models/backbones/reset.py
1.瓶颈模块
要构建resnet网络首先构建瓶颈模块如下所示:
class _Bottleneck(tf.keras.Model):
"""
瓶颈模块的实现
"""
def __init__(self, filters, block,
downsampling=False, stride=1, **kwargs):
super(_Bottleneck, self).__init__(**kwargs)
# 获取三个卷积的卷积核数量
filters1, filters2, filters3 = filters
# 卷积层命名方式
conv_name_base = 'res' + block + '_branch'
# BN层命名方式
bn_name_base = 'bn' + block + '_branch'
# 是否进行下采样
self.downsampling = downsampling
# 卷积步长
self.stride = stride
# 瓶颈模块输出的通道数
self.out_channel = filters3
# 1*1 卷积
self.conv2a = layers.Conv2D(filters1, (1, 1), strides=(stride, stride),
kernel_initializer='he_normal',
name=conv_name_base + '2a')
# BN层
self.bn2a = layers.BatchNormalization(name=bn_name_base + '2a')
# 3*3 卷积
self.conv2b = layers.Conv2D(filters2, (3, 3), padding='same',
kernel_initializer='he_normal',
name=conv_name_base + '2b')
# BN层
self.bn2b = layers.BatchNormalization(name=bn_name_base + '2b')
# 1*1卷积
self.conv2c = layers.Conv2D(filters3, (1, 1),
kernel_initializer='he_normal',
name=conv_name_base + '2c')
# BN层
self.bn2c = layers.BatchNormalization(name=bn_name_base + '2c')
# 下采样
if self.downsampling:
# 在短连接处进行下采样
self.conv_shortcut = layers.Conv2D(filters3, (1, 1), strides=(stride, stride),
kernel_initializer='he_normal',
name=conv_name_base + '1')
# BN层
self.bn_shortcut = layers.BatchNormalization(name=bn_name_base + '1')
def call(self, inputs, training=False):
"""
定义前向传播过程
:param inputs:
:param training:
:return:
"""
# 第一组卷积+BN+Relu
x = self.conv2a(inputs)
x = self.bn2a(x, training=training)
x = tf.nn.relu(x)
# 第二组卷积+BN+Relu
x = self.conv2b(x)
x = self.bn2b(x, training=training)
x = tf.nn.relu(x)
# 第三组卷积+BN
x = self.conv2c(x)
x = self.bn2c(x, training=training)
# 短连接
if self.downsampling:
shortcut = self.conv_shortcut(inputs)
shortcut = self.bn_shortcut(shortcut, training=training)
else:
shortcut = inputs
# 相加求和
x += shortcut
# 激活
x = tf.nn.relu(x)
# 最终输出
return x
2. RESNET
利用瓶颈模块构建backbone中的resNet.
class ResNet(tf.keras.Model):
"构建50或101层的resnet网络"
def __init__(self, depth, **kwargs):
super(ResNet, self).__init__(**kwargs)
# 若深度不是50或101报错
if depth not in [50, 101]:
raise AssertionError('depth must be 50 or 101.')
self.depth = depth
# padding
self.padding = layers.ZeroPadding2D((3, 3))
# 输入的卷积
self.conv1 = layers.Conv2D(64, (7, 7),
strides=(2, 2),
kernel_initializer='he_normal',
name='conv1')
# BN层
self.bn_conv1 = layers.BatchNormalization(name='bn_conv1')
# maxpooling
self.max_pool = layers.MaxPooling2D((3, 3), strides=(2, 2), padding='same')
# 第一组瓶颈模块
self.res2a = _Bottleneck([64, 64, 256], block='2a',
downsampling=True, stride=1)
self.res2b = _Bottleneck([64, 64, 256], block='2b')
self.res2c = _Bottleneck([64, 64, 256], block='2c')
# 第二组瓶颈模块:首个进行下采样
self.res3a = _Bottleneck([128, 128, 512], block='3a',
downsampling=True, stride=2)
self.res3b = _Bottleneck([128, 128, 512], block='3b')
self.res3c = _Bottleneck([128, 128, 512], block='3c')
self.res3d = _Bottleneck([128, 128, 512], block='3d')
# 第三组瓶颈模块:首个进行下采样
self.res4a = _Bottleneck([256, 256, 1024], block='4a',
downsampling=True, stride=2)
self.res4b = _Bottleneck([256, 256, 1024], block='4b')
self.res4c = _Bottleneck([256, 256, 1024], block='4c')
self.res4d = _Bottleneck([256, 256, 1024], block='4d')
self.res4e = _Bottleneck([256, 256, 1024], block='4e')
self.res4f = _Bottleneck([256, 256, 1024], block='4f')
# 若深度为101还需进行瓶颈模块的串联
if self.depth == 101:
self.res4g = _Bottleneck([256, 256, 1024], block='4g')
self.res4h = _Bottleneck([256, 256, 1024], block='4h')
self.res4i = _Bottleneck([256, 256, 1024], block='4i')
self.res4j = _Bottleneck([256, 256, 1024], block='4j')
self.res4k = _Bottleneck([256, 256, 1024], block='4k')
self.res4l = _Bottleneck([256, 256, 1024], block='4l')
self.res4m = _Bottleneck([256, 256, 1024], block='4m')
self.res4n = _Bottleneck([256, 256, 1024], block='4n')
self.res4o = _Bottleneck([256, 256, 1024], block='4o')
self.res4p = _Bottleneck([256, 256, 1024], block='4p')
self.res4q = _Bottleneck([256, 256, 1024], block='4q')
self.res4r = _Bottleneck([256, 256, 1024], block='4r')
self.res4s = _Bottleneck([256, 256, 1024], block='4s')
self.res4t = _Bottleneck([256, 256, 1024], block='4t')
self.res4u = _Bottleneck([256, 256, 1024], block='4u')
self.res4v = _Bottleneck([256, 256, 1024], block='4v')
self.res4w = _Bottleneck([256, 256, 1024], block='4w')
# 第四组瓶颈模块:首个进行下采样
self.res5a = _Bottleneck([512, 512, 2048], block='5a',
downsampling=True, stride=2)
self.res5b = _Bottleneck([512, 512, 2048], block='5b')
self.res5c = _Bottleneck([512, 512, 2048], block='5c')
# 输出通道数:C2,C3,C4,C5的输出通道数
self.out_channel = (256, 512, 1024, 2048)
def call(self, inputs, training=True):
"定义前向传播过程,每组瓶颈模块均输出结果"
x = self.padding(inputs)
x = self.conv1(x)
x = self.bn_conv1(x, training=training)
x = tf.nn.relu(x)
x = self.max_pool(x)
# 第1组瓶颈模块:输出c2
x = self.res2a(x, training=training)
x = self.res2b(x, training=training)
C2 = x = self.res2c(x, training=training)
# 第2组瓶颈模块:输出c3
x = self.res3a(x, training=training)
x = self.res3b(x, training=training)
x = self.res3c(x, training=training)
C3 = x = self.res3d(x, training=training)
# 第3组瓶颈模块:输出c4
x = self.res4a(x, training=training)
x = self.res4b(x, training=training)
x = self.res4c(x, training=training)
x = self.res4d(x, training=training)
x = self.res4e(x, training=training)
x = self.res4f(x, training=training)
if self.depth == 101:
x = self.res4g(x, training=training)
x = self.res4h(x, training=training)
x = self.res4i(x, training=training)
x = self.res4j(x, training=training)
x = self.res4k(x, training=training)
x = self.res4l(x, training=training)
x = self.res4m(x, training=training)
x = self.res4n(x, training=training)
x = self.res4o(x, training=training)
x = self.res4p(x, training=training)
x = self.res4q(x, training=training)
x = self.res4r(x, training=training)
x = self.res4s(x, training=training)
x = self.res4t(x, training=training)
x = self.res4u(x, training=training)
x = self.res4v(x, training=training)
x = self.res4w(x, training=training)
C4 = x
# 第4组瓶颈模块:输出c5
x = self.res5a(x, training=training)
x = self.res5b(x, training=training)
C5 = x = self.res5c(x, training=training)
# 返回所有的输出送入到fpn中
return (C2, C3, C4, C5)
4.1.2 fpn
FPN的作用是当前层的feature map会对未来层的feature map进行上采样,并加以利用。因为有了这样一个结构,当前的feature map就可以获得“未来”层的信息,这样的话低阶特征与高阶特征就有机融合起来了,提升检测精度。如下图所示:
整个架构中的结构如下图所示:
源码位置:fasterRCNN/detection/models/necks/fpn.py
class FPN(tf.keras.Model):
def __init__(self, out_channels=256, **kwargs):
'''
构建FPN模块:
out_channels:是输出特征图的通道数
'''
super(FPN, self).__init__(**kwargs)
# 输出通道数
self.out_channels = out_channels
# 使用1*1卷积对每个输入的特征图进行通道数调整
self.fpn_c2p2 = layers.Conv2D(out_channels, (1, 1),
kernel_initializer='he_normal', name='fpn_c2p2')
self.fpn_c3p3 = layers.Conv2D(out_channels, (1, 1),
kernel_initializer='he_normal', name='fpn_c3p3')
self.fpn_c4p4 = layers.Conv2D(out_channels, (1, 1),
kernel_initializer='he_normal', name='fpn_c4p4')
self.fpn_c5p5 = layers.Conv2D(out_channels, (1, 1),
kernel_initializer='he_normal', name='fpn_c5p5')
# 对深层的特征图进行上采样,使其与前一层的大小相同
self.fpn_p3upsampled = layers.UpSampling2D(size=(2, 2), name='fpn_p3upsampled')
self.fpn_p4upsampled = layers.UpSampling2D(size=(2, 2), name='fpn_p4upsampled')
self.fpn_p5upsampled = layers.UpSampling2D(size=(2, 2), name='fpn_p5upsampled')
# 3*3卷积,作用于融合后的特征图中得到最终的结果
self.fpn_p2 = layers.Conv2D(out_channels, (3, 3), padding='SAME',
kernel_initializer='he_normal', name='fpn_p2')
self.fpn_p3 = layers.Conv2D(out_channels, (3, 3), padding='SAME',
kernel_initializer='he_normal', name='fpn_p3')
self.fpn_p4 = layers.Conv2D(out_channels, (3, 3), padding='SAME',
kernel_initializer='he_normal', name='fpn_p4')
self.fpn_p5 = layers.Conv2D(out_channels, (3, 3), padding='SAME',
kernel_initializer='he_normal', name='fpn_p5')
# 对上一层的特征图进行下采样得到结果
self.fpn_p6 = layers.MaxPooling2D(pool_size=(1, 1), strides=2, name='fpn_p6')
def call(self, inputs, training=True):
# 定义前向传播过程
# 获取从resnet中得到的4个特征图
C2, C3, C4, C5 = inputs
# 对这些特征图进行1*1卷积和上采样后进行融合
P5 = self.fpn_c5p5(C5)
P4 = self.fpn_c4p4(C4) + self.fpn_p5upsampled(P5)
P3 = self.fpn_c3p3(C3) + self.fpn_p4upsampled(P4)
P2 = self.fpn_c2p2(C2) + self.fpn_p3upsampled(P3)
# 对融合后的特征图进行3*3卷积,得到最终的结果
P2 = self.fpn_p2(P2)
P3 = self.fpn_p3(P3)
P4 = self.fpn_p4(P4)
P5 = self.fpn_p5(P5)
# 对p5进行下采样得到p6特征图
P6 = self.fpn_p6(P5)
# 返回最终的结果
return [P2, P3, P4, P5, P6]
4.2 RPN网络
经典的检测方法生成检测框都非常耗时,如OpenCV adaboost使用滑动窗口+图像金字塔生成检测框;或如R-CNN使用选择性搜索方法生成检测框。而Faster RCNN则抛弃了传统的滑动窗口和SS方法,直接使用RPN生成候选区域,能极大提升检测速度。
RPN网络分为两部分,一部分是通过softmax分类判断anchor中是否包含目标,另一部分用于计算对于anchors的偏移量,以获得精确的候选区域。而最后的Proposal层则负责综合含有目标的anchors和对应bbox回归偏移量获取候选区域,同时剔除太小和超出边界的候选区域。
4.2.1 anchors
anchor在目标检测中表示 ,首先预设一组不同尺度不同长宽比的固定参考框,覆盖几乎所有位置, ,anchor技术将检测问题转换为 ,不再需要多尺度遍历滑窗,真正实现了又好又快。
在fasterRCNN中框出多尺度、多种长宽比的anchors,多种尺度,每个特征图中的像素点多个框。如下图所示:
由于有 FPN 网络,所以会在多个特征图中生成anchor,假设某一个特征图大小为hxw,首先会计算这个特征相对于输入图像的下采样倍数 stride:
如下图所示:
在这里每一个特征图对应一个尺度的anchor。
源码中anchor的生成方法:fasterRCNN/detection/core/anchor/anchor_generator.py
主要方法是:
- _generate_level_anchors:通过广播的方法生成每一个特征图的anchorbox
- _generate_valid_flags:标记真实图像中的anchor
- generate_pyramid_anchors:调用上述两个方法完成图像的anchor的生成
class AnchorGenerator:
def __init__(self,
scales=(32, 64, 128, 256, 512),
ratios=(0.5, 1, 2),
feature_strides=(4, 8, 16, 32, 64)):
'''
初始化anchor
'''
# scales: 生成的anchor的尺度
self.scales = scales
# ratios: anchor的长宽比
self.ratios = ratios
# feature_strides: 因为fpn生成了五种特征图,在每一个特征图上移动一个位置相当于原图的大小
self.feature_strides = feature_strides
def generate_pyramid_anchors(self, img_metas):
'''
生成anchor
参数:
img_metas: [batch_size, 11],图像的信息,包括原始图像的大小,resize的大小和输入到网络中图像的大小
返回:
anchors: [num_anchors, (y1, x1, y2, x2)] anchor的坐标,在原图像中的坐标
valid_flags: [batch_size, num_anchors] 是否为空的标志
'''
# 获取输入到网络中图像的大小:[1216, 1216]
pad_shape = calc_batch_padded_shape(img_metas)
# 获取图像的每一个特征图的大小:[(304, 304), (152, 152), (76, 76), (38, 38), (19, 19)]
feature_shapes = [(pad_shape[0] // stride, pad_shape[1] // stride)
for stride in self.feature_strides]
# 生成每一个特征图上anchor的位置信息: [277248, 4], [69312, 4], [17328, 4], [4332, 4], [1083, 4]
anchors = [
self._generate_level_anchors(level, feature_shape)
for level, feature_shape in enumerate(feature_shapes)
]
# 将所有的anchor串联在一个列表中:[369303, 4]
anchors = tf.concat(anchors, axis=0)
# 获取图像非0位置的大小:(800, 1067)
img_shapes = calc_img_shapes(img_metas)
# 获取anchor的非零标识
valid_flags = [
self._generate_valid_flags(anchors, img_shapes[i])
for i in range(img_shapes.shape[0])
]
# 堆叠为一个一维向量
valid_flags = tf.stack(valid_flags, axis=0)
# 停止梯度计算
anchors = tf.stop_gradient(anchors)
valid_flags = tf.stop_gradient(valid_flags)
# 返回anchor和对应非零标志
return anchors, valid_flags
def _generate_valid_flags(self, anchors, img_shape):
'''
移除padding位置的anchor
参数:
anchors: [num_anchors, (y1, x1, y2, x2)] 所有的anchor
img_shape: Tuple. (height, width, channels) 非0像素点的图像的大小
返回:
valid_flags: [num_anchors] 返回非0位置的anchor
'''
# 计算所有anchor的中心点坐标:[369300]
y_center = (anchors[:, 2] + anchors[:, 0]) / 2
x_center = (anchors[:, 3] + anchors[:, 1]) / 2
# 初始化flags为全1数组:[369300]
valid_flags = tf.ones(anchors.shape[0], dtype=tf.int32)
# 初始化相同大小的全0数组
zeros = tf.zeros(anchors.shape[0], dtype=tf.int32)
# 将anchor中心点在非0区域的置为1,其他置为0
valid_flags = tf.where(y_center <= img_shape[0], valid_flags, zeros)
valid_flags = tf.where(x_center <= img_shape[1], valid_flags, zeros)
# 返回标志结果
return valid_flags
def _generate_level_anchors(self, level, feature_shape):
'''生成fpn输出的某一个特征图的anchor
参数:
feature_shape: (height, width) 特征图大小
返回:
numpy.ndarray [anchors_num, (y1, x1, y2, x2)]:生成的anchor结果
'''
# 获取对应的尺度
scale = self.scales[level]
# 获取长宽比
ratios = self.ratios
# 获取对应步长
feature_stride = self.feature_strides[level]
# 获取不同长宽比下的scale
scales, ratios = tf.meshgrid([float(scale)], ratios)
# 尺度 [32, 32, 32]
scales = tf.reshape(scales, [-1])
# 长宽比 [0.5, 1, 2]
ratios = tf.reshape(ratios, [-1])
# 获取不同宽高比情况下的H和w
# [45, 32, 22]
heights = scales / tf.sqrt(ratios)
# [22, 32, 45]
widths = scales * tf.sqrt(ratios)
# 获取生成anchor对应的位置,假设步长为4时的结果: [0, 4, ..., 1216-4]
shifts_y = tf.multiply(tf.range(feature_shape[0]), feature_stride)
shifts_x = tf.multiply(tf.range(feature_shape[1]), feature_stride)
# 类型转换
shifts_x, shifts_y = tf.cast(shifts_x, tf.float32), tf.cast(shifts_y, tf.float32)
# 获取在图像中生成anchor的位置
shifts_x, shifts_y = tf.meshgrid(shifts_x, shifts_y)
# 将宽高分别相对于x,y进行广播, 得到宽高和中心点坐标
box_widths, box_centers_x = tf.meshgrid(widths, shifts_x)
box_heights, box_centers_y = tf.meshgrid(heights, shifts_y)
# 进行reshape得到anchor的中心点坐标和宽高
box_centers = tf.reshape(tf.stack([box_centers_y, box_centers_x], axis=2), (-1, 2))
box_sizes = tf.reshape(tf.stack([box_heights, box_widths], axis=2), (-1, 2))
# 拼接成一维向量,并以左上角和右下角坐标的形式表示 [304x304, 3, 4] => [277448, 4]
boxes = tf.concat([box_centers - 0.5 * box_sizes,
box_centers + 0.5 * box_sizes], axis=1)
# 返回最终的anchorbox
return boxes
那这些anchors是如何使用的呢?对于Conv layers特征提取得到的feature maps,为每一个点都分配这k个anchors作为初始的参考框,送入到softmax和全连接层中进行分类和回归,也就是一个二分类过程,判断anchor中是否包含目标,并对anchors进行修正。
4.2.2 RPN分类
一副MxN大小的矩阵送入Faster RCNN网络后,经过backbone特征提取到RPN网络变为HxW大小的特征图。如下图所示,是RPN进行分类的网络结构:(k=9)
先做一个1x1的卷积,得到[batchsize,H,W,18]的特征图,然后进行reshape,将特征图转换为[batchsize,9xH,W,2]的特征图后,送入softmax中进行分类,得到分类结果后,再进行reshape最终得到[batchsize,H,W,18]大小的结果,18表示k=9个anchor是否包含目标的概率值。
4.2.3 RPN回归
RPN回归的结构如下图所示:(k=9)
经过该卷积输出特征图为为[1, H, W,4x9],这里相当于feature maps每个点都有9个anchors,每个anchors又都有4个用于回归的:
变换量。
该变换量预测的是anchor与真实值之间的平移量和尺度因子:
坐标变换的源码为:fasterRCNN/detection/core/bbox/transforms.py
def bbox2delta(box, gt_box, target_means, target_stds):
'''计算box到gtbox的修正值.
参数
box: [..., (y1, x1, y2, x2)] : 要修正的box
gt_box: [..., (y1, x1, y2, x2)] : GT值
target_means: [4] :均值
target_stds: [4]:方差
'''
# 转化为tensor
target_means = tf.constant(
target_means, dtype=tf.float32)
target_stds = tf.constant(
target_stds, dtype=tf.float32)
# 类型转换
box = tf.cast(box, tf.float32)
gt_box = tf.cast(gt_box, tf.float32)
# 获取box的中心点坐标和宽高
height = box[..., 2] - box[..., 0]
width = box[..., 3] - box[..., 1]
center_y = box[..., 0] + 0.5 * height
center_x = box[..., 1] + 0.5 * width
# 获取Gtbox的中心点坐标和宽高
gt_height = gt_box[..., 2] - gt_box[..., 0]
gt_width = gt_box[..., 3] - gt_box[..., 1]
gt_center_y = gt_box[..., 0] + 0.5 * gt_height
gt_center_x = gt_box[..., 1] + 0.5 * gt_width
# 计算两者之间的平移值和尺度变换
dy = (gt_center_y - center_y) / height
dx = (gt_center_x - center_x) / width
dh = tf.math.log(gt_height / height)
dw = tf.math.log(gt_width / width)
# 组成一维向量
delta = tf.stack([dy, dx, dh, dw], axis=-1)
# 标准化
delta = (delta - target_means) / target_stds
# 返回结果
return delta
RPN的分类和回归的源码如下:fasterRCNN/detection/models/rpn_heads/rpn_head.py
class RPNHead(tf.keras.Model):
"""
完成RPN网络中的相关操作
"""
def __init__(self,
anchor_scales=(32, 64, 128, 256, 512),
anchor_ratios=(0.5, 1, 2),
anchor_feature_strides=(4, 8, 16, 32, 64),
proposal_count=2000,
nms_threshold=0.7,
target_means=(0., 0., 0., 0.),
target_stds=(0.1, 0.1, 0.2, 0.2),
num_rpn_deltas=256,
positive_fraction=0.5,
pos_iou_thr=0.7,
neg_iou_thr=0.3,
**kwags):
'''
RPN网络结构,如下所示:
/ - rpn_cls 分类(1x1 conv)
输入 - rpn_conv 卷积(3x3 conv) -
\ - rpn_reg 回归(1x1 conv)
参数
anchor_scales: anchorbox的面积,相对于原图像像素的
anchor_ratios: anchorbox的长宽比
anchor_feature_strides: 生成anchor的步长,相对于原图像素的
proposal_count:RPN最后生成的候选区域的个数,经过非极大值抑制
nms_threshold: 对RPN生成的候选区域进行NMS的参数阈值
target_means: [4] Bounding box refinement mean.
target_stds: [4] Bounding box refinement standard deviation.
num_rpn_deltas: int.
positive_fraction: float.
pos_iou_thr: 与GT的IOU大于该值的anchor为正例
neg_iou_thr: 与GT的IOU小于该值的anchor为负例
'''
super(RPNHead, self).__init__(**kwags)
# 参数初始化
# RPN最后生成的候选区域的个数,经过非极大值抑制
self.proposal_count = proposal_count
# 对RPN生成的候选区域进行NMS的参数阈值
self.nms_threshold = nms_threshold
self.target_means = target_means
self.target_stds = target_stds
# 调用anchor生成器生成对应的anchor
self.generator = anchor_generator.AnchorGenerator(
scales=anchor_scales,
ratios=anchor_ratios,
feature_strides=anchor_feature_strides)
# 将anchor划分为正负样本
self.anchor_target = anchor_target.AnchorTarget(
target_means=target_means,
target_stds=target_stds,
num_rpn_deltas=num_rpn_deltas,
positive_fraction=positive_fraction,
pos_iou_thr=pos_iou_thr,
neg_iou_thr=neg_iou_thr)
# 设置RPN网络的分类和回归损失
self.rpn_class_loss = losses.rpn_class_loss
self.rpn_bbox_loss = losses.rpn_bbox_loss
# 3*3卷积
self.rpn_conv_shared = layers.Conv2D(512, (3, 3), padding='same',
kernel_initializer='he_normal',
name='rpn_conv_shared')
# 1*1卷积 分类 每一个anchor分为2类
self.rpn_class_raw = layers.Conv2D(len(anchor_ratios) * 2, (1, 1),
kernel_initializer='he_normal',
name='rpn_class_raw')
# 1*1卷积 回归 每一个anchor的回归结果
self.rpn_delta_pred = layers.Conv2D(len(anchor_ratios) * 4, (1, 1),
kernel_initializer='he_normal',
name='rpn_bbox_pred')
def call(self, inputs, training=True):
'''
定义前向传播过程
参数:
inputs: [batch_size, feat_map_height, feat_map_width, channels]
FPN输出的一个特征图
返回:
rpn_class_logits: [batch_size, num_anchors, 2] 分类结果,以logits表示
rpn_probs: [batch_size, num_anchors, 2] 分类结果,经softmax之后的概率表示形式
rpn_deltas: [batch_size, num_anchors, 4] 回归结果,anchor的位置信息
'''
# 输出结果
layer_outputs = []
# 遍历输入中的每一特征图
for feat in inputs:
# 3*3 卷积,假设特征图大小为:(1, 304, 304, 256)
shared = self.rpn_conv_shared(feat)
# 激活:(1, 304, 304, 256)
shared = tf.nn.relu(shared)
# 分类过程
# 1*1卷积:输出大小为(1, 304, 304, 6)
x = self.rpn_class_raw(shared)
# reshape:(1, 277248, 2)
rpn_class_logits = tf.reshape(x, [tf.shape(x)[0], -1, 2])
# softmax进行分类:(1, 277248, 2),一共有277248个anchor,每个anchor有2个分类结果
rpn_probs = tf.nn.softmax(rpn_class_logits)
# 回归过程
# 1*1 卷积,输出大小为(1, 304, 304, 12)
x = self.rpn_delta_pred(shared)
# reshape:(1, 277248, 4),一共有277248个anchor,每个anchor有4个位置信息
rpn_deltas = tf.reshape(x, [tf.shape(x)[0], -1, 4])
# 将网络的分类和输出结果存放在layer_outputs
layer_outputs.append([rpn_class_logits, rpn_probs, rpn_deltas])
# 每一次迭代输出结果的大小为:
"""
(1, 277248, 2) (1, 277248, 2) (1, 277248, 4)
(1, 69312, 2) (1, 69312, 2) (1, 69312, 4)
(1, 17328, 2) (1, 17328, 2) (1, 17328, 4)
(1, 4332, 2) (1, 4332, 2) (1, 4332, 4)
(1, 1083, 2) (1, 1083, 2) (1, 1083, 4)
"""
# 将输出结果转换为列表
outputs = list(zip(*layer_outputs))
# 遍历输出,将不同特征图中同一类别的输出结果串联在一起
outputs = [tf.concat(list(o), axis=1) for o in outputs]
# 获取每一种输出:5个特征图的输出大小为:(1, 369303, 2) (1, 369303, 2) (1, 369303, 4)
rpn_class_logits, rpn_probs, rpn_deltas = outputs
# 返回输出结果
return rpn_class_logits, rpn_probs, rpn_deltas
4.2.4 Proposal Layer
Proposal Layer负责综合所有[dx(A),dy(A),dw(A),dh(A)]变换量和包含目标的anchors,计算出候选区域proposal,送入后续RoI Pooling Layer。
Proposal Layer有3个输入:anchors分类器结果,对应的bbox reg的[dx(A),dy(A),dw(A),dh(A)]变换量,以及im_info;另外还有参数feat_stride,用于计算anchor的步长。
Proposal Layer 完成以下处理:
- 生成anchors,利用[dx(A),dy(A),dw(A),dh(A)]对所有的anchors做bbox regression回归
- 按照输入的positive softmax scores由大到小排序anchors,提取前pre_nms_topN(e.g. 6000)个anchors,即提取修正位置后的positive anchors
- 限定超出图像边界的positive anchors为图像边界,防止后续roi pooling时proposal超出图像边界。
- 剔除尺寸非常小的positive anchors
- 对剩余的positive anchors进行NMS(nonmaximum suppression)
- Proposal Layer的输出是对应MxN输入图像尺度的坐标值[x1, y1, x2, y2]。
到此RPN网络的工作就结束了。
该部分的源码在:fasterRCNN/detection/models/rpn_heads/rpn_head.py
def _get_proposals_single(self,
rpn_probs,
rpn_deltas,
anchors,
valid_flags,
img_shape,
with_probs):
'''
计算候选区域结果
参数:
rpn_probs: [num_anchors] anchor是目标的概率值
rpn_deltas: [num_anchors, (dy, dx, log(dh), log(dw))] 回归得到的位置信息,对anchor进行修正
anchors: [num_anchors, (y1, x1, y2, x2)] anchor的位置
valid_flags: [num_anchors] anchor属于图像位置的标记信息
img_shape: np.ndarray. [2]. (img_height, img_width) 图像的大小
with_probs: bool. 是否输出分类结果
返回
proposals: 返回候选区域的列表
若with_probs = False,则返回:[num_proposals, (y1, x1, y2, x2)]
若with_probs = True,则返回:[num_proposals, (y1, x1, y2, x2, score)]
在这里num_proposals不会大于proposal_count
'''
# 图像的高宽
H, W = img_shape
# 将anchor的标记信息转换为布尔型, int => bool
valid_flags = tf.cast(valid_flags, tf.bool)
# 将无用的anchor过滤 ,并对分类和回归结果进行处理[369303] => [215169], respectively
rpn_probs = tf.boolean_mask(rpn_probs, valid_flags)
rpn_deltas = tf.boolean_mask(rpn_deltas, valid_flags)
anchors = tf.boolean_mask(anchors, valid_flags)
# 至多6000个结果会进行后续操作 min(6000, 215169) => 6000
pre_nms_limit = min(6000, anchors.shape[0])
# 获取至多6000个分类概率最高的anchor的索引
ix = tf.nn.top_k(rpn_probs, pre_nms_limit, sorted=True).indices
# 根据得到的索引值获取对应的分类,回归和anchor [215169] => [6000]
rpn_probs = tf.gather(rpn_probs, ix)
rpn_deltas = tf.gather(rpn_deltas, ix)
anchors = tf.gather(anchors, ix)
# 利用回归得到的结果对anchor进行修正, [6000, 4]
proposals = transforms.delta2bbox(anchors, rpn_deltas,
self.target_means, self.target_stds)
# 若修正后的结果超出图像范围则进行裁剪, [6000, 4]
window = tf.constant([0., 0., H, W], dtype=tf.float32)
proposals = transforms.bbox_clip(proposals, window)
# 对坐标值进行归一化, (y1, x1, y2, x2)
proposals = proposals / tf.constant([H, W, H, W], dtype=tf.float32)
# 进行NMS,获取最终大概2000个候选区域: [2000]
indices = tf.image.non_max_suppression(
proposals, rpn_probs, self.proposal_count, self.nms_threshold)
proposals = tf.gather(proposals, indices) # [2000, 4]
# 若要返回分类结果,则获取对应的分类值进行返回
if with_probs:
proposal_probs = tf.expand_dims(tf.gather(rpn_probs, indices), axis=1)
proposals = tf.concat([proposals, proposal_probs], axis=1)
# 返回候选区域
return proposals
4.3 ROIPooling
RoI Pooling层则负责收集proposal,并计算出 feature maps的候选区域,送入后续网络。
从网络架构中可以看出Rol pooling层有2个输入:
- CNN提取的feature maps
- RPN输出的候选区域proposal boxes(大小各不相同)
RoIpooling使用最大池化将任何有效的RoI区域内的特征转换成具有pool_H×pool_W的固定空间范围的小feature map,其中pool_H和pool_W是超参数,比如设置为7x7, 它们独立于任何特定的RoI,如下图所示21·
RoI Pooling 的作用过程,如下图所示:
- 由于RPN网络输出的proposal是对应MxN尺度的,所以首先使用spatial_scale参数将其映射回特征提取后(HxW)大小的feature map尺度;
- 再将每个proposal对应的feature map区域水平分为pooled_w x pooled_h的网格;
- 对网格的每一份都进行max pooling处理。
这样处理后,即使大小不同的proposal输出结果都是pooled_w x pooled_h固定大小,实现了固定长度输出,送入后续网络中进行处理。
在实现过程中,FPN网络产生了多个尺度特征图,那候选区域要映射到哪个特征图中呢?
在这里,不同尺度的ROI使用不同特征层作为ROI pooling层的输入,大尺度ROI就用后面一些的金字塔层,比如P5;小尺度ROI就用前面一点的特征层,比如P3,我们使用下面的公式确定ROI所在的特征层:
其中,224是ImageNet的标准输入,k0是基准值,设置为4,w和h是ROI区域的长和宽,假设ROI是112x112的大小,那么k = k0-1 = 4-1 = 3,意味着该ROI应该使用P3的特征层。k值会做取整处理,防止结果不是整数,而且为了保证k值在2-5之间,还会做截断处理。
源码在:fasterRCNN/detection/models/roi_extractors/roi_align.py
class PyramidROIAlign(tf.keras.layers.Layer):
def __init__(self, pool_shape, **kwargs):
'''
在多个特征图上完成ROIPooling
参数:
pool_shape: (height, widt