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)
- 提出一个端到端的可训练guided filtering layer,参数和引导图均可学习,大大增强了FCNs中joint upsampling的能力。
- 改进后的FCNs在多种视觉任务中10-100倍快于原先,达到了SOTA效果。
实验表明该方法用于多种视觉任务的baseline上同样表现出色。
Problem Statement
dense pixel-wise image prediction是图像任务中基本的处理操作。Fully Convolutional Networks(FCNs)在提升dense pixel-wise image prediction效果的同时,消耗了大量的计算复杂度和内存。作者期望加速FCNs。
为了加速FCNs,作者提出一个由粗到细的通用结构,首先下采样输入图像,在小尺寸上执行算法,再将结果上采样至原始分辨率。
这里的挑战在于如何将低分辨率输出还原至有丰富细节和清晰轮廓的原始分辨率图像。
- 作者拟采用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
原始版本
:高分辨率输入图像,
: 低分辨率输出图像
joint upsampling的目的在于生成(高分辨输出图像),即有
的细节和轮廓,又和
相似。
为了实现joint upsampling,guided filter的输入有(输入的低分辨率图像)、
(对应的高分辨率输入图像)输出为、
(低分辨率的输出图像),输出为
(高分辨率的输出图像)。
(这里的*表示元素相乘,k表示guided filter的窗口序号,i表示像素序号。)
首先通过最小化的重建误差获得
。接着将
上采样得到
。最后基于
得到
。
Fully Differentiable Guided Filter
初始的joint upsampling只能作为后处理步骤,因为不可微,因此将它改造成guided filtering layer。它的结构如下图所示。
图中的蓝线表示正向传播,黄线表示反向传播。
对应用均值滤波
(用box filter可加速)和局部线性模型可以得到
。对
做bilinear upsampling
。
通过
可得。
反向传播的推导如下所示。
生成特定任务的guidance map
在上一节的推导中,默认的通道数相同。如果不同,需要引入一个transformation function将通道数统一到guidance map。即使通道数相同,优于
的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,结构如下图所示。
和Fig 2相比,拿dilated conv替换了mean filter ,用pointwise conv block替换了local linear model。
Deep Guided Filtering Netword(DGF)
基于guided filtering layer,作者提出一个通用网络DGF,它的结构如下图所示。
在低分辨率图像上处理算法操作,再通过Guided Filtering layer(
)上采样得到输出,大幅降低计算复杂度和占用内存。
损失函数是目标图像和输出图像之前的差距。
Evaluation
Guided Filtering Layer中,作者迭代过程中出现了四种不同的joint upsampling形式,包括原始版本,可微版本、生成引导图的版本和最终版本。做实验时,对应的DGF网络分别被称为。
DGF的细节
优化函数选择了L2 loss。Leaky Relu的negative slope设为0.2
实验细节
数据集是MIT-Adobe FiveK,训练150epochs,输入尺寸512×512。为了保证生成能力,后又训练网络30个epoch,输入尺寸在512×512到1672×1672间变化。
尺寸始终为64×64。优化方法是Adam,学习率为0.0001,batch_size是1.
基本baseline是Deep Bilateral Learning(DBL)(文献[22]),速度和质量平衡地比较好。另一个baseline是CAN(文献[23]),在合理时间内达到了SOTA的效果。
实验结果
作者如何评估自己的方法,实验的setup是什么样的,有没有问题或者可以借鉴的地方。
Conclusion
Filtering guided filter可端到端训练,应用它的DGF网络能大幅降低计算复杂度,节省内存。
Notes
这一块儿的相关操作不是很熟悉。虽然论文都看过,但Kaiming He之后的好几篇数据公式或者英文单词没能特别理解,只懂个大概。
- 作者提出更高质量的guidance map可以引导joint upsampling学习到更高的表现。但本文中的guidance map是由输入的低分辨率图像卷积获得的,这样的guidance map效果存疑。
我能理解对后加卷积层获得更好的性能,但对于卷积层的输出叫guidance map不是很认同。
- bilateral filter [26, 27]
- guided filter [25]
Code
box filter
首先介绍box filter,它指在给定的滑动窗口下,对窗口内的像素值进行快速相加求和。用pytorch的实现代码如下
这里的逻辑我没有看懂,由于也不是现在的重点,先略过了。
import torch
from torch import nn
def diff_x(input, r):
assert input.dim() == 4
left = input[:, :, r:2 * r + 1]
middle = input[:, :, 2 * r + 1: ] - input[:, :, :-2 * r - 1]
right = input[:, :, -1: ] - input[:, :, -2 * r - 1: -r - 1]
output = torch.cat([left, middle, right], dim=2)
return output
def diff_y(input, r):
assert input.dim() == 4
left = input[:, :, :, r:2 * r + 1]
middle = input[:, :, :, 2 * r + 1: ] - input[:, :, :, :-2 * r - 1]
right = input[:, :, :, -1: ] - input[:, :, :, -2 * r - 1: -r - 1]
output = torch.cat([left, middle, right], dim=3)
return output
class BoxFilter(nn.Module):
def __init__(self, r):
super(BoxFilter, self).__init__()
self.r = r
def forward(self, x):
assert x.dim() == 4
# numpy.cumsum给定轴上的和
return diff_y(diff_x(x.cumsum(dim=2), self.r).cumsum(dim=3), self.r)
基础版本的Guided Filter
首先学习一个最基本版本的Guided Filter写法,明白中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
相比上一节的版本,区别在于:
- boxfilter被替换成了dilation conv
- A的计算方法由A = cov(x,y)/cov(x,x)变成了基于cov(x,y)、cov(x,x)的pointwise conv block
在这里学到的东西
- conv1的结果/conv2的结果,这也是可以回传的
- 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