1. 程式人生 > >Mask RCNN 原始碼解析 (1)

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,

[email protected]@@@@@@@@@@座標相關-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
#################################################################################
"""

【參考文獻】

相關推薦

Mask RCNN 原始碼解析 (1)

Mask RCNN 屬於 RCNN這一系列的應該是比較最終的版本,融合多種演算法的思想,這裡對Mask RCNN從原始碼進行解析,主要寫幾篇文章,一個總結大的思路,其他文章整理細節。 這篇文章為了簡單,主要從前向傳播和後向傳播,分兩部分進行介紹,主要以資料的流動為主線,分析

BItCoin原始碼解析(1)——Base58編碼

看了https://blog.csdn.net/pure_lady/article/category/5858993/2好久,決定寫下開篇。 比特幣加密演算法一共有兩類:非對稱加密演算法(橢圓曲線加密演算法)和雜湊演算法(SHA256,RIMPED160演算法)。比特幣私鑰(private ke

Google guava cache原始碼解析1--構建快取器(2)

此文已由作者趙計剛授權網易雲社群釋出。 歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。 CacheBuilder-->maximumSize(long size)     /**      * 

mask-RCNN 學習筆記1

摘要: 我們提出了一個概念上簡單,靈活,通用的物件例項分割框架。我們的方法有效地檢測影象中的物件,同時為每個例項生成高質量的分割掩碼。該方法稱為Mask R-CNN,通過增加一個與現有分支並行的用於預測掩碼物件的分支來增強Fast R-CNN的邊界檢測能力。 Mask

Google guava cache原始碼解析1--構建快取器(3)

此文已由作者趙計剛授權網易雲社群釋出。 歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。 下面介紹在LocalCache(CacheBuilder, CacheLoader)中呼叫的一些方法: CacheBuilder-->getConcurrencyLevel()

Tensorflow原始碼解析1 -- 核心架構和原始碼結構

1 主流深度學習框架對比 當今的軟體開發基本都是分層化和模組化的,應用層開發會基於框架層。比如開發Linux Driver會基於Linux kernel,開發Android app會基於Android Framework。深度學習也不例外,框架層為上層模型開發提

OkHttp 原始碼解析(1)

專案用到OkHttp,準備研究研究(OkHttp現在很火啊,Retrofit使用OkHttp,Volley支援替換底層http棧為OkHttp,甚至Google的最新原始碼裡,都用起了OkHttp,替換了原來用的HttpClient)。 OkHttp在網路

Mask-RCNN技術解析

一. Mask-RCNN 介紹        上篇文章介紹了 FCN,這篇文章引入個新的概念 Mask-RCNN,看著比較好理解哈,就是在 RCNN 的基礎上新增 Mask。        Mask-RCNN 來自於年輕有為的 Kaiming 大神,通過在 Faste

VS2015中STL原始碼解析1(霜之小刀)

VS2015中STL原始碼解析1(霜之小刀) QQ:2279557541 Email:[email protected] 1. 宣告 2 1. 宣告 本文參考了大量http:

JVM類載入機制(ClassLoader)原始碼解析(1)

其實JVM類載入機制,簡單地說就是類管理,也就是我們生成的class檔案。 三個步驟:裝載(load)、連結(link)、解析(Resolve)、還有初始化(Initialize) 關於網上有很多講解載入的方式,和呼叫的方式,還是幾個基本的classLoader,這裡就不在

faster rcnn原始碼解析

之前一直是使用faster rcnn對其中的程式碼並不是很瞭解,這次剛好復現mask rcnn就仔細閱讀了faster rcnn,主要參考程式碼是pytorch-faster-rcnn ,部分參考和借用了以下部落格的圖片 [1] CNN目標檢測(一

mask rcnn原始碼模型踩坑(__reduce_cython__)指南(MxNet)

mask rcnn模型踩坑指南(MxNet框架): 1.環境:anaconda3+tensorflow-gpu+python2.7.14 pip freeze: certifi==2017.11.5 / Cython==0.27.3 / easydict==1.7 / fr

Faster—RCNN原始碼解析之demo.py

1、模型選擇,以及分類型別: CLASSES = ('__background__', 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car',

Spring 事物原始碼解析1-事務概念

資料庫事務概述  事務首先是一系列操作組成的工作單元,該工作單元內的操作是不可分割的,即要麼所有操作都做,要麼所有操作都不做,這就是事務。 1事務特性 事務必需滿足ACID(原子性、一致性、隔離性和永續性)特性,缺一不可: 原子性(Atomicity

比特幣原始碼解析(1)

0x00 寫在前面 研究了這麼久的區塊鏈卻一直都沒有完整的看過一個區塊鏈專案的程式碼,甚至還一度沉迷各種ICO,每天看著各種貨幣層出不窮,跌跌漲漲,起起伏伏,不亦樂乎。現在看來,也許整體來講賺了點小錢,可是那又有什麼意義呢?終究不是長久之計。這兩天終於靜下來大

Java原始碼解析(1) —— Object

Java基類Object   java.lang.Object,Java所有類的父類,在你編寫一個類的時候,若無指定父類(沒有顯式extends一個父類)編譯器(一般編譯器完成該步驟)會預設的新增Object為該類的父類(可以將該類反編譯看其位元組碼,不過貌似

Spring5原始碼解析1-從啟動容器開始

@Configuration @ComponentScan public class AppConfig { } public class Main { public static void main(String[] args) { AnnotationConfigApplicationConte

大資料基礎(1)zookeeper原始碼解析

五 原始碼解析   public enum ServerState { LOOKING, FOLLOWING, LEADING, OBSERVING;}zookeeper伺服器狀態:剛啟動LOOKING,follower是FOLLOWING,leader是LEADING,observer是

LinkedList實現原理以及原始碼解析1.7)

LinkedList實現原理以及原始碼解析(1.7) 在1.7之後,oracle將LinkedList做了一些優化, 將1.6中的環形結構優化為了直線型了連結串列結構。 1、LinkedList定義: public class LinkedList<E>

ArrayList實現原理以及原始碼解析(補充JDK1.7,1.8)

ArrayList實現原理以及原始碼解析(補充JDK1.7,1.8) ArrayList的基本知識在上一節已經討論過,這節主要看ArrayList在JDK1.6到1.8的一些實現變化。 JDK版本不一樣,ArrayList類的原始碼也不一樣。 1、ArrayList類結構: