这篇文章针对有一定SLAM基础的同学或者对李群李代数的应用感兴趣的数学专业同学,已经很小众了,但对于真正干这行并想要更深理解的同学可能会很有帮助,因此花了些时间整理发出来。要从理论推导到后面代码应用都读懂难度还是比较大,尽量讲解。
和转换矩阵
分别属于SO(3)和SE(3)李群,我们重点针对SO(3)讲解。李群有几个(仅仅是这篇文章要使用的)重要性质. 性质1: SO(3)和SE(3)李群矩阵和它的转置互为逆矩阵
,行列式为1(有为-1的情况但不会在SLAM中出现)
。 性质2: 加法不封闭性和乘法封闭性,即两个李群矩阵相加后一般就不再是李群了,也就是李群当中不存在加法。两个李群想乘之后仍然是李群。现在如果有一个物体旋转了角度
,再此基础上我们如果再旋转一定角度,数学操作是对这个
再乘上另一个旋转矩阵
,最终得到的
仍然是一个旋转矩阵。但是你如果把这两个旋转矩阵加起来
则没有任何实际的物理意义。性质3:李群中的元素并不是独立的,一个旋转矩阵有9个元素,而实际旋转的自由度是3. 李代数关键点:每一个李群都有一个对应的李代数,我们记为小写的so(3)和se(3)。李代数的元素是线性独立的。一个旋转矩阵对应的李代数有3个元素。记一个李代数为
,定义如下操作
一个李群和它的李代数的对应关系为矩阵指数关系
李代数是有加法封闭性的,即两个李代数相加
仍可以得到一个李代数,对这个新的so(3)李代数进行指数操作可以得到一个新的旋转矩阵
。 李群李代数之间的关系对旋转的求导有很大的帮助。第一个链接前言部分里的公式(2)所提到的非线性最小二乘就是SLAM问题的关键。我把那个公式再写一下
已知两对匹配的3d点(实际应用中可能是图像的2d点和它现实中的3d点的匹配,或者2d图像点和它在另一图像中的对应2d点匹配)
,找到一个转换矩阵
使他们匹配后点与点之间的距离最小。最小二乘都会涉及到对要求解的量的求导。即
是已知常量,所以关键在于
这个式子乍一看还不简单?上下的\mathbf{T}约掉,结果自然就是
,但是实际上它是一个转换矩阵,对一个转换矩阵,这么做是错误的。回想导数的定义
应用在式子3中就应该是
现在你应该看到问题来了,由于李群的加法不封闭性,
根本就没有意义,不再是李群。导数的定义对李群不再有意义,我们上面假设能分子分母直接划掉相同的变量得到结果是根据导数的定义得来的,即假设加法成立,我们的式5就可以得到
但很遗憾,不行。 那么我们如何对转换矩阵求导呢?我们下面要讲解的应用中,把对转换矩阵的求导分开为对位移的求导和对旋转的求导(实际上有直接的公式可以应用不用分开,但是分开求会很直观)。我们前面写到
,
写为齐次点即
,代入式(3),我们得到
1作为常数部分我们不管,前半部分可以分别对旋转求导和对平移求偏导。
对位移的偏导非常好解决,位移是有加法的,求偏导时
视为常亮,所以上式的位移部分可以直接得到答案那就是单位矩阵
。一切的问题都来源于对旋转矩阵的偏导
旋转矩阵也是不存在加法的。那么我们该怎么办呢?接下来就是李代数的应用了。我们把上式等价于对旋转的李代数求导!为什么可以这么等价?我给出一个直观的解释。我们知道,平移之所以能很简单的求导是因为满足加法封闭性,即如果我们把对平移的求导应用到式(4)中,会有一个
,这个
趋于0表示针对当前的
趋于不移动。当对旋转求导时,我们应该找到一个小量,”加”到旋转上,使物体对于当前旋转,不再旋转,只要我们设计出一个小量,满足这个条件,我们就可以针对这个小量求导。我们不能直接加上一个所有元素都趋于0的旋转小量
,但是旋转对应的李代数可以。假设针对当前的旋转的李代数
,加上一个小量,
,当
趋于0时,旋转不变,于是乎,我们可以针对这个小量求导。有点晦涩,多思考一下,这已经是我能想到的最直观的解释了,理论证明可无穷尽也。那么对旋转的求导可以变换为
这个式子是可以求解的。这个式子在很多有关SLAM的书籍中写到过。这里还是用大家很可能有的中文教材<视觉SLAM十四讲>来讲解的。上面的式子对应的是第一版十四讲4.3.3的推导结果。这是李代数求导的第一种方法。这种方法其实相对接下来要讲的第二种方法直观,因为至少我们是根据导数的定义,实实在在的有李代数的加法来求解的。第二种求导方法则不再拘泥于单纯意义上的加法了。 旋转矩阵真的没有加法吗?其实对于旋转来说,乘法就是加法,我们要对一个旋转”加上”另一个旋转,实际上是对旋转矩阵做乘法。所以我们如果对当前的旋转量乘一个近乎于单位矩阵的旋转,其实就是对它”加”了一个小量,当这个旋转小量趋于单位矩阵时,就没有旋转发生。旋转矩阵趋于单位阵对应的李代数就趋于0向量。那么第二个求导方法就是针对当前的旋转乘上一个小的(近乎于单位矩阵)的旋转小量,并对它的李代数趋于0时求极限。
这就是十四讲第一版书上4.3.4扰动模型了。不过正如书上所说,”扰动”
可以是左扰动也可以是右扰动。书上给出的求导结果是左扰动结果,下面我给出右扰动的推导过程以及结果。
这里”李群性质1″是当李代数很小时(所以我们称之为扰动),
,这里
是3×3单位矩阵。”李代数性质2″是
。读者可以自己证明。
下面是激动人心的实际应用
有了李群的导数,接下来就是实际应用部分了。 我这里的实际应用不是说让你自己写几行代码去实现,而是去看看比较成熟的SLAM系统是在哪里用到上面的推导的。这需要你打开相关的论文和代码对比来看,需要些精力。我经常操作的是视觉惯性slam,好的开源库有VINS-FUSION,VINS-Mono(都来自于港科大,前者更新有更强的应用不过后者被更多人熟知),okvis等。okvis是多年前的作品,非常经典,但是代码难度比前面的大不少。综合了一下情况我决定使用VINS-Mono的代码,把李代数的求导结果和他们的代码里一一对应起来。代码在他们的github中 HKUST-Aerial-Robotics/VINS-Mono: A Robust and … – GitHub 我们以VINS-Mono想要求解的reprojection error为例。 vins的原论文 VINS-Mono: A Robust and Versatile Monocular Visual … – arXiv 式25把处于不同图像的对应的2d点投影到同一单位球上然后求残差。 在vins-mono里projection_factor.cpp
里Evaluate
函数中包含了误差函数以及倒数的结果。 首先来看前面几行几个参数
Eigen::Vector3d Pi(parameters[0][0], parameters[0][1], parameters[0][2]);
Eigen::Quaterniond Qi(parameters[0][6], parameters[0][3], parameters[0][4], parameters[0][5]);
Eigen::Vector3d Pj(parameters[1][0], parameters[1][1], parameters[1][2]);
Eigen::Quaterniond Qj(parameters[1][6], parameters[1][3], parameters[1][4], parameters[1][5]);
Eigen::Vector3d tic(parameters[2][0], parameters[2][1], parameters[2][2]);
Eigen::Quaterniond qic(parameters[2][6], parameters[2][3], parameters[2][4], parameters[2][5]);
double inv_dep_i = parameters[3][0];
这几行的parameter来源于传入的要优化的参数,具体来源于在estimator.cpp中的这两行
ProjectionFactor *f = new ProjectionFactor(pts_i, pts_j);
problem.AddResidualBlock(f, loss_function, para_Pose[imu_i], para_Pose[imu_j], para_Ex_Pose[0], para_Feature[feature_index]);
para_Pose[imu_i]
对应于parameter[0],是IMUi
时刻的位姿,para_Pose[imu_j]
对应于parameter[1],是IMUj
时刻位姿,para_Ex_Pose[0]
是相机和IMU的外参,对应parameter[2]。最后一个就对应inv_dep_i
.即该像素点深度的倒数。这几个是想要被优化的量。随后几行
Eigen::Vector3d pts_camera_i = pts_i / inv_dep_i;
Eigen::Vector3d pts_imu_i = qic * pts_camera_i + tic;
Eigen::Vector3d pts_w = Qi * pts_imu_i + Pi;
Eigen::Vector3d pts_imu_j = Qj.inverse() * (pts_w - Pj);
Eigen::Vector3d pts_camera_j = qic.inverse() * (pts_imu_j - tic);
对应vins的原论文式25第三个
的求法。注意式子中的back projection函数
在原文中它的作用是turns a pixel location into a unit vector using camera intrinsic parameters
。具体参见论文图6. 代码中的pts_i
对应的是公式中的
这是论文中所说的指向球面的unit vector
。用它除以深度的倒数(乘上深度),就对应代码中的pts_i / inv_dep_i
,获得一个在相机坐标系下的3d点
这个3d点乘以相机到body(或者说IMU)坐标系下的转换矩阵得到在IMU坐标系下的坐标。
对应的代码中的qic * pts_camera_i + tic
。剩下的式25的第3个式子的位姿变换和代码的每一行一一对应。上式用
转到世界坐标系,再转到
图像所在的坐标系最后转到
相机下的unit vector
即pts_camera_j
.
随后 代码中
#ifdef UNIT_SPHERE_ERROR
residual = tangent_base * (pts_camera_j.normalized() - pts_j.normalized());
#else
double dep_j = pts_camera_j.z();
residual = (pts_camera_j / dep_j).head<2>() - pts_j.head<2>();
#endif
用UNIT_SPHERE_ERROR
才和式25的第一行对应,如果不用的话就和普通的pinhole相机模型的点对应。为了方便起见,我还是推导的不用UNIT_SPHERE_ERROR
所得到的雅各比矩阵。因为我发现VINS, parameter.h中的#define UNIT_SPHERE_ERROR
这一行被注释掉了。 代码下一行 residual = sqrt_info * residual;
okvis也有这一行,这是因为ceres能接受的最小二乘优化只能是最简单的形式
,而带有协方差的优化的形式为
,所以代码中会对
做LLT分解,有
,那个sqrt_info自然就是
了。 综上针对某一个像素点在
frame下的像素坐标我们的residual就是
式子中
来源于测量(比如我们通过特征点匹配的方法知道了i,j是对应的,那时得到的j的坐标就是通过测量得到的),
表示取向量第1位到第n位。把上式具体写出来就是
residual需要求导的对象是body在
frame时的pose,即
。 首先我们计算简单的对位置求导。
其中
为2维,
为3d点
,很容易得到
然后(8)式对
求导,得到
对应代码中的
jaco_i.leftCols<3>() = ric.transpose() * Rj.transpose();
即
这里用到了SO(3)群的基本性质,SO(3)下的矩阵的逆等于它的转置。 之后我们对旋转求导数
导数的前一部分和式10一样,后一部分涉及到李群的求导,为书写方便我们先令
,因为左式本来就是
frame像素点投影到相机平面的3d点。后一部分
现在只对
求导数,那其他量视为是常量,我们令
,另外3d点
得到的是body 坐标系下的3d点,我们记为
。那么上式就变成了
现在的问题就是如何得到
了,简单记为
。那这儿很明显了,应用我们右扰动的推导结果,可以直接得到
代入
,我们就有了 这和代码中的
jaco_i.rightCols<3>() = ric.transpose() * Rj.transpose() * Ri * -Utility::skewSymmetric(pts_imu_i);
对应。 总的来说代码第一个求导部分得到了残差对
frame处位姿的求导。
if (jacobians[0])
{
Eigen::Map<Eigen::Matrix<double, 2, 7, Eigen::RowMajor>> jacobian_pose_i(jacobians[0]);
Eigen::Matrix<double, 3, 6> jaco_i;
jaco_i.leftCols<3>() = ric.transpose() * Rj.transpose();\\对位移求导
jaco_i.rightCols<3>() = ric.transpose() * Rj.transpose() * Ri * -Utility::skewSymmetric(pts_imu_i);\\对旋转求导
jacobian_pose_i.leftCols<6>() = reduce * jaco_i;\\reduce为式10
jacobian_pose_i.rightCols<1>().setZero(); \\这里有一个0列,回顾前面可以发现我们在求偏导时忽略了一个1的
}