文章目錄
- 原理解讀
- 代碼解讀
- 代碼結構圖
- 數據準備
- **combined_roidb**:
- get_roidb
- pascal_voc
- set_proposal_method
- gt_roidb
- get_training_roidb
- append_flipped_images
- prepare_roidb
- train_net
- filter_roidb
- RoIDataLayer
- 訓練階段
- create_architecture
- _build_network
- _image_to_head
- _anchor_component
- generate_anchors_pre
- generate_anchors
- _region_proposal
- _proposal_layer
- proposal_layer_tf
- bbox_transform_inv_tf
- _anchor_target_layer
- anchor_target_layer
- _compute_targets
- bbox_transform
- _unmap
- bbox_overlaps
- _proposal_target_layer
- proposal_target_layer
- _get_bbox_regression_labels
- _compute_targets
- _sample_rois
- _crop_pool_layer
- _head_to_tail
- **_region_classification**
- 損失函數
- 補充說明
代碼git地址:https://github.com/endernewton/tf-faster-rcnn
原理解讀
R-CNN --> FAST-RCNN --> FASTER-RCNN
R-CNN:
(1)輸入測試圖像;
(2)利用selective search 算法在圖像中從上到下提取2000個左右的Region Proposal;
(3)將每個Region Proposal縮放(warp)成227*227的大小並輸入到CNN,將CNN的fc7層的輸出作爲特徵;
(4)將每個Region Proposal提取的CNN特徵輸入到SVM進行分類;
(5)對於SVM分好類的Region Proposal做邊框迴歸,用Bounding box迴歸值校正原來的建議窗口,生成預測窗口座標.
缺陷:
(1) 訓練分爲多個階段,步驟繁瑣:微調網絡+訓練SVM+訓練邊框迴歸器;
(2) 訓練耗時,佔用磁盤空間大;5000張圖像產生幾百G的特徵文件;
(3) 速度慢:使用GPU,VGG16模型處理一張圖像需要47s;
(4) 測試速度慢:每個候選區域需要運行整個前向CNN計算;
(5) SVM和迴歸是事後操作,在SVM和迴歸過程中CNN特徵沒有被學習更新.
FAST-RCNN:
(1)輸入測試圖像;
(2)利用selective search 算法在圖像中從上到下提取2000個左右的建議窗口(Region Proposal);
(3)將整張圖片輸入CNN,進行特徵提取;
(4)把建議窗口映射到CNN的最後一層卷積feature map上;
(5)通過RoI pooling層使每個建議窗口生成固定尺寸的feature map;
(6)利用Softmax Loss(探測分類概率) 和Smooth L1 Loss(探測邊框迴歸)對分類概率和邊框迴歸(Bounding box regression)聯合訓練.
相比R-CNN,主要兩處不同:
(1)最後一層卷積層後加了一個ROI pooling layer;
(2)損失函數使用了多任務損失函數(multi-task loss),將邊框迴歸直接加入到CNN網絡中訓練
改進:
(1) 測試時速度慢:R-CNN把一張圖像分解成大量的建議框,每個建議框拉伸形成的圖像都會單獨通過CNN提取特徵.實際上這些建議框之間大量重疊,特徵值之間完全可以共享,造成了運算能力的浪費.
FAST-RCNN將整張圖像歸一化後直接送入CNN,在最後的卷積層輸出的feature map上,加入建議框信息,使得在此之前的CNN運算得以共享.
(2) 訓練時速度慢:R-CNN在訓練時,是在採用SVM分類之前,把通過CNN提取的特徵存儲在硬盤上.這種方法造成了訓練性能低下,因爲在硬盤上大量的讀寫數據會造成訓練速度緩慢.
FAST-RCNN在訓練時,只需要將一張圖像送入網絡,每張圖像一次性地提取CNN特徵和建議區域,訓練數據在GPU內存裏直接進Loss層,這樣候選區域的前幾層特徵不需要再重複計算且不再需要把大量數據存儲在硬盤上.
(3) 訓練所需空間大:R-CNN中獨立的SVM分類器和迴歸器需要大量特徵作爲訓練樣本,需要大量的硬盤空間.FAST-RCNN把類別判斷和位置迴歸統一用深度網絡實現,不再需要額外存儲.
FASTER -RCNN:
整體架構:
(1)輸入測試圖像;
(2)將整張圖片輸入CNN,進行特徵提取;
(3)用RPN生成建議窗口(proposals),每張圖片生成300個建議窗口;
(4)把建議窗口映射到CNN的最後一層卷積feature map上;
(5)通過RoI pooling層使每個RoI生成固定尺寸的feature map;
(6)利用Softmax Loss(探測分類概率) 和Smooth L1 Loss(探測邊框迴歸)對分類概率和邊框迴歸(Bounding box regression)聯合訓練.
我們先整體的介紹下上圖中各層主要的功能
1)、Conv layers提取特徵圖:
作爲一種CNN網絡目標檢測方法,Faster RCNN首先使用一組基礎的conv+relu+pooling層提取input image的feature maps,該feature maps會用於後續的RPN層和全連接層
2)、RPN(Region Proposal Networks):
RPN網絡主要用於生成region proposals,首先生成一堆Anchor box,對其進行裁剪過濾後通過softmax判斷anchors屬於前景(foreground)或者後景(background),即是物體or不是物體,所以這是一個二分類;同時,另一分支bounding box regression修正anchor box,形成較精確的proposal(注:這裏的較精確是相對於後面全連接層的再一次box regression而言)
3)、Roi Pooling:
該層利用RPN生成的proposals和VGG16最後一層得到的feature map,得到固定大小的proposal feature map,進入到後面可利用全連接操作來進行目標識別和定位
4)、Classifier:
會將Roi Pooling層形成固定大小的feature map進行全連接操作,利用Softmax進行具體類別的分類,同時,利用L1 Loss完成bounding box regression迴歸操作獲得物體的精確位置.
相比FASTER-RCNN,主要兩處不同:
(1)使用RPN(Region Proposal Network)代替原來的Selective Search方法產生建議窗口;
(2)產生建議窗口的CNN和目標檢測的CNN共享
改進:
(1) 如何高效快速產生建議框?
FASTER-RCNN創造性地採用卷積網絡自行產生建議框,並且和目標檢測網絡共享卷積網絡,使得建議框數目從原有的約2000個減少爲300個,且建議框的質量也有本質的提高.
網絡結構
現在,通過上圖開始逐層分析
Conv layers
Faster RCNN首先是支持輸入任意大小的圖片的,比如上圖中輸入的P*Q,進入網絡之前對圖片進行了規整化尺度的設定,如可設定圖像短邊不超過600,圖像長邊不超過1000,我們可以假定M*N=1000*600(如果圖片少於該尺寸,可以邊緣補0,即圖像會有黑色邊緣)
① 13個conv層:kernel_size=3,pad=1,stride=1;
卷積公式:
所以,conv層不會改變圖片大小(即:輸入的圖片大小=輸出的圖片大小)
② 13個relu層:激活函數,不改變圖片大小
③ 4個pooling層:kernel_size=2,stride=2;pooling層會讓輸出圖片是輸入圖片的1/2
經過Conv layers,圖片大小變成(M/16)*(N/16),即:60*40(1000/16≈60,600/16≈40);則,Feature Map就是60*40*512-d(注:VGG16是512-d,ZF是256-d),表示特徵圖的大小爲60*40,數量爲512
RPN(Region Proposal Networks):
Feature Map進入RPN後,先經過一次3*3的卷積,同樣,特徵圖大小依然是60*40,數量512,這樣做的目的應該是進一步集中特徵信息,接着看到兩個全卷積,即kernel_size=1*1,p=0,stride=1;
如上圖中標識:
① rpn_cls:60*40*512-d ⊕ 1*1*512*18 ==> 60*40*9*2
逐像素對其9個Anchor box進行二分類
② rpn_bbox:60*40*512-d ⊕ 1*1*512*36==>60*40*9*4
逐像素得到其9個Anchor box四個座標信息(其實是偏移量,後面介紹)
如下圖所示:
(2.1)、Anchors的生成規則
前面提到經過Conv layers後,圖片大小變成了原來的1/16,令_feat_stride=16,在生成Anchors時,我們先定義一個base_anchor,大小爲16*16的box(因爲特徵圖(60*40)上的一個點,可以對應到原圖(1000*600)上一個16*16大小的區域),源碼中轉化爲[0,0,15,15]的數組,參數ratios=[0.5, 1, 2],scales=[8, 16, 32]
先看[0,0,15,15],面積保持不變,長、寬比分別爲[0.5, 1, 2]是產生的Anchors box
如果經過scales變化,即長、寬分別均爲 (16*8=128)、(16*16=256)、(16*32=512),對應anchor box如圖
綜合以上兩種變換,最後生成9個Anchor box
所以,最終base_anchor=[0,0,15,15]生成的9個Anchor box座標如下:
[](javascript:void(0)😉
1 [[ -84. -40. 99. 55.]
2 [-176. -88. 191. 103.]
3 [-360. -184. 375. 199.]
4 [ -56. -56. 71. 71.]
5 [-120. -120. 135. 135.]
6 [-248. -248. 263. 263.]
7 [ -36. -80. 51. 95.]
8 [ -80. -168. 95. 183.]
9 [-168. -344. 183. 359.]]
[](javascript:void(0)😉
特徵圖大小爲60*40,所以會一共生成60*40*9=21600個Anchor box
源碼中,通過width:(0-60)*16,height(0-40)*16建立shift偏移量數組,再和base_anchor基準座標數組累加,得到特徵圖上所有像素對應的Anchors的座標值,是一個[216000,4]的數組
RPN的實現方式:在conv5-3的卷積feature map上用一個n*n的滑窗(論文中作者選用了n=3,即3*3的滑窗)生成一個長度爲256(對應於ZF網絡)或512(對應於VGG網絡)維長度的全連接特徵.然後在這個256維或512維的特徵後產生兩個分支的全連接層:
(1)reg-layer,用於預測proposal的中心錨點對應的proposal的座標x,y和寬高w,h;
(2)cls-layer,用於判定該proposal是前景還是背景.sliding window的處理方式保證reg-layer和cls-layer關聯了conv5-3的全部特徵空間.事實上,作者用全連接層實現方式介紹RPN層實現容易幫助我們理解這一過程,但在實現時作者選用了卷積層實現全連接層的功能.
(3)個人理解:全連接層本來就是特殊的卷積層,如果產生256或512維的fc特徵,事實上可以用Num_out=256或512, kernel_size=3*3, stride=1的卷積層實現conv5-3到第一個全連接特徵的映射.然後再用兩個Num_out分別爲2*9=18和4*9=36,kernel_size=1*1,stride=1的卷積層實現上一層特徵到兩個分支cls層和reg層的特徵映射.
(4)注意:這裏2*9中的2指cls層的分類結果包括前後背景兩類,4*9的4表示一個Proposal的中心點座標x,y和寬高w,h四個參數.採用卷積的方式實現全連接處理並不會減少參數的數量,但是使得輸入圖像的尺寸可以更加靈活.在RPN網絡中,我們需要重點理解其中的anchors概念,Loss fucntions計算方式和RPN層訓練數據生成的具體細節.
Anchors:字面上可以理解爲錨點,位於之前提到的n*n的sliding window的中心處.對於一個sliding window,我們可以同時預測多個proposal,假定有k個proposal即k個reference boxes,每一個reference box又可以用一個scale,一個aspect_ratio和sliding window中的錨點唯一確定.所以,我們在後面說一個anchor,你就理解成一個anchor box 或一個reference box.作者在論文中定義k=9,即3種scales和3種aspect_ratio確定出當前sliding window位置處對應的9個reference boxes, 4*k個reg-layer的輸出和2*k個cls-layer的score輸出.對於一幅W*H的feature map,對應W*H*k個錨點.所有的錨點都具有尺度不變性.
Loss functions:
在計算Loss值之前,作者設置了anchors的標定方法.正樣本標定規則:
-
如果Anchor對應的reference box與ground truth的IoU值最大,標記爲正樣本;
-
如果Anchor對應的reference box與ground truth的IoU>0.7,標記爲正樣本.事實上,採用第2個規則基本上可以找到足夠的正樣本,但是對於一些極端情況,例如所有的Anchor對應的reference box與groud truth的IoU不大於0.7,可以採用第一種規則生成.
-
負樣本標定規則:如果Anchor對應的reference box與ground truth的IoU<0.3,標記爲負樣本.
-
剩下的既不是正樣本也不是負樣本,不用於最終訓練.
-
訓練RPN的Loss是有classification loss (即softmax loss)和regression loss (即L1 loss)按一定比重組成的.
計算softmax loss需要的是anchors對應的groundtruth標定結果和預測結果,計算regression loss需要三組信息:
i. 預測框,即RPN網絡預測出的proposal的中心位置座標x,y和寬高w,h;
ii. 錨點reference box:
之前的9個錨點對應9個不同scale和aspect_ratio的reference boxes,每一個reference boxes都有一箇中心點位置座標x_a,y_a和寬高w_a,h_a;
iii. ground truth:標定的框也對應一箇中心點位置座標x*,y和寬高w,h*.因此計算regression loss和總Loss方式如下:
RPN訓練設置:
(1)在訓練RPN時,一個Mini-batch是由一幅圖像中任意選取的256個proposal組成的,其中正負樣本的比例爲1:1.
(2)如果正樣本不足128,則多用一些負樣本以滿足有256個Proposal可以用於訓練,反之亦然.
(3)訓練RPN時,與VGG共有的層參數可以直接拷貝經ImageNet訓練得到的模型中的參數;剩下沒有的層參數用標準差=0.01的高斯分佈初始化.
- RPN工作原理解析
爲了進一步更清楚的看懂RPN的工作原理,將Caffe版本下的網絡圖貼出來,對照網絡圖進行講解會更清楚
主要看上圖中框住的‘RPN’部分的網絡圖,其中‘rpn_conv/3*3’是3*3的卷積,上面有提到過,接着是兩個1*1的全卷積,分別是圖中的‘rpn_cls_score’和‘rpn_bbox_pred’,在上面同樣有提到過。接下來,分析網絡圖中其他各部分的含義
rpn-data:
爲特徵圖60*40上的每個像素生成9個Anchor box,並且對生成的Anchor box進行過濾和標記,參照源碼,過濾和標記規則如下:
① 去除掉超過1000*600這原圖的邊界的anchor box
② 如果anchor box與ground truth的IoU值最大,標記爲正樣本,label=1
③ 如果anchor box與ground truth的IoU>0.7,標記爲正樣本,label=1
④ 如果anchor box與ground truth的IoU<0.3,標記爲負樣本,label=0
剩下的既不是正樣本也不是負樣本,不用於最終訓練,label=-1
除了對anchor box進行標記外,另一件事情就是計算anchor box與ground truth之間的偏移量
令:ground truth:標定的框也對應一箇中心點位置座標x*,y和寬高w,h*
anchor box: 中心點位置座標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)
通過ground truth box與預測的anchor box之間的差異來進行學習,從而是RPN網絡中的權重能夠學習到預測box的能力
rpn_loss_cls、rpn_loss_bbox、rpn_cls_prob:
下面集體看下這三個,其中‘rpn_loss_cls’、‘rpn_loss_bbox’是分別對應softmax,smooth L1計算損失函數,‘rpn_cls_prob’計算概率值(可用於下一層的nms非最大值抑制操作)
補充:
① Softmax公式,計算各分類的概率值
② Softmax Loss公式,RPN進行分類時,即尋找最小Loss值
在’rpn-data’中已經爲預測框anchor box進行了標記,並且計算出與gt_boxes之間的偏移量,利用RPN網絡進行訓練。
RPN訓練設置:在訓練RPN時,一個Mini-batch是由一幅圖像中任意選取的256個proposal組成的,其中正負樣本的比例爲1:1。如果正樣本不足128,則多用一些負樣本以滿足有256個Proposal可以用於訓練,反之亦然
proposal:
’rpn_bbox_pred’,記錄着訓練好的四個迴歸值△x, △y, △w, △h。
源碼中,會重新生成60*40*9個anchor box,然後累加上訓練好的△x, △y, △w, △h,從而得到了相較於之前更加準確的預測框region proposal,進一步對預測框進行越界剔除和使用nms非最大值抑制,剔除掉重疊的框;比如,設定IoU爲0.7的閾值,即僅保留覆蓋率不超過0.7的局部最大分數的box(粗篩)。最後留下大約2000個anchor,然後再取前N個box(比如300個);這樣,進入到下一層ROI Pooling時region proposal大約只有300個
roi_data:
爲了避免定義上的誤解,我們將經過‘proposal’後的預測框稱爲region proposal(其實,RPN層的任務其實已經完成,roi_data屬於爲下一層準備數據)
主要作用:
① RPN層只是來確定region proposal是否是物體(是/否),這裏根據region proposal和ground truth box的最大重疊指定具體的標籤(就不再是二分類問題了,參數中指定的是81類)
② 計算region proposal與ground truth boxes的偏移量,計算方法和之前的偏移量計算公式相同
經過這一步後的數據輸入到ROI Pooling層進行進一步的分類和定位.
ROI Pooling
這層輸入的是RPN層產生的region proposal(假定有300個region proposal box)和VGG16最後一層產生的特徵圖(60*40* 512-d),遍歷每個region proposal,將其座標值縮小16倍,這樣就可以將在原圖(1000*600)基礎上產生的region proposal映射到60*40的特徵圖上,從而將在feature map上確定一個區域(定義爲RB)。
在feature map上確定的區域RB,根據參數pooled_w:7,pooled_h:7,將這個RB區域劃分爲7*7,即49個相同大小的小區域,對於每個小區域,使用max pooling方式從中選取最大的像素點作爲輸出,這樣,就形成了一個7*7的feature map
以此,參照上述方法,300個region proposal遍歷完後,會產生很多個7*7大小的feature map,故而輸出的數組是:[300,512,7,7],作爲下一層的全連接的輸入
ROI pooling layer實際上是SPP-NET的一個精簡版,SPP-NET對每個proposal使用了不同大小的金字塔映射,而ROI pooling layer只需要下采樣到一個7x7的特徵圖.對於VGG16網絡conv5_3有512個特徵圖,這樣所有region proposal對應了一個7*7*512維度的特徵向量作爲全連接層的輸入.
RoI Pooling就是實現從原圖區域映射到conv5區域最後pooling到固定大小的功能.
全連接層
經過roi pooling層之後,batch_size=300, proposal feature map的大小是7*7,512-d,對特徵圖進行全連接,參照下圖,最後同樣利用Softmax Loss和L1 Loss完成分類和定位
通過full connect層與softmax計算每個region proposal具體屬於哪個類別(如人,馬,車等),輸出cls_prob概率向量;同時再次利用bounding box regression獲得每個region proposal的位置偏移量bbox_pred,用於迴歸獲得更加精確的目標檢測框
即從PoI Pooling獲取到7x7大小的proposal feature maps後,通過全連接主要做了:
4.1)通過全連接和softmax對region proposals進行具體類別的分類
4.2)再次對region proposals進行bounding box regression,獲取更高精度的rectangle box
概念解釋:
SPP-NET
SSP-Net:Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition
先看一下R-CNN爲什麼檢測速度這麼慢,一張圖都需要47s!仔細看下R-CNN框架發現,對圖像提完Region Proposal(2000個左右)之後將每個Proposal當成一張圖像進行後續處理(CNN提特徵+SVM分類),實際上對一張圖像進行了2000次提特徵和分類的過程!這2000個Region Proposal不都是圖像的一部分嗎,那麼我們完全可以對圖像提一次卷積層特徵,然後只需要將Region Proposal在原圖的位置映射到卷積層特徵圖上,這樣對於一張圖像我們只需要提一次卷積層特徵,然後將每個Region Proposal的卷積層特徵輸入到全連接層做後續操作.(對於CNN來說,大部分運算都耗在卷積操作上,這樣做可以節省大量時間).
現在的問題是每個Region Proposal的尺度不一樣,直接這樣輸入全連接層肯定是不行的,因爲全連接層輸入必須是固定的長度.SPP-NET恰好可以解決這個問題.
由於傳統的CNN限制了輸入必須固定大小(比如AlexNet是224x224),所以在實際使用中往往需要對原圖片進行crop或者warp的操作:
-
crop:截取原圖片的一個固定大小的patch(物體可能會產生截斷,尤其是長寬比大的圖片)
-
warp:將原圖片的ROI縮放到一個固定大小的patch(物體被拉伸,失去“原形”,尤其是長寬比大的圖片)
SPP爲的就是解決上述的問題,做到的效果爲:不管輸入的圖片是什麼尺度,都能夠正確的傳入網絡.
具體思路爲:CNN的卷積層是可以處理任意尺度的輸入的,只是在全連接層處有限制尺度——換句話說,如果找到一個方法,在全連接層之前將其輸入限制到等長,那麼就解決了這個問題.具體方案如下圖所示:
如果原圖輸入是224x224,對於conv5出來後的輸出,是13x13x256的,可以理解成有256個這樣的filter,每個filter對應一張13x13的activation map.如果像上圖那樣將activation map pooling成4x4、2x2、1x1三張子圖,做max pooling後,出來的特徵就是固定長度的(16+4+1)x256那麼多的維度了.如果原圖的輸入不是224x224,出來的特徵依然是(16+4+1)x256;直覺地說,可以理解成將原來固定大小爲(3x3)窗口的pool5改成了自適應窗口大小,窗口的大小和activation map成比例,保證了經過pooling後出來的feature的長度是一致的.
IOU
除了對anchor box進行標記外,另一件事情就是計算anchor box與ground truth之間的偏移量
令:ground truth:標定的框也對應一箇中心點位置座標x*,y和寬高w,h*
anchor box: 中心點位置座標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)
通過ground truth box與預測的anchor box之間的差異來進行學習,從而是RPN網絡中的權重能夠學習到預測box的能力
NMS
用下圖一個案例來對NMS算法進行簡單介紹
如上圖所示,一共有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
所以,整個過程,可以用下圖形象的表示出來
其中,紅色的A框是生成的anchor box,而藍色的G’框就是經過RPN網絡訓練後得到的較精確的預測框,綠色的G是ground truth box
Bounding box regression
R-CNN中的bounding box迴歸
下面先介紹R-CNN和Fast R-CNN中所用到的邊框迴歸方法.
(1) 爲什麼要做Bounding-box regression?
如上圖所示,綠色的框爲飛機的Ground Truth,紅色的框是提取的Region Proposal.那麼即便紅色的框被分類器識別爲飛機,但是由於紅色的框定位不準(IoU<0.5),那麼這張圖相當於沒有正確的檢測出飛機.如果我們能對紅色的框進行微調,使得經過微調後的窗口跟Ground Truth更接近,這樣豈不是定位會更準確.確實,Bounding-box regression 就是用來微調這個窗口的.
(2) 迴歸/微調的對象是什麼?
(4) Bounding-box regression(邊框迴歸)
那麼經過何種變換才能從圖11中的窗口P變爲窗口呢?比較簡單的思路就是:
注意:只有當Proposal和Ground Truth比較接近時(線性問題),我們才能將其作爲訓練樣本訓練我們的線性迴歸模型,否則會導致訓練的迴歸模型不work(當Proposal跟GT離得較遠,就是複雜的非線性問題了,此時用線性迴歸建模顯然不合理).這個也是G-CNN: an Iterative Grid Based Object Detector多次迭代實現目標準確定位的關鍵.
線性迴歸就是給定輸入的特徵向量X,學習一組參數W,使得經過線性迴歸後的值跟真實值Y(Ground Truth)非常接近.即.那麼Bounding-box中我們的輸入以及輸出分別是什麼呢?
代碼解讀
代碼結構圖
Faster-RCNN網絡結構圖如下所示,理解該圖對理解整個流程極爲重要:
再結合這幅網絡結構說明圖,可以看的更加清楚:
可以看到,經過了基礎網絡部分之後得到的 feature map,然後被分爲兩支,進而得到 proposal,最後通過ROI層得到固定大小的feature,最終進行分類。
爲了更加具體的瞭解網絡的前饋以及訓練過程,我把該圖的前面抽取特徵的基礎網絡部分略去,把後面部分每個節點的計算以及數據維度做了一個標註,圖片如下:
爲了更好理解代碼結構,可查看代碼的結構圖:
其詳細解釋如下:
數據準備
首先,trainval_net.py
imdb, roidb = combined_roidb(args.imdb_name) # 輸入參數 imdb_name,默認是 voc_2007_trainval(數據集名字)
print '{:d} roidb entries'.format(len(roidb))
combined_roidb:
def get_roidb(imdb_name):
# factory.py 中的函數,調用的是 pascal_voc 的數據集對象
# get_imdb 默認返回的是 pascal_voc('trainval', '2007')
# 設置imdb的一些屬性,如圖片路徑,圖片名稱索引等,未讀取真正的圖片數據
imdb = get_imdb(imdb_name)
print('Loaded dataset `{:s}` for training'.format(imdb.name))
# 設置proposal方法
imdb.set_proposal_method(cfg.TRAIN.PROPOSAL_METHOD)
print('Set proposal method: {:s}'.format(cfg.TRAIN.PROPOSAL_METHOD))
# 得到用於訓練的roidb,定義在train.py,進行了水平翻轉,以及爲原始roidb添加了一些說明性的屬性
roidb = get_training_roidb(imdb)
return roidb
# imdb_names.split('+') 默認值是 voc_2007_trainval
# 需要調用內部函數 get_roidb
#如果需要訓練多個數據集,就在數據集之間用+號連接
roidbs = [get_roidb(s) for s in imdb_names.split('+')]
roidb = roidbs[0]
if len(roidbs) > 1:#跳過
for r in roidbs[1:]:
roidb.extend(r)
tmp = get_imdb(imdb_names.split('+')[1])
imdb = datasets.imdb.imdb(imdb_names, tmp.classes)
else:
# get_imdb方法定義在dataset/factory.py,通過名字得到imdb
imdb = get_imdb(imdb_names)# 即前面提到的 imdb=pascal_voc('trainval', '2007')
return imdb, roidb#roidb應該是屬於imdb的.roidb是沒有真正的讀取數據的,只是建立相關的數據信息
get_roidb
所以我們要先看imdb是如何產生的,然後看如何藉助imdb產生roidb
def get_imdb(name):
"""Get an imdb (image database) by name."""
if not __sets.has_key(name):
raise KeyError('Unknown dataset: {}'.format(name))
return __sets[name]()
從上面可見,get_imdb這個函數的實現原理:_sets是一個字典,字典的key是數據集的名稱,字典的value是一個lambda表達式(即一個函數指針),
__sets[name]()
這句話實際上是調用函數,返回數據集imdb,下面看這個函數:
for year in ['2007', '2012']:
for split in ['train', 'val', 'trainval', 'test']:
name = 'voc_{}_{}'.format(year, split)
__sets[name] = (lambda split=split, year=year: pascal_voc(split, year))
所以可以看到,執行的實際上是pascal_voc函數,參數是split 和 year(ps:默認情況下,name是voc_2007_trainval,所以這裏對應的split和year分別是trainval和2007);
很明顯,pascal_voc是一個類,這是調用了該類的構造函數,返回的也是該類的一個實例,所以這下我們清楚了imdb實際上就是pascal_voc的一個實例;
pascal_voc
那麼我們來看這個類的構造函數是如何的,以及輸入的圖片數據在裏面是如何組織的:
該類的構造函數如下:基本上就是設置了imdb的一些屬性,比如圖片的路徑,圖片名稱的索引,並沒有把真實的圖片數據放進來
class pascal_voc(imdb):
def __init__(self, image_set, year, devkit_path=None):
imdb.__init__(self, 'voc_' + year + '_' + image_set)
self._year = year # 設置年,2007
self._image_set = image_set # trainval
self._devkit_path = self._get_default_path() if devkit_path is None \
else devkit_path # 數據集的路徑'/home/sloan/py-faster-rcnn-master/data/VOCdevkit2007'
self._data_path = os.path.join(self._devkit_path, 'VOC' + self._year) # '/home/sloan/py-faster-rcnn-master/data/VOCdevkit2007/VOC2007'
self._classes = ('__background__', # always index 0
'aeroplane', 'bicycle', 'bird', 'boat',
'bottle', 'bus', 'car', 'cat', 'chair',
'cow', 'diningtable', 'dog', 'horse',
'motorbike', 'person', 'pottedplant',
'sheep', 'sofa', 'train', 'tvmonitor') # 21個類別
self._class_to_ind = dict(zip(self.classes, xrange(self.num_classes))) #給每個類別賦予一個對應的整數
self._image_ext = '.jpg' # 圖片的擴展名
self._image_index = self._load_image_set_index() # 把所有圖片的名稱加載,放在list中,便於索引讀取圖片
# Default to roidb handler
self._roidb_handler = self.selective_search_roidb
self._salt = str(uuid.uuid4())
self._comp_id = 'comp4'
# PASCAL specific config options
self.config = {'cleanup' : True,
'use_salt' : True,
'use_diff' : False,
'matlab_eval' : False,
'rpn_file' : None,
'min_size' : 2}
# 這兩句就是檢查前面的路徑是否存在合法了,否則後面無法運行
assert os.path.exists(self._devkit_path), \
'VOCdevkit path does not exist: {}'.format(self._devkit_path)
assert os.path.exists(self._data_path), \
'Path does not exist: {}'.format(self._data_path)
#
class imdb(object):
"""Image database."""
def __init__(self, name):
self._name = name
self._num_classes = 0
self._classes = []
self._image_index = []
self._obj_proposer = 'selective_search'
self._roidb = None
self._roidb_handler = self.default_roidb
# Use this dict for storing dataset specific config options
self.config = {}
注:如果想訓練自己的數據,把self._classes的內容換成自己的類別, '_background_'要保留。
得到的 imdb = pascal_voc(‘trainval’, ‘2007’) 記錄的內容如下:
[1] - _class_to_ind,dict 類型,key 是類別名,value 是 label 值(從 0 開始),其中 (key[0], value[0]) = [background, 0]
[2] - _classes,object 類別名,共 20(object classes) + 1(background) = 21 classes.
[3] - _data_path,數據集路徑
[4] - _image_ext,’.jpg’ 數據類型
[5] - _image_index,圖片索引列表
[6] - _image_set,’trainval’
[7] - _name,數據集名稱 voc_2007_trainval
[8] - _num_classes,0
[9] - _obj_proposer,selective_search
[10] - _roidb,None
[11] - classes,與_classes 相同
[12] - image_index,與_image_index 相同
[13] - name,數據集名稱,與 _name 相同
[14] - num_classes,類別數,21
[15] - num_images,圖片數
[16] - config,dict 類型,PASCAL 數據集指定的配置
set_proposal_method
那麼有了imdb之後,roidb又有什麼不同呢?爲什麼實際輸入的數據是roidb呢?
前面我們已經得到了imdb,但是imdb的成員roidb還是空白,啥都沒有,那麼roidb是如何生成的,其中又包含了哪些信息呢?
imdb.set_proposal_method(cfg.TRAIN.PROPOSAL_METHOD)
上面調用的函數,爲imdb添加了roidb的數據,我們看看如何添加的,見下面這個函數:
def set_proposal_method(self, method):
method = eval('self.' + method + '_roidb')
self.roidb_handler = method
這裏method傳入的是一個str:gt,所以method=eval(‘self.gt_roidb’)
gt_roidb
第一次加載數據時放到緩存裏,之後就直接到緩存取數據即可。
def gt_roidb(self):
"""
Return the database of ground-truth regions of interest.
This function loads/saves from/to a cache file to speed up future calls.
"""
cache_file = os.path.join(self.cache_path, self.name + '_gt_roidb.pkl')
if os.path.exists(cache_file):
with open(cache_file, 'rb') as fid:
try:
roidb = pickle.load(fid)
except:
roidb = pickle.load(fid, encoding='bytes')
print('{} gt roidb loaded from {}'.format(self.name, cache_file))
return roidb
gt_roidb = [self._load_pascal_annotation(index)
for index in self.image_index]
with open(cache_file, 'wb') as fid:
pickle.dump(gt_roidb, fid, pickle.HIGHEST_PROTOCOL)
print('wrote gt roidb to {}'.format(cache_file))
return gt_roidb
get_training_roidb
有了roidb後,後面的get_training_roidb(imdb)完成什麼功能:將roidb中的元素由5011個,通過水平對稱變成10022個;將index這個list的元素相應的也翻一番;
def get_training_roidb(imdb):
"""Returns a roidb (Region of Interest database) for use in training."""
if cfg.TRAIN.USE_FLIPPED:# 是否進行圖片翻轉
print('Appending horizontally-flipped training examples...')
# 對imdb中涉及到的圖像做了一個水平鏡像,使得trainval中的5011張圖片,變成了10022張圖片;
imdb.append_flipped_images()
print('done')
print('Preparing training data...')
# # 爲原始數據集的roidb添加一些說明性的屬性,max-overlap,max-classes...
rdl_roidb.prepare_roidb(imdb)# 準備數據
print('done')
return imdb.roidb
append_flipped_images
首先我們看看append_flipped_images函數:可以發現,roidb是imdb的一個成員變量,roidb是一個list(每個元素對應一張圖片),list中的元素是一個字典,字典中存放了5個key,分別是boxes信息,每個box的class信息,是否是flipped的標誌位,重疊信息gt_overlaps,以及seg_areas;分析該函數可知,將box的值按照水平對稱,原先roidb中只有5011個元素,經過水平對稱後通過append增加到5011*2=10022個;
def append_flipped_images(self):
num_images = self.num_images
widths = self._get_widths()
for i in range(num_images):
boxes = self.roidb[i]['boxes'].copy()
oldx1 = boxes[:, 0].copy()
oldx2 = boxes[:, 2].copy()
boxes[:, 0] = widths[i] - oldx2 - 1
boxes[:, 2] = widths[i] - oldx1 - 1
assert (boxes[:, 2] >= boxes[:, 0]).all()
entry = {'boxes': boxes,
'gt_overlaps': self.roidb[i]['gt_overlaps'],
'gt_classes': self.roidb[i]['gt_classes'],
'flipped': True}
self.roidb.append(entry)
self._image_index = self._image_index * 2
prepare_roidb
def prepare_roidb(imdb):
"""Enrich the imdb's roidb by adding some derived quantities that
are useful for training. This function precomputes the maximum
overlap, taken over ground-truth boxes, between each ROI and
each ground-truth box. The class with maximum overlap is also
recorded.
"""
roidb = imdb.roidb
if not (imdb.name.startswith('coco')):
sizes = [PIL.Image.open(imdb.image_path_at(i)).size
for i in range(imdb.num_images)]
for i in range(len(imdb.image_index)):
roidb[i]['image'] = imdb.image_path_at(i)#圖片名
if not (imdb.name.startswith('coco')):
roidb[i]['width'] = sizes[i][0]#圖片width
roidb[i]['height'] = sizes[i][1]#圖片height
# need gt_overlaps as a dense array for argmax
gt_overlaps = roidb[i]['gt_overlaps'].toarray()#轉換成one_hot
# max overlap with gt over classes (columns)
max_overlaps = gt_overlaps.max(axis=1)
# gt class that had the max overlap
max_classes = gt_overlaps.argmax(axis=1)
roidb[i]['max_classes'] = max_classes
roidb[i]['max_overlaps'] = max_overlaps
# sanity checks 合理性檢查
# max overlap of 0 => class should be zero (background)
zero_inds = np.where(max_overlaps == 0)[0]
assert all(max_classes[zero_inds] == 0)
# max overlap > 0 => class should not be zero (must be a fg class)
nonzero_inds = np.where(max_overlaps > 0)[0]
assert all(max_classes[nonzero_inds] != 0)
train_net
roidb 應該是屬於 imdb 的.
roidb 是沒有真正的讀取數據的,只是建立相關的數據信息.
def train_net(network, imdb, roidb, valroidb, output_dir, tb_dir,
pretrained_model=None,
max_iters=40000):
"""Train a Faster R-CNN network."""
#這裏對 roidb 先進行處理,即函數 filter_roidb,去除沒用的 RoIs
roidb = filter_roidb(roidb)
valroidb = filter_roidb(valroidb)
tfconfig = tf.ConfigProto(allow_soft_placement=True)
tfconfig.gpu_options.allow_growth = True
with tf.Session(config=tfconfig) as sess:
sw = SolverWrapper(sess, network, imdb, roidb, valroidb, output_dir, tb_dir,
pretrained_model=pretrained_model)
print('Solving...')
sw.train_model(sess, max_iters)
print('done solving')
filter_roidb
這裏對 roidb 先進行處理,即函數 filter_roidb,去除沒用的 RoIs,
def filter_roidb(roidb):
"""Remove roidb entries that have no usable RoIs."""
# 刪掉沒用的RoIs, 有效的圖片必須各有前景和背景ROI
def is_valid(entry):
# Valid images have:
# (1) At least one foreground RoI OR
# (2) At least one background RoI
overlaps = entry['max_overlaps']
# find boxes with sufficient overlap
fg_inds = np.where(overlaps >= cfg.TRAIN.FG_THRESH)[0]
# Select background RoIs as those within [BG_THRESH_LO, BG_THRESH_HI)
bg_inds = np.where((overlaps < cfg.TRAIN.BG_THRESH_HI) &
(overlaps >= cfg.TRAIN.BG_THRESH_LO))[0]
# image is only valid if such boxes exist
valid = len(fg_inds) > 0 or len(bg_inds) > 0
return valid
num = len(roidb)
filtered_roidb = [entry for entry in roidb if is_valid(entry)]
num_after = len(filtered_roidb)
print('Filtered {} roidb entries: {} -> {}'.format(num - num_after,
num, num_after))
return filtered_roidb
RoIDataLayer
目前爲止,上面只是準備了roidb的相關信息而已,真正的數據處理操作是在類RoIDataLayer裏,
def forward(self, bottom, top):函數中開始的,這個類在lib/roi_data_layer/layer.py文件中
blobs = self._get_next_minibatch()這句話產生了我們需要的數據blobs;這個函數又調用了minibatch.py文件中的def get_minibatch(roidb, num_classes):函數;
然後又調用了def _get_image_blob(roidb, scale_inds):函數;在這個函數中,我們終於發現了cv2.imread函數,也就是最終的讀取圖片到內存的地方:
def _get_image_blob(roidb, scale_inds):
"""Builds an input blob from the images in the roidb at the specified
scales.
"""
num_images = len(roidb)
processed_ims = []
im_scales = []
for i in range(num_images):
im = cv2.imread(roidb[i]['image'])
if roidb[i]['flipped']:
im = im[:, ::-1, :]
target_size = cfg.TRAIN.SCALES[scale_inds[i]]
im, im_scale = prep_im_for_blob(im, cfg.PIXEL_MEANS, target_size,
cfg.TRAIN.MAX_SIZE)
im_scales.append(im_scale)
processed_ims.append(im)
# Create a blob to hold the input images
blob = im_list_to_blob(processed_ims)
return blob, im_scales
終於,數據準備完事…
訓練階段
SolverWrapper通過construct_graph創建網絡、train_op等。
construct_graph通過Network的create_architecture創建網絡。
注:在代碼中,anchor,proposal,rois,boxes代表的含義其實都是一樣的,都是推薦的區域或者框,不過有所區別的地方在於
這幾個名詞有一個遞進的關係,最開始的使錨定的框anchor,數量最多約爲2萬個(根據resize後的圖片大小不同數量有所變化)
然後是rpn網絡推薦的框proposal,數量較多,train時候有2000個,
再然後是實際分類時候用到的rois框,每張圖片有256個,
最後得到的結果就是boxes
create_architecture
create_architecture通過_build_network具體創建網絡模型、損失及其他相關操作,得到rois, cls_prob, bbox_pred,定義如下
def create_architecture(self, mode, num_classes, tag=None,
anchor_scales=(8, 16, 32), anchor_ratios=(0.5, 1, 2)):
self._image = tf.placeholder(tf.float32, shape=[1, None, None, 3])
#圖像信息,高、寬、縮放比例im_scales(壓縮到最小邊長爲600,但是如果壓縮之後最大邊長超過2000,則以最大邊長2000爲限制條件)
#原圖大小(720*1280),resize後圖像大小爲(600*1067). im_scales=600/720=0.8333
self._im_info = tf.placeholder(tf.float32, shape=[3])#
self._gt_boxes = tf.placeholder(tf.float32, shape=[None, 5])
self._tag = tag
self._num_classes = num_classes
self._mode = mode
self._anchor_scales = anchor_scales
self._num_scales = len(anchor_scales)
self._anchor_ratios = anchor_ratios
self._num_ratios = len(anchor_ratios)
self._num_anchors = self._num_scales * self._num_ratios
training = mode == 'TRAIN'
testing = mode == 'TEST'
assert tag != None
# handle most of the regularizers here
weights_regularizer = tf.contrib.layers.l2_regularizer(cfg.TRAIN.WEIGHT_DECAY)
if cfg.TRAIN.BIAS_DECAY:
biases_regularizer = weights_regularizer
else:
biases_regularizer = tf.no_regularizer
# list as many types of layers as possible, even if they are not used now
with arg_scope([slim.conv2d, slim.conv2d_in_plane, \
slim.conv2d_transpose, slim.separable_conv2d, slim.fully_connected],
weights_regularizer=weights_regularizer,
biases_regularizer=biases_regularizer,
biases_initializer=tf.constant_initializer(0.0)):
#rois:256個anchors的類別(訓練時爲每個anchors的類別,測試時全0)
#cls_prob:256個anchors每一類別的概率
#bbox_pred:預測位置信息的偏移
rois, cls_prob, bbox_pred = self._build_network(training)#用於創建網絡
layers_to_output = {'rois': rois}
for var in tf.trainable_variables():
self._train_summaries.append(var)
if testing:
stds = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS), (self._num_classes))
means = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS), (self._num_classes))
self._predictions["bbox_pred"] *= stds#訓練時_region_proposal中預測的位置偏移減均值除標準差,因而測試時需要反過來
self._predictions["bbox_pred"] += means
else:
self._add_losses()
layers_to_output.update(self._losses)
val_summaries = []
with tf.device("/cpu:0"):
val_summaries.append(self._add_gt_image_summary())
for key, var in self._event_summaries.items():
val_summaries.append(tf.summary.scalar(key, var))
for key, var in self._score_summaries.items():
self._add_score_summary(key, var)
for var in self._act_summaries:
self._add_act_summary(var)
for var in self._train_summaries:
self._add_train_summary(var)
self._summary_op = tf.summary.merge_all()
self._summary_op_val = tf.summary.merge(val_summaries)
layers_to_output.update(self._predictions)
return layers_to_output
_build_network
_build_netword用於創建網絡
總體流程:網絡通過vgg1-5得到特徵net_conv後,送入rpn網絡得到候選區域anchors,去除超出圖像邊界的anchors並選出2000個anchors用於訓練rpn網絡(300個用於測試)。並進一步選擇256個anchors(用於rcnn分類)。之後將這256個anchors的特徵根據rois進行裁剪縮放及pooling,得到相同大小7*7的特徵pool5,pool5通過兩個fc層得到4096維特徵fc7,fc7送入_region_classification(2個並列的fc層),得到21維的cls_score和21*4維的bbox_pred。
def _build_network(self, is_training=True):
# select initializers
if cfg.TRAIN.TRUNCATED:
initializer = tf.truncated_normal_initializer(mean=0.0, stddev=0.01)
initializer_bbox = tf.truncated_normal_initializer(mean=0.0, stddev=0.001)
else:
initializer = tf.random_normal_initializer(mean=0.0, stddev=0.01)
initializer_bbox = tf.random_normal_initializer(mean=0.0, stddev=0.001)
net_conv = self._image_to_head(is_training)#得到輸入圖像的特徵
with tf.variable_scope(self._scope, self._scope):
# build the anchors for the image
#生成anchors,得到所有可能的anchors在原始圖像中的座標(可能超出圖像邊界)及anchors的數量
self._anchor_component()
# region proposal network
#RPN網絡,得到256個anchors的類別(訓練時爲每個anchors的類別,測試時全0)及位置(後四維)
rois = self._region_proposal(net_conv, is_training, initializer)
# region of interest pooling
#ROI pooling
if cfg.POOLING_MODE == 'crop':
pool5 = self._crop_pool_layer(net_conv, rois, "pool5")#對特徵圖通過rois得到候選區域,並對候選區域進行縮放,得到14*14的固定大小,進一步pooling成7*7大小
else:
raise NotImplementedError
fc7 = self._head_to_tail(pool5, is_training)#對固定大小的rois增加fc及dropout,得到4096維的特徵,用於分類及迴歸
with tf.variable_scope(self._scope, self._scope):
# region classification
#分類/迴歸網絡,對rois進行分類,完成目標檢測;進行迴歸,得到預測座標
cls_prob, bbox_pred = self._region_classification(fc7, is_training,
initializer, initializer_bbox)
self._score_summaries.update(self._predictions)
#rois:256*5
#cls_prob:256*21(類別數)
#bbox_pred:256*84(類別數*4)
return rois, cls_prob, bbox_pred
_image_to_head
_image_to_head用於得到輸入圖像的特徵
該函數位於vgg16.py中,定義如下
def _image_to_head(self, is_training, reuse=None):
with tf.variable_scope(self._scope, self._scope, reuse=reuse):
net = slim.repeat(self._image, 2, slim.conv2d, 64, [3, 3],
trainable=False, scope='conv1')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool1')
net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3],
trainable=False, scope='conv2')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool2')
net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3],
trainable=is_training, scope='conv3')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool3')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3],
trainable=is_training, scope='conv4')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool4')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3],
trainable=is_training, scope='conv5')
self._act_summaries.append(net)
self._layers['head'] = net
return net
_anchor_component
_anchor_component:用於得到所有可能的anchors在原始圖像中的座標(可能超出圖像邊界)及anchors的數量(特徵圖寬*特徵圖高*9)。該函數使用的self._im_info,爲一個3維向量,[0]代表圖像寬,[1]代表圖像高,[2]代表圖像縮放的比例(將圖像寬縮放到600,或高縮放到1000的最小比例,比如縮放到600*900、850*1000)。該函數調用generate_anchors_pre_tf並進一步調用generate_anchors來得到所有可能的anchors在原始圖像中的座標及anchors的個數(由於圖像大小不一樣,因而最終anchor的個數也不一樣)。
generate_anchors_pre_tf步驟如下:
- 通過_ratio_enum得到anchor時,使用 (0, 0, 15, 15) 的基準窗口,先通過ratio=[0.5,1,2]的比例得到anchors。ratio指的是像素總數(寬*高)的比例,而不是寬或者高的比例,得到如下三個archor(每個archor爲左上角和右下角的座標):
- 而後在通過scales=(8, 16, 32)得到放大倍數後的anchors。scales時,將上面的每個都直接放大對應的倍數,最終得到9個anchors(每個anchor爲左上角和右下角的座標)。將上面三個anchors分別放大就行了,因而本文未給出該圖。
之後通過tf.add(anchor_constant, shifts)得到縮放後的每個點的9個anchor在原始圖的矩形框。anchor_constant:1*9*4。shifts:N*1*4。N爲縮放後特徵圖的像素數。將維度從N*9*4變換到(N*9)*4,得到縮放後的圖像每個點在原始圖像中的anchors。
def _anchor_component(self):
with tf.variable_scope('ANCHOR_' + self._tag) as scope:
# just to get the shape right
#tf.ceil 向上取整
height = tf.to_int32(tf.ceil(self._im_info[0] / np.float32(self._feat_stride[0])))#圖像經過VGG16得到特徵圖的高寬
width = tf.to_int32(tf.ceil(self._im_info[1] / np.float32(self._feat_stride[0])))
if cfg.USE_E2E_TF:
#通過特徵圖寬高,_feat_stride(特徵圖對原始圖縮小的比例)等得到圖像上的所有可能的anchors(座標可能超出原始圖像邊界)和anchor數量
anchors, anchor_length = generate_anchors_pre_tf(
height,
width,
self._feat_stride,
self._anchor_scales,
self._anchor_ratios#指的是像素總數(寬*高)的比例
)
else:
anchors, anchor_length = tf.py_func(generate_anchors_pre,#得到所有可能的anchors在原始圖像中的座標(可能超出圖像邊界)及anchors數量
[height, width,
self._feat_stride, self._anchor_scales, self._anchor_ratios],
[tf.float32, tf.int32], name="generate_anchors")
anchors.set_shape([None, 4])
anchor_length.set_shape([])
self._anchors = anchors
self._anchor_length = anchor_length
generate_anchors_pre
def generate_anchors_pre_tf(height, width, feat_stride=16, anchor_scales=(8, 16, 32), anchor_ratios=(0.5, 1, 2)):
shift_x = tf.range(width) * feat_stride # 得到所有anchors在原始圖像的起始x座標:(0,feat_stride,2*feat_stride,...)
shift_y = tf.range(height) * feat_stride # 得到所有anchors在原始圖像的起始y座標:(0,feat_stride,2*feat_stride,...)
shift_x, shift_y = tf.meshgrid(shift_x, shift_y)
sx = tf.reshape(shift_x, shape=(-1,))
sy = tf.reshape(shift_y, shape=(-1,))
shifts = tf.transpose(tf.stack([sx, sy, sx, sy]))#width*height個四位矩陣
K = tf.multiply(width, height)#特徵圖總共像素數
shifts = tf.transpose(tf.reshape(shifts, shape=[1, K, 4]), perm=(1, 0, 2))#增加一維,變成1*(width*height)*4矩陣,而後變換緯度爲(width*height)*1*4
#得到9個anchors在原始圖像中的四個座標(放大比例默認爲16)
anchors = generate_anchors(ratios=np.array(anchor_ratios), scales=np.array(anchor_scales))
A = anchors.shape[0]#9
anchor_constant = tf.constant(anchors.reshape((1, A, 4)), dtype=tf.int32)#增加維度爲1*9*4
length = K * A#總共的anchors的個數(每個點對應A=9個anchor,共K=height*width個點)
#1*9*4的base anchor和(width*height)*1*4的偏移矩陣進行broadcast相加,得到(width*height)*9*4,
#並改變形狀爲(width*height*9)*4,得到所有的anchors的四個座標
anchors_tf = tf.reshape(tf.add(anchor_constant, shifts), shape=(length, 4))
return tf.cast(anchors_tf, dtype=tf.float32), length
generate_anchors
def generate_anchors(base_size=16, ratios=[0.5, 1, 2],
scales=2 ** np.arange(3, 6)):
"""
Generate anchor (reference) windows by enumerating aspect ratios X
scales wrt a reference (0, 0, 15, 15) window.
"""
base_anchor = np.array([1, 1, base_size, base_size]) - 1#base_anchor的四個座標
ratio_anchors = _ratio_enum(base_anchor, ratios)#通過ratio得到3個anchors的座標(3*4)
anchors = np.vstack([_scale_enum(ratio_anchors[i, :], scales)#3*4矩陣變成9*4矩陣,得到9個anchors的座標
for i in range(ratio_anchors.shape[0])])
return anchors
def _whctrs(anchor):
"""
Return width, height, x center, and y center for an anchor (window).
"""
w = anchor[2] - anchor[0] + 1#寬
h = anchor[3] - anchor[1] + 1#高
x_ctr = anchor[0] + 0.5 * (w - 1)#中心x
y_ctr = anchor[1] + 0.5 * (h - 1)#中心y
return w, h, x_ctr, y_ctr
def _mkanchors(ws, hs, x_ctr, y_ctr):
"""
Given a vector of widths (ws) and heights (hs) around a center
(x_ctr, y_ctr), output a set of anchors (windows).
"""
ws = ws[:, np.newaxis]#3維向量變成3*1矩陣
hs = hs[:, np.newaxis]
anchors = np.hstack((x_ctr - 0.5 * (ws - 1),#3*4矩陣
y_ctr - 0.5 * (hs - 1),
x_ctr + 0.5 * (ws - 1),
y_ctr + 0.5 * (hs - 1)))
return anchors
def _ratio_enum(anchor, ratios):#縮放比例爲像素總數的比例,而非單獨寬或者高的比例
"""
Enumerate a set of anchors for each aspect ratio wrt an anchor.
"""
w, h, x_ctr, y_ctr = _whctrs(anchor)#得到中心位置和寬高 [16,16,7.5,7.5]
size = w * h#總共像素數 256
size_ratios = size / ratios#縮放比例 [512,256,128]
ws = np.round(np.sqrt(size_ratios))#縮放後的寬,3維向量,值由大到小[23,16,11]
hs = np.round(ws * ratios)#縮放後的高,值由小到大 [12,16,22]
anchors = _mkanchors(ws, hs, x_ctr, y_ctr)#根據中心及寬高得到3個anchors的四個座標
return anchors
def _scale_enum(anchor, scales):
"""
Enumerate a set of anchors for each scale wrt an anchor.
"""
w, h, x_ctr, y_ctr = _whctrs(anchor)
ws = w * scales
hs = h * scales
anchors = _mkanchors(ws, hs, x_ctr, y_ctr)
return anchors
_region_proposal
_region_proposal用於將vgg16的conv5的特徵通過3*3的滑動窗得到rpn特徵,進行兩條並行的線路,分別送入cls和reg網絡。
cls網絡判斷通過1*1的卷積得到anchors是正樣本還是負樣本(由於anchors過多,還有可能有不關心的anchors,使用時只使用正樣本和負樣本),用於二分類rpn_cls_score;
reg網絡對通過1*1的卷積迴歸出anchors的座標偏移rpn_bbox_pred。這兩個網絡共用3*3 conv(rpn)。由於每個位置有k個anchor,因而每個位置均有2k個soores和4k個coordinates。
cls(將輸入的512維降低到2k維):3*3 conv + 1*1 conv(2k個scores,k爲每個位置archors個數,如9)
在第一次使用_reshape_layer時,由於輸入bottom爲1*?*?*2k,先得到caffe中的數據順序(tf爲batchsize*height*width*channels,caffe中爲batchsize*channels*height*width)to_caffe:1*2k*?*?,而後reshape後得到reshaped爲1*2*?*?,最後在轉回tf的順序to_tf爲1*?*?*2,得到rpn_cls_score_reshape。之後通過rpn_cls_prob_reshape(softmax的值,只針對最後一維,即2計算softmax),得到概率rpn_cls_prob_reshape(其最大值,即爲預測值rpn_cls_pred),再次_reshape_layer,得到1*?*?*2k的rpn_cls_prob,爲原始的概率。
reg(將輸入的512維降低到4k維):3*3 conv + 1*1 conv(4k個coordinates,k爲每個位置archors個數,如9)。
_region_proposal定義如下:
def _region_proposal(self, net_conv, is_training, initializer):
#vgg16提取後的特徵圖,先進行3*3卷積
#3*3的conv,作爲rpn網絡
rpn = slim.conv2d(net_conv, cfg.RPN_CHANNELS, [3, 3], trainable=is_training, weights_initializer=initializer,
scope="rpn_conv/3x3")
self._act_summaries.append(rpn)
#每個框進行2分類,判斷前景還是背景
#1*1的conv,得到每個位置的9個anchors分類特徵[1,?,?,9*2],
rpn_cls_score = slim.conv2d(rpn, self._num_anchors * 2, [1, 1], trainable=is_training,
weights_initializer=initializer,
padding='VALID', activation_fn=None, scope='rpn_cls_score')
# change it so that the score has 2 as its channel size
#reshape成標準形式
#[1,?,?,9*2]-->[1,?*9.?,2]
rpn_cls_score_reshape = self._reshape_layer(rpn_cls_score, 2, 'rpn_cls_score_reshape')
#以最後一維爲特徵長度,得到所有特徵的概率[1,?*9.?,2]
rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape, "rpn_cls_prob_reshape")
#得到每個位置的9個anchors預測的類別,[1,?,9,?]的列向量
rpn_cls_pred = tf.argmax(tf.reshape(rpn_cls_score_reshape, [-1, 2]), axis=1, name="rpn_cls_pred")
#變換回原始緯度,[1,?*9.?,2]-->[1,?,?,9*2]
rpn_cls_prob = self._reshape_layer(rpn_cls_prob_reshape, self._num_anchors * 2, "rpn_cls_prob")
#1*1的conv,每個位置的9個anchors迴歸位置偏移[1,?,?,9*4]
rpn_bbox_pred = slim.conv2d(rpn, self._num_anchors * 4, [1, 1], trainable=is_training,
weights_initializer=initializer,
padding='VALID', activation_fn=None, scope='rpn_bbox_pred')
if is_training:
#1.使用經過rpn網絡層後生成的rpn_cls_prob把anchor位置進行第一次修正
#2.按照得分排序,取前12000個anchor,再nms,取前面2000個(在test的時候就變成了6000和300)
rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
#獲取屬於rpn網絡的label:通過對所有的anchor與所有的GT計算IOU,通過消除再圖像外部的anchor,計算IOU>=0.7爲正樣本,IOU<0.3爲負樣本,
#得到再理想情況下各自一半的256個正負樣本(實際上正樣本大多隻有10-100個之間,相對負樣本偏少)
rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")#rpn_labels:特徵圖中每個位置對應的正樣本、負樣本還是不關注
# Try to have a deterministic order for the computing graph, for reproducibility
with tf.control_dependencies([rpn_labels]):
#獲得屬於最後的分類網絡的label
#因爲之前的anchor位置已經修正過了,所以這裏又計算了一次經過proposal_layer修正後的box與GT的IOU來得到label
#但是閾值不一樣了,變成了大於等於0.5爲1,小於爲0,並且這裏得到的正樣本很少,通常只有2-20個,甚至有0個,
#並且正樣本最多爲64個,負樣本則有比較多個,相應的也重新計算了一次bbox_targets
#另外,從RPN網絡出來的2000餘個rois中挑選256個
rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")#通過post_nms_topN個anchors的位置及爲1(正樣本)的概率得到256個rois及對應信息
else:
if cfg.TEST.MODE == 'nms':
rois, _ = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
elif cfg.TEST.MODE == 'top':
rois, _ = self._proposal_top_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
else:
raise NotImplementedError
self._predictions["rpn_cls_score"] = rpn_cls_score#每個位置的9個anchors是正樣本還是負樣本
self._predictions["rpn_cls_score_reshape"] = rpn_cls_score_reshape#每個anchors是正樣本還是負樣本
self._predictions["rpn_cls_prob"] = rpn_cls_prob#每個位置的9個anchors是正樣本和負樣本的概率
self._predictions["rpn_cls_pred"] = rpn_cls_pred#每個位置的9個anchors預測的類別,[1,?,9,?]的列向量
self._predictions["rpn_bbox_pred"] = rpn_bbox_pred#每個位置的9個anchors迴歸位置偏移
self._predictions["rois"] = rois#256個anchors的類別(第一維)及位置(後四維)
return rois#返回256個anchors的類別(第一維,訓練時爲每個anchors的類別,測試時全0)及位置(後四維)
_proposal_layer
_proposal_layer調用proposal_layer_tf,通過(N*9)*4個anchors,計算估計後的座標(bbox_transform_inv_tf),並對座標進行裁剪(clip_boxes_tf)及非極大值抑制(tf.image.non_max_suppression,可得到符合條件的索引indices)的anchors:rois及這些anchors爲正樣本的概率:rpn_scores。rois爲m*5維,rpn_scores爲m*4維,其中m爲經過非極大值抑制後得到的候選區域個數(訓練時2000個,測試時300個)。m*5的第一列爲全爲0的batch_inds,後4列爲座標(坐上+右下)
_proposal_layer如下
def _proposal_layer(self, rpn_cls_prob, rpn_bbox_pred, name):
with tf.variable_scope(name) as scope:
if cfg.USE_E2E_TF:
rois, rpn_scores = proposal_layer_tf(
rpn_cls_prob,
rpn_bbox_pred,
self._im_info,
self._mode,
self._feat_stride,
self._anchors,
self._num_anchors
)
else:
rois, rpn_scores = tf.py_func(proposal_layer,
[rpn_cls_prob, rpn_bbox_pred, self._im_info, self._mode,
self._feat_stride, self._anchors, self._num_anchors],
[tf.float32, tf.float32], name="proposal")
rois.set_shape([None, 5])
rpn_scores.set_shape([None, 1])
return rois, rpn_scores
proposal_layer_tf
def proposal_layer_tf(rpn_cls_prob, rpn_bbox_pred, im_info, cfg_key, _feat_stride, anchors, num_anchors):
if type(cfg_key) == bytes:
cfg_key = cfg_key.decode('utf-8')
pre_nms_topN = cfg[cfg_key].RPN_PRE_NMS_TOP_N
post_nms_topN = cfg[cfg_key].RPN_POST_NMS_TOP_N#訓練時爲2000,測試時爲300
nms_thresh = cfg[cfg_key].RPN_NMS_THRESH#nms的閾值,爲0.7
# Get the scores and bounding boxes
scores = rpn_cls_prob[:, :, :, num_anchors:]#[1,?,?,(9*2)]取後9個,應該是前9個代表9個anchors爲背景的概率,後9個代表9個anchors爲前景的概率
scores = tf.reshape(scores, shape=(-1,))
rpn_bbox_pred = tf.reshape(rpn_bbox_pred, shape=(-1, 4))#所有的anchors的四個座標
proposals = bbox_transform_inv_tf(anchors, rpn_bbox_pred)#已知anchors和偏移求預測的座標
proposals = clip_boxes_tf(proposals, im_info[:2])#限制預測座標在原始圖像上
# Non-maximal suppression
#通過nms得到分支最大的post_num_topN個座標的索引
indices = tf.image.non_max_suppression(proposals, scores, max_output_size=post_nms_topN, iou_threshold=nms_thresh)
boxes = tf.gather(proposals, indices)#得到post_nms_topN個對應的座標
boxes = tf.to_float(boxes)
scores = tf.gather(scores, indices)#得到post_nms_topN個對應的爲1的概率
scores = tf.reshape(scores, shape=(-1, 1))
# Only support single image as input
batch_inds = tf.zeros((tf.shape(indices)[0], 1), dtype=tf.float32)
blob = tf.concat([batch_inds, boxes], 1)#post_nms_topN*1個batch_inds和post_nms_topN*4個座標concat,得到post_nms_topN*5的blob
return blob, scores
bbox_transform_inv_tf
已知anchors和偏移求預測的座標
def bbox_transform_inv_tf(boxes, deltas):
boxes = tf.cast(boxes, deltas.dtype)
widths = tf.subtract(boxes[:, 2], boxes[:, 0]) + 1.0#寬
heights = tf.subtract(boxes[:, 3], boxes[:, 1]) + 1.0#高
ctr_x = tf.add(boxes[:, 0], widths * 0.5)#中心x
ctr_y = tf.add(boxes[:, 1], heights * 0.5)#中心y
dx = deltas[:, 0] #預測的tx
dy = deltas[:, 1] #預測的ty
dw = deltas[:, 2] #預測的tw
dh = deltas[:, 3]#預測的th
pred_ctr_x = tf.add(tf.multiply(dx, widths), ctr_x)#公式2已知xa,wa,tx反過來求預測的x中心座標
pred_ctr_y = tf.add(tf.multiply(dy, heights), ctr_y)#公式2已知ya,ha,ty反過來求預測的y中心座標
pred_w = tf.multiply(tf.exp(dw), widths)#公式2已知wa,tw反過來秋預測的w
pred_h = tf.multiply(tf.exp(dh), heights)#公式2已知ha,th反過來秋預測的h
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)
_anchor_target_layer
通過**_anchor_target_layer**首先去除anchors中邊界超出圖像的anchors。而後通過bbox_overlaps計算anchors(N*4)和gt_boxes(M*4)的重疊區域的值overlaps(N*M),並得到每個anchor對應的最大的重疊ground_truth的值max_overlaps(1*N),以及ground_truth的背景對應的最大重疊anchors的值gt_max_overlaps(1*M)和每個背景對應的anchor的位置gt_argmax_overlaps。之後通過_compute_targets計算anchors和最大重疊位置的gt_boxes的變換後的座標bbox_targets(見公式2後四個)。最後通過_unmap在變換回和原始的anchors一樣大小的rpn_labels(anchors是正樣本、負樣本還是不關注),rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights。
_anchor_target_layer定義:
def _anchor_target_layer(self, rpn_cls_score, name):
#rpn_cls_score:每個位置的9個anchors分類特徵[1,?,?,9*2]
with tf.variable_scope(name) as scope:
#rpn_labes:特徵圖中每個位置對應的時正樣本,負樣本還是不關注(去除了邊界在圖像外面的anchors)
#rpn_bbox_targets:特徵圖中每個位置和對應的正樣本的座標偏移(很多爲0)
#rpn_bbox_inside_weights:正樣本的權重爲1(去除負樣本和不關注的樣本,均爲0)
#rpn_bbox_outside_weights:正樣本和負樣本(不包括不關注的樣本)歸一化的權重
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")
rpn_labels.set_shape([1, 1, None, None])
rpn_bbox_targets.set_shape([1, None, None, self._num_anchors * 4])
rpn_bbox_inside_weights.set_shape([1, None, None, self._num_anchors * 4])
rpn_bbox_outside_weights.set_shape([1, None, None, self._num_anchors * 4])
rpn_labels = tf.to_int32(rpn_labels, name="to_int32")
self._anchor_targets['rpn_labels'] = rpn_labels
self._anchor_targets['rpn_bbox_targets'] = rpn_bbox_targets
self._anchor_targets['rpn_bbox_inside_weights'] = rpn_bbox_inside_weights
self._anchor_targets['rpn_bbox_outside_weights'] = rpn_bbox_outside_weights
self._score_summaries.update(self._anchor_targets)
return rpn_labels
anchor_target_layer
#rpn_cls_score:[1,?,?,9*2]
#gt_boxes:[?,5]
#im_info:[3]
#_feat_stride:16
#all_anchors:[?,4]
#num_anchors:9
def anchor_target_layer(rpn_cls_score, gt_boxes, im_info, _feat_stride, all_anchors, num_anchors):
"""Same as the anchor target layer in original Fast/er RCNN """
A = num_anchors#9
total_anchors = all_anchors.shape[0]#所有anchors的個數,9*特徵圖寬*特徵圖高 個
K = total_anchors / num_anchors
# allow boxes to sit over the edge by a small amount
_allowed_border = 0
# map of shape (..., H, W)
height, width = rpn_cls_score.shape[1:3]#rpn網絡得到的特徵的高寬
# only keep anchors inside the image
inds_inside = np.where(#所有anchors邊界可能超出圖像,取在圖像內部的anchors索引
(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, :]#得到在圖像內部anchors的座標
# label: 1 is positive, 0 is negative, -1 is dont care
labels = np.empty((len(inds_inside),), dtype=np.float32)#label:1 正樣本,0:負樣本;-1:不關注
labels.fill(-1)
# overlaps between the anchors and the gt boxes
# overlaps (ex, gt)
#計算每個anchors:n*4和每個真實位置gt_boxes:m*4的重疊區域的比的矩陣:n*m
overlaps = bbox_overlaps(
np.ascontiguousarray(anchors, dtype=np.float),
np.ascontiguousarray(gt_boxes, dtype=np.float))
#找到每行最大值的位置,即每個anchors對應的正樣本的位置,得到n維的行向量
argmax_overlaps = overlaps.argmax(axis=1)
#取出每個anchors對應的正樣本的重疊區域,n維向量(IOU值)
max_overlaps = overlaps[np.arange(len(inds_inside)), argmax_overlaps]
#找到每列最大值的位置,即每個真實位置對應的anchors的位置,得到m維的行向量
gt_argmax_overlaps = overlaps.argmax(axis=0)
#取出每個真實位置對應的anchors的重疊區域,m維向量
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
#將anchors對應的正樣本的重疊區域中小於閾值的置0
labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0
# fg label: for each gt, anchor with highest overlap
labels[gt_argmax_overlaps] = 1#每個真實位置對應的anchors置1
# fg label: above threshold IOU
#將anchors對應的正樣本的重疊區域中大於閾值的置1
labels[max_overlaps >= cfg.TRAIN.RPN_POSITIVE_OVERLAP] = 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
# 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#將多餘的負樣本設置爲不關注
bbox_targets = np.zeros((len(inds_inside), 4), dtype=np.float32)
#通過anchors和anchors對應的正樣本計算座標的偏移
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)
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))
#對前景和背景控制權重,positive_weights,negative_weights有互補的意味
#在_smooth_l1_loss裏面使用
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
#映射到原始的anchors(包含未知參數超出圖像邊界的anchors)對應的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)
bbox_inside_weights = _unmap(bbox_inside_weights, total_anchors, inds_inside, fill=0)
#所有anchors中正樣本的四個座標的權重軍設置爲1,其他爲0
bbox_outside_weights = _unmap(bbox_outside_weights, total_anchors, inds_inside, fill=0)
# labels
#(1*?*?)*9-->1*?*?*9-->1*9*?*?
labels = labels.reshape((1, height, width, A)).transpose(0, 3, 1, 2)
#1*9*?*?-->1*1*(9*?)*?
labels = labels.reshape((1, 1, A * height, width))
rpn_labels = labels#特徵圖中每個位置對應的正樣本、負樣本還是不關注(去除了邊界在圖像外面的anchors)
# bbox_targets
#1*(9*?)*?*4-->1*?*?*(9*4)
bbox_targets = bbox_targets \
.reshape((1, height, width, A * 4))
rpn_bbox_targets = bbox_targets#歸一化的權重
# bbox_inside_weights
bbox_inside_weights = bbox_inside_weights \
.reshape((1, height, width, A * 4))
rpn_bbox_inside_weights = bbox_inside_weights
# bbox_outside_weights
bbox_outside_weights = bbox_outside_weights \
.reshape((1, height, width, A * 4))
rpn_bbox_outside_weights = bbox_outside_weights
return rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights
_compute_targets
通過_compute_targets計算anchors和最大重疊位置的gt_boxes的變換後的座標bbox_targets(見公式2後四個)。
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後四個,結合anchors和對應的正樣本的座標計算座標的偏移
return bbox_transform(ex_rois, gt_rois[:, :4]).astype(np.float32, copy=False)#由於gt_rois是5列,去掉第一列的batch_inds
bbox_transform
def bbox_transform(ex_rois, gt_rois):
ex_widths = ex_rois[:, 2] - ex_rois[:, 0] + 1.0#anchor的寬
ex_heights = ex_rois[:, 3] - ex_rois[:, 1] + 1.0#anchor的高
ex_ctr_x = ex_rois[:, 0] + 0.5 * ex_widths#anchor的中心x
ex_ctr_y = ex_rois[:, 1] + 0.5 * ex_heights#anchor的中心y
gt_widths = gt_rois[:, 2] - gt_rois[:, 0] + 1.0#真實正樣本w
gt_heights = gt_rois[:, 3] - gt_rois[:, 1] + 1.0#真實正樣本h
gt_ctr_x = gt_rois[:, 0] + 0.5 * gt_widths#真實正樣本中心x
gt_ctr_y = gt_rois[:, 1] + 0.5 * gt_heights#真實正樣本中心y
targets_dx = (gt_ctr_x - ex_ctr_x) / ex_widths#通過公式2後四個的x*,xa,wa得到dx
targets_dy = (gt_ctr_y - ex_ctr_y) / ex_heights#通過公式2後四個的y*,ya,ha得到dy
targets_dw = np.log(gt_widths / ex_widths)
targets_dh = np.log(gt_heights / ex_heights)
targets = np.vstack(
(targets_dx, targets_dy, targets_dw, targets_dh)).transpose()
return targets
_unmap
最後通過_unmap在變換回和原始的anchors一樣大小的rpn_labels(anchors是正樣本、負樣本還是不關注),rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights。
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
bbox_overlaps
bbox_overlaps用於計算achors和ground truth box重疊區域的面積。
def bbox_overlaps(
np.ndarray[DTYPE_t, ndim=2] boxes,
np.ndarray[DTYPE_t, ndim=2] query_boxes):
"""
Parameters
----------
boxes: (N, 4) ndarray of float
query_boxes: (K, 4) ndarray of float
Returns
-------
overlaps: (N, K) ndarray of overlap between boxes and query_boxes
"""
cdef unsigned int N = boxes.shape[0]
cdef unsigned int K = query_boxes.shape[0]
cdef np.ndarray[DTYPE_t, ndim=2] overlaps = np.zeros((N, K), dtype=DTYPE)
cdef DTYPE_t iw, ih, box_area
cdef DTYPE_t ua
cdef unsigned int k, n
for k in range(K):
box_area = (
(query_boxes[k, 2] - query_boxes[k, 0] + 1) *
(query_boxes[k, 3] - query_boxes[k, 1] + 1)
)
for n in range(N):
iw = (
min(boxes[n, 2], query_boxes[k, 2]) -
max(boxes[n, 0], query_boxes[k, 0]) + 1
)
if iw > 0:
ih = (
min(boxes[n, 3], query_boxes[k, 3]) -
max(boxes[n, 1], query_boxes[k, 1]) + 1
)
if ih > 0:
ua = float(
(boxes[n, 2] - boxes[n, 0] + 1) *
(boxes[n, 3] - boxes[n, 1] + 1) +
box_area - iw * ih
)
overlaps[n, k] = iw * ih / ua
return overlaps
_proposal_target_layer
_proposal_target_layer調用proposal_target_layer,並進一步調用_sample_rois從之前_proposal_layer中選出的2000個anchors篩選出256個archors。_sample_rois將正樣本數量固定爲最大64(小於時補負樣本),並根據公式2對座標歸一化,通過_get_bbox_regression_labels得到bbox_targets。用於rcnn的分類及迴歸。該層只在訓練時使用;測試時,直接選擇了300個anchors,不需要該層了。
_proposal_target_layer定義如下
def _proposal_target_layer(self, rois, roi_scores, name):
#post_nms_topN個anchor的位置及爲1(正樣本)的概率
#只在訓練時使用該層,從post_nms_topN個anchors中選擇256個anchors
with tf.variable_scope(name) as scope:
#labels:正樣本和負樣本對應的真實的類別
#rois:從post_num_topN個anchors中選擇256個anchors(第一列的全0更新爲每個anchors對應的類別)
#roi_scores:256個anchors對應的正樣本的概率
#bbox_targets:256*(4*21)的矩陣,只有爲正樣本時,對應類別的座標纔不爲0,其他類別的座標全爲0
#bbox_inside_weights:256*(4*21)的矩陣,正樣本時,對應類別四個座標的權重爲1,其他全爲0
#bbox_outside_weights:256*(4*21)的矩陣,
rois, roi_scores, labels, bbox_targets, bbox_inside_weights, bbox_outside_weights = tf.py_func(
proposal_target_layer,
[rois, roi_scores, self._gt_boxes, self._num_classes],
[tf.float32, tf.float32, tf.float32, tf.float32, tf.float32, tf.float32],
name="proposal_target")
rois.set_shape([cfg.TRAIN.BATCH_SIZE, 5])
roi_scores.set_shape([cfg.TRAIN.BATCH_SIZE])
labels.set_shape([cfg.TRAIN.BATCH_SIZE, 1])
bbox_targets.set_shape([cfg.TRAIN.BATCH_SIZE, self._num_classes * 4])
bbox_inside_weights.set_shape([cfg.TRAIN.BATCH_SIZE, self._num_classes * 4])
bbox_outside_weights.set_shape([cfg.TRAIN.BATCH_SIZE, self._num_classes * 4])
self._proposal_targets['rois'] = rois
self._proposal_targets['labels'] = tf.to_int32(labels, name="to_int32")
self._proposal_targets['bbox_targets'] = bbox_targets
self._proposal_targets['bbox_inside_weights'] = bbox_inside_weights
self._proposal_targets['bbox_outside_weights'] = bbox_outside_weights
self._score_summaries.update(self._proposal_targets)
return rois, roi_scores
proposal_target_layer
#rnp_rois 爲post_nms_topN*5的矩陣
#rpn_scores爲post_nms_topN的矩陣,代表對應的anchors爲正樣本的概率
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
all_rois = rpn_rois
all_scores = rpn_scores
# Include ground-truth boxes in the set of candidate rois
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_per_image = cfg.TRAIN.BATCH_SIZE / num_images#每張圖片中最終選擇的rois
fg_rois_per_image = np.round(cfg.TRAIN.FG_FRACTION * rois_per_image)#正樣本的個數:0.25*rois_per_image
# Sample rois with classification labels and bounding box regression
# targets
#labels:正樣本和負樣本對應的真實的類別
#rois:從post_nms_topN個anchors中選擇256個anchors(第一列的全0更新爲每個anchors對應的類別)
#rois_scores:256個anchors對應的正樣本的概率
#bbox_targets:256*(4*21)的矩陣,只有爲正樣本時,對應類別的座標纔不爲0,其他類別的座標全爲0
#bbox_inside_weights:256*(4*21)的矩陣,正樣本時,對應類別四個座標的權重爲1,其他全爲0
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個anchors
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)
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
_get_bbox_regression_labels
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
"""
clss = bbox_target_data[:, 0]#第1列,爲類別
bbox_targets = np.zeros((clss.size, 4 * num_classes), dtype=np.float32)#256*(4*21)的矩陣
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)#每個正樣本的起始座標
end = start + 4#每個正樣本的終點座標(由於座標爲4)
bbox_targets[ind, start:end] = bbox_target_data[ind, 1:]#對應的座標偏移賦值給對應的類別
bbox_inside_weights[ind, start:end] = cfg.TRAIN.BBOX_INSIDE_WEIGHTS#對應的權重(1.0,1.0,1.0,1.0)
return bbox_targets, bbox_inside_weights
_compute_targets
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
targets = bbox_transform(ex_rois, gt_rois)#通過公式2後4個,結合256個anchor和對應的正樣本的座標計算座標的偏移
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))#座標減去均值除以標準差,進行歸一化
return np.hstack(
(labels[:, np.newaxis], targets)).astype(np.float32, copy=False)#之前的bbox的一列全0,此處第一列爲對應的類別
_sample_rois
#all_rois:第一列全0,後4列爲座標
#gt_boxes: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)
#計算anchors和gt_boxes重疊區域面積的比值
overlaps = bbox_overlaps(
np.ascontiguousarray(all_rois[:, 1:5], dtype=np.float),
np.ascontiguousarray(gt_boxes[:, :4], dtype=np.float))
gt_assignment = overlaps.argmax(axis=1)#得到每個anchors對應的gt_boxes的索引
max_overlaps = overlaps.max(axis=1)#得到每個anchors對應的gt_boxes的重疊區域的值
labels = gt_boxes[gt_assignment, 4]#得到每個anchors對應的gt_boxes的類別
# Select foreground RoIs as those with >= FG_THRESH overlap
#每個anchors對應的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
# Select background RoIs as those within [BG_THRESH_LO, BG_THRESH_HI)
#每個anchors對應的gt_boxes的重疊區域的值在給定閾值內作爲負樣本,得到負樣本的索引
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個anchors
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
labels[int(fg_rois_per_image):] = 0#負樣本對應的類別設置爲0
rois = all_rois[keep_inds]#從post_nms_topN個anchors中選擇256個anchors
roi_scores = all_scores[keep_inds]#256個anchors對應的正樣本的概率
#通過256個anchors的座標和每個anchors對應的gt_boxes的座標及這些anchors的真實類別得到座標偏移
#(將rois第一列的全0更新爲每個anchors對應的類別)
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
_crop_pool_layer
_crop_pool_layer用於將256個archors從特徵圖中裁剪出來縮放到14*14,並進一步max pool到7*7的固定大小,得到特徵,方便rcnn網絡分類及迴歸座標。
該函數先得到特徵圖對應的原始圖像的寬高,而後將原始圖像對應的rois進行歸一化,並使用tf.image.crop_and_resize(該函數需要歸一化的座標信息)縮放到[cfg.POOLING_SIZE * 2, cfg.POOLING_SIZE * 2],最後通過slim.max_pool2d進行pooling,輸出大小依舊一樣(25677*512)。
tf.slice(rois, [0, 0], [-1, 1])是對輸入進行切片。其中第二個參數爲起始的座標,第三個參數爲切片的尺寸。注意,對於二維輸入,後兩個參數均爲y,x的順序;對於三維輸入,後兩個均爲z,y,x的順序。當第三個參數爲-1時,代表取整個該維度。上面那句是將roi的從0,0開始第一列的數據(y爲-1,代表所有行,x爲1,代表第一列)
_crop_pool_layer定義如下:
def _crop_pool_layer(self, bottom, rois, name):
with tf.variable_scope(name) as scope:
batch_ids = tf.squeeze(tf.slice(rois, [0, 0], [-1, 1], name="batch_id"), [1])#得到第一列,爲類別
# Get the normalized coordinates of bounding boxes
bottom_shape = tf.shape(bottom)
height = (tf.to_float(bottom_shape[1]) - 1.) * np.float32(self._feat_stride[0])
width = (tf.to_float(bottom_shape[2]) - 1.) * np.float32(self._feat_stride[0])
x1 = tf.slice(rois, [0, 1], [-1, 1], name="x1") / width#由於crop_and_resize的bboxes範圍爲0-1,得到歸一化的座標
y1 = tf.slice(rois, [0, 2], [-1, 1], name="y1") / height
x2 = tf.slice(rois, [0, 3], [-1, 1], name="x2") / width
y2 = tf.slice(rois, [0, 4], [-1, 1], name="y2") / height
# Won't be back-propagated to rois anyway, but to save time
bboxes = tf.stop_gradient(tf.concat([y1, x1, y2, x2], axis=1))
pre_pool_size = cfg.POOLING_SIZE * 2
#根據bboxes裁減出256個特徵,並縮放到14*14(channels和bottem的channels一樣)batchsize爲256
crops = tf.image.crop_and_resize(bottom, bboxes, tf.to_int32(batch_ids), [pre_pool_size, pre_pool_size], name="crops")
return slim.max_pool2d(crops, [2, 2], padding='SAME')#max pool後得到7*7的特徵
_head_to_tail
_head_to_tail用於將上面得到的256個archors的特徵增加兩個fc層(ReLU)和兩個dropout(train時有,test時無),降維到4096維,用於_region_classification的分類及迴歸。
_head_to_tail位於vgg16.py中,定義如下
def _head_to_tail(self, pool5, is_training, reuse=None):
with tf.variable_scope(self._scope, self._scope, reuse=reuse):
pool5_flat = slim.flatten(pool5, scope='flatten')
fc6 = slim.fully_connected(pool5_flat, 4096, scope='fc6')
if is_training:
fc6 = slim.dropout(fc6, keep_prob=0.5, is_training=True,
scope='dropout6')
fc7 = slim.fully_connected(fc6, 4096, scope='fc7')
if is_training:
fc7 = slim.dropout(fc7, keep_prob=0.5, is_training=True,
scope='dropout7')
return fc7
_region_classification
fc7通過_region_classification進行分類及迴歸。fc7先通過fc層(無ReLU)降維到21層(類別數,得到cls_score),得到概率cls_prob及預測值cls_pred(用於rcnn的分類)。另一方面fc7通過fc層(無ReLU),降維到21*4,得到bbox_pred(用於rcnn的迴歸)。
_region_classification定義如下:
def _region_proposal(self, net_conv, is_training, initializer):
#vgg16提取後的特徵圖,先進行3*3卷積
#3*3的conv,作爲rpn網絡
rpn = slim.conv2d(net_conv, cfg.RPN_CHANNELS, [3, 3], trainable=is_training, weights_initializer=initializer,
scope="rpn_conv/3x3")
self._act_summaries.append(rpn)
#每個框進行2分類,判斷前景還是背景
#1*1的conv,得到每個位置的9個anchors分類特徵[1,?,?,9*2],
rpn_cls_score = slim.conv2d(rpn, self._num_anchors * 2, [1, 1], trainable=is_training,
weights_initializer=initializer,
padding='VALID', activation_fn=None, scope='rpn_cls_score')
# change it so that the score has 2 as its channel size
#reshape成標準形式
#[1,?,?,9*2]-->[1,?*9.?,2]
rpn_cls_score_reshape = self._reshape_layer(rpn_cls_score, 2, 'rpn_cls_score_reshape')
#以最後一維爲特徵長度,得到所有特徵的概率[1,?*9.?,2]
rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape, "rpn_cls_prob_reshape")
#得到每個位置的9個anchors預測的類別,[1,?,9,?]的列向量
rpn_cls_pred = tf.argmax(tf.reshape(rpn_cls_score_reshape, [-1, 2]), axis=1, name="rpn_cls_pred")
#變換回原始緯度,[1,?*9.?,2]-->[1,?,?,9*2]
rpn_cls_prob = self._reshape_layer(rpn_cls_prob_reshape, self._num_anchors * 2, "rpn_cls_prob")
#1*1的conv,每個位置的9個anchors迴歸位置偏移[1,?,?,9*4]
rpn_bbox_pred = slim.conv2d(rpn, self._num_anchors * 4, [1, 1], trainable=is_training,
weights_initializer=initializer,
padding='VALID', activation_fn=None, scope='rpn_bbox_pred')
if is_training:
#1.使用經過rpn網絡層後生成的rpn_cls_prob把anchor位置進行第一次修正
#2.按照得分排序,取前12000個anchor,再nms,取前面2000個(在test的時候就變成了6000和300)
rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
#獲取屬於rpn網絡的label:通過對所有的anchor與所有的GT計算IOU,通過消除再圖像外部的anchor,計算IOU>=0.7爲正樣本,IOU<0.3爲負樣本,
#得到再理想情況下各自一半的256個正負樣本(實際上正樣本大多隻有10-100個之間,相對負樣本偏少)
rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")#rpn_labels:特徵圖中每個位置對應的正樣本、負樣本還是不關注
# Try to have a deterministic order for the computing graph, for reproducibility
with tf.control_dependencies([rpn_labels]):
#獲得屬於最後的分類網絡的label
#因爲之前的anchor位置已經修正過了,所以這裏又計算了一次經過proposal_layer修正後的box與GT的IOU來得到label
#但是閾值不一樣了,變成了大於等於0.5爲1,小於爲0,並且這裏得到的正樣本很少,通常只有2-20個,甚至有0個,
#並且正樣本最多爲64個,負樣本則有比較多個,相應的也重新計算了一次bbox_targets
#另外,從RPN網絡出來的2000餘個rois中挑選256個
rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")#通過post_nms_topN個anchors的位置及爲1(正樣本)的概率得到256個rois及對應信息
else:
if cfg.TEST.MODE == 'nms':
rois, _ = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
elif cfg.TEST.MODE == 'top':
rois, _ = self._proposal_top_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
else:
raise NotImplementedError
self._predictions["rpn_cls_score"] = rpn_cls_score#每個位置的9個anchors是正樣本還是負樣本
self._predictions["rpn_cls_score_reshape"] = rpn_cls_score_reshape#每個anchors是正樣本還是負樣本
self._predictions["rpn_cls_prob"] = rpn_cls_prob#每個位置的9個anchors是正樣本和負樣本的概率
self._predictions["rpn_cls_pred"] = rpn_cls_pred#每個位置的9個anchors預測的類別,[1,?,9,?]的列向量
self._predictions["rpn_bbox_pred"] = rpn_bbox_pred#每個位置的9個anchors迴歸位置偏移
self._predictions["rois"] = rois#256個anchors的類別(第一維)及位置(後四維)
return rois#返回256個anchors的類別(第一維,訓練時爲每個anchors的類別,測試時全0)及位置(後四維)
通過以上步驟,完成了網絡的創建rois, cls_prob, bbox_pred = self._build_network(training)。
rois:256*5
cls_prob:256*21(類別數)
bbox_pred:256*84(類別數*4)
損失函數
faster rcnn包括兩個損失:rpn網絡的損失+rcnn網絡的損失。其中每個損失又包括分類損失和迴歸損失。分類損失使用的是交叉熵,迴歸損失使用的是smooth L1 loss。
程序通過**_add_losses**增加對應的損失函數。其中rpn_cross_entropy和rpn_loss_box是RPN網絡的兩個損失,cls_score和bbox_pred是rcnn網絡的兩個損失。前兩個損失用於判斷archor是否是ground truth(二分類);後兩個損失的batchsize是256。
將rpn_label(1,?,?,2)中不是-1的index取出來,之後將rpn_cls_score(1,?,?,2)及rpn_label中對應於index的取出,計算sparse_softmax_cross_entropy_with_logits,得到rpn_cross_entropy。
計算rpn_bbox_pred(1,?,?,36)和rpn_bbox_targets(1,?,?,36)的_smooth_l1_loss,得到rpn_loss_box。
計算cls_score(256*21)和label(256)的sparse_softmax_cross_entropy_with_logits:cross_entropy。
計算bbox_pred(256*84)和bbox_targets(256*84)的_smooth_l1_loss:loss_box。
最終將上面四個loss相加,得到總的loss(還需要加上regularization_loss)。
至此,損失構造完畢。
程序中通過_add_losses增加損失:
def _add_losses(self, sigma_rpn=3.0):
with tf.variable_scope('LOSS_' + self._tag) as scope:
# RPN, class loss
#每個anchors是正樣本還是負樣本
rpn_cls_score = tf.reshape(self._predictions['rpn_cls_score_reshape'], [-1, 2])
#特徵圖中每個位置對應的時正樣本、負樣本還是不關注(去除了邊界框在圖像外面的anchors)
rpn_label = tf.reshape(self._anchor_targets['rpn_labels'], [-1])
rpn_select = tf.where(tf.not_equal(rpn_label, -1))#不關注的anchors的索引
rpn_cls_score = tf.reshape(tf.gather(rpn_cls_score, rpn_select), [-1, 2])#去除不關注的anchors
rpn_label = tf.reshape(tf.gather(rpn_label, rpn_select), [-1])#去除不關注的label
rpn_cross_entropy = tf.reduce_mean(
tf.nn.sparse_softmax_cross_entropy_with_logits(logits=rpn_cls_score, labels=rpn_label))#rpn二分類的損失
# RPN, bbox loss
rpn_bbox_pred = self._predictions['rpn_bbox_pred']#每個位置的9個anchors迴歸位置偏移
rpn_bbox_targets = self._anchor_targets['rpn_bbox_targets']#特徵圖中每個位置和對應的正樣本的座標偏移(很多爲0)
rpn_bbox_inside_weights = self._anchor_targets['rpn_bbox_inside_weights']#正樣本的權重爲1(去除負樣本和不關注的樣本,均爲0)
rpn_bbox_outside_weights = self._anchor_targets['rpn_bbox_outside_weights']#正樣本和負樣本(不包括不關注的樣本)歸一化的權重
rpn_loss_box = self._smooth_l1_loss(rpn_bbox_pred, rpn_bbox_targets, rpn_bbox_inside_weights,
rpn_bbox_outside_weights, sigma=sigma_rpn, dim=[1, 2, 3])
# RCNN, class loss
cls_score = self._predictions["cls_score"]#用於rcnn分類的256個anchors的特徵
label = tf.reshape(self._proposal_targets["labels"], [-1])#正樣本和負樣本對應的真實的類別
#rcnn分類的損失
cross_entropy = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=cls_score, labels=label))
# RCNN, bbox loss
bbox_pred = self._predictions['bbox_pred']#RCNN ,bbox loss
bbox_targets = self._proposal_targets['bbox_targets']#256*(4*21)的矩陣,只有爲正樣本時,對應類別的座標纔不爲0,其他類別的座標全爲0
bbox_inside_weights = self._proposal_targets['bbox_inside_weights']
bbox_outside_weights = self._proposal_targets['bbox_outside_weights']
loss_box = self._smooth_l1_loss(bbox_pred, bbox_targets, bbox_inside_weights, bbox_outside_weights)
self._losses['cross_entropy'] = cross_entropy
self._losses['loss_box'] = loss_box
self._losses['rpn_cross_entropy'] = rpn_cross_entropy
self._losses['rpn_loss_box'] = rpn_loss_box
loss = cross_entropy + loss_box + rpn_cross_entropy + rpn_loss_box
regularization_loss = tf.add_n(tf.losses.get_regularization_losses(), 'regu')
self._losses['total_loss'] = loss + regularization_loss
self._event_summaries.update(self._losses)
return loss
smooth L1 loss定義如下(見fast rcnn論文):
程序中先計算pred和target的差box_diff,而後得到正樣本的差in_box_diff(通過乘以權重bbox_inside_weights將負樣本設置爲0)及絕對值abs_in_box_diff,之後計算上式(3)中的符號smoothL1_sign,並得到的smooth L1 loss:in_loss_box,乘以bbox_outside_weights權重,並得到最終的loss:loss_box。
其中_smooth_l1_loss定義如下:
def _smooth_l1_loss(self, bbox_pred, bbox_targets, bbox_inside_weights, bbox_outside_weights, sigma=1.0, dim=[1]):
sigma_2 = sigma ** 2
box_diff = bbox_pred - bbox_targets # 預測的和真實的相減
in_box_diff = bbox_inside_weights * box_diff # 乘以正樣本的權重1(rpn:去除負樣本和不關注的樣本,rcnn:去除負樣本)
abs_in_box_diff = tf.abs(in_box_diff) # 絕對值
smoothL1_sign = tf.stop_gradient(tf.to_float(tf.less(abs_in_box_diff, 1. / sigma_2))) # 小於閾值的截斷的標誌位
in_loss_box = tf.pow(in_box_diff, 2) * (sigma_2 / 2.) * smoothL1_sign + (abs_in_box_diff - (0.5 / sigma_2)) * (1. - smoothL1_sign) # smooth l1 loss
out_loss_box = bbox_outside_weights * in_loss_box # rpn:除以有效樣本總數(不考慮不關注的樣本),進行歸一化;rcnn:正樣本四個座標權重爲1,負樣本爲0
loss_box = tf.reduce_mean(tf.reduce_sum(out_loss_box, axis=dim))
return loss_box
補充說明
基礎網絡部分細節
-
代碼中主要使用了兩種尺寸,一個是原始圖片尺寸,一個是壓縮之後的圖片尺寸;原始圖片進入基礎網絡之前會被壓縮到MxN,但MxN 並不是固定的尺寸,而是把原始圖片等比例resize之後的尺寸,源代碼裏面設置的是壓縮到最小邊長爲600,但是如果壓縮之後最大邊長超過2000,則以最大邊長2000爲限制條件。
-
基礎網絡部分的說明,其中pooling層kernel_size=2,stride=2。這樣每個經過pooling層的MxN矩陣,都會變爲(M/2)*(N/2)大小,那麼,一個MxN大小的矩陣經過Convlayers中的4次pooling之後尺寸變爲(M/16)x(N/16)。那麼假設原圖爲720*1280,MxN爲600*1067,基礎網絡最終的conv5_3 輸出爲1*38*67*1024。
也就是說特徵圖對於原始圖像的感受野是16,fead_stride=16
-
在代碼中經常用到的im_info是什麼?
blobs['im_info'] = np.array([im_blob.shape[1], im_blob.shape[2],im_scales[0]]
可以看到,它裏面包含了三個元素,圖片的width,height,以及im_scales,也就是圖片被壓縮到600最小邊長尺寸時候被壓縮的比例,比如以3中提到的爲例,它就是0.833
-
Blobs 是什麼? 它裏面包含了groundtruth 框數據,圖片數據,圖片標籤的一個字典類型數據,需要說明的是它裏面每次只有一張圖片的數據,Faster RCNN 整個網絡每次只處理一張圖片,這是和我們以前接觸的網絡按照batch處理圖片的方式有所區別的;同時,代碼中涉及到的 batch_size 不是圖片的數量,而是每張圖片裏面提取出來的框的個數;mini_batch 是從一張圖上提取出來的256個anchor,不同的是,caffe 版本的代碼是使用2張圖片,每張圖片128個anchor進行訓練。
-
imdb是一個類,它對所有圖片名稱,路徑,類別等相關信息做了一個彙總;
roidb是imdb的一個屬性,裏面是一個字典,包含了它的GTbox,以及真實標籤和翻轉標籤。
-
anchor 是什麼?
上面我們已經得到了基礎網絡最終的conv5_3 輸出爲1*38*67*1024(1024是層數),在這個特徵參數的基礎上,通過一個3x3的滑動窗口,在這個38*67的區域上進行滑動,stride=1,padding=2,這樣一來,滑動得到的就是38*67個3x3的窗口。
對於每個3x3的窗口,計算這個滑動窗口的中心點所對應的原始圖片的中心點。然後作者假定,這個3x3窗口,是從原始圖片上通過SPP池化得到的,而這個池化的區域的面積以及比例,就是一個個的anchor。換句話說,對於每個3x3窗口,作者假定它來自9種不同原始區域的池化,但是這些池化在原始圖片中的中心點,都完全一樣。這個中心點,就是剛纔提到的,3x3窗口中心點所對應的原始圖片中的中心點。如此一來,在每個窗口位置,我們都可以根據9個不同長寬比例、不同面積的anchor,逆向推導出它所對應的原始圖片中的一個區域,這個區域的尺寸以及座標,都是已知的。而這個區域,就是我們想要的 proposal。所以我們通過滑動窗口和anchor,成功得到了 38*67x9 個原始圖片的proposal。接下來,每個proposal我們只輸出6個參數:每個 proposal 和 ground truth 進行比較得到的前景概率和背景概率(2個參數)(對應 cls_score);由於每個 proposal 和 ground truth 位置及尺寸上的差異,從 proposal 通過平移放縮得到 ground truth 需要的4個平移放縮參數(對應 bbox_pred)。
最後明確的一點就是在代碼中,anchor,proposal,rois ,boxes 代表的含義其實都是一樣的,都是推薦的區域或者框,不過有所區別的地方在於這幾個名詞有一個遞進的關係,最開始的是錨定的框 anchor,數量最多有約20000個(根據resize後的圖片大小不同而有數量有所變化),然後是RPN網絡推薦的框 proposal,數量較多,train時候有2000個,最後是實際分類時候用到的 rois 框,每張圖片有256個;最後得到的結果就是 boxes。
_region_proposal 部分(RPN)
[外鏈圖片轉存失敗(img-GNx0r4Xo-1566548279862)(C:\F\notebook\faster-rcnn\8.jpg)]
_region_proposal 的下面有三個主要的方法:
- _proposal_layer
主要生成推薦區域proposal和前景背景得分rpn_scores,相當於①上圖 部分.
_proposal_layer 有二個主要功能。
(1)使用經過rpn網絡層後生成的rpn_box_prob把anchor位置進行第一次修正;
(2)按照得分排序,取前12000個anchor,再nms,取前面2000個(但是這個數字在test的時候就變成了6000和300,這就是最後結果300個框的來源)。最終返回
proposals , scores,也就是rois, roi_scores。
-
_anchor_target_layer
主要生成第一次anchor的label,rpn_bbox_targets,以及前景背景的label,rpn_labels,相當於上圖 ②部分.
通過對所有的anchor與所有的GT計算IOU,由此得到 rpn_labels(特徵圖每個位置對應的正樣本、負樣本還是不關注), rpn_bbox_targets(anchors和anchors對應的正樣本計算的座標偏移), rpn_bbox_inside_weights(前景控制權重), rpn_bbox_outside_weights(背景控制權重,在損失函數中使用)這4個比較重要的第一次目標label,通過消除在圖像外部的 anchor,計算IOU >=0.7 爲正樣本,IOU <0.3爲負樣本,得到在理想情況下應該各自一半的256個正負樣本(實際上正樣本大多隻有10-100個之間,相對負樣本偏少)。
-
_proposal_target_layer
_proposal_target_layer主要功能是計算獲得屬於最後的分類網絡的label。
生成256個rois的label,以及這些rois的label,bbox_targets,相當於上圖③部分.
使用上一步得到的 proposals , scores,生成最後一步需要的labels, bbox_targets, bbox_inside_weights, bbox_outside_weights。
因爲之前的anchor位置已經修正過了,所以這裏又計算了一次經過 proposal_layer 修正後的的box與 GT的IOU來得到label, 但是閾值不一樣了,變成了大於等於0.5爲1,小於爲0,並且這裏得到的正樣本很少,通常只有2-20個,甚至有0個,並且正樣本最多爲64個,負樣本則有比較多個;相應的也重新計算了一次bbox_targets。
另外,這個函數的另外一個重要功能就是從RPN網絡出來的2000餘個rois中挑選256個
_crop_pool_layer 部分(替換Roi Pooling)
這一部分,作者使用了ROIpooling的另外一種實現形式,核心代碼如下:
x1 = tf.slice(rois, [0,1], [-1, 1], name="x1") / width
y1 = tf.slice(rois, [0,2], [-1, 1], name="y1") / height
x2 = tf.slice(rois, [0,3], [-1, 1], name="x2") / width
y2 = tf.slice(rois, [0,4], [-1, 1], name="y2") / height
# Won't be back-propagated to rois anyway, but to save time
bboxes = tf.stop_gradient(tf.concat([y1, x1, y2, x2], axis=1))
pre_pool_size = cfg.POOLING_SIZE * 2 #7*2
crops = tf.image.crop_and_resize(bottom,bboxes, tf.to_int32(batch_ids), [pre_pool_size, pre_pool_size],name="crops")
return slim.max_pool2d(crops, [2, 2],padding='SAME')
進入的是:conv5_3 輸出爲1*38*67*1024, 以及256個rois代表的位置。
輸出的是:
Tensor("resnet_v1_101_3/pool5/crops:0",shape=(256, 7, 7, 1024), dtype=float32)
這裏可以看到先對rois進行了一個轉換操作,h,w是resize後的圖像大小,把rois除以h,w就得到了rois在特徵圖上的位置,然後把conv5_3先crop,就是把roi對應的特徵crop出來,然後resize到14*14的大小,resize是爲了後面的統一大小,這個操作很有創意,也比較有意思,直接使用了tensorflow的圖像處理方法 crop_and_resize 來進行類似 ROI 的操作,最後再做了一個減半的pooling操作,得到7*7的特徵圖。
其他細節理解
-
什麼是NMS?
NMS是用來去掉冗餘的框的。
NMS的原理是抑制和當前分數最大的框IOU較高的框。如果閾值設置爲0.3, 那麼就是所有與當前分數最大的框box的iou小於閾值的得到保留,而大於閾值的box被這個框吸收,也就是被剔除,而只保留當前分數最大的框,然後在下一次重複這個過程,通過去掉當前最大分數的box的可以吸收的box,來一步一步縮減box規模,所以閾值越小,吸收掉的框越多,閾值越大,保留的框越多
-
batch_size 的區別?
有兩個batch size,一個是__C.TRAIN.RPN_BATCHSIZE = 256,這是用在RPN網絡裏面的,
num_fg = int(cfg.TRAIN.RPN_FG_FRACTION * cfg.TRAIN.RPN_BATCHSIZE)
另外一個是:
# Minibatch size (number of regions of interest [ROIs]) __C.TRAIN.BATCH_SIZE = 256 rois_per_image = cfg.TRAIN.BATCH_SIZE / num_images
這個是用在最後的分類網絡的,二者的數量都是256,但是後者的正樣本比例更少,最多使用 1/4 的 正樣本,即64個。
# Fraction of minibatch that is labeled foreground (i.e. class > 0) __C.TRAIN.FG_FRACTION = 0.25
-
RPN的樣本選取規則和最終Roi Pooling樣本選取規則有何不同?
RPN網絡樣本選取規則:
- 對每個標定的真值候選區域,與其重疊比例最大的anchor記爲前景樣本
- 對a)剩餘的anchor,如果其與某個標定重疊比例大於0.7,記爲前景樣本;如果其與任意一個標定的重疊比例都小於0.3,記爲背景樣本;正負樣本共256個,最多各佔一半
- 對a),b)剩餘的anchor,棄去不用
- 跨越圖像邊界的anchor棄去不用
Roi Pooling樣本選取規則:
- 從 RPN 生成的rois中抽取256個樣本
- 閾值變成了> 0.5 爲正, 在 ( 0, 0.5] 區間爲負
- 正樣本最多爲64個
-
anchor的box_target計算和proposal的box_target的計算的過程有一個細小的差異:
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))
proposal的box_target多一個標準化的過程,BBOX_NORMALIZE_MEANS是全0,沒有影響,除以BBOX_NORMALIZE_STDS(0.1,0.1,0.2,0.2)相當於把box_target擴大了10倍和5倍。
同時有:
if testing: stds = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS), (self._num_classes)) means = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS), (self._num_classes)) self._predictions["bbox_pred"] *= stds self._predictions["bbox_pred"] += means
同樣的,在測試的時候把這個擴大了10倍的預測框的值修正過來了。
-
bbox_inside_weights, bbox_outside_weights 這兩個權重具體是什麼意思?
# Deprecated (outside weights) __C.TRAIN.RPN_BBOX_INSIDE_WEIGHTS = (1.0, 1.0, 1.0, 1.0) # only the positive ones have regression targets bbox_inside_weights[labels == 1, :] = np.array(cfg.TRAIN.RPN_BBOX_INSIDE_WEIGHTS)
所以可以看出,bbox_inside_weights 它實際上就是控制迴歸的對象的,只有真正是前景的對象纔會被迴歸。
# Give the positive RPN examples weight of p * 1 / {num positives} # and give negatives a weight of (1 - p) # Set to -1.0 to use uniform example weighting __C.TRAIN.RPN_POSITIVE_WEIGHT = -1.0 if cfg.TRAIN.RPN_POSITIVE_WEIGHT < 0: # uniform weighting of examples (given non-uniform sampling) 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
可以看出,bbox_outside_weights 也是用1/N1, 2/N0 初始化,對前景和背景控制權重,比起上面多了一個背景的權重,從第二步來看, positive_weights ,negative_weights有互補的意味。
這兩個參數都是在 _smooth_l1_loss 裏面使用,
rpn_loss_box = self._smooth_l1_loss(rpn_bbox_pred, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights, sigma=sigma_rpn, dim=[1, 2, 3]) box_diff = bbox_pred - bbox_targets in_box_diff = bbox_inside_weights * box_diff abs_in_box_diff = tf.abs(in_box_diff) smoothL1_sign = tf.stop_gradient(tf.to_float(tf.less(abs_in_box_diff, 1. / sigma_2))) in_loss_box = tf.pow(in_box_diff, 2) * (sigma_2 / 2.) * smoothL1_sign \ + (abs_in_box_diff - (0.5 / sigma_2)) * (1. - smoothL1_sign) out_loss_box = bbox_outside_weights * in_loss_box ;
可以看出,bbox_outside_weights 就是爲了平衡 box_loss,cls_loss 的,因爲二個loss差距過大,所以它被設置爲 1/N 的權重。
論文提到的 _smooth_l1_loss 相當於一個二次方函數和直線函數的結合,但是爲什麼要這樣呢?不太懂,論文說它比較魯棒,沒有rcnn中使用的L2 loss 那麼對異常值敏感,當迴歸目標不受控制時候,使用L2 loss 會需要更加細心的調整學習率以避免梯度爆炸?_smooth_l1_loss消除了這個敏感性。
參考網址:
- https://zhuanlan.zhihu.com/p/32230004
- https://www.cnblogs.com/darkknightzh/p/10043864.html#_label2
- https://blog.csdn.net/zzyincsdn/article/details/83989606
- https://www.cnblogs.com/dudumiaomiao/p/6560841.html