Part1:快速学习实现仿射变换

关于Deformable Convolutional Networks的论文解读,共分为5个部分,本章是第一部分:

  • Part1: 快速学习实现仿射变换
  • Part2: Spatial Transfomer Networks论文解读
  • Part3: TenosorFlow实现STN
  • Part4: Deformable Convolutional Networks论文解读
  • Part5: TensorFlow实现Deformable ConvNets

主要包括仿射变换原理、论文STN解读和实现、Deformable ConvNets论文解读和实现。

本章讲解线性变换到仿射变换,线性插值到双线性插值,使用双线性插值实现仿射变换,并给出代码测试。

图像变换

无论是STN还是Deformable ConvNets的原理都和仿射变换和双线性插值相关。我们先讲图像变换中常见的线性变换和我们关注的仿射变换。后面再讲双线性插值。

线性变换(linear transformations)

考虑到如下定义:

  • 点$K$的坐标为 $ \left[ \begin{matrix}
    x \\
    y \end{matrix} \right] $ 代表一个$(2×1)$的列向量
  • 矩阵$ M= \left[ \begin{matrix}
    a&b \\
    c&d \end{matrix} \right] $ 代表shape$(2×2)$的矩阵

我们通过$K’=T(K)=MK$定义线性变换$T$。其中$M$内的$a,b,c,d$是可变参数。

恒等变换:

令$a=d=1,b=c=0$,即$M=\begin{bmatrix}
1&0 \\
0&1 \end{bmatrix}$,则:

$$K’=\begin{bmatrix} 1&0 \\
0&1 \end{bmatrix} \begin{bmatrix} x \\
y \end{bmatrix}=\begin{bmatrix} x \\
y \end{bmatrix}=K$$即此时$M$的值表示做恒等变换

放缩:

示意图如下:


mark

令$b=c=0$,即$M=\begin{bmatrix} a&0 \\
0&d \end{bmatrix}$,则:

$$K’=\begin{bmatrix} a&0 \\
0&b \end{bmatrix} \begin{bmatrix} x \\
y \end{bmatrix}=\begin{bmatrix} ax \\
by \end{bmatrix}$$ 注意放缩有isotropic scaling(同向异性),表示关于$x,y$的放缩方向是相同的;也有anisotropic(各向异性),放缩方向相反。

旋转:

示意图如下:


mark

考虑到想旋转$\theta$,令$a=d=\cos \theta,-b=c=\sin \theta$,则$M=\begin{bmatrix} \cos \theta&-\sin \theta \\
\sin \theta&\cos \theta \end{bmatrix}$:

$$K’=\begin{bmatrix} \cos \theta& -\sin \theta \\
\sin \theta&\cos \theta \end{bmatrix} \begin{bmatrix} x \\
y \end{bmatrix}=\begin{bmatrix} x\cos \theta -y\sin \theta\\
x\sin \theta +y\cos \theta \end{bmatrix}$$

shear:

示意图如下:


mark

图片需要对$y$做关于$x$的偏移,对$x$做关于$y$的偏移。例如:字体做斜体操作。令$a=d=1,b=m,c=n$,即$M=\begin{bmatrix} 1&m \\
n&1 \end{bmatrix}$则:

$$K’=\begin{bmatrix} 1& m \\
n&1 \end{bmatrix} \begin{bmatrix} x \\
y \end{bmatrix}=\begin{bmatrix} x+my\\
y+nx \end{bmatrix}$$

总结一下,这里讲了3个基本的线性变换:

  • 放缩
  • shear
  • 旋转

我们可将这三个变换矩阵表示为$H,S,R$,则变换可写成:
$$K’=R[S(HK)]=MK$$
其中$M=RSH$,即用一个矩阵来表示各种线性变换。


仿射变换(Affine Transformation)

常见的图像变换如下:

mark

对于这个$2×2$矩阵,可完成线性变换,将图形扭曲成其他形状。但这样的变换存储一个缺点:不能做平移,故需要进一调整。

在计算图像学中做平移操作过程如下:
mark

可以看到是添加一个轴,再变换。对此将参数矩阵由2D换成3D:

  • 点$K$变成了$(3×1)$的列向量$\begin{bmatrix} x \\
    y \\
    1 \end{bmatrix}$
  • 为了表示变换,添加了两个新参数,矩阵$M=\begin{bmatrix} a&b&e \\
    c&d&f \\
    0&0&1 \end{bmatrix}$变成了shape$(3×3)$的矩阵

注意到,我们需要2D的输出,可将M改为$2×3$卷积形式。

例如,做平移操作:
$$K’=\begin{bmatrix} 1&0&\Delta \\
0&1&\Delta \end{bmatrix}\begin{bmatrix} x\\
y\\
1 \end{bmatrix}=\begin{bmatrix} x+\Delta \\
y+\Delta \end{bmatrix}$$

使用这样一个技巧,可通过一个新的变换表示所有变换,这即是仿射变换,我们可以一般化结果,这4中变换使用放射矩阵表示:
$$M=\begin{bmatrix} a&b&c \\
d&e&f \end{bmatrix}$$

总结来讲就是:仿射变换=线性变换+平移功能



双线性插值(Bilinear Interpolation)

考虑到当我们做仿射变换时:例如旋转或放缩,图片中的像素会移动到其他地方。这会暴露出一个问题,输出中的像素位置可能没有对应的输入图片中的位置。 下面的旋转示例,可以清晰的看到输出中有些点没有在对应棋盘网格中央,这意味着输入中没有对应的像素点:

mark

为了支持这样输出是分数坐标点的,可使用双线性插值去寻找合适的颜色值。

线性插值

要说双线性插值,先看看线性插值。 已知坐标$(x_0,y_0)$和$(x_1,y_1)$,需要在$[x_0,x_1]$之间$x$插值,如下:

mark

两点之间的线性方程为:$$y-y_0=(x-x_0)\frac{y_1-y_0}{x_1-x_0}$$

变换一下上述公式即: $$y=y_0\frac{x_1-x}{x_1-x_0}+y_1\frac{x-x_0}{x_1-x_0}$$

双线性插值

双线性插值是线性插值的拓展~
4个像素点坐标为 $Q_{11}(x_{1},y_{1}),Q_{12}(x_{1},y_{2}),Q_{21}(x_{2},y_{1}),Q_{22}(x_{2},y_{2})$,像素值为$f(Q_{11}),f(Q_{12}),f(Q_{21}),f(Q_{22})$:

mark

先是线性插值获得$R_1(x,y_1),R_2(x,y_2)$:

$$f(R_1)=f(Q_{11})\frac{x_2-x}{x_2-x_1}+f(Q_{21})\frac{x-x_1}{x_2-x_1} \tag 1$$

$$f(R_2)=f(Q_{12})\frac{x_2-x}{x_2-x_1}+f(Q_{22})\frac{x-x_1}{x_2-x_1} \tag 2$$

再使用$R_1,R_2$纵向插值得到$P(x,y)$:
$$f(P)=f(R_1)\frac{y_2-y}{y_2-y_1}+f(R_2)\frac{y-y_1}{y_2-y_1} \tag 3$$

在像素计算中,通常是以4个相邻的像素点做插值,故所有分母项都为1,联立$(1)(2)(3)$可得:
$$f(P)=f(Q_{11})(x_2-x)(y_2-y)+f(Q_{21})(x-x_1)(y_2-y)+f(Q_{12})(x_2-x)(y-y_1)+f(Q_{22})(x-x_1)(y-y_1)$$

可以将公式化为:
$$ f(P)=[(x_2-x),(x-x_1)] \begin{bmatrix}
f(Q_{11}) &f(Q_{12})\\
f(Q_{21}) & f(Q_{22})
\end{bmatrix} [(y_2-y),(y-y_1)]$$



仿射变换代码测试

前面讲完了仿射变换和双线性插值,我们整理一下:仿射变换是我们的目的,双线性插值是帮助我们在图像上实现仿射变换。

在写代码前,理一下在图片上做仿射变换的思路,通常分为3个步骤:

  • 首先,创建采样网格(sampling grid)。网格和输入特征映射相同空间大小的棋盘网格(标注各个坐标点,用于存储仿射变换后的坐标点)
  • 其次,将仿射转换应用于上步生成的sampling grid,得到实际的采样坐标
  • 最后,使用双线性插值技术从原始图片上按照实际采样坐标采样得到最终结果

所有的代码可从我的github上下载,使用Jupyter环境~

准备资源

从Github上把图片clone下来,加载图片并组成4D的张量,

  • 导入包,注意utils是github上clone下来
  • 图片放在data目录下,使用img_to_array()加载图片为numpy arrays
  • 将图片按batch组合,同时处理多张图片,扩充程序的扩展性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np
from PIL import Image
from utils import img_to_array,array_to_img,visualize_grid,view_images
# params
DIMS = (400, 400)
CAT1 = 'cat1.jpg'
CAT2 = 'cat2.jpg'
data_path = './data/'
# 加载两张小猫图片
img1 = img_to_array(data_path + CAT1, DIMS)
img2 = img_to_array(data_path + CAT2, DIMS, view=True)
# 联合两张图片到一个batch,shape为(2, 400, 400, 3)
input_img = np.concatenate([img1, img2], axis=0)
print(img2.shape)
print("Input Img Shape: {}".format(input_img.shape))

输出:

1
2
(1, 400, 400, 3)
Input Img Shape: (2, 400, 400, 3)

原图:
mark

构建仿射变换生成采样矩阵

affine_grid_generator(height, width, M)为仿射变换函数:

  • height,width:为图片长和宽
  • M: 仿射矩阵。 shape为(num_batch, 2, 3).
  • Return: 返回sampling grid。shape为(num_batch, H, W, 2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def affine_grid_generator(height, width, M):
num_batch = M.shape[0] # 获取batchsize
# 创建棋盘grid,平分整个图片
x = np.linspace(-1, 1, width)
y = np.linspace(-1, 1, height)
x_t, y_t = np.meshgrid(x, y)
# 制作原始坐标点的列向量(xt, yt, 1)
ones = np.ones(np.prod(x_t.shape))
sampling_grid = np.vstack([x_t.flatten(), y_t.flatten(), ones])
# 把所有像素整到一起 sampling_grid的shape为(batch, 3, H*W)
sampling_grid = np.resize(sampling_grid, (num_batch, 3, height*width)) # 列向量扩展batch
# 做仿射矩阵运算 M*K
batch_grids = np.matmul(M, sampling_grid)
# batch grid 的 shape (num_batch, 2, H*W)
# 将仿射结果中H,W拆开
batch_grids = batch_grids.reshape(num_batch, 2, height, width)
batch_grids = np.moveaxis(batch_grids, 1, -1) #调整为(num_batch, H, W, 2)
# sanity check
print("Transformation Matrices: {}".format(M.shape))
print("Sampling Grid: {}".format(sampling_grid.shape))
print("Batch Grids: {}".format(batch_grids.shape))
return batch_grids

双线性插值函数

bilinear_sampler(input_img, x, y)为双线性采样:

  • input_img:采样的原始图片,(B, H, W, C)
  • x,y: ` 仿射变换后对应的采样点的x,y坐标
  • Return:采样后的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def bilinear_sampler(input_img, x, y):
"""
如果想要测试是否正常,将仿射变换改为恒等变换,查看是输入和输出
"""
# grab dimensions
B, H, W, C = input_img.shape
# 原本x,y在[-1,1]之间,现在放缩到 [0, W/H]
x = ((x + 1.) * W) * 0.5
y = ((y + 1.) * H) * 0.5
# 获取到每个(x_i, y_i)相邻的4个坐标
x0 = np.floor(x).astype(np.int64)
x1 = x0 + 1
y0 = np.floor(y).astype(np.int64)
y1 = y0 + 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)
# 取出对应的像素值
Ia = input_img[np.arange(B)[:,None,None], y0, x0]
Ib = input_img[np.arange(B)[:,None,None], y1, x0]
Ic = input_img[np.arange(B)[:,None,None], y0, x1]
Id = input_img[np.arange(B)[:,None,None], y1, x1]
# 依据双线性插值公式,计算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
return out

测试

设置仿射矩阵:

  • 设置仿射矩阵的值,对应不同的仿射变换
  • 调节仿射矩阵的batch维度
1
2
3
4
5
6
7
8
9
10
B, H, W, C = input_img.shape
# 设置仿射矩阵,依据上面的理论可自由调整
#M = np.array([[1., 0., 0.], [0., 1., 0.]]) 恒等变换
M = np.array([[1., 0., 0.5], [0., 1., 0.]]) #平移
#M = np.array([[0.707, -0.707, 0.], [0.707, 0.707, 0.]]) #旋转
# 调整维度
M = np.resize(M, (B, 2, 3))
# print (M.shape) # (2, 2, 3)

获取仿射矩阵采样点,采样:

  • 获取仿射矩阵生成的采样点
  • 将x,y维度拆分
  • 使用双线性插值采样
1
2
3
4
5
6
7
8
9
batch_grids = affine_grid_generator(H, W, M)
x_s = batch_grids[:, :, :, 0:1].squeeze()
y_s = batch_grids[:, :, :, 1:2].squeeze()
out = bilinear_sampler(input_img, x_s, y_s)
out = array_to_img(out[-1])
out.show()

输出:

1
2
3
Transformation Matrices: (2, 2, 3)
Sampling Grid: (2, 3, 160000)
Batch Grids: (2, 400, 400, 2)

原图:
mark

平移操作:
mark

旋转操作:
mark



参考资料



PS:双线性插值放缩图片时参考坐标

在实际的计算过程中,图像的左上角是坐标原点,我们记原始图像大小为$(H_s,W_s)$,目标图像大小为$(H_d,W_d)$,对应的长宽比例为$\frac{H_s}{H_d},\frac{W_s}{W_d}$

原图和目标图之间的对齐方式有两种:

  • 原点对齐方式:
    mark

此时对于目标图中的点$(i,j)$,对应的原图的为$$(i\frac{H_s}{H_d},j\frac{W_s}{W_d})$$

  • 中心对齐方式:
    mark

此时对于目标图中的点$(i,j)$,对应的原图的为$$((i+0.5)\frac{H_s}{H_d}-0.5,(j+0.5)\frac{W_s}{W_d}-0.5)$$

对于中心对齐,相当于增加了一个$0.5(\frac{H_s}{H_d}-1)$的偏移量,无论是放大还是缩小图片,都会让变换尽量的以中心对齐。

实际计算中由对齐方式计算出对应的坐标点,然后套用前面的双线性插值公式得到结果。

Thanks for your support!