Faster RCNN 學習與實現

Faster R-CNN 主要分爲兩個部分:

  • RPN(Region Proposal Network)生成高質量的 region proposal;
  • Fast R-CNN 利用 region proposal 做出檢測。

在論文中作者將 RPN 比作神經網絡的注意力機制("attention" mechanisms),告訴網絡看哪裏。爲了更好的理解,下面簡要的敘述論文的關鍵內容。

RPN

  • Input:任意尺寸的圖像
  • Output:一組帶有目標得分的目標矩形 proposals

爲了生成 region proposals,在基網絡的最後一個卷積層 x 上滑動一個小網絡。該小網絡由一個 \(3\times 3\) 卷積 conv1 和一對兄弟卷積(並行的)\(1\times 1\) 卷積 regcls 組成。其中,conv1 的參數 padding=1stride=1 以保證其不會改變輸出的特徵圖的尺寸。reg 作爲 box-regression 用來編碼 box 的座標,cls 作爲 box-classifaction 用來編碼每個 proposal 是目標的概率。詳細內容見我的博客:我的目標檢測筆記。論文中把不同 scale 和 aspect ratio 的 \(k\) 個 reference boxes(參數化的 proposal) 稱作 anchors(錨點)。錨點是滑塊的中心。

爲了更好的理解 anchors,下面以 Python 來展示其內涵。

錨點

首先利用COCO 數據集的使用中介紹的 API 來獲取一張 COCO 數據集的圖片及其標註。

先載入一些必備的包:

import cv2
from matplotlib import pyplot as plt
import numpy as np

# 載入 coco 相關 api
import sys
sys.path.append(r'D:\API\cocoapi\PythonAPI')
from pycocotools.dataset import Loader
%matplotlib inline

利用 Loader 載入 val2017 數據集,並選擇包含 'cat', 'dog', 'person' 的圖片:

dataType = 'val2017'
root = 'E:/Data/coco'
catNms = ['cat', 'dog', 'person']
annType = 'annotations_trainval2017'
loader = Loader(dataType, catNms, root, annType)

輸出結果:

Loading json in memory ...
used time: 0.762376 s
Loading json in memory ...
creating index...
index created!
used time: 0.401951 s

可以看出,Loader 載入數據的速度很快。爲了更加詳細的查看 loader,下面打印出現一些相關信息:

print(f'總共包含圖片 {len(loader)} 張')
for i, ann in enumerate(loader.images):
    w, h = ann['height'], ann['width']
    print(f'第 {i+1} 張圖片的高和寬分別爲: {w, h}')

顯示:

總共包含圖片 2 張
第 1 張圖片的高和寬分別爲: (612, 612)
第 2 張圖片的高和寬分別爲: (500, 333)

下面以第 1 張圖片爲例來探討 anchors。先可視化:

img, labels = loader[0]
plt.imshow(img);

輸出:

爲了讓特徵圖的尺寸大一點,可以將其 resize 爲 (800, 800, 3):

img = cv2.resize(img, (800, 800))
print(img.shape)

輸出:

(800, 800, 3)

下面藉助 MXNet 來完成接下來的代碼編程,爲了適配 MXNet 需要將圖片由 (h, w, 3) 轉換爲 (3, w, h) 形式。

img = img.transpose(2, 1, 0)
print(img.shape)

輸出:

(3, 800, 800)

由於卷積神經網絡的輸入是四維數據,故而,還需要:

img = np.expand_dims(img, 0)
print(img.shape)

輸出

(1, 3, 800, 800)

爲了和論文一致,我們也採用 VGG16 網絡(載入 gluoncv中的權重):

from gluoncv.model_zoo import vgg16
net = vgg16(pretrained=True)  #  載入權重

僅僅考慮直至最後一層卷積層(去除池化層)的網絡,下面查看網絡的各個卷積層的輸出情況:

from mxnet import nd
imgs = nd.array(img)  # 轉換爲 mxnet 的數據類型
x = imgs
for layer in net.features[:29]:
    x = layer(x)
    if "conv" in layer.name:
        print(layer.name, x.shape) # 輸出該卷積層的 shape

結果爲:

vgg0_conv0 (1, 64, 800, 800)
vgg0_conv1 (1, 64, 800, 800)
vgg0_conv2 (1, 128, 400, 400)
vgg0_conv3 (1, 128, 400, 400)
vgg0_conv4 (1, 256, 200, 200)
vgg0_conv5 (1, 256, 200, 200)
vgg0_conv6 (1, 256, 200, 200)
vgg0_conv7 (1, 512, 100, 100)
vgg0_conv8 (1, 512, 100, 100)
vgg0_conv9 (1, 512, 100, 100)
vgg0_conv10 (1, 512, 50, 50)
vgg0_conv11 (1, 512, 50, 50)
vgg0_conv12 (1, 512, 50, 50)

由此,可以看出尺寸爲 (800, 800) 的原圖變爲了 (50, 50) 的特徵圖(比原來縮小了 16 倍)。

感受野

上面的 16 不僅僅是針對尺寸爲 (800, 800),它適用於任意尺寸的圖片,因爲 16 是特徵圖的一個像素點的感受野(receptive field )。

感受野的大小是如何計算的?我們回憶卷積運算的過程,便可發現感受野的計算恰恰是卷積計算的逆過程(參考感受野計算1)。

\(F_k, S_k, P_k\) 分別表示第 \(k\) 層的卷積核的高(或者寬)、移動步長(stride)、Padding 個數;記 \(i_k\) 表示第 \(k\) 層的輸出特徵圖的高(或者寬)。這樣,很容易得出如下遞推公式:

\[ i_{k+1} = \lfloor \frac{i_{k}-F_{k}+2P_{k}}{s_{k}}\rfloor + 1 \]

其中 \(k \in \{1, 2, \cdots\}\),且 \(i_0\) 表示原圖的高或者寬。令 \(t_k = \frac{F_k - 1}{2} - P_k\),上式可以轉換爲

\[ (i_{k-1} - 1) = (i_{k} - 1) S_k + 2t_k \]

反推感受野, 令 \(i_1 = F_1\), 且\(t_k = \frac{F_k -1}{2} - P_k\), 且 \(1\leq j \leq L\), 則有

\[ i_0 = (i_L - 1)\alpha_L + \beta_L \]

其中 \(\alpha_L = \prod_{p=1}^{L}S_p\),且有:

\[ \beta_L = 1 + 2\sum_{p=1}^L (\prod_{q=1}^{p-1}S_q) t_p \]

由於 VGG16 的卷積核的配置均是 kernel_size=(3, 3), padding=(1, 1),同時只有在經過池化層才使得 \(S_j = 2\),故而 \(\beta_j = 0\),且有 \(\alpha_L = 2^4 = 16\)

錨點的計算

在編程實現的時候,將感受野的大小使用 base_size 來表示。下面我們討論如何生成錨框?爲了計算的方便,先定義一個 Box

import numpy as np


class Box:
    '''
    corner: (xmin,ymin,xmax,ymax)
    '''

    def __init__(self, corner):
        self._corner = corner

    @property
    def corner(self):
        return self._corner

    @corner.setter
    def corner(self, new_corner):
        self._corner = new_corner

    @property
    def w(self):
        '''
        計算 bbox 的 寬
        '''
        return self.corner[2] - self.corner[0] + 1

    @property
    def h(self):
        '''
        計算 bbox 的 高
        '''
        return self.corner[3] - self.corner[1] + 1

    @property
    def area(self):
        '''
        計算 bbox 的 面積
        '''
        return self.w * self.h

    @property
    def whctrs(self):
        '''
        計算 bbox 的 中心座標
        '''
        xctr = self.corner[0] + (self.w - 1) * .5
        yctr = self.corner[1] + (self.h - 1) * .5
        return xctr, yctr

    def __and__(self, other):
        '''
        運算符:&,實現兩個 box 的交集運算
        '''
        U = np.array([self.corner, other.corner])
        xmin, ymin, xmax, ymax = np.split(U, 4, axis=1)
        w = xmax.min() - xmin.max()
        h = ymax.min() - ymin.max()
        return w * h

    def __or__(self, other):
        '''
        運算符:|,實現兩個 box 的並集運算
        '''
        I = self & other
        return self.area + other.area - I

    def IoU(self, other):
        '''
        計算 IoU
        '''
        I = self & other
        U = self | other
        return I / U

類 Box 實現了 bbox 的交集、並集運算以及 IoU 的計算。下面舉一個例子來說明:

bbox = [0, 0, 15, 15]  # 邊界框
bbox1 = [5, 5, 12, 12] # 邊界框
A = Box(bbox)  # 一個 bbox 實例
B = Box(bbox1) # 一個 bbox 實例

下面便可以輸出 A 與 B 的高寬、中心、面積、交集、並集、Iou:

print('A 與 B 的交集', str(A & B))
print('A 與 B 的並集', str(A | B))
print('A 與 B 的 IoU', str(A.IoU(B)))
print(u'A 的中心、高、寬以及面積', str(A.whctrs), A.h, A.w, A.area)

輸出結果:

A 與 B 的交集 49
A 與 B 的並集 271
A 與 B 的 IoU 0.18081180811808117
A 的中心、高、寬以及面積 (7.5, 7.5) 16 16 256

下面重新考慮 loader。首先定義一個轉換函數:

def getX(img):
    # 將 img (h, w, 3) 轉換爲 (1, 3, w, h)
    img = img.transpose((2, 1, 0))
    return np.expand_dims(img, 0)

函數 getX 將圖片由 (h, w, 3) 轉換爲 (1, 3, w, h):

img, label = loader[0]
img = cv2.resize(img, (800, 800)) # resize 爲 800 x 800
X = getX(img)     # 轉換爲 (1, 3, w, h)
img.shape, X.shape

輸出結果:

((800, 800, 3), (1, 3, 800, 800))

與此同時,獲取特徵圖的數據:

features = net.features[:29]
F = features(imgs)
F.shape

輸出:

(1, 512, 50, 50)

接着需要考慮如何將特徵圖 F 映射回原圖?

全卷積(FCN):將特徵圖映射回原圖

faster R-CNN 中的 FCN 僅僅是有着 FCN 的特性,並不是真正意義上的卷積。faster R-CNN 僅僅是借用了 FCN 的思想來實現將特徵圖映射回原圖的目的,同時將輸出許多錨框。

特徵圖上的 1 個像素點的感受野爲 \(16\times 16\),換言之,特徵圖上的錨點映射回原圖的感受區域爲 \(16 \times 16\),論文稱其爲 reference box。下面相對於 reference box 依據不同的尺度與高寬比例來生成不同的錨框。

base_size = 2**4  # 特徵圖的每個像素的感受野大小
scales = [8, 16, 32]  # 錨框相對於 reference box 的尺度
ratios = [0.5, 1, 2]  # reference box 與錨框的高寬的比率(aspect ratios)

其實 reference box 也對應於論文描述的 window(滑動窗口),這個之後再解釋。我們先看看 scales 與 ratios 的具體含義。

爲了更加一般化,假設 reference box 圖片高寬分別爲 \(h, w\),而錨框的高寬分別爲 \(h_1, w_1\),形式化 scales 與 ratios 爲公式 1:

\[ \begin{cases} \frac{w_1 h_1}{wh} = s^2\\ \frac{h_1}{w_1} = \frac{h}{w} r \Rightarrow \frac{h_1}{h} = \frac{w_1}{w} r \end{cases} \]

可以將上式轉換爲公式 2:

\[ \begin{cases} \frac{w_1}{w} = \frac{s}{\sqrt{r}}\\ \frac{h_1}{h} = \frac{w_1}{w} r = s \sqrt{r} \end{cases} \]

同樣可以轉換爲公式3:

\[ \begin{cases} w_s = \frac{w_1}{s} = \frac{w}{\sqrt{r}}\\ h_s = \frac{h_1}{s} = h \sqrt{r} \end{cases} \]

基於公式 2 與公式 3 均可以很容易計算出 \(w_1,h_1\). 一般地,\(w=h\),公式 3 亦可以轉換爲公式 4:

\[ \begin{cases} w_s = \sqrt{\frac{wh}{r}}\\ h_s = w_s r \end{cases} \]

gluoncv 結合公式 4 來編程,本文依據 3 進行編程。無論原圖的尺寸如何,特徵圖的左上角第一個錨點映射回原圖後的 reference box 的 bbox = (xmain, ymin, xmax, ymax) 均爲 (0, 0, bas_size-1, base_size-1),爲了方便稱呼,我們稱其爲 base_reference box。基於 base_reference box 依據不同的 s 與 r 的組合生成不同尺度和高寬比的錨框,且稱其爲 base_anchors。編程實現:

class MultiBox(Box):
    def __init__(self, base_size, ratios, scales):
        if not base_size:
            raise ValueError("Invalid base_size: {}.".format(base_size))
        if not isinstance(ratios, (tuple, list)):
            ratios = [ratios]
        if not isinstance(scales, (tuple, list)):
            scales = [scales]
        super().__init__([0]*2+[base_size-1]*2)  # 特徵圖的每個像素的感受野大小爲 base_size
        # reference box 與錨框的高寬的比率(aspect ratios)
        self._ratios = np.array(ratios)[:, None]
        self._scales = np.array(scales)     # 錨框相對於 reference box 的尺度

    @property
    def base_anchors(self):
        ws = np.round(self.w / np.sqrt(self._ratios))
        w = ws * self._scales
        h = w * self._ratios
        wh = np.stack([w.flatten(), h.flatten()], axis=1)
        wh = (wh - 1) * .5
        return np.concatenate([self.whctrs - wh, self.whctrs + wh], axis=1)

    def _generate_anchors(self, stride, alloc_size):
        # propagete to all locations by shifting offsets
        height, width = alloc_size  # 特徵圖的尺寸
        offset_x = np.arange(0, width * stride, stride)
        offset_y = np.arange(0, height * stride, stride)
        offset_x, offset_y = np.meshgrid(offset_x, offset_y)
        offsets = np.stack((offset_x.ravel(), offset_y.ravel(),
                            offset_x.ravel(), offset_y.ravel()), axis=1)
        # broadcast_add (1, N, 4) + (M, 1, 4)
        anchors = (self.base_anchors.reshape(
            (1, -1, 4)) + offsets.reshape((-1, 1, 4)))
        anchors = anchors.reshape((1, 1, height, width, -1)).astype(np.float32)
        return anchors

下面看看具體效果:

base_size = 2**4  # 特徵圖的每個像素的感受野大小
scales = [8, 16, 32]  # 錨框相對於 reference box 的尺度
ratios = [0.5, 1, 2]  # reference box 與錨框的高寬的比率(aspect ratios)
A = MultiBox(base_size,ratios, scales)
A.base_anchors

輸出結果:

array([[ -84.,  -38.,   99.,   53.],
       [-176.,  -84.,  191.,   99.],
       [-360., -176.,  375.,  191.],
       [ -56.,  -56.,   71.,   71.],
       [-120., -120.,  135.,  135.],
       [-248., -248.,  263.,  263.],
       [ -36.,  -80.,   51.,   95.],
       [ -80., -168.,   95.,  183.],
       [-168., -344.,  183.,  359.]])

接着考慮將 base_anchors 在整個原圖上進行滑動。比如,特徵圖的尺寸爲 (5, 5) 而感受野的大小爲 50,則 base_reference box 在原圖滑動的情況(移動步長爲 50)如下圖:

x, y = np.mgrid[0:300:50, 0:300:50]
plt.pcolor(x, y, x+y);  # x和y是網格,z是(x,y)座標處的顏色值colorbar()

輸出結果:

原圖被劃分爲了 25 個 block,每個 block 均代表一個 reference box。若 base_anchors 有 9 個,則只需要按照 stride = 50 進行滑動便可以獲得這 25 個 block 的所有錨框(總計 5x5x9=225 個)。針對前面的特徵圖 F 有:

stride = 16  # 滑動的步長
alloc_size = F.shape[2:]  # 特徵圖的尺寸
A._generate_anchors(stride, alloc_size).shape

輸出結果:

(1, 1, 50, 50, 36)

即總共 \(50\times 50 \times 9=22500\) 個錨點。

更多後續內容見我的 GitHub:CV


  1. Lenc K, Vedaldi A. R-CNN minus R.[J]. british machine vision conference, 2015.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章