天天看點

faster rcnn源碼解析

之前一直是使用faster rcnn對其中的代碼并不是很了解,這次剛好複現mask rcnn就仔細閱讀了faster rcnn,主要參考代碼是pytorch-faster-rcnn ,部分參考和借用了以下部落格的圖檔

[1] CNN目标檢測(一):Faster RCNN詳解

姊妹篇mask rcnn解析

整體架構

faster rcnn源碼解析
  1. 首先圖檔進行放縮到W*H,然後送入vgg16(去掉了pool5),得到feature map(W/16, H/16)
  2. 然後feature map上每個點都對應原圖上的9個anchor,送入rpn層後輸出兩個: 這9個anchor前背景的機率以及4個坐标的回歸
  3. 每個anchor經過回歸後對應到原圖,然後再對應到feature map經過roi pooling後輸出7*7大小的map
  4. 最後對這個7*7的map進行分類和再次回歸

    (此處均為大體輪廓,具體細節見後面)

資料層

  1. 主要利用工廠模式适配各種資料集 factory.py中利用lambda表達式(泛函)
  2. 自定義适配自己資料集的類,繼承于imdb
  3. 主要針對資料集中生成roidb,對于每個圖檔保持其中含有的所有的box坐标(0-index)及其類别,然後順便儲存它的面積等參數,最後記錄所有圖檔的index及其根據index擷取絕對位址的方法
# factory.py
from datasets.mydataset import mydataset
for dataset in ['xxdataset']:
  for split in ['train', 'val', 'test']:
    name = '{}_{}'.format(dataset, split)
    __sets[name] = (lambda split=split,dataset=dataset: mydataset(split, dataset))
           

RPN

faster rcnn源碼解析

anchors生成

經過feature extraction後,feature map的大小是(W/16, H/16), 記為(w,h),然後每個feature map每個點生成k個anchor,論文中設定了3中ratio, 3種scale 共産生了w*h*9個anchors

faster rcnn源碼解析
# # array([[ -83.,  -39.,  100.,   56.],
#       [-175.,  -87.,  192.,  104.],
#       [-359., -183.,  376.,  200.],
#       [ -55.,  -55.,   72.,   72.],
#       [-119., -119.,  136.,  136.],
#       [-247., -247.,  264.,  264.],
#       [ -35.,  -79.,   52.,   96.],
#       [ -79., -167.,   96.,  184.],
#       [-167., -343.,  184.,  360.]])
#  先以左上角(0,0)為例生成9個anchor,然後在向右向下移動,生成整個feature map所有點對應的anchor
           

anchors前背景和坐标預測

正如整體架構上畫的那樣,feature map後先跟了一個3*3的卷積,然後分别用2個1*1的卷積,預測feature map上每個點對應的9個anchor屬于前背景的機率(9*2)和4個回歸的坐标(9*4)

# rpn
self.rpn_net = nn.Conv2d(self._net_conv_channels, cfg.RPN_CHANNELS, [, ], padding=)
self.rpn_cls_score_net = nn.Conv2d(cfg.RPN_CHANNELS, self._num_anchors * , [, ])
self.rpn_bbox_pred_net = nn.Conv2d(cfg.RPN_CHANNELS, self._num_anchors * , [, ])


rpn = F.relu(self.rpn_net(net_conv))
rpn_cls_score = self.rpn_cls_score_net(rpn) # batch * (num_anchors * 2) * h * w
rpn_bbox_pred = self.rpn_bbox_pred_net(rpn) # batch * (num_anchors * 4) * h * w
           

anchor target

對上一步産生的anchor配置設定target label,1前景or0背景or-1忽略,以便訓練rpn(隻有配置設定了label的才能計算loss,即參與訓練)

無NMS

1. 對于每個gt box,找到與他iou最大的anchor然後設為正樣本

2. 對于每個anchor隻要它與任意一個gt box iou>0.7即設為正樣本

3. 對于每個anchor它與任意一個gt box iou都<0.3即設為負樣本

4. 不是正也不是負的anchor被忽略

注意

正樣本的數量由num_fg = int(cfg.TRAIN.RPN_FG_FRACTION * cfg.TRAIN.RPN_BATCHSIZE)控制,預設是256*0.5=128,即最多有128個正樣本參與rpn的訓練. 假如正樣本有1234個,則随機抽1234-128個正樣本将其label設定為-1,即忽略掉,當然正樣本也有可能不足128個,那就都保留下來.

負樣本的數量由num_bg = cfg.TRAIN.RPN_BATCHSIZE - np.sum(labels == 1),同理如果超額也為多餘的忽略.

TRAIN.RPN_FG_FRACTION控制參與rpn訓練的正樣本的數量

注意在RPN階段需要的配置參數都有RPN字首,與後面的fast rcnn的參數差別開

# Max number of foreground examples
# __C.TRAIN.RPN_FG_FRACTION = 0.5
# Total number of examples
#__C.TRAIN.RPN_BATCHSIZE = 256

# subsample positive labels if we have too many
num_fg = int(cfg.TRAIN.RPN_FG_FRACTION * cfg.TRAIN.RPN_BATCHSIZE)
fg_inds = np.where(labels == )[]
if len(fg_inds) > num_fg:
  disable_inds = npr.choice(
    fg_inds, size=(len(fg_inds) - num_fg), replace=False)
  labels[disable_inds] = -

# subsample negative labels if we have too many
num_bg = cfg.TRAIN.RPN_BATCHSIZE - np.sum(labels == )
bg_inds = np.where(labels == )[]
if len(bg_inds) > num_bg:
  disable_inds = npr.choice(
    bg_inds, size=(len(bg_inds) - num_bg), replace=False)
  labels[disable_inds] = -
           

Fast RCNN

proposal

對RPN産生的anchor進行處理,有NMS

1. 首先利用4個坐标回歸值對預設的w*h*9個anchor進行坐标變換生成proposal

2. 然後利用前景機率對這些proposal進行降序排列,然後留下RPN_PRE_NMS_TOP_N個proposal 訓練是留下12000,測試是留下6000

3. 對剩下的proposal進行NMS處理,門檻值是0.7

4. 對于剩下的proposal,隻留下RPN_POST_NMS_TOP_N,訓練是2000,測試是300

最終剩下的proposal即為rois了

proposal target

對留下的proposal(train:2000, test沒有這個階段,因為測試不知道gt無法配置設定)配置設定target label,屬于具體哪一個類别,以便訓練後面的分類器, 下面以train階段的某個圖檔為例即該張圖檔有2000個proposal,gt中含有15個類别的box(不含背景) (全庫有20個類别)

# Minibatch size (number of regions of interest [ROIs])
# __C.TRAIN.BATCH_SIZE = 128
# Fraction of minibatch that is labeled foreground (i.e. class > 0)
# __C.TRAIN.FG_FRACTION = 0.25 控制fast rcnn中rois的正負樣本比例為1:3
num_images = 
rois_per_image = cfg.TRAIN.BATCH_SIZE / num_images # 預設為128
fg_rois_per_image = int(round(cfg.TRAIN.FG_FRACTION * rois_per_image))  # 0.25*128
           
  1. 計算每個roi(proposal)與15個gt box做iou,得到overlaps(2000, 15) ,然後選擇最大的iou作為這個roi的gt label(坑點: gt box的順序不一定和label對應,一定要取gt box的第4個次元作為label,因為可能包含15個gt box,但是全庫是有20中label的)
  2. 然後記roi與其target label的ovlap>TRAIN.FG_THRESH(0.5)的為fg,0.1
if fg_inds.numel() >  and bg_inds.numel() > :
  fg_rois_per_image = min(fg_rois_per_image, fg_inds.numel())
  fg_inds = fg_inds[torch.from_numpy(npr.choice(np.arange(, fg_inds.numel()), size=int(fg_rois_per_image), replace=False)).long().cuda()]
#  ......
#  主要解讀npr.choice(np.arange(0, fg_inds.numel()), size=int(fg_rois_per_image), replace=False)
#  在np.arange(0, fg_inds.numel())随機取int(fg_rois_per_image)個數,replace=False不允許重複
           

roi pooling

上一步得到了很多大小不一的roi,對應到feature map上也是大小不一的,但是fc是需要fixed size的,于是根據SPPNet論文筆記和caffe實作說明,出來了roi pooling(spp poolingfroze 前面的卷積隻更新後面的fc,why見fast rcnn的2.3段解釋的)

我主要參考了這篇部落格Region of interest pooling explained,但是我感覺它的示意圖是有問題的,應該有overlap的

1. 我們首先根據feature map和原圖的比例,把roi在原圖上的坐标映射到feature map上, 然後扣出roi對應部分的feature(藍色框為實際位置,浮點坐标(1.2,0.8)(7.2,9.7),四舍五入量化到紅色框(1,1)(7,10))

int roi_start_w = round(rois_flat[index_roi + ] * spatial_scale);  // spatial_scale 1/16
int roi_start_h = round(rois_flat[index_roi + ] * spatial_scale);
int roi_end_w = round(rois_flat[index_roi + ] * spatial_scale);
int roi_end_h = round(rois_flat[index_roi + ] * spatial_scale);
           
faster rcnn源碼解析

2. 對紅色紅色框進行roipooling

float bin_size_h = (float)(roi_height) / (float)(pooled_height);  // 9/7
float bin_size_w = (float)(roi_width) / (float)(pooled_width);  // 7/7=1
for (ph = ; ph < pooled_height; ++ph){
  for (pw = ; pw < pooled_width; ++pw){
    int hstart = (floor((float)(ph) * bin_size_h));  
    int wstart = (floor((float)(pw) * bin_size_w));
    int hend = (ceil((float)(ph + ) * bin_size_h));
    int wend = (ceil((float)(pw + ) * bin_size_w));
    hstart = fminf(fmaxf(hstart + roi_start_h, ), data_height);
    hend = fminf(fmaxf(hend + roi_start_h, ), data_height);
    wstart = fminf(fmaxf(wstart + roi_start_w, ), data_width);
    wend = fminf(fmaxf(wend + roi_start_w, ), data_width);
// ......
// 經過計算後w步長為1,視窗為1,沒有overlap,h視窗步長不定都有overlap,注意在ph=3時視窗為3了
// 注意邊界 pw=pooled_width-1時 wend=(ceil((float)(pw + 1) * bin_size_w))
//  =(ceil((float)pooled_width * (float)(roi_width) / (float)
//  =(pooled_width)))=ceil(roi_width)=roi_width
//  剛好把所有roi對應的feature map覆寫完,hend同理
//  roi_height roi_width小于pooled_height pooled_width時overlap就多一點呗
           
faster rcnn源碼解析

3. 對每個劃分的pool bin進行max或者average pooling最後得到7*7的feature map

分類和回歸

roi pooling後就得到fixed size的feature map(7*7),然後送入cls_score_net得到分類,送入bbox_pred_net粗暴的坐标回歸和rpn時一樣

self.cls_score_net = nn.Linear(self._fc7_channels, self._num_classes)
self.bbox_pred_net = nn.Linear(self._fc7_channels, self._num_classes * )
           

Loss采用smooth L1 Loss(和fast rcnn一緻,rcnn采用的是L2 Loss)。

faster rcnn源碼解析
prior_centers = center_size(prior_boxes) #(cx, cy, w, h)
gt_centers = center_size(gt_boxes) #(cx, cy, w, h)
# tx=(gx-px)/pw  gx是gt的中心坐标x,px是proposal的中心坐标x,pw是預測的寬。ty同理
center_targets = (gt_centers[:, :] - prior_centers[:, :]) / prior_centers[:, :]
# 參照tw,th的公式
# 加了log, 降低w,h産生的loss的數量級, 讓它在loss裡占的比重小些, 不至于因為w,h的loss太大而讓x,y産生的loss無用
# 因為若是x,y沒預測準确, w,h再準确也沒有用. 
size_targets = torch.log(gt_centers[:, :]) - torch.log(prior_centers[:, :])
all_targets = torch.cat((center_targets, size_targets), )
loss = F.smooth_l1_loss(deltas, all_targets, size_average=False)/(eps + prior_centers.size())
           
smoothed L1 Loss is a robust L1 loss that is less sensitive to outliers than the L2 loss used in R-CNN and SPPnet.

上述是Fast RCNN解釋為什麼采用smoothed L1, 因為它對噪音點不那麼敏感,即對離目标太遠的點不敏感。因為L2loss求導後 0.5*(t-v)^2 求導-> (t-v) 會有一個(t-v) 的系數在,如果v離t太遠梯度很容易爆炸(需要精緻地調節學習率),而smoothed L1中當|t-v|>1, |t-v|-0.5 求導-> 系數是±1, 這樣就避免了梯度爆炸, 也就是它更加魯棒。(t是target,v是需要預測出來的中心xy和尺寸wh)

faster rcnn源碼解析

測試

繼續假設全部類别數是20種

1. 圖檔送入網絡後前傳,沒有給anchor proposal指定gt的部分(忽略_anchor_target_layer _proposal_target_layer)

2. 經過proposal得到300個roi,經過cls_score_net bbox_pred_net得到每個roi在20個類别的置信度和4個坐标回歸值(可在測試時把這個回歸值用上,也可以不用)

3. 測試時300個roi類别未知,是以可以對應20個類别,即有300*20個box,300*20個置信度

3. 對每一類,取300個roi>thresh(預設為0.),然後進行nms獲得留下的box

4. 然後對20類留下的所有box,按機率排序,留下設定的max_per_image個box

有個不解就是為什麼對于每個roi,不是選擇其置信度最大的類别,而可以對應到20種類别,可能是map算法,同等置信度下,多一些box得分會高一些

for j in range(, imdb.num_classes):
  inds = np.where(scores[:, j] > thresh)[]
  cls_scores = scores[inds, j]
  cls_boxes = boxes[inds, j*:(j+)*]
  cls_dets = np.hstack((cls_boxes, cls_scores[:, np.newaxis])) \
    .astype(np.float32, copy=False)
  keep = nms(torch.from_numpy(cls_dets), cfg.TEST.NMS).numpy() if cls_dets.size >  else []
  cls_dets = cls_dets[keep, :]
  all_boxes[j][i] = cls_dets
           

延伸

驗證一下nms在訓練時是不是必須的

參考An Implementation of Faster RCNN with Study for Region Sampling

faster rcnn源碼解析

• First, take the top K regions according to RPN score.

• Then, non-maximal suppression (NMS) with overlapping ratio of 0.7 is applied to perform de-duplication.

• Third, top k regions are selected as RoIs.

Intuitively, it is more likely for large regions to overlap than small regions, so large regions have a higher chance to be suppressed對這句話保留意見,nms算的是iou,沒有偏向抑制大的region吧

ALL是top12000 proposal都送入後面的網絡,不進行nms PRE是利用第一行已經訓練好的faster rcnn直接得到最終的正負樣本比例 POW: 比例和scale成反比,詳細見文章。TOP是test是選擇top 5000不進行nms(faster rcnn本身是選擇top 6000然後nms,最後再取top300)

In fact, we find this advantage of TOP over NMS consistently exists when K is sufficiently large.

繼續閱讀