之前第二讲的地址
我们从最基本的内容讲起:点和向量。点就是空间中的基本元素,没有长度,没有体积。把两个点连接起来,就构成了向量。向量可以看成从某点指向另一点的一个箭头。当我们指定一个三维空间中的某个坐标系时,才可以谈论该向量在此坐标系下的坐标,也就是找到若干个实数对应这个向量。
在线性代数中,三维空间中的某个点的坐标也可以用 R 3 \mathbb{R}^{3} R3来描述。怎么描述呢?假设在这个线性空间内,我们找到该空间的一组基 ( e 1 , e 2 , e 3 ) (e_{1},e_{2},e_{3}) (e1,e2,e3),那么,任意向量 a a a在这组基下就有一个坐标:
a = [ e 1 , e 2 , e 3 ] a = [e_{1},e_{2},e_{3}] a=[e1,e2,e3] [ a 1 a 2 a 3 ] \begin{bmatrix}a_1\\a_2\\a_3\end{bmatrix} ⎣ ⎡a1a2a3⎦ ⎤ = a 1 e 1 + a 2 e 2 + a 3 e 3 . a_1e_1 + a_2e_2 + a_3e_3. a1e1+a2e2+a3e3.
这里的 ( a 1 , a 2 , a 3 ) T . (a1,a2,a3)^T. (a1,a2,a3)T.称为 a a a再次基下的坐标。坐标的具体取值,一是和向量本身有关,二是和坐标系的选取有关。坐标系一般情况下是由3个正交的坐标轴组成的。根据定义方式的不同,坐标系又分为左手系和右手系。左手系的第三个轴与右手系方向相反。大部分3D程序库使用右手系(如OpenGL、3DMax等),也有部分库使用左手系(如Unity、Direct3D等)。
线性代数中向量内积的定义:
a ⋅ b = a T b = ∑ i = 1 3 a i b i = ∣ a ∣ ∣ b ∣ c o s < a , b > . a·b = a^Tb = \sum\limits^3\limits_{i=1}a_ib_i = |a||b|cos. a⋅b=aTb=i=1∑3aibi=∣a∣∣b∣cos<a,b>.
其中 < a , b > <a,b>指向量 a , b a,b a,b的夹角。內积也可以描述向量间的投影关系。而外积则是:
a × b = ∥ e 1 e 2 e 3 a 1 a 2 a 3 b 1 b 2 b 3 ∥ = a × b = \begin{Vmatrix}e_1&e_2&e_3\\a_1&a_2&a_3\\b_1&b_2&b_3\end{Vmatrix} = a×b=∥ ∥e1a1b1e2a2b2e3a3b3∥ ∥= [ a 2 b 3 − a 3 b 2 a 3 b 1 − a 1 b 3 a 1 b 2 − a 2 b 1 ] \left[\begin{matrix} a_2b_3 - a_3b_2 \\ a_3b_1 - a_1b_3 \\ a_1b_2 - a_2b_1 \end{matrix}\right] ⎣ ⎡a2b3−a3b2a3b1−a1b3a1b2−a2b1⎦ ⎤ = = = [ 0 − a 3 a 2 a 3 0 − a 1 − a 2 a 1 0 ] b = d e f a \left[\begin{matrix} 0&-a_3&a_2 \\ a_3&0&-a_1 \\ -a_2&a_1&0 \end{matrix}\right]b\overset{def}{=} a ⎣ ⎡0a3−a2−a30a1a2−a10⎦ ⎤b=defa^ b . b. b.
外积的结果是一个向量,他的方向垂直于这两个向量,大小为 ∣ a ∣ ∣ b ∣ s i n < a , b > , |a||b|sin, ∣a∣∣b∣sin<a,b>,是两个向量张成的四边形的有向面积。对于外积运算,我们引入^符号,把 a a a写成一个矩阵。事实上是一个反对称矩阵,你可以将^记成一个反对称符号。这样就把外积 a × b a×b a×b写成了矩阵与向量的乘法 a a\; a^ b \;b b,把它变成了线性运算。这个符号将在后文经常用到。并且此符号是一个一一映射,意味着任何向量都对应着唯一的一个反对称矩阵,反之亦然:
a a a^ = [ 0 − a 3 a 2 a 3 0 − a 1 − a 2 a 1 0 ] . \left[\begin{matrix} 0&-a_3&a_2 \\ a_3&0&-a_1 \\ -a_2&a_1&0 \end{matrix}\right]. ⎣ ⎡0a3−a2−a30a1a2−a10⎦ ⎤.
同时,需要提醒的是,在不知道向量的坐标时,虽然无法通过向量的乘积直接计算,也可以通过长度和夹角来计算二者的內积。所以两个向量的內积结果和坐标系的选取是无关的。
考虑运动的机器人时,常见的做法是设定一个惯性坐标系(或者称为世界坐标系),可以认为它是固定不动的,例如书中p44的图3-1中的 x w , y w , z w x_w,y_w,z_w xw,yw,zw定义的坐标系。同时,相机和机器人是一个移动坐标系例如 x C , y C , z C x_C,y_C,z_C xC,yC,zC定义的坐标系。读者可能会问:相机视野中某个向量 p p p,它在相机坐标系下的坐标为 p c p_c pc,而从世界坐标系下看,它的坐标为 p w p_w pw,那么,这两个坐标之间是如何转换的呢?这时,就需要先得到改点针对机器人坐标系的坐标值,再根据机器人位姿变换到世界坐标系中。我们需要一种数学手段来描述这个变换关系。我们可以用一个矩阵 T T T来描述它。
两个坐标系之间的运动由一个旋转加上一个平移组成,这种运动称为刚体运动。相机的运动便是一个刚体运动。刚体运动的过程中,同一个向量在各个坐标系下的长度和夹角都不会发生变化。就像手机从手上滑下掉在地板上一样,在掉落到地板前,它的形状、大小、各个面的角度等性质不会有任何变化。手机坐标系到世界坐标之间,相差了一个欧式变换(Euclidean Transform)。
欧式变换由旋转和平移组成。我们首先考虑旋转。设某个正交基 ( e 1 , e 2 , e 3 ) (e_1,e_2,e_3) (e1,e2,e3)经过一次旋转变成了 ( e 1 ′ , e 2 ′ , e 3 ′ ) (e_1^{'},e_2^{'},e_3^{'}) (e1′,e2′,e3′)。那么,对于同一个向量 a a a(该向量并没有随着坐标系的旋转而发生运动),他在两个坐标系下的坐标为 [ a 1 , a 2 , a 3 ] T [a_1,a_2,a_3]^T [a1,a2,a3]T何 [ a 1 ′ , a 2 ′ , a 3 ′ ] T [a_1^{'},a_2^{'},a_3^{'}]^T [a1′,a2′,a3′]T。因为向量本身没变,所以根据坐标的定义,有
[ e 1 , e 2 , e 3 ] [e_1,e_2,e_3] [e1,e2,e3] [ a 1 a 2 a 3 ] \begin{bmatrix}a_1\\\\a_2\\\\a_3\end{bmatrix} ⎣ ⎡a1a2a3⎦ ⎤ = [ e 1 ′ , e 2 ′ , e 3 ′ ] [e_1^{'},e_2^{'},e_3^{'}] [e1′,e2′,e3′] [ a 1 ′ a 2 ′ a 3 ′ ] . \begin{bmatrix}a_1^{'}\\\\a_2^{'}\\\\a_3^{'}\end{bmatrix}. ⎣ ⎡a1′a2′a3′⎦ ⎤.
为了描述两个坐标之间的关系,我们对上述等式的左右两边同时左乘 [ e 1 T e 2 T e 3 T ] \begin{bmatrix}e_1^{T}\\\\e_2^{T}\\\\e_3^{T}\end{bmatrix} ⎣ ⎡e1Te2Te3T⎦ ⎤ ,那么左边的系数就变成了单位矩阵,所以:
[ a 1 a 2 a 3 ] \begin{bmatrix}a_1\\\\a_2\\\\a_3\end{bmatrix} ⎣ ⎡a1a2a3⎦ ⎤ = [ e 1 T e 1 ′ e 1 T e 2 ′ e 1 T e 3 ′ e 2 T e 1 ′ e 2 T e 2 ′ e 2 T e 3 ′ e 3 T e 1 ′ e 3 T e 2 ′ e 3 T e 3 ′ ] =\begin{bmatrix}e_1^Te_1^{'}&e_1^Te_2^{'}&e_1^Te_3^{'}\\\\e_2^Te_1^{'}&e_2^Te_2^{'}&e_2^Te_3^{'}\\\\e_3^Te_1^{'}&e_3^Te_2^{'}&e_3^Te_3^{'}\end{bmatrix} =⎣ ⎡e1Te1′e2Te1′e3Te1′e1Te2′e2Te2′e3Te2′e1Te3′e2Te3′e3Te3′⎦ ⎤ [ a 1 ′ a 2 ′ a 3 ′ ] = d e f R a ′ . \begin{bmatrix}a_1{'}\\\\a_2{'}\\\\a_3{'}\end{bmatrix} \overset{def}{=} Ra^{'}. ⎣ ⎡a1′a2′a3′⎦ ⎤=defRa′.
将中间的矩阵拿出来,定义成一个矩阵 R R R。这个矩阵由两组基之间的內积组成,刻画了旋转前后同一个向量的坐标变换关系。只要旋转是一样的,这个矩阵就是一样的。可以说,矩阵 R R R描述了旋转本身。因此,称为旋转矩阵(Rotation Matrix)。同时,该矩阵各分量是两个坐标系基的內积,由于基向量的长度为1,所以实际上是各基向量夹角的余弦值。所以这个矩阵也叫做方向余弦矩阵(Direction Cosine Matrix)。
旋转矩阵有一些特别的性质。事实上,它是一个行列式为1的正交矩阵。反之,行列式为1的正交矩阵也是一个旋转矩阵。所以,可以将 n 维 n维 n维旋转矩阵的集合定义如下:
S O ( n ) = { R ∈ R n × n ∣ R R T = I , d e t ( R ) = 1 } . SO(n) = \{{R∈\mathbb{R}^{n×n}|RR^T=I,det(R)=1\}}. SO(n)={R∈Rn×n∣RRT=I,det(R)=1}.
S O ( n ) SO(n) SO(n)为特殊正交群的意思。这个集合由 n n n维空间的旋转矩阵组成,特别地, S O ( 3 ) SO(3) SO(3)就是指三维空间的旋转。通过旋转矩阵,我们我们可以直接谈论坐标系之间的旋转变换,而不用再从基开始谈起。
由于旋转矩阵为正交矩阵,它的逆(即转置)描述了一个相反的旋转。按照上面的定义方式,有
a ′ = R − 1 a = R T a a^{'} = R^ {-1}a = R^Ta a′=R−1a=RTa
显然 R T R^T RT刻画了一个相反的旋转。
在欧式变化中,除了旋转还有平移。考虑世界坐标系中的向量 a a a,经过一次旋转(用 R R R描述)和一次平移 t t t后,得到了 a ′ a^{'} a′,那么把旋转和平移合到一起,有
a ′ = R a + t . a^{'} = Ra + t. a′=Ra+t.
其中, t t t称为平移向量。相比于旋转,平移部分只需把平移向量加到旋转之后的坐标上。上式,我们通过一个旋转矩阵 R R R和一个平移向量 t t t完整地描述了一个欧氏空间的坐标变换关系。实际当中,我们会定义坐标系1、坐标系2,那么向量 a a a在这两个坐标系下的坐标为 a 1 , a 2 a_1,a_2 a1,a2,它们之间的关系应该是:
a 1 = R 12 a 2 + t 12 . a_{1} = R_{12}a_2 + t_{12}. a1=R12a2+t12.
这里的 R 12 R_{12} R12是指“把坐标系2的向量变换到坐标系1”中。由于向量乘在这个矩阵的右边,它的下标是从右读到左的。同理,如果我们要表达“从1到2的旋转矩阵”时,就写成 R 21 R_{21} R21。请读者务必清楚本书的记法,因为不同书籍里写法不同,有的会记成左上/下标,而本文写在右侧下标。
关于平移 t 12 t_{12} t12,它实际对应的是坐标系1原点指向坐标系2原点的向量,在坐标系1下取的坐标,所以读者建议读者把它记作“从1到2的向量”。但是反过来的 t 21 t_{21} t21,即从2指向1的向量在坐标系2下的坐标,却并不等于 − t 12 -t_{12} −t12,而是和两个系的旋转还有关系。所以,“我的坐标”这四个字实际上指的是从世界坐标系指向自己坐标系原点的向量,在世界坐标系下渠道的坐标对应到数学符号上。对应到数学符号上,应该是 t W C t_{WC} tWC的取值。同理,它也不是 − t C W -t_{CW} −tCW。
上述的坐标变换关系不是一个线性关系。假设我们进行了两次变换: R 1 , t 1 和 R 2 , t 2 R_1,t_1和R_2,t_2 R1,t1和R2,t2:
b = R 1 a + t 1 , c = R 2 b + t 2 b=R_1a + t_1,\:\:c=R_2b+t_2 b=R1a+t1,c=R2b+t2
那么,从 a a a到 c c c的变换为
c = R 2 ( R 1 a + t 1 ) + t 2 c=R_2(R_1a+t_1)+t_2 c=R2(R1a+t1)+t2
这样的形式在变换多次之后会显得很啰嗦。因此,我们引入齐次坐标和变换矩阵,
[ a ′ 1 ] \begin{bmatrix}a^{'}\\\\1\end{bmatrix} ⎣ ⎡a′1⎦ ⎤ = [ R t 0 T 1 ] =\begin{bmatrix}R&t\\\\0^T&1\end{bmatrix} =⎣ ⎡R0Tt1⎦ ⎤ [ a 1 ] = d e f T \begin{bmatrix}a\\\\1\end{bmatrix} \overset{def}{=}T ⎣ ⎡a1⎦ ⎤=defT [ a 1 ] . \begin{bmatrix}a\\\\1\end{bmatrix}. ⎣ ⎡a1⎦ ⎤.
这是一个数学技巧:我们在一个三维向量的末尾添加1将其变成了一个,将其变成了四维向量,称为齐次坐标。对于这个四维向量,我们可以把旋转和平移写在一个矩阵里,使得整个关系变成线性关系。该式中,矩阵 T T T称为变换矩阵。
我们暂时用 a ~ \tilde{a} a~表示 a a a的齐次坐标。那么依靠齐次坐标和变换矩阵,两次变换的叠加就可以有很好的形式:
b ~ = T 1 a ~ , c ~ = T 2 b ~ ⇒ c ~ = T 2 T 1 a ~ \tilde{b}=T_1\tilde{a},\tilde{c}=T_2\tilde{b}\Rightarrow\tilde{c}=T_2T_1\tilde{a} b~=T1a~,c~=T2b~⇒c~=T2T1a~
但是区分齐次和非齐次坐标的符号令我们感到厌烦,因此此处只需要在向量末尾添加1或者去掉1。所以,在不引起歧义的情况下,以后我们就直接把它写成 b = T a b=Ta b=Ta的样子,默认其中进行了齐次坐标的转换。
关于变换矩阵 T T T,它具有比较特别的结构:左上角为旋转矩阵,右侧为平移向量,左下角为 0 0 0向量,右下角为1.这种矩阵又称为特殊欧式群:
S E ( 3 ) = { T = [ R t 0 T 1 ] ∈ R 4 × 4 ∣ R ∈ S O ( 3 ) , t ∈ R 3 } . SE(3) = \{{T=\begin{bmatrix}R&t\\\\0^T&1\end{bmatrix}∈\mathbb{R}^{4×4}|R∈SO(3),t∈\mathbb{R}^3\}}. SE(3)={T=⎣ ⎡R0Tt1⎦ ⎤∈R4×4∣R∈SO(3),t∈R3}.
与 S O ( 3 ) SO(3) SO(3)一样,求解该矩阵的逆表示一个反向的变化:
T − 1 = [ R T − R T t 0 T 1 ] . T^{-1}=\begin{bmatrix}R^T&-R^Tt\\\\0^T&1\end{bmatrix}. T−1=⎣ ⎡RT0T−RTt1⎦ ⎤.
同样,我们用 T 12 T_{12} T12这样的写法来表示从2到1的变换。并且,为了保持符号的简洁,在不引起歧义的情况下,以后不刻意区别齐次坐标与普通坐标的符号,默认使用的是符合运算法则的那一种例如,当我们写 T a Ta Ta时,使用的是齐次坐标(不然没法计算)。而写 R a Ra Ra时,使用的是非齐次坐标。
这部分的代码在slambook2/ch3/useEigen/eigenMatrix.cpp
中,强烈建议去照着原码码一遍。原码对Eigen库中的许多基础函数进行展示,对入门十分友好。其中有几个点本书作者提到:
对于作者问到的代码最后比较了求逆与求QR分解的运行效率(我只学过前者),运行效率截图如下(求逆、QR及ldlt)。
显然三个效率差不多都是倍数关系,看得出后两个的效率提升还是巨大的。
有了旋转矩阵来描述旋转,有了变换矩阵描述一个6自由度的三维刚体运动还不够。矩阵表示方式至少有以下两个缺点:
R = c o s θ I + ( 1 − c o s θ ) n n T + s i n θ n R = cosθI + (1 - cosθ)nn^T + sinθn R=cosθI+(1−cosθ)nnT+sinθn^.
符号^是向量到反对称矩阵的转换符。反之,我们也可以计算从一个旋转矩阵到旋转向量的转换。对于转角 θ θ θ,取两边的迹,有:
t r ( R ) = c o s θ t r ( I ) + ( 1 − c o s θ ) t r ( n n T ) + s i n θ t r ( n ˆ ) = 3 c o s θ + ( 1 − c o s θ ) = 1 + 2 c o s θ \begin{aligned} tr(R) &= cosθtr(I) + (1 - cosθ)tr(nn^T) + sinθtr(n\^{} ) \\ &= 3cosθ + (1 - cosθ) \\ &= 1 + 2cosθ \end{aligned} tr(R)=cosθtr(I)+(1−cosθ)tr(nnT)+sinθtr(nˆ)=3cosθ+(1−cosθ)=1+2cosθ
因此:
θ = arccos t r ( R ) − 1 2 θ = \arccos\frac{tr(R) - 1}{2} θ=arccos2tr(R)−1
关于转轴 n n n,旋转轴上的向量在旋转后不发生改变,说明:
R n = n Rn = n Rn=n
因此,转轴 n n n是矩阵 R R R特征值1对应的特征向量。求解此方程,再归一化,就得到了旋转轴。
无论是旋转矩阵还是旋转向量,它们虽然能描述旋转,但对人类来说是非常不直观的。当我们看到一个旋转矩阵或旋转向量时,很难想象出这个旋转究竟是什么样的。当它们变换时,我们也不知道物体是在向哪个方向转动。而欧拉角则直观明显地描述了旋转————它使用了3个分离的转角,把一个旋转分解成3次绕绕不同轴的旋转。这样一来,就很容易理解过程了。但是,由于分解方式有多钟,所以欧拉角也存在着众多不同的、易于混淆的定义方法。例如,先绕 X X X,再绕 Y Y Y轴,最后绕 Z Z Z轴旋转,就得到了一个 X Y Z XYZ XYZ轴的旋转。同理,可以定义 Z Y X ZYX ZYX的旋转方式。如果细分下去吧,则还需要区分每次是绕固定轴旋转的,还是绕旋转之后的轴旋转的,这也会给出不一样的定义方式。
在欧拉角当中比较常用的一种,便是用“偏航-俯仰-滚转”3个角度来描述一个旋转。它等价于 Z Y X ZYX ZYX轴的旋转,因此就以 Z Y X ZYX ZYX为例。假设一个刚体的前方(朝向我们的方向)为 X X X轴,右侧为 Y Y Y轴,上方为 Z Z Z轴,。那么, Z Y X ZYX ZYX转角相当于把任意旋转分解成以下3个轴上的转角:
此时,可以使用 [ r , p , y ] T [r,p,y]^T [r,p,y]T这样一个三维的向量描述任意旋转。这个向量十分直观,我们可以从这个向量想象出旋转的过程。其他的欧拉角也是通过这种方式,把旋转分解到3个轴上,得到一个三维的向量,只不过选用的轴及顺序顺序不一样。这里介绍的 r p y rpy rpy角是比较常用的一种,只有很少的欧拉角种类会有 r p y rpy rpy这样脍炙人口的名字。不同的欧拉角是按照旋转轴的顺序来称呼的。例如, r p y rpy rpy角的旋转顺序是 Z Y X ZYX ZYX。同样,也有 X Y Z 、 Z Y Z XYZ、ZYZ XYZ、ZYZ这样的欧拉角——但是它们就没有专门的称呼。
欧拉角的一个重大缺点是会碰到著名的万向锁问题(Gimbal Lock):在俯仰角为 ± 90 ° ±90° ±90°时,第一次旋转与第三次旋转将使用同一个轴,使得系统丢失了一个自由度(由3次旋转变成了2次旋转)。这被称为奇异性问题,在其他形式的欧拉角中也同样存在。理论上可以证明,只要想用3个实数来表达三维旋转,都会不可避免地碰到奇异性问题。由于这种原理,欧拉角不适用于插值和迭代,往往只用于人机交互中。我们因此也很少能在SLAM程序中直接使用欧拉角表达姿态,同样不会在滤波或优化中使用欧拉角表达旋转(因为奇异性的存在)。不过,若你想验证自己的算法是否有错,转换成欧拉角能够帮你快速分辨结果是否正确。在某些主体主要为2D运动的场合(例如扫地机、自动驾驶车辆),我们也可以把旋转分解为三个欧拉角,然后把其中一个拿出来作为定位信息输出。
旋转矩阵用9个量描述3自由度的旋转,具有冗余性;欧拉角和旋转向量是紧凑的,但具有奇异性。事实上,我们找不到不带奇异性的三维向量描述方式。这有点类似于用两个坐标表示地球表面(如经度和维度),将必定存在奇异性(维度为 ± 90 ° ±90° ±90°时经度无意义)。
回忆以前学习过的复数。我们用复数集 C \mathbb{C} C表示复平面上的向量,而复数的乘法则表示复平面上的旋转:例如,乘上复数 i i i相当于逆时针把一个复向量旋转 90 ° 90° 90°。类似地,在表达三维空间旋转时,也有一种类似于复数的代数:四元数。四元数是Hamilton找到的一种扩展的复数。它既是紧凑的,也没有奇异性。但是缺点就是,四元数不够直观,其运算稍复杂些。
把四元数与复数类比可以帮助你更快地理解四元数。例如,当我们想要将复平面的向量旋转 θ θ θ角时,可以给这个复向量乘以 e i θ e^{iθ} eiθ。这是极坐标表示的复数,它也可以写成普通的形式,只要使用欧拉公式即可:
e i θ = c o s θ + i s i n θ . e^{iθ} = cosθ + isinθ. eiθ=cosθ+isinθ.
这正是一个单位长度的复数。所以,在二维情况下,旋转可以由单位复数来描述。类似地,我们会看到,三维旋转可以由单位四元数来描述。
一个四元数 q q q拥有一个实部和三个虚部。本书中实部写在前面(也有写在尾的),像下面这样:
q = q 0 + q 1 i + q 2 j + q 3 k , q = q_0 + q_1i + q_2j + q_3k, q=q0+q1i+q2j+q3k,
其中, i , j , k i,j,k i,j,k为四元数的三个虚部。这三个虚部满足以下关系式:
{ i 2 = j 2 = k 2 = − 1 i j = k , j i = − k 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\\ jk = i,kj = -i\\ ki = j,ik = -j\end {cases} ⎩ ⎨ ⎧i2=j2=k2=−1ij=k,ji=−kjk=i,kj=−iki=j,ik=−j
如果把 i , j , k i,j,k i,j,k看成三个坐标轴,那么它们与自己的乘法和复数一样,相互之间的乘法和外积一样。有时,人们也用一个标量和一个向量来表达四元数:
q = [ s , v ] T , s = q 0 ∈ R , v = [ q 1 , q 2 , q 3 ] T ∈ R 3 q = [s,v]^T, s = q_0∈\mathbb{R},v = [q_1,q_2,q_3]^T∈\mathbb{R}^3 q=[s,v]T,s=q0∈R,v=[q1,q2,q3]T∈R3
这里, s s s称为四元数的实部,而 v v v称为它的虚部。如果一个四元数的虚部为0,则称为实四元数;反之,若它的实部为0,则成为虚四元数。
可以用单位四元数表示三维空间中任意一个旋转,不过这种表达方式和复数有着微妙的不同。在复数中,乘以 i i i意味着旋转 90 ° 90° 90°。这是否意味着四元数中,乘 i i i就是绕 i i i轴旋转 90 ° 90° 90°?那么, i j = k ij = k ij=k是否意味着,先绕 i i i旋转 90 ° 90° 90°,就等于绕 k k k轴 90 ° 90° 90°?显然可以想象一下不是。正确的情况是,乘以 i i i对应着旋转 180 ° 180° 180°,这样才能保证 i j = k ij = k ij=k的性质。而 i 2 = − 1 i^2 = -1 i2=−1,意味着绕 i i i轴旋转 360 ° 360° 360°后得到一个相反的东西。这个东西要旋转两周才会和它原先的样子相等。
常见的有四则运算、共轭、求逆、数乘等。下面分别介绍。
现有两个四元数 q a , q b q_a,q_b qa,qb,它们的向量表示为 [ s a , v a ] T , [ s b , v b ] T [s_a,v_a]^T,[s_b,v_b]^T [sa,va]T,[sb,vb]T,或者原始四元数表示为
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 . q_a = s_a + x_ai + y_aj + z_ak,q_b = s_b + x_bi + y_bj + z _ bk. qa=sa+xai+yaj+zak,qb=sb+xbi+ybj+zbk.
那么,其运算可表示如下。
1. 加法和减法 加法和减法 加法和减法
四元数 q a , q b q_a,q_b qa,qb的加减运算为
q a ± q b = [ s a ± s b , v a ± v b ] T . q_a ± q_b = [s_a ± s_b,v_a ± v_b]^T. qa±qb=[sa±sb,va±vb]T.
2. 乘法 乘法 乘法
乘法是把 q a q_a qa的每一项与 q b q_b qb的每项相乘,最后相加,虚部要按照式(3.21)进行。整理可得
q a q b = s a s b − x a x b − y a y b − z a z b ( s a x b + x a s b + y a z b − z a y b ) i ( s a y b − x a z b + y a s b + z a x b ) j ( s a z b + x a y b − y a x b + z a s b ) k \begin{aligned} q_aq_b&= s_as_b - x_ax_b - y_ay_b - z_az_b \\ &\;\;\;(s_ax_b + x_as_b + y_az_b - z_ay_b)i \\ &\;\;\;(s_ay_b - x_az_b + y_as_b + z_ax_b)j \\ &\;\;\;(s_az_b + x_ay_b - y_ax_b + z_as_b)k \end{aligned} qaqb=sasb−xaxb−yayb−zazb(saxb+xasb+yazb−zayb)i(sayb−xazb+yasb+zaxb)j(sazb+xayb−yaxb+zasb)k
虽然稍微复杂,但形式上是整齐有序的。如果写成向量形式并利用内外积运算,该表达会更加简洁:
q a q b = [ s a s b − v a T v b , s a v b + s b v a + v a × v b ] T q_aq_b = [s_as_b - v_a^Tv_b,s_av_b + s_bv_a + v_a × v_b]^T qaqb=[sasb−vaTvb,savb+sbva+va×vb]T
在该乘法的定义下,两个实四元数乘积仍是实的,这与复数是一致的。然而,我们注意到,由于最后一项外积的存在,四元数乘法通常是不可交换的,除非 v a v_a va和 v b v_b vb在 R 3 \mathbb{R}^3 R3中共线,此时外积项为零。
3. 模长 模长 模长
四元数的模长定义为
∥ q a ∥ = s a 2 + x a 2 + y a 2 + z a 2 \Vert q_a \Vert = \sqrt{s_a^2 + x_a^2 + y_a^2 + z_a^2} ∥qa∥=sa2+xa2+ya2+za2
可以验证,两个四元数乘积的模即模的乘积。这使得单位四元数相乘后仍是单位四元数。
∥ q a q b ∥ = ∥ q a ∥ ∥ q b ∥ \Vert q_aq_b\Vert = \Vert q_a\Vert\Vert q_b\Vert ∥qaqb∥=∥qa∥∥qb∥
4. 共轭 共轭 共轭
四元数的共轭是把虚部取成相反数:
q a ∗ = s a − x a i − y a j − z a k = [ s a , − v a ] T q_a^* = s_a - x_ai - y_aj - z_ak = [s_a,-v_a]^T qa∗=sa−xai−yaj−zak=[sa,−va]T
四元数共轭与其本身相乘,会得到一个实四元数,其实部为模长的平方:
q ∗ q = q q ∗ = [ s a 2 + v T v , 0 ] T . q^*q = qq^* = [s_a^2 \; +\;v^Tv,0]^T. q∗q=qq∗=[sa2+vTv,0]T.
5. 逆 逆 逆
一个四元数的逆为
q − 1 = q ∗ / ∥ q ∥ 2 . q^{-1} = q^*/\Vert q\Vert^2. q−1=q∗/∥q∥2.
按此定义,四元数和自己的逆的乘积为实四元数1:
q q − 1 = q − 1 q = 1. qq^{-1} = q^{-1}q = 1. qq−1=q−1q=1.
如果 q q q为单位四元数,其逆和共轭就是同一个量。同时,乘积的逆具有和矩阵相似的性质:
( q a q b ) − 1 = q b − 1 q a − 1 . (q_aq_b)^{-1} = q_b^{-1}q_a^{-1}. (qaqb)−1=qb−1qa−1.
6. 数乘 数乘 数乘
和向量相似,四元数可以与数相乘:
k q = [ k s , k v ] T . kq = [ks,kv]^T. kq=[ks,kv]T.
我们可以用四元数表达对一个点的旋转。假设有一个空间三维点 p = [ x , y , z ] ∈ R 3 p = [x,y,z] ∈ \mathbb{R}^3 p=[x,y,z]∈R3,以及一个由单位四元数 q q q指定的旋转。三维点 p p p经过旋转之后变为 p ′ p^{'} p′。如果使用矩阵描述,那么有 p ′ = R p p^{'} = Rp p′=Rp。而如果用四元数描述旋转,它们的关系又如何表达呢?
首先,把三维空间点用一个虚四元数来描述:
p = [ 0 , x , y , z ] T = [ 0 , v ] T . p = [0,x,y,z]^T = [0,v]^T. p=[0,x,y,z]T=[0,v]T.
相当于把四元数的3个虚部与空间中的3个轴相对应。那么,旋转后的点 p ′ p^{'} p′可表示为这样的乘积:
p ′ = q p q − 1 . p^{'} = qpq^{-1}. p′=qpq−1.
这里的乘法均为四元数乘法,结果也是四元数。最后把 p ′ p^{'} p′的虚部取出,即得旋转之后点的坐标。并且,可以验证,计算结果的实部为0,故为纯虚四元数。
任意单位四元数描述描述了一个旋转,该旋转也可用旋转矩阵或旋转向量描述。现在来考察四元数与旋转向量、旋转矩阵之间的转换关系。在此之前,四元数乘法也可以写成一种矩阵的乘法。设 q = [ s , v ] T q = [s,v]^T q=[s,v]T,那么,定义如下的符号 + + +和 ⊕ \oplus ⊕为
q + = [ s − v T v s I + v ˆ ] , q ⊕ = [ s − v T v s I − v ˆ ] . q^+ =\begin{bmatrix}s&-v^T\\\\v&sI + v\^{}\end{bmatrix},q^{\oplus} = \begin{bmatrix}s&-v^T\\\\v&sI - v\^{}\end{bmatrix}. q+=⎣ ⎡sv−vTsI+vˆ⎦ ⎤,q⊕=⎣ ⎡sv−vTsI−vˆ⎦ ⎤.
这两个符号将四元数映射成为一个4×4的矩阵。于是四元数乘法可以写成矩阵的形式:
q 1 + q 2 = [ s 1 − v 1 T v 1 s 1 I + v 1 ˆ ] [ s 2 v 2 ] = [ − v 1 T v 2 + s 1 s 2 s 1 v 2 + s 2 v 1 + v 1 ˆ v 2 ] = q 1 q 2 . q_1^+q_2 =\begin{bmatrix}s_1&-v_1^T\\\\v_1&s_1I + v_1\^{}\end{bmatrix}\begin{bmatrix}s_2\\\\v_2\end{bmatrix} = \begin{bmatrix}-v_1^Tv_2 + s_1s_2\\\\s_1v_2 + s_2v_1 +v_1\^{}v_2\end{bmatrix} = q_1q_2. q1+q2=⎣ ⎡s1v1−v1Ts1I+v1ˆ⎦ ⎤⎣ ⎡s2v2⎦ ⎤=⎣ ⎡−v1Tv2+s1s2s1v2+s2v1+v1ˆv2⎦ ⎤=q1q2.
同理亦可证:
q 1 q 2 = q 1 + q 2 = q 2 ⊕ q 1 q_1q_2 = q_1^+q_2=q_2^\oplus q_1 q1q2=q1+q2=q2⊕q1
然后,考虑使用四元数对空间点进行旋转的问题。根据前面的说法,有
p ′ = q p q − 1 = q + p + q − 1 = q + q − 1 ⊕ p . \begin{aligned}p^{'} &= qpq^{-1} = q^+p^+q^{-1}\\ &=q^{+}q^{-1\oplus}p.\end{aligned} p′=qpq−1=q+p+q−1=q+q−1⊕p.
代入两个符号对应的矩阵,得
q + ( q − 1 ) ⊕ = [ s − v T v s I + v ˆ ] [ s v T − v s I + v ˆ ] = [ 1 0 0 T v v T + s 2 I + 2 s v ˆ + ( v ˆ ) 2 ] . q^{+}(q^{-1})^\oplus = \begin{bmatrix}s&-v^T\\\\v&sI + v\^{}\end{bmatrix}\begin{bmatrix}s&v^T\\\\-v&sI + v\^{}\end{bmatrix} = \begin{bmatrix}1&0\\\\0^T&vv^T + s^2I + 2sv\^{} + (v\^{})^2\end{bmatrix}. q+(q−1)⊕=⎣ ⎡sv−vTsI+vˆ⎦ ⎤⎣ ⎡s−vvTsI+vˆ⎦ ⎤=⎣ ⎡10T0vvT+s2I+2svˆ+(vˆ)2⎦ ⎤.
因为 p ′ p^{'} p′和 p p p都是虚四元数,所以事实上该矩阵的右下角即给出了从四元数到旋转矩阵的变换关系:
R = v v T + s 2 I + 2 s v ˆ + ( v ˆ ) 2 . R = vv^T + s^2I + 2sv\^{} + (v\^{})^2. R=vvT+s2I+2svˆ+(vˆ)2.
为了得到四元数到旋转向量的转换公式,对上式两侧求迹,得
t r ( R ) = t r ( v v T + 3 s 2 + 2 s ⋅ 0 + t r ( ( v ˆ ) 2 ) = v 1 2 + v 2 2 + v 3 2 + 3 s 2 − 2 ( v 1 2 + v 2 2 + v 3 2 ) = ( 1 − s 2 ) + 3 s 2 − 2 ( 1 − s 2 ) = 4 s 2 − 1. \begin{aligned}tr(R) &= tr(vv^T + 3s^2 + 2s·0 + tr((v\^{})^2)\\&=v_1^2 + v_2^2 + v_3^2 + 3s^2 - 2(v_1^2 + v_2^2 + v_3^2)\\&=(1 - s^2) + 3s^2 - 2(1 - s^2)\\&=4s^2 - 1.\end{aligned} tr(R)=tr(vvT+3s2+2s⋅0+tr((vˆ)2)=v12+v22+v32+3s2−2(v12+v22+v32)=(1−s2)+3s2−2(1−s2)=4s2−1.
又由式(3.17)得
θ = arccos t r ( R − 1 ) 2 = arccos ( 2 s 2 − 1 ) . \begin{aligned}\theta &= \arccos\frac{tr(R-1)}{2}\\&=\arccos(2s^2 - 1)\end{aligned}. θ=arccos2tr(R−1)=arccos(2s2−1).
即
cos θ = 2 s 2 − 1 = 2 cos 2 θ 2 − 1 , \cos\theta = 2s^2 - 1 = 2\cos^2\frac{\theta}{2}-1, cosθ=2s2−1=2cos22θ−1,
所以:
θ = 2 arccos s \theta = 2\arccos{s} θ=2arccoss
至于旋转轴,如果在式(3.38)中用 q q q的虚部代替 p p p,易知 q q q的虚部组成的向量在旋转时是不动的,即构成旋转轴。于是只要将它除掉它的模长,即得。总而言之,四元数到旋转向量的转换公式如下:
{ θ = 2 arccos q 0 [ n x , n y , n z ] T = [ q 1 , q 2 , q 3 ] T / sin θ 2 . \begin{cases}\theta = 2\arccos{q_0}\\\begin{bmatrix}n_x,n_y,n_z\end{bmatrix}^T = \begin{bmatrix}q_1,q_2,q_3\end{bmatrix}^T/\sin{\frac{\theta}{2}}\end{cases}. {θ=2arccosq0[nx,ny,nz]T=[q1,q2,q3]T/sin2θ.
至于如何从其他方式转换到四元数,只需把上述步骤倒过来处理即可。在实际编程中,程序库通常会为我们准备好各种形式的转换。无论是四元数、旋转矩阵还是轴角,它们都可以用来描述同一个旋转。我们应该在实际中选择最方便的形式,而不必拘泥于某种特定的形式。
除了欧氏变换,3D空间还存在其他几种变换方式,只不过欧氏变换是最简单的。它们一部分和测量几何有关,因为在之后的讲解中可能会提到,所以先罗列出来。欧氏变换保持了向量的长度和夹角,相当于我们把一个刚体原封不动地进行了移动或旋转,不改变它自身的样子。其他几种变换则会改变它的外形。它们都拥有类似的矩阵表示。
我们之后会讲到,从真实世界到相机照片的变换是一个射影变换。如果相机的焦距为无穷远,那么这个变换为仿射变换。
Eigen中对各种形式的表达方式总结如下。注意每种类型都有单精度和双精度两种数据类型,而且和之前一样,不能由编译器自动转换。下面以双精度为例,你可以把最后的d改成f,即得到单精度的数据结构。
参考代码中对应的CMakeLists即可编译此程序。C++文件为slambook2/ch3/useGeometry/useGeometry.cpp
。其中演示了如何在程序中转换这几种表达方式。如果想要进一步了解Eigen的几何模块的读者可以点击Eigen参考。
请读者注意,程序代码通常和数学表示有一些细微的差别。例如,通过运算符重载,四元数和三维向量可以直接计算乘法,但在数学上则需要先把向量转成虚四元数,再利用四元数乘法进行计算,同样的情况也适用于变换矩阵乘三维向量的情况。总体而言,程序中的用法会比数学公式更灵活。
下面我们举一个小例子来演示坐标变换。
例子 设有小萝卜一号和小萝卜二号位于世界坐标系中。记世界坐标系为 W W W,小萝卜们的坐标系为 R 1 R_1 R1和 R 2 R_2 R2。小萝卜一号的位姿为 q 1 = [ 0.35 , 0.2 , 0.3 , 0.1 ] T , t 1 = [ 0.3 , 0.1 , 0.1 ] T q_1 = [0.35,0.2,0.3,0.1]^T,t_1 = [0.3,0.1,0.1]^T q1=[0.35,0.2,0.3,0.1]T,t1=[0.3,0.1,0.1]T。小萝卜二号的位姿为 q 2 = [ − 0.5 , 0.4 , − 0.1 , 0.2 ] T , t 2 = [ − 0.1 , 0.5 , 0.3 ] T q_2 = [-0.5,0.4,-0.1,0.2]^T,t_2 = [-0.1,0.5,0.3]^T q2=[−0.5,0.4,−0.1,0.2]T,t2=[−0.1,0.5,0.3]T。这里的 q q q和 t t t表达的是 T R k , W , k = 1 , 2 , T_{R_k,W},k = 1,2, TRk,W,k=1,2,也就是世界坐标系到相机坐标系的变换关系。现在,小萝卜一号看到某个点在自身的坐标系下坐标为 p R 1 = [ 0.5 , 0 , 0.2 ] T , p_{R_1} = [0.5,0,0.2]^T, pR1=[0.5,0,0.2]T,求该向量在小萝卜二号坐标系下的坐标。
这是一个非常简单,但又具有代表性的例子。在实际场景中你经常需要在同一个机器人的不同部分,或者不同机器人之间转换坐标。代码如slambook2/ch3/examples/coordinateTransform.cpp
。
程序输出的答案是 [ − 0.0309731 , 0.73499 , 0.296108 ] T , [-0.0309731,0.73499,0.296108]^T, [−0.0309731,0.73499,0.296108]T,计算过程也十分简单,只需计算
p R 2 = T R 2 , W T W , R 1 p R 1 p_{R_2} = T_{R_2,W}T_{W,R_1}p_{R_1} pR2=TR2,WTW,R1pR1
即可。注意四元数使用使用之前需要归一化。
如果你是第一次接触旋转和平移这些概念,可能会觉得它们的形式看起来很复杂,因为毕竟每种表达方式都可以与其他方式相互转换,而转换公式有时还比较长。虽然旋转矩阵、变换矩阵的数值可能不够直观,但我们可以很容易地把它们画在窗口里。
本节我们演示两个可视化的例子。首先,假设我们通过某种方式记录了一个机器人的运动轨迹,现在想把它画到一个窗口中。假设轨迹文件存储于trajectory.txt,每一行用下面的格式存储:
t i m e , t x , t y , t z , q x , q y , q z , q w , time,t_x,t_y,t_z,q_x,q_y,q_z,q_w, time,tx,ty,tz,qx,qy,qz,qw,
其中,time指该位姿的记录时间, t t t为平移, q q q为旋转四元数,均是以世界坐标系到机器人坐标系记录。下面我们从文件中读取这些轨迹,并显示到一个窗口中。原则上,如果只是谈论“机器人的位姿”,那么你可以使用 T W R T_{WR} TWR或者 T R W T_{RW} TRW,事实上它们也只差一个逆而已,这意味着知道其中一个就可以很轻松地得到另一个。如果你想要存储机器人的轨迹,那么可以存储所有时刻的 T W R T_{WR} TWR或者 T R W T_{RW} TRW,这并没有太大的差别。
在画轨迹的时候,我们可以把“轨迹”画成一系列点组成的序列,这和我们想象中的“轨迹”比较相似。严格说来,这其实是机器人(相机)坐标系的原点在世界坐标系中的坐标。考虑机器人坐标系的原点 O R O_R OR,此时的 O W O_W OW就是这个原点在世界坐标系下的坐标:
O W = T W R O R = t W R . O_W = T_{WR}O_R = t_{WR}. OW=TWROR=tWR.
这正是 T W R T_{WR} TWR的平移部分。因此,可以从 T W R T_{WR} TWR中直接看到相机在何处,这也是我们说 T W R T_{WR} TWR更为直观的原因。因此因此,在可视化程序里,轨迹文件存储了 T W R T_{WR} TWR而不是 T R W T_{RW} TRW。
最后,我们需要一个支持3D绘图的程序库。有许多库都支持3D绘图,如MATLAB,Python的Matplotlib、OpenGL等。在Linux中,一个常见的库是基于OpenGL的Pangolin库,它在支持OpenGL的绘图操作基础之上还提供一些GUI的功能。本书中,我们使用Git的submodule功能管理本书依赖的第三方库。读者可以进入3rdparty文件夹直接安装所需的库,Git保证了使用的版本是一致的。
具体的代码在slambook2/ch3/examples/plotTrajectory.cpp
,其中演示了如何在Pangolin中画出3D的位姿。我们用红、绿、蓝三种颜色画出每个位姿的三个坐标,然后用黑色线将轨迹连起来。其结果图如下所示:
除了显示轨迹,我们也可以显示3D窗口中相机的位姿。在slambook2/ch3/visualizeGeometry
中,我们以可视化的形式演示相机位姿的各种表达方式。当读者用鼠标操作相机时,左侧的方框里会实时显示相机位姿对应的旋转矩阵、平移、欧拉角和四元数,可以看到数据是如何变化的。不过你在运行的过程中可能会遇到一些bug,建议参考该博主的文章visualizeGeometry的问题解决
部分解决截图:(还有C++11标准改成C++14)
相机位姿运行截图:
− v q T v q 0 − ( q 0 v + v q × v ) T ( − v q ) = q 0 ( v v q − v q T v ) + ( v q × v ) T v q = ( v q × v ) T v q = ( v q × v ) v q = 0 \begin{aligned}&-v_q^Tvq_0 - (q_0v + v_q×v)^T(-v_q)\\&=q_0(vv_q - v_q^Tv) + (v_q × v)^Tv_q\\&= (v_q × v)^Tv_q\\&= (v_q × v)v_q\\&=0\end{aligned} −vqTvq0−(q0v+vq×v)T(−vq)=q0(vvq−vqTv)+(vq×v)Tvq=(vq×v)Tvq=(vq×v)vq