你好,我是朱维刚。欢迎你继续跟我学习线性代数,今天我们要讲的内容是“仿射空间”。

一听到仿射空间,你也许会觉得很奇怪,之前我们说过了线性空间,现在怎么又来一个空间?特别是“仿射”这个词,它有什么含义?它和线性空间的区别和联系又是什么呢?这我们就要从线性空间开始说起了。

我们知道,线性空间中有向量和标量两个对象,而仿射空间与线性空间的区别就在于它又加了一个对象,那就是“点”,而且它们的运算规则也不相同。比如,在仿射空间中,点和标量之间没有定义运算;向量和点之间有加法,运算结果是点;点和点之间有减法,运算结果是向量。

所以,仿射空间可以说是点和向量的空间,而且可以被看成是一个没有原点的线性空间。那你有没有想过,我们为什么要研究仿射空间呢?

那是因为仿射空间在计算机图形处理中有着极其重要的地位。在线性空间中,我们可以用矩阵乘向量的方法表示各种线性变换。但是,有一种常用的变换却不能用线性变换的方式表示,那就是平移,一个图形的平移是非线性的。为了表示平移,以及方便现实世界的描述,就需要使用仿射空间。

仿射子空间

和向量子空间一样,我们现在需要把注意力转移到更有实践意义的仿射子空间上。仿射子空间在计算机科学中的运用主要体现在计算机图形处理中,比如:图形的平移、缩放和旋转等等。

如果我们把 Ax 看成是线性,那么 Ax+b 就是仿射,其实就是多了一个平移。也就是说,我们完全可以把仿射子空间看成是线性子空间的平移。

这样理解很容易,不过,我们还是要看看数学上对仿射子空间的严格定义:V 是一个向量空间,U 是 V 的一个向量子空间,x0​ 是 V 中的元素,那仿射子空间 L 就等于:L=x0​+U:={x0​+u:u∈U}。

这里的 U 叫做方向,x0​ 叫做支撑点。仿射子空间在实数三维 R3 中有点、线和面,它们在坐标系中都是不过原点的。

仿射子空间也经常由参数来描述。因为,我们能通过参数,把表达式组合成方程形式,这样更有助于计算。假设,有一个 k 维的仿射子空间 L,它可以表示成:L=x0​+U。如果 U 有一个有序基 (b1​,⋯,bk​),那么,每一个属于仿射子空间 L 的元素 x,都能够表示成:x=x0​+λ1​b1​+⋯+λk​bk​。

在这个表达式中,(λ1​,⋯,λk​) 是实数参数,我们把这个表达式叫做“参数方程”,而 (b1​,⋯,bk​) 就是方向向量。

现在我们来看看,仿射子空间在不同维度中的几个例子,让你能从几何角度更了解仿射子空间。

一维仿射子空间,也叫做“线”,参数方程是:y=x0​+λb1​。也就是说,一条线是由一个支撑点 x0​ 和一个方向向量 b1​ 定义的。

二维仿射子空间,也叫做“平面”,参数方程是:y=x0​+λ1​b1​+λ2​b2​。也就是说,一个平面是由一个支撑点 x0​ 和两个线性独立的向量 b1​ 和 b2​ 定义的。

n−1 维仿射子空间,也叫做“超平面”,参数方程如下。

y=x0​+i=1∑n−1​λi​bi​

在这个参数方程中,(b1​,⋯,bn−1​) 是 n−1 维子空间的一个基。也就是说,超平面是由一个支撑点 x0​ 和 n−1 个线性独立的向量 (b1​,⋯,bn−1​) 所定义的。

仿射映射

空间说完,必定会来到动态部分,对应到今天的内容就是仿射映射了。仿射映射和向量空间之间的线性映射是类似的,很多线性映射的特性也能使用在仿射映射上。现在,我们试着来定义两个仿射空间之间的仿射映射。

现在我们有两个向量空间 V 和 W,一个 V 到 W 的线性映射 ϕ,以及一个属于向量空间 W 的向量 a。

φ:​ →Wx→a+ϕ(x)​

上面这样的映射就是 V 到 W 的仿射映射,其中,你可以看到 x 元素是通过线性映射函数 ϕ 和平移向量 a 进行仿射变换的。

事情就是那么简单,看上去就是一个线性映射加上一个向量,或者从几何角度说,就是做了一次线性变换后,进行了一次平移操作。而更专业一点的说法就是,每个 V 到 W 的仿射映射,都是“一个 V 到 W 的线性映射”和“一个 W 到 W 平移”的组合。

从这里你也可以判断出,仿射映射是保持了原几何结构和维度不变的。

接下来,我们再来看看三维世界中物体的仿射映射,这就包括物体的平移、缩放和旋转了。在几何空间中,物体的平移、旋转、放大缩小,这类操作如果从局部坐标系来看,就是在局部坐标系定义的点,或者向量 x 经过变换后,得到点或向量 y。这类变换可以用公式 y=Ax+v 来表示,其中 A 就是 3×3 矩阵,v 就是三维向量。

当然,我们还可以用矩阵来表示这类变换,也就是仿射变换矩阵和向量乘,这和公式 y=Ax+v 达到的效果是一样的。不过,由于仿射变换矩阵是在实践中经常用的方式,所以我们要来具体看看仿射变换矩阵。

仿射变换矩阵

在三维世界中,物体的平移、缩放和旋转,这些操作其实都可以放到一个 4×4 的矩阵中,并且统一用这个矩阵与向量的乘操作来进行物体的变换,或者说向量的空间变换。这个 4×4 仿射变换矩阵是下面这样的。

A′=⎣⎢⎢⎡​a11​a21​a31​0​a12​a22​a32​0​a13​a23​a33​0​a14​a24​a34​1​⎦⎥⎥⎤​

其中,a11​ 到 a33​ 的 3×3 矩阵就是变换公式 y=Ax+v 中的 A 矩阵,表示的是线性变换。而右上角的 a14​,a24​,a34​ 表示的是平移变换,也就是变换公式中的 v 向量。右下角的数字表示的则是整体缩放,现在它是 1,也就意味着不进行整体缩放。

在计算机图形图像处理中,仿射变换尤其重要,那是因为它能保持变换后的共线或共面性。也就是说,线段变换到线段,还是一条直线,变换前的线段中心点就是变换后的线段中心点。同样,三角形变换后,原三角形的重心还是变换后新三角形的重心。

现在,我们拿一个三角形来举例,因为三角形从计算机图形图像上来说是最基础的图形,是组合成其它多边形的基础。如果我们要变换一个三角形,只要对三个定点 a、b、c 进行仿射变换就行了,对于三角形边上的其它点,变换后还是一样会在边上,这样计算量会极大地降低,变换效率就提高了。

接下来,我们把具体的这几个仿射变换矩阵:平移、缩放和旋转,都单独拿出来,看看它们长什么样。

平移矩阵

我们先来看看 4×4 的平移仿射变换矩阵 A′,你有没有注意到矩阵中的 x、y、z?它们就像公式里写的那样固定在矩阵的右边,定义了矩阵在三个轴方向上的平移距离。

A′=⎣⎢⎢⎡​1000​0100​0010​xyz1​⎦⎥⎥⎤​

缩放矩阵

接下来,我们再来看看缩放仿射变换矩阵 A′,这里 x、y、z 的位置产生了变化,固定在了矩阵的对角线上,定义了矩阵在三个轴方向上的缩放大小。

A′=⎣⎢⎢⎡​x000​0y00​10z0​0001​⎦⎥⎥⎤​

旋转矩阵

最后,我们来看看旋转仿射变换矩阵 A′。这里有三种情况:绕 x 轴旋转、绕 y 轴旋转、绕 z 轴旋转。

绕 x 轴旋转的矩阵:

A′=⎣⎢⎢⎡​1000​0cosθsinθ0​0−sinθcosθ0​0001​⎦⎥⎥⎤​

绕 y 轴旋转的矩阵:

A′=⎣⎢⎢⎡​cosθ0−sinθ0​0100​sinθ0cosθ0​0001​⎦⎥⎥⎤​

绕 z 轴旋转的矩阵:

A′=⎣⎢⎢⎡​cosθsinθ00​−sinθcosθ00​0010​0001​⎦⎥⎥⎤​

在 3D 图形实践中,一般仿射变换矩阵上的操作都是可以叠加的。也就是说,我们可以通过连续的矩阵乘,来完成一系列的对象变换。比如,如果我们的对象要先平移,再缩放,最后再绕 x 轴旋转,那么我们就可以通过矩阵的三连乘来表达这个变换。

⎣⎢⎢⎡​1000​0100​0010​xyz1​⎦⎥⎥⎤​⎣⎢⎢⎡​x000​0y00​10z0​0001​⎦⎥⎥⎤​⎣⎢⎢⎡​1000​0cosθsinθ0​0−sinθcosθ0​0001​⎦⎥⎥⎤​

在这里,我也给你推荐一个数学库作为研究使用。因为工作的缘故,我们做的事情和 WebGL 有关,所以我推荐的是一个前端 TS 实现的数学库 TSM,你可以访问GitHub来了解。TSM 很好地封装了仿射映射,刚才的仿射变换矩阵的叠加操作也是封装好的,比如:平移(translate(vector:vec3):mat4);缩放(scale(vector:vec3):mat4);旋转(rotate(angle:number,u:vec3):mat4)。

本节小结

这一节课的重点是仿射空间和仿射映射。有关仿射空间,你一定要掌握的是仿射子空间在不同维度中的几个例子,特别是 n−1 维仿射子空间,也叫做“超平面”。因为超平面在机器学习的分类算法中很重要,比如 SVM 支持向量机的二分类算法就会用到它。

而在仿射映射中,仿射变换矩阵是重点。因为在 3D 计算机图形图像处理中,它能够被用来进行物体的平移、缩放和旋转。而且,在计算性能方面,仿射变换矩阵可以极大地降低计算机的运算量,提高变换效率。

线性代数练习场

好,今天的练习时刻到了,不过今天的练习会有一些特别。刚刚我推荐了前端 TS 实现的数学库 TSM,这里我再推荐另一个在 Python 中广泛使用的库 OpenCV。

我希望你能够使用它对一张 JPG 图片做一个简单的仿射变换:平移,平移 (50,20)。JPG 素材如下图所示。

友情提示:你可以使用 CV2 来读图片,NumPy 的 shape 来获取图片的行和列数据,再使用 NumPy 的 float32 产生仿射变换矩阵,最后使用 CV2 的 warpAffine 来完成图片的平移操作。我贴了部分代码在下面,其中的仿射变换矩阵的产生和仿射变换这两行代码是需要你来完成的。

import cv2
import numpy as np

//读取图片
img = cv2.imread(‘09.jpg’,0)
rows,cols = img.shape

//这里是你要完成的代码

//显示原图片和仿射变换后的图片
cv2.imshow(‘img’,dst)
cv2.waitKey(0)
cv2.destroyAllWindow

当然,你也可以用其它的库来完成,比如 TSM,这些都没问题,你可以自由发挥。

欢迎在留言区贴出你的代码和最后的输出结果,我会及时回复。同时,也欢迎你把这篇文章分享给你的朋友,一起讨论、学习。