KTN(kernel tranformer network 代码注释)

原文:Kernel Transformer Networks for Compact Spherical Convolution 
原文链接:IEEE Xplore Full-Text PDF:
代码链接:SLOF (siamlof.github.io)

原理

卷积核通过学习使得自身能够进行空间变换,来克服360°图像经过EPR投影以后在极坐标附近的失真状况。

 从上图可以看出在靠近两极的地方失真十分严重

当前遇见的问题:

1.360°全景图像存在严重的极区域失真,图像发生了弯曲等几何变形

2.普通的CNN卷积核形状固定,不能很好适应全景图像不同区域的变形

KTN引入了可学习的核变换模块,可以改变卷积核的形状、方向等。

         KTN的核变换是可学习的,可以通过反向传播学习对全景图像变形较优的卷积核变换。经过变换,卷积核可以自适应不同区域的形变,增强模型的鲁棒性。最终使用变形后的卷积核进行特征提取,实现对全景图像的可变形卷积。

主要的思想:

1.在源卷积核后面加入Kernel Transformer模块,这是KTN的核心组件

2.Kernel Transformer模块对源卷积核进行空间变换,如缩放、旋转等,输出目标卷积核

3.目标卷积核的形状、方向等会发生改变,从而更好地适应输入特征图的形状

4.ktn是可学习的,通过反向传播可以训练学习到对特征图变形较优的核变换方式

5.经过变换,卷积核可以自适应不同区域的形变

  

其中K是源CNN的卷积核,红色的是形变后的

代码部分:

KTN..py

代码的输入为x和对应的行号row,对x进行相应的线性插值,来改变卷积核在行方向上的形状

class RowBilinear(nn.Module):   #行方向的线性插值

    def __init__(self, n_in, kernel_shapes, pad=0):
        super(RowBilinear, self).__init__()

        n_transform = kernel_shapes.size(0)
        weights = []
        self.pad = pad
        for i in range(n_transform):
            kH = kernel_shapes[i,0].item()
            kW = kernel_shapes[i,1].item()
            n_out = (kH + 2 * pad) * (kW + 2 * pad)
            #print(n_out, n_in)
            weight = nn.Parameter(torch.Tensor(n_out, n_in)) #初始化一个线性层参数作为kernel权重
            weights.append(weight)
        # self.weights = nn.ParameterList(weights)
        self.weights = weights
        # setattr(parameter_list_module, 'weights', weights)

    def forward(self, x, row):   #row表示需要插值的行数
        weight = self.weights[row]
        weight = weight.to(x.device)
        return F.linear(x, weight) #F.linear对特定行进行线性插值

下面是个小case,可以看出x会随着学习到的权重发生改变 

ResidualKTN主要构建了KTN的残差结构,结构依次为行投影层,1*1卷积层,深度卷积层

class ResidualKTN(BilinearKTN):

    def initialize_ktn(self, kernel_size):
        self.bilinear = RowBilinear(kernel_size, self.kernel_shapes)   #行方向进行双线性插值进行核大小变换

        self.res1 = RowBilinear(kernel_size, self.kernel_shapes, pad=2)   #
        self.res2 = nn.Conv2d(self.n_in, self.n_in, 1)   #调整通道数
        self.res3 = nn.Conv2d(1, 1, 3, padding=0)        #用来提取特征
        self.res4 = nn.Conv2d(self.n_in, self.n_in, 1)
        self.res5 = nn.Conv2d(1, 1, 3, padding=0)

    def apply_ktn(self, x, row):   #根据不同的角度,变化核
        base = self.bilinear(x, row)   #进行核变换根据不同的角度,这是图中的下面一层

        okH, okW = self.kernel_shapes[row]  #获得原始的kw,kh
        x = self.res1(x, row)

        x = x.view(-1, self.n_in, okH+4, okW+4)
        x = self.res2(self.activation(x))
        x = x.view(-1, 1, okH+4, okW+4)
        x = self.res3(self.activation(x))

        x = x.view(-1, self.n_in, okH+2, okW+2)
        x = self.res4(self.activation(x))
        x = x.view(-1, 1, okH+2, okW+2)
        x = self.res5(self.activation(x))

        x = x.view(base.size())
        x = x + base
        return x

    def initialize_weight(self, **kwargs):
        for name, param in self.named_parameters():
            if name[-5:] == ".bias":
                param.data.zero_()
            elif name[-7:] == ".weight":
                param.data.normal_(std=0.01)
        self.initialize_bilinear(self.bilinear, **kwargs)
        self.initialize_bilinear(self.res1, **kwargs)

具体的结构如上图所示,值得注意的是通过两次此结构,不同的是感受野不同,可以更好捕捉特征。 

下面代码主要实现了

1.遍历目标核的每一个像素坐标

2.计算映射到球面坐标的行列号

3.获取投影向量p并赋值给权重参数

这样可以使用犬座标的参数初始化行方向插值核的权重

class BilinearKTN(KTN):

    def initialize_ktn(self, kernel_size):
        self.bilinear = RowBilinear(kernel_size, self.kernel_shapes) #创建RowBilinear对象作为现在插值层

    def apply_ktn(self, x, row):
        x = self.bilinear(x, row) #对x进行线性插值
        return x

    def initialize_weight(self, **kwargs):
        for name, param in self.named_parameters():
            if name[-5:] == ".bias":
                param.data.zero_()
            elif name[-7:] == ".weight":
                param.data.normal_(std=0.01)
        self.initialize_bilinear(self.bilinear, **kwargs)  #对于bilinear参数初始化

    def initialize_bilinear(self,
                            bilinear,
                            sphereH=320,
                            fov=65.5,
                            imgW=640,
                            dilation=1,
                            tied_weights=5):
        kH = self.src_kernel.size(2)
        sphereW = sphereH * 2
        projection = SphereProjection(kernel_size=kH,
                                      sphereH=sphereH,
                                      sphereW=sphereW,
                                      view_angle=fov,
                                      imgW=imgW)
        center = sphereW / 2
        for i, param in enumerate(bilinear.weights):
            param.data.zero_()
            tilt = i * tied_weights + tied_weights / 2 #计算俯仰角
            P = projection.buildP(tilt=tilt).transpose()  #产生投影矩阵
            okH = self.kernel_shapes[i,0].item() #原始核的高度
            okW = self.kernel_shapes[i,1].item()
            okH += bilinear.pad * 2  #计算出插值后的核的大小
            okW += bilinear.pad * 2

            sH = tilt - okH / 2
            sW = center - okW / 2
            for y in range(okH): #遍历每个像素计算映射到球面上的坐标,并根据投影矩阵赋值给权重
                row = y + sH
                if row < 0 or row >= sphereH:
                    continue
                for x in range(okW):
                    col = x + sW
                    if col < 0 or col >= sphereW:
                        continue
                    pixel = int(row * sphereW + col) 
                    p = P[pixel]
                    if p.nnz == 0:
                        continue
                    j = y * okW + x
                    for k in range(p.shape[1]):
                        param.data[j,k] = p[0,k]

首先计算出核变换后的大小形状,双线性插值后核的形状和大小必然改变,因此后面必须增加padding,然后在计算卷积核的参数。接下来就是经过下图一个遍历的过程,将p投影向量的参数赋值到对应索引的位置。

 

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值