二 线性变换, 齐次坐标, 变换组合, 变换分解, 3D变换

线性变换

缩放矩阵 Scale Matrix

缩放矩阵使用对角阵.

x' 读作 "x prime".

  1. x和y均匀缩放

    复制代码
     x与y同时缩放 0.5 倍, s = 0.5
     
     [x'] = [s, 0] [x] = [ 0.5x ]
     [y']   [0, s] [y]   [ 0.5y ]
  2. x和y不均匀缩放

    复制代码
     x缩放, y不变. sx = 0.5, sy = 1.0
    
     [x'] = [sx, 0] [x] = [ 0.5x ]
     [y']   [0, sy] [y]   [   y  ] 

翻转矩阵 Reflection Matrix

水平翻转

x相反, y不变

复制代码
x' = -x
y' = y

矩阵形式:

复制代码
[x'] = [-1, 0] [x] = [ -x ]
[y']   [ 0, 1] [y]   [  y ] 

切变矩阵 Shear Matrix

假设图形的 宽度为x, 高度为y, 切变长度为 a .

沿水平方向切变, 图像中所有点的y轴值均未发生变化, 所以有:

复制代码
y' = y

当 y = 0 时, 在底边上的任意点在水平方向上均未发生移动, 移动距离为 0 .

当 y = 1 时, 在顶边上的任意点在水平方向上的移动距离均为 a .

当 y = 0.5 时, 在中间的任意点在水平方向上的移动距离均为 a/2 .

那么实际上任意点在水平方向上的移动距离为: a * y , 所以有:

复制代码
[x'] = [1, a] [x] = [ x + a*y ]
[y']   [0, 1] [y]   [   y     ] 

要想写出一个变换的要点是, 找出变化之前的 x,y 和 变化之后的 x',y' 之间的关系.

旋转矩阵 Rotate Matrix

默认情况下的旋转指的是围绕坐标系 原点(0,0) 进行的 逆时针方向 的旋转.

变换矩阵推导:
复制代码
目的: 由 (x, y) => (x', y')    

需要找出一个矩阵[?], 其与(x, y) 相乘得到(x', y')
[x'] = [A?, B?] [x]
[y']   [C?, D?] [y]

假设图形的边长为 1, 逆时针旋转的角度为 θ .

先由特殊点来思考如何变换, 例如用右下角与左上角推导: 
1. 右下角的点为 (1, 0), 旋转后的点为 P(x', y').
复制代码
右下角所在的边就是底边, 沿逆时针向上旋转.
那么已知边长为 1, 旋转角度为 θ, 
底边 与 x轴投影 和 点P与x轴垂线, 三者构成直角三角形.
底边为斜边, x轴投影为 θ 的临边, 点P与x轴垂线为 θ 的对边:

cosθ = 临边 / 斜边 = x轴投影     / 底边  =  p点的x / 1  = P_x'
sinθ = 对边 / 斜边 = 点P与x轴垂线 / 底边  =  p点的y / 1  = P_y'

那么由右下角变换到点P为:

(1, 0) => (cosθ, sinθ)

矩阵表示:

[cosθ] = [A, B] [1]
[sinθ]   [C, D] [0]

=>

cosθ = A*1 + B*0 = A
sinθ = C*1 + D*0 = C

=>

[x'] = [cosθ, B?] [x]
[y']   [sinθ, D?] [y]
2. 左上角的点为 (0, 1), 旋转后的点为 P(x', y').
复制代码
左上角所在的边就是左边, 沿逆时针向左旋转.
那么已知边长为 1, 旋转角度为 θ, 
左边 与 y轴投影 和 点P与y轴垂线, 三者构成直角三角形.
左边为斜边, y轴投影为 θ 的临边, 点P与y轴垂线为 θ 的对边:
 
 cosθ = 临边 / 斜边 = y轴投影     / 左边  =  p点的y / 1  = P_x'
-sinθ = 对边 / 斜边 = 点P与y轴垂线 / 左边  =  p点的x / 1  = P_y'
注意这里移动后P点的x轴坐标是负值, 所以sinθ需要加一个负号.

那么由左上角变换到点P为:

(0, 1) => (-sinθ, cosθ)

矩阵表示:

[-sinθ] = [cosθ, B] [0]
[ cosθ]   [sinθ, D] [1]

=>

-sinθ = cosθ*0 + B*1 = B
 cosθ = sinθ*0 + D*1 = D

=>

[x'] = [cosθ, -sinθ] [x]
[y']   [sinθ,  cosθ] [y]

变换矩阵为:
[cosθ, -sinθ]
[sinθ,  cosθ]
线性变换

以上变换都可以统一为以下的表示形式, 称其为"线性变换":

复制代码
变换后坐标 = 变换矩阵 ✖️ 变换前坐标

x' = ax + by
y' = cx + dy

[x'] = [a, b] [x]
[y']   [c, d] [y]

齐次坐标

为什么需要齐次坐标, 仿射变换

图形进行平移变换, 假设在x方向平移 tx, 在y方向平移 ty :

复制代码
x' = x + tx
y' = y + ty

也就是说 x 和 y 需要加上某个常数.

那么是否能够像缩放, 旋转等变换一样使用矩阵表示?

复制代码
按照之前的矩阵表示:
[x'] = [a, b] [x]
[y']   [c, d] [y]

发现这种形式并不能加上某个常数, 所以修改为:

[x'] = [a, b] [x] + [tx]
[y']   [c, d] [y]   [ty]

可以看出平移不再符合线性变换的表示形式, 它是一个 "仿射变换".

人们为了让各种变换可以统一用一种形式表示, 后来发现引入齐次坐标可以解决这个问题.

2维空间的表示形式

给2维坐标增加一个维度:

复制代码
2D点  = (x, y, 1)T
2D向量 = (x, y, 0)T

那么平移可以表示为一个 矩阵×向量 的形式:

复制代码
[x'] =  [1, 0, tx]  [x]   =   [ x+tx ]
[y']    [0, 1, ty]  [y]       [ y+ty ]
[w']    [0, 0,  1]  [1]       [  1   ]

那么为什么给2维的点或向量增加第3个维度是1或0, 它有哪些好处:

复制代码
1. 向量 +  向量 = 向量    第3维 0+0=0
2.   点 -  点   = 向量    第3维 1-1=0
3.   点 +  向量 = 点      第3维 1+0=1
4.   点 +  点   = 中点    第3维 1+1=2

关于第4点为什么是中点?
当w≠0时, 令第3维度变为1
[x]  =>  [x/w]
[y]      [y/w] 
[w]      [ 1 ]

那么当 w=2 时, 各维度除以2, 得到的也就是中间点的坐标.

仿射变换的齐次坐标表示

1. 使用齐次坐标统一表示所有变换

仿射变换:

复制代码
[x'] = [a, b] [x] + [tx]
[y']   [c, d] [y]   [ty]

表示为齐次坐标:

复制代码
[x'] = [a, b, tx] [x]
[y']   [c, d, ty] [y]
[1 ] = [0, 0,  1] [1]

那么这样也就是把平移变换这种特殊的变换也统一表示为了一个 矩阵×向量 的形式.

2. 各种变换的具体形式
规律总结:
复制代码
[x'] = [a, b, tx] [x]
[y']   [c, d, ty] [y]
[1 ] = [0, 0,  1] [1]

1. 最后一行永远都是 0 0 1
2. 平移永远是写在最右列的前2个数
3. a,b,c,d 区域控制的是缩放和旋转
缩放
复制代码
[sx, 0, 0]
[0, sy, 0]
[0, 0,  1]
旋转
复制代码
[cosθ, -sinθ,  0]
[sinθ, cosθ,   0]
[0,       0,   1]
平移
复制代码
[1,  0, tx]
[0,  1, ty]
[0, 0,  1 ]

变换组合

复杂变换可以通过一系列简单变换来完成.

变换的顺序是重要的, 比如先平移后旋转, 与先旋转后平移, 结果是不一样的.

因为矩阵的乘法不满足交换律.

例如先 旋转45度 再 平移(1,0) 的矩阵表示:

复制代码
自右向左:

            [x] = [1, 0, 1]  [cos45°, -sin45°,  0]  [x]
T(1,0)·R45  [y]   [0, 1, 0]  [sin45°,  cos45°,  0]  [y]
            [1]   [0, 0, 1]  [     0,       0,  1]  [1]

假设有n个变换步骤, 则:

复制代码
An(...A2(A1(X))) = An ... A2 · A1 · [x]
                                    [y]
                                    [1]

因为矩阵乘法满足结合律, 那么可以先计算A1~An部分, 
而无论有多少个3X3齐次坐标矩阵相乘, 结果依旧是一个3X3矩阵.

那么也就是说一个3X3矩阵就可以表示非常复杂的组合变换.

变换分解

对于图形不在坐标原点情况下的变换, 可以通过先移动到原点, 再变换, 最后再移动回去.

例如, 对于任意点c进行旋转:

复制代码
自右向左:
T(c) · R(θ) · T(-c)

顺时针旋转(逆旋转) 旋转矩阵的逆 = 旋转矩阵的转置

当角 θ 为负时, 图形按照顺时针旋转, 那么矩阵为:

复制代码
Rθ =    [cosθ, -sinθ]
        [sinθ,  cosθ]

=>

cosθ 与 cos-θ 结果一样, sin相反.
R-θ =   [cosθ,   sinθ]
        [-sinθ,  cosθ]

通过观察会发现:
实际上逆旋转的矩阵就是原旋转矩阵的转置, 行变列, 列边行.
而且逆旋转矩阵是原矩阵的逆矩阵.

重要性质: 在旋转变换中

复制代码
旋转矩阵的逆 = 旋转矩阵的转置

在数学中, 如果一个矩阵的逆等于转置, 那么这个矩阵叫做 "正交矩阵".

3D 变换

3维空间的表示形式

3维空间的变换就是在2维的基础上再添加一个值:

复制代码
2D点  = (x, y, 1)T
2D向量 = (x, y, 0)T

3D 点 = (x, y, z, 1)T
3D 向量 = (x, y, z, 0)T

当 w≠0 时, 有:
(x, y, z, w) 的3D点为 (x/w, y/w, z/w, 1)

3D空间的齐次坐标表示

复制代码
[x'] = [a, b, c, tx] [x]
[y']   [d, e, f, ty] [y]
[z']   [g, h, i, tz] [z]
[1 ]   [0, 0, 0,  1] [1]

在3维空间中也遵循, 先线性变换, 后计算平移量 的操作顺序.

缩放
复制代码
[sx, 0, 0,  0] 
[ 0, sy, 0, 0] 
[ 0, 0, sz, 0] 
[ 0, 0, 0,  1] 
平移
复制代码
[ 1, 0, 0, tx ] 
[ 0, 1, 0, ty ] 
[ 0, 0, 1, tz ] 
[ 0, 0, 0,  1 ] 
绕轴旋转 (x轴, y轴, z轴)

x轴:

复制代码
x轴不变 (1,0,0)
[ 1,    0,     0,  0 ] 
[ 0, cosθ, -sinθ,  0 ] 
[ 0, sinθ,  cosθ,  0 ] 
[ 0,    0,     0,  1 ] 

z轴:

复制代码
z轴不变 (0,0,1)
[ cosθ, -sinθ,  0,  0 ] 
[ sinθ,  cosθ,  0,  0 ] 
[    0,     0,  1,  0 ] 
[    0,     0,  0,  1 ] 

y轴:

复制代码
y轴不变 (0,1,0)
[ cosθ,  0,  sinθ,  0 ] 
[    0,  1,     0,  0 ] 
[ -sinθ, 0,  cosθ,  0 ] 
[    0,  0,     0,  1 ] 

根据右手螺旋定则, 任意两轴叉乘得到第三轴:

复制代码
x × y = z
y × z = x
z × x = y

由于 z x x 得到的y轴与 x × z 得到的y轴方向相反
所以y轴与其它两个轴的变换矩阵不同
任意方向旋转

任意的3D旋转都可以通过基本的绕轴旋转来组合完成.

复制代码
Rxyz(α,β,γ) = Rx(α)Ry(β)Rz(γ)

将任意旋转分解为3轴基本旋转: 罗德里格斯旋转公式

复制代码
旋转轴为(以原点为起点的向量n) n = (nx,ny,nz)T,旋转角度为 α

R(n,α) = cosαI + (1−cosα)nnT + sinα [ 0, -nz,  ny]
                                    [ nz,  0, -nx]
                                    [-ny, nx,   0]​

注意点:

  • 必须有 (|n|=1)。如果 (n) 不是单位向量,先归一化。
  • 这对应右手系、主动旋转(把向量转过去)。如果你在某些图形 API 里用的是被动旋转/坐标系旋转,可能会出现角度取负或矩阵转置的差异。

关于四元数

比如2维空间旋转15°的一个矩阵, 另一个是旋转25°的矩阵, 把这两个矩阵加起来求平均, 那么结果并不是旋转20° .

可以知道旋转矩阵并不适合直接做差值计算, 那么对于此类问题四元数就可以方便的解决.

相关推荐
XiaoHu02076 小时前
C++的IO流
开发语言·c++
ULTRA??6 小时前
排序算法之快排与TIMSORT的比较测试,python
c++·python·算法·golang
胡萝卜3.07 小时前
构建安全的C++内存管理体系:从RAII到智能指针的完整解决方案
运维·开发语言·c++·人工智能·安全·智能指针·raii
拾光Ծ7 小时前
【优选算法】双指针算法:专题一
数据结构·c++·算法
MSTcheng.7 小时前
【C++】如何快速实现一棵支持key或key-value的二叉搜索树?关键技巧一文掌握!
开发语言·c++·算法·二叉搜索树
野生风长7 小时前
从零开始的c语言:指针高级应用(下)(回调函数,qsort函数模拟实现, strlen和sizeof)
java·c语言·开发语言·c++·算法
渡我白衣7 小时前
计算机组成原理(6):进位计数制
c++·人工智能·深度学习·神经网络·机器学习·硬件工程
ULTRA??9 小时前
插入排序算法实现(二分查找搜索版本)
c++·算法
郭涤生15 小时前
布隆过滤器
c++