0%

目标检测篇:FPN

在目标检测领域,很难保证所要检测目标的大小都是类似的,MNIST,cifar10,imageNet 等玩具数据集除外。实际场景中,往往目标大小不一致、长宽比例不一致、图片的大小也不一致。长宽比例不一致可以通过之前提到的 Faster R-CNN 1 来解决。FPN 的全称是 Feature Pyramid Networks 2 ,本算法重点关注目标多尺度的问题。因为传统两阶段检测(区域提议、区域识别)算法基于特征图进行预测,通常来自网络骨干的最后一层,回导致小物体信息的丢失。

背景

来看一下传统的多尺度检测方法:

  • 图 a 中,一看就是常规的方法,将一张图片的多个尺度的特征进行预测。对某一输入图片我们通过压缩或放大从而形成不同尺寸的图片作为模型输入,使用同一模型对这些不同尺寸的图片分别处理后,最终再将这些分别得到的特征(feature maps)组合起来。此种方法缺点在于需要对同一图片在更改维度后输入处理多次,计算缓慢。
  • 图 b 中,用单一尺寸的图片做为输入,然后经 CNN 模型处理后,拿最终一层的feature maps 作为最终的特征集。优点是计算简单,如大多数 R-CNN 系列目标检测方法所用,如 Faster R-CNN 等。因此最终这些模型对小维度的目标检测性能不是很好。我曾遇到过在 $8000\times 8000$ 的图像中检测 $10\times 10$ 目标的任务,使用普通的 backbone 去卷积时,一不小心由于 stride 过大,直接错过了目标,导致小目标检测的性能急剧下降。
  • 图 c 中,用单一尺寸的图片做为输入,此方法除选取最后一层的特征外,选用稍靠下的反映图片 low level 信息的 feature maps。然后将这些不同层次的特征简单合并起来,用于最终的特征组合输出。但依然会忽略一些具有更低级别信息,对更小维度的目标检测效果就不大好。
  • 图 d 中,用单一尺寸的图片作为输入,选取所有层的特征联合起来做为最终的特征输出。另外还对各层所反映的不同级别的特征信息进行了自上向下的整合,能更好检测目标。而此方法正是我们本文中要讲的 FPN CNN 特征提取方法。

注意:

  • 对于卷积神经网络而言,不同深度对应着不同层次的语义特征,浅层网络分辨率高,学的更多是浅层特征,如细节、边缘等;深层网络分辨率低,学的更多是深层语义特征,如物体轮廓、类别等。

FPN

FPN 是传统 CNN 网络对图片信息进行表达的一种增强整合。目的是为了改进 CNN 网络的特征提取方式,从而可以使最终输出的特征更好地涵盖输入图片各个维度的信息。它包括两个基本过程:自下至上的通路,即计算不同尺寸的特征;自上至下的通路,即自上至下的特征补充。

自下而上

也就是网络的前向计算部分,而每层的输出都是上一层输出尺寸的 1/2,就完成了传统金字塔方法和 CNN 网络的名词的对应。

自上而下

将深层的有更强语义信息的 feature 经过上采样变成具有高分辨率特征图像的过程。然后再与下一层得到的 feature 经过侧边连接相加,进行增强。最底层的输出会有细节信息和高层特征,检测大目标的同时,也不会忽略小目标。

增强后的数据经过一个 3×3 卷积的处理,原文的意思是这个卷积能减少上采样导致混叠的不利影响,就可以作为网络的输出了。这个图看不懂的话,可以看下面的代码。代码看不懂的话,我也没办法了

这里需要注意的是,每一层,也就是这三个 predict,输出的通道数是相同的。

与 RPN 结合

FPN 将提取到的特征送到 RPN 中用于目标检测,RPN 再去用于用于预测边界框和前景、背景。因为 FPN 有多个层次的输出,所以在每一层的输出后面接入一个 3×3 卷积的处理,在共用两个并联的 1x1 的卷积层用于预测前景背景和目标框的位置。所以一般而言,称特征提取器为 backbone,FPN 为 neck,RPN 预测部分为 head。图片来源 4

与 Faster RCNN 结合

这里,就参考这篇文章 3 吧,我就不照抄了。大体意思就是。在 backbone 中接入 FPN,生成不同尺寸的特征图。使用 RPN 生成不同的 ROI。根据 ROI 尺寸的不同,在不同尺寸的特征图中选择特征块,如下图所示 4

程序

讲真,挺容易理解的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import torch
import torch.nn as nn
import torch.nn.functional as F


# 一个卷积残差块
class Bottleneck(nn.Module):
expansion = 4

def __init__(self, in_planes, planes, stride=1):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_planes,
out_channels=planes,
kernel_size=1,
bias=False)
self.bn1 = nn.BatchNorm2d(planes)

self.conv2 = nn.Conv2d(in_channels=planes,
out_channels=planes,
kernel_size=3,
stride=stride,
padding=1,
bias=False)
self.bn2 = nn.BatchNorm2d(planes)

self.conv3 = nn.Conv2d(in_channels=planes,
out_channels=self.expansion * planes,
kernel_size=1,
bias=False)
self.bn3 = nn.BatchNorm2d(num_features=self.expansion * planes)

self.shortcut = nn.Sequential()

# 步长不为 1 或者 输入特征不等于输出特征
if stride != 1 or in_planes != self.expansion * planes:
# 残差块
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels=in_planes,
out_channels=self.expansion * planes,
kernel_size=1,
stride=stride,
bias=False), nn.BatchNorm2d(self.expansion * planes))

def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = F.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out += self.shortcut(x)
out = F.relu(out)
return out


class FPN(nn.Module):
def __init__(self, block, num_blocks):
super(FPN, self).__init__()
self.in_planes = 64

self.conv1 = nn.Conv2d(in_channels=3,
out_channels=64,
kernel_size=7,
stride=2,
padding=3,
bias=False)
self.bn1 = nn.BatchNorm2d(64)

# Bottom-up layers, backbone of the network
# planes 是输出特征
# channel 变化:3 -> 64 -> 64 -> 256
self.layer1 = self._make_layer(block=block,
planes=64,
num_blocks=num_blocks[0],
stride=1)
# channel 变化:64*4 -> 128 -> 128 -> 512
self.layer2 = self._make_layer(block=block,
planes=128,
num_blocks=num_blocks[1],
stride=2)
# out_channel: 1024
self.layer3 = self._make_layer(block=block,
planes=256,
num_blocks=num_blocks[2],
stride=2)
# out_channel 2048
self.layer4 = self._make_layer(block=block,
planes=512,
num_blocks=num_blocks[3],
stride=2)

# Top layer
# layer4 后面接一个1x1, 256 conv,得到金字塔最顶端的feature
self.toplayer = nn.Conv2d(in_channels=2048,
out_channels=256,
kernel_size=1,
stride=1,
padding=0)

# Smooth layers
# 这个是上面引文中提到的抗 『混叠』 的3x3卷积
# 由于金字塔上的所有feature共享classifier和regressor
# 要求它们的channel dimension必须一致
# 这个用于多路预测
self.smooth1 = nn.Conv2d(in_channels=256,
out_channels=256,
kernel_size=3,
stride=1,
padding=1)
self.smooth2 = nn.Conv2d(in_channels=256,
out_channels=256,
kernel_size=3,
stride=1,
padding=1)
self.smooth3 = nn.Conv2d(in_channels=256,
out_channels=256,
kernel_size=3,
stride=1,
padding=1)

# Lateral layers
# 为了匹配channel dimension引入的1x1卷积
# 注意这些backbone之外的extra conv,输出都是256 channel
self.latlayer1 = nn.Conv2d(in_channels=1024,
out_channels=256,
kernel_size=1,
stride=1,
padding=0)
self.latlayer2 = nn.Conv2d(in_channels=512,
out_channels=256,
kernel_size=1,
stride=1,
padding=0)
self.latlayer3 = nn.Conv2d(in_channels=256,
out_channels=256,
kernel_size=1,
stride=1,
padding=0)

def _make_layer(self, block, planes, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1)
layers = []
for stride in strides:
layers.append(block(self.in_planes, planes, stride))
self.in_planes = planes * block.expansion
return nn.Sequential(*layers)

## FPN的lateral connection部分: upsample以后,element-wise相加
def _upsample_add(self, x, y):
_, _, H, W = y.size()
# 上采样到指定尺寸
return F.upsample(x, size=(H, W), mode='bilinear') + y

def forward(self, x):
# Bottom-up
c1 = F.relu(self.bn1(self.conv1(x)))
c1 = F.max_pool2d(c1, kernel_size=3, stride=2, padding=1)
c2 = self.layer1(c1)
c3 = self.layer2(c2)
c4 = self.layer3(c3)
c5 = self.layer4(c4)

# Top-down
# P5: 金字塔最顶上的feature 2048 -> 256
p5 = self.toplayer(c5)
# P4: 上一层 p5 + 侧边来的 c4
# 其余同理
p4 = self._upsample_add(p5, self.latlayer1(c4))
p3 = self._upsample_add(p4, self.latlayer2(c3))
p2 = self._upsample_add(p3, self.latlayer3(c2))

p4 = self.smooth1(p4)
p3 = self.smooth2(p3)
p2 = self.smooth3(p2)
return p2, p3, p4, p5


def FPN101():
# 2 通过步长,控制上一层的图片尺寸是下一层图片尺寸的几倍,这里都是 2
return FPN(Bottleneck, [2, 2, 2, 2])


def test():
net = FPN101()
fms = net(torch.randn((1, 3, 600, 900), requires_grad=True))
for fm in fms:
print(fm.size())


test()

# ref: https://github.com/kuangliu/pytorch-fpn/blob/master/fpn.py

references

感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章