slam入门——十四讲笔记(二)

第3讲 三维空间刚体运动

本节目标:理解三维空间的刚体运动描述方式:旋转矩阵、变换矩阵、四元数和欧拉角

3.1 旋转矩阵

3.1.1 点和向量,坐标系

需要注意,不要把向量与它的坐标两个概念混淆。一个向量是指空间当中的一样东西,如说 a \mathbf{a} a。这里 a \mathbf{a} a并不是和若干个实数相关联的。还有当我们指定这个三维空间中的某个坐标系时,才可以谈论该向量在此坐标系下的坐标,也就是熬到若干个实数对应这个向量。例如,三维空间中的某个向量的坐标可以用 R 3 \mathbb{R}^3 R3当中的三个数来描述。某个点的坐标也可以用 R 3 \mathbb{R}^3 R3来描述。怎么描述的呢?如果我们确定一个坐标系,也就是一个线性空间的基 ( e 1 , e 2 , e 3 ) (\mathbf{e_1}, \mathbf{e_2}, \mathbf{e_3}) (e1,e2,e3),那就可以谈论向量 a \mathbf{a} a在这组基下的坐标了:
a = [ e 1 e 2 e 3 ] [ a 1 a 2 a 3 ] = a 1 e 1 + a 2 e 2 + a 3 e 3 \mathbf{a} = \begin{bmatrix} \mathbf{e_1} & \mathbf{e_2} & \mathbf{e_3} \end{bmatrix} \begin{bmatrix} a1 \\ a2 \\ a3 \end{bmatrix} = a_1 \mathbf{e_1} + a_2 \mathbf{e_2} + a_3 \mathbf{e_3} a=[e1e2e3]a1a2a3=a1e1+a2e2+a3e3

所以,这个坐标的具体取值,一个是和向量本身有关,第二也是和坐标系的选取有关。

对于 a , b ∈ R 3 \mathbf{a,b} \in \mathbb{R}^3 a,bR3,内积可以写成:
a ⋅ b = a T b = ∑ i = 1 3 a i b i = ∣ a ∣ ∣ b ∣ c o s < a , b > (3.2) \mathbf{a \cdot b} = \mathbf{a^Tb} = \sum_{i=1}^{3}{a_ib_i} = \mathbf{|a||b|}cos<\mathbf{a,b}> \qquad \text{(3.2)} ab=aTb=i=13aibi=abcos<a,b>(3.2)
外积可以写成
a × b = [ i i k a 1 a 2 a 3 b 1 b 2 b 3 ] = [ a 2 b 3 − a 3 b 2 a 3 b 1 − a 1 b 3 a 1 b 2 − a 2 b 1 ] = [ 0 − a 3 a 2 a 3 0 − a 1 − a 2 a 1 0 ] b = a ^ b \mathbf{a \times b} = \begin{bmatrix} \mathbf{i} & \mathbf{i} & \mathbf{k} \\ a_1 & a_2 & a_3 \\ b_1 & b_2 & b_3 \end{bmatrix} = \begin{bmatrix} a_2b_3-a_3b_2 \\ a_3b_1-a_1b_3 \\ a_1b_2-a_2b_1 \end{bmatrix} = \begin{bmatrix} 0 & -a_3 & a_2 \\ a_3 & 0 & -a_1 \\ -a_2 & a_1 & 0 \end{bmatrix} \mathbf{b}= \mathbf{\hat{a}} \mathbf{b} a×b=ia1b1ia2b2ka3b3=a2b3a3b2a3b1a1b3a1b2a2b1=0a3a2a30a1a2a10b=a^b

还可以用外积表示向量的旋转。考虑两个不平行的向量 a \mathbf{a} a, b \mathbf{b} b,我们要描述 a \mathbf{a} a b \mathbf{b} b之间是如何旋转的。我们可以用一个向量来描述三维空间中两个向量的旋转关系。在右手法则下,我们用右手的四个指头从 a \mathbf{a} a转向 b \mathbf{b} b,其大拇指朝向就是旋转向量的方向,事实上也是 a × b \mathbf{a}×\mathbf{b} a×b的方向。它的大小由 a \mathbf{a} a b \mathbf{b} b的夹角决定。通过这种方式,我们构造了从 a \mathbf{a} a b \mathbf{b} b的一个旋转向量。这个向量同样位于三维空间中,在此坐标系下,可以用三个实数来描述它。

在这里插入图片描述

3.1.2 坐标系间的欧式变换

与向量间的旋转类似,我们同样可以描述两个坐标系之间的旋转关系,再加上平移,统称为坐标系之间的变换关系。在机器人运动过程中,常见的做法是设定一个惯性坐标系(或者叫世界坐标系),可以认为它是固定不动的。同时,相机或机器人则是一个移动坐标系。我们会问:相机视野中某个向量 p \mathbf{p} p,它的坐标为 p c p_c pc,而从世界坐标系下看,它的坐标是 p w p_w pw。这两个坐标之间是如何转换的呢?这时,就需要先得到该点针对机器人坐标系坐标值,再根据机器人位姿转换到世界坐标系中,这个转换关系由一个矩阵 T T T来描述,如图3-2所示。
.在这里插入图片描述

相机运动是一个刚体运动,它保证了同一个向量在各个坐标系下的长度和夹角都不会发生变化。这种变换称为欧式变换。一个欧式变换一般由一个旋转和一个平移两部分组成。首先来考虑旋转。我们设某个单位正交基 ( e 1 , e 2 , e 3 ) \begin{pmatrix} e_1,e_2,e_3 \end{pmatrix} (e1,e2,e3)经过一次旋转,变成了 ( e 1 ′ , e 2 ′ , e 3 ′ ) \begin{pmatrix}e'_1,e'_2,e'_3\end{pmatrix} (e1,e2,e3)。那么,对于同一个向量 a \mathbf{a} a(注意该向量并没有随着坐标系的旋转而发生改动),它在两个坐标系下的坐标为 [ a 1 , a 2 , a 3 ] T \begin{bmatrix}a_1,a_2,a_3\end{bmatrix}^T [a1,a2,a3]T [ a 1 ′ , a 2 ′ , a 3 ′ ] T \begin{bmatrix}a'_1,a'_2,a'_3\end{bmatrix}^T [a1,a2,a3]T。根据坐标的定义,有:
[ e 1 , e 2 , e 3 ] [ a 1 a 2 a 3 ] = [ e 1 ′ , e 2 ′ , e 3 ′ ] [ a 1 ′ a 2 ′ a 3 ′ ] ( 3.4 ) \begin{bmatrix}e_1,e_2,e_3\end{bmatrix} \begin{bmatrix} a_1\\a_2\\a_3\end{bmatrix} = \begin{bmatrix}e'_1,e'_2,e'_3\end{bmatrix} \begin{bmatrix}a'_1\\a'_2\\a'_3\end{bmatrix}\quad(3.4) [e1,e2,e3]a1a2a3=[e1,e2,e3]a1a2a3(3.4)
为了描述两个坐标之间的关系,我们对上面等式左右同时左乘 [ e 1 T e 2 T e 3 T ] \begin{bmatrix}e^T_1\\e^T_2\\e^T_3 \end{bmatrix} e1Te2Te3T,那么左边的系数变成了单位矩阵,所以:
在这里插入图片描述
我们把中间的矩阵拿出来,定义成一个矩阵 R R R。这个矩阵由两组基之间的内积组成,刻画了旋转前后同一个向量的坐标变换关系。只要旋转是一样的,那么这个矩阵也是一样的。可以说,矩阵 R R R描述了旋转本身。因此它又称为旋转矩阵

旋转矩阵有一些特别的性质。事实上,它是一个行列式为1的正交矩阵。反之,行列式为1的正交矩阵也是一个旋转矩阵。所以,我们可以把旋转矩阵的集合定义如下:
S O ( n ) = { R ∈ R n × n ∣ R R T = I , d e t ( R ) = 1 } (3.6) SO(n)=\{R\in\mathbb{R}^{n\times n}|RR^T=I,det(R)=1\} \quad \text{(3.6)} SO(n)={RRn×nRRT=I,det(R)=1}(3.6)
S O ( n ) SO(n) SO(n)是特殊正交群(Special Orthogonal Group)的意思。这个集合由n维空间的旋转矩阵组成,特别的,SO(3)就是三维空间的旋转矩阵。通过旋转矩阵,我们可以直接谈论两个坐标系之间的旋转变换,而不用再从基开始谈起了。换句话说,旋转矩阵可以描述相机的旋转

由于旋转矩阵为正交阵,它的逆(即转置)描述了一个相反的旋转。按照上面的定义方式,有:
a ′ = R − 1 a = R T a . (3.7) \mathbf{a}'=R^{-1}\mathbf{a}=R^{T}\mathbf{a}.\quad \text{(3.7)} a=R1a=RTa.(3.7)
显然 R T R^{T} RT刻画了一个相反的旋转

在欧式变换中,除了旋转之外还有一个平移。考虑世界坐标系中的向量 a \mathbf{a} a,经过一次旋转(用 R R R描述)和一次平移 t \mathbf{t} t后,得到了 a ′ \mathbf{a}' a,那么把旋转和平移合到一起,有:
a ′ = R a + t . (3.8) \mathbf{a}'=R\mathbf{a}+\mathbf{t}.\quad \text{(3.8)} a=Ra+t.(3.8)
其中, t \mathbf{t} t称为平移向量。通过上式,我们用一个旋转矩阵 R R R和一个平移向量 t \mathbf{t} t完整地描述了一个欧式空间的坐标变换关系。

3.1.3 变换矩阵与齐次坐标

式(3.8)完整地表达了欧式空间的旋转与平移,不过还存在一个小问题:这里的变换关系不是一个线性关系。这样的形式在变换多次后会过于复杂。因此,我们要引入齐次坐标和变换矩阵重写式(3.8):
在这里插入图片描述
这是一个数学技巧:我们把一个三维向量的末尾添加 1 1 1,变成了四维向量,称为齐次坐标。对于这个四维向量,我们可以把旋转和平移写在一个矩阵里面,使得整个关系变成了线性关系。该式中,矩阵 T T T称为变换矩阵(Transform Matrix)

关于变换矩阵 T T T,它具有比较特别的结构:左上角为旋转矩阵,右侧为平移向量,左下角为 0 \mathbf{0} 0向量,右下角为 1 1 1。这种矩阵又称特殊欧式群(Special Euclidean Group):
S E ( 3 ) = { T = [ R t 0 T 1 ] ∈ R 4 × 4 ∣ R ∈ S O ( 3 ) , t ∈ R 3 } . (3.12) SE(3) = \{T=\begin{bmatrix}R&t\\ \mathbf{0}^T & 1 \end{bmatrix} \in \mathbb{R}^{4\times 4} | R \in SO(3), \mathbf{t} \in \mathbb{R}^3 \}. \quad \text{(3.12)} SE(3)={T=[R0Tt1]R4×4RSO(3),tR3}.(3.12)
与SO(3)一样,求解该矩阵的逆表示一个反向的变换:
T − 1 = [ R T R T t 0 T 1 ] . (3.13) T^{-1}=\begin{bmatrix}R^T &R^T\mathbf{t} \\ \mathbf{0}^T & 1\end{bmatrix}. \quad \text{(3.13)} T1=[RT0TRTt1].(3.13)

回顾一下这部分的内容:首先,我们说了向量和它的坐标表示,并介绍了向量间的运算;然后,坐标系之间的运动由欧式变换描述,它由平移和旋转组成。旋转可以由旋转矩阵 S O ( 3 ) SO(3) SO(3)描述,而平移直接由一个 R 3 \mathbb{R}^3 R3向量描述。最后,如果将平移和旋转放在一个矩阵中,就形成了变换矩阵 S E ( 3 ) SE(3) SE(3)

3.2 实践:Eigen

如果你的ubuntu上还没有安装Eigen。请输入以下命令来安装它:

sudo apt-get install libeigen3-dev

Eigen头文件的默认位置在“/usr/include/eigen3/”中,如果不确定,可以输入以下命令来查找它的位置。

sudo updatedb
locate eigen3

相比于其他库,Eigen特殊之处在于,它是一个纯用头文件搭建起来的库(这非常神奇!)。这意味着你只能找到它的头文件,而没有.so或.a那样的二进制文件。我们在使用时,只需引入Eigen的头文件即可,不需要链接它的库文件(因为它没有库文件)。

下面写一段代码,来实际练习一下Eigen的使用:

#include <iostream>
using namespace std;
#include <ctime>
// Eigen 部分
#include <Eigen/Core>
// 稠密矩阵的代数运算(逆,特征值等)
#include <Eigen/Dense>

#define MATRIX_SIZE 50

/****************************
* 本程序演示了 Eigen 基本类型的使用
****************************/

int main( int argc, char** argv )
{
    // Eigen 中所有向量和矩阵都是Eigen::Matrix,它是一个模板类。它的前三个参数为:数据类型,行,列
    // 声明一个2*3的float矩阵
    Eigen::Matrix<float, 2, 3> matrix_23;

    // 同时,Eigen 通过 typedef 提供了许多内置类型,不过底层仍是Eigen::Matrix
    // 例如 Vector3d 实质上是 Eigen::Matrix<double, 3, 1>,即三维向量
    Eigen::Vector3d v_3d;
	// 这是一样的
    Eigen::Matrix<float,3,1> vd_3d;

    // Matrix3d 实质上是 Eigen::Matrix<double, 3, 3>
    Eigen::Matrix3d matrix_33 = Eigen::Matrix3d::Zero(); //初始化为零
    // 如果不确定矩阵大小,可以使用动态大小的矩阵
    Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic > matrix_dynamic;
    // 更简单的
    Eigen::MatrixXd matrix_x;
    // 这种类型还有很多,我们不一一列举

    // 下面是对Eigen阵的操作
    // 输入数据(初始化)
    matrix_23 << 1, 2, 3, 4, 5, 6;
    // 输出
    cout << matrix_23 << endl;

    // 用()访问矩阵中的元素
    for (int i=0; i<2; i++) {
        for (int j=0; j<3; j++)
            cout<<matrix_23(i,j)<<"\t";
        cout<<endl;
    }

    // 矩阵和向量相乘(实际上仍是矩阵和矩阵)
    v_3d << 3, 2, 1;
    vd_3d << 4,5,6;
    // 但是在Eigen里你不能混合两种不同类型的矩阵,像这样是错的
    // Eigen::Matrix<double, 2, 1> result_wrong_type = matrix_23 * v_3d;
    // 应该显式转换
    Eigen::Matrix<double, 2, 1> result = matrix_23.cast<double>() * v_3d;
    cout << result << endl;

    Eigen::Matrix<float, 2, 1> result2 = matrix_23 * vd_3d;
    cout << result2 << endl;

    // 同样你不能搞错矩阵的维度
    // 试着取消下面的注释,看看Eigen会报什么错
    // Eigen::Matrix<double, 2, 3> result_wrong_dimension = matrix_23.cast<double>() * v_3d;

    // 一些矩阵运算
    // 四则运算就不演示了,直接用+-*/即可。
    matrix_33 = Eigen::Matrix3d::Random();      // 随机数矩阵
    cout << matrix_33 << endl << endl;

    cout << matrix_33.transpose() << endl;      // 转置
    cout << matrix_33.sum() << endl;            // 各元素和
    cout << matrix_33.trace() << endl;          // 迹
    cout << 10*matrix_33 << endl;               // 数乘
    cout << matrix_33.inverse() << endl;        // 逆
    cout << matrix_33.determinant() << endl;    // 行列式

    // 特征值
    // 实对称矩阵可以保证对角化成功
    Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> eigen_solver ( matrix_33.transpose()*matrix_33 );
    cout << "Eigen values = \n" << eigen_solver.eigenvalues() << endl;
    cout << "Eigen vectors = \n" << eigen_solver.eigenvectors() << endl;

    // 解方程
    // 我们求解 matrix_NN * x = v_Nd 这个方程
    // N的大小在前边的宏里定义,它由随机数生成
    // 直接求逆自然是最直接的,但是求逆运算量大

    Eigen::Matrix< double, MATRIX_SIZE, MATRIX_SIZE > matrix_NN;
    matrix_NN = Eigen::MatrixXd::Random( MATRIX_SIZE, MATRIX_SIZE );
    Eigen::Matrix< double, MATRIX_SIZE,  1> v_Nd;
    v_Nd = Eigen::MatrixXd::Random( MATRIX_SIZE,1 );

    clock_t time_stt = clock(); // 计时
    // 直接求逆
    Eigen::Matrix<double,MATRIX_SIZE,1> x = matrix_NN.inverse()*v_Nd;
    cout <<"time use in normal inverse is " << 1000* (clock() - time_stt)/(double)CLOCKS_PER_SEC << "ms"<< endl;
    
	// 通常用矩阵分解来求,例如QR分解,速度会快很多
    time_stt = clock();
    x = matrix_NN.colPivHouseholderQr().solve(v_Nd);
    cout <<"time use in Qr decomposition is " <<1000*  (clock() - time_stt)/(double)CLOCKS_PER_SEC <<"ms" << endl;

    return 0;
}

要编译它,需要在CMakeLists.txt里制定Eigen的头文件目录。CMakeLists.txt内容如下:

cmake_minimum_required( VERSION 2.8 )
project( useEigen )

set( CMAKE_BUILD_TYPE "Release" )
set( CMAKE_CXX_FLAGS "-O3" )

# 添加Eigen头文件
include_directories( "/usr/include/eigen3" )

# in osx and brew install
# include_directories( /usr/local/Cellar/eigen/3.3.3/include/eigen3 )

# 因为只有头文件,我们不需要再用target_link_libraries语句将程序链接到库上。

add_executable( eigenMatrix eigenMatrix.cpp )

输出结果如下,代码中有详细注释,就不一一解释每行输出了。在这里插入图片描述

3.3 旋转向量和欧拉角

3.3.1 旋转向量

通过3.1节,我们有了旋转矩阵来描述旋转,有了变换矩阵描述一个六自由度的三维刚体运动,是不是已经足够了呢?但是,矩阵的表示方式至少有以下几个缺点:

  1. S O ( 3 ) SO(3) SO(3)的旋转矩阵有九个量,但一次旋转只有三个自由度。因此这种表达方式是冗余的。同理,变换矩阵用十六个量表达了六自由度的变换。那么,是否有更紧凑的表示呢?
  2. 旋转矩阵自身带有约束:它必须是个正交矩阵,且行列式为 1 1 1。变换矩阵也是如此。当我们想要估计或优化一个旋转矩阵/变换矩阵时。这些约束会使得求解变得更困难。

因此,我么希望有一种方式能够紧凑地描述旋转和平移。例如,用一个三维向量表达旋转,用六维向量表达变换。


对于坐标系的旋转,我们知道,任意旋转都可以用一个旋转轴一个旋转角来刻画。于是,我们可以使用一个向量,其方向与旋转轴一致,而长度等于旋转角。这种向量,称为旋转向量(或轴角,Axis-Angle)。这种表示法只需一个三维向量即可描述旋转。同样,对于变换矩阵,我们使用一个旋转向量和一个平移向量即可表达一次变换。这时的维数正好是六维。

事实上,旋转向量就是第四讲准备介绍的李代数。 旋转向量和旋转矩阵之间是如何转换的呢?假设有一个旋转轴为 n \mathbf{n} n,角度为 θ \theta θ的旋转,显然,它对应的旋转向量为 θ n \theta\mathbf{n} θn。由旋转向量到旋转矩阵的过程由**罗德里格斯公式(Rodriguez’s Formula)**表明,由于推到过程比较复杂,只给出转换的结果:
R = cos ⁡ θ I + ( 1 − cos ⁡ θ ) n n T + sin ⁡ θ n ^ (3.14) R=\cos\theta I+(1-\cos\theta)\mathbf{n}\mathbf{n}^T+\sin\theta\hat\mathbf{n} \quad\text{(3.14)} R=cosθI+(1cosθ)nnT+sinθn^(3.14).
符号^是向量到反对称的转换符。反之,我们也可以计算从一个旋转矩阵到旋转向量的转换。对于转角 θ \theta θ,有:
t r ( R ) = cos ⁡ θ t r ( I ) + ( 1 − cos ⁡ θ ) t r ( n n T ) + sin ⁡ θ t r ( n ^ ) = 3 cos ⁡ θ + ( 1 − cos ⁡ θ ) (3.15) = 1 + 2 cos ⁡ θ . \begin{aligned} tr(R)&=\cos\theta tr(I) + (1-\cos\theta) tr(\mathbf{n}\mathbf{n}^T) + \sin\theta tr(\hat\mathbf{n})\\ &=3\cos\theta + (1-\cos\theta) \quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad \text{(3.15)}\\ &=1+2\cos\theta. \end{aligned} tr(R)=cosθtr(I)+(1cosθ)tr(nnT)+sinθtr(n^)=3cosθ+(1cosθ)(3.15)=1+2cosθ.

因此:
θ = arccos ⁡ ( t r ( R ) − 1 2 ) . (3.16) \theta = \arccos(\cfrac{tr(R)-1}{2}). \quad\quad \text{(3.16)} θ=arccos(2tr(R)1).(3.16)
关于转轴 n \mathbf{n} n,由于旋转轴上的向量在旋转后不发生改变,说明
R n = n R\mathbf{n} = \mathbf{n} Rn=n
因此,转轴 n \mathbf{n} n是矩阵 R R R特征值 1 1 1对应的特征向量。求解此方程,再归一化,就得到了旋转轴。也可以从“旋转轴经过旋转之后不变”的几何角度看待这个方程。仍然剧透几句,这里的两个转换公式在下一讲仍将出现,你会发现它们正是 S O ( 3 ) SO(3) SO(3)上李群与李代数的对应关系。

3.3.2 欧拉角

下面我们来说说欧拉角。
虽然旋转矩阵、旋转向量能描述旋转,但对人类非常不直观。而欧拉角则提供了一种非常直观的方式来描述旋转——它使用了三个分离的转角,把一个旋转分解成三次绕不同轴的旋转。当然,由于分解方式有许多种,所以欧拉角也存在着不同的定义方法。你或许在航空、航模中听说过“俯仰角”、“偏航角”这些词。欧拉角当中比较常用的一种,便是用“偏航-俯仰-滚转”(yaw-pitch-roll)三个角度来描述一个旋转的。大概如图3-3所示。


欧拉角一个重大缺点是会碰到著名的万向锁问题(Gimbal Lock):在俯仰角为 ± 9 0 ° \pm90^{°} ±90°时,第一次旋转与第三次旋转将使用同一个轴,使得系统丢失了一个自由度(由三次旋转变成了两次旋转)。这被称为奇异性问题,在其他形式的欧拉角中也同样存在。理论上可以证明,只要我们想用三个实数来表达三维旋转时,都会不可避免地碰到奇异性问题。由于这种原理,欧拉角不适合于插值和迭代,往往只用于人机交互中。我们也很少在SLAM程序中直接使用欧拉角表达姿态,同样不会在滤波或优化中使用欧拉角表达旋转(因为它具有奇异性)。不过,若你想验证自己算法是否有错时,转换成欧拉角能够快速辨认结果的正确与否。

在这里插入图片描述

3.4 四元数

3.4.1 四元数的定义

旋转矩阵用九个量描述三自由度的旋转,具有冗余性;
欧拉角和旋转向量是紧凑的,但具有奇异性;

事实上,我们找不到不带奇异性的三维向量描述方式。这有点类似于,当我们想用两个坐标表示地球表面时(如经度、纬度),必定存在奇异性(纬度为 ± 9 0 ° \pm90^{°} ±90°时经度无意义)。三维旋转是一个三维流形,想要无奇异性地表达它,用三个量是不够的。

回忆复数,类似地,在表达三维空间旋转时,也有一种类似于复数的代数:四元数(Quaternion)。四元数时Hamilton找到的一种扩展的复数。它既是紧凑的,也没有奇异性。如果说缺点的话,四元数不够直观,其运算稍微复杂一些。

一个四元数 q \mathbf{q} q拥有一个实部和三个虚部。
q = q 0 + q 1 i + q 2 j + q 3 k , (3.17) \mathbf{q} = q_0 + q_1i + q_2j + q_3k, \qquad \text{(3.17)} q=q0+q1i+q2j+q3k,(3.17)
其中 i , j , k i,j,k i,j,k为四元数的三个虚部。这三个虚部满足关系式:
{ i 2 = j 2 = k 2 = − 1 i j = k , j i = − k (3.18) j k = i , k j = − i k i = j , i k = − j \begin{cases} &i^2=j^2=k^2=-1 \\ &ij=k,ji=-k \qquad \text{(3.18)}\\ &jk=i,kj=-i\\ &ki=j,ik=-j \end{cases} i2=j2=k2=1ij=k,ji=k(3.18)jk=i,kj=iki=j,ik=j
由于它的这种特殊表示形式,有时人们也用一个标量和一个向量来表达四元数:
q = [ s , v ] , s = q 0 ∈ R , v = [ q 1 , q 2 , q 3 ] T ∈ R 3 , \mathbf{q} = \begin{bmatrix} s, \mathbf{v} \end{bmatrix}, s=q_0 \in \mathbb{R}, \mathbf{v} = \begin{bmatrix}q_1,q_2,q_3\end{bmatrix}^T \in \mathbb{R}^3, q=[s,v],s=q0R,v=[q1,q2,q3]TR3,
这里, s s s称为四元数的实部,而 v \mathbf{v} v称为它的虚部。如果一个四元数虚部为 0 \mathbf{0} 0,称之为实四元数。反之,若它的实部为 0 0 0,称之为虚四元数

我们能用单位四元数表示三维空间中任意一个旋转。这种表达方式和旋转矩阵、旋转向量有什么关系呢?我们不妨先来看旋转向量。假设某个旋转是绕单位向量 n = [ n x , n y , n z ] T \mathbf{n}=\begin{bmatrix}n_x,n_y,n_z\end{bmatrix}^T n=[nx,ny,nz]T进行了角度为 θ \theta θ的旋转,那么这个旋转的四元数形式为:
q = [ cos ⁡ θ 2 , n x sin ⁡ θ 2 , n y sin ⁡ θ 2 , n z sin ⁡ θ 2 ] T . (3.19) \mathbf{q} = \begin{bmatrix}\cos{\cfrac{\theta}{2}}, n_x\sin{\cfrac{\theta}{2}}, n_y\sin{\cfrac{\theta}{2}}, n_z\sin{\cfrac{\theta}{2}} \end{bmatrix}^T. \qquad \text{(3.19)} q=[cos2θ,nxsin2θ,nysin2θ,nzsin2θ]T.(3.19)
反之,我们亦可从单位四元数中计算出对应旋转轴与夹角:
{ θ = 2 arccos ⁡ q 0 (3.20) [ n x , n y , n z ] T = [ q 1 , q 2 , q 3 ] T / sin ⁡ θ 2 \begin{cases} \theta = 2\arccos q_0 \qquad\qquad\qquad\qquad\qquad\qquad \text{(3.20)} \\ \begin{bmatrix}n_x,n_y,n_z\end{bmatrix}^T = \begin{bmatrix}q_1,q_2,q_3\end{bmatrix}^T / \sin{\cfrac{\theta}{2}} \end{cases} θ=2arccosq0(3.20)[nx,ny,nz]T=[q1,q2,q3]T/sin2θ
对式 ( 3.19 ) (3.19) (3.19) θ \theta θ加上 2 π 2\pi 2π,我们得到一个相同的旋转,但此时对应的四元数变成了 − q -\mathbf{q} q。因此,在四元数中,任意的旋转都可以由两个互为相反数的四元数表示。

3.4.2 四元数的运算

四元数和通常复数一样,可以进行一系列运算。常见的有四则运算、数乘、求逆、共轭等等。我们分别介绍它们。
现有两个四元数 q a , q b \mathbf{q_a},\mathbf{q_b} qa,qb,它们的向量表示为 [ s a , υ a ] \begin{bmatrix}s_a, \mathbf{\upsilon_a}\end{bmatrix} [sa,υa] [ s b , υ b ] \begin{bmatrix}s_b, \mathbf{\upsilon_b}\end{bmatrix} [sb,υb],或者原始四元数表示为:
q a = s a + x a i + y a j + z a k , q b = s b + x b i + y b j + z b k . \mathbf{q_a}=s_a+x_ai+y_aj+z_ak, \mathbf{q_b}=s_b+x_bi+y_bj+z_bk. qa=sa+xai+yaj+zak,qb=sb+xbi+ybj+zbk.
那么,它们的运算可表示如下。

  1. 加法和减法
    在这里插入图片描述

  2. 乘法
    在这里插入图片描述
    在这里插入图片描述

  3. 共轭

在这里插入图片描述

  1. 模长
    在这里插入图片描述


  2. 在这里插入图片描述

  3. 数乘与点乘
    在这里插入图片描述

3.4.3 用四元数表示旋转

在这里插入图片描述

3.4.4 四元数到旋转矩阵的转换

任意单元四元数描述了一个旋转,该旋转亦可用旋转矩阵或旋转向量描述。从旋转向量到四元数的转换方式已在式(3.20)中给出。因此,现在看来,把四元数转换为矩阵的最直观方法,是先把四元数 q \mathbf{q} q转换为轴角 θ \theta θ n \mathbf{n} n,然后再根据罗德里格斯公式转换为矩阵。不过那样要计算一个 arccos ⁡ \arccos arccos函数,代价较大。实际上这个计算可以通过一定的技巧绕过的。我们省略过程中的推到,直接给出四元数到旋转矩阵的转换方式。
在这里插入图片描述
值得一提的是,由于 q \mathbf{q} q − q -\mathbf{q} q表示同一个旋转,事实上一个 R R R对应的四元数表示并不是唯一的。同时,除了上面给出的转换方式外,还存在其他几种计算方法,而本书(《视觉slam十四讲》)都省略了。实际编程中,当 q 0 q_0 q0接近 0 0 0时,其余三个分量会非常大,导致解不稳定,此时我们再考虑使用其他的方式进行转换。

最后,无论是四元数、旋转矩阵还是轴角,它们都可以用来描述同一个旋转。我们应该在实际中选择对我们最为方便的形式,而不必拘泥于某种特定的样子。

3.5 *相似、仿射和射影变换

3D空间中的变换,除了欧式变换之外,还存在其余几种,其中欧式变换是最简单的。它们一部分和测量几何有关,因为在之后的章节中可能会提到,所以我们先罗列出来。欧式变换保持了向量的长度和夹角,相当于我们把一个刚体原封不动地进行了移动或旋转,不改变它自身的样子。而其他几种变换则会改变它的外形。他们都拥有类似地矩阵表示。

  1. 相似变换
    相似变换比欧式变换多了一个自由度,它允许物体进行均匀的缩放,其矩阵表示为:
    T S = [ s R t 0 T 1 ] . ( 3.37 ) T_S = \begin{bmatrix}sR & \mathbf{t} \\ \mathbf{0}^T & 1\end{bmatrix}. \qquad (3.37) TS=[sR0Tt1].(3.37)
    注意到旋转部分多了一个缩放因子 s s s,表示我们在对向量旋转之后,可以在 x , y , z x,y,z x,y,z三个坐标上进行均匀的缩放。由于含有缩放,相似变换不再保持图形的面积不变。你可以想象一个边长为1的立方体通过相似变换后,变成边长为10的样子(但仍然是立方体)。
  2. 仿射变换
    放射变换的矩阵形式如下:
    T A = [ A t 0 T 1 ] . ( 3.38 ) T_A = \begin{bmatrix}A & \mathbf{t} \\ \mathbf{0}^T & 1\end{bmatrix}. \qquad (3.38) TA=[A0Tt1].(3.38)
    与欧式变换不同的是,仿射变换只要求 A A A是一个可逆矩阵,而不必是正交矩阵。放射变换也叫正交投影。经过仿射变换之后,立方体就不再是方的了,但是各个面仍然是平行四边形。
  3. 射影变换
    射影变换是最一般的变换,它的矩阵形式为:
    T P = [ A t a T v ] . ( 3.39 ) T_P = \begin{bmatrix}A & \mathbf{t} \\ \mathbf{a}^T & v\end{bmatrix}. \qquad (3.39) TP=[AaTtv].(3.39)
    它左上角为可逆矩阵 A A A,右上为平移 t \mathbf{t} t,左下缩放 a T \mathbf{a}^T aT。由于采用齐坐标,当 v ≠ 0 v\neq0 v=0时,我们可以对整个矩阵除以 v v v得到一个右下角为 1 1 1的矩阵;否则,则得到右下角为 0 0 0的矩阵。因此,2D的射影变换一共有8个自由度,3D则共有15个自由度。


    射影变换是现在讲过的变换中,形式最为一般的。从真实世界到相机照片的变换可以看成一个射影变换。可以想象一个原本方形的地板砖,在照片当中是什么样子:首先,它不再是方形的。由于近大远小的关系,它甚至不是平行四边形,而是一个不规则的四边形。

表3-1总结了目前讲到的几种变换的性质。注意在“不变性质”中,从下到上是有包含关系的。例如,欧式变换除了保体积之外,也具有保平行、相交等性质。
在这里插入图片描述

我们之后会说到,从真实世界到相机照片的变换是一个射影变换。如果相机的焦距为无穷远,那么这个变换则为仿射变换。不过,在详细讲述相机模型之前,我们只要对它们有个大致的印象即可。

3.6 实践:Eigen几何模块

现在,我们来实际演练一下前面讲到的各种旋转表达方式。我们将在Eigen中使用四元数、欧拉角和旋转矩阵,演示他们之间的变换方式。我们还会给出一个可视化程序,帮助读者理解这几个变换的关系。

#include <iostream>
#include <cmath>
using namespace std;

#include <Eigen/Core>
// Eigen 几何模块
#include <Eigen/Geometry>

/****************************
* 本程序演示了 Eigen 几何模块的使用方法
****************************/

int main ( int argc, char** argv )
{
    // Eigen/Geometry 模块提供了各种旋转和平移的表示
    // 3D 旋转矩阵直接使用 Matrix3d 或 Matrix3f
    Eigen::Matrix3d rotation_matrix = Eigen::Matrix3d::Identity();
    // 旋转向量使用 AngleAxis, 它底层不直接是Matrix,但运算可以当作矩阵(因为重载了运算符)
    Eigen::AngleAxisd rotation_vector ( M_PI/4, Eigen::Vector3d ( 0,0,1 ) );     //沿 Z 轴旋转 45 度
    cout .precision(3);
    cout<<"rotation matrix =\n"<<rotation_vector.matrix() <<endl;                //用matrix()转换成矩阵
    // 也可以直接赋值
    rotation_matrix = rotation_vector.toRotationMatrix();
    // 用 AngleAxis 可以进行坐标变换
    Eigen::Vector3d v ( 1,0,0 );
    Eigen::Vector3d v_rotated = rotation_vector * v;
    cout<<"(1,0,0) after rotation = "<<v_rotated.transpose()<<endl;
    // 或者用旋转矩阵
    v_rotated = rotation_matrix * v;
    cout<<"(1,0,0) after rotation = "<<v_rotated.transpose()<<endl;

    // 欧拉角: 可以将旋转矩阵直接转换成欧拉角
    Eigen::Vector3d euler_angles = rotation_matrix.eulerAngles ( 2,1,0 ); // ZYX顺序,即roll pitch yaw顺序
    cout<<"yaw pitch roll = "<<euler_angles.transpose()<<endl;

    // 欧氏变换矩阵使用 Eigen::Isometry
    Eigen::Isometry3d T=Eigen::Isometry3d::Identity();                // 虽然称为3d,实质上是4*4的矩阵
    T.rotate ( rotation_vector );                                     // 按照rotation_vector进行旋转
    T.pretranslate ( Eigen::Vector3d ( 1,3,4 ) );                     // 把平移向量设成(1,3,4)
    cout << "Transform matrix = \n" << T.matrix() <<endl;

    // 用变换矩阵进行坐标变换
    Eigen::Vector3d v_transformed = T*v;                              // 相当于R*v+t
    cout<<"v tranformed = "<<v_transformed.transpose()<<endl;

    // 对于仿射和射影变换,使用 Eigen::Affine3d 和 Eigen::Projective3d 即可,略

    // 四元数
    // 可以直接把AngleAxis赋值给四元数,反之亦然
    Eigen::Quaterniond q = Eigen::Quaterniond ( rotation_vector );
    cout<<"quaternion = \n"<<q.coeffs() <<endl;   // 请注意coeffs的顺序是(x,y,z,w),w为实部,前三者为虚部
    // 也可以把旋转矩阵赋给它
    q = Eigen::Quaterniond ( rotation_matrix );
    cout<<"quaternion = \n"<<q.coeffs() <<endl;
    // 使用四元数旋转一个向量,使用重载的乘法即可
    v_rotated = q*v; // 注意数学上是qvq^{-1}
    cout<<"(1,0,0) after rotation = "<<v_rotated.transpose()<<endl;

    return 0;
}

CMakeLists.txt:

cmake_minimum_required( VERSION 2.8 )
project( geometry )

# 添加Eigen头文件
include_directories( "/usr/include/eigen3" )

add_executable( eigenGeometry eigenGeometry.cpp )

编译运行

mkdir build
cd build
cmake ..
make 
./eigenGeometry

输出结果:
在这里插入图片描述

3.7 可视化演示

#include <iostream>
#include <iomanip>
using namespace std;

#include <Eigen/Core>
#include <Eigen/Geometry>
using namespace Eigen;

#include <pangolin/pangolin.h>

struct RotationMatrix {
    Matrix3d matrix = Matrix3d::Identity();
};

ostream& operator << (ostream& out, const RotationMatrix& r) {
    out.setf(ios::fixed);
    Matrix3d matrix = r.matrix;
    out << "=";
    out << "[" << setprecision(2) << matrix(0,0) << "," << matrix(0,1) << "," << matrix(0,2) << "],"
        << "["<<matrix(1,0)<<","<<matrix(1,1)<<","<<matrix(1,2)<<"],"
        << "["<<matrix(2,0)<<","<<matrix(2,1)<<","<<matrix(2,2)<<"]";

    return out;
}

istream& operator >> (istream& in, RotationMatrix& r) {
    return in;
}

struct TranslationVector {
    Vector3d trans = Vector3d(0,0,0);
};

ostream& operator << (ostream& out, const TranslationVector& t) {
    out << "=[" << t.trans(0) << "," << t.trans(1) << "," << t.trans(2) << "]";
    return out;
}

istream& operator >> (istream& in, TranslationVector& t) {
    return in;
}

struct QuaternionDraw {
    Quaterniond q;
};

ostream& operator << (ostream& out, const QuaternionDraw& quat) {
    auto c = quat.q.coeffs();
    out << "=[" << c[0] << "," << c[1] << "," << c[2] << "," << c[3] << "]";
    return out;
}

istream& operator >> (istream& in, QuaternionDraw& quat) {
    return in;
}


int main() {

    pangolin::CreateWindowAndBind ( "visualize geometry", 1000, 600);
    glEnable(GL_DEPTH_TEST);
    pangolin::OpenGlRenderState s_cam (
            pangolin::ProjectionMatrix(1000, 600, 420, 420, 500, 300, 0.1, 1000),
            pangolin::ModelViewLookAt(3, 3, 3, 0, 0, 0, pangolin::AxisY)
            );

    const int UI_WIDTH = 500;

    pangolin::View& d_cam = pangolin::CreateDisplay().SetBounds(0.0, 1.0, pangolin::Attach::Pix(UI_WIDTH), 1.0, -1000.0f/600.0f).SetHandler(new pangolin::Handler3D(s_cam));

    // ui
    pangolin::Var<RotationMatrix> rotation_matrix("ui.R", RotationMatrix());
    pangolin::Var<TranslationVector> translation_vector("ui.t", TranslationVector());
    pangolin::Var<TranslationVector> euler_angles("ui.rpy", TranslationVector());
    pangolin::Var<QuaternionDraw> quaternion("ui.q", QuaternionDraw());
    pangolin::CreatePanel("ui").SetBounds(0.0, 1.0, 0.0, pangolin::Attach::Pix(UI_WIDTH));

    while(!pangolin::ShouldQuit()) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        d_cam.Activate(s_cam);

        pangolin::OpenGlMatrix matrix = s_cam.GetModelViewMatrix();
        Matrix<double, 4, 4> m = matrix;
        // m = m.inverse();
        RotationMatrix R;
        for(int i=0; i<3; ++i) {
            for(int j=0; j<3; ++j) {
                R.matrix(i,j) = m(j,i);
            }
        }
        rotation_matrix = R;

        TranslationVector t;
        t.trans = Vector3d(m(0,3), m(1,3), m(2,3));
        t.trans = -R.matrix*t.trans; // T 求逆 之后的平移向量t
        translation_vector = t;

        TranslationVector euler;
        euler.trans = R.matrix.transpose().eulerAngles(2,1,0);
        euler_angles = euler;

        QuaternionDraw quat;
        quat.q = Quaterniond(R.matrix);
        quaternion = quat;

        glColor3f(1.0, 1.0, 1.0);

        pangolin::glDrawColouredCube();

        // draw the original axis
        glLineWidth(3);

        glColor3f(0.8f, 0.f, 0.f);
        glBegin(GL_LINES);
        glVertex3f(0,0,0);
        glVertex3f(10,0,0);

        glColor3f(0.f, 0.8f, 0.f);
        glVertex3f( 0,0,0 );
        glVertex3f( 0,10,0 );

        glColor3f( 0.2f,0.2f,1.f);
        glVertex3f( 0,0,0 );
        glVertex3f( 0,0,10 );

        glEnd();

        pangolin::FinishFrame();
    }

    return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.17)
project(visualizeGeometry)

set(CMAKE_CXX_STANDARD 14)

include_directories("/usr/include/eigen3")

# 添加Pangolin依赖
find_package(Pangolin)
include_directories(${Pangolin_INCLUDE_DIRS})

add_executable(visualizeGeometry visualizeGeometry.cpp)
target_link_libraries(visualizeGeometry ${Pangolin_LIBRARIES} )

运行结果:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值