Press "Enter" to skip to content

目标检测篇:FPN

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

在目标检测领域,很难保证所要检测目标的大小都是类似的,MNIST,cifar10,imageNet 等玩具数据集除外。实际场景中,往往目标大小不一致、长宽比例不一致、图片的大小也不一致。长宽比例不一致可以通过之前提到的 Faster R-CNN来解决。FPN 的全称是 Feature Pyramid Networks,本算法重点关注目标多尺度的问题。

 

背景

 

 

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

 

stride

 

注意:

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

FPN

 

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

 

自下而上

 

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

 

自上而下

 

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

 

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


我也没办法了

 

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

 

 

与 Faster R-CNN 结合

 

这里,就参考这篇文章吧,我就不照抄了。

 

程序

 

讲真,挺容易理解的。

 

import torch
import torch.nn as nn
import torch.nn.functional as F

# 一个卷积残差块
class Bottleneck(nn.Module):
    expansion = 4
    def __init__(self, _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

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注