引言
搬运工 + 自己阅读代码理解。
pipeline如图:
本节则是阅读总结Initialization
部分。estimator.cpp-->processImage()
.
在提取的图像的 Features 和做完 IMU 的预积分之后,进入了系统的初始化环节,首先,利用 SFM 进行纯视觉估计滑窗内所有帧的位姿及 3D 点逆深度,最后与 IMU 预积分进行对齐求解初始化参数。主要的目的有以下两个:
- 系统使用单目相机,如果没有一个良好的尺度估计,就无法对两个传感器做进一步的融合,这个时候需要恢复出尺度;
- 要对 IMU 进行初始化,IMU 会受到 bias 的影响,所以要得到 IMU 的 bias。
所以需要从初始化中恢复出尺度、重力、速度以及 IMU 的 bias,因为视觉 (SFM) 在初始化的过程中有着较好的表现,所以在初始化的过程中主要以 SFM 为主,然后将 IMU 的预积分结果与其对齐,即可得到较好的初始化结果。
初始化采用视觉和 IMU 的松耦合方案,首先用 SFM 求解滑窗内所有帧的位姿,和所有路标点的 3D 位置,然后跟 IMU 预积分的值对齐,求解重力方向、尺度因子、陀螺仪 bias 及每一帧对应的速度。
1.初始化流程
初始化的流程图如下所示:
首先,对代码中几个易混变量进行说明:
- 传到 processImage 的 image 表示当前帧跟踪到上一帧中的特征点集合,也就是当前帧观测到的所有的路标点(不包括在当前帧新提取的点),如VIN1.视觉前端表中 track_cnt≥2 的 feature,image 类型为:
- FeatureManager 中几个变量:
VINS里面分四步进行estimator.cpp-->processImage()
:
1.第一个就是旋转外参校准;
2.第二个就是找到某帧作为系统初始化原点,计算3D地图点;(下面的相机初始化)
3.第三就是将相机坐标系转到IMU坐标系中;
4.第四就是相机与IMU对齐,包括IMU零偏初始化,速度,重力向量,尺度初始化.
相机初始化estimator.cpp-->processImage()-->initialStructure()
函数:
- 先判断IMU是否有足够的variance, 求all_image_frame中所有帧的平均加速度, 并计算加速度的方差, 判断方差是否足够大
- 调用relativePose判断sliding window是否某一帧l与当前帧视差足够大, 如果存在的话, 则根据特征匹配用五点法计算l和当前帧的relative R,t, 用来做后面的SFM
- 调用GlobalSFM.construct进行SFM, 估计slidingwindow中的camera pose和features的3D position.
- 然后对all_image_frame中没有参与SFM的帧的位姿进行了PnP求解.
- 调用visualInitialAlign()进行视觉和IMU的校准.
简洁:
- 求取本质矩阵求解位姿(relativePose)
- 三角化特征点(sfm.construct)
- PnP 求解位姿(cv::solvePnP)
- 转换到 IMU 坐标系下
-
c0 坐标系作为参考系
- 不断重复直到恢复出滑窗内的 Features 和相机位姿
2.CalibrationExRotation
estimator.cpp-->processImage()-->CalibrationExRotation()
;转换为数学模型实际就是求解超定方程组,可以利用最小二乘也可以使用奇异值分解,由于一般不满秩故采用奇异值分解求解。
相对旋转是相机与 IMU 之间外参的一部分。相机与 IMU 之间的旋转标定非常重要,偏差
1~2∘,系统的精度就会变得很低。通过视觉 SFM 获取了相邻两关键帧之间相机的相对旋转
Rck+1ck,IMU 经过预积分得到的旋转矩阵为
Rbk+1bk。相机与 IMU 之间的相对旋转为
Rcb ,则对于任一帧满足:
Rbk+1bkRcb=RcbRck+1ck(1)将旋转矩阵写为四元数,则上式可以写为
qbk+1bk⊗qcb=qcb⊗qck+1ck(2)将其写为左乘和右乘的形式
([qbk+1bk]L−[qck+1ck]R)qcb=Qk+1kqcb=0(3)
[q]L、
[q]R分别表示 四元数左乘矩阵和 四元数右乘矩阵,其定义为(四元数实部在后)
L=[qwI3+[qxyz]×−qxyzqxyzqw]
[q]R=[qwI3−[qxyz]×−qxyzqxyzqw](4)那么对于 n 对测量值,则有
⎣⎢⎢⎢⎡w10Q10w21Q21⋮wNN−1QNN−1⎦⎥⎥⎥⎤qcb=QNqcb=0(5)其中
wNN−1为外点剔除权重,其与相对旋转求得的角度残差有关,
N 为计算相对旋转需要的测量对数,其由最终的终止条件决定。角度残差可以写为
θk+1k=arccos⎝⎛2tr(R^cb−1Rbk+1bk−1R^cbRck+1ck)−1⎠⎞(6)
从而权重为
wk+1k={1,θk+1kthreshold,θk+1k<threshold(一般为5 度)otherwise(7)至此,就可以通过求解方程
QNqcb=0得到相对旋转,解为
QN的左奇异向量中最小的奇异值对应的特征向量(是最小二乘法的最优解,之前证明过)
但是,在这里还要注意求解的终止条件 (校准完成的终止条件) 。在足够多的旋转运动中,我们可以很好的估计出相对旋转
Rcb ,这时
QN 对应一个准确解,且其零空间的秩为 1。但是在校准的过程中,某些轴向上可能存在退化运动 (如匀速运动),这时
QN的零空间的秩会大于 1。判断条件就是
QN 的第二小的奇异值是否大于某个阈值,若大于则其零空间的秩为1,反之秩大于1,相对旋转
Rcb的精度不够,校准不成功。
bool InitialEXRotation::CalibrationExRotation(vector<pair<Vector3d, Vector3d>> corres, Quaterniond delta_q_imu, Matrix3d &calib_ric_result)
{
//! Step1:滑窗内的帧数加1
frame_count++;
//! Step2:计算两帧之间的旋转矩阵
Rc.push_back(solveRelativeR(corres));
//! IMU 预积分得到的旋转
Rimu.push_back(delta_q_imu.toRotationMatrix());
Rc_g.push_back(ric.inverse() * delta_q_imu * ric);
Eigen::MatrixXd A(frame_count * 4, 4);
A.setZero();
int sum_ok = 0;
for (int i = 1; i <= frame_count; i++)
{
Quaterniond r1(Rc[i]);
Quaterniond r2(Rc_g[i]);
//!Step3:求取估计出的相机与IMU之间旋转的残差
double angular_distance = 180 / M_PI * r1.angularDistance(r2);
//ROS_DEBUG("%d %f", i, angular_distance);
//! Step4:计算外点剔除的权重
double huber = angular_distance > 5.0 ? 5.0 / angular_distance : 1.0;
++sum_ok;
Matrix4d L, R;
//! Step5:求取系数矩阵,公式(3)的系数矩阵Q
//! 得到相机对极关系得到的旋转q的左乘
double w = Quaterniond(Rc[i]).w();
Vector3d q = Quaterniond(Rc[i]).vec();
L.block<3, 3>(0, 0) = w * Matrix3d::Identity() + Utility::skewSymmetric(q);
L.block<3, 1>(0, 3) = q;
L.block<1, 3>(3, 0) = -q.transpose();
L(3, 3) = w;
//! 得到由IMU预积分得到的旋转q的右乘
Quaterniond R_ij(Rimu[i]);
w = R_ij.w();
q = R_ij.vec();
R.block<3, 3>(0, 0) = w * Matrix3d::Identity() - Utility::skewSymmetric(q);
R.block<3, 1>(0, 3) = q;
R.block<1, 3>(3, 0) = -q.transpose();
R(3, 3) = w;
//拼接大矩阵Q,公式(5)的系数矩阵Q
A.block<4, 4>((i - 1) * 4, 0) = huber * (L - R);
}
//!Step6:通过SVD分解,求取相机与IMU的相对旋转
//!解为系数矩阵A的右奇异向量V中最小奇异值对应的特征向量
JacobiSVD<MatrixXd> svd(A, ComputeFullU | ComputeFullV);
Matrix<double, 4, 1> x = svd.matrixV().col(3);
Quaterniond estimated_R(x);
ric = estimated_R.toRotationMatrix().inverse();
//cout << svd.singularValues().transpose() << endl;
//cout << ric << endl;
//!Step7:判断对于相机与IMU的相对旋转是否满足终止条件
//!1.用来求解相对旋转的IMU-Cmaera的测量对数是否大于滑窗大小。
//!2.A矩阵第二小的奇异值是否大于某个阈值,使A得零空间的秩为1
Vector3d ric_cov;
ric_cov = svd.singularValues().tail<3>();
if (frame_count >= WINDOW_SIZE && ric_cov(1) > 0.25)
{
calib_ric_result = ric;
return true;
}
else
return false;
}
接下来介绍estimator.cpp-->processImage()-->initialStructure()
函数流程:
3.检测 IMU 可观性
estimator.cpp-->processImage()-->initialStructure()
:通过计算线加速度的标准差,判断IMU是否有充分运动激励,以进行初始化注意这里并没有算上all_image_frame的第一帧,所以求均值和标准差的时候要减一。参考文章为什么标准差可以代表可观性?
// 计 算 均 值
map<double, ImageFrame>::iterator frame_it;
Vector3d sum_g;
for (frame_it = all_image_frame.begin(), frame_it++; frame_it != all_image_frame.end(); frame_it++)
{
double dt = frame_it->second.pre_integration->sum_dt;
Vector3d tmp_g = frame_it->second.pre_integration->delta_v / dt;
sum_g += tmp_g;
}
// 计 算 方 差
Vector3d aver_g;
aver_g = sum_g * 1.0 / ((int)all_image_frame.size() - 1);
double var = 0;
for (frame_it = all_image_frame.begin(), frame_it++; frame_it != all_image_frame.end(); frame_it++)
{
double dt = frame_it->second.pre_integration->sum_dt;
Vector3d tmp_g = frame_it->second.pre_integration->delta_v / dt;
var += (tmp_g - aver_g).transpose() * (tmp_g - aver_g);
//cout << "frame g " << tmp_g.transpose() << endl;
}
// 计 算 标 准 差
var = sqrt(var / ((int)all_image_frame.size() - 1));
//ROS_WARN("IMU variation %f!", var);
if (var < 0.25)// ! 以 标 准 差 判 断 可 观 性
{
// ROS_INFO("IMU excitation not enouth!");
//return false;
}
4.relativePose
estimator.cpp-->processImage()-->initialStructure()-->relativePose()
;在滑窗内寻找与当前帧的匹配特征点数较多的关键帧作为参考帧, 并通过求本质矩阵(E)cv::findFundamentalMat 计算出当前帧到参考帧的 T;参考E2Rt.
/**
* [Estimator::relativePose 在滑窗中寻找与最新的关键帧共视关系较强的关键帧]
* @param relative_R [description]
* @param relative_T [description] window_size ==> i
* @param l [description]
* @return [description]
*/
bool Estimator::relativePose(Matrix3d &relative_R, Vector3d &relative_T, int &l)
solve_Spts.cpp–>solveRelativeRT()
/**
* [MotionEstimator::solveRelativeRT 利用5点法求解变换矩阵T]
* @param corres [匹配点]
* @param Rotation [旋转量]
* @param Translation [平移量]
* @return [description]
*/
bool MotionEstimator::solveRelativeRT(const vector<pair<Vector3d, Vector3d>> &corres, Matrix3d &Rotation, Vector3d &Translation)
这个函数调用opencv函数:cv::Mat E = cv::findFundamentalMat(*)
求解出本质矩阵E,再利用E求解出T(R、t)参考E2Rt.VINs中代码solve_Spts.cpp-->recoverPose
如下:
/**
* 从本质矩阵中恢复出变换矩阵
* 函数调用decomposeEssentialMat()利用SVD分解求解出四中可能然后再验证出符合要求的唯一解,和十四讲中的计算是一样的!
* @param E
* @param _points1
* @param _points2
* @param _cameraMatrix
* @param _R
* @param _t
* @param _mask 求解本质矩阵时得到的内外点的标志位
* @return
*/
int recoverPose(*)
主要函数流程:relativePose()-->solveRelativeRT(求得E)-->recoverPose()[求得T]-->decomposeEssentialMat()[SVD分解]
5.GlobalSFM.construct
首先,将 feature 队列,放到 vector sfm_f 中,其中 SFMFeature 中的observation 存放的是观测到该路标点的 FrameId 及特征点坐标,如下所示:
GlobalSFM 的流程如下,其中,参考帧为上一步选出来的最共视帧,最后得到的sfm_tracked_points 为三角化出来的 3D 路标点。
几个主要的函数:
1.estimator.cpp–>initialStructure()–>construct():
/**initial_sfm.cpp
* [GlobalSFM::construct description]
* @param frame_num [滑窗内Frame个数]
* @param q [旋转量]
* @param T [平移量]
* @param l [共视帧的id]
* @param relative_R now ===> l
* @param relative_T
* @param sfm_f 滑窗内所有的features
* @param sfm_tracked_points 三角化得到的points
* @return
* bool GlobalSFM::construct(*)
*/
2.estimator.cpp–>initialStructure()–>construct()–>solveFrameByPnP():
/**initial_sfm.cpp
* [GlobalSFM::solveFrameByPnP PnP求解滑窗内关键帧的位姿]
* @param R_initial [description] 位姿,世界到相机坐标系
* @param P_initial [description]
* @param i [description]
* @param sfm_f [description]
* @return [description]
*/
bool GlobalSFM::solveFrameByPnP()
PnP(Perspective-n-Point)是求解3D到2D点对运动的方法.已知
n个空间点
[X,Y,Z,1](前一帧的相机坐标系)以及他们的投影位置
(u1,v1,1)(归一化平面坐标系),使用直接线性变换(DLT)求解位姿。参考3D-2D:PnP_DLT.P3P。
⎣⎡u1v11⎦⎤=[R∣t]3∗4⎣⎢⎢⎡XYZ1⎦⎥⎥⎤4∗1(8)
这个函数中主要的函数则是调用opencv的函数cv::solvePnP()
调用OpenCV 的 PnP 求解,可选择EPNP,DLS等方法.对滑窗内每一帧图像,都跟上一步 SFM 得到的所有 3D 路标点,进行 cv::solvePnP 求解位姿。纯视觉初始化时,采用第一帧
c0作为基准坐标系,若要转化为从 body 坐标系到
c0坐标系,可以进行如下变换:
qbkc0=qckc0⊗(qcb)−1(9)
spbkc0=spckc0−Rbkc0pcb(10)
上式推导如下,这里采用个人(崔神)较熟悉的写法:
Tc0←bk=Tc0←ckTb←c−1
⇔Tc0−bkTb←c=Tc0←ck
⇔Rc0←bk(Rb←cPck+tb←c)+tc0−bk=Rc0←ckPck+tc0←ck(11)
⟹{Rc0←bk=Rc0←ckRb←c−1tc0←bk=tc0←ck−Rc0←bktb←c
3.estimator.cpp–>initialStructure()–>construct()–>triangulateTwoFrames():
/**initial_sfm.cpp
* [GlobalSFM::triangulateTwoFrames 三角化恢复出两帧共视的匹配点]
* @param frame0 [特征帧1]
* @param Pose0 [位姿1]
* @param frame1 [特征帧2]
* @param Pose1 [位姿2]
* @param sfm_f [特征点集合] 求取的点在世界坐标系
*/
void GlobalSFM::triangulateTwoFrames(*)
三角化这里详细推导过,求解过程和第二小节CalibrationExRotation相似,同样是使用奇异值分解求解超定方程的最小二乘解,至于奇异值的最小特征值对应的特征向量为什么是最小二乘的最优解也推导过,不再做笔记。同时这个函数主要是调用triangulatepoint()
函数进行三角化:
/**
* [GlobalSFM::triangulatePoint 三角化恢复特征点深度]
* @param Pose0 [相机位姿1]
* @param Pose1 [相机位姿2]
* @param point0 [特征点1]
* @param point1 [特征点2]
* @param point_3d [恢复出的3D点]
*
* 求取的点在Pose0和Pose1的参考坐标系下
*
* |uP2-P0|
* |vP2-P1|
* |uQ2-Q0|P=0 参考ORB-SLAM初始化部分,DLT求解
* |vQ2-Q1|
* 解为矩阵P的最小奇异值对应的单位奇异矢量
*/
void GlobalSFM::triangulatePoint(*)
6.visualInitialAlign
这一节还有点蒙,懒得往上翻,再放一下这个图:
IMU 预积分与视觉结构对齐示意图:
SFM 与 IMU 预积分结果对齐主要解决 3 个问题:
- 陀螺仪偏置的校准;
- 初始化速度、重力向量和尺度因子;
- 重力向量的精调。
estimator.cpp-->processImage()-->initialStructure()-->visualInitialAlign()
;
/**vins_estimator/src/initial/initial_aligment.cpp
* [VisualIMUAlignment 视觉与IMU对齐,主要解决三个问题:
* 1) 修正陀螺仪的bias
* 2)初始化速度、重力向量g和尺度因子(Metric scale)
* 3)改进重力向量的量值]
* @param all_image_frame [包含图像特征和IMU预积分的集合]
* @param Bgs [陀螺仪bias]
* @param g [重力向量]
* @param x [尺度因子]
* @return [description]
*/
bool VisualIMUAlignment(*)
6.1.陀螺仪 bias 校正
estimator.cpp-->processImage()-->initialStructure()-->visualInitialAlign()-->solveGyroscopeBias()
;
考虑滑动窗口中的连续两个关键帧
bk 和
bk+1,通过 SFM 可以获取两个关键帧时刻本体坐标系相对于参考相机坐标系的旋转
qbkc0和
qbk+1c0 ,通过 IMU 预积分可以获得两个关键帧时刻本体坐标系之间的相对旋转约束
γbk+1bk ,校正陀螺仪偏置的目标函数为:
δbwmink∈B∑∥∥∥qbk+1c0−1⊗qbkc0⊗γbk+1bk∥∥∥2(12)其中(一阶Taylor)
γbk+1bk≈γ^bk+1bk⊗⎣⎡121Jδbkkδθbk+1bkδbwk⎦⎤(13)
B 表示滑动窗口中的所有关键帧,等式(13)为 IMU 预积分中推导的,
γbk+1bk关于陀螺仪偏置误差
δbw 的一阶近似。
等式(12)中目标函数的最小值为单位四元数,所以可将等式(12)进一步改写为(这里的四元数采用实部在前,虚部在后的形式):
qbk+1c0−1⊗qbkc0⊗γbk+1bk=⎣⎢⎢⎡1000⎦⎥⎥⎤
γ^bk+1bk⊗⎣⎡121Jδbwkδθbk+1bkδbwk⎦⎤=qbkc0−1⊗qbk+1c0⊗⎣⎢⎢⎡1000⎦⎥⎥⎤(14)
[121Jδbk+1δθbk+1bkδbwk]=γ^bk+1bk−1⊗qbkc0−1⊗qbk+1c0⊗⎣⎢⎢⎡1000⎦⎥⎥⎤只考虑虚部,则有:
Jδbwkδθbk+1bkδbwk=2(γ^bk+1bk−1⊗qbkc0−1⊗qbk+1c0)vec(15)将等式(15)中等号坐标转为正定矩阵:
[Jδbwkδθbk+1bk]TJδbwkδθbk+1bkδbwk=2[Jδbwkδθbk+1bk]T(γ^bk+1bk−1⊗qbkc0−1⊗qbk+1c0)vec(16)
这样就可以使用 cholesky 分解求解矩阵(16),获取目标函数达到最小的解
δbw,从而完成对陀螺仪偏置的校正。
/**
* [solveGyroscopeBias description]
* @param all_image_frame [包含图像特征和IMU预积分的集合]
* @param Bgs [陀螺仪bias]
* 这部分可以参考文献[1]中公式(15)部分(注:'表示求逆)
*
* Min sum||q_cb1' × q_cb0 × r_bkbk+1|| (1)
*
* 公式(15)的最小值是1(q(1,(0,0,0),所以将其前半部分移到右边得
* | 1 |
* r_bkbk+1^ ·| 1/2·J·bw|=q_cb0' q_cb1 (2)
*
* | 1 |
* | 1/2·J·bw|=r_bkbk+1^' × q_cb0' × q_cb1 (3)
*
* 只取四元数的虚部并求解,
* JTJ·bw = 2JT(r_bkbk+1^' × q_cb0' × q_cb1).vec (4)
* 即,A·bw=b,将多个帧综合起来为
* sum(A)bw = sum(b) (5)
*/
//gyro的标定思路是:对于相邻的两帧, vision有一个相对旋转q, IMU预积分有一个相对旋转γ, Align的目的就是寻找一个最好的δbw使得两个相对旋转尽可能接近
void solveGyroscopeBias(map<double, ImageFrame> &all_image_frame, Vector3d* Bgs)
6.2.初始化速度、重力和尺度因子
estimator.cpp-->processImage()-->initialStructure()-->visualInitialAlign()-->LinearAlignment(×)
;
这一部分的内容对应于 VINS-Mono 代码 initial_aligment.cpp 中的 LinearAlignment()函数。待优化的变量为:
χI=⎣⎢⎢⎢⎢⎢⎢⎢⎡vb0b0vb0b0⋮vb0b0gc0s⎦⎥⎥⎥⎥⎥⎥⎥⎤(17)
将预积分项中的世界坐标系
w 换为
c0 坐标系:
αbk+1bk=Rc0bk(pbk+1c0−pbkc0−vbkc0Δtk+21gc0Δtk2)(18)
βbk+1bk=Rc0bk(vbk+1c0−vbkc0+gc0Δtk)(19)
pbk+1c0和
pbkc0可由视觉 SFM 获得:
pbkc0=spbkc0(20)
pbk+1c0=spbk+1c0(21)将等式(20)(21)代入等式(18):
αbk+1bk=Rc0bc(s(pbk+1c0−pbkc0)−vbkc0Δtk+21gc0Δtk2)(22)将等式(19)(22)中的速度都转换到
c0 坐标系下:
αbk+1bk=Rc0bk(s(pbk+1c0−pbkc0)−Rbkc0vbkbkΔtk+21gc0Δtk2)(23)
βbk+1bk=Rc0bk(Rbk+1c0vbk+1bk+1−Rbkc0vbkbk+gc0Δtk)(24)想办法将等式(23)(24)转换成
Hx=b的形式,这样便于利用 cholesky 进行求解。将等式(10)带入等式(23):
αbk+1bk=Rc0bk(s(pbk+1c0−pbkc0)−Rbkc0vbkbkΔtk+21gc0Δtk2)⇒
αbk+1bk=Rc0bk((spck+1c0−Rbk+1c0pcb)−(spckc0−Rbkc0pcb)−Rbkc0vbkbkΔtk+21gc0Δtk2)⇒
αbk+1bk=Rc0bks(pck+1c0−pckb0)−Rc0bkRbk+1c0pcb+pcb−vbkbkΔtk+21Rc0bkgc0Δtk2⇒
(25)
−vbkbkΔtk+21Rc0bkgc0Δtkbk(pck+1c0−pckc0)s=αbk+1bk+Rc0bkRbk+1c0pcb−pcb⇒
[−IΔtk021Rc0bkΔtk2Rc0bk(pck+1c0−pckc0)]⎣⎢⎢⎡vbkbkvbk+1bk+1gc0s⎦⎥⎥⎤=αbk+1bk+Rc0bkRbk+1c0pcb−pcb
同样地也将等式(24)转换为矩阵形式:
βbk+1bk=Rc0bk(Rbk+1c0vbk+1bk+1−Rbkc0vbkbk+gc0Δtk)⇒
−vbkbk+Rc0bkRbk+1c0vbk+1bk+1+Rc0b2gc0Δtk=βbk+1bk⇒
(26)
[−IRc0bkRc0bk+1Rc0bkΔtk0]⎣⎢⎢⎡vbkbkvbk+1bk+1gc0s⎦⎥⎥⎤=βbk+1bk
联立等式(25)(26):
[−IΔtk−I0Rc0bkRbk+1c021Rc0bxΔtk2Rc0bkΔtkRc0bk(pck+1c0−pckc0)0]⎣⎢⎢⎡vbkbkvbk+1bk+1gc0s⎦⎥⎥⎤=[αbk+1bk+Rc0bkRbk+1c0pcb−pcbβbk+1bk](27)
理论上等式(27)中的等号应该成立,但是由于各种噪声与数值运算的误差,等号并不严格成立,将理论真值
αbk+1bk,βbk+1bk替换为通过 IMU 测量值计算出的标称值
α^bk+1bk,β^bk−1bk得到约等式:
[−IΔtk−I0Rc0bkRbk+1c021Rc0bxΔtk2Rc0bkΔtkRc0bk(pck+1c0−pckc0)0]⎣⎢⎢⎡vbkbkvbk+1bk+1gc0s⎦⎥⎥⎤≈[αbk+1bk+Rc0bkRbk+1c0pcb−pcbβbk+1bk](28)
令:
Hbk+1bk=[−IΔtk−I0Rc0bkRbk+1c021Rc0bxΔtk2Rc0bkΔtkRc0bk(pck+1c0−pckc0)0](29)
χbk+1bk=⎣⎢⎢⎢⎢⎢⎡vbkbkvbkbkvbk+1bk+1gc0s⎦⎥⎥⎥⎥⎥⎤(30)
z^bk+1bk=c[αbk+1bk+Rc0bkRbk+1c0pcb−pcbβbk+1bk](31)
约等式(28)可简写为:
Hbk+1bkχbk+1bk≈z^bk+1bk(32)
通过解线性最小二乘问题:
χbk+1bkmink∈B∑∥∥∥z^bk+1bk−Hbk+1bkχbk+1bk∥∥∥2(33)
即可优化
χI,即滑动窗口中所有关键帧的速度、重力向量在
c0坐标系中的分量列阵和单目尺度因子
s 。
/**
* [LinearAlignment 初始化速度、重力向量g和尺度因子(Metric scale)]
* @param all_image_frame [包含图像特征和IMU预积分的集合]
* @param g [重力向量]
* @param x [description]
* @return [description]
* 参考
*/
bool LinearAlignment(map<double, ImageFrame> &all_image_frame, Vector3d &g, VectorXd &x)
6.3.修正重力矢量
对应代码中 vins_estimator/src/initial/initial_aligment.cpp-->RefineGravity()
函数.
假设重力的模值固定,且其数值为已知量,这样重力向量的自由度就由 3 变成了 2。因此将重力向量重新参数化:重力矢量的模长固定(9.8),其为 2 个自由度,在切空间上对其参数化:
g^=∥g∥g^+w1b1+w2b2=∥g∥g^+[b1b2][w1w2]=∥g∥g+bw既是:
g^=∥g∥⋅g^+ω1b1
+ω2b2
=∥g∥⋅g^^+B
ω
,B∈R3×2,ω
∈R2×1(34)
g^为重力向量的方向向量在
C0坐标系下的坐标,
b1,b2为重力向量正切空间的一对正交基。重新参数化后的重力向量由
ω 控制。令
g^=gc0代入公式(28)得:
[−IΔtk−I0Rc0bkRbk+1c021Rc0bxΔtk2Rc0bkΔtkRc0bk(pck+1c0−pckc0)0]⎣⎢⎢⎡vbkbkvbk+1bk+1ωs⎦⎥⎥⎤
=[αbk+1bk−pcb+Rc0bkRbk+1c0pcb−21Rc0bkΔtk2∥g∥⋅gβbk+1bk−Rc0bkΔtk∥g∥⋅g
](35)
即:
H6×9XI9×1=b6×1,ω2×1=[ω1,ω2]T这样,可以用 Cholosky 分解下面方程求解
XI :
HTHXI=HTb(35)
同样通过 Cholosky 分解求得
gc0 ,即相机
C0 系下的重力向量。最后,通过将
gc0 旋转至惯性坐标系(世界系)中的 z 轴方向 [0,0,1],可以计算第一帧相机系到惯性系的旋转矩阵
qc0w,这样就可以将所有变量调整至惯性世界系(水平坐标系,z 轴与重力方向对齐)中。
/**vins_estimator/src/initial/initial_aligment.cpp
* [RefineGravity 纠正重力向量和速度]
* @param all_image_frame [description]
* @param g [description]
* @param x [description]
*/
//每次估计完g以后, 都会做一次归一化使得其模值为G, 所以可以理解为这里的refine其实是refine重力的方向, 同时refine其他估计量.
//因为方向基本不会差太多, w1,w2通常很小, 所以可以做这样的近似.
void RefineGravity(map<double, ImageFrame> &all_image_frame, Vector3d &g, VectorXd &x)