之前雖然也了解一丟丟的 Faster RCNN,但卻一直沒用過,因此一直都是一知半解狀態。這里結合書中描述和 PyTorch 官方代碼來好好瞅瞅。
論文:
Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks
Feature Pyramid Networks for Object Detection
一. 總覽
Faster RCNN 從功能模塊來看,可大致分為 特征提取,RPN,RoI Pooling,RCNN 四個模塊,這里代碼上選擇了 ResNet50 + FPN 作為主干網絡:
model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=False)
1.1 特征提取
這里不用多說,就是選個合適的 Backbone 罷了,不過為了提升特征的判決性,一般會采用 FPN 的結構(自下而上、自上而下、橫向連接、卷積融合)。
1.2 RPN
這部分其實可以看成 One-Stage 檢測器的檢測輸出部分。實際上對於只檢測一類目標來說,可以直接拿去用了。RPN 在 Faster RCNN 中的作用是,結合先驗的 Anchor,將背景和前景區分開來(二分類),這樣的話大量的先驗 Anchor 就可以被篩選出來,並作些許的回歸(使得 Anchor 更接近於真實目標)。
1.3 RoI Pooling
這部分將結合上一步得到的 refine 后的 Anchor 和特征提取網絡中的 feature map。將這些 Anchor 映射到 feature map 上並通過 Pooling 操作將這些代表 anchor 的 feature 拉到同一維度。這樣的話就可以拿去給 RCNN 做最后更細致的多分類和回歸了。
1.4 RCNN
這里將 RoI Pooling 得到的特征送入后面的網絡中,預測每一個 RoI 的分類和邊界框回歸。
二. RPN
2.1 Anchor Generator
以官方 PyTorch torchvision 里的 Faster RCNN 代碼為例:輸入圖片尺度為 768x1344,5 個 feature map 分別經過了 stride=(4, 8, 16, 32, 64),得到了 5 個大小為 (192x336, 96x168, 48x84, 24x42, 12x21) 的 feature。
代碼中預定義了 5 個尺度(32, 64, 128, 256, 512) ,3 種 aspect_ratio (0.5, 1.0, 2.0) 的 Anchor。這樣的話我們可以得到 5 組 base_anchor, 每一組包含 3 個面積相同,寬高比不同的以原點為中心點的基礎錨框。
[-23., -11., 23., 11.] [-45., -23., 45., 23.] [-91., -45., 91., 45.]
[-16., -16., 16., 16.] [-32., -32., 32., 32.] [-64., -64., 64., 64.]
[-11., -23., 11., 23.] [-23., -45., 23., 45.] [-45., -91., 45., 91.]
[-181., -91., 181., 91.] [-362., -181., 362., 181.]
[-128., -128., 128., 128.] [-256., -256., 256., 256.]
[ -91., -181., 91., 181.] [-181., -362., 181., 362.]
然后將這些 base_anchor 撒到對應的 feature map 上(起點是 [0,0])。這樣的話共有:
(192*336*3=193536) + (96x168*3=48384) + (48*84*3=12096) + (24*42*3=3024) + (12*21*3=756) = 257796 個 Anchor
具體參考 torchvision/models/detection/rpn.py
anchors = self.anchor_generator(images, features)
2.2 RPN Head
這部分很簡單,就是將特征提取網絡獲得的 feature map 先經過一個 1x1 的卷積操作,然后分別用兩個 3x3 的卷積進行分類和回歸操作。以大小是 256x48x84 的 feature map 為例,經過 1x1 的卷積操作(不改變特征圖尺寸),假定 feature map 上的寬高平面上每個點有 3 個 Anchor (寬高比分別是 0.5, 1.0, 2.0),那個分類支路上的 3x3 卷積輸出維度是 3x48x84,而回歸支路上的 3x3 卷積輸出維度是 12x48x84。
有多少 Anchor 就有多少分類和回歸結果,最終多個尺度(FPN 5 個尺度)cancat 后的分類維度為 257796x1, 回歸維度為 257796x4。值得注意的是這里的回歸結果是基於編碼后的 Anchor 的偏移量。說道這里就要將下 Faster RCNN 里的 encode 和 decode 過程。區別於之前的 SSD 和 YOLOV3 兩個檢測算法的編解碼方式:
記 $x, y, w, h$ 是檢測框的中心點坐標和寬高,$x, x_a, x^*$ 分別代表檢測框、Anchor 和 GT 的對象坐標, $t_x$ 是檢測偏移量。
解碼:
參考 torchvision/models/detection/rpn.py
proposals = self.box_coder.decode(pred_bbox_deltas.detach(), anchors)
\begin{equation}
\label{decode}
\begin{split}
& x = t_x * w_a + x_a \\
& y = t_y * h_a + y_a \\
& w = e^{t_w} * w_a \\
& h = e^{t_h} * h_a \\
\end{split}
\end{equation}
同理,在計算 loss 時我們需要將 GT 進行編碼
編碼:
參考 torchvision/models/detection/rpn.py
regression_targets = self.box_coder.encode(matched_gt_boxes, anchors)
\begin{equation}
\label{encode}
\begin{split}
& t_x^* = (x^* - x_a) / w_a \\
& t_y^* = (y^* - y_a) / h_a \\
& t_w^* = log(\frac{w^*}{w_a}) \\
& t_h^* = log(\frac{h^*}{h_a}) \\
\end{split}
\end{equation}
解碼后就將 Anchor + BBox_reg 轉換成了 Proposal 了, 注意每個 Proposal 的物理意義是輸入圖片上的(xmin, ymin, xmax, ymax) 。
2.3 篩選 Proposal
首先每個檢測尺度上篩選出前 min(pre_nms_top_n, num_anchors) 個 anchor(上面 257796 個 Proposal 就會篩選出 4756 個),用 scale_level 來標記每個 Proposal 的尺度等級,值域集合為 {0, 1, 2, 3, 4};
隨后對這些篩選出來的 Proposal, 按照 clip, remove_small_boxes, 每個尺度上分別做 nms(具體實現時是把所有的 Proposal + (scale_level * Proposal.max()) 來加速操作的)至多保留 post_nms_top_n 個 Proposal。
具體參考 torchvision/models/detection/rpn.py
boxes, scores = self.filter_proposals(proposals, objectness, images.image_sizes, num_anchors_per_level)
三. RoI Pooling
有了篩選出來的 Proposal,我們就可以將映射到某個 feature map 上然后利用 RoI Pooling 提取每個 Proposal 的特征供后續的 rcnn 細致的分類和回歸了。
參考 torchvision/models/detection/roi_heads.py
box_features = self.box_roi_pool(features, proposals, image_shapes)
代碼中只選擇了其中 4 個尺度來進行操作(最小的那個 feature map 沒有用到)。
因為是多尺度特征,把每個 Proposal 映射到哪個 feature_map 上是個問題,代碼是用 LevelMapper 這玩意來操作的, 理論參考 RPN 論文 。
\begin{equation}
\label{level}
k = \lfloor k_0 + log2(\sqrt{wh}/224) \rfloor
\end{equation}
其中 $k_0$ 是一個面積為 224*224 的 Proposal 應該處於的 feature map level。其他尺度的 Proposal 按照上面的公式安排 feature map level。
代碼中選擇的 256x48x84 這個尺度的 feature map 為 $k_0$, 對應 224*224 這個大小范圍的 Proposal。
隨后使用 roi_align 提取每個 Proposal 的特征
參考 torchvision/ops/poolers.py
result_idx_in_level = roi_align( per_level_feature, rois_per_level, output_size=self.output_size, spatial_scale=scale, sampling_ratio=self.sampling_ratio)
這樣的話 我們就可以獲得 post_nms_top_n 個 256 x 7 x7 維度的前景框的特征。最后 flatten 特征維度接上兩個全連接層獲得 post_nms_top_n 個 1024 維度的特征。
四. RCNN
這部分很簡單,就是兩個全連接層,一個用於分類,一個用於回歸。
參考 torchvision/models/detection/faster_rcnn.py 里的 class FastRCNNPredictor(nn.Module)
self.cls_score = nn.Linear(in_channels, num_classes)
self.bbox_pred = nn.Linear(in_channels, num_classes * 4)