MaskRCNN源码解析3:RPN、ProposalLayer、DetectionTargetLayer

MaskRCNN源码解析1:整体结构概述

MaskRCNN源码解析2:特征图与anchors生成

MaskRCNN源码解析3:RPN、ProposalLayer、DetectionTargetLayer

MaskRCNN源码解析4-0:ROI Pooling 与 ROI Align理论

MaskRCNN源码解析4:头网络(Networks Heads)解析

MaskRCNN源码解析5:损失部分解析

 

目录

 MaskRCNN概述:

B),RPN与anchors筛选

1,RPN

2,ProposalLayer

3,DetectionTargetLayer

 MaskRCNN概述:

       Mask R-CNN是一个小巧、灵活的通用对象实例分割框架(object instance segmentation)。它不仅可对图像中的目标进行检测,还可以对每一个目标给出一个高质量的分割结果。它在Faster R-CNN[1]基础之上进行扩展,并行地在bounding box recognition分支上添加一个用于预测目标掩模(object mask)的新分支。该网络还很容易扩展到其他任务中,比如估计人的姿势,也就是关键点识别(person keypoint detection)。该框架在COCO的一些列挑战任务重都取得了最好的结果,包括实例分割(instance segmentation)、候选框目标检测(bounding-box object detection)和人关键点检测(person keypoint detection)。

参考文章:

Mask RCNN 学习笔记

MaskRCNN源码解读

令人拍案称奇的Mask RCNN

论文笔记:Mask R-CNN

Mask R-CNN个人理解

解析源码地址:

https://github.com/matterport/Mask_RCNN

B),RPN与anchors筛选

1,RPN

RPN网络在代码中是先通过建立rpn_model,然后再遍历特征图,得到以下变量:

# rpn_class_logits:[batch_size,H * W * anchors_per_location,2] anchors分类器logits(在softmax之前)
# rpn_probs:[batch_size,H * W * anchors_per_location,2] anchors分类器概率。
# rpn_bbox:[batch_size,H * W * anchors_per_location,(dy,dx,log(dh),log(dw))]  anchors的坐标偏移量

整体RPN代码如下:

       # *************************4,生成RPN网络数据集*********************************************************************
        # 循环遍历2得到的特征图,通过rpn([p])生成以下数据:
        # rpn_class_logits:[batch_size,H * W * anchors_per_location,2] anchors分类器logits(在softmax之前)
        # rpn_probs:[batch_size,H * W * anchors_per_location,2] anchors分类器概率。
        # rpn_bbox:[batch_size,H * W * anchors_per_location,(dy,dx,log(dh),log(dw))]  anchors的坐标偏移量

        # RPN Model  建立RPN模型,下面传参数(特征图)
        rpn = build_rpn_model(config.RPN_ANCHOR_STRIDE,   # RPN_ANCHOR_STRIDE = 1
                              len(config.RPN_ANCHOR_RATIOS), config.TOP_DOWN_PYRAMID_SIZE) # TOP_DOWN_PYRAMID_SIZE = 256

        # 循环遍历2得到的特征图,通过rpn([p])生成以下数据:
        # rpn_class_logits:[batch_size,H * W * anchors_per_location,2] anchors分类器logits(在softmax之前)
        # rpn_probs:[batch_size,H * W * anchors_per_location,2] anchors分类器概率。
        # rpn_bbox:[batch_size,H * W * anchors_per_location,(dy,dx,log(dh),log(dw))]  anchors的坐标偏移量
        layer_outputs = []  # list of lists
        for p in rpn_feature_maps:    # rpn_feature_maps = [P2,P3,P4,P5,P6].
            layer_outputs.append(rpn([p]))

        # Concatenate layer outputs将各个特征图的输出合并
        # Convert from list of lists of level outputs to list of lists of outputs across levels.
        # e.g. [[a1, b1, c1], [a2, b2, c2]] => [[a1, a2], [b1, b2], [c1, c2]]
        output_names = ["rpn_class_logits", "rpn_class", "rpn_bbox"]
        outputs = list(zip(*layer_outputs))   # zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
        outputs = [KL.Concatenate(axis=1, name=n)(list(o))
                   for o, n in zip(outputs, output_names)]

        rpn_class_logits, rpn_class, rpn_bbox = outputs    # 几个特征图对应信息合在一起

 
建立RPN网络的Keras模型的代码如下,这里并没有执行操作,而是调用rpn_graph()函数执行具体操作。

"""
建立RPN网络的Keras模型。
它包含了RPN graph,因此共享权重可以被多次使用

anchor_stride:控制锚点的密度。 通常为1(要素图中的每个像素)或2(每个其他像素)。
anchors_per_location:特征图中每个像素的锚点数量  3
depth(深度):骨干特征图的深度。256

返回Keras Model对象。 调用时,模型输出为:
rpn_class_logits:[batch_size,H * W * anchors_per_location,2] anchors分类器logits(在softmax之前)
rpn_probs:[batch_size,H * W * anchors_per_location,2] anchors分类器概率。
rpn_bbox:[batch_size,H * W * anchors_per_location,(dy,dx,log(dh),log(dw))]  anchors的坐标偏移量
"""
def build_rpn_model(anchor_stride, anchors_per_location, depth):

    input_feature_map = KL.Input(shape=[None, None, depth], name="input_rpn_feature_map")

    # 返回KerasModel对象。 调用时,模型输出为:
    # rpn_class_logits:[batch_size,H * W * anchors_per_location,2] anchors分类器logits(在softmax之前)
    # rpn_probs:[batch_size,H * W * anchors_per_location,2] anchors分类器概率。
    # rpn_bbox:[batch_size,H * W * anchors_per_location,(dy,dx,log(dh),log(dw))]  anchors的坐标偏移量
    outputs = rpn_graph(input_feature_map, anchors_per_location, anchor_stride)   # ***
    return KM.Model([input_feature_map], outputs, name="rpn_model")

rpn_graph()函数,对传进来的特征图先统一做3*3的卷积,将通道数转换为512维。然后分别进入分类和回归操作,

  • 在分类操作中,先用1*1的卷积核对上一步的结果进行卷积,得到2*3维数据,再reshape成[N,w*h*3,2],N表示batch_size大小,w*h*3表示该特征图共生成多少个anchors,2表示正样本和负样本相应数据的两个维度。rpn_class_logits用于后面计算rpn分类损失,rpn_probs表示正样本和负样本的置信度。
  • 在回归操作中,先用1*1的卷积核对上一步的结果进行卷积,得到4*3维数据,再reshape成[N,w*h*3,4],N表示batch_size大小,w*h*3表示该特征图共生成多少个anchors,4表示预测框的四个坐标。rpn_bbox用于后面计算rpn回归损失。

rpn_graph()函数处理数据结构图如下:

MaskRCNN源码解析3:RPN、ProposalLayer、DetectionTargetLayer_第1张图片

 rpn_graph()函数源代码如下:

"""
建立RPN的计算图。
feature_map:特征图[batch, height, width, depth]
anchors_per_location:特征图中每个像素的锚点数量 3
anchor_stride:控制锚点的密度。 通常为1(要素图中的每个像素)或2(每个其他像素)。

返回值:
    rpn_class_logits:[batch_size,H * W * anchors_per_location,2] anchors分类器logits(在softmax之前)
    rpn_probs:[batch_size,H * W * anchors_per_location,2] anchors分类器概率。
    rpn_bbox:[batch_size,H * W * anchors_per_location,(dy,dx,log(dh),log(dw))]  anchors的坐标偏移量             
              
"""
def rpn_graph(feature_map, anchors_per_location, anchor_stride):

    # TODO: check if stride of 2 causes alignment issues if the feature map is not even.
    # Shared convolutional base of the RPN  先3*3卷积
    shared = KL.Conv2D(512, (3, 3), padding='same', activation='relu',
                       strides=anchor_stride,
                       name='rpn_conv_shared')(feature_map)

    # 1)**********分类操作***********************************************
    # Anchor Score. [batch, height, width, anchors per location * 2].
    x = KL.Conv2D(2 * anchors_per_location, (1, 1), padding='valid',
                  activation='linear', name='rpn_class_raw')(shared)

    # Reshape to [batch, anchors, 2]
    rpn_class_logits = KL.Lambda(
        lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 2]))(x)

    # Softmax on last dimension of BG/FG.   前景背景概率
    rpn_probs = KL.Activation(
        "softmax", name="rpn_class_xxx")(rpn_class_logits)

    # 2)**********回归操作***********************************************
    # Bounding box refinement. [batch, H, W, anchors per location * depth]
    # where depth is [x, y, log(w), log(h)]
    x = KL.Conv2D(anchors_per_location * 4, (1, 1), padding="valid",
                  activation='linear', name='rpn_bbox_pred')(shared)

    # Reshape to [batch, anchors, 4]
    rpn_bbox = KL.Lambda(lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 4]))(x)

    return [rpn_class_logits, rpn_probs, rpn_bbox]

 

2,ProposalLayer

ProposalLayer的作用主要:
1. 将rpn网路的输出应用到得到的anchors,首先对输出的概率进行排序(概率就是上一步得到的rpn_probs表示正样本和负样本的置信度),获取score靠前的前6000个anchor
2. 利用rpn_bbox对anchors进行修正
3. 舍弃掉修正后边框超过图片大小的anchor,由于我们的anchor的坐标的大小是归一化的,只要坐标不超过0 1区间即可
4. 利用非极大抑制的方法获得最后的2000个anchor

MaskRCNN源码解析3:RPN、ProposalLayer、DetectionTargetLayer_第2张图片

 

整体调用ProposalLayer部分的代码如下:

        # *************************5,anchors第一次筛选****************************************************************
        # 将第4步rpn网路的输出应用到第2步得到的anchors,首先对输出的概率进行排序,
        # 保留其中预测为前景色概率大的一部分(具体值可以在配置文件中进行配置),
        # 然后选取对应的anchor,利用rpn的输出回归值对anchor进行第一次修正。
        # 修正完利用NMS方法,删除其中的一部分anchor。获的最后的anchor。

        # 在标准化坐标中为[batch,N,(y1,x1,y2,x2)],并填充了零。
        proposal_count = config.POST_NMS_ROIS_TRAINING if mode == "training"\
            else config.POST_NMS_ROIS_INFERENCE    # #  POST_NMS_ROIS_TRAINING = 2000   POST_NMS_ROIS_INFERENCE = 1000

        # ProposalLayer的作用主要
        # 1. 根据rpn网络,获取score靠前的前6000个anchor
        # 2. 利用rpn_bbox对anchors进行修正
        # 3. 舍弃掉修正后边框超过图片大小的anchor,由于我们的anchor的坐标的大小是归一化的,只要坐标不超过0 1区间即可
        # 4. 利用非极大抑制的方法获得最后的2000个anchor
        rpn_rois = ProposalLayer(    # *****
            proposal_count=proposal_count,           # 从生成的261888个anchors中选择预选框的数量(2000)
            nms_threshold=config.RPN_NMS_THRESHOLD,  # RPN_NMS_THRESHOLD = 0.7
            name="ROI",
            config=config)([rpn_class, rpn_bbox, anchors])

        if mode == "training":
            # Class ID mask to mark class IDs supported by the dataset the image came from.
            active_class_ids = KL.Lambda(
                lambda x: parse_image_meta_graph(x)["active_class_ids"]   # 解析包含其组件的图像属性的张量。
                )(input_image_meta)   # # 81+12=93

            if not config.USE_RPN_ROIS:   # USE_RPN_ROIS = True
                # Ignore predicted ROIs and use ROIs provided as an input.
                input_rois = KL.Input(shape=[config.POST_NMS_ROIS_TRAINING, 4],
                                      name="input_roi", dtype=np.int32)
                # Normalize coordinates
                target_rois = KL.Lambda(lambda x: norm_boxes_graph(
                    x, K.shape(input_image)[1:3]))(input_rois)
            else:
                target_rois = rpn_rois    #  筛选后的预选框 2000个

 ProposalLayer类的定义,主要数据操作在call中实现。这部分逻辑不是太复杂,代码中都有注释,这里就不多说了。

# ProposalLayer的作用主要
# 1. 根据rpn网络,获取score靠前的前6000个anchors
# 2. 利用rpn_bbox对anchors进行修正
# 3. 舍弃掉修正后边框超过图片大小的anchor,由于我们的anchor的坐标的大小是归一化的,只要坐标不超过0 1区间即可
# 4. 利用非极大抑制的方法获得2000个anchors
class ProposalLayer(KE.Layer):

    def __init__(self, proposal_count, nms_threshold, config=None, **kwargs):
        super(ProposalLayer, self).__init__(**kwargs)  # super() 函数是用于调用父类(超类)的一个方法。
        self.config = config
        self.proposal_count = proposal_count     # 2000
        self.nms_threshold = nms_threshold       # 0.7

    # call() 用来执行 ProposalLayer 的职能, 即当前 Layer 所有的计算过程均在该函数中完成
    def call(self, inputs):
        # Box Scores. Use the foreground class confidence. [Batch, num_rois, 1]
        # Box 分数,使用前景类置信度,[Batch, num_rois, 1]
        scores = inputs[0][:, :, 1]    # inputs传入的参数     rpn_probs 前景背景概率
        # Box deltas [batch, num_rois, 4]
        # Box偏移量 [batch, num_rois, 4]
        deltas = inputs[1]    # rpn_bbox 偏移量
        # RPN和最终检测的边界框优化标准偏差。 RPN_BBOX_STD_DEV = np.array([0.1, 0.1, 0.2, 0.2])
        deltas = deltas * np.reshape(self.config.RPN_BBOX_STD_DEV, [1, 1, 4])
        # Anchors
        anchors = inputs[2]    # anchors 特征图生成的所有anchors

        # Improve performance by trimming to top anchors by score
        # and doing the rest on the smaller subset.
        # 根据anchors的得分(使用前景类置信度)选择top_k个anchors,并在这top_k 个anchors上操作以提高性能
        pre_nms_limit = tf.minimum(self.config.PRE_NMS_LIMIT, tf.shape(anchors)[1])  # 保持ROI的数量 PRE_NMS_LIMIT = 6000  num(anchors)=261888

        # 获取top_k anchors的索引 此时k=pre_nms_limit=6000
        ix = tf.nn.top_k(scores, pre_nms_limit, sorted=True, name="top_anchors").indices

        # 根据top_k的索引获取相应的scores、偏移量deltas、anchors
        scores = utils.batch_slice([scores, ix], lambda x, y: tf.gather(x, y),  # tf.gather根据索引,从输入张量中依次取元素,构成一个新的张量。
                                   self.config.IMAGES_PER_GPU)  # IMAGES_PER_GPU = 1
        deltas = utils.batch_slice([deltas, ix], lambda x, y: tf.gather(x, y),
                                   self.config.IMAGES_PER_GPU)
        pre_nms_anchors = utils.batch_slice([anchors, ix], lambda a, x: tf.gather(a, x),
                                    self.config.IMAGES_PER_GPU, names=["pre_nms_anchors"])

        # 对anchors应用偏移量,以获取更加精确的anchors。Apply deltas to anchors to get refined anchors.
        # [batch, N, (y1, x1, y2, x2)]
        boxes = utils.batch_slice([pre_nms_anchors, deltas],
                                  lambda x, y: apply_box_deltas_graph(x, y),  # 转换公式见**
                                  self.config.IMAGES_PER_GPU,
                                  names=["refined_anchors"])

        # 裁剪到图像边界。 由于我们在归一化坐标下,Clip to image boundaries. Since we're in normalized coordinates,
        # clip to 0..1 range. [batch, N, (y1, x1, y2, x2)]
        window = np.array([0, 0, 1, 1], dtype=np.float32)
        boxes = utils.batch_slice(boxes,
                                  lambda x: clip_boxes_graph(x, window),  # 确保每个框坐标范围为0-1
                                  self.config.IMAGES_PER_GPU,
                                  names=["refined_anchors_clipped"])

        # Filter out small boxes
        # According to Xinlei Chen's paper, this reduces detection accuracy
        # for small objects, so we're skipping it.
        # 过滤小box 根据ChenxiniChen的论文,这降低了对小物体的检测精度,因此我们跳过了它。

        # Non-max suppression  此处从6000个中根据nms再选择出2000个
        def nms(boxes, scores):
            indices = tf.image.non_max_suppression(
                boxes, scores, self.proposal_count,
                self.nms_threshold, name="rpn_non_max_suppression")
            proposals = tf.gather(boxes, indices)
            # Pad if needed
            padding = tf.maximum(self.proposal_count - tf.shape(proposals)[0], 0)
            proposals = tf.pad(proposals, [(0, padding), (0, 0)])
            return proposals
        proposals = utils.batch_slice([boxes, scores], nms,
                                      self.config.IMAGES_PER_GPU)
        return proposals

    def compute_output_shape(self, input_shape):   # 用来计算输出张量的 shape
        return (None, self.proposal_count, 4)

 

3,DetectionTargetLayer

DetectionTargetLayer层主要是对上一步 ProposalLayer层选出的2000个rois做进一步筛选,得到最终用于训练的200个(当前代码里设置的值是200)正负样本。

DetectionTargetLayer的输入包含了:target_rois, input_gt_class_ids, gt_boxes, input_gt_masks。
其中target_rois是第5步ProposalLayer输出的结果。

  • 首先,计算target_rois中的每一个rois和哪一个真实的框gt_boxes iou值,
  • 如果最大的iou大于0.5,则被认为是正样本,负样本是iou小于0.5并且和crowd box相交不大的anchor,选择出了正负样本,还要保证样本的均衡性,具体可以在配置文件中进行配置。
  •  最后计算了正样本中的anchor和哪一个真实的框最接近,用真实的框和anchor计算出偏移值,
  •  并且将mask的大小resize成28 * 28 的(我猜测利用的是双线性差值的方式,因为mask的值不是0就是1,0是背景,1是前景)这些都是后面的分类和mask网络要用到的真实的值

 

MaskRCNN源码解析3:RPN、ProposalLayer、DetectionTargetLayer_第3张图片

 

整体调用DetectionTargetLayer部分的代码如下:

            # *************************6,生成检测目标,anchors第二次筛选**********************************************************
            # DetectionTargetLayer的输入包含了:target_rois, input_gt_class_ids, gt_boxes, input_gt_masks。
            # 其中target_rois是第5步ProposalLayer输出的结果。
            # 首先,计算target_rois中的每一个rois和哪一个真实的框gt_boxes iou值,
            # 如果最大的iou大于0.5,则被认为是正样本,负样本是iou小于0.5并且和crowd box相交不大的anchor,
            # 选择出了正负样本,还要保证样本的均衡性,具体可以在配置文件中进行配置。
            # 最后计算了正样本中的anchor和哪一个真实的框最接近,用真实的框和anchor计算出偏移值,
            # 并且将mask的大小resize成28 * 28 的(我猜测利用的是双线性差值的方式,因为mask的值不是0就是1,0是背景,一是前景)
            # 这些都是后面的分类和mask网络要用到的真实的值
            rois, target_class_ids, target_bbox, target_mask =\
                DetectionTargetLayer(config, name="proposal_targets")([  # 从2000个rooposals中选择200个正负样本用于最终的优化训练
                    target_rois, input_gt_class_ids, gt_boxes, input_gt_masks])

其中的DetectionTargetLayer类定义如下,该类的call函数中又调用了detection_targets_graph()函数进行了实际的数据操作,完成的功能是从2000个rooposals中选择200个正负样本用于最终的优化训练。

# DetectionTargetLayer的输入包含了:target_rois, input_gt_class_ids, gt_boxes, input_gt_masks。
# 其中target_rois是第5步ProposalLayer输出的结果。
# 首先,计算target_rois中的每一个rois和哪一个真实的框gt_boxes iou值,
# 如果最大的iou大于0.5,则被认为是正样本,负样本是iou小于0.5并且和crowd box相交不大的anchor,
# 选择出了正负样本,还要保证样本的均衡性,具体可以在配置文件中进行配置。
# 最后计算了正样本中的anchor和哪一个真实的框最接近,用真实的框和anchor计算出偏移值,
# 并且将mask的大小resize成28 * 28 的(我猜测利用的是双线性差值的方式,因为mask的值不是0就是1,0是背景,一是前景)
# 这些都是后面的分类和mask网络要用到的真实的值
class DetectionTargetLayer(KE.Layer):

    def __init__(self, config, **kwargs):
        super(DetectionTargetLayer, self).__init__(**kwargs)
        self.config = config

    def call(self, inputs):
        proposals = inputs[0]     # 预选框
        gt_class_ids = inputs[1]  # 类别序号
        gt_boxes = inputs[2]      # GT框
        gt_masks = inputs[3]      # GT mask

        # Slice the batch and run a graph for each slice 对批次进行切片并为每个切片运行图
        # TODO: Rename target_bbox to target_deltas for clarity
        # 待办事项:为清楚起见,将target_bbox重命名为target_deltas
        names = ["rois", "target_class_ids", "target_bbox", "target_mask"]
        outputs = utils.batch_slice(
            [proposals, gt_class_ids, gt_boxes, gt_masks],
            lambda w, x, y, z: detection_targets_graph(   # ***  # 从2000个rooposals中选择200个正负样本用于最终的优化训练
                w, x, y, z, self.config),
            self.config.IMAGES_PER_GPU, names=names)
        return outputs

detection_targets_graph()函数的对数据的具体操作如下,对关键部分代码添加了注释,这里同样不用文字描述了。

"""
Generates detection targets for one image. Subsamples proposals and
generates target class IDs, bounding box deltas, and masks for each.
为一张图像生成检测目标。 对proposals进行二次筛选,并为每个proposals生成目标类ID,边界框偏移量和掩码。

Inputs:
proposals: [POST_NMS_ROIS_TRAINING, (y1, x1, y2, x2)] in normalized coordinates. Might
           be zero padded if there are not enough proposals.
gt_class_ids: [MAX_GT_INSTANCES] int class IDs
gt_boxes: [MAX_GT_INSTANCES, (y1, x1, y2, x2)] in normalized coordinates.
gt_masks: [height, width, MAX_GT_INSTANCES] of boolean type.

Returns: Target ROIs and corresponding class IDs, bounding box shifts,
and masks.
rois: [TRAIN_ROIS_PER_IMAGE, (y1, x1, y2, x2)] in normalized coordinates
class_ids: [TRAIN_ROIS_PER_IMAGE]. Integer class IDs. Zero padded.
deltas: [TRAIN_ROIS_PER_IMAGE, (dy, dx, log(dh), log(dw))]
masks: [TRAIN_ROIS_PER_IMAGE, height, width]. Masks cropped to bbox
       boundaries and resized to neural network output size.

Note: Returned arrays might be zero padded if not enough target ROIs.
注意:如果目标ROI不够,返回的数组可能会补零。
"""
# 从2000个rooposals中选择200个正负样本用于最终的优化训练
def detection_targets_graph(proposals, gt_class_ids, gt_boxes, gt_masks, config):

    # Assertions
    asserts = [   # tf.greater(a,b) 通过比较a、b两个值的大小来输出对错。
        tf.Assert(tf.greater(tf.shape(proposals)[0], 0), [proposals],
                  name="roi_assertion"),
    ]
    # tf.identity是返回一个一模一样新的tensor的op,这会增加一个新节点到gragh中,这时control_dependencies就会生效
    with tf.control_dependencies(asserts):
        proposals = tf.identity(proposals)

    # Remove zero padding   删除零填充
    proposals, _ = trim_zeros_graph(proposals, name="trim_proposals")
    gt_boxes, non_zeros = trim_zeros_graph(gt_boxes, name="trim_gt_boxes")
    gt_class_ids = tf.boolean_mask(gt_class_ids, non_zeros, name="trim_gt_class_ids")
    gt_masks = tf.gather(gt_masks, tf.where(non_zeros)[:, 0], axis=2, name="trim_gt_masks")

    # Handle COCO crowds(人群)
    # A crowd box in COCO is a bounding box around several instances. Exclude
    # them from training. A crowd box is given a negative class ID.
    # 在coco数据集中,有的框会标注很多的物体,在训练中,去掉这些框
    crowd_ix = tf.where(gt_class_ids < 0)[:, 0]
    non_crowd_ix = tf.where(gt_class_ids > 0)[:, 0]
    crowd_boxes = tf.gather(gt_boxes, crowd_ix)
    gt_class_ids = tf.gather(gt_class_ids, non_crowd_ix)
    gt_boxes = tf.gather(gt_boxes, non_crowd_ix)
    gt_masks = tf.gather(gt_masks, non_crowd_ix, axis=2)

    # Compute overlaps matrix [proposals, gt_boxes]
    # 计算[proposals,gt_boxes]的IOU
    overlaps = overlaps_graph(proposals, gt_boxes)

    # Compute overlaps with crowd boxes [proposals, crowd_boxes]
    crowd_overlaps = overlaps_graph(proposals, crowd_boxes)
    crowd_iou_max = tf.reduce_max(crowd_overlaps, axis=1)
    no_crowd_bool = (crowd_iou_max < 0.001)

    # Determine positive and negative ROIs
    # 确定 正样本和负样本
    roi_iou_max = tf.reduce_max(overlaps, axis=1)   # 按列求最大值
    # 1. Positive ROIs are those with >= 0.5 IoU with a GT box
    # 1. 和真实的框的iou值大于0.5时,被认为是正样本
    positive_roi_bool = (roi_iou_max >= 0.5)
    positive_indices = tf.where(positive_roi_bool)[:, 0]   # 获取正样本索引
    # 2. Negative ROIs are those with < 0.5 with every GT box. Skip crowds.
    # 2. 负样本是iou小于0.5并且和crowd box相交不大的anchor
    negative_indices = tf.where(tf.logical_and(roi_iou_max < 0.5, no_crowd_bool))[:, 0]  # 获取负样本索引

    # Subsample ROIs. Aim for 33% positive   争取正样本占比33%
    # Positive ROIs  正样本
    positive_count = int(config.TRAIN_ROIS_PER_IMAGE *config.ROI_POSITIVE_RATIO)  # 200 *0.33
    positive_indices = tf.random_shuffle(positive_indices)[:positive_count]
    positive_count = tf.shape(positive_indices)[0]
    # Negative ROIs. Add enough to maintain positive:negative ratio.
    # 负ROI。 添加足够的量以维持正负比率。
    r = 1.0 / config.ROI_POSITIVE_RATIO   # ROI_POSITIVE_RATIO = 0.33
    negative_count = tf.cast(r * tf.cast(positive_count, tf.float32), tf.int32) - positive_count   # 根据正样本数量计算负样本数量
    negative_indices = tf.random_shuffle(negative_indices)[:negative_count]
    # Gather selected ROIs 根据正负样本索引选择正负样本ROIs
    positive_rois = tf.gather(proposals, positive_indices)
    negative_rois = tf.gather(proposals, negative_indices)

    # 计算正样本和哪个真实的框最接近。 Assign positive ROIs to GT boxes.
    positive_overlaps = tf.gather(overlaps, positive_indices)  # 根据正样本索引,选择与GT的IOU值
    roi_gt_box_assignment = tf.cond(     # tf.cond()类似于c语言中的if...else...,用来控制数据流向,但是仅仅类似而已
        tf.greater(tf.shape(positive_overlaps)[1], 0),
        true_fn = lambda: tf.argmax(positive_overlaps, axis=1),   # 找与GT IOU最大的值
        false_fn = lambda: tf.cast(tf.constant([]),tf.int64)
    )
    roi_gt_boxes = tf.gather(gt_boxes, roi_gt_box_assignment)   # IOU最大的ROI匹配的GT框
    roi_gt_class_ids = tf.gather(gt_class_ids, roi_gt_box_assignment)   # IOU最大的ROI匹配的GT类别序号

    # Compute bbox refinement for positive ROIs
    # 用最接近的真实框修正rpn网络预测的框
    deltas = utils.box_refinement_graph(positive_rois, roi_gt_boxes)   # 将GT框转换成偏移量
    deltas /= config.BBOX_STD_DEV   # BBOX_STD_DEV = np.array([0.1, 0.1, 0.2, 0.2])  RPN和最终检测的边界框优化标准偏差。

    # Assign positive ROIs to GT masks
    # Permute masks to [N, height, width, 1]
    # 为GT masks分配正的ROI
    # 将masks置换为[N,高度,宽度,1]
    transposed_masks = tf.expand_dims(tf.transpose(gt_masks, [2, 0, 1]), -1)
    # Pick the right mask for each ROI
    roi_masks = tf.gather(transposed_masks, roi_gt_box_assignment)  # IOU最大的ROI匹配的 GT mask

    # 计算目标mask Compute mask targets
    boxes = positive_rois
    if config.USE_MINI_MASK:   # USE_MINI_MASK = True
        # Transform ROI coordinates from normalized image space to normalized mini-mask space.
        # 将ROI坐标从归一化的图像空间转换为归一化的小型mask空间。
        y1, x1, y2, x2 = tf.split(positive_rois, 4, axis=1)
        gt_y1, gt_x1, gt_y2, gt_x2 = tf.split(roi_gt_boxes, 4, axis=1)
        gt_h = gt_y2 - gt_y1
        gt_w = gt_x2 - gt_x1
        y1 = (y1 - gt_y1) / gt_h
        x1 = (x1 - gt_x1) / gt_w
        y2 = (y2 - gt_y1) / gt_h
        x2 = (x2 - gt_x1) / gt_w
        boxes = tf.concat([y1, x1, y2, x2], 1)
    box_ids = tf.range(0, tf.shape(roi_masks)[0])
    # crop_and_resize相当于roipolling的操作
    masks = tf.image.crop_and_resize(tf.cast(roi_masks, tf.float32), boxes,
                                     box_ids,
                                     config.MASK_SHAPE)
    # 去除mask多余的尺寸。Remove the extra dimension from masks.
    masks = tf.squeeze(masks, axis=3)

    # Threshold mask pixels at 0.5 to have GT masks be 0 or 1 to use with binary cross entropy loss.
    masks = tf.round(masks)  # tf.round()四舍五入函数

    # Append negative ROIs and pad bbox deltas and masks that are not used for negative ROIs with zeros.
    rois = tf.concat([positive_rois, negative_rois], axis=0)   # 正负样本合并
    N = tf.shape(negative_rois)[0]   # 负样本数量
    # 每幅图像中要输入到分类器的ROI数量 TRAIN_ROIS_PER_IMAGE = 200,正负样本 1:3
    P = tf.maximum(config.TRAIN_ROIS_PER_IMAGE - tf.shape(rois)[0], 0)  # 正负样本和200还差多少个
    rois = tf.pad(rois, [(0, P), (0, 0)])  # tf.pad:填充函数  差的样本用0填充,  rois是最终待训练的正负样本
    roi_gt_boxes = tf.pad(roi_gt_boxes, [(0, N + P), (0, 0)])
    roi_gt_class_ids = tf.pad(roi_gt_class_ids, [(0, N + P)])
    deltas = tf.pad(deltas, [(0, N + P), (0, 0)])  # 偏移量填充
    masks = tf.pad(masks, [[0, N + P], (0, 0), (0, 0)])  # masks填充

    return rois, roi_gt_class_ids, deltas, masks

你可能感兴趣的:(#,目标检测算法)