mask-rcnn解讀

原理解讀

如果不瞭解Faster-RCNN 的,灰常有必要先搞懂Faster-RCNN

https://blog.csdn.net/u010901792/article/details/100041135

簡介

在這裏插入圖片描述

​ 圖1 Mask R-CNN整體架構

Mask R-CNN是一個實例分割(Instance segmentation)算法,可以用來做“目標檢測”、“目標實例分割”、“目標關鍵點檢測”。

  1. 實例分割(Instance segmentation)和語義分割(Semantic segmentation)的區別與聯繫

聯繫:語義分割和實例分割都是目標分割中的兩個小的領域,都是用來對輸入的圖片做分割處理;

區別:

在這裏插入圖片描述

​ 圖2 實例分割與語義分割區別

  1. 通常意義上的目標分割指的是語義分割,語義分割已經有很長的發展歷史,已經取得了很好地進展,目前有很多的學者在做這方面的研究;然而實例分割是一個從目標分割領域獨立出來的一個小領域,是最近幾年才發展起來的,與前者相比,後者更加複雜,當前研究的學者也比較少,是一個有研究空間的熱門領域,如圖3所示,這是一個正在探索中的領域;

在這裏插入圖片描述
​ 圖3 實例分割與語義分割區別

  1. 觀察圖3中的c和d圖,c圖是對a圖進行語義分割的結果,d圖是對a圖進行實例分割的結果。兩者最大的區別就是圖中的"cube對象",在語義分割中給了它們相同的顏色,而在實例分割中卻給了不同的顏色。即實例分割需要在語義分割的基礎上對同類物體進行更精細的分割。

Mask R-CNN是一個非常靈活的框架,可以增加不同的分支完成不同的任務,可以完成目標分類、目標檢測、語義分割、實例分割、人體姿勢識別等多種任務

總體架構

Mask-RCNN 大體框架還是 Faster-RCNN 的框架,可以說在基礎特徵網絡之後又加入了全連接的分割子網,由原來的兩個任務(分類+迴歸)變爲了三個任務(分類+迴歸+分割)。Mask R-CNN 採用和Faster R-CNN相同的兩個階段:

第一個階段具有相同的第一層(即RPN),掃描圖像並生成提議(proposals,即有可能包含一個目標的區域);

第二階段,除了預測種類和bbox迴歸,並添加了一個全卷積網絡的分支,對每個RoI預測了對應的二值掩膜(binary mask),以說明給定像素是否是目標的一部分。所謂二進制mask,就是當像素屬於目標的所有位置上時標識爲1,其它位置標識爲 0。示意圖如下:

在這裏插入圖片描述

這樣做可以將整個任務簡化爲mulit-stage pipeline,解耦了多個子任務的關係,現階段來看,這樣做好處頗多。

總體流程如下:

在這裏插入圖片描述

  • 首先,輸入一幅你想處理的圖片,然後進行對應的預處理操作,或者預處理後的圖片;

  • 然後,將其輸入到一個預訓練好的神經網絡中(ResNet等)獲得對應的feature map;

  • 接着,對這個feature map中的每一點設定預定個的ROI,從而獲得多個候選ROI;

  • 接着,將這些候選的ROI送入RPN網絡進行二值分類(前景或背景)和BB迴歸,過濾掉一部分候選的ROI;

  • 接着,對這些剩下的ROI進行ROIAlign操作(即先將原圖和feature map的pixel對應起來,然後將feature map和固定的feature對應起來);

  • 最後,對這些ROI進行分類(N類別分類)、BB迴歸和MASK生成(在每一個ROI裏面進行FCN操作)。

在這裏插入圖片描述

其中 黑色部分爲原來的 Faster-RCNN,紅色部分爲在 Faster網絡上的修改:

與faster RCNN的區別:

  • ResNet+FPN

    作者替換了在faster rcnn中使用的vgg網絡,轉而使用特徵表達能力更強的殘差網絡。

    另外爲了挖掘多尺度信息,作者還使用了FPN網絡。

  • 將 Roi Pooling 層替換成了 RoiAlign;

    解決Misalignment 的問題,說白了就是對 feature map 的插值。直接的ROIPooling的那種量化操作會使得得到的mask與實際物體位置有一個微小偏移;

  • 添加並列的 FCN層(mask層);

    FCN全卷積神經網絡(semantic segmentation語義分割)

架構分解

各大部件原理講解

backbone

backbone是一系列的卷積層用於提取圖像的feature maps,比如可以是VGG16,VGG19,GooLeNet,ResNet50,ResNet101等,這裏主要講解的是ResNet101的結構。

​ ResNet(深度殘差網絡)實際上就是爲了能夠訓練更加深層的網絡提供了有利的思路,畢竟之前一段時間裏面一直相信深度學習中網絡越深得到的效果會更加的好,但是在構建了太深層之後又會使得網絡退化。ResNet使用了跨層連接,使得訓練更加容易。

在這裏插入圖片描述

網絡試圖讓一個block的輸出爲f(x) + x,其中的f(x)爲殘差,當網絡特別深的時候殘差f(x)會趨近於0,從而f(x) + x就等於了x,即實現了恆等變換,不管訓練多深性能起碼不會變差。

在網絡中只存在兩種類型的block,在構建ResNet中一直是這兩種block在交替或者循環的使用,所有接下來介紹一下這兩種類型的block(indetity block, conv block):

在這裏插入圖片描述
​ 跳過三個卷積的identity block

圖中可以看出該block中直接把開端的x接入到第三個卷積層的輸出,所以該x也被稱爲shortcut,相當於捷徑似得。注意主路上第三個卷積層使用激活層,在相加之後才進行了ReLU的激活。

在這裏插入圖片描述
​ 跳過三個卷積並在shortcut上存在的卷積的conv block

與identity block其實是差不多的,只是在shortcut上加了一個卷積層再進行相加。注意主路上的第三個卷積層和shortcut上的卷積層都沒激活,而是先相加再進行激活的。

其實在作者的代碼中,主路中的第一個和第三個卷積都是1*1的卷積(改變的只有feature maps的通道大小,不改變長和寬),爲了降維從而實現卷積運算的加速;注意需要保持shortcut和主路最後一個卷積層的channel要相同才能夠進行相加。

下面展示一下ResNet101的整體框架:

在這裏插入圖片描述
​ ResNet101整體架構

從圖中可以得知ResNet分爲了5個stage,C1-C5分別爲每個Stage的輸出,這些輸出在後面的FPN中會使用到。你可以數數,看看是不是總共101層,數的時候除去BatchNorm層。注:stage4中是由一個conv_block和22個identity_block,如果要改成ResNet50網絡的話只需要調整爲5個identity_block.

FPN(Feature Pyramid Networks)

FPN解決了什麼問題?

答:FPN的提出是爲了實現更好的feature maps融合,一般的網絡都是直接使用最後一層的feature maps,雖然最後一層的feature maps 語義強,但是位置和分辨率都比較低,容易檢測不到比較小的物體。FPN的功能就是融合了底層到高層的feature maps ,從而充分的利用了提取到的各個階段的特徵(ResNet中的C2-C5 )。

簡單來說,就是把底層的特徵和高層的特徵進行融合,便於細緻檢測

下面來看一下相似的網絡:

這裏寫圖片描述

​ FPN是爲了自然地利用CNN層級特徵的金字塔形式,同時生成在所有尺度上都具有強語義信息的特徵金字塔。所以FPN的結構設計了top-down結構和橫向連接,以此融合具有高分辨率的淺層layer和具有豐富語義信息的深層layer。這樣就實現了從單尺度的單張輸入圖像,快速構建在所有尺度上都具有強語義信息的特徵金字塔,同時不產生明顯的代價。

​ 上面一個帶有skip connection的網絡結構在預測的時候是在finest level(自頂向下的最後一層)進行的,簡單講就是經過多次上採樣並融合特徵到最後一步,拿最後一步生成的特徵做預測。

而FPN網絡結構和上面的類似,區別在於預測是在每一層中獨立進行的。後面的實驗證明finest level的效果不如FPN好,原因在於FPN網絡是一個窗口大小固定的滑動窗口檢測器,因此在金字塔的不同層滑動可以增加其對尺度變化的魯棒性。另外雖然finest level有更多的anchor,但仍然效果不如FPN好,說明增加anchor的數量並不能有效提高準確率。

在這裏插入圖片描述
​ FPN特徵融合圖

自下而上的路徑

CNN的前饋計算就是自下而上的路徑,特徵圖經過卷積覈計算,通常是越變越小的,也有一些特徵層的輸出和原來大小一樣,稱爲“相同網絡階段”(same network stage )。對於本文的特徵金字塔,作者爲每個階段定義一個金字塔級別, 然後選擇每個階段的最後一層的輸出作爲特徵圖的參考集。 這種選擇是很自然的,因爲每個階段的最深層應該具有最強的特徵。具體來說,對於ResNets,作者使用了每個階段的最後一個殘差結構的特徵激活輸出。將這些殘差模塊輸出表示爲{C2, C3, C4, C5},對應於conv2,conv3,conv4和conv5的輸出,並且注意它們相對於輸入圖像具有{4, 8, 16, 32}像素的步長(也就是感受野)。考慮到內存佔用,沒有將conv1包含在金字塔中。

自上而下的路徑和橫向連接

自上而下的路徑(the top-down pathway )是如何去結合低層高分辨率的特徵呢?方法就是,把更抽象,語義更強的高層特徵圖進行上取樣,然後把該特徵橫向連接(lateral connections )至前一層特徵,因此高層特徵得到加強。值得注意的是,橫向連接的兩層特徵在空間尺寸上要相同。這樣做應該主要是爲了利用底層的定位細節信息。

下圖顯示連接細節。把高層特徵做2倍上採樣(最鄰近上採樣法,可以參考反捲積),然後將其和對應的前一層特徵結合(前一層要經過1 * 1的卷積核才能用,目的是改變channels,應該是要和後一層的channels相同),結合方式就是做像素間的加法。重複迭代該過程,直至生成最精細的特徵圖。迭代開始階段,作者在C5層後面加了一個1 * 1的卷積核來產生最粗略的特徵圖,最後,作者用3 * 3的卷積核去處理已經融合的特徵圖(爲了消除上採樣的混疊效應),以生成最後需要的特徵圖。爲了後面的應用能夠在所有層級共享分類層,這裏作者固定了3*3卷積後的輸出通道爲d,這裏設爲256.因此所有額外的卷積層(比如P2)具有256通道輸出。這些額外層沒有用非線性。

{C2, C3, C4, C5}層對應的融合特徵層爲{P2, P3, P4, P5},對應的層空間尺寸是相通的。

這裏寫圖片描述

在這裏插入圖片描述

從圖中可以看出+的意義爲:左邊的底層特徵層通過1*1的卷積得到與上一層特徵層相同的通道數;上層的特徵層通過上採樣得到與下一層特徵層一樣的長和寬再進行相加,從而得到了一個融合好的新的特徵層。舉個例子說就是:C4層經過1*1卷積得到與P5相同的通道,P5經過上採樣後得到與C4相同的長和寬,最終兩者進行相加,得到了融合層P4,其他的以此類推。

​ 注:P2-P5是將來用於預測物體的bbox,box-regression,mask的,而P2-P6是用於訓練RPN的,即P6只用於RPN網絡中。

應用

Faster R-CNN+Resnet-101
要想明白FPN如何應用在RPN和Fast R-CNN(合起來就是Faster R-CNN),首先要明白Faster R-CNN+Resnet-101的結構,直接理解就是把Faster-RCNN中原有的VGG網絡換成ResNet-101,ResNet-101結構如下圖:

這裏寫圖片描述

Faster-RCNN利用conv1到conv4-x的91層爲共享卷積層,然後從conv4-x的輸出開始分叉,一路經過RPN網絡進行區域選擇,另一路直接連一個ROI Pooling層,把RPN的結果輸入ROI Pooling層,映射成7 * 7的特徵。然後所有輸出經過conv5-x的計算,這裏conv5-x起到原來全連接層(fc)的作用。最後再經分類器和邊框迴歸得到最終結果。整體框架用下圖表示:

這裏寫圖片描述

RPN中的特徵金字塔網絡

RPN是Faster R-CNN中用於區域選擇的子網絡,RPN是在一個13 * 13 * 256的特徵圖上應用9種不同尺度的anchor,本篇論文另闢蹊徑,把特徵圖弄成多尺度的,然後固定每種特徵圖對應的anchor尺寸,很有意思。也就是說,作者在每一個金字塔層級應用了單尺度的anchor,{P2, P3, P4, P5, P6}分別對應的anchor尺度爲{32^2, 64^2, 128^2, 256^2, 512^2 },當然目標不可能都是正方形,本文仍然使用三種比例{1:2, 1:1, 2:1},所以金字塔結構中共有15種anchors。RPN結構:

這裏寫圖片描述

從圖上看出各階層共享後面的分類網絡。這也是強調爲什麼各階層輸出的channel必須一致的原因,這樣才能使用相同的參數,達到共享的目的。

注意上面的p6,根據論文中所指添加:

這裏寫圖片描述

正負樣本的界定和Faster RCNN差不多:如果某個anchor和一個給定的ground truth有最高的IOU或者和任意一個Ground truth的IOU都大於0.7,則是正樣本。如果一個anchor和任意一個ground truth的IOU都小於0.3,則爲負樣本。

Fast R-CNN 中的特徵金字塔網絡

Fast R-CNN 中很重要的是ROI Pooling層,需要對不同層級的金字塔制定不同尺度的ROI。
ROI Pooling層使用region proposal的結果和中間的某一特徵圖作爲輸入,得到的結果經過分解後分別用於分類結果和邊框迴歸。
然後作者想的是,不同尺度的ROI使用不同特徵層作爲ROI pooling層的輸入,大尺度ROI就用後面一些的金字塔層,比如P5;小尺度ROI就用前面一點的特徵層,比如P4。那怎麼判斷ROI該用那個層的輸出呢?這裏作者定義了一個係數Pk,其定義爲:
這裏寫圖片描述

224是ImageNet的標準輸入,k0是基準值,設置爲5,代表P5層的輸出(原圖大小就用P5層),w和h是ROI區域的長和寬,假設ROI是112 * 112的大小,那麼k = k0-1 = 5-1 = 4,意味着該ROI應該使用P4的特徵層。k值應該會做取整處理,防止結果不是整數。
然後,因爲作者把conv5也作爲了金字塔結構的一部分,那麼從前全連接層的那個作用怎麼辦呢?這裏採取的方法是增加兩個1024維的輕量級全連接層,然後再跟上分類器和邊框迴歸,認爲這樣還能使速度更快一些。

總結

作者提出的FPN(Feature Pyramid Network)算法同時利用低層特徵高分辨率和高層特徵的高語義信息,通過融合這些不同層的特徵達到預測的效果。並且預測是在每個融合後的特徵層上單獨進行的,這和常規的特徵融合方式不同。

ROIAlign

ROI Pooling和ROIAlign最大的區別是:

前者使用了兩次量化操作,對於roi pooling,經歷了兩個量化的過程:

  • 從roi proposal到feature map的映射過程。如[x/16],這裏x是原始roi的座標值,而方框代表四捨五入。

  • 從feature map劃分成7*7的bin,每個bin使用max pooling。

而後者並沒有採用量化操作,使用了線性插值算法,具體的解釋如下所示。

ROI Pooling

在faster rcnn中,anchors經過proposal layer升級爲proposal,需要經過ROI Pooling進行size的歸一化後才能進入全連接網絡,也就是說ROI Pooling的主要作用是將proposal調整到統一大小。步驟如下:

  • 將proposal映射到feature map對應位置
  • 將映射後的區域劃分爲相同大小的sections
  • 對每個sections進行max pooling/avg pooling操作
    舉例說明:

[外鏈圖片轉存失敗(img-2TLQogVd-1566556125150)(C:\F\notebook\mask-rcnn\11.png)]

考慮一個8*8大小的feature map,經過一個ROI Pooling,以及輸出大小爲2*2.

1)輸入的固定大小的feature map (圖一)
2)region proposal 投影之後位置(左上角,右下角座標):(0,4),(4,4)(圖二)
3)將其劃分爲(2*2)個sections(因爲輸出大小爲2*2),我們可以得到(圖三) ,不整除時錯位對齊(Fast RCNN)

4)對每個section做max pooling,可以得到(圖四)

ROI Pooling侷限性

在常見的兩級檢測框架(比如Fast-RCNN,Faster-RCNN,RFCN)中,ROI Pooling 的作用是根據預選框的位置座標在特徵圖中將相應區域池化爲固定尺寸的特徵圖,以便進行後續的分類和包圍框迴歸操作。由於預選框的位置通常是由模型迴歸得到的,一般來講是浮點數,而池化後的特徵圖要求尺寸固定。故ROI Pooling這一操作存在兩次量化的過程。

將候選框邊界量化爲整數點座標值。從roi proposal到feature map的映射時,取[x/16],這裏x是原始roi的座標值,而方框代表四捨五入。 將量化後的邊界區域平均分割成 k x k 個單元(bin), 對每一個單元的邊界進行量化,每個bin使用max pooling。事實上,經過上述兩次量化,此時的候選框已經和最開始迴歸出來的位置有一定的偏差,這個偏差會影響檢測或者分割的準確度。在論文裏,作者把它總結爲“不匹配問題(misalignment)。

下面我們用直觀的例子具體分析一下上述區域不匹配問題。

在這裏插入圖片描述
​ ROI Pooling技術

如圖所示,爲了得到固定大小(7X7)的feature map,我們需要做兩次量化操作:

1)圖像座標 — feature map座標,

2)feature map座標 — ROI feature座標。

我們來說一下具體的細節,如圖我們輸入的是一張800x800的圖像,在圖像中有兩個目標(貓和狗),狗的BB大小爲665x665,經過VGG16網絡後,我們可以獲得對應的feature map,如果我們對卷積層進行Padding操作,我們的圖片經過卷積層後保持原來的大小,但是由於池化層的存在,我們最終獲得feature map 會比原圖縮小一定的比例,這和Pooling層的個數和大小有關。在該VGG16中,我們使用了5個池化操作,每個池化操作都是2Pooling,因此我們最終獲得feature map的大小爲800/32 x 800/32 = 25x25(是整數),但是將狗的BB對應到feature map上面,我們得到的結果是665/32 x 665/32 = 20.78 x 20.78,結果是浮點數,含有小數,但是我們的像素值可沒有小數,那麼作者就對其進行了量化操作(即取整操作),即其結果變爲20 x 20,在這裏引入了第一次的量化誤差;然而我們的feature map中有不同大小的ROI,但是我們後面的網絡卻要求我們有固定的輸入,因此,我們需要將不同大小的ROI轉化爲固定的ROI feature,在這裏使用的是7x7的ROI feature,那麼我們需要將20 x 20的ROI映射成7 x 7的ROI feature,其結果是 20 /7 x 20/7 = 2.86 x 2.86,同樣是浮點數,含有小數點,我們採取同樣的操作對其進行取整吧,在這裏引入了第二次量化誤差。其實,這裏引入的誤差會導致圖像中的像素和特徵中的像素的偏差,即將feature空間的ROI對應到原圖上面會出現很大的偏差。原因如下:比如用我們第二次引入的誤差來分析,本來是2.86,我們將其量化爲2,這期間引入了0.86的誤差,看起來是一個很小的誤差呀,但是你要記得這是在feature空間,我們的feature空間和圖像空間是有比例關係的,在這裏是1:32,那麼對應到原圖上面的差距就是0.86 x 32 = 27.52。這個差距不小吧,這還是僅僅考慮了第二次的量化誤差。這會大大影響整個檢測算法的性能,因此是一個嚴重的問題。

簡而言之:

做segment是pixel級別的,但是faster rcnn中roi pooling有2次量化操作導致了沒有對齊 .

ROIAlign

在這裏插入圖片描述

爲了解決ROI Pooling的上述缺點,作者提出了ROI Align這一改進的方法(如上圖)。ROI Align的思路很簡單:取消量化操作,使用雙線性插值的方法獲得座標爲浮點數的像素點上的圖像數值,從而將整個特徵聚集過程轉化爲一個連續的操作。如下圖所示:

藍色的虛線框表示卷積後獲得的feature map,黑色實線框表示ROI feature,最後需要輸出的大小是2x2,那麼我們就利用雙線性插值來估計這些藍點(虛擬座標點,又稱雙線性插值的網格點)處所對應的像素值,最後得到相應的輸出。這些藍點是2x2Cell中的隨機採樣的普通點,作者指出,這些採樣點的個數和位置不會對性能產生很大的影響,你也可以用其它的方法獲得。然後在每一個橘紅色的區域裏面進行max pooling或者average pooling操作,獲得最終2x2的輸出結果。我們的整個過程中沒有用到量化操作,沒有引入誤差,即原圖中的像素和feature map中的像素是完全對齊的,沒有偏差,這不僅會提高檢測的精度,同時也會有利於實例分割。

在這裏插入圖片描述
​ 雙線性插值

  • 遍歷每一個候選區域,保持浮點數邊界不做量化。

  • 將候選區域分割成k x k個單元,每個單元的邊界也不做量化。

  • 在每個單元中計算固定四個座標位置,用雙線性內插的方法計算出這四個位置的值,然後進行最大池化操作。

這裏對上述步驟的第三點作一些說明:這個固定位置是指在每一個矩形單元(bin)中按照固定規則確定的位置。比如,如果採樣點數是1,那麼就是這個單元的中心點。如果採樣點數是4,那麼就是把這個單元平均分割成四個小方塊以後它們分別的中心點。顯然這些採樣點的座標通常是浮點數,所以需要使用插值的方法得到它的像素值。在相關實驗中,作者發現將採樣點設爲4會獲得最佳性能,甚至直接設爲1在性能上也相差無幾。

事實上,ROI Align 在遍歷取樣點的數量上沒有ROIPooling那麼多,但卻可以獲得更好的性能,這主要歸功於解決了misalignment的問題。值得一提的是,我在實驗時發現,ROI Align在VOC2007數據集上的提升效果並不如在COCO上明顯。經過分析,造成這種區別的原因是COCO上小目標的數量更多,而小目標受misalignment問題的影響更大(比如,同樣是0.5個像素點的偏差,對於較大的目標而言顯得微不足道,但是對於小目標,誤差的影響就要高很多)。

在這裏插入圖片描述
​ ROIAlign技術

如圖所示,爲了得到爲了得到固定大小(7X7)的feature map,ROIAlign技術並沒有使用量化操作,即我們不想引入量化誤差,比如665 / 32 = 20.78,我們就用20.78,不用什麼20來替代它,比如20.78 / 7 = 2.97,我們就用2.97,而不用2來代替它。這就是ROIAlign的初衷。那麼我們如何處理這些浮點數呢,我們的解決思路是使用“雙線性插值”算法。雙線性插值是一種比較好的圖像縮放算法,它充分的利用了原圖中虛擬點(比如20.56這個浮點數,像素位置都是整數值,沒有浮點值)四周的四個真實存在的像素值來共同決定目標圖中的一個像素值,即可以將20.56這個虛擬的位置點對應的像素值估計出來。

roi-align總結:

對於每個roi,映射之後座標保持浮點數,在此基礎上再平均切分成k*k個bin,這個時候也保持浮點數。再把每個bin平均分成4個小的空間(bin中更小的bin),然後計算每個更小的bin的中心點的像素點對應的概率值。這個像素點大概率是一個浮點數,實際上圖像的浮點是沒有像素值的,但這裏假設這個浮點數的位置存儲一個概率值,這個值由相鄰最近的整數像素點存儲的概率值經過雙線性插值得到,其實也就是根據這個中心點所在的像素值找到所在的大bin對應的4個整數像素存儲的值,然後乘以多個參數進行插值。這些參數其實就是那4個整數像素點和中心點的位置距離關係構成參數。最後再在每個大bin中對4箇中心點進行max或者mean的pooling。

在這裏插入圖片描述

其實就是對每個格子再次劃分四個格子,進行四次採樣,每次採樣的位置都是四個格子的中心點,由於我知道了它的位置,但是不知道這個中心點位置的具體像素值是多少,所以採用雙線性插值法求這個中心點位置的具體像素(利用它周圍位置的像素點求),然後對着四個位置的像素值取最大值,得到這個大格子的輸出。

補充:

  • 雙線性差值

1.線性插值法

這裏講解線性插值法的推導爲了給雙線性插值公式做鋪墊。
  線性插值法是指使用連接兩個已知量的直線來確定在這個兩個已知量之間的一個未知量的值的方法。

在這裏插入圖片描述

  1. 雙線性插值

雙線性插值是插值算法中的一種,是線性插值的擴展。利用原圖像中目標點四周的四個真實存在的像素值來共同決定目標圖中的一個像素值,其核心思想是在兩個方向分別進行一次線性插值。

在這裏插入圖片描述

FCN

Fully Convolutional Networks 全卷積網絡是CNN在語義分割領域一次重大的突破,圖像語義分割,簡而言之就是對一張圖片上的所有像素點進行分類。

下面我們重點看一下FCN所用到的三種技術:

  • 不含全連接層(fc)的全卷積(fully conv)網絡。可適應任意尺寸輸入。
  • 大數據尺寸的反捲積(deconv)層。能夠輸出精細的結果。
  • 結合不同深度層結果的跳級(skip)結構。同時確保魯棒性和精確性

1.卷積化(convolutionalization)

分類所使用的網絡通常會在最後連接全連接層,它會將原來二維的矩陣(圖片)壓縮成一維的,從而丟失了空間信息,最後訓練輸出一個標量,這就是我們的分類標籤。

而圖像語義分割的輸出則需要是個分割圖,且不論尺寸大小,但是至少是二維的。所以,我們丟棄全連接層,換上卷積層,而這就是所謂的卷積化了。

img

這幅圖顯示了卷積化的過程,圖中顯示的是AlexNet的結構,簡單來說卷積化就是將其最後三層全連接層全部替換成卷積層。

全連接層轉換爲卷積層:

在兩種變換中,將全連接層轉化爲卷積層在實際運用中更加有用。假設一個卷積神經網絡的輸入是 224x224x3 的圖像,一系列的卷積層和下采樣層將圖像數據變爲尺寸爲 7x7x512 的激活數據體。AlexNet使用了兩個尺寸爲4096的全連接層,最後一個有1000個神經元的全連接層用於計算分類評分。我們可以將這3個全連接層中的任意一個轉化爲卷積層:

針對第一個連接區域是[7x7x512]的全連接層,令其濾波器尺寸爲F=7,這樣輸出數據體就爲[1x1x4096]了。

針對第二個全連接層,令其濾波器尺寸爲F=1,這樣輸出數據體爲[1x1x4096]。

對最後一個全連接層也做類似的,令其F=1,最終輸出爲[1x1x1000]

2.上採樣(Upsampling)

實際上,上採樣(upsampling)一般包括2種方式:

  1. Resize,如雙線性插值直接縮放,類似於圖像縮放(這種方法在原文中提到)
  2. Deconvolution,也叫Transposed Convolution。

傳統的網絡是subsampling的,對應的輸出尺寸會降低;upsampling的意義在於將小尺寸的高維度feature map恢復回去,以便做pixelwise prediction,獲得每個點的分類信息。

轉置卷積

卷積矩陣

我們可以將一個卷積操作用一個矩陣表示。這個表示很簡單,無非就是將卷積核重新排列到我們可以用普通的矩陣乘法進行矩陣卷積操作。如下圖就是原始的卷積核:

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
簡單來說,這個卷積矩陣除了重新排列卷積核的權重之外就沒有啥了,然後卷積操作可以通過表示爲卷積矩陣和輸入矩陣的列向量形式的矩陣乘積形式進行表達。

重點就在於這個卷積矩陣,你可以從16(4×4 )到4(2×2 )因爲這個卷積矩陣尺寸正是4×16 ,然後,如果你有一個16×4的矩陣,你就可以從4(2×2)到16(4×4 )了,這不就是一個上採樣的操作嗎?

轉置卷積矩陣

我們想要從4(2×2)到16(4×4),因此我們使用了一個16×4的矩陣,但是還有一件事情需要注意,我們是想要維護一個1到9的映射關係。

假設我們轉置這個卷積矩陣C (4×16)變爲C.T (16×4)。我們可以對C用一個列向量(4×1)進行矩陣乘法,從而生成一個16×1 的輸出矩陣。這個轉置矩陣正是將一個元素映射到了9個元素。

在這裏插入圖片描述

詳解:

從矩陣計算的角度來看卷積。比如下面這個簡單的卷積計算,卷積參數(kernal=3,stride=1,padding=0),輸入尺寸爲 4,輸出尺寸爲 (4-3)/1+1=2。

在這裏插入圖片描述

對於上述卷積運算,我們把上圖所示的 3*3 卷積核在整個矩陣上的計算過程展成一個如下所示的 [4,16] 的稀疏矩陣 , 其中非 0 元素 wi,j 表示卷積核的第 i行和第j列。

在這裏插入圖片描述

再把 4*4的輸入特徵展成16*1 的矩陣X,那麼由 Y=AX得到一個 [4,1] 的輸出特徵矩陣,把它重新排列成2*2 的輸出特徵就得到最終的結果,從上述分析中可以看出卷積層的計算是可以轉換成矩陣計算的。

在反向傳播時,我們已知更深層返回的損失

在這裏插入圖片描述

所以,卷積計算中前向傳播就是把輸入左乘卷積矩陣A ,反向傳播就是把梯度左乘卷積矩陣的轉置ATA^T

反捲積與卷積恰好相反,前向傳播就是把輸入左乘卷積矩陣的轉置 ATA^T,反向傳播就是把梯度左乘卷積矩陣A 。因此,反捲積又可以稱爲轉置卷積。

“反捲積”(the transpose of conv) 可以理解爲upsample conv.
卷積核爲:3x3; no padding , strides=1

img

那看下strides=2的時候。

img

在實際計算過程中,我們要轉化爲矩陣的乘積的形式,將卷積核轉化爲Toeplitz matrix,將輸入reshape爲列矩陣。
舉個簡單的例子
比如 input= [4,4],Reshape之後,爲X=[16,1]
A(可以理解爲濾波器)=[4,16]
那麼A*X=Y=[4,1]。Reshape Y=[2,2]
所以,通過A卷積,我們從shape=[4,4]變成了shape=[2,2]

反過來。
輸入X=[2,2],reshape之後爲[4,1]
A的轉置爲[16,4]
那麼ATX=YA^T*X=Y=[16,1],reshape爲[4,4]
所以,通過A的轉置 - “反捲積”,我們從shape=[2,2]得到了shape=[4,4]

也就是輸入feature map X=[3,3]經過了卷積濾波A=[3,3] 輸出爲 [2,2] ,所以padding=0,stride=1
反捲積則是
輸入feature map X=[2,2],經過了反捲積濾波A=[3,3].輸出爲[3,3].padding=0,stride=1

那麼[3,3]的卷積核(濾波器)是怎麼轉化爲[4,16]或者[16,4]的呢?

通過Toeplitz matrix,不清楚自己百度下~

重點來了:對於卷積操作很瞭解,這裏不多說。我們梳理一下反捲積的操作:

首先看stride=1時候的反捲積:這裏寫的是no padding,但是其實這對應的是正常卷積操作的no padding,然而實際意義上卷積操作是no padding,那麼反捲積就是full padding;同時帶來一個新的問題,那麼padding到底是多少呢?這裏我目前理解的是添加的padding值等於(kernel_size - stride),像此處就是padding = kernel_size - stride = 3 - 1 = 2,那麼padding添加爲2。同樣對於下面stride=2的時候,padding = 3 - 2 = 1。

但是當stride>1的時候,需要在原輸入中插入0像素值,如上圖stride=2的時候,填充padding其實是進行了兩個步驟:

  • 其一是根據步長stride來填充,即在原輸入矩陣中間插入(stride-1)= 2 - 1 = 1個像素值爲0的值;

  • 其二再根據padding來填充,padding = kernel_size - stride = 3 - 2 = 1,所以需要在輸入外圍填充1個像素值爲0的值。

另外要說明一點反捲積和轉置卷積的真正區別:

反捲積在數學含義上是可以還原輸入信號的;但是轉置卷積只能還原到原來輸入的shape,其value值是不一樣的。

借用一個反池化的圖簡單說明一下轉置卷積是可以恢復到原來輸入的shape,但是其value值是不一樣的。

我們知道,池化是不可逆的過程,然而我們可以通過記錄池化過程中,最大激活值得座標位置。然後在反池化的時候,只把池化過程中最大激活值所在的位置座標的值激活,其它的值置爲0,當然這個過程只是一種近似,因爲我們在池化的過程中,除了最大值所在的位置,其它的值也是不爲0的。

3.跳躍結構(Skip Architecture)

其實直接使用前兩種結構就已經可以得到結果了,但是直接將全卷積後的結果上採樣後得到的結果通常是很粗糙的。所以這一結構主要是用來優化最終結果的,思路就是將不同池化層的結果進行上採樣,然後結合這些結果來優化輸出,具體結構如下:

對第5層做32倍反捲積(deconvolution),得到的結果不太精確。於是將第 4 層和第 3 層的輸出也依次反捲積

在這裏插入圖片描述

**FCN例子:**輸入可爲任意尺寸圖像彩色圖像;輸出與輸入尺寸相同,深度爲:20類目標+背景=21,模型基於AlexNet

在這裏插入圖片描述

  • 藍色:卷積層

  • 綠色:Max Pooling層

  • 黃色: 求和運算, 使用逐數據相加,把三個不同深度的預測結果進行融合:較淺的結果更爲精細,較深的結果更爲魯棒

  • 灰色: 裁剪, 在融合之前,使用裁剪層統一兩者大小, 最後裁剪成和輸入相同尺寸輸出

  • 對於不同尺寸的輸入圖像,各層數據的尺寸(height,width)相應變化,深度(channel)不變

  • 全卷積層部分進行特徵提取, 提取卷積層(3個藍色層)的輸出來作爲預測21個類別的特徵

  • 圖中虛線內是反捲積層的運算, 反捲積層(3個橙色層)可以把輸入數據尺寸放大。和卷積層一樣,升採樣的具體參數經過訓練確定

1.以經典的AlexNet分類網絡爲初始化。最後兩級是全連接(紅色),參數棄去不用

在這裏插入圖片描述

2.從特徵小圖(16∗16∗4096)預測分割小圖(16∗16∗21),之後直接升採樣爲大圖。

在這裏插入圖片描述

反捲積(橙色)的步長爲32,這個網絡稱爲FCN-32s。

3.升採樣分爲兩次完成(橙色×2), 在第二次升採樣前,把第4個pooling層(綠色)的預測結果(藍色)融合進來。使用跳級結構提升精確性

在這裏插入圖片描述

第二次反捲積步長爲16,這個網絡稱爲FCN-16s。

4.升採樣分爲三次完成(橙色×3), 進一步融合了第3個pooling層的預測結果

在這裏插入圖片描述

第三次反捲積步長爲8,記爲FCN-8s。

較淺層的預測結果包含了更多細節信息。比較2,3,4階段可以看出,跳級結構利用淺層信息輔助逐步升採樣,有更精細的結果

在這裏插入圖片描述

FCN的有點和不足

FCN 的優勢在於:

  • 可以接受任意大小的輸入圖像(沒有全連接層)
  • 更加高效,避免了使用鄰域帶來的重複計算和空間浪費的問題

其不足也很突出:

  • 得到的結果還不夠精細
  • 沒有充分考慮像素之間的關係,缺乏空間一致性

Loss計算與分析

由於增加了mask分支,每個ROI的Loss函數如下所示:

L=Lcls+Lbox+Lmask L=L_{cls}+L_{box}+L_{mask}

其中Lcls和Lbox和Faster r-cnn中定義的相同。對於每一個ROI,mask分支有Km*m維度的輸出,其對K個大小爲m*m的mask進行編碼,即K個分辨率爲m*m的二值的掩膜。依據預測類別分支預測的類型i,只將第i的二值掩膜輸出記爲Lmask.

掩膜分支的損失計算如下示意圖:
在這裏插入圖片描述

1.mask branch 預測K個種類的m×m二值掩膜輸出
2.依據種類預測分支(Faster R-CNN部分)預測結果:當前RoI的物體種類爲i
3.第i個二值掩膜輸出就是該RoI的損失Lmask

對於預測的二值掩膜輸出,我們對每個像素點應用sigmoid函數,整體損失定義爲平均二值交叉損失熵。

引入預測K個輸出的機制,允許每個類都生成獨立的掩膜,避免類間競爭。這樣做解耦了掩膜和種類預測。不像是FCN的方法,在每個像素點上應用softmax函數,整體採用的多任務交叉熵,這樣會導致類間競爭,最終導致分割效果差。

代碼解讀

整體流程:

在這裏插入圖片描述

創建模型對象:

model = modellib.MaskRCNN(mode="training", config=config,
                          model_dir=MODEL_DIR)

搭建backbone網絡

搭建特徵提取網絡用於圖片的特徵的提取,maskrcnn使用了金字塔(FPN)的網絡方式進行特徵的提取,使用的網絡是resnet101

# 生成金字塔網絡,並在每層提取特徵
_, C2, C3, C4, C5 = resnet_graph(input_image, config.BACKBONE,
                                             stage5=True, train_bn=config.TRAIN_BN)

FPN特徵提取

構建RPN需要的Feature maps 和 Mask所需要的Feature maps(需要網絡計算).

在這裏插入圖片描述

#構造FPN金字塔特徵
# Top-down Layers
# TODO: add assert to varify feature map sizes match what's in config
P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c5p5')(C5)
P4 = KL.Add(name="fpn_p4add")([
    KL.UpSampling2D(size=(2, 2), name="fpn_p5upsampled")(P5),
    KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c4p4')(C4)])
P3 = KL.Add(name="fpn_p3add")([
    KL.UpSampling2D(size=(2, 2), name="fpn_p4upsampled")(P4),
    KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c3p3')(C3)])
P2 = KL.Add(name="fpn_p2add")([
    KL.UpSampling2D(size=(2, 2), name="fpn_p3upsampled")(P3),
    KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (1, 1), name='fpn_c2p2')(C2)])
# Attach 3x3 conv to all P layers to get the final feature maps.

#3*3卷積消除上採樣帶來的混疊效應
P2 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p2")(P2)
P3 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p3")(P3)
P4 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p4")(P4)
P5 = KL.Conv2D(config.TOP_DOWN_PYRAMID_SIZE, (3, 3), padding="SAME", name="fpn_p5")(P5)
# P6 is used for the 5th anchor scale in RPN. Generated by
# subsampling from P5 with stride of 2.
P6 = KL.MaxPooling2D(pool_size=(1, 1), strides=2, name="fpn_p6")(P5)

# Note that P6 is used in RPN, but not in the classifier heads.
rpn_feature_maps = [P2, P3, P4, P5, P6]
mrcnn_feature_maps = [P2, P3, P4, P5]

RPN網絡(需要網絡計算)

在這裏插入圖片描述

這步的特點:

(1) 對於RPN的是用的同樣卷積filter參數對不同的feature map進行卷積。

(2) RPN在對原始的feature map進行卷積的時候是不改變原始feature map的大小,這樣的話,原始的feature map就具有cell的概念。

(3) 生成的261888個 box數據是對於anchor部分的delta數據

  #有了FPN生成的這些特徵圖,就可以用這些特徵圖生成anchor
        # Anchors 如果是訓練情況,就在金字塔的特徵圖上生成anchor,如果是inference,則anchor就是輸入的anchor
        if mode == "training":
            #獲取金字塔層面上所有的anchor,是在原始圖的大小上生成的anchor
            #在金字塔的特徵圖上以每個像素爲中心,以配置文件的anchor大小爲寬高,生成anchor,
            #並根據特徵圖相對原圖縮小的比例,還原到原始的輸入的圖片上,
            #也就是說,這些生成的anchor是在原始的圖片上的座標
            anchors = self.get_anchors(config.IMAGE_SHAPE)
            # Duplicate across the batch dimension because Keras requires it
            # TODO: can this be optimized to avoid duplicating the anchors?
            anchors = np.broadcast_to(anchors, (config.BATCH_SIZE,) + anchors.shape)
            # A hack to get around Keras's bad support for constants
            anchors = KL.Lambda(lambda x: tf.Variable(anchors), name="anchors")(input_image)
        else:
            anchors = input_anchors

        #搭建rpn網絡,將得到的金字塔特徵分別輸入到rpn網絡中,得到網絡的分類和迴歸值
        # RPN Model  返回的是keras的Module對象, 注意keras中的Module對象是可call的
        rpn = build_rpn_model(config.RPN_ANCHOR_STRIDE,
                              len(config.RPN_ANCHOR_RATIOS),config.TOP_DOWN_PYRAMID_SIZE)
        
        ....
        
        #rpn網絡的輸出值
        rpn_class_logits, rpn_class, rpn_bbox = outputs

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.

ProposalLayer(不需要網絡計算)

這部分的特點

(1) 把第二步的box delta數據和anchor數據結合,生成最後的RPN box數據, 這裏的anchor是像素尺度的值

(2) 然後對score從大到小做NMS,保留前面的1000 or 2000個

(3) 這部分不需要網絡計算,相對於是對網絡計算的結果進行了部分的篩選

在這裏插入圖片描述

#ProposalLayer的作用主要:
#1.根據rpn網絡,獲取score靠前的前n個anchor
#2.利用rpn_bbox對anchors進行修正
#3.捨棄修正後邊框超過圖片大小的anchor,由於我們的anchor的座標大小是歸一化的,只要座標不超過0-1即可
#4.利用非極大抑制的方法獲得最後的anchor
rpn_rois = ProposalLayer(
    proposal_count=proposal_count,
    nms_threshold=config.RPN_NMS_THRESHOLD,
    name="ROI",
    config=config)([rpn_class, rpn_bbox, anchors])

DetectionTargetLayer

DetectionTargetLayer的輸入包含了:

target_rois, input_gt_class_ids, gt_boxes, input_gt_masks。其中target_rois是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和哪個真實的框最接近,計算出和真實框的距離,以及要預測的mask,這些信息都會在網絡的頭的#classify和mask網絡所使用
def detection_targets_graph(proposals, gt_class_ids, gt_boxes, gt_masks, config):
   """Generates detection targets for one image. Subsamples proposals and
   generates target class IDs, bounding box deltas, and masks for each.
   Inputs:
   proposals: [N, (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, NUM_CLASSES, (dy, dx, log(dh), log(dw))]
           Class-specific bbox refinements.
   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.
   """
   # Assertions
   asserts = [
       tf.Assert(tf.greater(tf.shape(proposals)[0], 0), [proposals],
                 name="roi_assertion"),
   ]
   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)
   crowd_masks = tf.gather(gt_masks, crowd_ix, axis=2)
   #下面就是一張圖片中真實存在的物體用於訓練
   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]
   #計算iou的值
   overlaps = overlaps_graph(proposals, gt_boxes)

   # Compute overlaps with crowd boxes [anchors, crowds]
   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
   #和真實的框的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.
   #負樣本是是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
   # Positive ROIs
   positive_count = int(config.TRAIN_ROIS_PER_IMAGE *
                        config.ROI_POSITIVE_RATIO)
   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.
   r = 1.0 / config.ROI_POSITIVE_RATIO
   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
   #選擇出正負樣本
   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)
   roi_gt_box_assignment = tf.cond(
       tf.greater(tf.shape(positive_overlaps)[1], 0),
       true_fn = lambda: tf.argmax(positive_overlaps, axis=1),
       false_fn = lambda: tf.cast(tf.constant([]),tf.int64)
   )
   roi_gt_boxes = tf.gather(gt_boxes, roi_gt_box_assignment)
   roi_gt_class_ids = tf.gather(gt_class_ids, roi_gt_box_assignment)

   # Compute bbox refinement for positive ROIs
   #用最接近的真實框修正rpn網絡預測的框
   deltas = utils.box_refinement_graph(positive_rois, roi_gt_boxes)
   deltas /= config.BBOX_STD_DEV

   # Assign positive ROIs to GT masks
   # Permute masks to [N, height, width, 1]
   transposed_masks = tf.expand_dims(tf.transpose(gt_masks, [2, 0, 1]), -1)
   # Pick the right mask for each ROI
   # 計算和每一個rois最接近的框的mask
   roi_masks = tf.gather(transposed_masks, roi_gt_box_assignment)

   # Compute mask targets
   boxes = positive_rois
   if config.USE_MINI_MASK:
       # Transform ROI coordinates from normalized image space
       # to normalized mini-mask space.
       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)
   # 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)

   # 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]
   P = tf.maximum(config.TRAIN_ROIS_PER_IMAGE - tf.shape(rois)[0], 0)
   rois = tf.pad(rois, [(0, P), (0, 0)])
   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)])

   return rois, roi_gt_class_ids, deltas, 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, NUM_CLASSES, (dy, dx, log(dh), log(dw))]
Class-specific bbox refinements.
masks: [TRAIN_ROIS_PER_IMAGE, height, width). Masks cropped to bbox
boundaries and resized to neural network output size.

通過rpn網絡得到的anchor,選擇出來正負樣本,並計算出正樣本和真實框的差距,以及要預測的mask的值,這些都是在後面的網絡中計算損失函數需要的真實值。

Feature Pyramid Network Heads(fpn_classifier_graph)

在這裏插入圖片描述

該網絡是maskrcnn的最後一層,與之並行的還有一個mask分支,在這裏先介紹一下這個分類網絡。

由上面得到的roi的大小並不一樣,因此,需要一個網絡類似於fasterrcnn中的roipooling,將rois轉換成大小一樣的特徵圖。maskrcnn中使用的是PyramidROIAlign。PyramidROIAlign首先根據下面的公司計算每一個roi來自於金字塔特徵的P2到P5的哪一層的特徵:
在這裏插入圖片描述

# Assign each ROI to a level in the pyramid based on the ROI area.
        y1, x1, y2, x2 = tf.split(boxes, 4, axis=2)
        h = y2 - y1
        w = x2 - x1
        # Use shape of first image. Images in a batch must have the same size.
        image_shape = parse_image_meta_graph(image_meta)['image_shape'][0]
        # Equation 1 in the Feature Pyramid Networks paper. Account for
        # the fact that our coordinates are normalized here.
        # e.g. a 224x224 ROI (in pixels) maps to P4
        #計算每個roi映射到哪一層的金字塔特徵的輸出上
        image_area = tf.cast(image_shape[0] * image_shape[1], tf.float32)
        roi_level = log2_graph(tf.sqrt(h * w) / (224.0 / tf.sqrt(image_area)))
        roi_level = tf.minimum(5, tf.maximum(
            2, 4 + tf.cast(tf.round(roi_level), tf.int32)))
        roi_level = tf.squeeze(roi_level, 2)

然後從對應的特徵圖中取出座標對應的區域,利用雙線性插值的方式進行pooling操作。PyramidROIAlign會返回resize成相同大小的rois。

將得到的特徵塊輸入到fpn_classifier_graph網絡中,得到分類和迴歸值。

下面是fpn_classifier_graph網絡的定義,

def fpn_classifier_graph(rois, feature_maps, image_meta,
                         pool_size, num_classes, train_bn=True,
                         fc_layers_size=1024):
    """Builds the computation graph of the feature pyramid network classifier
    and regressor heads.
    rois: [batch, num_rois, (y1, x1, y2, x2)] Proposal boxes in normalized
          coordinates.
    feature_maps: List of feature maps from different layers of the pyramid,
                  [P2, P3, P4, P5]. Each has a different resolution.
    - image_meta: [batch, (meta data)] Image details. See compose_image_meta()
    pool_size: The width of the square feature map generated from ROI Pooling.
    num_classes: number of classes, which determines the depth of the results
    train_bn: Boolean. Train or freeze Batch Norm layers
    fc_layers_size: Size of the 2 FC layers
    Returns:
        logits: [N, NUM_CLASSES] classifier logits (before softmax)
        probs: [N, NUM_CLASSES] classifier probabilities
        bbox_deltas: [N, (dy, dx, log(dh), log(dw))] Deltas to apply to
                     proposal boxes
    """
    # ROI Pooling
    # Shape: [batch, num_boxes, pool_height, pool_width, channels]
    x = PyramidROIAlign([pool_size, pool_size],
                        name="roi_align_classifier")([rois, image_meta] + feature_maps)
    # Two 1024 FC layers (implemented with Conv2D for consistency)
    x = KL.TimeDistributed(KL.Conv2D(fc_layers_size, (pool_size, pool_size), padding="valid"),
                           name="mrcnn_class_conv1")(x)
    x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn1')(x, training=train_bn)
    x = KL.Activation('relu')(x)
    x = KL.TimeDistributed(KL.Conv2D(fc_layers_size, (1, 1)),
                           name="mrcnn_class_conv2")(x)
    x = KL.TimeDistributed(BatchNorm(), name='mrcnn_class_bn2')(x, training=train_bn)
    x = KL.Activation('relu')(x)
 
    shared = KL.Lambda(lambda x: K.squeeze(K.squeeze(x, 3), 2),
                       name="pool_squeeze")(x)
 
    # Classifier head
    mrcnn_class_logits = KL.TimeDistributed(KL.Dense(num_classes),
                                            name='mrcnn_class_logits')(shared)
    mrcnn_probs = KL.TimeDistributed(KL.Activation("softmax"),
                                     name="mrcnn_class")(mrcnn_class_logits)
 
    # BBox head
    # [batch, boxes, num_classes * (dy, dx, log(dh), log(dw))]
    x = KL.TimeDistributed(KL.Dense(num_classes * 4, activation='linear'),
                           name='mrcnn_bbox_fc')(shared)
    # Reshape to [batch, boxes, num_classes, (dy, dx, log(dh), log(dw))]
    s = K.int_shape(x)
    mrcnn_bbox = KL.Reshape((s[1], num_classes, 4), name="mrcnn_bbox")(x)
 
    return mrcnn_class_logits, mrcnn_probs, mrcnn_bbox

返回值爲:

Returns:
logits: [N, NUM_CLASSES] classifier logits (before softmax)
probs: [N, NUM_CLASSES] classifier probabilities
bbox_deltas: [N, (dy, dx, log(dh), log(dw))] Deltas to apply to
proposal boxes

MASk部分(build_fpn_mask_graph)

大的邏輯是:

輸入:

  • 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以後的值

主要是調用了一個ROI align生成 14x14大小的feature map然後,做一個deconv把feature map變大到(28x28),每個類別取sigmoid.

流程圖如下

在這裏插入圖片描述

mask網絡的輸入和1.6網絡的輸入值是一樣的,也會經過PyramidROIAlign(這個地方可以進行一個提取,放在最後的網絡之前)

#創建mask的分類頭
def build_fpn_mask_graph(rois, feature_maps, image_meta,
                         pool_size, num_classes, train_bn=True):
    """Builds the computation graph of the mask head of Feature Pyramid Network.
    rois: [batch, num_rois, (y1, x1, y2, x2)] Proposal boxes in normalized
          coordinates.
    feature_maps: List of feature maps from different layers of the pyramid,
                  [P2, P3, P4, P5]. Each has a different resolution.
    image_meta: [batch, (meta data)] Image details. See compose_image_meta()
    pool_size: The width of the square feature map generated from ROI Pooling.
    num_classes: number of classes, which determines the depth of the results
    train_bn: Boolean. Train or freeze Batch Norm layers
    Returns: Masks [batch, roi_count, height, width, num_classes]
    """
    # ROI Pooling
    # Shape: [batch, boxes, pool_height, pool_width, channels]
    x = PyramidROIAlign([pool_size, pool_size],
                        name="roi_align_mask")([rois, image_meta] + feature_maps)
 
    # Conv layers
    x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
                           name="mrcnn_mask_conv1")(x)
    x = KL.TimeDistributed(BatchNorm(),
                           name='mrcnn_mask_bn1')(x, training=train_bn)
    x = KL.Activation('relu')(x)
 
    x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
                           name="mrcnn_mask_conv2")(x)
    x = KL.TimeDistributed(BatchNorm(),
                           name='mrcnn_mask_bn2')(x, training=train_bn)
    x = KL.Activation('relu')(x)
 
    x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
                           name="mrcnn_mask_conv3")(x)
    x = KL.TimeDistributed(BatchNorm(),
                           name='mrcnn_mask_bn3')(x, training=train_bn)
    x = KL.Activation('relu')(x)
 
    x = KL.TimeDistributed(KL.Conv2D(256, (3, 3), padding="same"),
                           name="mrcnn_mask_conv4")(x)
    x = KL.TimeDistributed(BatchNorm(),
                           name='mrcnn_mask_bn4')(x, training=train_bn)
    x = KL.Activation('relu')(x)
 
    x = KL.TimeDistributed(KL.Conv2DTranspose(256, (2, 2), strides=2, activation="relu"),
                           name="mrcnn_mask_deconv")(x)
    x = KL.TimeDistributed(KL.Conv2D(num_classes, (1, 1), strides=1, activation="sigmoid"),
                           name="mrcnn_mask")(x)
    return x

有一個細節需要注意的就是1.6經過PyramidROIAlign得到的特徵圖是7*7大小的,二1.7經過PyramidROIAlign得到的特徵圖大小是14*14

最後的返回值是:

Returns: Masks [batch, roi_count, height, width, num_classes]

損失函數

maskrcnn中總共有五個損失函數,分別是rpn網絡的兩個損失,分類的兩個損失,以及mask分支的損失函數。前四個損失函數與fasterrcnn的損失函數一樣,最後的mask損失函數的採用的是mask分支對於每個RoI有Km2 維度的輸出。K個(類別數)分辨率爲m*m的二值mask。
因此作者利用了a per-pixel sigmoid,並且定義 Lmask 爲平均二值交叉熵損失(the average binary cross-entropy loss).
對於一個屬於第k個類別的RoI, Lmask 僅僅考慮第k個mask(其他的掩模輸入不會貢獻到損失函數中)。這樣的定義會允許對每個類別都會生成掩模,並且不會存在類間競爭。

  # Losses
            rpn_class_loss = KL.Lambda(lambda x: rpn_class_loss_graph(*x), name="rpn_class_loss")(
                [input_rpn_match, rpn_class_logits])
            rpn_bbox_loss = KL.Lambda(lambda x: rpn_bbox_loss_graph(config, *x), name="rpn_bbox_loss")(
                [input_rpn_bbox, input_rpn_match, rpn_bbox])
            class_loss = KL.Lambda(lambda x: mrcnn_class_loss_graph(*x), name="mrcnn_class_loss")(
                [target_class_ids, mrcnn_class_logits, active_class_ids])
            bbox_loss = KL.Lambda(lambda x: mrcnn_bbox_loss_graph(*x), name="mrcnn_bbox_loss")(
                [target_bbox, target_class_ids, mrcnn_bbox])
            mask_loss = KL.Lambda(lambda x: mrcnn_mask_loss_graph(*x), name="mrcnn_mask_loss")(
                [target_mask, target_class_ids, mrcnn_mask])

總的模型

上面只是介紹了模型的每一步,要把各個模型串聯起來,纔可以形成一個整體的網絡,網絡的整體定義如下:

# Model
            inputs = [input_image, input_image_meta,
                      input_rpn_match, input_rpn_bbox, input_gt_class_ids, input_gt_boxes, input_gt_masks]
            if not config.USE_RPN_ROIS:
                inputs.append(input_rois)
            outputs = [rpn_class_logits, rpn_class, rpn_bbox,
                       mrcnn_class_logits, mrcnn_class, mrcnn_bbox, mrcnn_mask,
                       rpn_rois, output_rois,
                       rpn_class_loss, rpn_bbox_loss, class_loss, bbox_loss, mask_loss]
            model = KM.Model(inputs, outputs, name='mask_rcnn')

以上整體總結:

在這裏插入圖片描述

兩次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的座標值也可以爲像素意義,也可以爲歸一化的意義

參考:

https://blog.csdn.net/WZZ18191171661/article/details/79453780

https://blog.csdn.net/xiamentingtao/article/details/78598511

https://blog.csdn.net/qinghuaci666/article/details/80900882

https://blog.csdn.net/qinghuaci666/article/details/80920202

https://zhuanlan.zhihu.com/p/22976342

https://blog.csdn.net/remanented/article/details/79564045

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章