做图像处理或者数据增强的过程中,经常需要用得各种变换来处理图片。本文详细的说明了线性变换、仿射变换、透视变换的定义、几何意义、学习表达。重点给出透视变换的计算过程,并给出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线性变换的几何意义

满足如下几何性质

  1. 变换前是直线的,变换后依然是直线
  2. 直线比例保持不变
  3. 变换前是原点的,变换后依然是原点

二、仿射变换

2.1 仿射变换的几何意义

仿射变换从几何意义看不需要满足线性变换的第三点,即仿射变换满足如下两点即可

  1. 变换前是直线的,变换后依然是直线
  2. 直线比例保持不变

三、常见的变换

  1. (Identity)恒等变换
  2. (Translation)平移变换
  3. (Rotation)旋转变换
  4. (Scaling)尺度变换
  5. (Reflection)反射变换
  6. (Shear)错切 示意图如下 image.png 图片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)

原图: image.png 变换之后的 image.png 这其实是一个错切变换,由于b=0,所以没有发生平移

五、透视变换

透视变换分为两个步骤

  1. 二维空间的点变到三维空间 把任意二维空间的点(u,v)变换到三维空间点(x, y, z) image.png 2、把三维空间再变换二维空间 然后再把(x, y, z)除以z得到$(x^{’}, y^{’}) $ 下面的两个方程的中$k_{31}系数后面少了u$ image.png 综上两个步骤透视变换是把二维空间的点(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个点就可以求解上面的方程。推理过程如下: image.png 把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)

输出如下: image.png 可以看到和上面我们自已实现得出的结果是一样的,只不过我们返回的一维向量,opencv返回的3*3的矩阵 我们再回过头来看一下一开始透视变换矩阵

image.png

我们把矩阵分为A、B、C三个部分。可以知道A是用来做线性变换的,B是用来做平移的,C是用来做透视变换的,至于$a_{33}$是没什么用处的,通常可以设置为1

六、常见的变换矩阵

在3.1节我们列出了6种变换,下面出他们的变换矩阵

    1. 恒等变换 image.png
    1. 平移变换 image.png
    1. 旋转变换 旋转45度 image.png
    1. 尺度变换 image.png
    1. 反射变换 下图是左右变换矩阵,请自行推导变换矩阵 image.png
    1. 错切 错切45度,请自行推导变换矩阵,提示:$tan(45^o) = 1$ image.png

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)

image.png

先平移再旋转 仅交换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)

image.png

在计算的过程中,为了方便计算,我们在变换矩阵(仿射变换(2*3))的下面concat一个 $[[0, 0, 1]]$矩阵,方便矩阵的乘积。 注意矩阵乘积的循序对最终变换结果的影响

参考文献

  1. https://www.tutorialspoint.com/dip/perspective_transformation.htm
  2. https://www.sciencedirect.com/topics/computer-science/perspective-transformation
  3. https://web.archive.org/web/20150222120106/xenia.media.mit.edu/~cwren/interpolator/
  4. 如何通俗地讲解「仿射变换」这个概念?
  5. Perspective Transform Estimation