在SLAM中,我们需要对姿态进行估计和优化。但是旋转矩阵自身是有约束的,增加优化难度。因此我们需要引入李群和李代数,可以将位姿估计转换为无约束的优化问题。
群(Group)
群是一种集合加上一种运算的代数结构。如果记集合为,运算为,如果满足以下性质,则称为群:
1. 封闭性:
.
2.
结合律:.
3.
幺元:.
4.
逆:.
可以验证旋转矩阵和转换矩阵与矩阵乘法运算都可以构成群。其中旋转矩阵与乘法构成的群为,特殊正交群,欧式转换矩阵与乘法构成特殊欧氏群.
### 李群(Lie Group) ###
具有连续(光滑)性质的群。李群既是群也是流形。SO(3),SE(3)都是李群,但是只有定义良好的乘法,没有加法,所以难以进行极限求导()等操作。
引出李代数
一种李群,对应一个李代数。李代数用小写表示,如.
任意旋转矩阵,则.
考虑R随时间变化,有.
两侧对t求导:.
则:.
所以我们可以知道:是一个反对称矩阵。反对称矩阵都会有一个对应的向量,假设对应的向量为,则:
两侧同时右乘,得到:
所以我们可以看到对R求导之海,多出来一个.
假如,对于进行泰勒展开:
在很短的时间()里,假设不会变化。
如果解上述微分方程(),可以得到:
所以我们就得到了一个在附近的近似估计。
实际上就是对应的李代数。
李代数
李代数由一个集合,一个数域,一个二元运算组成。这个运算某一定程度上反映了两个数的差异。李代数满足下面几个性质:
1.
封闭性:
2.
双线性:,有:
3.
自反性:
4.
雅科比等价:
其中二元运算被称为李括号。
李代数
###:
.
李括号:.
Note:
.
李代数
指数与对数映射
之前引出李代数中推导的,R是李群SO(3),而是李代数so(3).他们之间是有一一对应的关系的。恩,从上面的推导中并不能保证R就是的exp映射,但是实际上就是这样做的。
但是如何对一个进行指数映射?映射还需转换为.对于一个矩阵,我们是没法进行exp运算的。因此我们使用泰勒展开:
同样上述展开也没那么容易进行,好在还有一些别的方法。
任何一个向量,我们都可以分解成方向和长度,也就是.其中为单位向量。
然后,我们可以验证的是:
因为我们,所以上式提供了很好的办法来解决exp问题。经过推导,我们可以得到:
很神奇的事情,上面竟然就是罗德里克斯公式(推导过程实际上不复杂,省略掉了)。
对应的还有一个对数映射:
= (R)^{ } = (_ {n=0}^(R-I)^{n+1} )$
当然,既然我们已经知道罗德里克斯公式了,就不用上式这么复杂的式子去求对数映射了。
旋转角,和旋转矩阵不是一一对应的,因为旋转矩阵唯一的情况下,旋转角可能有多个(角度多了)。这意味着指数映射是满射的。如果角度限制到,那么李群和李代数元素就是一一对映的了。
同样,我们也可以得到与的指数映射和对数映射:
上式中
它的对数映射,右上角与之前一样,而这里的,通过可以计算出来J,从而解线性方程得到.
J为雅克比矩阵。
李代数的求导与扰动模型
之所以要学习李群和李代数,是因为我们的SO(3)和SE(3)都是没有定义加法的。所以无法求导,对于优化非常难办。因此我们想做的是能否将李群和李代数上对应关系运用到求导中,把对李群的求导,转化为对李代数的求导。
所以我们希望的是:.
很遗憾的是,上面的式子不成立。(告辞)
BCH公式
有一个BCH公式,它是对于矩阵来说的以及李群李代数相关的指数相乘展开:
$ (exp(A)exp(B)) A+B+ 2 [A,B ]+ {12} [A, [A ,B]]+… $
可以看到,在数学上,它会有这么多的余项存在的。不过我们做近似的估计。如果或者有一个量是小量,我们可以忽略其二次项。直接考虑到李代数的转换:
$ (exp(_1^{ })exp(_2^{ }))^{ } {
. $
既然导数模型左或者右加上一个小量,上式中,第一个对应的是为R对应的李代数,而为小量,对应到李群,就是R左乘一个小量,第二个式子对应的就是右乘一个小量了。我们后面规定使用为左乘,实际上右乘的J和左乘也相差不多。
右乘的雅科比矩阵:.
也就是:.
同理,如果我们在李代数上进行加法:
上述都是左乘模型。
同样的,对于SE(3)也有类似的BCH近似公式: $$
\exp(\Delta \xi ^{\hat{} }) \exp(\xi^{\hat{} }) \approx
\exp((\mathcal{J}_l^{-1}\Delta \xi + \xi)^{\hat{} }),\\
\exp(\xi^{\hat{} })\exp(\Delta \xi ^{\hat{} }) \approx
\exp((\mathcal{J}_r^{-1}\Delta \xi + \xi)^{\hat{} }).
$$
的形式比较复杂,我们也用不上,就不提了。
so(3)求导
我们想要实现对旋转矩阵的求导,由于旋转矩阵是乘法定义的,所以直接在SO(3)上无法定义出来导数。因此要转换到对应的李代数上来求得导数。
为了得到so(3)上的导数,有两种办法。第一种,是通过so(3)来实现,另一种是给SO(3)左乘一个矩阵来实现。
导数模型
根据导数的定义:
上面的过程用到了BCH展开,泰勒近似,以及的性质:$a^{
}b = -b^{ }a
J_l$,我们不想计算它。所以看看扰动模型。
扰动模型
扰动模型是在李群上左乘一个很小的矩阵
,假设它对应的李代数为
。通过扰动模型得到的导数为
.
扰动模型求导和上面导数模型可以使用同样的套路,而且更简单。因此就不详细写出来了。
可以看到扰动模型的比导数模型得到的结果更加简便一些。因此扰动模型相对于导数模型来说更加的实用。
se(3)求导
对于se(3)的求导,我们直接给出扰动模型:
如果没有李群和李代数的提出,求导就没有理论依据了。而因为有了李群和李代数这种映射关系,我们可以通过将李群用李代数来表示,而李代数是可以进行求导的。从而实现对旋转矩阵的求导。
Sophus库
李群李代数听得人头大。不过,我们不用自己去完成这些东西。对于李群李代数支持比较好的库是sophus,我们就使用这个库来实现SLAM中李群李代数需要的应用。
sophus是eigen的一个扩展,它在eigen的基础上实现了一些李群李代数的操作,没有任何别的依赖项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| #include <iostream> #include <cmath> using namespace std;
#include <Eigen/Core> #include <Eigen/Geometry>
#include "sophus/so3.h" #include "sophus/se3.h" int main( int argc, char** argv ) { Eigen::Matrix3d R = Eigen::AngleAxisd(M_PI/2, Eigen::Vector3d(0,0,1)).toRotationMatrix(); cout<<"RotationMatrix R: \n"<<R<<endl; Sophus::SO3 SO3_R(R); Sophus::SO3 SO3_v( 0, 0, M_PI/2 ); Eigen::Quaterniond q(R); Sophus::SO3 SO3_q( q ); cout<<"SO(3) from matrix: "<<SO3_R<<endl; cout<<"SO(3) from vector: "<<SO3_v<<endl; cout<<"SO(3) from quaternion :"<<SO3_q<<endl; Eigen::Vector3d so3 = SO3_R.log(); cout<<"so3 = "<<so3.transpose()<<endl; cout<<"so3 hat=\n"<<Sophus::SO3::hat(so3)<<endl; cout<<"so3 hat vee= "<<Sophus::SO3::vee( Sophus::SO3::hat(so3) ).transpose()<<endl; Eigen::Vector3d update_so3(1e-4, 0, 0); Sophus::SO3 SO3_updated = Sophus::SO3::exp(update_so3)*SO3_R; cout<<"SO3 updated = "<<SO3_updated<<endl; cout<<"************我是分割线*************"<<endl; Eigen::Vector3d t(1,0,0); Sophus::SE3 SE3_Rt(R, t); Sophus::SE3 SE3_qt(q,t); cout<<"SE3 from R,t= "<<endl<<SE3_Rt<<endl; cout<<"SE3 from q,t= "<<endl<<SE3_qt<<endl; typedef Eigen::Matrix<double,6,1> Vector6d; Vector6d se3 = SE3_Rt.log(); cout<<"se3 = "<<se3.transpose()<<endl; cout<<"se3 hat = "<<endl<<Sophus::SE3::hat(se3)<<endl; cout<<"se3 hat vee = "<<Sophus::SE3::vee( Sophus::SE3::hat(se3) ).transpose()<<endl; Vector6d update_se3; update_se3.setZero(); update_se3(0,0) = 1e-4d; Sophus::SE3 SE3_updated = Sophus::SE3::exp(update_se3)*SE3_Rt; cout<<"SE3 updated = "<<endl<<SE3_updated.matrix()<<endl; return 0; }
|