Faster RCNN 代码解析

前言

本文主要是对 Faster RCNN 流程和代码进行解析
参考视频: Faster RCNN源码解析
Faster RCNN 源码:https://github.com/WZMIAOMIAO/deep-learning-for-image-processing/tree/master/pytorch_object_detection/faster_rcnn
对上面的源码添加注释:https://github.com/yaomiaoqiu/cv/tree/main/faster_rcnn

内容

Faster RCNN 结构图
先看 network_files/faster_rcnn_framework.FasterRCNN 类,我们可以看见这个类继承 FasterRCNNBase 这个类。

进入 FasterRCNN 这个类,这个类主要是用来对初始化参数进行处理,然后初始化 FasterRCNNBase 这个类。

进入 FasterRCNNBase 这个类,这个类初始化接收四个参数。第一个参数为 backbone,来自 FasterRCNN 的初始化参数;第二个参数为 rpn,是通过 RegionProposalNetwork 类构造的;第三个参数为 roi_heads,是通过 RoIHeads 类构造的;第四个参数为 transform,是通过 GeneralizedRCNNTransform 类构造的。我们看其 forward 函数,如果是训练模式,我们要检测传入的 target 是否符合规范,然后因为我们要对图片进行处理,因此要定义一个列表来保存原始图片的宽和高。调用 transform 函数对图像进行预处理。

transform 函数是通过 network_files/transform.GeneralizedRCNNTransform 类构造的。

进入 GeneralizedRCNNTransform 这个类,这个类负责对输入的数据进行处理。在此类的 forward 函数中,我们传入图片和图片的相关信息,我们调用normailze 函数对图片进行标准化处理,调用 resize 函数将图片缩放到指定的大小范围内,并对应缩放boxes信息(调用 resize_boxes 函数根据图像的缩放比例来缩放box)。为了方便我们将box还原到原图上,我们需要记录 resize 后的尺寸,然后我们将 resize 后的图片通过 batch_images 函数打包成一个 batch。batch_images 函数的具体操作为:获取一批图像中最大的宽和高,然后将所有图片都填充到最大的宽和高。最后返回 resize batch 后的图片列表和图片相关信息。

回到 FasterRCNNBase.forward 中,调用 backbone 函数得到特征图,并将特征图保存在有序字典中。

backbone 函数来自 backbone/*_model 中的模型类,就是正常的特征提取模型。

回到 FasterRCNNBase.forward 中,用 rpn 函数将特征层以及标注 target 信息传入 rpn 中生成预测的 proposal 坐标和损失。

rpn 函数是由 network_files/rpn_function.RegionProposalNetwork 这个类构建的。

进入 RegionProposalNetwork 这个类。我们依然先看 forward 函数。

首先,forward 函数有三个参数,第一个 images,我们传入的图片,类型为预处理后的图片列表;features,我们获取到的图片特征,是多个预测特征层组成的 OrderedDict;targets,包含图片的真实box。返回值为 boxes,RPN 网络预测的每张图片的 boxes;losses,模型训练过程的损失。

在函数中,首先我们获取到每个预测特征层的特征,是个列表,列表中每个元素是一个预测特征层的特征。然后,通过 head 函数(对应上图的RPNHead)计算每个预测特征层上的预测目标概率和bboxes regression参数。

现在我们将目光转向 head 函数,此函数是我们在定义类的时候传入的参数。通过分析 faster_rcnn_framework 中的 FasterRCNN 类,我们可以知道这个 head 函数是通过 rpn_function 中的 RPNHead 这个类构建的。这个类的功能是通过滑动窗口计算预测目标概率与bbox regression参数。对于特征图上的每个3x3的滑动窗口,计算出滑动窗口中心点对应原始图像上的中心点,并计算出k个anchor的预测的目标分数(这里的目标只是指前景或者背景,因为是前景和背景,所以一个有2k 个结果)和 4k 个边界框回归参数。
请添加图片描述
现在我们回到 forward 函数。我们通过 anchor_generator 函数生成一个 batch 图像所有的 anchors 信息。

我们将目光转到 anchor_generator 函数,此函数也是传入的参数,通过分析 faster_rcnn_framework 中的 FasterRCNN 类,我们可以知道这个 anchor_generator 函数是通过 rpn_function 中的 AnchorsGenerator 这个类构建的。

对于 AnchorsGenerator 类,我们初始化时传入的参数是 anchor 的尺寸和高宽比,我们会对参数进行类型判断,确保尺寸和高宽比是 Tuple[Tuple[int]] 类型。我们先看 forward 函数。在函数中,我们获取每个预测特征层的尺寸,然后获取输入图像的高宽,最后计算特征层上的一步等于原始图像上的多少步长。我们调用 set_cell_anchors 函数生成 anchors 模板。

进入到 set_cell_anchors 这个函数,我们可以发现对于每个尺寸和宽高比,我们都会调用 generate_anchors 函数生成 anchor,并将其保存。最终会生成 3*3=9 个anchors 模板。

进入到 generate_anchors 函数,通过代码的注释可以发现,我们最终获取了符合要求的 anchor 的宽高,并保存了 anchor 的坐标(anchor 的中心为坐标轴的原点)。

现在回到 forward 函数,我们调用 cached_grid_anchors 函数来获取每张预测特征图映射回原图的 anchors 坐标信息。

进入到 cached_grid_anchors 函数,在此函数中调用 grid_anchors 函数来计算预测特征图对应原始图像上的所有 anchors 的坐标。

进入到 grid_anchors 函数,对于每个预测特征层,我们将图片分为 grid_width*grid_height 份,每份的的长度为 stride_width/height ,这样我们就能将预测特征图的 1 和图像的 1 对应起来,将原图用网格用坐标表示,然后计算 anchors 坐标在原图的坐标偏移量。因为我们要将 anchors 放在原图的每个点上,并且每个点的位置不同,因此我们要记录每个点anchors的偏移量。将 anchors 模板与原图上的坐标偏移量相加得到原图上所有 anchors 的坐标信息。最终会返回 anchors(shape: [all_num_anchors, 4])。下图更好解释。
请添加图片描述
现在回到 forward 函数,对于一个 batch 中的每张图片,我们保存每张图片所有预测特征图映射回原图的 anchors 坐标信息。最终 forward 函数将会返回一个列表,列表中的每个元素都是 batch 中一张图片的所有 anchor 在图片上坐标集合。形状为 [batch_size, all_num_anchors, 4]。

现在回到 RegionProposalNetwork 类的 forward 函数中,记录每个预测特征层上的预测目标张量的形状即 [anchor_num_per_ceil, height, width],然后计算每个预测特征层产生的 anchor 数目。调用 concat_box_prediction_layers 函数调整预测目标概率和bboxes regression 参数的形状。

进入到 concat_box_prediction_layers 函数。对于每个预测特征层,预测目标概率的形状为 [batch_size, anchors_num_per_position * classes_num, height, width],其中 classes_num =1 ,因为只有前景和背景;bboxes regression 参数的形状为 [batch_size, anchors_num_per_position * 4, height, width]。将预测目标概率的形状变为 [batch_size, -1, classes_num];将 bboxes regression 参数的形状变为 [batch_size, -1, 4],这样我们可以知道 -1 代表的是 anchor 的数目。最后再将多个预测特征层上的anchor进行拼接,然后展平,展平后 batch 和 anchor结合在一起形成 [batch * anchor, C]。

现在回到 forward 函数,调用 box_coder.decode 函数将预测的bbox regression参数应用到 anchors 上得到最终预测 bbox 坐标。

解释一下最终预测 bbox 坐标的获取。由前面 3*3 移动窗口进行 anchor 预测分数和 bbox regression 参数的生成过程可以发现,它们是相互对应的,即使经过一些操作,但它们的对应关系并没有变。

box_coder 是由 network_files/det_utils 的 BoxCoder 类构建的,decode 函数是类中的函数。

现在进入 decode 函数,此函数有两个参数,一个是 rel_codes,表示 bbox regression 参数,其形状为 [batch*anchor, 4];另一个是 boxes,表示 anchors,其形状为 [batch_size, all_num_anchors, 4]。我们记录每张图片的 anchors 数目,将所有 batch 的 anchor 拼接在一起,获取这一批图片的总 anchor 数。我们通过 decode_singel 函数来将预测的 bbox regression 参数应用到对应的 anchors 上来得到预测的 bbox 的坐标。

进入 decode_single 函数,根据 anchor 信息和预测的边界框回归参数来获取最终的 proposal 参数(PPT公式),然后将获取到的 proposal 中心点坐标,宽高转换为左上角和右下角坐标的形式。最终返回获取到的 proposal 坐标([anchors, 4])。
请添加图片描述
回到 decode 函数,我们将获取到的预测 box(proposal) 坐标形状变为 [anchors, 1, 4]。

回到 forward 函数,我们将我们获取到的 proposal 的形状变为 [num_images, anchors/num_images, 4]。然后调用 filter_proposals 函数来筛除小 boxes 框,nms 处理,根据预测概率获取前 post_nms_top_n 个目标。

进入 filter_proposals 函数,此函数有 4 个参数。第一个为 proposals,为我们预测的 bbox 坐标,形状为 [num_images, anchors/num_images, 4];第二个为 objectness,为预测的目标概率,形状为 [batch * anchor, C];第三个为 image_shapes,为每张图片的 size 信息,形状为 [batch, h, w];第四个为 num_anchors_per_level,为每个预测特征层的 anchors 数量,形状为 [num_anchors]。我们获取图片的数目,不进行 objectness 的反向传播,将 objectness 的形状变为 [num_image, batch * all_anchor * C / num_image]。接着,我们用 levels 记录不同预测特征层的 anchors 索引信息(相当于用不同的数表示不同预测特征层的 anchors),这样我们就可以知道哪些 anchors 是属于哪些预测特征层,levels 的形状为 [level, num_anchor_per_level]。(为什么我们要有这个 level?因为 proposal 中我们将所有预测特征层中的 anchor 拼接在一起了,我们无法区分哪个 anchor 是属于哪个 level)。然后我们将 levels 在 0 维度拼接,形状变为 [sum(level * num_anchors_per_level)]。我们将 levels 的形状变成和 objectness 相同,形状为 [num_image, sum(level * num_anchors_per_level)](此时 sum(level * num_anchors_per_level) = batch * all_anchor * C / num_image)。我们通过 _get_top_n_idx 函数来获取每张预测特征图上预测概率排前 pre_nms_top_n 的 anchors 的索引值。

进入到 _get_top_n_idx 函数,将 objectness 在 1 维分割(torch.split),分割长度为num_anchors_per_level,这样分割后的形状为 [level, num_image, num_anchors_per_level],因此 ob 的形状为 [num_image, num_anchors_per_level]。我们获取到每个预测特征层上 anchor 的数目,然后将 pre_nms_top_n 和 num_anchors 相比取小的(训练时,pre_nms_top_n=2000,预测时为 1000)。我们用 topk 函数获取到预测目标概率前 pre_nms_top_n 的索引,要注意我们获取到的索引都是从 0 开始的,因此我们要将偏移量加入到索引中,这样才能获取到每个 anchor 实际对应的索引,偏移量为每一层的 anchor 数目。最终的返回值为每张预测特征图上预测概率排前 pre_nms_top_n 的 anchors 的索引值,形状为 [level, num_image, pre_nms_top_n_anchors] —> [num_image, sum(pre_nms_top_n)]。

现在回到 filter_proposals 函数,我们构建一个 batch_idx 张量(形状 [num_image, 1])来表示一批图片的索引。根据每张预测特征图预测概率排前pre_nms_top_n 的 anchors 索引值获取相应概率信息([batch_size, sum(pre_nms_top_n)]),获取到每张图片上前 pre_nms_top_n 的 anchors 索引值对应的预测特征层的 mask ([batch_size, sum(pre_nms_top_n)]),获取测概率排前pre_nms_top_n 的 anchors 索引值获取相应 bbox 坐标信息([batch_size, sum(pre_nms_top_n)])。接着,我们遍历每张图像的相关预测信息,调用 box_ops.clip_boxes_to_image 函数将 proposal 中越界的坐标调整到图片的边界上。

进入 network_files/boxes.clip_boxes_to_image 函数,没啥好说的,代码很清晰。

回到 filter_proposals 函数,调用 box_ops.remove_small_boxes 函数返回 proposal 满足宽高都大于 min_size 的索引。

进入 network_files/boxes.remove_small_boxes 函数,调用函数返回满足条件的索引。torch.where 取Tensor中符合条件的坐标。

回到 filter_proposals 函数,我们保留满足条件的预测信息。获取分数高于我们设定阈值的 proposal 的索引,保留这些预测信息。然后调用 box_ops.batched_nms 函数返回进行nms处理后,按照scores排序好的索引。

进入 network_files/boxes.batched_nms 函数,我们获取到所有 proposal 中最大的坐标值,我们让每个 idxs(level mask,一般是这个样子:[0…01…12…2])乘上最大坐标值,这个操作为每一个类别/每一层生成一个很大的偏移量,proposal 加上对应层的偏移量后,保证不同类别/层之间 proposal 不会有重合的现象(为什么要这样做?这样做在进行 nms 处理时就可以对不同的预测特征层的 proposal 进行 nms 处理,只需要执行 1 次,不需要针对每一层进行处理)。最后,调用 nms 函数获取满足条件的 proposal 的索引(按照 scores 递减)。

回到 filter_proposals 函数,我们只保存前 post_nms_top_n (训练时为2000,预测时为1000)个索引,然后通过切片的方式获取最终的满足条件的 proposal 和 scores。

回到 RegionProposalNetwork.forward 函数,我们获取到了筛除小boxes框,nms处理,根据预测概率的前 post_nms_top_n 个 proposal 和目标分数。下面我们将调用 assign_targets_to_anchors 计算每个 anchors 最匹配的gt,并划分为正样本,背景以及废弃的样本。

进入到 assign_target_to_anchors 函数,我们传入两个参数。一个参数是 anchors,是我们前面生成的一个 batch 图像的所有 anchors,形状为 [batch_size, all_num_anchors, 4];另一个参数为 targets,为 list(Dict(Tensor)),list 每个元素是一张图片,Dict 中包含多个信息,有 {'boxes':Tensor, 'labels':Tensor, 'image_id': Tensor, 'area': Tensor, 'iscrowd': Tensor} 。我们遍历每张图片的 anchors 和 targets,首先获取到 gt_boxes(rpn 只区分前景和背景,因此不需要类别,只需要 box 的坐标),然后调用 box_ops.box_iou 计算 anchors 和真实 box 的 iou 信息。

进入 network_files/boxes.box_iou 函数,每个区域都是 (x1, y1, x2, y2)的形式,计算交并比就是将两个区域相交的面积除以两个区域相并的面积。假设两个区域的坐标为 [ x a 1 , y a 1 , x a 2 , y a 2 ] , [ x b 1 , y b 1 , x b 2 , y b 2 ] [x_{a1},y_{a1},x_{a2},y_{a2}],[x_{b1},y_{b1},x_{b2},y_{b2}] [xa1,ya1,xa2,ya2],[xb1,yb1,xb2,yb2],那么它们相交部分的左上角坐标为 x 1 = m a x ( x a 1 , x b 1 ) , y 1 = m a x ( y a 1 , y b 1 ) x_1=max(x_{a1},x_{b1}),y_1=max(y_{a1},y_{b1}) x1=max(xa1,xb1),y1=max(ya1,yb1),右下角坐标为 x 2 = m i n ( x a 2 , x b 2 ) , y 2 = m i n ( y a 2 , y b 2 ) x_2=min(x_{a2},x_{b2}),y_2=min(y_{a2},y_{b2}) x2=min(xa2,xb2),y2=min(ya2,yb2),这样交部分的面积为 m a x ( 0 , x 2 − x 1 ) ∗ m a x ( 0 , y 2 − y 1 ) max(0,x_2-x_1)*max(0,y_2-y_1) max(0,x2x1)max(0,y2y1)

img
因为两个 box 的数目不一样,因此我们要采用广播机制进行计算,通过代码注释可以发现最终返回的 IOU 是一个 N*M 的矩阵,矩阵的每个元素都是交并比,N 是 gt_box 的数目,M 是 anchor 的数目。

回到 assign_targets_to_anchors 函数,调用 proposal_matcher 函数计算每个anchors 与 gt 匹配 iou 最大的索引(如果iou<0.3索引置为-1,0.3<iou<0.7索引为-2)。

proposal_matchernetwork_files/det_utils.Matcher 类构建的,我们看此类的 _call_ 函数,此函数接收一个 iou 矩阵([gt_box 数目,anchor 数目])。我们获取每个 anchor 与所有 gt 的最大 iou 值 matched_vals 以及对应的索引 matches。从这些索引中找出 iou 小于 low_threshold 的索引并将其置为 -1,找出 iou 在 low_threshold 与 high_threshold 之间的索引,将其置为 -2。判断是否采用正负样本采样中的第一条规则,如果采用,则调用 set_low_quality_matches_ 函数来防止有的 gt 没有被 anchor 所匹配到,我们将每个 gt-anchor 的最大 IOU 对应的 anchor 也记为正样本。
请添加图片描述
进入 set_low_quality_matches_ 函数,我们为每个 gt_box 寻找与其 iou 最大的 anchor,然后寻找每个 gt_box 与其 iou 最大的 anchor 索引,一个 gt 匹配到的最大 iou 可能有多个 anchor。记录每个 gt 对应的最大 IOU 的 anchor 的索引,在 matches 中保留该 anchor 匹配 gt 最大 iou 的索引,即使 iou 低于设定的阈值。
请添加图片描述
回到 network_files/det_utils.Matcher._call_ 函数,这样返回的 matches 就记录了每个 anchors 对应的 iou 最大的 gt_box 的索引。

回到 assign_targets_to_anchors 函数,我们通过切片的方式就可以获取到每个 anchor 对应的 gt_box。在 matches 中,索引 >=0 的为正样本,索引为 -1 的为负样本,索引为 -2 的为丢弃样本。根据上述样本分类在 labels_per_image 中记录所有 anchors 匹配后的标签(正样本处标记为1,负样本处标记为0,丢弃样本处标记为-1),最终返回labels(形状为 [batch_size, num_anchors],里面内容为 1,0,-1)和匹配好的 gt_box(形状为 [batch_size, num_anchors, 4])。

回到 RegionProposalNetwork.forward 函数,通过 box_coder.encode 函数来计算 regression 参数。

进入 network_files/det_utils.BoxCoder.encode 函数,该函数有两个参数,一个是 reference_boxes,为 anchor 对应的 gt_box;另一个是 proposals,为 anchors;这两个参数之间是对应的。统计每张图像的 anchors 个数,然后分别将两个 box 拼接(就是将多张图片的 anchors 拼接在一起,方便处理),再利用 encode_single 函数计算 gt 的回归参数。

进入 encode_single 函数,获取数据的设备和类型,然后构建一个权重张量,再调用 encode_boxes 函数来计算 gt 回归参数。

进入 encode_boxes 函数,我们获取了 权重、proposals(感觉叫 anchor 更好些)、gt 的坐标,并计算了 proposals 和 gt 的回归参数,然后利用公式计算量 gt 相对于 proposals 的回归参数(回归偏移量),最终返回回归偏移量。
请添加图片描述
回到 encode_single 函数,返回回归偏移量。

回到 encode 函数,将回归偏移量按每张图片的 box 数分开,由 [image_size * anchors, 4] —> [image_size, anchors, 4] (差不多是这个意思),返回回归偏移量。

回到 RegionProposalNetwork.forward 函数,调用 compute_loss 函数计算 RPN 网络的分类损失和边界框回归损失。

进入 compute_loss 函数,函数接收四个参数。调用 fg_bg_sampler 函数来选择正负样本,我们并不使用所有的正负样本。

fg_bg_sampler 是由 det_utils.BalancedPositiveNegativeSampler 构建的。

进入 network_files/det_utils.BalancedPositiveNegativeSampler 类,我们看它的 _call_ 函数,它接收的参数为 compute_loss 函数的参数 labels(matched_idxs)。遍历每张图片的 matched_idxs,获取正负样本的索引,指定正负样本的数量(给定总数量和正样本占比),随机选择指定数量的正负样本,构建正负样本的 mask,记录每张图片的正负样本 mask。返回 mask。

回到 compute_loss 函数,获取到指定数目的正负样本索引 mask 后,将一个 batch 内的所有正负样本 mask 分别拼接在一起,然后获取到 mask 值(两种取值 0 或 1)不为 0 的索引,这就对应着选定的正负样本的索引。将所有正负样本索引拼接在一起形成 sampled_inds,将预测前景概率 objectness 展开,将 labels 和 regression_targets 进行拼接,这样最终的形状分别为 sampled_inds([pos+neg]), objectness ([num_anchors]),labels([num_anchors]),regression_targets([num_anchors, 4])。调用 det_utils.smooth_l1_loss 函数计算边界框回归损失。

进入 network_files/det_utils.smooth_l1_loss 函数,它接收的参数为正样本 anchor 的预测回归参数,正样本 anchor 对应的 gt 的回归参数。

回到 compute_loss 函数,调用 F.binary_cross_entropy_with_logits 函数来计算目标预测概率损失,函数接收两个参数,一个是所有选定的正负样本的目标预测分数,另一个是所有选定的正负样本的真实分数(正样本为 1,负样本为 0)。返回两个损失。

回到 forward 函数,记录这两个损失。最终 forward 函数返回预测的 proposal 和损失。

回到 FasterRCNNBase.forward 中,调用 roi_heads 函数将 rpn 生成的数据以及标注 target 信息传入 fast rcnn 后半部分生成最终的 box 信息和损失。

roi_heads 函数来自 network_files/roi_head.RoIHeads 这个类。ROI_Head(将 roi pooling,box_head 以及 box_predictor 结合在一起)

进入 network_files/roi_head.RoIHeads 类,我们看它的 forward 函数。检查 targets 的数据类型是否正确,如果是训练模式,那么我们调用 select_training_samples 函数来划分正负样本,统计对应gt的标签以及边界框回归信息。

进入 select_training_samples 函数,函数接收两个参数,一个是 rpn 网络预测的 proposal,另一个是真实目标标注信息。从真实目标标注信息中获取标注好的 gt_boxes 和 gt_labels 信息,调用 add_gt_proposals 函数将 gt_boxes 拼接到 proposal 后面。

进入 add_gt_proposals 函数,就是使用 cat 函数将每张图片预测的 proposals 和 gt_boxes 拼接在一起。返回拼接后的 proposal。

回到 select_training_samples 函数,调用 assign_targets_to_proposals 为每个 proposal 匹配对应的 gt_boxes,并划分正负样本。

进入 assign_targets_to_proposals 函数,遍历每张图像的 proposals, gt_boxes, gt_labels 信息,和 assign_targets_to_anchors 一样,先调用 box_ops.box_iou 函数计算 proposal 与每个 gt_box 的 iou,然后调用 proposal_matcher 函数计算 proposal 与每个 gt_box 匹配的 iou 最大值,并记录索引 matched_idxs_in_image(此列表中每个元素的索引对应 proposal)。然后将 matched_idxs_in_image 中值为 -1,-2 调整到 0,防止出现匹配标签越界的情况。利用处理后的索引获取 proposal 匹配到的 gt 对应的标签。将 matched_idxs_in_image 中值为 -1 的索引对应的 proposal 设置为负样本,即将这个 proposal 对应的 gt 标签设置为 0。将值为 -2 的索引对应的 proposal 设置为丢弃样本,即将这个 proposal 对应的 gt 标签设置为 -1。返回处理好的 proposal 与 gt 对应的索引(类似于 [1,3,-1,-2,2,3,1,-2,-1,0,…],其中大于等于0的元素表示 gt 的索引)和 proposal 匹配到的 gt 对应标签(类似于 [1,2,-1,2,1,0,3,…],其中大于0的元素表示 gt 对应的标签)。看代码比较清晰。

回到 select_training_samples 函数,调用 subsample 函数按给定的数量和比例采样正负样本。

进入 subsample 函数,接收 lables 作为参数,调用 fg_bg_sampler 函数来选择正负样本(前面已经说过了),遍历每张图片的正负样本索引,记录每张图片所有采集的样本的索引。返回每张图片所有采集的样本的索引。

回到 select_training_samples 函数,遍历每张图像,获取每张图像的正负样本索引,利用正负样本索引获取到正负样本的 proposals 信息并修改 proposals,获取对应正负样本的真实类别信息并修改 labels,获取对应正负样本的 gt 索引信息并修改 matched_idxs,获取图片的 gt_box 信息,根据图片的 gt_box 信息和正负样本对应的 gt 索引信息获取对应正负样本的gt box 信息,并将其保存在 matched_gt_boxes 中。调用 box_coder.encode 函数(前面说过了)根据 gt 和 proposal 计算边框回归参数(针对 gt 的)。最终返回只包含我们选取的正负样本的 proposals(一张图片选了 512 个 proposal),对应的 labels 和边框回归参数。

回到 RoIHeads.forward 函数,调用 box_roi_pool 函数将 proposal 变为 7*7 尺寸。这个函数对应架构图中的 ROIpooling。

box_roi_pool 函数是由 MultiScaleRoIAlign 类构建的,而 MultiScaleRoIAlign 类来自于 torchvision.ops。使用这个类构建的函数可以使我们的结果更准确,定位效果更好,roi pooling 会进行多次取整操作导致定位误差。其底层代码解析网上有。这部分应该包含两部分,一部分是将 rpn 生成的 proposal 映射到特征图上,另一部分就是将特征图上的 proposal 尺寸变为 7*7。FasterRCNN系列之ROIAlign - 知乎 (zhihu.com)

回到 RoIHeads.forward 函数,调用 box_head 函数实现展平和两个全连接层。

box_head 函数是由 faster_rcnn_framework.TwoMLPHead 类构造的,其对应图中的 Two MLPHead 部分。

进入 faster_rcnn_framework.TwoMLPHead 类,类初始化时传入两个参数,一个是输入的 channel 数,它等于 backbone 提取特征的 channel*7*7,另一个为 representation_size,设置为 1024,此类的 forward 函数就是将 ROIpooling 后的 proposal 特征矩阵(其形状为 [batch_proposals_num, channel, height, weight],此代码中为 [1024, 256, 7, 7]。1024 为一个 batch 的 proposal 数目,1024=2*512,2 是一个 batch 的大小,512 是我们在生成的 2000 个 proposal 中选取 512 个 proposal 进行训练;256 是指每个 proposal 经过 roi_align 后的 channel)先展平([1024, 256*7*7])然后通过两个全连接层,最终变为 [1024, 1024]。

回到 RoIHeads.forward 函数,调用 box_predictor 函数来预测目标类别和边界框回归参数。

box_predictor 函数是由 faster_rcnn_framework.FastRCNNPredictor 类构建的,对应于图中的 FastRCNNPredictor 部分。

进入 faster_rcnn_framework.FastRCNNPredictor 类,我们可以看到其由两个全连接层组成,一个用于预测 proposal 的目标分数(输入节点个数为 TwoMLPHead 的最终输出节点个数 1024,输出节点个数为 目标类别数(包含背景));另一个用于预测定位回归参数(输入节点个数为 TwoMLPHead 的最终输出节点个数 1024,输出节点个数为目标类别数 * 4)。看其 forward 函数,先展平(这步没有作用,数据已经展平了),然后分别进行全连接层操作,最终返回预测的 proposal 目标分数(形状为 [batch_proposals_num,num_class])和预测的定位回归参数(形状为 [batch_proposals_num, num_class * 4])。

回到 RoIHeads.forward 函数,我们要计算损失,如果是训练过程,我们就调用 fastrcnn_loss 函数来计算损失。

进入 fastrcnn_loss 函数,函数参数在代码中由介绍,我们将真实类别信息 labels 和真实目标边界框信息 regression_targets 进行拼接(将一个 batch 内的图片的信息拼接到一起,变成 [1024],[1024, 4] ),调用 F.cross_entropy 函数计算类别损失。我们获取标签类别大于 0 (正样本)的索引并且取出 labels 中值大于 0 的部分(正样本对应的标签)。我们对 box_regression (预测边目标界框回归信息)进行 reshape 处理,将其形状变为 [batch_proposals_num, num_class * 4] —> [batch_proposals_num, num_class, 4]。我们在预测时是对每一个类别都预测一个边界框回归参数,但在计算损失时,我们只需要 proposal 对应真实类别的预测边界框回归参数。目标边界框损失我们只计算正样本的损失。我们使用 det_utils.smooth_l1_loss 函数来计算边界框损失信息,函数的第一个参数是box_regression 中正样本对应的真实类别的预测边界框回归参数。(box_regression 中有多个 proposal,每个proposal 有多个类别的预测边界框回归参数,sampled_pos_inds_subset 是选择 proposal 中的正样本,labels_pos 是正样本对应的真实类别)。第二个参数是真实目标边界框信息中正样本的边界框回归参数。

进入 network_files/det_utils.smooth_l1_loss 函数,函数有四个参数,第一个参数是预测的内容,第二个参数是真实的内容。对于从 fastrcnn_loss 传进来的参数,第一个参数的形状为 [预测 proposal 中正样本数目,4(proposal 的回归参数)];第二个参数的形状为 [真实边界框正样本数目,4]。使用下面公式计算边界框回归损失,不同的是我们引入了一个 beta,并且公式也有部分修改,代码中的 n 就相当于公式中的 |x|,beta 相当于 判断条件中的 1。
请添加图片描述
回到 fastrcnn_loss 函数,返回分类损失和边界框回归损失。

回到 RoIHeads.forward 函数,将损失保存。如果是预测过程,我们不会进行正负样本的划分,我们会使用 rpn 网络给我们提供的 1000 个 proposal,我们会调用 postprocess_detections 函数对网络的预测数据进行后处理。

进入 postprocess_detections 函数,获取到每张图片的预测 proposal 数量,调用 box_coder.decode 函数(前面说过了)根据 proposal 以及预测的回归参数计算最终的 box 坐标。对预测的类别结果进行 softmax 处理。根据每张图片的 proposal 数目分割预测分数和预测 proposal。遍历每张图像预测信息,裁剪预测的 proposal 信息,将越界的坐标调整到图片边界上,为每个类别创建一个类别标签,将其形状变为和预测分数一样,移除索引为 0 的所有信息(0代表背景),重构预测信息形状,移除低概率目标,移除小目标,执行 nms 处理,执行后的结果会按照 scores 从大到小进行排序返回,获取 scores 排在前 topk 个预测目标,保存每张图片的预测信息。(看 postprocess_detections.ppt)

回到 RoIHeads.forward 函数,将每张图片预测的 box 坐标,box 的类别,box 的得分记录。返回每张图片的预测结果和损失。

回到 FasterRCNNBase.forward 函数,当我们调用完 roi_heads 函数后,我们会调用 postprocess 函数对网络的预测结果进行后处理(主要将bboxes还原到原图像尺度上)。

postprocess 函数来自 network_files/transform.GeneralizedRCNNTransform 这个类。

进入 GeneralizedRCNNTransform.postprocess 函数,如果是训练阶段,直接返回结果就行;否则,遍历每张图片的预测信息,调用 resize_boxes 函数将预测的 box 缩放到原图像尺度上,然后将结果中的 box 用缩放后的 box 替代,最终返回包含缩放后的 box 的预测结果。

回到 FasterRCNNBase.forward 函数。保存两个损失损失,并将损失或最终结果通过 eager_outputs 函数返回。

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值