Title

Fast End-to-End Trainable Guided Filter

Information

论文地址:https://arxiv.org/abs/1803.05619
github地址:https://github.com/wuhuikai/DeepGuidedFilter

Summary

作者基于guided filter,提出一个可微的模块guided filtering layer嵌入FCNs。通过在低分辨率图像上执行算法再还原,达到加速FCNs、节省内存的目的。在guided filtering layer基础上提出一个可学习的变换函数,可生成引导图。基于这种FCNs,构造了网络Deep Guided Filtering Network(DGF),在各种计算机视觉任务中都取得了质量、速度、内存占用的SOTA效果。

Contribution(s)

  1. 提出一个端到端的可训练guided filtering layer,参数和引导图均可学习,大大增强了FCNs中joint upsampling的能力。
  2. 改进后的FCNs在多种视觉任务中10-100倍快于原先,达到了SOTA效果。
  3. 实验表明该方法用于多种视觉任务的baseline上同样表现出色。

    Problem Statement

    dense pixel-wise image prediction是图像任务中基本的处理操作。Fully Convolutional Networks(FCNs)在提升dense pixel-wise image prediction效果的同时,消耗了大量的计算复杂度和内存。作者期望加速FCNs。

  4. 为了加速FCNs,作者提出一个由粗到细的通用结构,首先下采样输入图像,在小尺寸上执行算法,再将结果上采样至原始分辨率。

这里的挑战在于如何将低分辨率输出还原至有丰富细节和清晰轮廓的原始分辨率图像。

  1. 作者拟采用joint upsampling解决问题1。joint upsampling能根据输入的低分辨图图像和高分辨率引导图生成高分辨率输出。

但joint upsampling的能力有限,不足以生成符合需求的高分辨率图像。

Method(s)

作者的叙述逻辑挺好的。
为了增强FCNs中joint upsampling的还原能力,作者将guided filter构造成一个可微模块guided filtering layer,这样就可以(1)和FCNs一起训练;(2)根据不同任务学习到不同参数,有自适应能力;(3)能被高分辨率ground truth监督。
通过这种嵌入guided filtering layer的FCNs,作者提出了一个用于pixel-wise image prediction的网络Deep Guided Filtering Network(DGF)。

Guided Filtering Layer

作者由最初始版本的guided filter慢慢改到最终版本,整个迭代一共产生了四种joint upsampling

原始版本

Fast End-to-End Trainable Guided Filter - 图1:高分辨率输入图像,Fast End-to-End Trainable Guided Filter - 图2: 低分辨率输出图像
joint upsampling的目的在于生成Fast End-to-End Trainable Guided Filter - 图3(高分辨输出图像),即有Fast End-to-End Trainable Guided Filter - 图4的细节和轮廓,又和Fast End-to-End Trainable Guided Filter - 图5相似。
为了实现joint upsampling,guided filter的输入有Fast End-to-End Trainable Guided Filter - 图6(输入的低分辨率图像)、Fast End-to-End Trainable Guided Filter - 图7(对应的高分辨率输入图像)输出为、Fast End-to-End Trainable Guided Filter - 图8(低分辨率的输出图像),输出为Fast End-to-End Trainable Guided Filter - 图9(高分辨率的输出图像)。
Fast End-to-End Trainable Guided Filter - 图10
Fast End-to-End Trainable Guided Filter - 图11
(这里的*表示元素相乘,k表示guided filter的窗口序号,i表示像素序号。)
首先通过最小化Fast End-to-End Trainable Guided Filter - 图12的重建误差获得Fast End-to-End Trainable Guided Filter - 图13。接着将Fast End-to-End Trainable Guided Filter - 图14上采样得到Fast End-to-End Trainable Guided Filter - 图15。最后基于Fast End-to-End Trainable Guided Filter - 图16得到Fast End-to-End Trainable Guided Filter - 图17

Fully Differentiable Guided Filter

初始的joint upsampling只能作为后处理步骤,因为不可微,因此将它改造成guided filtering layer。它的结构如下图所示。
Fast End-to-End Trainable Guided Filter - 图18
图中的蓝线表示正向传播,黄线表示反向传播。
Fast End-to-End Trainable Guided Filter - 图19应用均值滤波Fast End-to-End Trainable Guided Filter - 图20(用box filter可加速)和局部线性模型可以得到Fast End-to-End Trainable Guided Filter - 图21。对Fast End-to-End Trainable Guided Filter - 图22做bilinear upsampling Fast End-to-End Trainable Guided Filter - 图23Fast End-to-End Trainable Guided Filter - 图24通过Fast End-to-End Trainable Guided Filter - 图25可得。
反向传播的推导如下所示。
Fast End-to-End Trainable Guided Filter - 图26

生成特定任务的guidance map

在上一节的推导中,默认Fast End-to-End Trainable Guided Filter - 图27的通道数相同。如果不同,需要引入一个transformation function将通道数统一到guidance map。即使通道数相同,优于Fast End-to-End Trainable Guided Filter - 图28的guidance map也是需要的。为了利用guided filtering layer的可微特性,作者继续用可学习的transformation function,即Fig2中的F(I)函数。它是由2个1×1×16的卷积,1个adaptive normalization layer[文献23]、一个leaky relu层构成的FCN block。F(I)函数可以生成需要的guidance map。

Convolutional Guided Filtering Layer

在之前所有分析的基础上,需要端到端去学习的参数只有F(I)的卷积权重,但这样guided filter表现的效果也不是很好。因此作者在前面分析的基础上引入了一些其它需要学习的参数。引入后的网络结构被称为convolutional guided filter layer,结构如下图所示。
Fast End-to-End Trainable Guided Filter - 图29
和Fig 2相比,拿dilated conv替换了mean filter Fast End-to-End Trainable Guided Filter - 图30,用pointwise conv block替换了local linear model。

Deep Guided Filtering Netword(DGF)

基于guided filtering layer,作者提出一个通用网络DGF,它的结构如下图所示。
Fast End-to-End Trainable Guided Filter - 图31
在低分辨率图像上处理算法操作Fast End-to-End Trainable Guided Filter - 图32,再通过Guided Filtering layer(Fast End-to-End Trainable Guided Filter - 图33)上采样得到输出,大幅降低计算复杂度和占用内存。
损失函数是目标图像和输出图像之前的差距。

Evaluation

Guided Filtering Layer中,作者迭代过程中出现了四种不同的joint upsampling形式,包括原始版本,可微版本、生成引导图的版本和最终版本。做实验时,对应的DGF网络分别被称为Fast End-to-End Trainable Guided Filter - 图34

DGF的细节

Fast End-to-End Trainable Guided Filter - 图35

优化函数选择了L2 loss。Leaky Relu的negative slope设为0.2

实验细节

数据集是MIT-Adobe FiveK,训练150epochs,Fast End-to-End Trainable Guided Filter - 图36输入尺寸512×512。为了保证生成能力,后又训练网络30个epoch,输入尺寸在512×512到1672×1672间变化。Fast End-to-End Trainable Guided Filter - 图37尺寸始终为64×64。优化方法是Adam,学习率为0.0001,batch_size是1.
基本baseline是Deep Bilateral Learning(DBL)(文献[22]),速度和质量平衡地比较好。另一个baseline是CAN(文献[23]),在合理时间内达到了SOTA的效果。

实验结果

Fast End-to-End Trainable Guided Filter - 图38
Fast End-to-End Trainable Guided Filter - 图39

作者如何评估自己的方法,实验的setup是什么样的,有没有问题或者可以借鉴的地方。

Conclusion

Filtering guided filter可端到端训练,应用它的DGF网络能大幅降低计算复杂度,节省内存。

Notes

这一块儿的相关操作不是很熟悉。虽然论文都看过,但Kaiming He之后的好几篇数据公式或者英文单词没能特别理解,只懂个大概。

  • Joint upsampling主要有两种,bilateral upsampling和guided filter

    Criticism

  1. 作者提出更高质量的guidance map可以引导joint upsampling学习到更高的表现。但本文中的guidance map是由输入的低分辨率图像卷积获得的,这样的guidance map效果存疑。

我能理解对Fast End-to-End Trainable Guided Filter - 图40后加卷积层获得更好的性能,但对于卷积层的输出叫guidance map不是很认同。

  1. guided filtering layer反向传播时,bilinear upsample的传播实现有些不明确

    Reference

  • bilateral filter [26, 27]
  • guided filter [25]

Code

box filter

首先介绍box filter,它指在给定的滑动窗口下,对窗口内的像素值进行快速相加求和。用pytorch的实现代码如下
这里的逻辑我没有看懂,由于也不是现在的重点,先略过了。

  1. import torch
  2. from torch import nn
  3. def diff_x(input, r):
  4. assert input.dim() == 4
  5. left = input[:, :, r:2 * r + 1]
  6. middle = input[:, :, 2 * r + 1: ] - input[:, :, :-2 * r - 1]
  7. right = input[:, :, -1: ] - input[:, :, -2 * r - 1: -r - 1]
  8. output = torch.cat([left, middle, right], dim=2)
  9. return output
  10. def diff_y(input, r):
  11. assert input.dim() == 4
  12. left = input[:, :, :, r:2 * r + 1]
  13. middle = input[:, :, :, 2 * r + 1: ] - input[:, :, :, :-2 * r - 1]
  14. right = input[:, :, :, -1: ] - input[:, :, :, -2 * r - 1: -r - 1]
  15. output = torch.cat([left, middle, right], dim=3)
  16. return output
  17. class BoxFilter(nn.Module):
  18. def __init__(self, r):
  19. super(BoxFilter, self).__init__()
  20. self.r = r
  21. def forward(self, x):
  22. assert x.dim() == 4
  23. # numpy.cumsum给定轴上的和
  24. return diff_y(diff_x(x.cumsum(dim=2), self.r).cumsum(dim=3), self.r)

基础版本的Guided Filter

首先学习一个最基本版本的Guided Filter写法,明白Fast End-to-End Trainable Guided Filter - 图41中A,b怎么求,如何得到O

class GuidedFilter(nn.Module):
    def __init__(self, r, eps=1e-8):
        super(GuidedFilter, self).__init__()

        self.r = r
        self.eps = eps
        self.boxfilter = BoxFilter(r)


    def forward(self, x, y):
        n_x, c_x, h_x, w_x = x.size()
        n_y, c_y, h_y, w_y = y.size()

        assert n_x == n_y
        assert c_x == 1 or c_x == c_y
        assert h_x == h_y and w_x == w_y
        assert h_x > 2 * self.r + 1 and w_x > 2 * self.r + 1

        # N
        N = self.boxfilter(Variable(x.data.new().resize_((1, 1, h_x, w_x)).fill_(1.0)))

        # mean_x
        mean_x = self.boxfilter(x) / N
        # mean_y
        mean_y = self.boxfilter(y) / N
        # cov_xy
        cov_xy = self.boxfilter(x * y) / N - mean_x * mean_y
        # var_x
        var_x = self.boxfilter(x * x) / N - mean_x * mean_x

        # 这里是求A和b的重点
        # A
        A = cov_xy / (var_x + self.eps)
        # b
        b = mean_y - A * mean_x

        # mean_A; mean_b
        mean_A = self.boxfilter(A) / N
        mean_b = self.boxfilter(b) / N

        return mean_A * x + mean_b

这里的y是引导图,根据输入图像x和引导图y得到A,b,从而得到输出O。
A = cov(x,y)/cov(x,x)
b = mean(y) - A*mean(x)

(仅)可微版本的Guided Filter

对应Fully Differentiable Guided Filter那一节

class FastGuidedFilter(nn.Module):
    def __init__(self, r, eps=1e-8):
        super(FastGuidedFilter, self).__init__()

        self.r = r
        self.eps = eps
        self.boxfilter = BoxFilter(r)


    def forward(self, lr_x, lr_y, hr_x):
        n_lrx, c_lrx, h_lrx, w_lrx = lr_x.size()
        n_lry, c_lry, h_lry, w_lry = lr_y.size()
        n_hrx, c_hrx, h_hrx, w_hrx = hr_x.size()

        assert n_lrx == n_lry and n_lry == n_hrx
        assert c_lrx == c_hrx and (c_lrx == 1 or c_lrx == c_lry)
        assert h_lrx == h_lry and w_lrx == w_lry
        assert h_lrx > 2*self.r+1 and w_lrx > 2*self.r+1

        ## N
        N = self.boxfilter(Variable(lr_x.data.new().resize_((1, 1, h_lrx, w_lrx)).fill_(1.0)))

        ## mean_x
        mean_x = self.boxfilter(lr_x) / N
        ## mean_y
        mean_y = self.boxfilter(lr_y) / N
        ## cov_xy
        cov_xy = self.boxfilter(lr_x * lr_y) / N - mean_x * mean_y
        ## var_x
        var_x = self.boxfilter(lr_x * lr_x) / N - mean_x * mean_x

        # 求到A_l, b_l
        ## A
        A = cov_xy / (var_x + self.eps)
        ## b
        b = mean_y - A * mean_x

        # 根据A_l, b_l上采样得到A_h, b_h
        ## mean_A; mean_b
        mean_A = F.interpolate(A, (h_hrx, w_hrx), mode='bilinear', align_corners=True)
        mean_b = F.interpolate(b, (h_hrx, w_hrx), mode='bilinear', align_corners=True)

        return mean_A*hr_x+mean_b

这里求A,b的方法和上一节一样,但这里求到的A_l, b_l,需要用bilinear upsampling求到A_h, b_h。思路也非常清晰。

最终版本的Guided Filter

对应Convolutional Guided Filtering Layer一节

class ConvGuidedFilter(nn.Module):
    def __init__(self, radius=1, norm=nn.BatchNorm2d):
        super(ConvGuidedFilter, self).__init__()

        self.box_filter = nn.Conv2d(3, 3, kernel_size=3, padding=radius, dilation=radius, bias=False, groups=3)
        self.conv_a = nn.Sequential(nn.Conv2d(6, 32, kernel_size=1, bias=False),
                                    norm(32),
                                    nn.ReLU(inplace=True),
                                    nn.Conv2d(32, 32, kernel_size=1, bias=False),
                                    norm(32),
                                    nn.ReLU(inplace=True),
                                    nn.Conv2d(32, 3, kernel_size=1, bias=False))
        self.box_filter.weight.data[...] = 1.0

    def forward(self, x_lr, y_lr, x_hr):
        _, _, h_lrx, w_lrx = x_lr.size()
        _, _, h_hrx, w_hrx = x_hr.size()

        N = self.box_filter(x_lr.data.new().resize_((1, 3, h_lrx, w_lrx)).fill_(1.0))
        ## mean_x
        mean_x = self.box_filter(x_lr)/N
        ## mean_y
        mean_y = self.box_filter(y_lr)/N
        ## cov_xy
        cov_xy = self.box_filter(x_lr * y_lr)/N - mean_x * mean_y
        ## var_x
        var_x  = self.box_filter(x_lr * x_lr)/N - mean_x * mean_x

        ## A
        A = self.conv_a(torch.cat([cov_xy, var_x], dim=1))
        ## b
        b = mean_y - A * mean_x

        ## mean_A; mean_b
        mean_A = F.interpolate(A, (h_hrx, w_hrx), mode='bilinear', align_corners=True)
        mean_b = F.interpolate(b, (h_hrx, w_hrx), mode='bilinear', align_corners=True)

        return mean_A * x_hr + mean_b

相比上一节的版本,区别在于:

  1. boxfilter被替换成了dilation conv
  2. A的计算方法由A = cov(x,y)/cov(x,x)变成了基于cov(x,y)、cov(x,x)的pointwise conv block

在这里学到的东西

  1. conv1的结果/conv2的结果,这也是可以回传的
  2. boxfilter的核需要固定一下,即代码中的N

基于作者的最终版本进行改进

作者的版本里有两个不太好的地方:
1、第19行,使用x_lr的数据再resize,然后再填上1.0。(这个逻辑就很不优雅,而且resize操作在部分场景中不支持的。当然我也能理解作者的用意,它是希望全1的这个张量和x_lr在同一个device中)
2、同样是第19行,N能训练结束之后其实是个常量。但在inference的过程中,仍然每次都需要去计算N的值
3、第21、23、25、27行使用的/N,除法的神经网络不是很受欢迎,尤其在部署的时候。这里想把它改成乘法。

改进后的代码展示在下方。
它的优势:

  • 将1/N的值作为网络权重保存下来了
  • 因为保存的是1/N,因此inference过程中不涉及除法

它的不足:

  • N的维度必须被固定下来,即y_lr的维度必须和N保持一致,只能是3, 512, 512

    class ConvGuidedFilter(nn.Module):
      def __init__(self, radius=1):
          super(ConvGuidedFilter, self).__init__()
          self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
          self.box_filter = nn.Conv2d(3, 3, kernel_size=3, padding=radius, dilation=radius, bias=True, groups=3)
          self.conv_a = nn.Sequential(nn.Conv2d(6, 32, kernel_size=1, bias=True),
                                      nn.ReLU(inplace=True),
                                      nn.Conv2d(32, 32, kernel_size=1, bias=True),
                                      nn.ReLU(inplace=True),
                                      nn.Conv2d(32, 3, kernel_size=1, bias=False))
          self.box_filter.weight.data[...] = 1.0
          self.register_buffer('N', torch.ones(1, 3, 512, 512))
    
      def forward(self, x_lr, y_lr, x_hr, is_train=False):
          _, _, h_lrx, w_lrx = x_lr.size()
          _, _, h_hrx, w_hrx = x_hr.size()
    
          if is_train:
              self.N = 1/self.box_filter(torch.ones_like(y_lr))
          N = self.N.repeat(x_lr.shape[0], 1, 1, 1)
          ## mean_x
          mean_x = self.box_filter(x_lr)*N
          ## mean_y
          mean_y = self.box_filter(y_lr)*N
          ## cov_xy
          cov_xy = self.box_filter(x_lr * y_lr)*N - mean_x * mean_y
          ## var_x
          var_x  = self.box_filter(x_lr * x_lr)*N - mean_x * mean_x
    
          ## A
          A = self.conv_a(torch.cat([cov_xy, var_x], dim=1))
          ## b
          b = mean_y - A * mean_x
    
          ## mean_A; mean_b
          mean_A = F.interpolate(A, (h_hrx, w_hrx), mode='bilinear', align_corners=True)
          mean_b = F.interpolate(b, (h_hrx, w_hrx), mode='bilinear', align_corners=True)
          out = mean_A * x_hr + mean_b
          return out