endernewton/tf-faster-rcnn項目研究之三:項目的代碼結構及網絡原理

從本節開始,將正式對項目代碼進行解讀。因代碼衆多,在一篇文章中說完是不現實的,故打算分成若干篇幅來陸續解讀。

本文部分內容參考自以下帖子,致謝!

https://www.cnblogs.com/wangyong/p/8513563.html

https://blog.csdn.net/Mr_KkTian/article/details/78158785

一、項目代碼結構

├── data           //用來保存數據集,如VOC2007、coco等
│   ├── cache   //保存訓練集和測試集的proposals,如voc_2007_test_gt_roidb.pkl,格式[{ },{ },...,{ }]。

                          //如果文件存在則首先從這讀取;否則讀取.xml文件得到proposal,同時在該目錄下生成對應的.pkl文件                                    //Note:訓練集合和測試集變化了,一定的先delete該目錄下的對應的.pkl文件
│   ├── coco    //存有訪問COCO數據集的Python COCO API  
│   ├── demo   //演示用圖片,執行demo.py時使用
│   ├── imagenet_weights  //存有ImageNet數據集預訓練的分類模型(如vgg16,res101),vgg16.ckpt和res101.ckpt
│   ├── res101_voc_2007_trainval+voc_2012_trainval //由原名爲voc_0712_80k-110k.tgz解壓出來的,faster-rcnn(res101)網絡訓練好的模型
│   ├── scripts                 //包含fetch_faster_rcnn_models.sh,該腳本可用來下載訓練好的faster-rcnn模型
│   ├── vgg16_voc_0712_80k-110k.tgz     //原文件名爲voc_0712_80k-110k.tgz  faster-rcnn(vgg16)的模型壓縮文件             
│   ├── vgg16_voc_2007_trainval+voc_2012_trainval  //由原名爲voc_0712_80k-110k.tgz解壓出來的,faster-rcnn(vgg16)網絡訓練好的模型
│   ├── VOCdevkit       //PASCAL VOC 2007數據集開發工具箱,其實不需要
│   ├── VOCdevkit2007 //VOC2007數據集,其結構參看其它文章
├── docker      //構建docker的不同版本的cuda
│   ├── Dockerfile.cuda-7.5
│   └── Dockerfile.cuda-8.0
├── experiments
│   ├── cfgs    //保存$NET.yml文件,針對具體網絡的配置參數
│   ├── logs    //保存每次訓練和測試的日誌
│   └── scripts //保存三個.sh腳本,用於demo演示、測試和訓練
├── lib    //需make編譯
│   ├── datasets   //基類imdb  針對具體數據集的派生類如pascal_voc  coco 
│   ├── layer_utils  //與anchor proposal相關
│   ├── Makefile
│   ├── model   //config配置文件   nms bbox test train_val等
│   ├── nets    //基類Network,針對具體網絡的派生類(如mobilenet_v1,resnet_v1,vgg16)
│   ├── nms     //c和cuda的加速代碼,生成共享庫(.so)
│   ├── roi_data_layer  //RoI層
│   ├── setup.py  //用於構建Cython模塊
│   └── utils  //一些輔助工具,計時、可視化 
├── LICENSE
├── output  //保存訓練模型和測試結果
│   ├── res101
│   └── vgg16
├── README.md
├── run_demover1.sh   //演示demo
├── run_test.sh       //測試
├── run_train.sh      //訓練+測試
├── tensorboard       //可視化tensorboard
│   ├── res101
│   └── vgg16
└── tools
    ├── convert_from_depre.py
    ├── demo.py      
    ├── demover1.py      //demo
    ├── _init_paths.py
    ├── _init_paths.pyc
    ├── __pycache__
    ├── reval.py     
    ├── test_net.py      //測試
    └── trainval_net.py  //訓練+驗證

 /tf-faster-rcnn/output目錄結構

├── res101   //在faster-rcnn(res101)
│   ├── voc_2007_test    //測試結果,按類別保存的.pkl文件
│   │   └── default
│   ├── voc_2007_trainval   //訓練的模型保存在該文件夾下
│   │   └── default
│   └── voc_2007_trainval+voc_2012_trainval  //在voc_07+voc_12上訓練好的faster-rcnn(res101)模型 從/data目錄下軟鏈接過來的
└── vgg16
    ├── voc_2007_test     //測試結果,按類別保存的.pkl文件
    │   └── default
    ├── voc_2007_trainval  //訓練的模型保存在該文件夾下
    │   └── default
    └── voc_2007_trainval+voc_2012_trainval
        └── default -> ../../../data/vgg16_voc_2007_trainval+voc_2012_trainval/   //軟鏈接過來的faster-rcnn(vgg6)模型

二、網絡基本原理

1、概述

該項目是faster rcnn基於tensorflow的一個實現版本。有關原理性的東西,此處簡略概述一下:

總體上可分爲幾個部分:  特徵提取層、生成anchors、rpn層、FC層,Classifier層,損失函數。該網絡是一個複合網絡,因此理解起來較爲困難。

(1)特徵提取層:一副待檢測圖片首先進入特徵提取層,經過該層後輸出不同維度的特徵圖。需要注意的是,特徵提取層可以使用多種基礎網絡進行特徵提取,比如VGG16或resnet101等,所選網絡不同,輸出的特徵圖尺寸不同。

(2)anchors生成:根據輸出的特徵圖,以及諸多預設參數,即可生成anchors集合。這裏的anchors本質上是指,在原始圖上畫出(實際並沒有畫,可以腦補)的一系列大大小小的矩形框,共計M*N*9個anchors,用於進行物體檢測。M、N是輸出特徵圖的寬高。

  (3)  rpn層:根據第(1) 步輸出的特徵,從anchors中挑選得到256個anchors(這時被稱爲rois)用於物體檢測和框座標迴歸,其中含128個正樣本和128個負樣本。這些樣本用於判斷是物體或者是背景,這是個二分類問題;這些樣本還用於預測框的位置座標,這是個迴歸問題。因此rpn層包含了分類和迴歸兩個網絡。

(4)roi pooling層:將256個rois進行剪裁縮放以適應後續的卷積操作。

(5)在特徵提取層(conv層)上添加FC層。

(6)Classifier層:通過FC層與softmax計算每個rois的具體類別,並輸出cls_prob概率向量;同時再次利用bounding box regression獲得每個rois的位置座標偏移量bbox_pred,用於迴歸更加精確的目標檢測框。如下圖示:

(7)損失函數

下面將進入正題,挨個分析網絡的各組成部分。

2、特徵提取層(conv層、卷積層)

如之前所說,特徵提取層是採用了遷移學習的方法。常用的選擇可以是VGG-16或Resnet-101。resnet有多種結構形式,如下圖所示。本項目中選擇使用resnet101來進行特徵選擇。

renet網絡結構圖

conv層一般包含了conv,pooling,relu三種結構。以renet101爲例,首先有個輸入7x7x64的卷積,然後經過3 + 4 + 23 + 3 = 33個building block,每個block爲3層,所以有33 x 3 = 99層,最後有個fc層(用於分類),所以1 + 99 + 1 = 101層,共計有101層網絡(注:101層網絡僅僅指卷積或者全連接層,而激活層或者Pooling層並沒有計算在內)。

resnet101包含了conv1,conv2_x,conv3_x,conv4_x,conv5_x,到底哪層的輸出結果作爲特徵輸出呢?

從resnet_v1.py代碼中的_image_to_head函數中可以看出,實際上是conv4_x這層的輸出結果被作爲特徵圖。如果debug此段代碼,會發現輸出結果的維度是(1,?,?,1024),正好符合conv4_x的輸出。如下圖示:

è¿éåå¾çæè¿°

可以發現conv4_x的最後的輸出被RPN和RoI Pooling共同使用(共享),而conv5_x(共9層網絡)都作用於RoI Pooling之後的一堆特徵圖(14 x 14 x 1024),特徵圖的大小維度也剛好符合原本的ResNet101中conv5_x的輸入;

注意,在別的faster rcnn的實現中,conv層可能採用了VGG16。此時,所有的卷積層不改變輸入圖像的寬高大小,只有pooling層每次會導致輸出寬高變爲原來的一半,一共會經過4次pooling。因此,最終特徵圖的寬高會變爲原圖的1/16。本例中使用的是resnet101網絡,經過對源代碼的debug發現,conv4輸出的特徵圖寬高也是原圖的1/16。其實可以通過計算得出這個結論,此處不再細挖。

最後一點需要說明的是,輸入的待檢測圖像,並不需要限定分辨率大小。進入網絡之前對輸入圖像做尺寸標準化處理,設定圖像短邊不超過600,圖像長邊不超過1000,因此可以假定圖片最大尺寸爲1000*600。此處的相關代碼,可以參看minibatch.py文件中的_get_image_blob()函數。基本思想就是短邊先按600來進行縮放,如果發現縮放後長邊大於1000,那麼就改用1000來縮放長邊,再縮放短邊,確保最大尺寸不超過1000*600。

2、生成anchors

可分爲以下幾部分: 計算anchors偏移矩陣,生成9個base-anchors、前二者結合計算anchors。這幾部分的功能都在源碼文件snippets.py中的generate_anchors_pre_tf函數中。

(1) 計算anchors偏移矩陣

先介紹個參數,self._feat_stride = [16, ]

無論Conv層選擇何種網絡,這個參數的值都是16。查閱了許多資料,都說這個值是指原始圖經過特徵提取後得到的特徵圖的邊的縮小比例。如果是VGG網絡,像上面所說的,輸出的尺寸會變爲原來的1/16之一,因此這個參數是指針對特徵圖的縮放倍數。但這個結論對於選擇了resnet101網絡來說,也成立嗎?此處存疑。

不管怎麼講,代碼中確實使用了這個參數來計算anchors的偏移矩陣。過程如下:

1)根據原圖尺寸和_feat_stride參數,計算特徵圖大小M*N。

2)根據M、N值的大小,生成寬和高的兩個關於16的倍數序列,都是從0開始。比如這樣:0,16,32,48,64.........

3)對這兩個序列使用各種(神奇)變換(主要是tf.meshgrid和reshape),就會形成需要的偏移矩陣的前兩列。

4)如下圖,圖中每個方格代表一個16*16像素的滑動窗口,會在原始圖像上進行滑動。偏移矩陣前兩列數值代表的就是滑動窗口的左上角的偏移座標點。

偏移矩陣的後兩列值跟前兩列是相同的,其實是爲了後面矩陣計算方便。主要理解前兩列值就可以。

 爲方便觀察其邏輯,現假定M=5,N=4的特徵矩陣,完整的偏移矩陣如下,注意前兩列與後兩列值相同:

關於anchors的偏移矩陣生成到此完畢,但真實圖像的偏移矩陣比這個長的多。

(2)生成9個base-anchors

1)生成一個邊長爲16的方框,座標[0,0.15,15],以及中心點的座標[7.5, 7.5]。

2)通過_ratio_enum函數, 以這個方框爲基礎,生成三個框的寬度爲[23,16,11],高度爲[12,16,22]。

3)通過_mkanchors函數,結合這三個框的中心點[7.5, 7.5],計算出三個框的座標位置: 

[-3.5  2.  18.5 13. ]

[ 0.   0.  15.  15. ]

[ 2.5 -3.  12.5 18. ]

大致如示意圖:

4)通過_scale_enum函數,把上面任何一個框乘以縮放尺寸[8,16,32](請注意,就是self._anchor_scales這個參數),即可獲得三個新框,因此一共可以獲得9個新框;這9個框,就是我們需要的base-anchors。其座標位置如下:

 [ -84.  -40.   99.   55.]
 [-176.  -88.  191.  103.]
 [-360. -184.  375.  199.]
 [ -56.  -56.   71.   71.]
 [-120. -120.  135.  135.]
 [-248. -248.  263.  263.]
 [ -36.  -80.   51.   95.]
 [ -80. -168.   95.  183.]
 [-168. -344.  183.  359.]

大致如示意圖:

(3)前二者結合計算anchors

有了基本的base-anchors,同時也有了這些anchors在原始圖上的座標偏移矩陣,要取得這些anchors在原圖的真正座標,只要這二者做矩陣相加就可以了。

這裏需要看源碼,1*9*4的基本的base-archors和(width*height)*1*4的偏移矩陣進行broadcast相加,得到(width*height)*9*4 ,並變形爲width*height*9)*4,得到所有的archors在原圖上的四個真正座標。  相加就是,兩個向量的最後一維(4個座標值)的各個對應位置的元素相加。

這是利用了張量的加法,注意其中的broadcasting機制: 最後一維的長度必須相同,其它兩兩相加的維度至少有一個是一維,一維向非一維的長度做擴展。

最後再說一點,這裏很多人誤以爲是通過原始圖的特徵圖去計算原圖上的anchor位置,其實不是,這裏跟特徵圖沒太大關係。只是借用了特徵圖尺寸大小(16*16)的一個框先生成基礎的anchor,然後用這個anchor在原始圖上進行窗口滑動,會得到一個個的窗口的座標,這裏得到的一個個窗口,就是最終生成的一堆anchors。

3、rpn層(region proposal network layer)

這個層算是faster rcnn的核心了,也是網絡中較複雜的地方。源代碼詳見network.py中的_region_proposal函數。該網絡大致經過以下步驟來進行處理:

  • 進一步特徵提取: 首先把經過特徵提取層提取出來的特徵,經過一個3*3的卷積核進行卷積,得到的新的特徵圖(姑且稱之爲rpn特徵),並行送入兩個網絡,即cls和reg網絡。參見下圖。

 

  • 每個anchor進行分類與迴歸: cls網絡對anchors進行二分類,判斷每個anchor是前景(物體)還是背景(非物體),因此一個anchor對應2個得分值;使用reg網絡對每個anchor的座標位置進行迴歸,期望得到準確的座標位置(實際上是相對於ground truth的偏移值),一個anchor會有4個位置座標偏差值。
  • 根據預測框的偏移量來矯正anchors座標,取得預測框的座標,這時候anchors有了新的名字proposals。
  • 剪裁proposals,執行NMS算法,保留2000個proposals,proposals又有了新名字rois。
  • 過濾後anchors生成標籤labels,取值爲三種情況(1正樣本, 0負樣本, -1非樣本)。
  • 在上面的labels中進行標籤設置,總計256個樣本。
  • 計算過濾後anchors和其對應的GT的位置座標偏移bbox_targets。
  • 計算bbox_weights。
  • 將labels、bbox_targets和bbox_weights映射回原始anchors(未過濾),這樣所有anchors(未過濾)都有對應的label、bbox_targets、bbox_weights。
  • 從上面2000個rois中篩選256個rois。

下面來看相關的代碼,並梳理其邏輯:

(1)cls網絡(上圖中上面的一條支線)

 rpn特徵經過18(9*2)個1*1的卷積,把特徵的維度從512降爲9*2,這是在判斷通過1*1的卷積得到archors是正樣本還是負樣本。

輸出值有兩個rpn_cls_pred(分類預測得分),rpn_cls_prob(物體和背景的概率)。需要監督的信息是Y=0,1,表示這個區域是否是ground truth。

再經過_reshape_layer函數,改變特徵的維度,進一步爲分類做準備。

最後經過_softmax_layer函數,對輸入的特徵的最後一維進行softmax計算,得到每個anchor是物體或背景的概率值(rpn_cls_prob);

(2)reg網絡(上圖中下面的一條支線)

rpn特徵經過36(9*4)個1*1的卷積,把特徵的維度從512降爲9*4。想想看cls網絡裏爲什麼是降維爲9*2,而這裏是9*4?原因就是cls網絡裏每個anchor有2個得分值,而reg網絡中每個anchor將有4個得分值,代表對anchor的座標的迴歸值(實際是相對於groud truth的偏移量)。

通過ground truth標定框與anchor之間的差異(就是兩個框中心點、寬、高的差值)來回歸學習,從而使得rpn層的權重參數得到逐步調整,進而使得anchor與ground truth標定框逼近重合。

(3) 根據預測框的偏移量矯正anchors座標,取得預測框的座標。

這一步是在bbox_transform_inv_tf函數中完成,該函數接收原始anchors和reg網絡輸出的座標位置偏移量兩個參數。首先有以下的偏移量計算公式:

ground truth:原圖的物體標定框對應一箇中心點位置座標(x,y)和寬高w,h
anchors座標: 也有中心點位置座標(x_a,y_a)和寬高w_a,h_a
偏移量的計算如下:
△x=(x-x_a)/w_a 
△y=(y-y_a)/h_a
△w=log(w/w_a) 
△h=log(h/h_a)

而這些△x、△y、△w、△h的值是知道的,就是傳入的reg網絡輸出的座標位置偏移量。那麼根據以上公式,很方便的就能計算出一個較爲準確(比原始anchor座標準確,但跟ground truth有差距)的預測框的座標,具體代碼如下:

  # 把anchor的數據類型轉化爲跟偏移量一樣的類型
  boxes = tf.cast(boxes, deltas.dtype) 
  widths = tf.subtract(boxes[:, 2], boxes[:, 0]) + 1.0  # anchor寬
  heights = tf.subtract(boxes[:, 3], boxes[:, 1]) + 1.0  # anchor高
  ctr_x = tf.add(boxes[:, 0], widths * 0.5)   # anchor中心x座標
  ctr_y = tf.add(boxes[:, 1], heights * 0.5)  # anchor中心y座標

  dx = deltas[:, 0]  # 偏移量(框)的中心點x座標
  dy = deltas[:, 1]  # y座標
  dw = deltas[:, 2]  # 寬度
  dh = deltas[:, 3]  # 高度

  # 公式已知xa,wa,tx反過來求預測的x中心座標
  pred_ctr_x = tf.add(tf.multiply(dx, widths), ctr_x)
  # 公式已知ya,ha,ty反過來求預測的y中心座標
  pred_ctr_y = tf.add(tf.multiply(dy, heights), ctr_y)
  # 公式已知wa,tw反過來求預測的w
  pred_w = tf.multiply(tf.exp(dw), widths)
  # 公式已知ha,th反過來求預測的h
  pred_h = tf.multiply(tf.exp(dh), heights)

在bbox_transform_inv_tf函數的最後,直接求出預測框左上角和右下角的座標並返回,代碼如下:

# 預測框的起始和終點四個座標
  pred_boxes0 = tf.subtract(pred_ctr_x, pred_w * 0.5)
  pred_boxes1 = tf.subtract(pred_ctr_y, pred_h * 0.5)
  pred_boxes2 = tf.add(pred_ctr_x, pred_w * 0.5)
  pred_boxes3 = tf.add(pred_ctr_y, pred_h * 0.5)

return tf.stack([pred_boxes0, pred_boxes1, pred_boxes2, pred_boxes3], axis=1)

(3)剪裁proposals,執行NMS算法進行過濾,獲取最終的proposals

剪裁:使用clip_boxes_tf函數進行剪裁,剪裁掉超出原始圖片邊框的部分,這樣就把proposals限制在原始圖片上。該函數接收兩個參數proposals和im_info。

說下im_info。對於一個大小爲PxQ的圖像,傳入Faster RCNN前首先reshape到固定MxN,M、N取值不超過1000、600,這就是上面說過的輸入圖像做尺寸標準化處理。im_info=[M, N, scale_factor]則保存了此次縮放的所有信息。

接下來說說NMS算法,其實是不難理解的,看看下面的圖例:

上面的圖中,一共出現了2個人(就是本文中所說的物體或者說前景),每個人有若干個anchors。但是我們想要剛好框住每個人的anchor,也就是最終只需要找出兩個完美的框(anchor)。怎麼找呢?

在上圖中一共有6個識別爲人的框,每一個框有一個置信率。我們需要找出每個人最大的置信率的那個框,其餘的幹掉:

  • 按置信率排序: 0.95, 0.9, 0.9, 0.8, 0.7, 0.7
  • 取最大0.95的框爲一個物體框
  • 剩餘5個框中,去掉與0.95框重疊率IoU大於0.6(可以另行設置),則保留0.9, 0.8, 0.7三個框
  • 重複上面的步驟,直到沒有框了,0.9爲一個框
  • 選出來的爲: 0.95, 0.9

以上這個過程,就是所謂的NMS算法(非極大值抑制)。該算法可以理解爲局部最大搜索。這個局部代表的是一個鄰域,鄰域有兩個參數可變,一是鄰域的維數,二是鄰域的大小。

在代碼中,使用了tenorflow內置函數non_max_suppression完成了NMS算法。該函數接收四個參數:要進行處理的box,每個box得分,box最多保留的個數,將不同鄰域區分開的閾值。經過NMS處理,按照傳入的post_nms_topN=2000,最終留下2000個得分最大的proposals,這時候它們被稱爲rois。

這一步寫的不夠詳細。其實一同返回的,除了rois,還有一個概率向量rpn_scores。

(4)生成anchors的標籤

通過_anchor_target_layer函數來設置每個anchors的標籤,傳入的是每個anchor的二分類特徵。在這個函數中,實際上調用的是anchor_target_layer.py文件中的anchor_target_layer函數來完成主要的功能,需要傳給該函數以下參數:

每個anchor的二分類特徵

  • _gt_boxes(該圖對應的ground truth信息,每條前四個值是框座標,最後一個類別索引)
  • _im_info(圖像壓錯信息)
  • _feat_stride(圖像壓縮比例)
  • _anchors(原始圖上所有的anchors)
  • _num_anchors(特徵圖上每個點對應的anchors數量,爲9)

這個調用函數的代碼如下,請注意,這裏使用了tf.py_func()的方式來調用。

  def _anchor_target_layer(self, rpn_cls_score, name):
    with tf.variable_scope(name) as scope:
      rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights = tf.py_func(
        anchor_target_layer,
        [rpn_cls_score, self._gt_boxes, self._im_info, self._feat_stride, self._anchors, self._num_anchors],
        [tf.float32, tf.float32, tf.float32, tf.float32],
        name="anchor_target")
    .
    .
    .
    return rpn_labels

首先去掉所有座標超出原圖的的anchors,相當於對anchors進行了過濾。

接下來創建一個label向量,用來標記每個樣本是正樣本還是負樣本,因此使用了過濾後anchors的數量長度做初始化,並填充-1值作爲默認值,表示這不是樣本(非樣本)。

然後通過bbox_overlaps計算過濾後的archors(N*4)和gt_boxes(M*4)的重疊面積與非重疊面積的比值矩陣:overlaps(N*M),並得到以下幾個值:

  • 每個archor對應的最大重疊ground_truth的值max_overlaps(1*N),即:爲每一個過濾後的anchor找到與其重疊最好的GT。可以想見,如果某個圖只有一個ground trueh,那麼max_overlaps一定是0,其本質是指哪個gt最匹配該anchors,可以找到與過濾後anchors數量同樣多個索引值。
  • 上面得到過濾後每個anchor對應的的IOU值最大的GT的索引值,這一步取出這個IOU值,即max_overlaps。
  • 每個圖有一個或多個GT,按列求最大值,可獲得該GT對應的anchor的索引值gt_argmax_overlaps,這是個M維的行向量,因爲GT一共有M個;
  • 這塊餘下的幾行代碼貌似沒啥用處,這個稍晚可以進行驗證一下。

以上計算過程,看着很複雜,其實通過下圖(圖中只是隨便的舉例)能很直觀的理解。

以一個5*3的比值矩陣overlaps來說:

對行求最大值,直接可以獲取到該行anchor對應的GT的索引值,如anchor1對應的GT的索引是0;anchor2的GT的索引是2;

對列求最大值,直接可以獲取該列GT對應的anchor的索引值,如GT1對應的anchor的索引是0;GT2對應的anchor的索引是3;

現在,是時候給標籤向量label進行賦值了。根據每個anchor的max_overlaps和gt_argmax_overlaps來給label向量做填充:

  • max_overlaps < 預定義的閾值,設爲0(該anchor是個負樣本,或者說是背景)
  • max_overlaps > 預定義的閾值,設爲1(該anchor是個正樣本,或者說物體)
  • 每個gt_argmax_overlaps所對應的anchor設置爲1(該anchor是個正樣本,或者說物體)

到這裏,有必要展示一下相關代碼:

  # only keep anchors inside the image
  inds_inside = np.where(
    (all_anchors[:, 0] >= -_allowed_border) &
    (all_anchors[:, 1] >= -_allowed_border) &
    (all_anchors[:, 2] < im_info[1] + _allowed_border) &  # width
    (all_anchors[:, 3] < im_info[0] + _allowed_border)  # height
  )[0]

  # keep only inside anchors
  anchors = all_anchors[inds_inside, :]

  # label: 1 is positive, 0 is negative, -1 is dont care
  # label: 1 正樣本, 0 負樣本, -1 非樣本
  labels = np.empty((len(inds_inside),), dtype=np.float32)
  labels.fill(-1)

  # overlaps between the anchors and the gt boxes
  # overlaps (ex, gt)
  # bbox_overlaps返回一個N*K的array,N爲roi的個數,K爲GT個數。對應元素(n,k)存的是第n個roi與第k個GT的:重疊面積/(roi面積+GT面積-重疊面積)
  overlaps = bbox_overlaps(np.ascontiguousarray(anchors, dtype=np.float), np.ascontiguousarray(gt_boxes, dtype=np.float))
  argmax_overlaps = overlaps.argmax(axis=1)
  max_overlaps = overlaps[np.arange(len(inds_inside)), argmax_overlaps]
  gt_argmax_overlaps = overlaps.argmax(axis=0)
  # 下面這兩行代碼貌似沒啥用。。。。。。。待驗證
  gt_max_overlaps = overlaps[gt_argmax_overlaps, np.arange(overlaps.shape[1])]
  gt_argmax_overlaps = np.where(overlaps == gt_max_overlaps)[0]

  if not cfg.TRAIN.RPN_CLOBBER_POSITIVES:
    # assign bg labels first so that positive labels can clobber them
    # first set the negatives
    # 將archors對應的正樣本的重疊區域中小於閾值的置0,認爲是背景
    labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0

  # fg label: for each gt, anchor with highest overlap
  # 每個真實位置對應的archors置1
  labels[gt_argmax_overlaps] = 1
  if cfg.TRAIN.RPN_CLOBBER_POSITIVES:
    # assign bg labels last so that negative labels can clobber positives
    labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0

最後說一下,bbox_overlaps函數在util/bbox.pyx中定義,編譯後的文件以.so結尾,在python中進行調用。這是cython的編程方式,此處不過多追究。

(5)在上面的labels中進行標籤設置,正負樣本合計256個,多餘的標記爲非樣本

接上面,依然通過anchor_target_layer.py文件中的anchor_target_layer函數來進行。

如果正樣本數超過128個,則隨機挑選128個anchors作爲正樣本,並將多餘的正樣本的標籤設置爲-1(非樣本),因爲不需要關注那麼多;

如果有過多的負樣本,則只隨機選擇(256-正樣本個數)個負樣本,並將多餘的負樣本的標籤設置爲-1(非樣本),因爲不需要關注那麼多;相關代碼如下:

  # subsample positive labels if we have too many
  # 如果有過多的正樣本,則只隨機選擇num_fg=0.5*256=128個正樣本
  num_fg = int(cfg.TRAIN.RPN_FG_FRACTION * cfg.TRAIN.RPN_BATCHSIZE)
  fg_inds = np.where(labels == 1)[0]
  if len(fg_inds) > num_fg:
    disable_inds = npr.choice(
      fg_inds, size=(len(fg_inds) - num_fg), replace=False)
    # 將多於的正樣本設置爲不關注
    labels[disable_inds] = -1

  # subsample negative labels if we have too many
  # 如果有過多的負樣本,則只隨機選擇 num_bg=256-正樣本個數 個負樣本
  num_bg = cfg.TRAIN.RPN_BATCHSIZE - np.sum(labels == 1)
  bg_inds = np.where(labels == 0)[0]
  if len(bg_inds) > num_bg:
    disable_inds = npr.choice(
      bg_inds, size=(len(bg_inds) - num_bg), replace=False)
    # 將多餘的負樣本設置爲不關注
    labels[disable_inds] = -1

(6)計算過濾後anchors和其對應的GT的位置座標偏移

通過_compute_targets函數計算過濾後的anchors和GT的位置座標偏移bbox_targets。

(7)計算bbox_weights

使用(過濾後的anchors數量,4)初始化bbox_inside_weights和bbox_outside_weights兩個向量並填充0,被用於後面框的位置迴歸損失函數。這裏稍微解釋下這兩個向量:

  • bbox inside weights:

       當該anchor的label=1時填充值1,否則仍爲0。

       用來設置正樣本回歸 loss 的權重,默認爲 1(負樣本爲0,即可以區分正負樣本是否計算 loss)。

       原論文中,正樣本計算迴歸 loss,負樣本不計算(可認爲 loss 爲 0)。

  •  bbox outside weights:

       當該anchor的label=-1時填充0,其餘填充值爲1/[num(bg)+num(fg)],實際就是1/256。       

       用來平衡 RPN 分類 Loss 和迴歸 Loss 的權重,對應論文(the reg term is normalized by the number of anchor locations)。

(8)將labels、bbox_targets和bbox_weights映射回原始anchors(未過濾前)的尺度

通過三次_unmap函數調用在變換回和原始archors一樣大小的rpn_labels(archors是正樣本、負樣本還是非樣本),rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights。

假設RPN網絡的輸入feature map大小是38*50,那麼最終這3個變量的第一個維度就是9*38*50=17100, 也就是最原始的anchor數量:

  • 對於輸入參數labels,尺度被擴展到(17100,0)並默認填充值-1。根據輸入的labels的值填充擴展後的labels。
  • 對於輸入參數bbox_targets,尺度被擴展到(17100,4)並默認填充值0。根據輸入的bbox_targets填充擴展後的bbox_targets。
  • 對於輸入參數bbox_inside_weights,尺度被擴展到(17100,4)並默認填充值0。根據輸入的bbox_inside_weights填充擴展後的bbox_inside_weights。
  • 對於輸入參數bbox_outside_weights,同上。

這樣,四個向量被按照原始的anchor數量進行了擴展。 後面的代碼不過是爲了進行接下來的運算而進行了適當的變形,不再詳細討論。貼下源碼:

  .
  .
  .
  bbox_targets = np.zeros((len(inds_inside), 4), dtype=np.float32)
  # 通過archors和archors對應的正樣本計算座標的偏移
  bbox_targets = _compute_targets(anchors, gt_boxes[argmax_overlaps, :])

  bbox_inside_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)
  # only the positive ones have regression targets
  # 正樣本的四個座標的權重均設置爲1
  bbox_inside_weights[labels == 1, :] = np.array(cfg.TRAIN.RPN_BBOX_INSIDE_WEIGHTS)

  bbox_outside_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)
  if cfg.TRAIN.RPN_POSITIVE_WEIGHT < 0:
    # uniform weighting of examples (given non-uniform sampling)
    # 記錄需要訓練的anchor,即標籤爲0與1的
    num_examples = np.sum(labels >= 0)
    positive_weights = np.ones((1, 4)) * 1.0 / num_examples  # 歸一化的權重
    negative_weights = np.ones((1, 4)) * 1.0 / num_examples  # 歸一化的權重
  else:
    assert ((cfg.TRAIN.RPN_POSITIVE_WEIGHT > 0) &
            (cfg.TRAIN.RPN_POSITIVE_WEIGHT < 1))
    positive_weights = (cfg.TRAIN.RPN_POSITIVE_WEIGHT /
                        np.sum(labels == 1))
    negative_weights = ((1.0 - cfg.TRAIN.RPN_POSITIVE_WEIGHT) /
                        np.sum(labels == 0))
  bbox_outside_weights[labels == 1, :] = positive_weights  # 歸一化的權重
  bbox_outside_weights[labels == 0, :] = negative_weights  # 歸一化的權重

  # map up to original set of anchors
  # 由於上面使用了inds_inside,此處將labels,bbox_targets,bbox_inside_weights,bbox_outside_weights映射到原始的archors(包含未知
  # 參數超出圖像邊界的archors)對應的labels,bbox_targets,bbox_inside_weights,bbox_outside_weights,同時將不需要的填充fill的值
  labels = _unmap(labels, total_anchors, inds_inside, fill=-1)
  bbox_targets = _unmap(bbox_targets, total_anchors, inds_inside, fill=0)
  # 所有archors中正樣本的四個座標的權重均設置爲1,其他爲0
  bbox_inside_weights = _unmap(bbox_inside_weights, total_anchors, inds_inside, fill=0)
  bbox_outside_weights = _unmap(bbox_outside_weights, total_anchors, inds_inside, fill=0)

  # labels
  labels = labels.reshape((1, height, width, A)).transpose(0, 3, 1, 2)
  labels = labels.reshape((1, 1, A * height, width))
  # 特徵圖中每個位置對應的是正樣本、負樣本還是非樣本(去除了邊界在圖像外面的archors)
  rpn_labels = labels

  # bbox_targets
  bbox_targets = bbox_targets.reshape((1, height, width, A * 4))  # 1*(9*?)*?*4==>1*?*?*(9*4)

  # 特徵圖中每個位置和對應的正樣本的座標偏移(很多爲0)
  rpn_bbox_targets = bbox_targets
  # bbox_inside_weights
  bbox_inside_weights = bbox_inside_weights.reshape((1, height, width, A * 4))  # 1*(9*?)*?*4==>1*?*?*(9*4)
  rpn_bbox_inside_weights = bbox_inside_weights

  # bbox_outside_weights
  bbox_outside_weights = bbox_outside_weights.reshape((1, height, width, A * 4))  # 1*(9*?)*?*4==>1*?*?*(9*4)
  #   歸一化的權重
  rpn_bbox_outside_weights = bbox_outside_weights
  return rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights

# 擴充變量到未過濾前的anchors的尺寸,並進行值填充
def _unmap(data, count, inds, fill=0):
  """ Unmap a subset of item (data) back to the original set of items (of
  size count) """
  if len(data.shape) == 1:
    ret = np.empty((count,), dtype=np.float32)  # 得到1維矩陣
    ret.fill(fill)   # 默認填充fill的值
    ret[inds] = data  # 有效位置填充具體數據
  else:
    ret = np.empty((count,) + data.shape[1:], dtype=np.float32)  # 得到對應維數的矩陣
    ret.fill(fill)  # 默認填充fill的值
    ret[inds, :] = data  # 有效位置填充具體數據
  return ret

# 計算anchor和對應的GT間的位置座標偏移
def _compute_targets(ex_rois, gt_rois):
  """Compute bounding-box regression targets for an image."""

  assert ex_rois.shape[0] == gt_rois.shape[0]
  assert ex_rois.shape[1] == 4
  assert gt_rois.shape[1] == 5

  # 通過公式2後四個,結合archor和對應的正樣本的座標計算座標的偏移
  # 由於gt_rois是5列,去掉最後一列的batch_inds
  return bbox_transform(ex_rois, gt_rois[:, :4]).astype(np.float32, copy=False)  

(9)從上面2000個rois中篩選256個rois及相關信息

這是通過_proposal_target_layer函數進行的,該函數接收2000個rois及概率向量rpn_scores(見上文),經處理後返回以下:

  • rois:               從post_nms_topN個rois中選擇256個rois(第一列的全0更新爲每個rois對應的類別)
  • roi_scores:    256個archors對應的爲正樣本的概率
  • bbox_targets:256*(4*21)的矩陣,只有爲正樣本時,對應類別的座標纔不爲0,其他類別的座標全爲0
  • bbox_inside_weights:  256*(4*21)的矩陣,正樣本時,對應類別四個座標的權重爲1,其他全爲0
  • bbox_outside_weights:256*(4*21)的矩陣,正樣本時,對應類別四個座標的權重爲1,其他全爲0

_proposal_target_layer實際上調用了proposal_target_layer.py中的proposal_target_layer函數,進而調用_sample_rois函數來完成具體的處理,這裏有幾個參數解釋下:

  • num_images          :每次處理的圖片數。在faster rcnn中,每次只處理1張,故此取值1
  • rois_per_image      :  沒張圖片使用的rois數,爲256
  • fg_rois_per_image :前景(物體)在rois中佔比,爲0.25,所以正樣本爲64個

具體來說_sample_rois函數(這裏的理解可以參看上面的部分,很類似):

  • 首先,再次使用到了bbox_overlaps函數,這次傳入的是2000個rois及該圖像的所有GT,計算二者之間的IOU矩陣向量overlaps。
  • 對overlaps按行求最大值,可以獲得每個rois對應的GT的索引(gt_assignment)、IOU值(max_overlaps)。
  • 獲取到每個rois對應的GT中標記的類別索引,設置到labels標籤中。這裏有點讓人疑惑,爲何所有rois的
  • 根據TRAIN.FG_THRESH(0.5)和TRAIN.BG_THRESH_LO(0.0)兩個預定義的閾值,判斷正負樣本,並得到其位置索引,存放在正負樣本位置索引向量中,正樣本位置索引向量fg_inds,正樣本位置索引向量bg_inds。判斷邏輯如下:

       如果rois和GT的max_overlaps > TRAIN.FG_THRESH, 那麼是正樣本,其anchor的位置索引保存在正樣本索引向量中;

       如果rois和GT的max_overlaps >TRAIN.BG_THRESH_LO 且 < TRAIN.FG_THRESH,那麼是負樣本,其anchor的位置索引保存在負樣本索引向量中。

  • 選擇最終的256個rois,有如下規則:

正負樣本都存在則選擇最多fg_rois_per_image個正樣本,不夠的話,補充負樣本;

只有正樣本,選擇rois_per_image個正樣本。這裏請注意npr.choice()函數的應用,會有重複採樣可能。

只有負樣本,選擇rois_per_image個正樣本。這裏請注意npr.choice()函數的應用,會有重複採樣可能。

在只有正樣本或負樣本時,首先會判斷樣本數和rois_per_image(值爲256)的大小關係,如果小於則爲true;這時使用npr.choice()函數進行採樣,因爲不滿足rois_per_image個樣本,所以會進行重複採樣;反之,則不進行重複採樣。

把上面步驟獲得的正負樣本索引放在一個樣本索引向量keep_inds中,注意,正樣本在前,負樣本在後。

  • 從之前的2000個樣本的labels(值爲1)中,根據keep_inds構造這256個樣本的標籤labels,並給前面的正樣本賦值0。
  • 從之前的2000個rois中,根據keep_inds獲取256個樣本rois。
  • 從之前的2000個all_scores中,根據keep_inds獲取256個roi_scores。
  • 使用_compute_targets函數,傳入256個roi的位置框、GT的位置框、以及256個類別標籤labels,計算得到256個roi的位置偏移;並使用對應的標籤來填充第一列,最終形成(256,5)的向量bbox_target_data。
  • 使用_get_bbox_regression_labels函數,傳入bbox_target_data和類別數num_classes,求得最終計算loss時使用的ground truth邊框迴歸值和bbox_inside_weights:

  bbox_targets:256*(4*21)的矩陣,正樣本時,位置[20,23]是框的迴歸座標值,[0,19]是0;負樣本時整個樣本[0,83]全0;
             bbox_inside_weights:256*(4*21)的矩陣,正樣本時,位置[20,23]設值1,[0,19]是0;負樣本時整個樣本[0,83]全0;

至此,proposal_target_layer函數執行完畢,這真是一個複雜的過程。該函數返回如下向量:

  labels:256個正樣本和負樣本對應的真實的類別
  rois:從post_nms_topN個archors中選擇256個archors(第一列的全0更新爲每個archors對應的類別)
  roi_scores:256個archors對應的爲正樣本的概率
  bbox_targets:256*(4*21)的矩陣
  bbox_inside_weights:256*(4*21)的矩陣

到這裏,有必要貼下相關代碼:

def proposal_target_layer(rpn_rois, rpn_scores, gt_boxes, _num_classes):
  """
  Assign object detection proposals to ground-truth targets. Produces proposal
  classification labels and bounding-box regression targets.
  """

  # Proposal ROIs (0, x1, y1, x2, y2) coming from RPN
  # (i.e., rpn.proposal_layer.ProposalLayer), or any other source
  # rpn_rois爲post_nms_topN*5的矩陣 (2000*5)
  all_rois = rpn_rois
  # rpn_scores爲post_nms_topN的矩陣,代表對應的archors爲正樣本的概率
  all_scores = rpn_scores

  # Include ground-truth boxes in the set of candidate rois
  # USE_GT=False,未使用這段代碼
  if cfg.TRAIN.USE_GT:
    zeros = np.zeros((gt_boxes.shape[0], 1), dtype=gt_boxes.dtype)
    all_rois = np.vstack(
      (all_rois, np.hstack((zeros, gt_boxes[:, :-1])))
    )
    # not sure if it a wise appending, but anyway i am not using it
    all_scores = np.vstack((all_scores, zeros))

  # 該程序只能一次處理一張圖片
  num_images = 1
  # 每張圖片中最終選擇的rois
  rois_per_image = cfg.TRAIN.BATCH_SIZE / num_images
  # 正樣本的個數:0.25*rois_per_image
  fg_rois_per_image = np.round(cfg.TRAIN.FG_FRACTION * rois_per_image)

  # Sample rois with classification labels and bounding box regression
  # targets
  labels, rois, roi_scores, bbox_targets, bbox_inside_weights = _sample_rois(
    all_rois, all_scores, gt_boxes, fg_rois_per_image,
    rois_per_image, _num_classes)   # 選擇256個archors

  rois = rois.reshape(-1, 5)
  roi_scores = roi_scores.reshape(-1)
  labels = labels.reshape(-1, 1)
  bbox_targets = bbox_targets.reshape(-1, _num_classes * 4)
  bbox_inside_weights = bbox_inside_weights.reshape(-1, _num_classes * 4)
  # 256*(4*21)的矩陣,正樣本時,對應類別四個座標的權重爲1,其他全爲0
  bbox_outside_weights = np.array(bbox_inside_weights > 0).astype(np.float32)

  return rois, roi_scores, labels, bbox_targets, bbox_inside_weights, bbox_outside_weights


def _get_bbox_regression_labels(bbox_target_data, num_classes):
  """Bounding-box regression targets (bbox_target_data) are stored in a
  compact form N x (class, tx, ty, tw, th)

  This function expands those targets into the 4-of-4*K representation used
  by the network (i.e. only one class has non-zero targets).

  Returns:
      bbox_target (ndarray): N x 4K blob of regression targets
      bbox_inside_weights (ndarray): N x 4K blob of loss weights
  """
  # 第1列,爲類別
  clss = bbox_target_data[:, 0]
  # 256*(4*21)的矩陣
  bbox_targets = np.zeros((clss.size, 4 * num_classes), dtype=np.float32)
  bbox_inside_weights = np.zeros(bbox_targets.shape, dtype=np.float32)
  # 正樣本的索引
  inds = np.where(clss > 0)[0]
  for ind in inds:
    # 正樣本的類別
    cls = clss[ind]
    # 每個正樣本的起始座標
    start = int(4 * cls)
    # 每個正樣本的終止座標(由於座標爲4)
    end = start + 4
    # 對應的座標偏移賦值給對應的類別
    bbox_targets[ind, start:end] = bbox_target_data[ind, 1:]
    # 對應的權重(1.0, 1.0, 1.0, 1.0)賦值給對應的類別
    bbox_inside_weights[ind, start:end] = cfg.TRAIN.BBOX_INSIDE_WEIGHTS

  # bbox_targets:256*(4*21)的矩陣,只有爲正樣本時,對應類別的座標纔不爲0,其他類別的座標全爲0
  # bbox_inside_weights:256*(4*21)的矩陣,正樣本時,對應類別四個座標的權重爲1,其他全爲0
  return bbox_targets, bbox_inside_weights


def _compute_targets(ex_rois, gt_rois, labels):
  """Compute bounding-box regression targets for an image."""

  assert ex_rois.shape[0] == gt_rois.shape[0]
  assert ex_rois.shape[1] == 4
  assert gt_rois.shape[1] == 4

  # 通過公式2後四個,結合256個archor和對應的正樣本的座標計算座標的偏移
  targets = bbox_transform(ex_rois, gt_rois)
  if cfg.TRAIN.BBOX_NORMALIZE_TARGETS_PRECOMPUTED:
    # Optionally normalize targets by a precomputed mean and stdev
    targets = ((targets - np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS))
               / np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS))  # 座標減去均值除以標準差,進行歸一化
  # 之前的bbox第一列爲全0,此處第一列爲對應的類別
  return np.hstack((labels[:, np.newaxis], targets)).astype(np.float32, copy=False)

# all_rois第一列全0,後4列爲座標;gt_boxes前4列爲座標,最後一列爲類別
def _sample_rois(all_rois, all_scores, gt_boxes, fg_rois_per_image, rois_per_image, num_classes):
  """Generate a random sample of RoIs comprising foreground and background
  examples.
  """
  # overlaps: (rois x gt_boxes)
  # 計算archors和gt_boxes重疊區域面積的比值
  overlaps = bbox_overlaps(
    np.ascontiguousarray(all_rois[:, 1:5], dtype=np.float),
    np.ascontiguousarray(gt_boxes[:, :4], dtype=np.float))
  # 得到每個archors對應的gt_boxes的索引
  gt_assignment = overlaps.argmax(axis=1)
  # 得到每個archors對應的gt_boxes的重疊區域的值
  max_overlaps = overlaps.max(axis=1)
  # 得到每個archors對應的gt_boxes的類別索引
  labels = gt_boxes[gt_assignment, 4]

  # Select foreground RoIs as those with >= FG_THRESH overlap
  # 每個archors對應的gt_boxes的重疊區域的值大於閾值的作爲正樣本,得到正樣本的索引
  fg_inds = np.where(max_overlaps >= cfg.TRAIN.FG_THRESH)[0]
  # Guard against the case when an image has fewer than fg_rois_per_image
  # 每個archors對應的gt_boxes的重疊區域的值在給定閾值內的作爲負樣本,得到負樣本的索引
  # Select background RoIs as those within [BG_THRESH_LO, BG_THRESH_HI)
  bg_inds = np.where((max_overlaps < cfg.TRAIN.BG_THRESH_HI) &
                     (max_overlaps >= cfg.TRAIN.BG_THRESH_LO))[0]

  # Small modification to the original version where we ensure a fixed number of regions are sampled
  # 最終選擇256個archors
  if fg_inds.size > 0 and bg_inds.size > 0:  # 正負樣本均存在,則選擇最多fg_rois_per_image個正樣本,不夠的話,補充負樣本
    fg_rois_per_image = min(fg_rois_per_image, fg_inds.size)
    fg_inds = npr.choice(fg_inds, size=int(fg_rois_per_image), replace=False)
    bg_rois_per_image = rois_per_image - fg_rois_per_image
    to_replace = bg_inds.size < bg_rois_per_image
    bg_inds = npr.choice(bg_inds, size=int(bg_rois_per_image), replace=to_replace)
  elif fg_inds.size > 0:   # 只有正樣本,選擇rois_per_image個正樣本
    to_replace = fg_inds.size < rois_per_image
    fg_inds = npr.choice(fg_inds, size=int(rois_per_image), replace=to_replace)
    fg_rois_per_image = rois_per_image
  elif bg_inds.size > 0:  # 只有負樣本,選擇rois_per_image個負樣本
    to_replace = bg_inds.size < rois_per_image
    bg_inds = npr.choice(bg_inds, size=int(rois_per_image), replace=to_replace)
    fg_rois_per_image = 0
  else:
    import pdb
    pdb.set_trace()

  # The indices that we're selecting (both fg and bg)
  # 正樣本和負樣本的索引
  keep_inds = np.append(fg_inds, bg_inds)
  # Select sampled values from various arrays:
  # 正樣本和負樣本對應的真實的類別
  labels = labels[keep_inds]
  # Clamp labels for the background RoIs to 0
  # 負樣本對應的類別設置爲0
  labels[int(fg_rois_per_image):] = 0
  # 從post_nms_topN個archors中選擇256個archors
  rois = all_rois[keep_inds]
  # 256個archors對應的爲正樣本的概率
  roi_scores = all_scores[keep_inds]

  bbox_target_data = _compute_targets(rois[:, 1:5], gt_boxes[gt_assignment[keep_inds], :4], labels)
  bbox_targets, bbox_inside_weights = _get_bbox_regression_labels(bbox_target_data, num_classes)
  return labels, rois, roi_scores, bbox_targets, bbox_inside_weights

4、roi pooling層

爲什麼需要這個層呢?

按理說,上面的所有操作做完後應該直接送入Classifier層,但Classifier層是個卷積層。對於卷積網絡而言(如VGG,Resnet等),它只接收固定尺寸的輸入數據。

而我們的特徵提取層(conv)層的輸出的大小卻是不固定尺寸的,雖然可以使用截取或拉伸的方式強行讓輸入數據符合卷積網絡的輸入要求,但這會產生丟失像素或圖像失真問題。因此,可以採取RoI Pooling方式來解決這個問題。

在源碼中通過_crop_pool_layer函數來實現該層,該函數接收conv層的特徵圖、256個rois作爲參數。具體做法如下:

  • 獲取rois的第一列

先通過tf.slice()函數來截取rois的第一列的標籤類別值,返回的是shape爲(256,1)的張量,接着通過tf.squeeze()去掉維度爲1的維度,最終變成了一個shape爲(256,)的類別標籤。

  • rois做歸一化並做反向傳播截止

根據conv層輸出的特徵圖的寬高和參數_feat_stride,計算原始圖的寬高。利用此寬高,對rois的位置座標做歸一化處理,即:歸一化的x1和x2 = rois的x1和x1分別除以原始圖寬; 歸一化的y1和y2 = rois的y1和y1分別除以原始圖高。接着使用tf.concat()函數把歸一化處理後的x1,x2,y1,y2組合成[y1,x1,y2,x2]的形式,這樣rois經過歸一化後,其shape依然是(256,4)。

這裏有人也許會奇怪,爲什麼要把原來好好的[x1,y1,x2,y2]的順序,換成[y1,x1,y2,x2]的順序?其實答案很簡單,僅僅爲了滿足下面tf.image.crop_and_resize()函數的接口要求。

注意,這裏使用了一個tf.stop_gradient()函數,並傳入了歸一化的rois,表明這裏對反向傳播做了截止。但不明白爲什麼要這麼做,暫時存疑。

  • 剪裁併縮放rois

接下來使用tf.image.crop_and_resize()函數,在conv層輸出的特徵圖上,先將rois對應的特徵框切割出來(crop動作),並按照某一尺度(14×14)進行雙線性插值resize,再使用tf.nn.max_pool將特徵轉化爲更小的維度(7×7)。

這個crop,感覺有點像做方形月餅:先製作一個大的矩形的月餅,然後用方形月餅模子從大矩形月餅的左上角開始逐個切割。等到這個大矩形月餅從左到右從上倒下挨個用模子切割完畢,大月餅消失,產生了一堆小的方形月餅。

在這個比喻中,大矩形月餅就是原始圖輸出的特徵圖,即conv層輸出;小方形月餅模子就是rois。現實中,小方形月餅模子是不會有任何變化,變化的是人控制模子會移動,而rois中的座標位置,恰恰提供了移動的位置。不知道這個比喻形象不。

5、在特徵提取層(conv層)上添加FC層

使用_head_to_tail()函數在roi pooling層的輸出上添加FC層(fc7)。這個FC層是來自Resnet101網絡的conv5_x這個block塊。最後接了一個average pooling,得到2048維特徵,分別用於分類和框迴歸。

6、Classifier層(貌似也被稱爲rcnn層)

fc7層的輸出通過_region_classification進行分類及迴歸

fc7先通過fc層(無ReLU)降維到21層(類別數,得到cls_score),得到類別的概率cls_prob及預測的類別cls_pred(用於rcnn的分類);另一方面fc7通過fc層(無ReLU),降維到21*4,得到bbox_pred(用於rcnn的迴歸)。

 

 

 

 

 

 

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