Mask RCNN 源代码解析 (1) - 整体思路

Mask RCNN 属于 RCNN这一系列的应该是比较最终的版本,融合多种算法的思想,这里对Mask RCNN从源代码进行解析,主要写几篇文章,一个总结大的思路,其他文章整理细节。

这篇文章为了简单,主要从前向传播和后向传播,分两部分进行介绍,主要以数据的流动为主线,分析流程图和核心函数。主要参考的代码是Pytorch的mask RCNN版本, 这个版本的Mask RCNN代码只支持一个图片处理。

【Inference部分】

前向部分的主要函数是在MaskRCNN类的predict函数中,主要的输入数据是

molded_images = input[0] #(1,3,1024,1024) Variable
image_metas = input[1] #(1,89) ndarray 位数是(1+3+4+num_classes)
#分别是(图像id 1个值, 图像形状3个值的list, window数据4个值的list, 81个数据的list,全部为0的81个数据)

(1) 图像数据,(1,3,1024,1024) - 这里的图像是对原图进行了缩放和padding到1024的尺寸

(2) 当前图像的meta数据,(1,89) - 图像id 1个值, 原始图像数据形状 3个值,原始图像中1024图像中的window值 4个值, 81个类别数据,这里全部为0

下面还是分步骤介绍

1:FPN特征提取 - 构建RPN需要的Feature maps 和 Mask 所需要的Feature maps (需要网络计算)

这部分涉及的函数比较简单:

# Feature extraction
# (1,256,256,256) - P2
# (1,256,128,128) - P3
# (1,256,64,64) - P4
# (1,256,32,32) - P5
# (1,256,16,16) - P6
[p2_out, p3_out, p4_out, p5_out, p6_out] = self.fpn(molded_images)# molded_images (1,3,1024,1024) Variable

至于FPN属于特征提取的基础处理,这篇文章就不详细介绍了

2:RPN (需要网络计算)

这步的特点:

(1) 对于RPN的是用的同样卷积filter参数对不同的feature map进行卷积。

(2) RPN在对原始的feature map进行卷积的时候是不改变原始feature map的大小,这样的话,原始的feature map就具有cell的概念。

(3) 生成的261888个 box数据是对于anchor部分的delta数据

这步主题代码部分:

        # 对所有的feature金字塔进行遍历
        for p in rpn_feature_maps:
            # rpn(p)返回的结果
            # (batch,H*W*anchors per location,2) anchors_per_location = 3
            # (batch,H*W*anchors per location,2)
            # (batch,H*W**anchors per location,4)
            #return [rpn_class_logits, rpn_probs, rpn_bbox]
            layer_outputs.append(self.rpn(p))#同一个rpn对多个feature pyramid进行一样的操作,牛逼
        #layer_outputs是一个list[list]的结构,存放的是不同feature map的处理结果
        #0号 [(1,256*256*3,2),(1,256*256*3,2),(1,256*256*3,4)]
        #1号 [(1,128*128*3,2),(1,128*128*3,2),(1,128*128*3,4)]
        #2号 [(1,64*64*3,2),(1,64*64*3,2),(1,64*64*3,4)]
        #3号 [(1,32*32*3,2),(1,32*32*3,2),(1,32*32*3,4)]
        #4号 [(1,16*16*3,2),(1,16*16*3,2),(1,16*16*3,4)]

RPN部分的代码如下:

class RPN(nn.Module):
    """Builds the model of Region Proposal Network.

    anchors_per_location: number of anchors per pixel in the feature map
    anchor_stride: Controls the density of anchors. Typically 1 (anchors for
                   every pixel in the feature map), or 2 (every other pixel).

    Returns:
        rpn_logits: [batch, H, W, 2] Anchor classifier logits (before softmax)
        rpn_probs: [batch, W, W, 2] Anchor classifier probabilities.
        rpn_bbox: [batch, H, W, (dy, dx, log(dh), log(dw))] Deltas to be
                  applied to anchors.
    """
    #####这里的RPN是对多个feature parymid同样的操作,换句话说,RPN是对多个不同大小的feature map做同样的3x3卷积操作
    # 创建同样一个对象,然后对feature map进行遍历操作,依次对feature map进行处理
    #anchors_per_location = 3
    #anchor_stride = 1
    #depth = 256
    def __init__(self, anchors_per_location, anchor_stride, depth):
        super(RPN, self).__init__()
        self.anchors_per_location = anchors_per_location #3 每个feature map上生成anchor的个数
        self.anchor_stride = anchor_stride#1 基本为1,表示每个feature map的像素点都生成anchor
        self.depth = depth#256 -- feature map的输入channel数目

        #对于一个输入的feature map,用3x3的kernel卷积,然后输出结果一样大小的feature map
        self.padding = SamePad2d(kernel_size=3, stride=self.anchor_stride)#为了保证卷积以后水平尺寸不变,而进行padding
        self.conv_shared = nn.Conv2d(self.depth, 512, kernel_size=3, stride=self.anchor_stride)
        self.relu = nn.ReLU(inplace=True)

        #降维度到类别
        self.conv_class = nn.Conv2d(512, 2 * anchors_per_location, kernel_size=1, stride=1)

        self.softmax = nn.Softmax(dim=2)

        #降维到坐标
        self.conv_bbox = nn.Conv2d(512, 4 * anchors_per_location, kernel_size=1, stride=1)

    def forward(self, x):
        # Shared convolutional base of the RPN
        x = self.relu(self.conv_shared(self.padding(x)))#结果x -- (1,512,H,W)

        # Anchor Score. [batch, anchors per location * 2, height, width]. -- 代表了是背景还是物体
        rpn_class_logits = self.conv_class(x)#从(1,512,H,W) - > (1, anchors per location * 2, H, W) -- (1,3*2,H,W)

        # Reshape to [batch, 2, anchors] 这里应该是[batch,anchors,2]
        rpn_class_logits = rpn_class_logits.permute(0,2,3,1) # (1,  H, W,anchors per location * 2)
        rpn_class_logits = rpn_class_logits.contiguous()# 把这些数据变成连续排列,形状是 (B*H*W*anchor per location*2,)
        #又把数据,解析成(B,H*W*anchors per location,2)形状
        rpn_class_logits = rpn_class_logits.view(x.size()[0], -1, 2)#(B,H*W*anchors per location,2)

        # Softmax on last dimension of BG/FG. -- 在最后一个维度上做Softmax 结果是(B,H*W*anchors per location,2)
        rpn_probs = self.softmax(rpn_class_logits)

        # Bounding box refinement. [B, H, W, anchors per location, depth]
        # where depth is [x, y, log(w), log(h)]
        # 基于同样的x,用另外的卷积网络来预测box的坐标值
        rpn_bbox = self.conv_bbox(x)#结果是(B,4*anchors per location,H,W)

        # Reshape to [batch, 4, anchors] 这里应该是(batch, anchors, 4)
        rpn_bbox = rpn_bbox.permute(0,2,3,1)# (B,H,W,anchors per location*4)
        rpn_bbox = rpn_bbox.contiguous()#不用做成连续的,直接reshape或者view也是可以的吧
        rpn_bbox = rpn_bbox.view(x.size()[0], -1, 4)#(B,H*W*anchors per location,4)

        #(batch,H*W*anchors per location,2)
        #(batch,H*W*anchors per location,2)
        ##(batch,H*W**anchors per location,4)
        #rpn_class_logits和rpn_probs形状一样,只是做了一个概率计算
        return [rpn_class_logits, rpn_probs, rpn_bbox]

3: Region Proposal (不需要网络计算)

这部分的特点

(1) 把第二步的box delta数据和anchor数据结合,生成最后的RPN box数据, 这里的anchor是像素尺度的值

(2) 然后对score从大到小做NMS,保留前面的1000 or 2000个

(3) 这部分不需要网络计算,相对于是对网络计算的结果进行了部分的筛选

主要的函数是

# 输入数据信息:
# 这时候的RPN的结果是坐标数据的集合,已经没有feature map scale的信息了

# 输出结果数据:
# 结果是 (1,1000,4)

# 函数作用:
# 实际上就是针对输入的RPN结果(delta数据),结合anchor数据,生成坐标, ROI 在这里已经是坐标数据了,往后anchor就不需要了
# 然后对score从大到小做NMS,保留前面的1000 or 2000个
# @@@@@@@@@@@坐标相关-3 @@@@@@@@@@@ rpn_rois box 值是对1024图像的高度宽度归一化后的坐标值
rpn_rois = proposal_layer([rpn_class, rpn_bbox],#RPN的输出值 - [(1,261888,2) (1,261888,4)]各个RPN的类别和坐标数据
                            proposal_count=proposal_count,# 1000
                            nms_threshold=self.config.RPN_NMS_THRESHOLD,#0.7
                            anchors=self.anchors,#[261888,4]
                            config=self.config)

4:RCNN部分(需要网络计算)

大的逻辑是:

输入:

是rpn_rois (1000,4),1000个box的数据,和 Feature maps 数据[p2_out, p3_out, p4_out, p5_out] (1,256,256,256) (1,256,128,128)(1,256,64,64)(1,256,32,32)

需要完成两个操作

(1): 判断出来这些box属于哪些类别

(2): 对这些box进行坐标的调整,然后进一步根据类别的概率大小进行筛选ROI

输出:

detections (16,6) 具体的检测到的box的值,这里一共检测到了16个box, 每个box的值格式是

[y1, x1, y2, x2, class_id, score], score 为概率值

这里采用了两个函数完成上述操作:

(1) Classifier 

- 函数的操作是首先对于输入的feature maps 和 ROIs 首先进行ROI align 得到结果是(1000,256,7,7)的feature

- 然后针对这些feature用卷积,转换成向量,用7x7的卷积把(1000,256,7,7)->(1000,256,1,1)的数据

- 然后再用全连接,把(1000,256,1,1)->(1000,81) -- 代表的是ROI框81个类别

- 然后再用全连接,把(1000,256,1,1)->(1000,81*4) -- 代表的是ROI框81个类别的4个坐标偏移

- @@@@@@@@@@@坐标相关-4 @@@@@@@@@@@ mrcnn_bbox box 值是box deltas 格式是[dy,dx,log(dh),log(dw)]

- 这里的deltas值是相对于传入box的一个偏移,用的是box本身的高度宽度来进行了小数化

# Proposal classifier and BBox regressor heads

# 输入
# mrcnn_feature_maps = [p2_out, p3_out, p4_out, p5_out] (1,256,256,256) (1,256,128,128)(1,256,64,64)(1,256,32,32)
# rpn_rois = (1,1000,4)

# 输出
# mrcnn_class_logits (1000,81) 结果是这1000个ROI所属物体的类别的score
# mrcnn_class (1000,81) 结果是这1000个ROI所属物体的类别的概率
# mrcnn_bbox (1000,81,4) 结果是1000个ROI对每个类别所产生的bbox的偏移,这里不是真正的坐标,只是针对RPN结果的delta

# 函数作用
# 函数的作用是对于输入的feature maps 和 ROIs 首先进行ROI align 得到结果是(1000,256,7,7)的feature
# 然后针对这些feature用卷积,转换成向量,用7x7的卷积把(1000,256,7,7)->(1000,256,1,1)的数据
# 然后再用全连接,把(1000,256,1,1)->(1000,81)   -- 代表的是ROI框81个类别
# 然后再用全连接,把(1000,256,1,1)->(1000,81*4) -- 代表的是ROI框81个类别的4个坐标便宜
# @@@@@@@@@@@坐标相关-4 @@@@@@@@@@@ mrcnn_bbox box 值是box deltas 格式是[dy,dx,log(dh),log(dw)]
# 这里的deltas值是相对于传入box的一个偏移,用的是box本身的高度宽度来进行了小数化
mrcnn_class_logits, mrcnn_class, mrcnn_bbox = self.classifier(mrcnn_feature_maps, rpn_rois)

(2) Detection layer

-函数作用

具体逻辑是

(1)确定ROI的类别: 对于输入的1000个ROIs的坐标,根据probs找到这1000个ROIs的类别最大值,

(2)调整ROI的 box坐标: 然后认为这1000个ROIs的box delta是deltas (1000,81,4)中类别最大值对应的delta,对ROIs的box进行调整

这时候的ROI在box的坐标值上已经调整,然后每个ROI也具有类别信息

(3)去除各个ROI属于背景的对象,然后根据各个ROI的最大类别score,取0.7的阈值,然后筛选出来一些ROI,得到129个ROIs

(4)然后对于这129个ROI,按类别,分别做NMS,

-@@@@@@@@@@@坐标相关-5 @@@@@@@@@@@ detections box 值是在1024图像内的像素坐标值

-detections的形状是(16,6)

#######################################################################################################
# detection_layer 函数
# 输入
# rpn_rois = (1,1000,4) -- 原始的RPN结果
# mrcnn_class (1000,81) -- 用ROI align算出来的类别
# mrcnn_bbox (1000,81,4) -- 用ROI align算出来的坐标偏移

# 输出
# output is [batch, num_detections, (y1, x1, y2, x2, class_id, score)] in image coordinates
# (16,6)

# 函数作用
# 逻辑是对于输入的1000个ROIs的坐标,根据probs找到这1000个ROIs的类别最大值,
# 然后认为这1000个ROIs的box delta是deltas (1000,81,4)中类别最大值对应的delta,对ROIs的box进行调整
# 这时候的ROI在box的坐标值上已经调整,然后每个ROI也具有类别信息
# (1) -- 用最大类别的Box delta来调整ROI的box
# (2) -- 用最大类别的score值来去除一些ROI,同时直接去除背景ROI
# (3) -- 对剩下的ROI,按照类别做NMS
# @@@@@@@@@@@坐标相关-5 @@@@@@@@@@@ detections box 值是在1024图像内的像素坐标值
# detections的形状是(16,6)
detections = detection_layer(self.config, rpn_rois, mrcnn_class, mrcnn_bbox, image_metas)

这里的detection的形状是(16,6), 每个box含有的数据格式是(y1, x1, y2, x2, class_id, score)

整体流程图如下:

5:Mask 部分 (需要网络计算)

大的逻辑是:

输入:

mrcnn_feature_maps = [p2_out, p3_out, p4_out, p5_out] (1,256,256,256) (1,256,128,128)(1,256,64,64)(1,256,32,32)

detection_boxes = (16,4)

需要完成

对每个box,做一个分割,求出物体所在的区域,这里需要对于输入的box,回到原来的feature maps上再做一次ROI aligin然后算出固定大小的mask

输出:

mrcnn_mask (16,81,28,28) --- 代表的每个类别mask的score value的sigmoid以后的值

核心的代码比较简单,如下

# Create masks for detections
# 输入
# mrcnn_feature_maps = [p2_out, p3_out, p4_out, p5_out] (1,256,256,256) (1,256,128,128)(1,256,64,64)(1,256,32,32)
# detection_boxes = (16,4)
# 输出
# (16,81,28,28) --- 代表的每个类别的mask值

def forward(self, x, rois):
    #pool_size = 14
    x = pyramid_roi_align([rois] + x, self.pool_size, self.image_shape)#(11,256,14,14)
    x = self.conv1(self.padding(x))#(11,256,14,14)
    x = self.bn1(x)#(11,256,14,14)
    x = self.relu(x)#(11,256,14,14)
    x = self.conv2(self.padding(x))##(11,256,14,14)
    x = self.bn2(x)#(11,256,14,14)
    x = self.relu(x)#(11,256,14,14)
    x = self.conv3(self.padding(x))#(11,256,14,14)
    x = self.bn3(x)#(11,256,14,14)
    x = self.relu(x)#(11,256,14,14)
    x = self.conv4(self.padding(x))#(11,256,14,14)
    x = self.bn4(x)#(11,256,14,14)
    x = self.relu(x)#(11,256,14,14)

    x = self.deconv(x)#(11,256,28,28)
    x = self.relu(x)#(11,256,28,28)

    x = self.conv5(x)#(11,81,28,28)
    x = self.sigmoid(x)#(11,81,28,28)

    return x

主要是调用了一个ROI align生成 14x14大小的feature map然后,做一个deconv把feature map变大到(28x28),每个类别取sigmoid.

流程图如下

输出(16,81,28,28)sigmoid分割值,mask函数本身就结束了,但是后续还有一个真实mask的计算过程,主要包含如下

(1)根据box类别值,直接选出对应的mask数据

(2)28x28 mask变换到原图中box的大小, 双线性插值, mask在这里做变换的时候有一个形变,调用的函数是

 mask = scipy.misc.imresize(mask, (y2 - y1, x2 - x1), interp='bilinear').astype(np.float32) / 255.0 #(384,136)

这个函数接受的是小数输入,输出255范围的图片

(3)  0.5阈值,得到二值化mask

6:以上整体总结

两次Score排序,两次roi align, 两次box delta (一次是anchor+region proposal= rois, 一次是rois+回归误差 = detections)

或者是对feature map访问了3次,第一次做RPN,第二次,ROI align进行坐标的回归调整和类别判断,第三次,ROI align计算mask

1: 首先是在网络创建的时候生成所有的anchor

2: 然后是函数rpn对不同的feature map生成region proposal --- region proposal 的意义是在anchor上的delta

3: (这里score第一次排序--从261888-1000)函数proposal_layer内排序,结合所有的(anchor和RPN生成ROI),并且排序取前1000个ROI 得到1000个roi box

4-1: 然后是函数classifier,对有1000个roi进行(ROI algin,第一次), 生成每个roi的物体类别(1000,81)的输出和对于ROI的坐标偏移(1000,81,4)的输出

4-2: 在对81个类别取最大(score排序第二次排序---从1000-16),认为类别最大所对应的score和Box delta,就是roi的score和detla,这样就可以对每个ROI的score赋值,(同时调整box的delta)

5: 在获得detection (16,6)的结果以后,在回到原来的feature map list上做(ROI algin,第二次),然后生成各个类别的mask

另外凡是和坐标相关的数据,比如RPN的输出,回归调整坐标的输出,ROI align的输入,涉及到网络输出的坐标数据肯定是小数,另外需要把坐标值作为输入的也是小数

关于box deltas的地方有一个比较搞的地方是 [dy,dx,log(dh),log(dw)] 这里的dy,dx,log(dh),log(dw)都是相对于传入box的高度和宽度来计算,传入box的坐标值也可以为像素意义,也可以为归一化的意义

【训练部分】

在inference部分,一共是5大部分

1: FPN特征提取 - 构建RPN需要的Feature maps 和 Mask 所需要的Feature maps (需要网络计算)
2: RPN (需要网络计算)
3: Region Proposal (不需要网络计算)
4:RCNN部分(需要网络计算)
5:Mask 部分 (需要网络计算)

其中和网络计算相关的是 1,2,4,5,而3是完全不和网络相关,在训练的时候只需要关心1,2,4,5部分,换句话说,我们需要训练的是 

1: FPN -- 特征提取部分

2: RPN -- feature maps进行卷积,生成对于anchor的delta部分,主要训练CNN filter

3: RCNN -- 主要训练根据ROI align以后得到特征,训练全连接网络计算正确的关于ROI box的delta, 和全连接网络,计算关于ROI box的正确类别

4: Mask -- 主要训练根据ROI align以后得到特征,进行mask回归的部分

训练的核心问题是选择哪些网络的输出进行训练,和怎么给这些选定的输出造target

下面分别介绍

训练部分的核心函数是 train_epoch(),函数的输入是7个值

#原始图像------------------------
images = inputs[0]#(1,3,1024,1024)

#图像meta数据------------------------
image_metas = inputs[1]#(1,89) 位数是(1+3+4+num_classes) 分别是(图像id 1个值, 图像形状3个值的list, window数据4个值的list, 81个数据的list,全部为0的81个数据)

#RPN的Label数据------------------------
#这里的rpn_match和rpn_bbox实际上是训练的label
rpn_match = inputs[2]#(1,261888,1) - 记录了每个anchor的正负样本情况

#这256个里面包含了正负anchor的数据,正anchor是delta数据,负anchor不考虑
rpn_bbox = inputs[3]#(1,256,4) -- 这256个里面包含了正负anchor的数据,正anchor是delta数据,负anchor不考虑

#图像的物体信息数据------------------------
gt_class_ids = inputs[4] #(1,2) -- bbox对应的物体类别
gt_boxes = inputs[5]  #(1,2,4) -- bbox对应的坐标 1024图像像素坐标
gt_masks = inputs[6]  #(1,2,56,56)

1:RPN训练数据选择和训练目标构建

这部分大逻辑是,RPN的输出box值,是通过anchor与gt box进行匹配来找出那些RPN box需要进行训练,匹配大的anchor对应的RPN box作为正样本,匹配小的anchor对应的RPN box作为负样本,正样本需要训练box delta和 box objectness 概率值,负样本只需要训练objectness概率值。这部分训练可以说是相对独立的。

(1) RPN预测输出的原始数据: 

rpn_class_logits----(1,216888,2) 代表了每个RPN box是物体和背景的类别

rpn_probs, rpn_bbox---(1,216888,4) 代表了每个RPN box相对于anchor的delta数据

(2) 刷选RPN输出数据进行训练

通过anchor与gt box的匹配,找出需要对哪些RPN box进行训练,筛选函数是在build_rpn_target()中

# 函数输入:
#anchors - (1,261888,4) 所有的anchor数据
#gt_boxes - (4,4) gt box坐标
#gt_class_ids -- (4,) gt 类别

# 函数输出: RPN Targets
# (1)标记哪些是positive anchor
# rpn_match -- (261888,) --- 1表示和gt box match上的, -1表示没有match上, 0表示中性
# (2)positive anchor所对应的RPN需要预测的目标值
# [N, (dy, dx, log(dh), log(dw))] Anchor bbox deltas
# rpn_bbox  -- (256,4) -- 为match上的anchor,与gt box的delta,这里256代表了有256个anchor match上了-- 实际上这个delta也是RPN应该要输出的值
# RPN_TRAIN_ANCHORS_PER_IMAGE = 256 这里的256是总共选择训练的anchor数目


# 函数作用:
# 给定anchors和GT boxes, 计算overlaps, 然后找出postive anchors, 计算出这些postive anchor的delta,从而使这些postive anchor来预测GT boxes
# 返回值rpn_match的形状是(261888,)记录了每个anchor为正负anchor的情况, 1:正anchor, -1:负anchor, 0为中性,不考虑, 其中正负anchor一共256个,不超过一半
# rpn_bbox 的形状(256,4) 为正anchor需要预测的delta, 负anchor再这里不考虑,为全0值,也就是说不需要计算delta
# 负的anchor box delta为0
rpn_match, rpn_bbox = build_rpn_targets(image.shape, self.anchors, gt_class_ids, gt_boxes, self.config)

核心问题: 从261888个anchor中找出128个正anchor和128负anchor来训练,是以anchor为中心来考虑问题

负anchor,与所有gt box IOU小与0.3的

正anchor 1,是各个gt box IOU对应最大的anchor

正anchor 2,与任意gt box IOU大于0.7

如果正负anchor有超过256一半的,那么就随机选取子集合

(3) 刷选的结果和对应的target

筛选的结果为 :

rpn_match -- (261888,1) 1:正anchor, -1:负anchor, 0为中性,不考虑, 其中正负anchor一共256个,不超过一半

Target是:

rpn_bbox --(256,4) 为正anchor需要预测的delta, 负anchor再这里不考虑,为全0值,也就是说不需要计算delta,正anchor的数据排布在rpn_bbox的前面

2:RCNN classifier训练数据选择和训练目标构建

首先需要回忆一下RCNN classifier这部分在inference时候的输入和输出和这部分的作用

输入数据:

rpn_rois (1000,4),1000个box的数据

Feature maps 数据 [p2_out, p3_out, p4_out, p5_out] (1,256,256,256) (1,256,128,128)(1,256,64,64)(1,256,32,32)

输出数据:

mrcnn_class_logits (1000,81) 结果是这1000个ROI所属物体的类别的score
mrcnn_class (1000,81) 结果是这1000个ROI所属物体的类别的概率
mrcnn_bbox (1000,81,4) 结果是1000个ROI对每个类别所产生的bbox的偏移,这里不是真正的坐标,只是针对RPN结果的delta

函数的作用:

(1): 计算出这些ROIs属于哪些类别

(2): 计算出这些ROIs 对于各个类别需要调整的box 坐标

--附加信息-----------------------------------------------------------------------------------------------------------------------------------------------------------

那么对于classifier来说,在训练的时候,是需要首先计算得到rpn_rois数据,这部分实际上是和前向传播一样,用RPN得到原始的很多个box,然后根据box的objectness score值调用proposal_layer()函数进行筛选,得到最后的rpn_rois, 只是在训练和测试的时候筛选rpn_rois的个数是不一样的,一个是1000,一个是2000。所以总结一下,对于classifier来说,不用管在它之前的数据运算,只需要关注自己的输入,和期望的输出即可。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

那么训练Classifier的逻辑是

在前向传播中,逻辑是

(1) 这里的rpn_rois输入认为都是物体出现的可能区域,是objectness值比较大的box

(2) classfier的输出是原始rpn_rois的各个类别概率,并且对原始rpn_rois的一个按照类别box调整

那么后续就是对各个类别的概率进一步排序,取阈值,留下一些box,认为是物体出现的位置, 是对rpn_rois的一个不断的选择的过程。

在训练过程中,逻辑是

现在再来按照前面训练RPN的逻辑整理一下思路

(1) Classifier的输出数据是

mrcnn_class_logits (1000,81) 结果是这1000个ROI所属物体的类别的score
mrcnn_class (1000,81) 结果是这1000个ROI所属物体的类别的概率
mrcnn_bbox (1000,81,4) 结果是1000个ROI对每个类别所产生的bbox的偏移,这里不是真正的坐标,只是针对RPN结果的delta,这里为什么还是delta,开始不解,后来想了一下很简单,必须是delta,因为每次输入的数据是一个box坐标值,一直在变化,所以最后的输出也需要根据输入来进行调整。

后面的分析是关键,

(1) 在前向传播的时候,认为rpn_rois的box都是物体可能出现的位置,计算rpn_rois这个值的时候,也是对于rpn_rois的box框置信度进行从大到小排序;但是这里的在训练的时候,rpn_rois作为classifier的输入来说又是另外一种情况,对于classifier来说rpn_rois可能会出现在物体附件,也可能会出现在背景部分(虽然前面已经按照置信度排序了,但还是会有一些框在背景部分),所以对于classifier来说需要进一步的对rpn_rois进行处理,判断类别和调整框的位置。让出现的物体附近的rpn_rois输出物体类别同时调整坐标,而出现在背景位置的rpn_rois输出背景类别,这时候不需要调整方框坐标。

(2)正常的情况下,我们需要知道上面mrcnn_class (1000,81) 和mrcnn_bbox(1000,81,4)所期望的目标值,需要造出各自的target或label,即 mrcnn_class_target (1000,81) 和 mrcnn_bbox_target (1000,81,4),对于label信息,我们现在手上只有 gt box的位置和类别。所以这里需要用gt box的数据来给classifier的输出造label。

这里可以按照RPN的做法一样,对1000个输出数据,用1000个rpn_rois输入数据和gt box进行匹配,把这1000个rpn_rois的输出数据即mrcnn_class (1000,81) 和mrcnn_bbox(1000,81,4)分为正负样本,然后正样本来拟合类别值和调整坐标值,负样本只是拟合类别值。这样的做法是有一个效率问题,实际上可以先对rpn_rois进行筛选,把rpn_rois本身分为正负样本,考虑到由于gt box的个数偏小,所以为了正负样本平衡的问题,比如目前只有1个gt box, 那么最后可能匹配到的正rpn_rois是10个,负的rpn_rois选取20个,那么1000个样本选出来以后,得到30个rpn_rois,然后在进入classifier进行训练,这样的话就减少了9970个classifier的计算。

相当于是在网络的输入阶段进行了筛选,实际上在训练RPN的时候也可以采用同样的方法,先依据anchor和gt box的匹配情况,把anchor划分为正负样本,然后只对这些anchor对应的cell做RPN,然后训练RPN, 但是实际编程比较麻烦。

(2) 训练Classifier数据筛选,在输入的阶段就进行筛选

刷选的过程是在函数detection_target_layer()中完成

"""
# 输入 -- RPN网络实际输出roi值和gt信息
# rpn_rois -- (1,2000,4) -- RPN网络实际输出的delta和anchor和综合以后的数据
# gt_class_ids -- (1,6) -- 图像物体的类别
# gt_boxes -- (1,6,4)   -- 图像物体的坐标
# gt_masks -- (1,6,56,56) -- mask是0,1数据

# 输出 -- 对RPN网络的roi进行筛选, 结合gt box数据,从中筛选一些rois作为正负样本进行训练
# rois -- (9,4) --- 从2000个rois筛选出来的正负roi, 负的ROIs也是ROI,是有实际意义的在原图中的方框
# target_class_ids -- (9,) 这9个值里面包含了背景类别,背景类别为0
# target_deltas -- (9,4) 实际上就3前面3个box有值 -- 这个delta是拿roi的box值和gt box值比,相当于是对roi进行一个delta调整的目标值
# target_mask -- (9,28,28) -- 这个28x28的mask是对原始gt box 的56x56的mask进行roi align来获得

# 输出
# rois -- (N,4) N个ROis, 里面包含了正负ROIs的坐标值, 把原始的2000个筛选到了N个
# roi_gt_class_ids --- (N,) N个ROIs的类别值 ---存在背景的class,即class=0
# deltas -- (N,4) --- N个ROis的box delta值 ---对背景的box, box delta = 0
# masks --(N,28,28)---N个ROis的mask值 ---对背景的box, mask = 0
# 实际上这4个输出数据是为了训练classifier和mask,其中rois是classifier和mask head的输入, 中间两个是classifier的输出, 最后一个为mask的输出
"""
# 这里对roi进行筛选的方法是,进一步计算roi和gt的overlap 对于IOU>0.5的作为正样本,而IOU<0.5的做为负样本
# TRAIN_ROIS_PER_IMAGE = 200, 包含了总共的正负ROI个数,并且其中正样本占0.33 - 66个, 负样本134个,任何部分超过这个限制,那么就随机取子集
# 在筛选roi的同时,也把这些roi所对应的训练target找到
def detection_target_layer(proposals, gt_class_ids, gt_boxes, gt_masks, config):

对于函数的4个输出

""""
# 输出 -- 对RPN网络的roi进行筛选, 结合gt box数据,从中筛选一些rois作为正负样本进行训练
# rois -- (9,4) --- 从2000个rois筛选出来的正负roi, 负的ROIs也是ROI,是有实际意义的在原图中的方框
# target_class_ids -- (9,) 这9个值里面包含了背景类别,背景类别为0
# target_deltas -- (9,4) 实际上就3前面3个box有值 -- 这个delta是拿roi的box值和gt box值比,相当于是对roi进行一个delta调整的目标值
# target_mask -- (9,28,28) -- 这个28x28的mask是对原始gt box 的56x56的mask进行roi align来获得

# rois -- (N,4) N个ROis, 里面包含了正负ROIs的坐标值, 把原始的2000个筛选到了N个
# roi_gt_class_ids --- (N,) N个ROIs的类别值 ---存在背景的class,即class=0
# deltas -- (N,4) --- N个ROis的box delta值 ---对背景的box, box delta = 0
# masks --(N,28,28)---N个ROis的mask值 ---对背景的box, mask = 0
# 实际上这4个输出数据是为了训练classifier和mask,其中rois是classifier和mask head的输入, 中间两个是classifier的输出, 最后一个为mask的输出
"""

最为关键的是这里的rois-(9,4)是对原始rpn_rois的一个取子集的操作,从下面的代码可以看出来

positive_rois = proposals[positive_indices.data,:]
 rois = torch.cat((positive_rois, negative_rois), dim=0)#(N,4)

函数作用:

对rpn_rois,结合gt box进一步筛选,并且生成被筛选出来的rois的坐标delta目标和类别目标,mask 目标,为训练classifier和mask做准备。这里和mask相关的下面解释

函数逻辑总结:
(1)首先从2000个ROI刷选出一些Positive ROIs, 条件是与gt box IOU满足>0.5,并且随机选择了66个,TRAIN_ROIS_PER_IMAGE = 200, 包含了总共的正负ROI个数,并且其中正样本占0.33 - 66个, 负样本134个,任何部分超过这个限制,那么就随机取子集
(2)为正ROIs计算其target, 包括delta box, class_id, mask
(3)从2000个ROI里面筛选出来一些Negative ROIs, 条件是与gt box IOU满足<0.3,并且随机选择一定的数目,保证正负样本比为1:3,Negative ROIs的target类别是背景为0,而target delta box是没有的,为0。
(4)整合正负ROis的target,输出rois,roi_gt_class_ids,deltas,masks

3:Mask部分训练

首先需要回忆一下Mask这部分在inference时候的输入和输出和这部分的作用

输入数据:

mrcnn_feature_maps = [p2_out, p3_out, p4_out, p5_out] (1,256,256,256) (1,256,128,128)(1,256,64,64)(1,256,32,32)

detection_boxes = (16,4)

输出数据:

(16,81,28,28) --- 代表的每个类别mask的score value的sigmoid以后的值

函数的作用:

对于检测到的物体(16,4)个物体,在特征图中再次做ROI align,然后upsample输出其各个类别的mask,这里的(28,28)的平面数据是物体的形状描述,真实label数据是一个二值数据。

训练逻辑

那么对于mask部分,在训练的时候,输入部分,可以用classifier的结果,也就是最后detection以后的结果作为detection box坐标的输入,也可以将rpn_rois的Box作为输入,训练rpn_rois 所对应box的mask. 这里是选择了后者。

所以在inference和train的时候,mask函数的输入是不一样的。

在inference的时候是如下,这里的detection box是检测结果,形状(16,4)

 mrcnn_mask = self.mask(mrcnn_feature_maps, detection_boxes)

在train的时候是如下,这里的rois是训练classifier的输入,是从2000个rpn_rois中筛选出来的,形状是(9,4)

mrcnn_mask = self.mask(mrcnn_feature_maps, rois)

Mask对应的label生成

这里的问题需要注意的是,gt box本身有一个mask是确定的,而rois也框的有一个区域和这个区域有一个roi align 出来的(16,81,28,28) feature也是确定,那么核心问题就是让这个(16,81,28,28) feature去拟合gt box对应的mask还是拟合其他的,这里肯定不能拟合gt box对应的mask,因为这时候的rois和gt box本身就有一个差别,所以需要计算rois目前所对应的mask.

这里在计算ROI对应mask的时候,又采用了一个box相对坐标之间的转换。具体可以专门写一个文章解析一下

   # Assign positive ROIs to GT masks -- 实际上是获得了ROI所对应的gt mask数据
        roi_masks = gt_masks[roi_gt_box_assignment.data,:,:]#

        # Compute mask targets
        # 这句是没有用的废话
        boxes = positive_rois#(13,4) -- 坐标相对原图的roi

        if config.USE_MINI_MASK:#True
            # Transform ROI corrdinates from normalized image space to normalized mini-mask space.
            # 做ROI align的时候,是以gt box生成的56x56的图片为基准来做
            # 这里是把roi对应的gt mask看作一个整图,然后计算出roi在gt mask中所对应的坐标,相当于是在gt mask中能够索引到roi所对应的部分
            # 实际上没有引入新的数据概念,只是方便计算roi所实际对应的mask而已
            # 本来应该是用图像的整体坐标来索引roi对应的mask数据,这里是直接用缩放过后的gt mask来进行转换

            #这里的roi左边和gt box坐标都是原图坐标,并且是小数坐标
            y1, x1, y2, x2 = positive_rois.chunk(4, dim=1)#positive ROI的四个坐标 ---y1,x1,y2,x2的形状一样,都是(13,1) --- 这个还是原图中的坐标, 已经是float坐标了,小于1
            gt_y1, gt_x1, gt_y2, gt_x2 = roi_gt_boxes.chunk(4, dim=1)#gt box的四个坐标 -- 也是原图中的坐标

            gt_h = gt_y2 - gt_y1#gt box的高度 -- 小数高度
            gt_w = gt_x2 - gt_x1#gt box的宽度 -- 小数宽度

            #这时候那么x,y应该能够有正
            # (1)减法作用是: roi box坐标先是转换到gt box为基准的左边,相当于是点坐标的转换
            # (2)除以gt box的尺寸,相当于是对当前点,对gt box的尺寸做归一化
            y1 = (y1 - gt_y1) / gt_h# - 用gt box高度宽度进行归一化
            x1 = (x1 - gt_x1) / gt_w#
            y2 = (y2 - gt_y1) / gt_h#
            x2 = (x2 - gt_x1) / gt_w#

            boxes = torch.cat([y1, x1, y2, x2], dim=1)#(13,4) -- 坐标相对gt box的roi (这里的gt box本身已经 被resize到56*56)

上面是坐标转换,把图像基准的roi坐标,转换为以gt box为基准的坐标,然后调用ROI align那套东西

  #box_ids 0 ~ roi_masks.size()-1的数据 --- 0~12
        box_ids = Variable(torch.arange(roi_masks.size()[0]), requires_grad=False).int()

        if config.GPU_COUNT:
            box_ids = box_ids.cuda()

        # MASK_SHAPE = [28, 28]
        #roi_masks.unsqueeze(1) - 结果(1,13,56,56) --- 认为是输入图像 --这里是roi对应的gt box mask值, unsqueeze增加一个维度
        #boxes (13,4) -- 在这个上面截取(13,4)个区域
        #box_ids (13,)

        # CropAndResizeFunction 类
        # 构造函数: __init__(self, crop_height, crop_width, extrapolation_value=0):
        # 前行传播函数:forward(self, image, boxes, box_ind): MASK_SHAPE -- 这里是28
        # 输出(13,28,28)#

        #RoIAlign based on crop_and_resize.
        #See more details on https://github.com/ppwwyyxx/tensorpack/blob/6d5ba6a970710eaaa14b89d24aace179eb8ee1af/examples/FasterRCNN/model.py#L301
        #:param featuremap: NxCxHxW
        #:param boxes: Mx4 float box with (x1, y1, x2, y2) **without normalization**
        #:param box_ind: M
        #:return: MxCxoHxoW

        #先创建在调用对象
        masks = Variable(CropAndResizeFunction(config.MASK_SHAPE[0], config.MASK_SHAPE[1], 0)(roi_masks.unsqueeze(1), boxes, box_ids).data, requires_grad=False)

Mask训练的loss

Mask本身输出数据(16,81,28,28) --- 代表的每个类别mask的score value的sigmoid以后的值。

那么算loss的时候,是取出相应的类别的(28,28)值,然后计算相应的0,1拟合loss, 就是所谓的Binary cross entropy loss

        # Binary cross entropy
        loss = F.binary_cross_entropy(y_pred, y_true)#这个是每个位置预测0或者1

 

【其他部分随便整理】

1:对于ROI align的调用位置统计

"""
###############ROI Align调用位置总结#################################
# POOL_SIZE 7x7
# MASK_POOL_SIZE 7x7
#
# 这里的ROI align相当于是做了两次
# ----------------------------第一次是:----------------------------
# 特点:
# (1)pool size 是 7x7
# (2)输入的坐标是rpn得到的1000个rois,是(1,1000,4)
# (2)得到的结果是对这1000个rois做类别的检测和坐标偏差的回归
#
# 调用位置: mrcnn_class_logits, mrcnn_class, mrcnn_bbox =
self.classifier(mrcnn_feature_maps, rpn_rois)
#
# 输入数据:
# x = mrcnn_feature_maps --- [p2_out, p3_out, p4_out, p5_out] 
(1,256,256,256) (1,256,128,128)(1,256,64,64)(1,256,32,32)
# rois = rpn_rois --- (1,1000,4)
#
# 输出数据:
# x = (1000,256,7,7) 为这1000个roi做roi align以后的结果数据
#
# 函数作用:
# 输入feature map list和rois,对每个roi做roi align,得到(256,7,7)的结果
# 在做ROI align的时候,有一个核心的问题,是level的选择,
因为这时候的rois只有位置数据,那么有一个问题是怎么一个roi对应到4个feature map上的哪一个
#
# ----------------------------第二次是:----------------------------
# 特点:
# (1)pool size 是 14x14
# (2)输入的坐标是对检测得到的结果(对RPN调整筛选以后的值)是(16,4)
# (2)得到的结果是这16个检测到的物体的mask,大小是
#
#
#
# 调用位置: mrcnn_mask = self.mask(mrcnn_feature_maps, detection_boxes)
#
# 输入数据:
# mrcnn_feature_maps = [p2_out, p3_out, p4_out, p5_out] (1,256,256,256) (1,256,128,128)
(1,256,64,64)(1,256,32,32)
# detection_boxes = (16,4)
#
# 输出数据:
# mrcnn_mask = (16,81,28,28) --- 代表的每个类别 mask的 score value的sigmoid以后的值
#
# 函数作用:
# 输入feature map list和rois,对每个检测结果做roi align,得到(256,14,14)的结果
#
#
# ----------------------------比较这两次ROI align的做法----------------------------
# (1)pool size一个是7x7, 一个是14x14
# (2)传入的feature list, 都是 P2~P5
# (3)不同的是坐标不一样,一个是RPN输出的ROI,(1,1000,4), 一个是检测得到的detection result, (16,4)
# (4)在ROI align这一步对于每个box,输出的结果,一个是 (256,7,7), 一个是(256,14,14)
#
# ---------------------------这里有一个比较明显的特征----------------------------
# 对于同样的Feature map list,
# (1)在用RPN生成ROIs的时候,用的是 P2~P6
# (2)在用classifier函数对ROIs(1000个)进行类别判断,坐标回归的时候,用的是P2~P5
# (3)在用mask函数对detection result进行mask判断的时候,用的是P2~P5
# 可以发现对于所有的任务,都是公用了一个feature map list
#################################################################################
"""

【参考文献】

https://github.com/multimodallearning/pytorch-mask-rcnn

猜你喜欢

转载自blog.csdn.net/hnshahao/article/details/81231211