A Simple Faster-RCNN 代碼理解學習


寫在前面的話

在弄清楚RCNN、Fast-RCNN和Faster-RCNN的原理和區別后,找到了一份開源代碼(具體鏈接見參考資料第一條)研究。第一次看這份代碼的時候,我直接去世(doge,pytorch也只是新手的我真的是原地爆炸,后來發現主要是自己沉不住氣看,后面看另一篇博主的代碼解析的時候(具體鏈接見參考資料第二條),上面寫着“這份代碼刪除注釋只有2000行左右,而我看了差不多7天,自己和大佬的差距真的挺遠的”。我...,看都看不下來????
哦呵,上述是發生在這篇博客之前的見聞。因為在上一篇原理部分就faster-rcnn的邏輯就已經梳理的差不多了,所以這里記錄一下代碼中重要和難理解的部分。
希望自己不要急於求成,沉住氣!吾生也有涯,而學也無涯,以有涯隨無涯,共勉!

項目的運行流程

train和test相比多出了計算loss的模塊,test過程不加入loss模塊或者直接跳過

代碼理解

三種代碼理解和解釋,我大部分都在代碼(見折疊代碼中)中注釋了

./utils/eval_tool.py

eval_detection_voc
from __future__ import division

from collections import defaultdict
import itertools
import numpy as np
import six  # 解決python2和python3不兼容的問題
from model.utils.bbox_tools import bbox_iou
def eval_detection_voc(
        pred_bboxes, pred_labels, pred_scores, gt_bboxes, gt_labels,
        gt_difficults=None,
        iou_thresh=0.5, use_07_metric=False):

    # 獲取不同類別的precision和recall
    prec, rec = calc_detection_voc_prec_rec(
        pred_bboxes, pred_labels, pred_scores,
        gt_bboxes, gt_labels, gt_difficults,
        iou_thresh=iou_thresh)
    # 獲取不同類別的AP 
    ap = calc_detection_voc_ap(prec, rec, use_07_metric=use_07_metric)
    
    # 獲取ap和mAP
    return {'ap': ap, 'map': np.nanmean(ap)}
calc_detection_voc_prec_rec
def calc_detection_voc_prec_rec(
        pred_bboxes, pred_labels, pred_scores, gt_bboxes, gt_labels,
        gt_difficults=None,
        iou_thresh=0.5):
    # 將於圖片有關的變量轉換為迭代器,每次迭代的作用針對下一張圖片進行檢測
    pred_bboxes = iter(pred_bboxes)
    pred_labels = iter(pred_labels)
    pred_scores = iter(pred_scores)
    gt_bboxes = iter(gt_bboxes)
    gt_labels = iter(gt_labels)
    # 因為VOC數據集中存在難以識別的Ground Truth,所以加上difficult,
    # 如果是difficult可以不參與計算,在下面的代碼中體現為標記為-1
    if gt_difficults is None:
        gt_difficults = itertools.repeat(None)
    else:
        gt_difficults = iter(gt_difficults)

    # 初始化記錄Precision和Recall數量的字典,第一級鍵應該是類別
    n_pos = defaultdict(int)
    score = defaultdict(list)
    match = defaultdict(list)

    # 對每張圖片進行運算,統計各類別的匹配和不匹配的數量
    for pred_bbox, pred_label, pred_score, gt_bbox, gt_label, gt_difficult in \
            six.moves.zip(
                pred_bboxes, pred_labels, pred_scores,
                gt_bboxes, gt_labels, gt_difficults):

        if gt_difficult is None:
            gt_difficult = np.zeros(gt_bbox.shape[0], dtype=bool)

        for l in np.unique(np.concatenate((pred_label, gt_label)).astype(int)):  # 對類別進行遍歷
            # 獲取l類的pred_bbox, pred_label, pred_score, gt_bbox, gt_label, gt_difficult
            pred_mask_l = pred_label == l
            pred_bbox_l = pred_bbox[pred_mask_l]
            pred_score_l = pred_score[pred_mask_l]
            order = pred_score_l.argsort()[::-1]
            pred_bbox_l = pred_bbox_l[order]
            pred_score_l = pred_score_l[order]
            # 將預測類別為l的pred_box和pred_score收集起來並且降序排列
            
            gt_mask_l = gt_label == l
            gt_bbox_l = gt_bbox[gt_mask_l]
            gt_difficult_l = gt_difficult[gt_mask_l]

            n_pos[l] += np.logical_not(gt_difficult_l).sum()
            # 表示一共有多少個ground truth,這里就可以發現忽視了難以判別的ground truth
            score[l].extend(pred_score_l)

            if len(pred_bbox_l) == 0:  # 如果這個圖片l類的bbox不存在
                continue
            if len(gt_bbox_l) == 0:  # 如果這個圖片存在l類的bbox,但是沒有l類的ground truth 就可以標記未不匹配
                match[l].extend((0,) * pred_bbox_l.shape[0])
                continue

            # VOC評估遵循整數類型的邊界框。這里不懂?
            pred_bbox_l = pred_bbox_l.copy()
            pred_bbox_l[:, 2:] += 1  
            gt_bbox_l = gt_bbox_l.copy()
            gt_bbox_l[:, 2:] += 1

            # 計算這張圖片l類的bbox和ground truth 之間的IoU,生成的矩陣大小為(M,N)
            # M :pred_bbox_l的長度,N:gt_bbox_l的長度
            iou = bbox_iou(pred_bbox_l, gt_bbox_l)
            gt_index = iou.argmax(axis=1)
            # 這里應該是找到和pred_bbox_l中IOU最大的ground truth,shape:(M,)
            # pred_bbox_l和ground truth最大的iou都小於閾值的標記為不匹配的,-1
            gt_index[iou.max(axis=1) < iou_thresh] = -1
            del iou

            selec = np.zeros(gt_bbox_l.shape[0], dtype=bool)
            for gt_idx in gt_index:
                if gt_idx >= 0:
                    if gt_difficult_l[gt_idx]:  # 該ground truth是識別困難的,就不參與計算
                        match[l].append(-1)
                    else:
                        if not selec[gt_idx]:  # 該位置的pred_bbox存在超過IOU閾值的ground truth,就標記為匹配成功,1
                            match[l].append(1)
                        else:  # 一個ground truth只能被對應一次
                            match[l].append(0)
                    selec[gt_idx] = True
                else:
                    match[l].append(0)
            # match應該表示pred_bbox有多少個對應上了ground truth,而且留下了列表,可以記錄預測框的對應情況
            
    for iter_ in (
            pred_bboxes, pred_labels, pred_scores,
            gt_bboxes, gt_labels, gt_difficults):
        if next(iter_, None) is not None:
            raise ValueError('Length of input iterables need to be same.')
    # 保證相同長度
    
    n_fg_class = max(n_pos.keys()) + 1  # 總共的類別數量
    prec = [None] * n_fg_class 
    rec = [None] * n_fg_class

    for l in n_pos.keys():
        score_l = np.array(score[l])
        match_l = np.array(match[l], dtype=np.int8)
        
        # 獲取按照得分降序排列的所有pred_bbox
        order = score_l.argsort()[::-1]
        # 匹配列表也按照得分降序排列
        match_l = match_l[order]

        # 我第一看到這里的時候感覺是很不對的,直覺上來說應該是np.sum,而不是np.cumsum,但是這里的指標是AP(下面博文會介紹)
        # 這里按照得分降序排列計算出tp和fp,是為了計算PR曲線和AP。
        tp = np.cumsum(match_l == 1)
        fp = np.cumsum(match_l == 0)

        # 如果fp+tp == 0 ,那么prec[l] = nan
        prec[l] = tp / (fp + tp)
        # 如果n_pos[l] <= 0,那么rec[l] = None
        if n_pos[l] > 0:
            rec[l] = tp / n_pos[l]

    return prec, rec
calc_detection_voc_ap
def calc_detection_voc_ap(prec, rec, use_07_metric=False):
    # 初始化記錄的變量
    n_fg_class = len(prec)
    ap = np.empty(n_fg_class)
    for l in six.moves.range(n_fg_class):
        # 提前做成判斷,防止程序報錯
        if prec[l] is None or rec[l] is None:
            ap[l] = np.nan
            continue

        if use_07_metric:  # 是否使用VOC2007計算方法(下面博文會進行介紹)
            # 11 point metric
            ap[l] = 0
            for t in np.arange(0., 1.1, 0.1):
                if np.sum(rec[l] >= t) == 0:
                    p = 0
                else:
                    p = np.max(np.nan_to_num(prec[l])[rec[l] >= t])
                ap[l] += p / 11
        else:  # VOC 2007以后的方法(下面博文會進行介紹)
            mpre = np.concatenate(([0], np.nan_to_num(prec[l]), [0]))
            mrec = np.concatenate(([0], rec[l], [1]))
            mpre = np.maximum.accumulate(mpre[::-1])[::-1]
            i = np.where(mrec[1:] != mrec[:-1])[0]
            ap[l] = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])

    return ap

三個函數之間的關系:eval_detection_voc將calc_detection_voc_prec_rec的結果傳遞給calc_detection_voc_ap獲得每個種類的AP和mAP。

AP和mAP

AP:Precision-Recall曲線下的面積
mAP:各類別AP的均值
\(AP = \int_0^1p(r)dr\),但是一般情況下是采用通過采樣recall值使用分段矩形的面積近似代替積分值,這里就存在着兩種方法

計算方法

  • VOC2007以前:只需要選取當Recall >= 0, 0.1, 0.2, ..., 1共11個點時的Precision最大值,然后AP就是這11個Precision的平均值。
  • VOC2007以后:需要針對每一個不同的Recall值(包括0和1),選取其大於等於這些Recall值時的Precision最大值,然后計算PR曲線下面積(通過采樣點計算各個矩形面積之和)作為AP值。

栗子

(本示例來自於參考資料第3條)
假設,對於Aeroplane類別,我們網絡有以下輸出(BB表示BoundingBox序號,IoU>0.5時GT=1):

BB confidence GT
BB1 0.9 1
BB2 0.9 1
BB1 0.8 1
BB3 0.7 0
BB4 0.7 0
BB5 0.7 1
BB6 0.7 0
BB7 0.7 0
BB8 0.7 1
BB9 0.7 1

因此,我們有 TP=5 (BB1, BB2, BB5, BB8, BB9), FP=5 (重復檢測到的BB1也算FP)。除了表里檢測到的5個GT以外,我們還有2個GT沒被檢測到,因此: FN = 2. 這時我們就可以按照Confidence的順序給出各處的PR值,如下

rank precision recall
1 1.00 0.14
2 1.00 0.29
3 0.66 0.29
4 0.50 0.29
5 0.40 0.29
6 0.50 0.43
7 0.43 0.43
8 0.38 0.43
9 0.44 0.57
10 0.50 0.71

對於上述PR值
1.如果我們采用:VOC2010之前的方法,我們選取Recall >= 0, 0.1, ..., 1的11處Percision的最大值:1, 1, 1, 0.5, 0.5, 0.5, 0.5, 0.5, 0, 0, 0。此時Aeroplane類別的 AP = 5.5 / 11 = 0.5
2.VOC2010及以后的方法,對於Recall >= 0, 0.14, 0.29, 0.43, 0.57, 0.71, 1,我們選取此時Percision的最大值:1, 1, 1, 0.5, 0.5, 0.5, 0。此時Aeroplane類別的 AP = (0.14-0)*1 + (0.29-0.14)*1 + (0.43-0.29)*0.5 + (0.57-0.43)*0.5 + (0.71-0.57)*0.5 + (1-0.71)*0 = 0.5

./model/utils/bbox_tool.py

loc2bbox
def loc2bbox(src_bbox, loc):
    '''
    通過一開始定義的scr_bbox和變形比例loc,求出變形轉換后的dst_bbox
    '''
    if src_bbox.shape[0] == 0:
        return xp.zeros((0, 4), dtype=loc.dtype)
    
    # 統一數據格式
    src_bbox = src_bbox.astype(src_bbox.dtype, copy=False)
    # 在我上一篇博客中介紹了bounding box的回歸可以參考一下
    # 變形比例主要是在x,y,h,w進行轉換,所以先求出原始bbox的x,y,w,h
    src_height = src_bbox[:, 2] - src_bbox[:, 0]
    src_width = src_bbox[:, 3] - src_bbox[:, 1]
    # 標記為了中心坐標
    src_ctr_y = src_bbox[:, 0] + 0.5 * src_height
    src_ctr_x = src_bbox[:, 1] + 0.5 * src_width
    
    # 分隔不同的變形比例
    dy = loc[:, 0::4]   
    dx = loc[:, 1::4]
    dh = loc[:, 2::4]
    dw = loc[:, 3::4]
    # 插入x.newaxis變成列向量
    # 變形轉換
    ctr_y = dy * src_height[:, xp.newaxis] + src_ctr_y[:, xp.newaxis]
    ctr_x = dx * src_width[:, xp.newaxis] + src_ctr_x[:, xp.newaxis]
    h = xp.exp(dh) * src_height[:, xp.newaxis]
    w = xp.exp(dw) * src_width[:, xp.newaxis]
    
    # 轉換為bbox的形式
    dst_bbox = xp.zeros(loc.shape, dtype=loc.dtype)
    dst_bbox[:, 0::4] = ctr_y - 0.5 * h
    dst_bbox[:, 1::4] = ctr_x - 0.5 * w
    dst_bbox[:, 2::4] = ctr_y + 0.5 * h
    dst_bbox[:, 3::4] = ctr_x + 0.5 * w
    return dst_bbox
bbox2loc
def bbox2loc(src_bbox, dst_bbox):
    '''
    通過原始bbox和已經存在的bbox(一般是ground truth),求出src_bbox到dst_bbox的變形比例
    一般用於損失函數的計算
    '''
    # src_bbox (x,y,h,w)
    height = src_bbox[:, 2] - src_bbox[:, 0]
    width = src_bbox[:, 3] - src_bbox[:, 1]
    ctr_y = src_bbox[:, 0] + 0.5 * height
    ctr_x = src_bbox[:, 1] + 0.5 * width
    # ground truth (x,y,h,w)
    base_height = dst_bbox[:, 2] - dst_bbox[:, 0]
    base_width = dst_bbox[:, 3] - dst_bbox[:, 1]
    base_ctr_y = dst_bbox[:, 0] + 0.5 * base_height
    base_ctr_x = dst_bbox[:, 1] + 0.5 * base_width

    eps = xp.finfo(height.dtype).eps
    # eps是獲取非負的最小值,防止除以0
    height = xp.maximum(height, eps)
    width = xp.maximum(width, eps)

    dy = (base_ctr_y - ctr_y) / height
    dx = (base_ctr_x - ctr_x) / width
    dh = xp.log(base_height / height)
    dw = xp.log(base_width / width)

    loc = xp.vstack((dy, dx, dh, dw)).transpose()
    return loc
bbox_iou
def bbox_iou(bbox_a, bbox_b):
    '''
    計算兩組bboxes的之間的IoU,bbox_a.shape = (N,4),bbox_b.shape = (M,4)
    生成的IoU矩陣.shape = (N,M)
    '''
    if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
        raise IndexError

    # 左下角
    tl = xp.maximum(bbox_a[:, None, :2], bbox_b[:, :2])
    # 右上角
    br = xp.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:])
    # (tl<br)是為了保證兩個bbox是有相交部分的,無相交的部分的就是0
    area_i = xp.prod(br - tl, axis=2) * (tl < br).all(axis=2)
    area_a = xp.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)
    area_b = xp.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1)
    return area_i / (area_a[:, None] + area_b - area_i)
generate_anchor_base
def generate_anchor_base(base_size=16, ratios=[0.5, 1, 2],
                         anchor_scales=[8, 16, 32]):
    '''
    這是產生基礎的anchor box的函數
    基礎指的是這里anchor box不與任何圖片有關,只是基於batch_size大小區域和坐標原點為(0,0)的生成的anchor box
    具體我自己的理解會在下面博文中闡述
    '''
    # 這里表示batch_size大小區域的中心點
    py = base_size / 2.
    px = base_size / 2.

    anchor_base = np.zeros((len(ratios) * len(anchor_scales), 4),
                           dtype=np.float32)
    for i in six.moves.range(len(ratios)):
        for j in six.moves.range(len(anchor_scales)):
            # 存在9中不同的anchor box
            h = base_size * anchor_scales[j] * np.sqrt(ratios[i])
            w = base_size * anchor_scales[j] * np.sqrt(1. / ratios[i])
            # 轉換成標准的bbox格式,(ymin,xmin,ymax,xmax)
            index = i * len(anchor_scales) + j
            anchor_base[index, 0] = py - h / 2.
            anchor_base[index, 1] = px - w / 2.
            anchor_base[index, 2] = py + h / 2.
            anchor_base[index, 3] = px + w / 2.
    return anchor_base
    # 這里雖然產生了bbox,但是是基於坐標原點為(0,0)產生的9個不同形狀的anchor box,是base
    # 在之后的使用中必須加上bbox的偏移量才能正確的表示候選框
    # 偏移量是因為不同像素點除了左上角的一個以外都不是以(0,0)坐標原點繪制候選框的

anchor base

(我寫的注釋都是我自己的理解,可能會有些抽象,寫下來方便自己以后回顧記憶)

左邊的圖表示在genenrate_anchor_base函數中生成的anchor,是基於以(0,0)為坐標原點的生成的,但是在任意區域就需要進行偏移。

_enumerate_shifted_anchor_torch

這個函數在./model/region_proposal_network.py中(來看看在這份代碼中是如何體現偏移的)

def _enumerate_shifted_anchor_torch(anchor_base, feat_stride, height, width):
    # Enumerate all shifted anchors:
    #
    # add A anchors (1, A, 4) to
    # cell K shifts (K, 1, 4) to get
    # shift anchors (K, A, 4)
    # reshape to (K*A, 4) shifted anchors
    # return (K*A, 4)

    # !TODO: add support for torch.CudaTensor
    # xp = cuda.get_array_module(anchor_base)
    import torch as t
    shift_y = t.arange(0, height * feat_stride, feat_stride)
    shift_x = t.arange(0, width * feat_stride, feat_stride)
    shift_x, shift_y = xp.meshgrid(shift_x, shift_y)
    shift = xp.stack((shift_y.ravel(), shift_x.ravel(),
                      shift_y.ravel(), shift_x.ravel()), axis=1)
    A = anchor_base.shape[0]
    K = shift.shape[0]
    anchor = anchor_base.reshape((1, A, 4)) + \
             shift.reshape((1, K, 4)).transpose((1, 0, 2))
    anchor = anchor.reshape((K * A, 4)).astype(np.float32)
    return anchor

用一張圖來介紹一下這個函數的作用:

進行卷積和pooling運算后,特征圖變小了,但是anchor是原圖上的體現,即(ymin,xmin,ymax,xmax)都是在原圖上的坐標,那么在特征圖上的每一個像素點感受野的左上角坐標便是根據這個像素點生成anchor box所需要基於的坐標(也就是該像素點anchor base 的偏移量,似乎有點繞)。反正特征圖上每個像素點的anchor base都需要偏移,而且偏移的距離是該像素點感受野的左上角坐標。

代碼中變量shift就是偏移量, anchor = anchor_base.reshape((1, A, 4)) + shift.reshape((1, K, 4)).transpose((1, 0, 2))便是讓每個像素點上的anchor base偏移通過這個像素點計算出來的偏移量。假設特征圖是(C,H,W),N = H*W,那么anchor是(K = N*A,4)。

然后偏移量的計算也可以用一張圖來表示:(偏移16是因為特征圖相比原圖縮小了16倍,這里我是不理解的,因為我認為原圖與特征圖之間的映射是復雜的,具體可以見我上一篇博客)

shift_x,shift_y就是為了生成圖中紅藍以及省略號兩個矩陣,shift表示每個像素點的偏移量矩陣,為什么是四維的,我認為是(ymin,xmin,ymax,xmax)的原因,也就是說每個偏移量用四個來表示,而不是我圖中說的兩個就可以了。
最后anchor_base和shift相加生成最終的anchor。最終結果:

./model/utils/creator_tools.py

ProposalTargetCreator
class ProposalTargetCreator(object):
    def __init__(self,
                 n_sample=128,
                 pos_ratio=0.25, pos_iou_thresh=0.5,
                 neg_iou_thresh_hi=0.5, neg_iou_thresh_lo=0.0
                 ):
        self.n_sample = n_sample  # 生成樣本的數量
        self.pos_ratio = pos_ratio  # 正樣本比例
        self.pos_iou_thresh = pos_iou_thresh  # 將一個ROI划分為正樣本的IoU閾值
        self.neg_iou_thresh_hi = neg_iou_thresh_hi  # 將一個ROI划分為負樣本的IoU最大值
        self.neg_iou_thresh_lo = neg_iou_thresh_lo  # 將一個ROI划分為負樣本的IoU最小值,但是faster-rcnn中默認是0.1

    def __call__(self, roi, bbox, label,
                 loc_normalize_mean=(0., 0., 0., 0.),
                 loc_normalize_std=(0.1, 0.1, 0.2, 0.2)):
        '''
        給定ROIs和bboxes(ground truth),以及bbox的label生成數量合適的正負樣本
        正樣本指的是和其中一個ground truth的IoU大於閾值的ROIs,或者是ground truth也可以,因為正樣本比較少
        負樣本指的是和所有ground truth的IoU都在neg_iou_thresh_hi和neg_iou_thresh_lo之間
        '''
        n_bbox, _ = bbox.shape

        # 新的ROI由原來的ROIs和ground truth組成,將ground truth 看成正樣本
        roi = np.concatenate((roi, bbox), axis=0)
        # 每張圖片上正樣本的數量
        pos_roi_per_image = np.round(self.n_sample * self.pos_ratio)
        # iou矩陣下半部分是對角陣
        iou = bbox_iou(roi, bbox)
        # 獲得每個ROI對應的ground truth,原來的ground truth便是對應自己
        gt_assignment = iou.argmax(axis=1)
        max_iou = iou.max(axis=1)
        # label偏移 [0, n_fg_class - 1] to [1, n_fg_class].
        # 0表示背景,獲取label,最靠近哪個ground truth,便是哪個ground truth的標簽
        gt_roi_label = label[gt_assignment] + 1

        # 找到正樣本的索引
        pos_index = np.where(max_iou >= self.pos_iou_thresh)[0]
        # 最多構成pos_roi_per_image個正樣本
        pos_roi_per_this_image = int(min(pos_roi_per_image, pos_index.size))
        # 在正樣本中隨機選取和排列
        if pos_index.size > 0:
            pos_index = np.random.choice(
                pos_index, size=pos_roi_per_this_image, replace=False)

        # 找到負樣本的索引
        neg_index = np.where((max_iou < self.neg_iou_thresh_hi) &
                             (max_iou >= self.neg_iou_thresh_lo))[0]
        # 下面和正樣本采用同樣的方式生成負樣本
        neg_roi_per_this_image = self.n_sample - pos_roi_per_this_image
        neg_roi_per_this_image = int(min(neg_roi_per_this_image,
                                         neg_index.size))
        if neg_index.size > 0:
            neg_index = np.random.choice(
                neg_index, size=neg_roi_per_this_image, replace=False)

        # 獲取正負樣本label和ROI
        keep_index = np.append(pos_index, neg_index)
        gt_roi_label = gt_roi_label[keep_index]
        gt_roi_label[pos_roi_per_this_image:] = 0  # negative labels --> 0
        sample_roi = roi[keep_index]

        # 每個樣本與最合適的ground truth計算變形比例,gt_assignment[keep_index]就是每個樣本對應的ground truth索引
        gt_roi_loc = bbox2loc(sample_roi, bbox[gt_assignment[keep_index]])
        # 因為輸出的結果需要放入網絡中訓練,所以對其標准化
        gt_roi_loc = ((gt_roi_loc - np.array(loc_normalize_mean, np.float32)
                       ) / np.array(loc_normalize_std, np.float32))

        return sample_roi, gt_roi_loc, gt_roi_label
AnchorTargetCreator
class AnchorTargetCreator(object):
    def __init__(self,
                 n_sample=256,
                 pos_iou_thresh=0.7, neg_iou_thresh=0.3,
                 pos_ratio=0.5):
        # 返回ROI的數量,也可以說是anchor box的數量,anchor和ROI在數量上是等價的
        # anchor + loc = ROI
        self.n_sample = n_sample  
        self.pos_iou_thresh = pos_iou_thresh  # 將anchor視作正樣本的IoU閾值
        self.neg_iou_thresh = neg_iou_thresh  # 將anchor視作負樣本的IoU閾值
        self.pos_ratio = pos_ratio  # 正樣本的比例

    def __call__(self, bbox, anchor, img_size):
        '''
        給定bbox(ground truth)和anchor,返回每個anchor的label和變形比例loc
        這是為了RPN網絡的訓練,生成的每個anchor的label和loc,在RPN網絡中也會預測一份,兩者便可以計算損失
        '''
        img_H, img_W = img_size  # 圖片的大小

        n_anchor = len(anchor)
        inside_index = _get_inside_index(anchor, img_H, img_W)  # 獲得整個anchor完全在img里面的anchor index
        anchor = anchor[inside_index]
        argmax_ious, label = self._create_label(
            inside_index, anchor, bbox)  # 獲得inside_anchor的label和與之最接近的ground truth索引

        # 獲得inside_anchor的loc
        loc = bbox2loc(anchor, bbox[argmax_ious])

        # 補充完善,獲得所有anchor的變形比例和二分類標簽
        label = _unmap(label, n_anchor, inside_index, fill=-1)  # outside_index的label == -1
        loc = _unmap(loc, n_anchor, inside_index, fill=0)  # outside_index的loc == (0,0,0,0)
        return loc, label

    def _create_label(self, inside_index, anchor, bbox):
        # label: 1 is positive, 0 is negative, -1 is dont care
        label = np.empty((len(inside_index),), dtype=np.int32)
        label.fill(-1)

        argmax_ious, max_ious, gt_argmax_ious = \
            self._calc_ious(anchor, bbox, inside_index)

        # 如果anchor與ground truth 的最大iou都小於self.neg_iou_thresh那么該anchor是負樣本
        label[max_ious < self.neg_iou_thresh] = 0

        # 每個ground truth都有一個IoU最大的anchor,這個anchor為正樣本
        label[gt_argmax_ious] = 1

        # 各個anchor的最大重疊度大於閾值的置為正樣本
        label[max_ious >= self.pos_iou_thresh] = 1

        # 正樣本的數量
        n_pos = int(self.pos_ratio * self.n_sample)
        pos_index = np.where(label == 1)[0]
        if len(pos_index) > n_pos:
            disable_index = np.random.choice(
                pos_index, size=(len(pos_index) - n_pos), replace=False)
            label[disable_index] = -1
        # 只保留n_pos個正樣本

        # 正樣本的數量
        n_neg = self.n_sample - np.sum(label == 1)
        neg_index = np.where(label == 0)[0]
        if len(neg_index) > n_neg:  # 按照道理因為存在label == -1的數量>=0,所以這個if應該不可能成立
            disable_index = np.random.choice(
                neg_index, size=(len(neg_index) - n_neg), replace=False)
            label[disable_index] = -1
        # 保證n_neg個負樣本
        # 返回:與每個anchor 重疊度最高的ground truth,每個anchor的label
        return argmax_ious, label  

    def _calc_ious(self, anchor, bbox, inside_index):
        # ious between the anchors and the gt boxes
        ious = bbox_iou(anchor, bbox)
        argmax_ious = ious.argmax(axis=1)  # 求出與anchor 重疊度最高的ground truth
        max_ious = ious[np.arange(len(inside_index)), argmax_ious]  # 求出與anchor 重疊度最高的ground truth 之間的iou
        gt_argmax_ious = ious.argmax(axis=0)
        gt_max_ious = ious[gt_argmax_ious, np.arange(ious.shape[1])]
        gt_argmax_ious = np.where(ious == gt_max_ious)[0]  # 求出與每個ground truth 重疊度最高的anchor

        return argmax_ious, max_ious, gt_argmax_ious


def _unmap(data, count, index, fill=0):
    # count里面index以外的補充為fill,其他為data
    if len(data.shape) == 1:
        ret = np.empty((count,), dtype=data.dtype)
        ret.fill(fill)
        ret[index] = data
    else:
        ret = np.empty((count,) + data.shape[1:], dtype=data.dtype)
        ret.fill(fill)
        ret[index, :] = data
    return ret


def _get_inside_index(anchor, H, W):
    # 返回完全在H,W(圖片的大小)里面的anchor索引
    index_inside = np.where(
        (anchor[:, 0] >= 0) &
        (anchor[:, 1] >= 0) &
        (anchor[:, 2] <= H) &
        (anchor[:, 3] <= W)
    )[0]
    return index_inside
ProposalCreator
class ProposalCreator:
    def __init__(self,
                 parent_model,
                 nms_thresh=0.7,
                 n_train_pre_nms=12000,
                 n_train_post_nms=2000,
                 n_test_pre_nms=6000,
                 n_test_post_nms=300,
                 min_size=16
                 ):
        self.parent_model = parent_model  # 傳過來的model,用來表示train或者test,使用不同的nms閾值
        self.nms_thresh = nms_thresh
        self.n_train_pre_nms = n_train_pre_nms
        self.n_train_post_nms = n_train_post_nms
        self.n_test_pre_nms = n_test_pre_nms
        self.n_test_post_nms = n_test_post_nms
        self.min_size = min_size  # 候選框的最小大小,太小的就選擇丟棄

    def __call__(self, loc, score,
                 anchor, img_size, scale=1.):
        '''
        給定anchor,loc,score,主要是對anchor進行處理、非極大抑制和選擇,返回通過RPN網絡選擇的ROIs
        '''
        # 通過父網絡判斷網絡模式
        if self.parent_model.training:
            n_pre_nms = self.n_train_pre_nms
            n_post_nms = self.n_train_post_nms
        else:
            n_pre_nms = self.n_test_pre_nms
            n_post_nms = self.n_test_post_nms

        # 生成ROI
        roi = loc2bbox(anchor, loc)

        # 對ROI進行修剪,保證在圖片范圍內
        roi[:, slice(0, 4, 2)] = np.clip(
            roi[:, slice(0, 4, 2)], 0, img_size[0])
        roi[:, slice(1, 4, 2)] = np.clip(
            roi[:, slice(1, 4, 2)], 0, img_size[1])

        # 拋棄較小的候選框
        min_size = self.min_size * scale
        hs = roi[:, 2] - roi[:, 0]
        ws = roi[:, 3] - roi[:, 1]
        keep = np.where((hs >= min_size) & (ws >= min_size))[0]
        roi = roi[keep, :]
        score = score[keep]

        # 對每個ROI的score降序排列,同時選擇n_pre_nms個
        order = score.ravel().argsort()[::-1]
        if n_pre_nms > 0:
            order = order[:n_pre_nms]
        roi = roi[order, :]
        score = score[order]

        # 非極大抑制,按照得分降序排列返回剩下的ROI索引
        keep = nms(
            torch.from_numpy(roi).cuda(),
            torch.from_numpy(score).cuda(),
            self.nms_thresh)
        if n_post_nms > 0:  # 控制數量
            keep = keep[:n_post_nms]
        roi = roi[keep.cpu().numpy()]
        return roi
這三種creator重點在於搞清楚在網絡中的位置,這樣子比較容易理解。然后還有一點這里面雖然是在網絡結構中,但是不是網絡,只能說是網絡的組件,里面都是不可以學習參數。

參考博客和資料

謝謝各位大佬🎉🎉🎉
同一個博主我貼上了一份資料,其他資料地址同源

人生此處、絕對樂觀


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM