一、相关知识
参考https://kevinzakka.github.io/2017/01/10/stn-part1/
Spatial Transformer Network是一个可学习的模块,旨在以可计算和可参数化的方式增加卷积神经网络Convolutional Neural Networks的空间不变性spatial invariance。
这一章节主要是为了介绍空间变换中的仿射变换affine transformations,和双线性插值bilinear interpolation。
1.1图像变换
图像变换image transformations,里面包含了放缩、旋转、裁剪、平移等,这是为affine transformations的背景知识。为了简化理解,限定图像变换在二维图像上进行操作,从线性变换开始。
定义:
- 坐标为xy的点K表示为2×1的列向量。
- 矩阵M=abcd表示一个2×2的方阵
通过改变参数M的参数a、b、c、d来研究矩阵积(matrix product)K'=TK=MK定义的线性变换T(linear transformation),
-
-
- 恒等变换
-
K'=MK=1001xy=xy=K ; a=d=1 and b=c=0
恒等变换identity transform就是说点K在变换后,坐标位置并没有在平面plane上移动。
1.1.2、放缩
K'=MK=p00qxy=pxqy ; a=p and d=q and b=c=0
a和b都是大于零的正数,各向同性放缩(isotropic scaling)是指x,y方向上的放缩因子(scaling factor)是相同的s,s>0就是图像放大enlarging,s<0就是图像缩小shrinking。
1.1.3、旋转
旋转变化的还是x、y分开计算,分别投影到x方向和y方向就可以了。
1.1.4、错切
所谓错切实际上就是x方向的变化与y成比例关系,y方向的变化与x成比例关系。
- 缩放scaling:x和y放线按照标量放缩scales
- 错切shearing:x的平移量与y成正比,y的平移量与x成正比。
- 旋转rotating:按照原点角度θ进行旋转。
我们可以将空间变换操作写成以下形式:
K'=R[SHK]
遵循相应的变换顺序,其中K是我们之前的坐标点错切shearing变换,是尺度变换,是旋转变换,因为矩阵相乘满足结合律associative,即M=RSH,要注意相关顺序,因为矩阵乘法是不满足交换律的commutative。
K'=MK
由此我们将简单的空间变换组合成了矩阵相乘的形式,运用到高维空间时,相应的矩阵乘法依旧成立。
1.1.5、平移
平移translation变换要稍微特殊一定,之前的变换都是通过2x2的变换矩阵来实现线性变换的,但实际上平移translation不是线性变换,我们希望能将其包含在变换矩阵当中。
为了解决这个难题dilemna,我们试用齐次坐标homogeneous coordinates将2D坐标向量转换成3D的坐标向量。
- 坐标点K变成3×1的列向量column vector
- 变换矩阵M相应的变成3×3的方阵square matrix
为了可以表示平移变换,我们将方阵M的第三列修改为,之后我们就可以通过在齐次坐标homogeneous cooridinates中通过线性变换来实现平移了。有一个地方需要注意的是,如果我们希望输出的仍然是一个2D的坐标向量xy,我们就需要将M修改为2×3的矩阵。
例如我们在x、y方向都做∆大小的平移,那么
四种线性变换(线性变换都叫仿射变换affine transformation)如下图,而四种仿射变换矩阵如上面M所示,包含六个变量:
1.2 双线性插值
双线性插值bilinear interpolation设计的出发点是,当图像经过仿射变换是affine transformation,在输出图中的坐标点没有和输入图像存在对应关系。
上图经历了旋转变换,我们发现交叉点不再位于正方形的中心,这意味着旋转后的点在原图中这些点没有像素值
例如产生的新坐标位置是6.7,3.2这样的小数像素位置fractional pixel locations没办法和图像坐标点对应起来的。为了解决这个问题,双线性插值引入了在6.7,3.2这样指定位置四个对角方向diagonal directions作为产生该坐标点灰度值的依据(原图上)。
我们的目标是希望找到坐标点P的像素值,因此我们分别使用Q11,Q21和Q12,Q22的加权平均值weighted average来计算R1和R2,然后我们再利用R1和R2的加权平均值weighted average计算P点的像素值。
实际上是在x方向和y方向上分别插值interpolating,因此叫做双线性插值bilinear interpolation。如果改变插值顺序得到的结果是一样的。
给定点P=x,y和它相应的4个角点corner coordinates、、、。首先对x方向插值:
上述关系的得来可以逆推例如:,随后在y方向上插值:
1.3、实现
仿射变换affine transformation一般会遵循以下三个步骤:
- 首先创建(x,y)坐标组成的采样网格sampling grid,创建一个相同维度的坐标网格meshgrid。x∈0,W,y∈0,H
- 然后我们把变换矩阵transformation matrix应用到主权的采样网格sampling grid
- 最后,我们使用期望的插值技术interpolation从原图利用网格采样。
这种方法与直接将变换矩阵用在原图上有所不同
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from utils import img2array, array2img,deg2rad
#params
DIMS = (400, 400)
CAT1 = '../data/cat1.jpg'
CAT2 = '../data/cat2.jpg'
#load both cat images
img1 = img2array(CAT1, DIMS, expand=True)
img2 = img2array(CAT2, DIMS, expand=True)
# array2img(img1[0]).show()
# array2img(img2[0]).show()
# print(img1.shape)
#concat into tensor of shape(2,40,40,3)#批量尺寸是2,这意味着我没要对批量中的每个图像都运用相同的变换矩阵transformation matrices M
input_img = np.concatenate([img1, img2], axis=0)
#dimension sanity check
print("Input Img Shape: {}".format(input_img.shape))
###########################################
#grab shape
num_batch, H, W, C = input_img.shape
#initialize M to indentity transform初始化一个恒等矩阵,如果双线性插值是正确的,那么输入的图像应当和输出的图像基本上是一样的
# M = np.array([[1., 0., 0.],[0.,1.,0.]])#恒等变换Identity Transform
# M = np.array([[1., 0., 0.5],[0.,1.,0.5]])#平移变换Translation Transform
M = np.array([[0.707, -0.707, 0.],[0.707,0.707,0.]])#旋转变换Rotation Transform
#repeat num_batch times
M = np.resize(M, (num_batch, 2, 3))#2x3的矩阵是包含平移变换的。
###########################################
#生成一个meshgrid,然后从生成的meshgird和变换矩阵transformation matrix M中生成采样网格的结果。
#creat normalized 2D grid
x = np.linspace(-1, 1, W)#创建一个均匀分布在-1~1之间的normalized one,个数分别是W和H个
y = np.linspace(-1, 1, H)#相对图像坐标系而言x对应的是宽度,即矩阵的列数,y对应的是图像的高度,即矩阵的行数
x_t, y_t = np.meshgrid(x,y)#x_t为x行向量纵向排列y个,y_t为y列向量横向排列x个
# plt.plot(x_t, y_t,
# color='red', # 全部点设置为红色
# marker='.', # 点的形状为圆点
# linestyle='') # 线型为空,也即点与点之间不用线连接
# plt.grid(True)
# plt.show()
###########################################
#需要增加网格的维度来创建齐次坐标homogeneous coordinates
#reshape to (x_t, y_t, 1)
ones = np.ones(np.prod(x_t.shape))#计算x_t维数的乘积,并以此创建全1的行向量
# print(ones.shape)
sampling_grid = np.vstack([x_t.flatten(),y_t.flatten(),ones])#将各维度行向量沿垂直方向放置。即[x;y;1]的列向量
###########################################
#上面只是创建了1个采样网格,我们还需要复制到各个批次
#repeat grid num_batch times
sampling_grid = np.resize(sampling_grid,(num_batch,3,H*W))
###########################################
#将变换矩阵运用到采样网格上
#transform the sampling grid i,e, batch multiply
batch_grids = np.matmul(M, sampling_grid)#(num_batch,2,3)*(num_batch,3,H*W)=(num_batch,2,H*W)
#batch grid has shape(num_batch, 2, H*W)
#reshape to (num_batch, height, width, 2)
batch_grids = batch_grids.reshape(num_batch, 2, H, W)
batch_grids = np.moveaxis(batch_grids, 1, -1)#将第2维和最后1维进行换位,为后面拆分x、y做准备
#(num_batch, width, height, 2)宽度是x,高度是y
# print(batch_grids.shape)
###########################################
#这一步编写双线性采样器bilinear sampler,在给定的采样网格中插入原始图像的像素值。
#将x和y维度的数据拆分开来,然后重新放缩到height/width范围内
x_s = batch_grids[:, :, :, 0:1].squeeze()
y_s = batch_grids[:, :, :, 1:2].squeeze()
#rescale x and y to [0, W/H]
x = ((x_s + 1.) * W) * 0.5
y = ((y_s + 1.) * W) * 0.5
# plt.plot(batch_grids[0,:,:,0], batch_grids[0,:,:,1],
# color='red', # 全部点设置为红色
# marker='.', # 点的形状为圆点
# linestyle='') # 线型为空,也即点与点之间不用线连接
# plt.grid(True)
# plt.show()
###########################################
#现在对于每个坐标(x_i, y_i)我们希望获取4个角点坐标
#grab 4 nearest corner points for each(x_i,y_i)
x0 = np.floor(x).astype(np.int64)
x1 = x0 + 1
y0 = np.floor(y).astype(np.int64)#ceiling function
y1 = y0 +1
###########################################
#保证x,y坐标不超过W-1,H-1,对于大于的数据进行截断
#make sure it's inside img range [0,H] or [0, W]
x0 = np.clip(x0, 0, W-1)
x1 = np.clip(x1, 0, W-1)
y0 = np.clip(y0, 0, H-1)
y1 = np.clip(y1, 0, H-1)
###########################################
#(x0, y0), (x0, y1), (x1, y0), (x1, y1)
#look up pixel values at corner coords
Ia = input_img[np.arange(num_batch)[:,None,None], y0, x0]
Ib = input_img[np.arange(num_batch)[:,None,None], y1, x0]
Ic = input_img[np.arange(num_batch)[:,None,None], y0, x1]
Id = input_img[np.arange(num_batch)[:,None,None], y1, x1]
###########################################
#计算权重系数
#calculate deltas
wa = (x1 - x) * (y1 - y)
wb = (x1 - x) * (y - y0)
wc = (x - x0) * (y1 - y)
wd = (x - x0) * (y - y0)
###########################################
#根据之前提到的公式做乘法和加法
#add dimension for addition
wa = np.expand_dims(wa, axis=3)
wb = np.expand_dims(wb, axis=3)
wc = np.expand_dims(wc, axis=3)
wd = np.expand_dims(wd, axis=3)
#compute output
out = wa*Ia + wb*Ib + wc*Ic + wd*Id
###########################################
print(out.shape)
plt.imshow(out[0,:,:,:])
plt.show()
二 、 Spatial Transformer NetWorks
参考:https://kevinzakka.github.io/2017/01/18/stn-part2/
2.1、设计目的
我们期望在分类任务中系统对于任意输入是鲁棒的,也就是说如果输入经过某些变换,输出的结果应当也是一致的。
- 尺度变化scale variation.
- 观察点的变化viewpoint variation:相当于旋转和平移了。
- 形变deformation:非刚体non rigid bodies可以形变deformed和扭曲twisted成不寻常的形状
虽然相对于人来辨别的时候这些变化影响很小,但计算机输入的是灰度值,当产生变换的时候,影响的将是图像中的所有像素。因而理想的分类模型应该可以从纹理和形状中分离出对象的姿态和形变。
如果模型可以使用一些裁剪和尺度归一化的组合来简化后续的分类任务就非常棒了。
2.2 池化层
CNNs中的最大池化层(local max-pooling layers)允许处理输入特征一定程度的空间形变。
(b)
(c)
图2.1、max pooling(2x2 stride=2)的平移不变性、旋转不变性、尺度不变性
为什么它能够提供不变性呢?池化pooling输入是非常复杂的,它会将输入首先划分到不同的单元,然后对这些复杂的单元进行池化pool,输出更简单的单元。例如3张不同方向的数字7,我们在每张图上池化每个小网格来检测数字7,从而忽略它在网格中的位置,然后通过聚集像素值来获取近似的结果。
但是池化也有一些缺点,比如他会破坏原有信息destructive。2*2的max pooling中会丢掉75%的信息,这使得我们肯定会丢失准确的位置信息。但是在视觉识别任务中位置信息非常重要。
另外池化pooling的一个缺点是它需要定位和预先定义local and predefined。一个较小的接受域内,在池化pooling操作的影响只是在网络深层上,这意味着中间过程的特征图也许已经收到了输入图像很大程度上的失真。我们也不可能随意的增加池化的接受域。
卷及网络ConvNets在输入失真的时候结果并不是一成不变的,它仅有的限制是在一个有限制的、预定义的池化机制来处理数据的空间变化。这里就是spatial transformer network起作用的地方。
2.3、Spatial Transformer Networks(STNs)
空间转换器机制Spatial transformer 通过显式空间转换能力的卷积网络来解决上述问题。它有三个显著的属性:
- 模块化modular:STNs可以通过简单的操作就嵌入到现有的结构体系中。
- 可微分differentiable:STNs可以在端到端的训练中进行反向传播。
- 动态的dynamic:相对于池化操作pooling在所有输入样本上做相同操作,STNs可以在每个输入样本的特征图上激活空间变换
如图2.1,Localisation net使用来计算空间形变参数θ的网络,可以使CNNs也可以是全连接层.G是输出V的标准采样网格(即普通的坐标网格),TθG是由采样网格G和变换参数形成的变换网格,它是由网格生成器生成的,最后由输入U和变换的采样网格通过采样生成器Sampler生成输出V。Localisation net和Sampler采样机制定义了空间变换。
图2.2、空间形变模块的结构
- 首先创建(x,y)坐标组成的采样网格sampling grid,创建一个相同维度的坐标网格meshgrid。x∈0,W,y∈0,H
- 然后我们把变换矩阵transformation matrix应用到主权的采样网格sampling grid
- 最后,我们使用期望的插值技术interpolation从原图利用网格采样。
实际上就是将空间变换参数变换到所有批量的坐标网格上,然后利用插值技术从原图像上相应位置领域灰度值生成当前点的灰度值。
2.3.1 局部化网络
局部化网络的目的是输出仿射变换affine transformation参数θ,局部化网路localisation net定义如下:
- 输入input:形如(H, W , C)的特征图U
- 输出output:形状为(6,)的变换行向量
- 结构architecture:全连接层网络fully-connected network或者卷积网络ConvNet都可以。
我们希望通过训练可以输出更精确的空间变换参数θ,如何称之为更精确,比如说我们有一个数字7逆时针旋转了90°的图像,经过2 epochs后可能顺时针旋转了45°,5 epochs后也许就学会了精确的90°旋转,这样就变成了我们所见的正常数字7的形状,更容易训练。
另一方面,通过存储层之间的权重值,局部化网络localisation network学会了如何旋转每个训练样本的知识。
2.3.2、参数化采样网格
网格生成器的作用是输出一个参数化采样网格parametrised sampling grid,输入图像会通过这些采样点输出到期望的变换后图像。
准确的来说,网格生成器grid generator首先创建一个形状和输入图像(H,W)形状一致的标准化网格normalized meshgrid,即一组覆盖整个输入特征图的索引(xt,yt)(这里上标t表示在输出特征图上的目标坐标target),然后我们将放射变换运用到这个网格上,因为想引入平移变换,因此通过在坐标列向量中增加一行全1的行向量来得到齐次等价homogeneous equivalent。最后我们重排列6参数θ到2*3的矩阵,虽有执行矩阵乘法活的到期望的参数化采样网格parametrised sampling grid。
列向量xsys由一系列的索引组成,这告诉我们如何在输入图像上采样生成我们期望变换后的图像。如果这些在原图上的坐标索引是小数fractional形式的话,就可以利用双线性插值方式。
2.3.3、可微分的图像采样
双线性插值bilinear interpolation是可微的differentiable。利用输入的特征图feature map和生成的参数化采样网格parametrised sampling grid,随后在利用双线性采样bilinear sampling就可以获得输出的特征图V(H’, W’, C’),通过指定采样网格sampling grid形式我们也是可以执行上采样和下采样的。
也不一定是双线性采样bilinear sampling,也可以是其他的采样核,但是前提是其他的采样核也必须是可微分的,允许损失函数的梯度可以回传到我们的局部化网络localisation network。
运用采样网格到输入图像U输出图像V。(a)是恒等变换(U=V)(b)是仿射变换(rotation)