做图像处理或者数据增强的过程中,经常需要用得各种变换来处理图片。本文详细的说明了线性变换、仿射变换、透视变换的定义、几何意义、学习表达。重点给出透视变换的计算过程,并给出python实现代码。经验证和opencv的结果是一样的。 虽然opencv或者其他的库有现成的函数可供调用,但是我们还是需要明白这些函数输出的意义。比如opencv的getAffineTransform返回一个2*3的矩阵,这个2*3矩阵的意义是什么? 线性变换是仿射变换的特例,仿射变换是透视变换的特例
一、线性变换
1.1线性变换的定义
如果$f: V->W$满足如下两条性质,那么$f$就是线性变换
- 可加性(additivity):$f(x+y) = f(x)+f(y)$
- 齐次性(homogeneity):$f(ax) = af(x)$ 当然也可以把这两个性质合并一下, 对任意的$a$下式总成立: $f(x+ay)=f(x)+af(y)$
那么思考一下下面两个变换是不是线性变换 (1)$f(x,y) = x+2y$ (2)$f(x,y) = x+1$ 第一式子是线性变换,但是第二个式子是不满足齐次性的。因为$f(ax)=ax+1 ≠ a(x+1)$ 从齐次性可以看出,线性变换一定是过零点的
1.2线性变换的几何意义
满足如下几何性质
- 变换前是直线的,变换后依然是直线
- 直线比例保持不变
- 变换前是原点的,变换后依然是原点
二、仿射变换
2.1 仿射变换的几何意义
仿射变换从几何意义看不需要满足线性变换的第三点,即仿射变换满足如下两点即可
- 变换前是直线的,变换后依然是直线
- 直线比例保持不变
三、常见的变换
- (Identity)恒等变换
- (Translation)平移变换
- (Rotation)旋转变换
- (Scaling)尺度变换
- (Reflection)反射变换
- (Shear)错切
示意图如下
图片from 仿射变换及其变换矩阵的理解 在上面所有的变换中只有平移变换不是线性变换(不满足齐次性),其他的都是线性变换 所以仿射变换就是线性变换+平移
四、仿射变换和线性变换的数学表达
$Y = AX+b$ 其中A是一个m*n的矩阵,b是一个n维的向量。 其中 A表示线性变换,b表示平移 。A,b合在一起就可以表示一个仿射变换 上式可以写如下两个形式,用矩阵的方式方便计算 1、
$$Y = [A|b]*\left[\begin{matrix}X\\1 \end{matrix}\right]$$
2、 $$ \left[\begin{matrix}Y\\1 \end{matrix}\right] = \left[\begin{matrix}A & b \\0 & 1 \end{matrix}\right] * \left[\begin{matrix}X\\1 \end{matrix}\right] $$
在二维平面上,三个点就可以确定一个平面。我们用opencv来简单是实现一下方式变换的效果。
import cv2
import numpy as np
from PIL import Image
// 加载图片
image_path = '/Users/zhangpan/Pictures/temp/lena.jpeg'
image = cv2.imread(image_path)
// 原图片上的三个点
affine_transform_point0 = np.float32([[0,0],[0,100],[100,0]])
// 仿射变换以后的三个点
affine_transform_point1 = np.float32([[0,0],[100,100],[100,0]])
// M 就是仿射变换的矩阵
M = cv2.getAffineTransform(affine_transform_point0, affine_transform_point1)
print(M)
仿射变换矩阵M的输出如下,其中$A = M[:,:2], b = M[:, 2]$
array([[1., 1., 0.],
[0., 1., 0.]])
将仿射矩阵应用于图片:
perspective_image = cv2.warpPerspective(np.array(image), M_P, (400,200))
Image.fromarray(affined_image)
原图:
变换之后的
这其实是一个错切变换,由于b=0,所以没有发生平移
五、透视变换
透视变换分为两个步骤
- 二维空间的点变到三维空间
把任意二维空间的点(u,v)变换到三维空间点(x, y, z)
2、把三维空间再变换二维空间 然后再把(x, y, z)除以z得到$(x^{’}, y^{’}) $ 下面的两个方程的中$k_{31}系数后面少了u$
综上两个步骤透视变换是把二维空间的点(u,v)变换到二维空间的点$(x^{’}, y^{’}) $,只不过二维空间以及不是从前那个二维空间了。下面我们重点来求解一下上面的方程。$(u,v), (x^{’}, y^{’}) $是已知的变换前后点。$k_{11},k_{12},k_{13},k_{21},k_{22},k_{23},k_{31},k_{32}$这8个参数是未知的。我们知道求解8个未知数的参数方程至少需要8个方程。所以只需要变换前后的4个点就可以求解上面的方程。推理过程如下:
把4个变换前后的点安装上面矩阵的排列进去就可以求解了,下面给出python代码的实现。
def find_coeffs(pa, pb):
matrix = []
for p1, p2 in zip(pa, pb):
matrix.append([p1[0], p1[1], 1, 0, 0, 0, -p2[0] * p1[0], -p2[0] * p1[1]])
matrix.append([0, 0, 0, p1[0], p1[1], 1, -p2[1] * p1[0], -p2[1] * p1[1]])
A = np.matrix(matrix, dtype=np.float)
B = np.array(pb).reshape(8)
// np.linalg.inv 矩阵求逆
res = np.dot(np.linalg.inv(A.T * A) * A.T, B)
return np.array(res).reshape(8)
举例说明
perspective_transform_point0 = np.float32([[0,0],[0,100],[100,0], [100,100]])
perspective_transform_point1 = np.float32([[20,20],[100,100],[100,0], [300,100]])
coeffs = find_coeffs(perspective_transform_point0, perspective_transform_point1)
print(coeffs)
输出如下:
[ 1.00000000e+00 5.00000000e-01 4.29278149e-12 -2.59463132e-15
5.00000000e-01 2.70920439e-13 -2.39711063e-17 -5.00000000e-03]
保留两位小数
print(np.round(coeffs, 2))
[ 1. 0.5 0. -0. 0.5 0. -0. -0.01]
opencv 中同样提供求解透视变换矩阵的方法,我们验证一下
perspective_transform_point0 = np.float32([[0,0],[0,100],[100,0], [100,100]])
perspective_transform_point1 = np.float32([[0,0],[100,100],[100,0], [300,100]])
M_P = cv2.getPerspectiveTransform(perspective_transform_point0, perspective_transform_point1)
print(M_P)
输出如下:
可以看到和上面我们自已实现得出的结果是一样的,只不过我们返回的一维向量,opencv返回的3*3的矩阵
我们再回过头来看一下一开始透视变换矩阵
我们把矩阵分为A、B、C三个部分。可以知道A是用来做线性变换的,B是用来做平移的,C是用来做透视变换的,至于$a_{33}$是没什么用处的,通常可以设置为1
六、常见的变换矩阵
在3.1节我们列出了6种变换,下面出他们的变换矩阵
- 恒等变换
- 恒等变换
- 平移变换
- 平移变换
- 旋转变换
旋转45度
- 旋转变换
旋转45度
- 尺度变换
- 尺度变换
- 反射变换
下图是左右变换矩阵,请自行推导变换矩阵
- 反射变换
下图是左右变换矩阵,请自行推导变换矩阵
- 错切
错切45度,请自行推导变换矩阵,提示:$tan(45^o) = 1$
- 错切
错切45度,请自行推导变换矩阵,提示:$tan(45^o) = 1$
6.1 变换矩阵的乘积
将变换矩阵应用于坐标的公式为$AX+b$, 即坐标向量左乘变换矩阵。
先旋转再平移
M = np.float32([[1, 0, 100],[-0, 1, 0]]) # x 方向平移100
M_rotate = np.float32([[math.sqrt(2)/2, math.sqrt(2)/2, 0],[-math.sqrt(2)/2, math.sqrt(2)/2, 0]]) # 逆时针旋转45度
M1 = np.concatenate((M, np.array([[0,0,1]])), 0)
M2 = np.concatenate((M_rotate, np.array([[0,0,1]])), 0)
M_T = M1 @ M2
image_rotate = cv2.warpAffine(np.array(image), M_T[:2], (400,400))
Image.fromarray(image_rotate)
先平移再旋转 仅交换M1,M2的循序
M = np.float32([[1, 0, 100],[-0, 1, 0]]) # x 方向平移100
M_rotate = np.float32([[math.sqrt(2)/2, math.sqrt(2)/2, 0],[-math.sqrt(2)/2, math.sqrt(2)/2, 0]]) # 逆时针旋转45度
M1 = np.concatenate((M, np.array([[0,0,1]])), 0)
M2 = np.concatenate((M_rotate, np.array([[0,0,1]])), 0)
M_T = M2 @ M1
image_rotate = cv2.warpAffine(np.array(image), M_T[:2], (400,400))
Image.fromarray(image_rotate)
在计算的过程中,为了方便计算,我们在变换矩阵(仿射变换(2*3))的下面concat一个 $[[0, 0, 1]]$矩阵,方便矩阵的乘积。 注意矩阵乘积的循序对最终变换结果的影响