本文結合CVPR 2018論文"Structure Inference Net: Object Detection Using Scene-Level Context and Instance-Level Relationships",詳細解析Faster RCNN(tensorflow版本)代碼,以及該論文中的一些操作步驟。
-
Faster RCNN整個的流程就是使用VGG等網絡提取全圖的feature map以及使用RPN網絡預測一些object proposal(物體bbox的形式),使用ROI Pooling操作,提取出每個物體的特征圖,然后輸入到兩層全連接神經網絡進行物體類別以及bbox坐標的預測,抽象版的流程圖可以看下面兩幅圖。
為了更清楚的體現代碼中的結構,按照代碼中的流程繪制了下面這張圖:(圖太大截圖不好看,可使用鏈接下載。)
-
下面分析代碼是根據一次典型的實驗的執行順序來分析的,這樣比較方便理解。首先貼出跑一次Faster RCNN的典型參數配置:
'DEDUP_BOXES': 0.0625,
'EPS': 1e-14,
'EXP_DIR': 'faster_rcnn_end2end',
'GPU_ID': 0,
'IS_MULTISCALE': False,
'MATLAB': 'matlab',
'MODELS_DIR': 'XXX/SIN/models/pascal_voc',
'PIXEL_MEANS': array([[[102.9801, 115.9465, 122.7717]]]),
'RNG_SEED': 3,
'ROOT_DIR': 'XXX/SIN',
'TEST':
{'BBOX_REG': True,
'DEBUG_TIMELINE': False,
'HAS_RPN': True,
'MAX_SIZE': 1000,
'NMS': 0.3,
'PROPOSAL_METHOD': 'selective_search',
'RPN_MIN_SIZE': 16,
'RPN_NMS_THRESH': 0.7,
'RPN_POST_NMS_TOP_N': 300,
'RPN_PRE_NMS_TOP_N': 6000,
'SCALES': [600],
'SVM': False},
'TRAIN':
{'ASPECT_GROUPING': True,
'BATCH_SIZE': 128,
'BBOX_INSIDE_WEIGHTS': [1.0, 1.0, 1.0, 1.0],
'BBOX_NORMALIZE_MEANS': [0.0, 0.0, 0.0, 0.0],
'BBOX_NORMALIZE_STDS': [0.1, 0.1, 0.2, 0.2],
'BBOX_NORMALIZE_TARGETS': True,
'BBOX_NORMALIZE_TARGETS_PRECOMPUTED': True,
'BBOX_REG': True,
'BBOX_THRESH': 0.5,
'BG_THRESH_HI': 0.5,
'BG_THRESH_LO': 0.0,
'DEBUG_TIMELINE': False,
'DISPLAY': 10,
'FG_FRACTION': 0.25,
'FG_THRESH': 0.5,
'GAMMA': 0.1,
'HAS_RPN': True,
'IMS_PER_BATCH': 1,
'LEARNING_RATE': 0.0005,
'MAX_SIZE': 1000,
'MOMENTUM': 0.9,
'PROPOSAL_METHOD': 'gt',
'RPN_BATCHSIZE': 256,
'RPN_BBOX_INSIDE_WEIGHTS': [1.0, 1.0, 1.0, 1.0],
'RPN_CLOBBER_POSITIVES': False,
'RPN_FG_FRACTION': 0.5,
'RPN_MIN_SIZE': 16,
'RPN_NEGATIVE_OVERLAP': 0.3,
'RPN_NMS_THRESH': 0.7,
'RPN_POSITIVE_OVERLAP': 0.7,
'RPN_POSITIVE_WEIGHT': -1.0,
'RPN_POST_NMS_TOP_N': 2000,
'RPN_PRE_NMS_TOP_N': 12000,
'SCALES': [600],
'SNAPSHOT_INFIX': '',
'SNAPSHOT_ITERS': 5000,
'SNAPSHOT_PREFIX': 'VGGnet_fast_rcnn',
'STEPSIZE': 80000,
'USE_FLIPPED': True,
'USE_PREFETCH': False},
'USE_GPU_NMS': True}
結合上述配置,下面代碼用到的參數就可以很方便的在這里查閱。
-
數據准備部分:數據准備為整個模型提供圖像數據以及roi的bbo信息。整體涉及到的代碼文件如下:
調用文件順序:tools/train_net.py\(\rightarrow\)(combined_roidb)\(\rightarrow\)datasets/factory.py.get_imdb\(\rightarrow\)imdb.set_proposal_method\(\rightarrow\)lib/fast_rcnn/train.py(get_training_roidb)\(\rightarrow\)imdb.append_flipped_images\(\rightarrow\)pascal_voc.roidb\(\rightarrow\)pascal_voc.gt_roidb
(1)通過從factory得到imdb對象,並且通過set_proposal_method將roi_db_handler設置為gt_roidb
(2)調用train.py的get_training_roidb函數,其中要調用imdb.append_flipped_imags函數,這個是基類imdb的成員函數,這個函數里面,對所有圖像的for循環里,調用了self.roidb,這個時候就會調用imdb的roidb函數,而roidb函數檢查類成員_roidb是否為空,如果不是空就直接返回,否則調用roi_db_handler函數,因為上面已經設置成gt_roidb,實際是執行了gt_roidb()函數,而這個函數是在pascal_voc類中(其他數據集也是如此,每個數據集都具體實現了gt_roidb()函數)
(3)gt_roidb讀取pascal_voc的標注,形成的roidb格式為:
list[
dict 1(for image 1):
{'boxes': (np.array,(N, 4),左上角頂點和右下角頂點坐標,從0開始),
'gt_classes':(np.array, (N,),每個框類別號,21類),
'gt_overlaps':(np.array, (N, 21),稀疏矩陣,每個框對應的那一類是1,其他是0),
'seg_areas':(np.array, (N,),每個框的面積),
'flipped':False},
dict 2(for image 2):{...}
............
]
(4)applied_flipped_images就是對每張圖像的每個框都作一個水平對稱變換,即x坐標變,y坐標不變。結果就是gt_roidb的長度加倍。
(5)接下來,還在get_training_roidb函數內,由於HAS_RPN=True, IS_MULTISCALE=False, 因此調用rdl_roidb的prepare_roidb函數,即roi_data_layer文件夾內的roidb文件中的函數,進一步添加roidb數據字段。注意,此步添加的字段在是不寫入cache中的,是在每次程序運行時添加的。添加后的格式變為:
list[
dict 1(for image 1):
{'boxes': (np.array,(N, 4),左上角頂點和右下角頂點坐標,從0開始),
'gt_classes':(np.array, (N,),每個框類別號,21類),
'gt_overlaps':(np.array, (N, 21),稀疏矩陣,每個框對應的那一類是1,其他是0),
'seg_areas':(np.array, (N,),每個框的面積),
'flipped':False,
'image':當前圖像的路徑,
'width':圖像寬度,
'height':圖像高度,
'max_classes':(np.array, (N,), 每個框與overlap最大的類別號,也就是類別標號),
'max_overlaps':(np.array, (N,),全1)}
dict 2(for image 2):{...}
............
]
(6)至此,get_training_roidb函數執行完畢,返回了roidb,回到combined_roidb函數內,然后檢查是否有多個數據集的roidb,比如pascal_voc_2007_trainval和pascal_voc_2012_trainval,將他們合並成一個roidb的list,最后返回imdb和roidb對象。
(7)接下來,開始真正調用train.py文件中的train_net函數。roidb要先經過篩選,即檢查每個圖像至少有一個前景ROI(overlap大於0.5)或者一個背景ROI(overlap大於等於0小於0.5)。這樣imdb和roidb就完全准備好了。開始調用train_net函數。(8)train_net函數中,需要構造solver對象,而solver類在初始化時需要繼續對roidb添加更多的信息,涉及到的函數是roidb.py文件中的add_bbox_regression_targets函數。在該函數中可以看到為每個圖像的roidb加入了"bbox_targets"字段,調用函數為_compute_targets,輸入參數為
rois: 'boxes': (np.array,(N, 4),左上角頂點和右下角頂點坐標,從0開始)
max_overlaps: 'max_overlaps':(np.array, (N,),全1)
max_classes: 'max_classes':(np.array, (N,), 每個框與overlap最大的類別號,也就是類別標號)
其實這里調用這個函數沒什么意義,因為這里全是grondtruth的bbox,而這個_compute_targets函數我們稍后會看到他是計算一個回歸的偏移量的,而gt的bbox傳進去,計算的偏移量當然全部都是0。因此可以看到添加的bbox_targets字段是一個(N, 5)的矩陣,第一列是類別,后面四列全0,這里只要知道一下被添加了一個字段就好了。下面就開始調用solver的train_model函數。
(9)roidb提供了標注信息,imdb提供了一個數據基類,里面有一些工具接口。那么實際網絡跑起來的時候,也需要准備圖像數據輸入。因此接下來再關注solver的train_model成員函數中每次圖像數據是如何生成的。
(10)首先生成一個data_layer,根據參數設置,得到一個roi_data_layer,類初始化參數為上面得到的roidb和類別數,接下來是一系列的網絡輸出、損失定義,后面再看。我們看真正訓練時的每次循環,一句blobs=data_layer.forward(),這句已經准備好了傳給feed_dict的所有數據,因此很關鍵。
(11)我們看這個forward是如何提供圖像數據的。在初始化roi_data_layer對象時,同時生成了一個len(roidb)長度的標號亂序,比如有5張圖像,那么一個標號亂序可以為[2, 3, 0, 1, 4],以及初始化當前游標為0。每次forward,都調用_get_next_minibatch函數,這個函數又調用_get_next_minibatch_inds函數,這個流程是:(HAS_RPN=True)如果當前游標已經到達標號亂序的盡頭,即每張圖像都遍歷了一遍,就重新生成一個亂序,重置游標為0。然后截取亂序的[cur,cur+IMS_PER_BATCH)部分,並令游標向前推進IMS_PER_BATCH。Faster RCNN要求每個batch只有一張圖像,因此IMS_PER_BATCH=1,這個后面會多次涉及。
(HAS_RPN=False)只抽取有object的圖像。
然后,再調用minibatch.py/get_mini_batch函數,輸入參數是抽取的那個batch圖像的roidb形成的list和類別數。
(12)get_minibatch函數: random_scale_inds是為batch的每個圖像隨機產生的一個從SCALE(參數)中取scale的下標,這是一個list,和roidb list的長度相同。因為SCALE=[600],因此后面每個圖像用的scale都是600.
BATCH_SIZE=128是ROI的batch,注意區分上面的IMS_PER_BATCH=1是圖像的batch。前者要被后者整除。
rois_per_image=BATCH_SIZE/IMS_PER_BATCH=128
fg_rois_per_image=128$\times$0.25=32
調用_get_image_blob函數,輸入參數是roidb和random_scale_inds,返回im_blob和im_scales。首先根據roidb讀入圖像,如果roidb的flipped屬性是True則水平翻轉,注意要看是用什么工具讀圖,如果是opencv通道順序BGR,對應的PIXEL_MEANS要符合這個順序。再調用prep_im_for_blob(im, cfg.PIXEL_MEANS, target_size, cfg.TRAIN.MAX_SIZE),其中target_size就是該圖像對應的scale,即SCALE[random_scale_inds[i]],此處為600。MAX_SIZE=1000。這個函數先將圖像減均值,然后嘗試用target_size/短邊長度,得到一個scale,再用這個scale乘長邊,如果大於MAX_SIZE,則scale為MAX_SIZE/長邊長度。再使用opencv將圖像resize。最后得到的圖像結果是:要么長邊=1000,短邊小於600;要么短邊=600,長邊<=1000。這個函數返回處理后的圖像和該圖resize使用的scale。這樣循環調用prep_im_for_blob后,得到processed_ims和im_scales的list。然后將processed_ims傳給im_list_to_blob函數,這個函數先找到這批圖像的最大短邊和最大長邊,作為整個blob的高和寬,初始化一個BATCH \(\times\) H \(\times\) W \(\times\) 3的blob,依次將每個圖像填入,這就是blob。空白位置用0填充。因為這里每個圖像batch只有一張圖像,因此沒有空白位置。
回到get_minibatch函數,經過上述步驟,我們已經得到了關於圖像batch的im_blob(BATCH \(\times\) H \(\times\) W \(\times\) 3)和每個圖像resize使用的im_scale(list)。注意每個im_blob的長寬可能不一樣,因為是根據圖像而定的。
返回的blobs是一個dict,它包含的字段為:'data':im_blob,這個就是上面返回的im_blob。另外,還要提取出不是背景的gt框,形成一個gt_boxes矩陣,他是從roidb的gt_classes字段中提出標簽非0的框號,然后從boxes字段提出這些前景框,最后形成一個N \(\times\) 5的矩陣,前4是列框的坐標(左上角右下角),第5列是類別標號。另外一個im_info字段,前兩維分別是im_blob的高和寬,第三維是scale。因此最后形成的blobs格式如下:blobs={
'data':(np.array, (1\(\times\)H\(\times\)W$\times$3)),
'im_info':(1$\times$3, 高和寬,scale),
'gt_boxes'😦(N$\times$5), 前景框,前四維坐標,最后一維類別)
}
至此,數據完全准備好,每次抽取1張圖像提供blobs包含圖像數據、高寬、scale,還有gt bboxes。
-
網絡模型部分
接下來會詳細解析一些比較特殊的layer的代碼,VGG部分的Conv,max pooling等就略過了。每一層的解析都是按照代碼控制邏輯逐段逐段解釋,最好對着代碼一起看。
-
anchor_target_layer:
-
輸入參數:
rpn_cls_score: 1\(\times\)h\(\times\)w$\times$18(注意tensorflow和其他不一樣,默認時通道維度是最后一維)
gt_boxes:N$\times$5
im_info: 1$\times$3
data: 1\(\times\)H\(\times\)W$\times$3
-
運行
_anchors: 9$\times$4,使用3個ratio(0.5, 1, 2)和3個scale(8, 16, 32)對基礎框([0,0,15,15])做變換,得到9種框。
height, width: 縮小16倍后的特征圖的高寬,即h, w
shift_x, shift_y:
shift_x = np.arange(0, width) * _feat_stride
shift_y = np.arange(0, height) * _feat_stride
shift_x, shift_y = np.meshgrid(shift_x, shift_y)
shifts = np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())).transpose()
該段代碼首先將特征圖上的每個點乘16對應回原圖的位置,meshgrid將shift_x當成行向量,在0維度堆疊shift_y的長度那么多次,將shift_y當成列向量,在1維度堆疊shift_y那么多次,ravel()將矩陣按行展開。比如,height=4,width=3,那么生成的shifts如下:
array([[ 0, 0, 0, 0], [16, 0, 16, 0], [32, 0, 32, 0], [ 0, 16, 0, 16], [16, 16, 16, 16], [32, 16, 32, 16], [ 0, 32, 0, 32], [16, 32, 16, 32], [32, 32, 32, 32], [ 0, 48, 0, 48], [16, 48, 16, 48], [32, 48, 32, 48]])
從這個例子可以看到一些規律,0維度大小12,就是特征圖面積大小,並且逐行逐行看,他是按行主序的順序遍歷特征圖上的沒一點的。如果將_anchors的每個框和這個shifts的每一行相加,就是得到了所有anchors在原圖的位置。shifts就是anchor的偏移量。比如說,取定原圖上的左上角第一個點(對應取shifts的第一行),分別加上_anchors的9行,就得到了這個位置的9個anchors,取定原圖第一行第二列的點(對應shifts第二行),分別加上_anchors的9行,就又得到了這個位置的9個anchors。那么具體怎么使用矩陣操作相加呢?我們按照代碼中的字母來將形狀符號化,_anchors的形狀是(9,4)=(A, 4),shifts形狀是(h\(\times\)w, 4)=(K, 4),我們利用broadcast,先將shifts reshape成(1, K, 4),並轉置成(K, 1, 4),_anchors reshape成(1, A, 4),根據broadcast這兩個符合規則,可以相加,相加的結果形狀是(K, A, 4),這個也是有順序的,也就是K個通道分別表示特征圖上的K個位置,而每個通道的A \(\times\) 4的矩陣就代表這個位置上的A個anchors。這樣,我們最終得到了在原圖上的A\(\times\)K個anchors的坐標,記為all_anchors。
all_anchors需要篩選,控制一個邊界參數,經過初步篩選,仍記為all_anchors,此時數目應該小於等於A \(\times\) K個。現在記all_anchors: Na \(\times\) 4, labels: (Na,),用-1填充 計算all_anchors和gt_boxes的overlaps(Na\(\times\)No),代碼片段如下
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]
這段代碼之后,argmax_overlaps計算每個anchor覆蓋的gt最多的那個gt_box序號,max_overlaps記錄每個anchor對應的這個最大overlap值。gt_max_overlaps記錄每個gt,對應與之重合的最大overlap,gt_argmax_overlaps則記錄每個gt box,對應的覆蓋最大的anchor序號,注意,每個gt box可能有多個anchor與之對應。
接下來,labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0 此句最大overlap都小於0.3閾值的anchor的label置為0; labels[gt_argmax_overlaps] = 1 將與每個gt_box overlap最大的anchor的label置為1,注意,可能會將上一步的標簽糾正過來。labels[max_overlaps >= cfg.TRAIN.RPN_POSITIVE_OVERLAP] = 1將最大overlap大於0.7的anchor的label也置為1;
然后如果正樣本過多,需要進行正樣本的采樣。首先計算num_fg=RPN_FG_FRACTION\(\times\)RPN_BATCHSIZE=0.5$\times$256=128,fg_inds將labels=1的anchor序號抽出來,如果這個數目多於num_fg,那么隨機抽取一些disabled掉,即將labels置為-1。然后,num_bg=RPN_BATCH_SIZE-sum(labels==1),同樣也抽取labels=0的,多於num_bg的就disabled掉一些,設為-1。通常前景框數目遠少於背景框,二者加起來一共256個,注意現在all_anchors的數目還是沒有變,labels的大小也和all_anchors數目一樣,只不過其中標1和標0的數目加起來是256,還有其他的沒有被考慮的就標為-1。
bbox_targets:對於上述的每個篩選后的anchor計算他和對應的最大IoU的gt box之間的偏移量,_compute_targets(調用bbox_transform)完成這個任務,返回的結果是Na$\times$4,分別是(dx, dy, dw, dh),其中dx和dy是(gt_ctr_x-ex_ctr_x)/ex_widths, 即相對偏差;dw和dh是np.log(gt_widths/ex_widths);注意,Faster RCNN回歸的目標不是框的真實位置,而是anchor相對於gt box的偏差值,因此在預測時需要利用預測的偏差值計算出真正的框的位置。
bbox_inside_weights是Na \(\times\) 4,正樣例(labels=1)的行全是1;其他都是0。bbox_outside_weights也是Na \(\times\) 4,值分情況,如果參數RPN_POSITIVE_WEIGHT<0,那么所有正負樣例的行都是1/(Np+Nn);否則如果在(0,1)間,那么正樣例權重RPN_POSITIVE_WEIGHT/(Np),負樣例權重(1-RPN_POSITIVE_WEIGHT)/(Nn)
最后一步,因為上面的anchor經過篩選,一些在圖像內的標號記為inds_inside,即有Na個,而一開始未經任何篩選時總共有K\(\times\)A個,因此我們還是要求回所有K\(\times\)A個anchors對應上述的所有量。_unmap函數完成這個任務,比如labels,它先生成一個K\(\times\)A大小的一維向量,然后向inds_inside的位置填入labels,其他被拋棄掉的不在圖像內的anchors的label填充-1;同理,bbox_targets,bbox_inside_weights,bbox_outside_weights也全部都是這種操作,被拋棄掉的對應行填0;這樣,這四個變量的形狀分別是(K\(\times\)A, ),(K\(\times\)A, 4),(K\(\times\)A, 4),(K\(\times\)A, 4).
再reshape一下,reshape也有技巧,上面已經說到anchors的排列順序是有規律的,每個位置的A個anchor的信息先排,接下來是下面一個位置,也就是上面的0維度K\(\times\)A的大小可以看成有K組A,而位置變動是行主序的,因此reshape的時候先reshape成(1, h, w, A)或者(1, h, w, A$\times$4),再轉置成(1, A, h, w)或者(1, A \(\times\) 4, h, w),labels比較特別,最后reshape成(1,1,A\(\times\)h, w)。最后返回的四個變量格式如下: rpn_labels, (1,1,A\(\times\)h, w),表示每個位置的每個anchor是正樣本(1)還是負樣本(0)還是被忽略的(-1)
rpn_bbox_targets, (1, A$\times$4, h, w),通道表示每個位置的A個anchor的回歸目標,無目標的填充0;
rpn_bbox_inside_weights, (1, A \(\times\) 4, h, w),未被忽略的,全1,包括正負樣本;被忽略的,填0;
rpn_bbox_outside_weights, (1, A \(\times\) 4, h, w),未被忽略的,全都是1/(Np+Nn);被忽略的,填0。
總結一下,這一層的功能就是給每個位置的9個anchor生成表示正負樣本的label和回歸的目標值,以及權重,提供給RPN進行訓練。
-
-
reshape_layer
-
輸入參數:
rpn_cls_score,(1, h, w, 18)
通道數:d
-
運行:
代碼模型中用到兩個reshape layer。第一個reshape layer,d=2,將rpn_cls_score變成(1, 9h, w, 2),然后進行softmax,特別注意tensorflow默認最后一維是通道,softmax也是默認通道間進行,因此都將通道放在最后一維。輸出rpn_cls_prob。 第二個reshape layer,d=18,緊接着將rpn_cls_prob還原成(1, h, w, 18),為rpn_cls_prob_reshape
-
-
proposal_layer
-
輸入參數:
rpn_cls_prob_reshape,(1, h, w, 18),(1, 18, h, w)
rpn_bbox_pred,(1, h, w, 36),層內轉置成(1, 36, h, w)
im_info: (1,3)
-
運行:
一些設置的參數如下,后面用到:
_num_anchors = 9
pre_nms_topN = cfg[cfg_key].RPN_PRE_NMS_TOP_N=12000(train)/6000(test)
post_nms_topN = cfg[cfg_key].RPN_POST_NMS_TOP_N=2000(train)/300(test)
if cfg_key == 'TEST':
post_nms_topN = 256【SIN論文的設置】
nms_thresh = cfg[cfg_key].RPN_NMS_THRESH=0.7
min_size = cfg[cfg_key].RPN_MIN_SIZE=16
scores = rpn_cls_prob_reshape[:, _num_anchors:, :, :]取第二組9個通道為前景的概率值,即scores形狀為(1,9,h,w)
bbox_deltas = rpn_bbox_pred, (1, 36, h,w) 記住我們預測的是偏移值,因此叫做deltas沒毛病。
和anchor_target_layer一樣,也每個位置產生9個anchor,堆疊成anchors, (K\(\times\)A, 4), 遍歷順序是先遍歷完一個位置的所有anchor,然后寬度遍歷,最后高度遍歷,這種遍歷順序記作(h,w,a)
bbox_deltas = bbox_deltas.transpose((0, 2, 3, 1)).reshape((-1, 4)),現在形狀變成(9\(\times\)h\(\times\)w, 4),遍歷順序(h, w, a)
scores = scores.transpose((0, 2, 3, 1)).reshape((-1, 1)),形狀變成(9\(\times\)h\(\times\)w, 1),遍歷順序(h,w,a)
proposals = bbox_transform_inv(anchors, bbox_deltas),回想anchor_target_layer,他給每個anchor產生的回歸目標是到各個gt box的偏移量,bbox_transform函數完成這個計算。那么現在我們模型回歸出bbox_deltas,因此只要在anchors基礎上做一個bbox_transform_inv的逆運算,就可以計算出模型預測的proposals的框,形狀是和anchors形狀一樣,(9\(\times\)h\(\times\)w, 4)=(K\(\times\)A, 4),左上角右下角頂點坐標值。
進一步對proposals做后續處理,首先是clipped,即每個box的邊界縮回到不超過原圖邊界;然后_filter_boxes, 通過上面的min_size\(\times\)scale,scale從im_info獲得,限制每個框的最小高寬,返回保留的框的序號;proposals和scores都取序號索引的框;這時框的數目少於A\(\times\)K個。
order = scores.ravel().argsort()[::-1]
if pre_nms_topN > 0:
order = order[:pre_nms_topN]
proposals = proposals[order, :]
scores = scores[order]
order是將scores展開,並由大到小排序的標號,如果有pre_nms_topN的限制,就先截取分數最高的pre_nms_topN個框,比如12000個(注意如果少於這個數就是全部),然后proposals和scores都按照這個順序將框排好。這個時候的框已經沒有(h,w,a)的遍歷順序了。
然后再做NMS。NMS的步驟就是對於分數由高到低排序的框,從分數高的開始,看他和后面每一個沒有被扔掉的框的IoU是否大於閾值,是的話就將后面的這些框扔掉;
keep = nms(np.hstack((proposals, scores)), nms_thresh)
delta = 0.05
while (len(keep)) < 256:
keep = nms(np.hstack((proposals, scores)), nms_thresh + delta)
delta = delta + 0.05
這段代碼進行了nms操作,並且保證保留下來的框有至少256個,通過提高閾值實現。
if post_nms_topN > 0:
keep = keep[:post_nms_topN]
proposals = proposals[keep, :]
scores = scores[keep]
batch_inds = np.zeros((proposals.shape[0], 1), dtype=np.float32)
blob = np.hstack((batch_inds, proposals.astype(np.float32, copy=False)))
最后,如果有post_nms_topN,就截取,SIN任務測試時是截256個。然后給proposals這個4列的矩陣在前面加一個全0列,表示batch_inds。因為只有一張圖像,所以batch序號是全0.
總結一下,proposal_layer就是將預測出的rpn_bbox_pred(框的偏移量)拿過來,經過一系列的操作,生成真正的proposals,形狀是5列,注意這里是rpn的proposals,只有是否前景之分,沒有對應的物體類別,這一層的用處是還原出真正的proposal信息,在test時用於prediction。
-
-
proposal target layer
-
輸入參數:
rpn_rois: 5列,來自於proposal_layer的rpn預測出的bbox,第一列全0,表示batch id;
gt_boxes:五列,最后一列是框的類別
_num_classes: 類別數
-
運行:
all_rois = np.vstack((all_rois, np.hstack((zeros, gt_boxes[:, :-1])))) 將rois和gt_boxes在0維拼合在一起,數據還是五列,第一列全0,后四列是box坐標;
num_images = 1
rois_per_image = cfg.TRAIN.BATCH_SIZE / num_images=128/1=128
fg_rois_per_image = np.round(cfg.TRAIN.FG_FRACTION * rois_per_image)=0.25*128=32
labels, rois, bbox_targets, bbox_inside_weights = _sample_rois( all_rois, gt_boxes, fg_rois_per_image, rois_per_image, _num_classes)
具體看_sample_rois函數,傳入參數all_rois, gt_boxes, 32, 128, 21。 該函數首先計算all_rois和gt_boxes的overlaps,得出每個roi bbox最大IoU的gt_box對應的類別標號,用labels表示。然后用前景閾值0.5篩選出前景框標號,記為fg_inds。
fg_rois_per_this_image = int(min(fg_rois_per_image, fg_inds.size))
if fg_inds.size > 0:
fg_inds = npr.choice(fg_inds, size=fg_rois_per_this_image, replace=False)
這段代碼,當fg_inds的個數比fg_rois_per_image大時,就只篩選32個出來;否則,全部保留; 同樣,bg框也是篩選,最后篩選出來前景+背景128個;
相應的,labels設置,將背景框label置為0;接下來就和anchor_target_layer類似了,傳入rois,gt_boxes給_compute_targets來計算要回歸的偏移量,唯一不同的就是需要將偏移量Normalize,即減均值和除以標准差,返回的bbox_target_data有五列,第一列是label,后面四列是回歸的目標;然后繼續調用_get_bbox_regression_labels,主要目的是將bbox_target_data擴充成輸入到網絡的形式,即表示回歸目標的4個元素值擴充成84維,只有class label對應的那4個位置填上目標值,其他位置為0。返回的bbox_targets和bbox_inside_weights都是84列,后者對應label的4個位置全1,其余全0.
最后,這一層返回如下:
rois:128$\times$5,第一列是全0,后面是框的左上角右下角坐標;
labels: 128$\times$1,每個框的物體類別;
bbox_targets: 128$\times$84,每個框回歸的偏差值,經過了normalize
bbox_inside_weights, bbox_outside_weights: 128$\times$84對應類別位置為1.
總結:這一層就是將proposal_layer提供的roi加上物體類別標簽和bbox的回歸目標,並計算權重weights。注意上面的anchor_target_layer加上的標簽和回歸目標用於rpn訓練,這里的用於目標檢測訓練。
-
-
Roi Pooling
-
輸入參數:
特征圖,例如VGG16的conv5-3,shape是(1, h, w, 512)
rois: bbox,(N, 5),第一列的batch_id在此處用到了,來自於propoosal_target_layer
輸出特征圖的大小,一般為7
縮放比例:一般為1/16.
-
運行:
輸出形狀:(N, 7,7, 512)注意,ROI Pooling和max pooling相似,都是逐通道地區域最大值。
先將ROI框的坐標比例縮小16倍(這里有四舍五入操作,會造成偏差,也是ROI pooling被改造成ROI align的原因),假設特征圖大小為h\(\times\)w,然后將特征圖的這個區域划分成H\(\times\)W個網格,每個網格高h/H個像素,寬w/W個像素,然后在這些小網格做Max Pooling,因此得到一個H\(\times\)W的統一大小的ROI的特征圖,一般為7$\times$7。一般這里都是只有一張圖像,因此batch_id全0;而如果有多張圖像,就應該根據rois的batch_id(應該叫image_id更恰當)去找該圖像的特征圖,在這上面pooling,最后輸出來的各個特征圖按照rois的標號順序在第0維堆疊。
-
-
【SIN模塊專屬層】union_box_layer
-
輸入參數:
rois:(128, 5),【注意:proposal_target_layer提供了四個輸出,而network.py中定義的union_box_layer在一開始對輸入做了處理,因為input一開始是(roi-data, im_info),而roi-data是來自proposal_target_layer的四個矩陣,是元組形式,因此只取了第一塊,即rois這個128*5的矩陣】
im_info:(1,3)
-
運行:
返回整個圖的框,即(1, 5)矩陣,第一維是0,后四維實際就是整個圖的框
-
-
【SIN模塊專屬層】edge_box_layer
-
輸入參數:
與union_box_layer相同。
-
運行:
主要作用:形成論文中的\(R^P_{j\rightarrow i}\)的12維向量關系,這是位置關系。實際上就是對所有128個rois進行二重遍歷,計算IoU,如果IoU小於0.6,認為有關系,則產生一個12維向量,包括兩個box各自的寬高面積(除以對應的值作單位化,比如寬除以原圖寬,面積除以原圖面積),這里有6維。然后是他們的相對位置關系,詳細可見原文,注意是用接收信息的框減掉傳過來信息的框,比如\(R^P_{j\rightarrow i}\),那么相對關系應該是\(i\)框減掉\(j\)框。這里也有6維,一共12維向量。對於IoU大於0.6的,不認為他們有關系,因此12維全0,最后返回的是(128*128, 12)的矩陣。
-
-
【SIN模塊專屬層】structure_inference_spmm
-
輸入參數:
fc6: (129, 2048),這是由物體框roi_pooling后的(128, 7,7,512)特征圖和全圖框roi_pooling后的(1,7,7,512)在0維連接后進行一個全連接得到的特征。
edges: 由edge_box_layer得到的(128$\times$128, 12)bbox之間關系的特征。
-
運行:
n_steps=2
n_boxes=128
n_inputs=2048
n_hidden_o=n_hidden_e=2048
ofe = edges
ofo, ofs=tf.split(input[0], [n_boxes, 1], 0) #將fc6在0維分成兩份,得到ofo (128, 2048)和ofs (1, 2048)
fo = tf.reshape(ofo, [n_boxes, n_inputs]) #(128, 2048),每個物體框的視覺特征
fs = tf.reshape(ofs, [1, n_inputs])
fs = tf.concat(n_boxes * [fs], 0)
fs = tf.reshape(fs, [n_boxes, 1, n_inputs])
fe = tf.reshape(ofe, [n_boxes * n_boxes, 12])
這段最后得到的fs是(128, 1, 2048),因為是concat堆疊的,每個2048維都一樣;表示由場景提供的context特征。
fe是(128 \(\times\) 128, 12),表示物體之間位置關系的特征;
u: 訓練參數,(12, 1)
W: 訓練參數,(2048, 2048)
接下來計算原文中的\(e_{j\rightarrow i}=relu(W_pR^p_{j\rightarrow i})*tanh(W_v[f_i, f_j])\),因為任意兩個box之間的關系是一個標量值,下面要計算出的是矩陣,即進行矩陣化的操作。
PE = fe與u相乘,即將128 \(\times\) 128個box pair的12維關系特征壓縮成1維表示,然后reshape成(128, 128)的矩陣,再relu,這和原來的bbox順序一樣,因此可以reshape,這個就相當於公式中的relu部分。
oinput=fs[:,0,:] (128, 2048) ,場景提供的context信息,在以后的迭代中每次輸入都不變。
hi=fo #(128, 2048) 輸入到GRU的初始隱狀態,即128個物體框的視覺特征
開始GRU迭代兩步:
X: 是hi變成(128, 128, 2048),明確他的意義,這里有128通道,每個通道128 \(\times\) 2048矩陣,每個矩陣中的每一行就是每個物體框的特征,那么根據原文的描述,X是用來計算每個節點接受的信息;
對應原文中E的計算,需要計算tanh部分,這里實現和文中稍有不同,計算視覺的相互影響關系是用\(tanh(YWY^T)\)的方式,其中W是對稱正交矩陣,不管方式如何,得到VE是(128,128),因此計算E就是用PE\(\times\)VE,再進行一個softmax。E是(128,128),我們進一步記作Z。這里的每一個位置就是任意兩個框之間的影響權重。
接下來就要計算每個物體節點接收的關系信息m,結合上面說到的X,我們先看原文的公式,
\[m_i^e=max_{j\in V}(e_{j\rightarrow i}f_j^v) \]對於物體i,他接收的關系是先讓E中其他物體j對i的影響因子乘以j的視覺特征,這個實現起來非常巧妙,將Z變成(128, 128, 1),這樣的話每個128 \(\times\) 1的矩陣就是所有其他框對當前框的影響因子,這個可以根據edge_box_layer的計算順序推知。那么根據變形關系,對於Z的通道i,就是對應要計算的物體i,也是對應X的通道i,然后如果讓Z\(\times\)X,利用了broadcast機制,比如我們看第1通道,分別抽出Z和X的第一通道,就是計算第一個物體的接收信息,那么128 \(\times\) 1的矩陣和128 \(\times\) 2048矩陣相乘,就是公式中的各自影響因子和各自視覺特征相乘。這樣,我們得到了(128, 128, 2048)的tensor,再根據公式中的max,應該是在1維度進行取max操作,表示按照其他框的影響取最大值,因此最終得到M (128, 2048),表示128個框分別接收的信息。
計算好由edge獲得的信息einput(就是M)后,就可以輸入兩個GRU進行迭代了。Scene GRU的初始輸入x是fs全圖特征(128, 2048),這個每次迭代都不變,而edge GRU的輸入是einput,他們共同的隱狀態是hi,初始隱狀態是(128, 2048)的物體特征,隨着每次迭代,取兩個GRU的更新后的隱狀態的均值作為新的隱狀態,並且重新計算einput,進行下一次迭代。最終返回一個(128, 2048)的物體特征矩陣,這個就是替代原始Faster RCNN輸入到全連接層進行分類和定位的特征。
-
-
-
模型訓練
loss的構成:loss主要分成4個部分。
-
rpn分類損失
從anchor_target_layer返回數據讀取第一塊rpn_label,排成一列,共有K\(\times\)A個,讀取rpn_cls_score_reshape數據,reshape成(K\(\times\)A, 2)矩陣,然后根據label取出不為-1的行,共256行,然后輸入給tf.nn.sparse_softmax_cross_entropy_with_logits計算分類損失。
-
rpn回歸損失
從anchor_target_layer返回數據讀取第二到第四塊數據,分別是回歸的目標值,計算modified L1 loss的inside和outside weight,都reshape成(1, h, w, A*4),從而計算出L1 loss。
-
目標檢測分類損失
讀取最后的cls_score,(128, 21),從proposal_target_layer返回數據讀取第二塊labels,排成一列,然后計算tf.nn.sparse_softmax_cross_entropy_with_logits分類損失。
-
目標檢測回歸損失
讀取最后的bbox_pred,(128, 84),從proposal_target_layer返回數據讀取第三到第五塊數據,同樣計算L1 loss。
-
至此,全部主要的代碼分析完畢,跟着數據走非常重要!!搞清楚每一層輸出數據的形狀!
參考資料:
【1】tensorflow版的Faster RCNN代碼:https://github.com/smallcorgi/Faster-RCNN_TF.git
【2】SIN論文:Structure Inference Net: Object Detection Using Scene-Level Context and Instance-Level Relationships, Yong Liu, Ruiping Wang, Xilin Chen, CVPR 2018.
【3】Faster RCNN及SIN模型結構Visio繪圖鏈接:https://pan.baidu.com/s/10GKd777lEE31ekoPWMja5A 密碼:eo4j
對於原創博文:如需轉載請注明出處http://www.cnblogs.com/Kenneth-Wong/