Faster RCNN(tensorflow)代碼詳解

本文結合CVPR 2018論文"Structure Inference Net: Object Detection Using Scene-Level Context and Instance-Level Relationships",詳細解析Faster RCNN(tensorflow版本)代碼,以及該論文中的一些操做步驟。git

  1. Faster RCNN整個的流程就是使用VGG等網絡提取全圖的feature map以及使用RPN網絡預測一些object proposal(物體bbox的形式),使用ROI Pooling操做,提取出每一個物體的特徵圖,而後輸入到兩層全鏈接神經網絡進行物體類別以及bbox座標的預測,抽象版的流程圖能夠看下面兩幅圖。github

    爲了更清楚的體現代碼中的結構,按照代碼中的流程繪製了下面這張圖:(圖太大截圖很差看,可以使用連接下載。)網絡

  2. 下面分析代碼是根據一次典型的實驗的執行順序來分析的,這樣比較方便理解。首先貼出跑一次Faster RCNN的典型參數配置:app

    'DEDUP_BOXES': 0.0625,dom

    'EPS': 1e-14,ide

    'EXP_DIR': 'faster_rcnn_end2end',函數

    'GPU_ID': 0,工具

    'IS_MULTISCALE': False,post

    '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}

    結合上述配置,下面代碼用到的參數就能夠很方便的在這裏查閱。

  3. 數據準備部分:數據準備爲整個模型提供圖像數據以及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。

  4. 網絡模型部分

    接下來會詳細解析一些比較特殊的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輸入到全鏈接層進行分類和定位的特徵。

  5. 模型訓練

    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/

相關文章
相關標籤/搜索