这节课非常简单,没太大可以解释的,由于数学知识我会在数学专题中详细介绍,笔记中的介绍略了非常多,学过大学线性代数的对于这节课应该有很好的掌握。

向量与线性代数

图形学依赖于线性代数,图形学不仅仅依赖于线性代数,它还依赖微积分,概率统计,光学,力学,信号处理,数值分析,艺术等等。我们这里介绍线性代数。

线性代数可以处理物体的旋转,缩放,平移等等操作是计算机图形学中的基础操作。

向量

  • 向量是一个有长度且有方向的量
  • 向量无论你怎么在空间中平移它表示的都是同一个向量,没有绝对的开始位置
  • 我们通常使用 $\vec{a}$ 或者粗体 $\mathbf{a}$ 来表示一个向量
  • 一个二维向量与二维空间中的点一一对应(其他维度同理):我们通常使用一个相同维度的点来表示一个向量,例如二维向量:$\vec{a} = (x,y)$ or $\vec{a}^T = \begin{pmatrix} x \\ y \end{pmatrix}$,其中 $T$ 是向量的转置操作
  • 它可以使用终点减去起点得到:$\vec{AB} = B - A$

向量的模

  • 向量的模(长度)我们写作:$\left | \left | \vec{a} \right | \right |$
  • 向量的模我们可以这么计算:$\left | \left | \vec{a} \right | \right | = \sqrt{a_x^2 + a_y^2 }$,其中$\vec{a}= (a_x,a_y)$
  • 单位向量它的模为 1
  • 将一个向量变为单位向量(归一化):$\hat{\vec{a}} = \frac{\vec{a}}{\left | \left | \vec{a} \right | \right |}$
  • 单位向量我们通常用来表示方向

向量的运算

  • 我们有向量 $\vec{a} = (a_x,a_y,a_z)$ 和向量 $\vec{b} = (b_x,b_y,b_z)$
  • 向量的加法:$\vec{a} + \vec{b} = (a_x + b_x, a_y + b_y, a_z + b_z)$
    • 在几何上,向量的加法遵循平行四边形法则和三角形法则
  • 向量的减法:$\vec{a} - \vec{b} = (a_x - b_x, a_y - b_y, a_z - b_z)$
  • 向量的点积:$\vec{a} \cdot \vec{b}= \left | \left | \vec{a} \right | \right | \left | \left | \vec{b} \right | \right | \cos \theta = a_x b_x + a_y b_y + a_z b_z $其中 $\theta$ 是两个向量的夹角
    • 所以我们可以利用点积来计算两个向量的夹角余弦值 $$ \cos \theta = \frac{\vec{a} \cdot \vec{b}}{\left | \left | \vec{a} \right | \right | \left | \left | \vec{b} \right | \right | } $$
    • 对于单位向量的点积,就等于它们的夹角余弦值
  • 向量的叉积:$\vec{a} \times \vec{b} = (a_y b_z - a_z b_y, a_z b_x - a_x b_z, a_x b_y - a_y b_x)$叉积的方向遵循右手定则
    • 伸开右手,使大拇指与其余四个手指垂直,并且都与手掌在同一个平面内
    • 让四个手指从第一个向量沿着小于 180° 的角度转向第二个向量
    • 此时,大拇指所指的方向就是 $a \times b$ 的方向
    • $a \times b$ 始终垂直于 $a$ 和 $b$ 所在的平面
    • 如果交换顺序 $b \times a$ ,则方向完全相反
    • 如果是二维向量叉积,则 $a \times b = a_x b_y - a_y b_x$ 没有方向是个标量
  • 叉乘的运算律
  • 向量的投影,想象一下有一束灯光平行于向量 $\vec{a}$ 从 $\vec{b}$ 的上方照射到 $\vec{b}$,然后 $\vec{b}$ 的影子在 $\vec{a}$ 上,这个影子就叫做,向量 $\vec{b}$ 在向量 $\vec{a}$ 上的投影,我们通常可以这么计算 $$ \vec{b}_{\bot} = ||\vec{b}|| \cos\theta$$
  • 我们可以使用点乘判断“前”与“后”,如果一个向量与一个平面的法线向量(垂直于平面的向量)的点乘大于0 则这个向量在平面的前面,如果小于零则在平面的后面
  • 判断一个向量在另一个向量的左边还是右边,判断一个点在三角形内部和外部都可以使用向量叉积这个工具 例如这幅图中,左边这幅图 $\vec{a}$和$\vec{b}$ 在 $xoy$ 平面上,我们用肉眼能看到 $\vec{b}$ 在 $\vec{a}$ 的左边,如果一个向量 $\vec{b}$ 在另一个向量 $\vec{b}$ 的左边那么 $\vec{a} \times \vec{b}$ 方向一定是正的 右边这幅图,我们可以使用叉积判断一个点是否在三角形内部(图形学中经常需要判断这个问题),如果一个点在三角形内部,那么它依次与三角形顶点构成的向量与三角形的边向量的叉积的方向是相同的

矩阵

是一个按照矩形行列排列的数的集合
例如

$$ A = \begin{pmatrix} 1 & 3\\ 5 & 2\\ 0&4 \end{pmatrix} $$

矩阵的运算

  • 矩阵的加法:$A + B = C$,把对应位置对应的元素相加
  • 矩阵的减法:$A - B = C$,把对应位置对应的元素相减
  • 矩阵的数乘:$kA = B$,把矩阵 $A$ 中所有元素都乘以数 $k$
  • 矩阵的转置:$A^T = B$,把矩阵 $A$ 的行和列对调,例如 $$ A = \begin{pmatrix} 1 & 3\\ 5 & 2\\ 0&4 \end{pmatrix}, A^T = \begin{pmatrix} 1 & 5 & 0\\ 3 & 2 & 4 \end{pmatrix} $$
  • 矩阵的乘法:$A_{mn} \times B_{np} = C_{mp}$,矩阵的乘法,矩阵 $A$ 的列数必须等于矩阵 $B$ 的行数,一个 $m*n$ 的矩阵只能和一个 $n*p$ 的矩阵相乘,得到一个 $m*p$ 的矩阵,被定义为:有矩阵$A = a_{ij}$,矩阵$B = b_{ij}$,则矩阵 $C$ $$c_{ij} = \sum_{k=1}^{n} a_{ik}b_{kj} = a_{i1}b_{1j} + a_{i2}b_{2j} + \dots + a_{in}b_{nj}$$

矩阵的乘法运算律

  • 矩阵没有交换律 : $A \times B \neq B \times A$
  • 矩阵的结合律: $A \times (B \times C) = (A \times B) \times C$
  • 矩阵的左分配律: $A \times (B + C) = A \times B + A \times C$
  • 矩阵的右分配律: $(A + B) \times C = A \times C + B \times C$
  • 数乘结合律: $k(A \times B) = kA \times B = A \times kB$
  • 消去律不成立: 如果 $A \times B = A \times C$,你不能简单地推导出 $B = C$(除非 $A$ 是可逆矩阵)

向量与矩阵的乘法

你把向量看成一个只有一列的矩阵就行了,这样就变为了矩阵的乘法了 这个矩阵将向量变换为它关于y轴对称的向量

单位矩阵

单位矩阵 $I$ 是一个 $n*n$ 的矩阵,它的对角线上的元素都是1,其他元素都是0,相当于实数中的 1,1 乘以任何数都等于 1,单位矩阵也类似,$I$ 乘以任何矩阵都等于这个矩阵本身

  • 矩阵的逆:如果一个矩阵$A$乘另一个矩阵$B$等于单位矩阵$ A\times B = I$,那么矩阵$B$就是矩阵$A$的逆矩阵,记作$B = A^{-1}$

矩阵在图形学中的应用

矩阵在图形学中充满了大量的运用,最常用的场景就是相机投影,运动,模型变换,旋转,缩放等操作,这些操作都应用了矩阵变换这么一个操作,实际上就是矩阵的乘法操作

作业解析

这节课的作业是环境配置,我们不使用虚拟机,而是直接在windows上配置环境,我们使用cmake来构建项目,这节课的作业只要求使用Eigen库,但是后续需要使用到opencv,所以这里一起配置了 我们假设你已经安装了cmake,并且知道cmake的基础用法,并且安装了msvc/clangd/mingw等编译器。

  1. 安装Eigen库 Eigen库是一个开源的C++线性代数库,我们可以从GitLab中下载,这里给的是5.0.0版本,下载解压到你希望的位置,这里我解压到了 F:\libs\eigen注意这个路径必须是你打开这个目录时必须有一个CMakeLists.txt的文件
  2. 安装opencv 去opencv官网下载opencv这里下载的是4.12.0版本,如果你是windows系统,那么你会得到一个安装程序,将安装路径配置到你希望的位置,这里我选择到 F:\libs这样最后你打开F:\libs\opencv目录时应该有一个LICENSE.txt文件
  3. 修改原课程的cmake文件 原课程的cmake文件是
1cmake_minimum_required (VERSION 2.8.11)
2project (Transformation)
3
4find_package(Eigen3 REQUIRED)
5include_directories(EIGEN3_INCLUDE_DIR)
6
7add_executable (Transformation main.cpp)

我们将其改为

 1# 原来的cmake版本太低了,可能不兼容新的eigen和opencv我们将其版本设置高一点
 2cmake_minimum_required(VERSION 3.10)
 3project(Transformation)
 4
 5# 我们使用c++17
 6set(CMAKE_CXX_STANDARD 17)
 7set(CMAKE_CXX_STANDARD_REQUIRED ON)
 8
 9# 由于Eigen是纯头文件库,所以我们指定Eigen的include目录在哪就行了
10# 由于在`F:/libs/eigen`目录下Eigen文件夹中就是我们需要的头文件源码,所以这里与原作业对不上,所以需要修改cpp的头文件包含
11set(EIGEN3_INCLUDE_DIR "F:/libs/eigen") 
12include_directories(${EIGEN3_INCLUDE_DIR})
13
14
15add_executable (Transformation main.cpp)

我们再将main.cpp的包含头文件修改为

1#include <Eigen/Core>
2#include <Eigen/Dense>
3#include <cmath>
4#include <iostream>

这样我们就能成功编译了 以下就是示例中注释要求TODO的完整代码,没什么好讲的,直接用Eigen库进行运算就行了

 1#include <Eigen/Core>
 2#include <Eigen/Dense>
 3#include <cmath>
 4#include <iostream>
 5
 6int main() {
 7
 8  // Basic Example of cpp
 9  std::cout << "Example of cpp \n";
10  float a = 1.0, b = 2.0;
11  std::cout << a << std::endl;
12  std::cout << a / b << std::endl;
13  std::cout << std::sqrt(b) << std::endl;
14  std::cout << std::acos(-1) << std::endl;
15  std::cout << std::sin(30.0 / 180.0 * acos(-1)) << std::endl;
16
17  // Example of vector
18  std::cout << "Example of vector \n";
19  // vector definition
20  Eigen::Vector3f v(1.0f, 2.0f, 3.0f);
21  Eigen::Vector3f w(1.0f, 0.0f, 0.0f);
22  // vector output
23  std::cout << "Example of output \n";
24  std::cout << v << std::endl;
25  // vector add
26  std::cout << "Example of add \n";
27  std::cout << v + w << std::endl;
28  // vector scalar multiply
29  std::cout << "Example of scalar multiply \n";
30  std::cout << v * 3.0f << std::endl;
31  std::cout << 2.0f * v << std::endl;
32
33  // Example of matrix
34  std::cout << "Example of matrix \n";
35  // matrix definition
36  Eigen::Matrix3f i, j;
37  i << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0;
38  j << 2.0, 3.0, 1.0, 4.0, 6.0, 5.0, 9.0, 7.0, 8.0;
39  // matrix output
40  std::cout << "Example of output \n";
41  std::cout << i << std::endl;
42  // matrix add i + j
43  std::cout << "Example of add \n";
44  std::cout << i + j << std::endl;
45  // matrix scalar multiply i * 2.0
46  std::cout << "Example of scalar multiply \n";
47  std::cout << i * 2.0f << std::endl;
48  // matrix multiply i * j
49  std::cout << "Example of matrix multiply \n";
50  std::cout << i * j << std::endl;
51  // matrix multiply vector i * v
52  std::cout << "Example of matrix-vector multiply \n";
53  std::cout << i * v << std::endl;
54
55  return 0;
56}

接下来是 我们还没有学习齐次坐标,这是下一节课的内容,在这里布置这个作业应该是让我们提前进行预习,这里简单介绍以下齐次坐标

齐次坐标

齐次坐标简单来说是将 $n$ 维向量用 $n+1$ 维坐标来表示的一种数学方式,这样做的好处是方便我们进行平移操作。 在笛卡尔坐标系下,旋转和缩放操作都可以使用矩阵乘进行操作,而平移只能通过加法操作,但是我们希望只通过矩阵乘法表示这些操作,这就需要引入齐次坐标,在齐次坐标中平移也可以写成矩阵乘法的形式。这样,所有的变换都能统一成矩阵乘法,从而方便GPU进行大规模并行计算。

将一个笛卡尔坐标转为齐次坐标:

$$(x, y) \rightarrow (x, y, 1)$$

将一个齐次坐标转为笛卡尔坐标

$$(x, y, w) \rightarrow \left(\frac{x}{w}, \frac{y}{w}\right)$$

在齐次坐标下$(1, 2, 1)$、$(2, 4, 2)$ 和 $(5, 10, 5)$ 都表示笛卡尔空间中的同一个点 $(1, 2)$。

平移操作

假设我们要将点在 $x$ 轴平移 $t_x$,在 $y$ 轴平移 $t_y$,平移矩阵 $\mathbf{T}$ 如下:

$$\mathbf{T} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}$$

计算过程

$$\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \\ y + t_y \\ 1 \end{bmatrix}$$

旋转操作

假设我们要将点绕原点逆时针旋转角度 $\theta$,旋转矩阵 $\mathbf{R}$ 在齐次坐标下表示为:

$$\mathbf{R} = \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}$$

计算过程:

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

缩放操作

虽然作业中没要求,但这里还是介绍一下, 将一个点$(x,y,1)^T$在 $x$ 轴方向缩放 $s_x$ 倍,在 $y$ 轴方向缩放 $s_y$ 倍

$$\mathbf{S} = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix}$$

计算过程:

$$\begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} s_x \cdot x \\ s_y \cdot y \\ 1 \end{bmatrix}$$

开始做作业

注意通常我们进行矩阵与向量相乘的时候,通常我们使用矩阵乘以一个向量,而不是向量乘以一个矩阵,所以如果我们进行这么一个操作 $A \times B \times C \times \vec{v}$那么对向量的操作是从右项左进行的,先对 $\vec{v}$ 进行 $C$ 操作再是 $B$ 操作 $A$ 操作。
所以我们要对向量$\mathbf{P} = (2,1)$
操作的第一步是先把他化为齐次坐标 $\mathbf{P} = (2,1,1)$
然后我们对它绕原点先逆时针旋转也就是需要一个旋转矩阵

$$\mathbf{R} = \begin{bmatrix} \cos(\frac{\pi}{4}) & -\sin(\frac{\pi}{4}) & 0 \\ \sin(\frac{\pi}{4}) & \cos(\frac{\pi}{4}) & 0 \\ 0 & 0 & 1 \end{bmatrix}$$

再平移 (1,2) 也就是需要一个平移矩阵

$$\mathbf{T} = \begin{bmatrix} 1 & 0 & 1 \\ 0 & 1 & 2 \\ 0 & 0 & 1 \end{bmatrix}$$

最后的公式是

$$\mathbf{P}_t = \mathbf{T}\times \mathbf{R} \times \mathbf{P}^{T}$$

用代码实现

 1#include <Eigen/Core>
 2#include <Eigen/Dense>
 3#include <cmath>
 4#include <iostream>
 5
 6int main() {
 7
 8  const double PI = acos(-1);
 9  const double angle_45 = 45.0 / 180.0 * PI;
10
11  Eigen::Matrix3d rotation_matrix;
12  rotation_matrix << cos(angle_45), -sin(angle_45), 0, sin(angle_45),
13      cos(angle_45), 0, 0, 0, 1;
14
15  Eigen::Matrix3d Translation_matrix;
16  Translation_matrix << 1, 0, 1, 0, 1, 2, 0, 0, 1;
17
18  Eigen::Vector3d point(2, 1, 1);
19
20  std::cout << "Rotation Matrix: \n" << rotation_matrix << std::endl;
21  std::cout << "Translation Matrix: \n" << Translation_matrix << std::endl;
22  std::cout << "Point: \n" << point.transpose() << std::endl;
23
24  std::cout << "Rotated and Translated Point: \n"
25            << Translation_matrix * rotation_matrix * point << std::endl;
26  return 0;
27}

最后得到结果:

Rotation Matrix: 
 0.707107 -0.707107         0
 0.707107  0.707107         0
        0         0         1
Translation Matrix:
1 0 1
0 1 2
0 0 1
Point:
2 1 1
Rotated and Translated Point:
1.70711
4.12132
      1