综述
论文题目:《Learning to Zoom: a Saliency-Based Sampling Layer for Neural Networks》
会议时间:European Conference on Computer Vision 2018 (ECCV, 2018)
介绍
如果把一般的普通采样看成均衡的采样过程,那么显著性采样就可以看成不均衡的采样过程。将原图X看成一个网格,将网格顶点设置为V,普通的采样相当于在网格上均匀取点,如每隔一个点取一个数据,最后经过下采样得到的图像长宽均是原来的一半,并且图像所蕴含的信息分布大体不变;显著性采样也是在网格上取点,但是取点过程受采样图的影响,采样图上的数据越大,则在该区域周围取的点就越多,最后的采样图中,该区域上的信息就越丰富,与利用放大镜放大该区域的效果类似。
显著性采样的过程相当于探索新的几何形状V‘,使原来均匀的网格点受采样图的影响,做特殊的收缩与扩张。采样图上数据大的区域,相当于在原始的网格点中做了一个扩张操作;数据小的区域,相当于在原始网格点中做了一个收缩操作。换句话说,采样图中数据比较大的区域会吸引周围数据比较小的区域,具体如下图:
图例中采样图S上的点越亮,代表采样图该位置上的数据越大,通过吸引周围数值比较小的区域,达到扭曲原图坐标系的效果,最终可以实现放大该区域细节的目的。
利用采样图进行显著性采样的过程可以转化为求解重采样图和输入图像之间映射的问题。假设原图坐标为(x,y),映射到重采样图的像素点坐标为(x’,y’),(x’,y’)的生成,可以看成(x,y)被周围坐标点拉扯的结果,拉扯力度与采样图上周围点的数据大小以及(x,y)与其他坐标点的距离有关。如上图中,由于白色区域采样图上的数据比较大,因此白色区域对周围的拉扯力度大,会对周围的像素做一个吸引,也就是将周围的像素统一拉向自己;而上图中黑色区域,由于采样图上的数据比较小,拉扯力度比较小,因此只能被拉向白色区域。因此,作者设计了一个高斯距离核函数k(v’,v),点v与其他像素点v’的距离越大,该值越小,距离越小,该值越大。假设映射可以写成两个函数f(v)和g(v),则该函数可以表示成如下形式,其中Q表示计算得到的采样图:
f
(
v
)
=
∑
v
′
Q
(
v
′
)
k
(
v
′
,
v
)
v
x
∑
v
′
Q
(
v
′
)
k
(
v
′
,
v
)
,
g
(
v
)
=
∑
v
′
Q
(
v
′
)
k
(
v
′
,
v
)
v
y
∑
v
′
Q
(
v
′
)
k
(
v
′
,
v
)
.
f(v)=\frac{\sum_{v'}Q(v')k(v',v)v_x}{\sum_{v'}Q(v')k(v',v)},\\ g(v)=\frac{\sum_{v'}Q(v')k(v',v)v_y}{\sum_{v'}Q(v')k(v',v)}.
f(v)=∑v′Q(v′)k(v′,v)∑v′Q(v′)k(v′,v)vx,g(v)=∑v′Q(v′)k(v′,v)∑v′Q(v′)k(v′,v)vy.
利用上述公式进行重采样操作,显著性较高的区域会吸引其他像素,因此采样更密集,并且引入了距离因素k,可以充当正则化器,避免所有像素点收敛到相同的坐标点。
注:
- 采样过程不一定必须精准到原图上的像素点,介于两个像素点之间的采样点,利用插值运算实现采样。
源码实现
方法函数
这里对源码做了一下修改,只实现采样过程
定义类时:传入图片尺寸
前向传播时:
- 输入:待采样的图片(特征)、尺寸为31的采样图
- 输出:显著性采样后的图片(特征)
class Saliency_Sampler(nn.Module):
def __init__(self, input_size=448):
super(Saliency_Sampler, self).__init__()
# 采用图的尺寸
self.grid_size = 31
# 用于对采样图的填充
self.padding_size = 30
# 采样图经过扩充后的长宽尺寸
self.global_size = self.grid_size + 2 * self.padding_size
# 图片输入的尺寸
self.input_size_net = input_size
self.conv_last = nn.Conv2d(256, 1, kernel_size=1, padding=0, stride=1)
# 高斯核,对应论文公式(3)中的k
gaussian_weights = torch.FloatTensor(makeGaussian(2 * self.padding_size + 1, fwhm=13))
# 输入输出均为1的卷积运算,kernel_size为61*61,用于公式(2,3)的计算
# 因为f,g中每个值都由原显著图每个点计算,并且求和得到
self.filter = nn.Conv2d(1, 1, kernel_size=(2 * self.padding_size + 1, 2 * self.padding_size + 1), bias=False)
# 将卷积参数定义为高斯核参数,即公式中的k
# 高斯核,对应论文中的距离核k,该卷积操作对应论文公式(7),乘积+求和
self.filter.weight[0].data[:, :, :] = gaussian_weights
# 初始化梯度矩阵,两张梯度图,分别代表x的梯度和y的梯度
# 数值从-1到2均匀变化
self.P_basis = torch.zeros(2, self.grid_size + 2 * self.padding_size, self.grid_size + 2 * self.padding_size)
for k in range(2):
for i in range(self.global_size):
for j in range(self.global_size):
self.P_basis[k, i, j] = k * (i - self.padding_size) / (self.grid_size - 1.0) + (1.0 - k) * (
j - self.padding_size) / (self.grid_size - 1.0)
def create_grid(self, x):
# 输入:x 扩充后的采样图
# 预先定义一个和采样图尺寸相同的全零矩阵
P = torch.autograd.Variable(
torch.zeros(1, 2, self.grid_size + 2 * self.padding_size, self.grid_size + 2 * self.padding_size).cuda(),
requires_grad=False)
# 初始化为均匀的网格图
P[0, :, :, :] = self.P_basis
# 将P扩展维度,第一维度大小从1扩展到batch_size,即将所有batch的梯度图都做一个初始化
P = P.expand(x.size(0), 2, self.grid_size + 2 * self.padding_size, self.grid_size + 2 * self.padding_size)
# 将输入进行堆叠,变成两张图,此时x_cat的尺寸为batch*2*91*91
x_cat = torch.cat((x, x), 1)
# 卷积后得到的尺寸为batch*1*31*31,卷积核尺寸为61*61,这里卷积核参数被设置为高斯核,即公式(7,8)中的距离核k
# 这里的卷积相当于论文中的公式(7,8)中的分母,采样图与距离核乘积再求和
p_filter = self.filter(x)
# 这里得到(2*batch)*1*91*91的矩阵,初始化的均匀网格图与采样图做点乘,相当于公式(7,8)中分子的一部分
x_mul = torch.mul(P, x_cat).view(-1, 1, self.global_size, self.global_size)
# 这里得到batch*2*31*31的矩阵,再次利用卷积操作实现求和再相加,最终得到公式(7,8)中的分子
all_filter = self.filter(x_mul).view(-1, 2, self.grid_size, self.grid_size)
# 将all_filter分离,分出x方向和y方向
x_filter = all_filter[:, 0, :, :].contiguous().view(-1, 1, self.grid_size, self.grid_size)
y_filter = all_filter[:, 1, :, :].contiguous().view(-1, 1, self.grid_size, self.grid_size)
# 将结果相除,分别得到f与v
x_filter = x_filter / p_filter
y_filter = y_filter / p_filter
xgrids = x_filter * 2 - 1
ygrids = y_filter * 2 - 1
# 范围控制到-1到1之间
xgrids = torch.clamp(xgrids, min=-1, max=1)
ygrids = torch.clamp(ygrids, min=-1, max=1)
xgrids = xgrids.view(-1, 1, self.grid_size, self.grid_size)
ygrids = ygrids.view(-1, 1, self.grid_size, self.grid_size)
# 此时grid为batch*2*31*31,为采样网格
grid = torch.cat((xgrids, ygrids), 1)
# 上采样,将梯度图放大
grid = F.interpolate(grid, size=(self.input_size_net, self.input_size_net), mode='bilinear', align_corners=True)
# 最终变成batch*448*448*2
grid = torch.transpose(grid, 1, 2)
grid = torch.transpose(grid, 2, 3)
return grid
def forward(self, x, p):
# x表示待采样的图像
# p表示采样图,通道数为1,宽高尺寸均为self.grid_size(之前设置好的)
# p:(batch, 1, self.grid_size, self.grid_size)
# 将采样图做一个填充,便于下面对u,v的计算
p = nn.ReplicationPad2d(self.padding_size)(p)
# 通过采样图,得到采样网格图
grid = self.create_grid(p)
# 利用得到的网格图对输入图像做一个网格采样,即显著采样过程
x_sampled = F.grid_sample(x, grid)
# 返回采样数据
return x_sampled
高斯核的生成
def makeGaussian(size, fwhm = 3, center=None):
# x为0到60,一共61个数
x = np.arange(0, size, 1, float)
# np.newaxis表示增加一个维度
# 此时y的维度为(61,1)
y = x[:,np.newaxis]
# 是否输入中心
if center is None:
x0 = y0 = size // 2
else:
x0 = center[0]
y0 = center[1]
# 计算高斯核
return np.exp(-4*np.log(2) * ((x-x0)**2 + (y-y0)**2) / fwhm**2)
以上内容仅是笔者的个人见解,若有问题或者不清楚的地方,欢迎沟通交流。