• 【文献阅读】Cascaded Partial Decoder for Fast and Accurate Salient Object Detection


    写在前面

    这是2019CVPR的一篇文章,关于显著目标检测的。提出了一种CPD框架
    大概记录一下对这个文章的理解,做了一些图,如有不对,欢迎指正
    文章链接:论文下载
    代码地址:CPD代码

    整体框架

    这篇文章的模型是基于VGG16设计的

    VGG16分为五个阶段

    在这里插入图片描述

    我们先说一下本文的网络模型的整体结构:

    将VGG16这五个阶段得到的特征图,分别表示为 f i = f 1 , f 2 , f 3 , f 4 , f 5 f_i=f_1,f_2,f_3,f_4,f_5 fi=f1,f2,f3,f4,f5,大小为 H / 2 ( i − 1 )     , W / 2 ( i − 1 ) H/2^{(i-1)} \ \ \ , W/2^{(i-1)} H/2(i1)   ,W/2(i1)

    作者设计了一种分支网络,利用最后两个卷积块构造了两个分支(注意力分支和检测分支)

    这是作者给出的网络结构

    在这里插入图片描述

    在这里插入图片描述

    上面是注意力分支,下面是检测分支。

    在注意力分支作者设计了一个部分解码器partial decoder 来集成后三层卷积块 f 3 , f 4 , f 5 f_3,f_4,f_5 f3,f4,f5提取的特征

    经过这个部分解码器,生成了初始的显著图像 S i S_i Si

    我们把这个部分解码器叫做 D a = g a ( f 3 a , f 4 a , f 5 a ) D_a=g_a (f_3^a,f_4^a,f_5^a) Da=ga(f3a,f4a,f5a),解码器输出 S i S_i Si

    S i S_i Si再经过一个整体注意力模块(holistic attention module,HAM)后,得到一个增强的特征 S h S_h Sh

    作者说,“我们可以通过整合三层特征来获得相对精确的显著图,因此Sh有效地消除了特征f3中的干扰”

    S h S_h Sh将用来在下面的检测分支细化特征 f 3 f_3 f3

    然后我们将特征图 f 3 f_3 f3 S h S_h Sh按元素相乘,得到检测分支的细化特征 f 3 d = f 3 ⊙ S h f_3^d =f_3\odot S_h f3d=f3Sh

    同理,将 f 4 f_4 f4 S h S_h Sh按元素相乘,得到 f 4 d = f 4 ⊙ S h f_4^d =f_4\odot S_h f4d=f4Sh

    f 5 f_5 f5 S h S_h Sh按元素相乘,得到 f 5 d = f 5 ⊙ S h f_5^d =f_5\odot S_h f5d=f5Sh

    我们为检测分支构造另一个部分解码器 D d = g d ( f 3 d , f 4 d , f 5 d ) D_d = g_d(f_3^d,f_4^d,f_5^d) Dd=gd(f3d,f4d,f5d),这个解码器输出最终的显著图像 S d S_d Sd

    使用ground truth联合训练两个分支,两个分支参数不共享。

    这就是网络的整体结构和处理流程

    下面我们主要详细说下两个部分解码器(partial decoder)和整体注意力模块(holistic attention module,HAM)

    部分解码器

    这个部分解码器,主要目的就是快速的集成三个块得到的特征图,这里作者设计了一个上下文模块conext module

    主要借鉴于这篇论文

    • Receptive Field Block Net for Accurate and Fast Object Detection

    这篇文章是ECCV2018年,主要贡献就是提出了RFB(Receptive Field Block)

    出发点是模拟人类视觉的感受野从而加强网络的特征提取能力

    做法就是将特征图通过不同卷积分支,最后再合并不同分支的结果,然后再与特征图合并。

    在这里插入图片描述

    网络结构如下图所示:

    就是在ReLU之前,通过这个模块,增加感受野

    在这里插入图片描述

    左边的结构是原始的RFB,右边的结构相比RFB把3×3的conv变成了两个1×3和3×1的分支,一是减少了参数量,二是增加了更小的感受野,这样也是在模拟人类视觉系统,捕捉更小的感受野。

    而本文作者在原有的RFB基础上增加了一个分支。

    我画了个图

    在这里插入图片描述

    代码如下:

    class RFB(nn.Module):
        def __init__(self, in_channel, out_channel):
            super(RFB, self).__init__()
            self.relu = nn.ReLU(True)
            self.branch0 = nn.Sequential(
                nn.Conv2d(in_channel, out_channel, 1),
            )
            self.branch1 = nn.Sequential(
                nn.Conv2d(in_channel, out_channel, 1),
                nn.Conv2d(out_channel, out_channel, kernel_size=(1, 3), padding=(0, 1)),
                nn.Conv2d(out_channel, out_channel, kernel_size=(3, 1), padding=(1, 0)),
                nn.Conv2d(out_channel, out_channel, 3, padding=3, dilation=3)
            )
            self.branch2 = nn.Sequential(
                nn.Conv2d(in_channel, out_channel, 1),
                nn.Conv2d(out_channel, out_channel, kernel_size=(1, 5), padding=(0, 2)),
                nn.Conv2d(out_channel, out_channel, kernel_size=(5, 1), padding=(2, 0)),
                nn.Conv2d(out_channel, out_channel, 3, padding=5, dilation=5)
            )
            self.branch3 = nn.Sequential(
                nn.Conv2d(in_channel, out_channel, 1),
                nn.Conv2d(out_channel, out_channel, kernel_size=(1, 7), padding=(0, 3)),
                nn.Conv2d(out_channel, out_channel, kernel_size=(7, 1), padding=(3, 0)),
                nn.Conv2d(out_channel, out_channel, 3, padding=7, dilation=7)
            )
            self.conv_cat = nn.Conv2d(4*out_channel, out_channel, 3, padding=1)
            self.conv_res = nn.Conv2d(in_channel, out_channel, 1)
    
            for m in self.modules():
                if isinstance(m, nn.Conv2d):
                    m.weight.data.normal_(std=0.01)
                    m.bias.data.fill_(0)
    
        def forward(self, x):
            x0 = self.branch0(x)
            x1 = self.branch1(x)
            x2 = self.branch2(x)
            x3 = self.branch3(x)
    
            x_cat = torch.cat((x0, x1, x2, x3), 1)
            x_cat = self.conv_cat(x_cat)
    
            x = self.relu(x_cat + self.conv_res(x))
            return x
    
    • 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

    回到我们的论文中,VGG16后三个卷积块提取的特征,首先全部经过一个RFB,然后再送入我们的部分解码器

    这个部分解码器具体做了些什么呢?

    首先,VGG16的五个块每次通过池化都会让图像的尺寸减半。

    参考文章:https://blog.csdn.net/qq_42012782/article/details/123222042

    但是本论文中使用的VGG是修改过的

    池化层放在卷积块的前面,并且第一个卷积块不池化。

    具体的可以去看代码,此处只要知道

    假设输入图片是WxW,第一个块输出尺寸等于原尺寸,第二个块原尺寸的1/2,第三个块是1/4,第四个块是1/8,第五个块是1/16

    这个部分解码器,就是将x5,x4,x3拿出来。

    • 将x5记作x_1

    • 将x5与x4相乘,记作x_2。

    • 再将x5,x4,x3三个相乘,记作x_3。

    • 最后将三个特征cat起来

    • 然后经过两个卷积输出一张显著图像。

    如果大小不一样,那就先上采样,扩大大小,再经过一个3x3的卷积,改变一下通道数(padding=1,stride=1,大小不变)

    大小就一样了,就可以相乘了

    做了个示意图,虚线框圈起来的就是部分解码器做的事情

    在这里插入图片描述

    代码如下:

    class aggregation(nn.Module):
        def __init__(self, channel):
            super(aggregation, self).__init__()
            self.relu = nn.ReLU(True)
    
            self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
            self.conv_upsample1 = nn.Conv2d(channel, channel, 3, padding=1)
            self.conv_upsample2 = nn.Conv2d(channel, channel, 3, padding=1)
            self.conv_upsample3 = nn.Conv2d(channel, channel, 3, padding=1)
            self.conv_upsample4 = nn.Conv2d(channel, channel, 3, padding=1)
            self.conv_upsample5 = nn.Conv2d(2*channel, 2*channel, 3, padding=1)
    
            self.conv_concat2 = nn.Conv2d(2*channel, 2*channel, 3, padding=1)
            self.conv_concat3 = nn.Conv2d(3*channel, 3*channel, 3, padding=1)
            self.conv4 = nn.Conv2d(3*channel, 3*channel, 3, padding=1)
            self.conv5 = nn.Conv2d(3*channel, 1, 1)
    
            for m in self.modules():
                if isinstance(m, nn.Conv2d):
                    m.weight.data.normal_(std=0.01)
                    m.bias.data.fill_(0)
    
        def forward(self, x1, x2, x3):
            # x1: 1/16 x2: 1/8 x3: 1/4
            x1_1 = x1
            x2_1 = self.conv_upsample1(self.upsample(x1)) * x2
            x3_1 = self.conv_upsample2(self.upsample(self.upsample(x1))) \
                   * self.conv_upsample3(self.upsample(x2)) * x3
    
            x2_2 = torch.cat((x2_1, self.conv_upsample4(self.upsample(x1_1))), 1)
            x2_2 = self.conv_concat2(x2_2)
    
            x3_2 = torch.cat((x3_1, self.conv_upsample5(self.upsample(x2_2))), 1)
            x3_2 = self.conv_concat3(x3_2)
    
            x = self.conv4(x3_2)
            x = self.conv5(x)
    
            return x
    
    • 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

    整体注意力

    在这里插入图片描述

    我们可以拿上面注意力分支得到的显著图 S i S_i Si直接与下面的特征图相乘,然后送入检测分支。但是这样的做法有利有弊,如果我们获得的 S i S_i Si是准确的,那么我们这样做可以很好的抑制干扰,但是如果我们的 S i S_i Si是错误的,那么这种做法就会得到不好的结果。

    所以,本文作者提出了“整体注意力模块”Holistic Attention Module

    这个模块的目的是扩大初始显著图 S i S_i Si中显著目标的面积
    S h = M A X ( f m i n _ m a x ( C o n v g ( S i , k ) ) , S i ) S_h=MAX(f_{min\_max}(Conv_g(S_i,k)),S_i ) Sh=MAX(fmin_max(Convg(Si,k)),Si)
    先拿 S i S_i Si做一个高斯核为k,偏执为0的卷积运算。 C o n v g ( S i , k ) Conv_g(S_i,k) Convg(Si,k)

    f m i n _ m a x ( ) f_{min\_max}() fmin_max()是一个归一化函数,使值模糊映射在范围[0,1]内

    M A X ( ) MAX() MAX()是一个极大值函数,由于卷积运算会模糊Si,它倾向于增加Si显著区域的权重系数

    这边代码上做的是:

    • 先将Si做一个卷积运算(高斯核为gkern(31, 4))
    • 然后做一个归一化 f m i n _ m a x ( ) f_{min\_max}() fmin_max()
    • 然后去一个最大值
    • 最后与x3特征相乘,作为新的x3,参与检测分支

    代码如下:

    class HA(nn.Module):
        # holistic attention module
        def __init__(self):
            super(HA, self).__init__()
            gaussian_kernel = np.float32(gkern(31, 4))
            gaussian_kernel = gaussian_kernel[np.newaxis, np.newaxis, ...]
            self.gaussian_kernel = Parameter(torch.from_numpy(gaussian_kernel))
    
        def forward(self, attention, x):
            soft_attention = F.conv2d(attention, self.gaussian_kernel, padding=15)
            soft_attention = min_max_norm(soft_attention)
            x = torch.mul(x, soft_attention.max(attention))
            return x
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    然后下面检测分支,拿到这个x3,以及x4,x5,同样的做一个部分解码器,这个解码器与注意力分支的那个解码器操作一致。

    最终输出结果 S d S_d Sd

    整体代码

    整个网络的流程代码如下:

    class CPD_VGG(nn.Module):
        def __init__(self, channel=32):
            super(CPD_VGG, self).__init__()
            self.vgg = B2_VGG()
            self.rfb3_1 = RFB(256, channel)
            self.rfb4_1 = RFB(512, channel)
            self.rfb5_1 = RFB(512, channel)
            self.agg1 = aggregation(channel)
    
            self.rfb3_2 = RFB(256, channel)
            self.rfb4_2 = RFB(512, channel)
            self.rfb5_2 = RFB(512, channel)
            self.agg2 = aggregation(channel)
    
            self.HA = HA()
            self.upsample = nn.Upsample(scale_factor=4, mode='bilinear', align_corners=False)
    
        def forward(self, x):
            x1 = self.vgg.conv1(x)
            x2 = self.vgg.conv2(x1)
            x3 = self.vgg.conv3(x2)
    
            x3_1 = x3
            x4_1 = self.vgg.conv4_1(x3_1)
            x5_1 = self.vgg.conv5_1(x4_1)
            x3_1 = self.rfb3_1(x3_1)
            x4_1 = self.rfb4_1(x4_1)
            x5_1 = self.rfb5_1(x5_1)
            attention = self.agg1(x5_1, x4_1, x3_1)
    
            x3_2 = self.HA(attention.sigmoid(), x3)
            x4_2 = self.vgg.conv4_2(x3_2)
            x5_2 = self.vgg.conv5_2(x4_2)
            x3_2 = self.rfb3_2(x3_2)
            x4_2 = self.rfb4_2(x4_2)
            x5_2 = self.rfb5_2(x5_2)
            detection = self.agg2(x5_2, x4_2, x3_2)
    
            return self.upsample(attention), self.upsample(detection)
    
    • 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
  • 相关阅读:
    1076 Forwards on Weibo
    剑指offer 44. 从1到n整数中1出现的次数
    【动力节点】JavaWeb系列 (老杜B站视频笔记整理)
    工程管理系统简介 工程管理系统源码 java工程管理系统 工程管理系统功能设计
    07.自定义指令传参小窍门
    【Java接口性能优化】skywalking使用
    商越科技:渗透测试保障平台安全,推动线上采购高效运转
    自监督直接和具体任务的结合(Task Related Self-Supervised Learning)的探索
    git pull 报错: 在签出前,请清理存储库工作树
    一起来学Kotlin:概念:8. Kotlin Control Flow: When, For, While, Range 等使用
  • 原文地址:https://blog.csdn.net/holly_Z_P_F/article/details/127429430