MaskRCNN源码解析1:整体结构概述
MaskRCNN源码解析2:特征图与anchors生成
MaskRCNN源码解析3:RPN、ProposalLayer、DetectionTargetLayer
MaskRCNN源码解析4-0:ROI Pooling 与 ROI Align理论
MaskRCNN源码解析4:头网络(Networks Heads)解析
MaskRCNN源码解析5:损失部分解析
目录
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)。
参考文章:
解析源码地址:
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()函数处理数据结构图如下:
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
整体调用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网络要用到的真实的值
整体调用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