我是靠谱客的博主 雪白唇彩,这篇文章主要介绍Vins-mono 源码笔记 (6) 紧耦合优化,现在分享给大家,希望可以做个参考。

紧耦合优化

    • 优化主流程
    • 边缘化
      • 边缘化最老帧
      • 边缘化次新帧
      • 补充
      • preMarginalize()
      • marginalize()
        • ThreadsConstructA()
    • 滑动窗口

优化主流程

视觉惯性紧耦合优化的主要实现在 void Estimator::optimization() 中,其中主要采用ceres完成优化,ceres的使用总结见 slam中ceres的常见用法总结
Vins Mono的视觉惯性紧耦合优化执行的是Motion-only BA,也就是只对相机Pose进行优化,而不优化特征点的逆深度。
下面是该函数的流程:
1、添加优化的参数,即图优化的节点,由一下几个部分构成:
(1)、滑动窗口所有帧的Pose节点。

复制代码
1
2
3
4
5
6
7
8
for (int i = 0; i < WINDOW_SIZE + 1; i++) { // 位姿pose的参数化 定义pose的增量加法... ceres::LocalParameterization *local_parameterization = new PoseLocalParameterization(); problem.AddParameterBlock(para_Pose[i], SIZE_POSE, local_parameterization); // pose Ps、Rs转变 6自由度 7个参数 problem.AddParameterBlock(para_SpeedBias[i], SIZE_SPEEDBIAS); // Vs、Bas、Bgs }

(2)、相机的外参数节点。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 相机的外参 for (int i = 0; i < NUM_OF_CAM; i++) { ceres::LocalParameterization *local_parameterization = new PoseLocalParameterization(); // R T problem.AddParameterBlock(para_Ex_Pose[i], SIZE_POSE, local_parameterization); if (!ESTIMATE_EXTRINSIC) { ROS_DEBUG("fix extinsic param"); problem.SetParameterBlockConstant(para_Ex_Pose[i]); // 通过这个将参数固定 不进行优化 } else ROS_DEBUG("estimate extinsic param"); }

(3)、时间戳

复制代码
1
2
3
4
5
6
7
// 时间戳 if (ESTIMATE_TD) { problem.AddParameterBlock(para_Td[0], 1); //problem.SetParameterBlockConstant(para_Td[0]); }

2、如果存在先验信息,则添加先验信息约束,该模块稍后解析。

复制代码
1
2
3
4
5
6
7
8
9
// 如果有先验信息 添加边缘化残差,丢弃帧的约束 MarginalizationFactor if (last_marginalization_info) // 初始化为 nullptr { // construct new marginlization_factor MarginalizationFactor *marginalization_factor = new MarginalizationFactor(last_marginalization_info); problem.AddResidualBlock(marginalization_factor, NULL, last_marginalization_parameter_blocks); }

3、添加观测约束:
(1)、IMU的预积分约束

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
// 添加各个相机的IMU残差 for (int i = 0; i < WINDOW_SIZE; i++) { int j = i + 1; if (pre_integrations[j]->sum_dt > 10.0) continue; // 预积分约束 IMUFactor* imu_factor = new IMUFactor(pre_integrations[j]); // 添加残差 连接 i-j的pose与速度偏置节点 problem.AddResidualBlock(imu_factor, NULL, para_Pose[i], para_SpeedBias[i], para_Pose[j], para_SpeedBias[j]); }

(2)、添加视觉重投影误差

复制代码
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
// 添加视觉的重投影误差 for (auto &it_per_id : f_manager.feature) { it_per_id.used_num = it_per_id.feature_per_frame.size(); if (!(it_per_id.used_num >= 2 && it_per_id.start_frame < WINDOW_SIZE - 2)) continue; ++feature_index; int imu_i = it_per_id.start_frame, imu_j = imu_i - 1; Vector3d pts_i = it_per_id.feature_per_frame[0].point; for (auto &it_per_frame : it_per_id.feature_per_frame) { imu_j++; if (imu_i == imu_j) { continue; } Vector3d pts_j = it_per_frame.point; // 是否需要估计时间戳对齐 if (ESTIMATE_TD) { ProjectionTdFactor *f_td = new ProjectionTdFactor(pts_i, pts_j, it_per_id.feature_per_frame[0].velocity, it_per_frame.velocity, it_per_id.feature_per_frame[0].cur_td, it_per_frame.cur_td, it_per_id.feature_per_frame[0].uv.y(), it_per_frame.uv.y()); problem.AddResidualBlock(f_td, loss_function, para_Pose[imu_i], para_Pose[imu_j], para_Ex_Pose[0], para_Feature[feature_index], para_Td[0]); /* double **para = new double *[5]; para[0] = para_Pose[imu_i]; para[1] = para_Pose[imu_j]; para[2] = para_Ex_Pose[0]; para[3] = para_Feature[feature_index]; para[4] = para_Td[0]; f_td->check(para); */ } else // 直接添加普通的视觉重投影误差 { 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]); } f_m_cnt++; } }

这里区分了是否进行时间戳对齐,如果对齐 则采用 ProjectionTdFactor 类来完成残差的构建。

4、回环的残差,暂时先放着不看。
5、对上述构建的优化问题进行求解。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 优化设置 ceres::Solver::Options options; // 求解方式 options.linear_solver_type = ceres::DENSE_SCHUR; // options.num_threads = 2 优化方法 DOGLEG options.trust_region_strategy_type = ceres::DOGLEG; // 迭代数量 options.max_num_iterations = NUM_ITERATIONS; // options.use_explicit_schur_complement = true; // options.minimizer_progress_to_stdout = true; // options.use_nonmonotonic_steps = true; if (marginalization_flag == MARGIN_OLD) options.max_solver_time_in_seconds = SOLVER_TIME * 4.0 / 5.0; else options.max_solver_time_in_seconds = SOLVER_TIME; TicToc t_solver; ceres::Solver::Summary summary; ceres::Solve(options, &problem, &summary); //cout << summary.BriefReport() << endl; ROS_DEBUG("Iterations : %d", static_cast<int>(summary.iterations.size())); ROS_DEBUG("solver costs: %f", t_solver.toc());

6、消除相机位姿在零空间的变化,同时将优化的结果保存在 该函数稍后单独解析 !!!

复制代码
1
2
double2vector()

7、优化完成后,由于之后滑动窗口会剔除掉一帧,那么则是对需要剔除的帧进行边缘化,边缘化的目的是为了获取先验信息矩阵,即被剔除的帧对其他帧的约束。
同样分两种情况:(1)、marg最老帧 (2)、marg次新帧
8、slideWindow() 滑动窗口剔除一帧后,对保存的数据进行更新。

边缘化

边缘化过程中,有三个主要的数据结构- MarginalizationFactor、ResidualBlockInfo、MarginalizationInfo。
整个大致的套路都是: 1、构造出Factor ——> 2、 用Factor构造出 ResidualBlockInfo ——> 3、将ResidualBlockInfo通过addResidualBlockInfo()函数添加到MarginalizationInfo类中。

那么为啥要这样设计呢?
首先,ceres定义的每个Factor都可以理解成定义了一种残差以及jacobian的计算方式,它是 ceres::CostFunction 的子类,比如 IMUFactor 定义的是IMU预积分残差,它内部重写的Evaluate()定义了该残差jacobian的计算方式。
而ResidualBlockInfo 则可以理解为利用残差构造的一条边,它的定义为:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 残差信息块 struct ResidualBlockInfo { ResidualBlockInfo(ceres::CostFunction *_cost_function, ceres::LossFunction *_loss_function, std::vector<double *> _parameter_blocks, std::vector<int> _drop_set) : cost_function(_cost_function), loss_function(_loss_function), parameter_blocks(_parameter_blocks), drop_set(_drop_set) {} // 求该残差的jacobian void Evaluate(); ceres::CostFunction *cost_function; ceres::LossFunction *loss_function; // 该边/残差的优化参数 std::vector<double *> parameter_blocks; // 待marg的优化变量id std::vector<int> drop_set; double **raw_jacobians; std::vector<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> jacobians; // 残差 IMU 15×1 视觉 2×1 Eigen::VectorXd residuals; int localSize(int size) { return size == 7 ? 6 : size; } };

用一个Factor, 鲁棒核函数 LossFunction,以及该Factor(边/残差)的优化参数parameter_blocks,以及需要marg的参数drop_set 构造出 ResidualBlockInfo 对象 。ResidualBlockInfo类中 定义了函数 Evaluate() , 该 Evaluate() 函数内部则会调用 CostFunction 也就是Factor的 Evaluate()去计算jacobian。

另一个数据结构就是 MarginalizationInfo 类 ,它的定义如下:
MarginalizationInfo类的定义为:

复制代码
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
class MarginalizationInfo { public: ~MarginalizationInfo(); int localSize(int size) const; int globalSize(int size) const; // 添加残差信息 void addResidualBlockInfo(ResidualBlockInfo *residual_block_info); // marg相关的两个主要函数 void preMarginalize(); // 求 jacobian void marginalize(); // 求解jacobian后 构建A、b矩阵 std::vector<double *> getParameterBlocks(std::unordered_map<long, double *> &addr_shift); // 所有的残差 类似与图优化中的边 std::vector<ResidualBlockInfo *> factors; // m 表示 marg掉的总维度 n 表示保留的总维度 int m, n; // 优化变量参数的地址 + 参数的变量个数 std::unordered_map<long, int> parameter_block_size; //global size int sum_block_size; // 参数 地址 + 状态参数的位置 id std::unordered_map<long, int> parameter_block_idx; //local size // 所有优化变量内存地址 + 数据 preMarginalize()设置 std::unordered_map<long, double *> parameter_block_data; // 边缘化后 保留的各个优化变量的维度 std::vector<int> keep_block_size; //global size // 各个优化变量的id std::vector<int> keep_block_idx; //local size // std::vector<double *> keep_block_data; // 最后边缘化后的H矩阵 反求Jacobian Eigen::MatrixXd linearized_jacobians; // 最后边缘化后反求先验残差 Eigen::VectorXd linearized_residuals; const double eps = 1e-8; };

ResidualBlockInfo构造好后,通过 MarginalizationInfo 类的 addResidualBlockInfo()函数添加到 MarginalizationInfo 类对象中,函数如下:

复制代码
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
// 给图优化添加一个残差 void MarginalizationInfo::addResidualBlockInfo(ResidualBlockInfo *residual_block_info) { // factors 保存所有 ResidualBlockInfo 即将残差添加到图优化中 factors.emplace_back(residual_block_info); // 提取残差相关的参数数组指针 std::vector<double *> &parameter_blocks = residual_block_info->parameter_blocks; // 每个参数的变量个数 cost_function 即Factor std::vector<int> parameter_block_sizes = residual_block_info->cost_function->parameter_block_sizes(); // 遍历该残差的全部优化参数 for (int i = 0; i < static_cast<int>(residual_block_info->parameter_blocks.size()); i++) { // 记录该参数的地址 double *addr = parameter_blocks[i]; // 记录该参数的变量个数 int size = parameter_block_sizes[i]; parameter_block_size[reinterpret_cast<long>(addr)] = size; // 参数块地址以及变量个数 保存到hash map } for (int i = 0; i < static_cast<int>(residual_block_info->drop_set.size()); i++) { // 需要marg的状态的地址 double *addr = parameter_blocks[residual_block_info->drop_set[i]]; // 需要marg的状态放置与parameter_block_idx parameter_block_idx[reinterpret_cast<long>(addr)] = 0; // 需要marg的设置为0 } }

这个函数的要点是:
1、将残差添加到 factors 容器中。
2、再将每个残差的优化参数的内存地址以及维度size保存在 parameter_block_size map中 .
3、接着将每个残差需要marg的变量也就是drop_set中的变量,添加到 parameter_block_idx,并且先设置id=0。

梳理了出现的数据结构后,再看 Marginalization 的代码就好理解了,下面是过程:

首先边缘化分为边缘化最老帧与边缘化次新帧,先分析边缘化最老帧 ;

边缘化最老帧

(1)、首先处理老先验信息

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 如果之前存在先验信息矩阵 则在对当前图优化问题边缘化前,要将老先验信息矩阵添加 if (last_marginalization_info) // last_marginalization_info 在该函数的后面被赋值 { vector<int> drop_set; // 检查要marg的状态 位于之前的哪个位置 for (int i = 0; i < static_cast<int>(last_marginalization_parameter_blocks.size()); i++) { // 首先看要marg的最老帧的状态 位于哪个位置 将位置记录在 drop_set if (last_marginalization_parameter_blocks[i] == para_Pose[0] || last_marginalization_parameter_blocks[i] == para_SpeedBias[0]) drop_set.push_back(i); } // 用老先验信息矩阵 construct new marginlization_factor MarginalizationFactor *marginalization_factor = new MarginalizationFactor(last_marginalization_info); // 构建残差块 即图优化中的边 相当于 problem.AddResidualBlock ResidualBlockInfo *residual_block_info = new ResidualBlockInfo(marginalization_factor, NULL, last_marginalization_parameter_blocks, drop_set); // 添加边 marginalization_info->addResidualBlockInfo(residual_block_info); }

还是那个套路,
先创建Factor,这里用老 MarginalizationInfo 来构建 MarginalizationFactor
MarginalizationFactor *marginalization_factor = new MarginalizationFactor(last_marginalization_info);

在该构造函数中,将 MarginalizationFactor的 parameter_block_sizes_ 设置为 last_marginalization_info 的 keep_block_size容器,
以及将MarginalizationFactor的 num_residuals_ 设置为 last_marginalization_info的保留参数维数 n 。

然后用 marginalization_factor 构造 残差块ResidualBlockInfo,这个没什么好说的。

最后将残差块residual_block_info 通过 addResidualBlockInfo()添加到 marginalization_info 中,这个函数参考前面的分析。

(2)、处理0-1帧的预积分观测
首先构建IMU Factor - IMUFactor

用 IMUFactor 构建残差

复制代码
1
2
3
4
ResidualBlockInfo *residual_block_info = new ResidualBlockInfo(imu_factor, NULL, vector<double *>{para_Pose[0], para_SpeedBias[0], para_Pose[1], para_SpeedBias[1]}, vector<int>{0, 1});

优化参数为第0帧与第1帧的P、V、Q, marg掉第0帧的P、V、Q。
然后 残差添加到 marginalization_info 。

(3)、将最老帧看到的特征点构建重投影误差,
首先用第0帧的观测与第j帧的观测构造重投影Factor -
ProjectionFactor *f = new ProjectionFactor(pts_i, pts_j);
然后构建残差 -

复制代码
1
2
3
4
ResidualBlockInfo *residual_block_info = new ResidualBlockInfo(f, loss_function, vector<double *>{para_Pose[imu_i], para_Pose[imu_j], para_Ex_Pose[0], para_Feature[feature_index]}, vector<int>{0, 3});

marg掉第0帧Pose,以及所有看到的特征点的逆深度。

(4)、preMarginalize() 遍历所有添加的残差 ,然后求jacobian。
(5)、marginalize() 执行边缘化。
(6)、由于执行下一次优化时的marg时,会通过 vector2double()将优化后的状态放入 para_Pose、para_SpeedBias等数组中,而由于 slideWindow()实际上改变了marg后的保留状态在滑动窗口的位置,所以这里提前计算出,marg后保留状态在滑动窗口 para_Pose、para_SpeedBias 等数组中的位置。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// hash_map std::unordered_map<long, double *> addr_shift; // 窗口滑动后 ,当前状态保存的位置会前移一位 for (int i = 1; i <= WINDOW_SIZE; i++) { // 第i帧的内存地址 关联 第i-1帧的内存地址 addr_shift[reinterpret_cast<long>(para_Pose[i])] = para_Pose[i - 1]; // para_Pose[i - 1] 的值为 首地址 可以理解为double [SIZE_POSE]的数组名 addr_shift[reinterpret_cast<long>(para_SpeedBias[i])] = para_SpeedBias[i - 1]; } for (int i = 0; i < NUM_OF_CAM; i++) addr_shift[reinterpret_cast<long>(para_Ex_Pose[i])] = para_Ex_Pose[i]; if (ESTIMATE_TD) { addr_shift[reinterpret_cast<long>(para_Td[0])] = para_Td[0]; } // 返回的 parameter_blocks 即 下一次marg时,保留的变量 位于的 para_Pose、 para_SpeedBias...等数组的内存地址 // 遍历 parameter_block_idx vector<double *> parameter_blocks = marginalization_info->getParameterBlocks(addr_shift); if (last_marginalization_info) delete last_marginalization_info; last_marginalization_info = marginalization_info; // 将当前先验信息矩阵 保存为 老先验 last_marginalization_parameter_blocks = parameter_blocks; // 保存地址

边缘化次新帧

当次新帧非关键帧时,需要marg掉,此时的次新帧的特征点的观测与周围的关键帧很相似,因此可以直接丢弃(IMU的预积分要传递给下一帧),所以这里marg不需要添加任何观测的残差,仅仅只把老的先验信息残差添加进来进行marg。
但是下面这里添加旧先验信息的部分,ROS_ASSERT(last_marginalization_parameter_blocks[i] != para_SpeedBias[WINDOW_SIZE - 1])表示不能包含 para_SpeedBias[WINDOW_SIZE - 1] ?但是 para_SpeedBias[WINDOW_SIZE - 1] 应该存在的吧?? 还有 if (last_marginalization_parameter_blocks[i] == para_Pose[WINDOW_SIZE - 1]) 为什么只有 para_Pose[WINDOW_SIZE - 1] 而不包含 para_SpeedBias[WINDOW_SIZE - 1] ,应该速度偏置也需要marg吧?? 为啥不处理 para_SpeedBias[WINDOW_SIZE - 1] ???
还望知道的朋友告知!!

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (last_marginalization_info) { vector<int> drop_set; // for (int i = 0; i < static_cast<int>(last_marginalization_parameter_blocks.size()); i++) { // 不能包括有 次新帧的速度偏置状态 为啥???????? ROS_ASSERT(last_marginalization_parameter_blocks[i] != para_SpeedBias[WINDOW_SIZE - 1]); // 次新帧的Pose 设置为 marg 速度偏置 不 marg ???????? if (last_marginalization_parameter_blocks[i] == para_Pose[WINDOW_SIZE - 1]) drop_set.push_back(i); } // construct new marginlization_factor MarginalizationFactor *marginalization_factor = new MarginalizationFactor(last_marginalization_info); ResidualBlockInfo *residual_block_info = new ResidualBlockInfo(marginalization_factor, NULL, last_marginalization_parameter_blocks, drop_set); // 添加到Factor 设置 parameter_block_size 添加要marg 的到 parameter_block_idx marginalization_info->addResidualBlockInfo(residual_block_info); }

补充

1、marg时添加老的先验信息的原因。
进行新的边缘化时,需要将旧的先验约束一并加入,让这些旧的先验约束继续约束保留帧,否则不加先验约束的边缘化会导致系统尺度的缺失,尤其是系统在进行退化运动时(如无人机的悬停和恒速运动)。一般来说只有两个轴向的加速度不为0的时候,才能保证尺度可观,而退化运动对于无人机或者机器人来说是不可避免的。所以在系统处于退化运动的时候,要加入先验信息保证尺度的客观性。
参考: VINS-Mono代码分析与总结(完整版)

preMarginalize()

当所有的残差都添加完毕后,执行 preMarginalize()函数,该函数要点:
1、对每个添加到 MarginalizationInfo 的残差 求jacobian,也就是构建Hi = Ji^T* Wi * Ji 的 Ji 。
2、将每个残差的状态变量拷贝到 parameter_block_data。

复制代码
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
// marg的准备 void MarginalizationInfo::preMarginalize() { // factors 在 addResidualBlockInfo 函数中添加 保存所有的 residual_block_info // factors 就相当于图优化中的边 遍历每一条边 for (auto it : factors) { // 求解该边的jacobian it->Evaluate(); // 将状态保存到 parameter_block_data // 获得该残差的各优化参数维度 std::vector<int> block_sizes = it->cost_function->parameter_block_sizes(); for (int i = 0; i < static_cast<int>(block_sizes.size()); i++) { // 先获得该参数的地址 long addr = reinterpret_cast<long>(it->parameter_blocks[i]); // 获得该参数的维度 用与复制数据是计算大小 int size = block_sizes[i]; // 如果没有在 parameter_block_data 中 则添加 if (parameter_block_data.find(addr) == parameter_block_data.end()) { double *data = new double[size]; memcpy(data, it->parameter_blocks[i], sizeof(double) * size); // 将数据拷贝到 data 中 parameter_block_data[addr] = data; // 保存 } } } }

然后就是 Marginalization 的正式部分 ——
void MarginalizationInfo::marginalize()

marginalize()

函数整体的流程如下:
1、设置各个状态的位置 ,放置与 parameter_block_idx map中 ,parameter_block_idx 的pair的第一个元素为状态的内存地址,第二个元素为状态的位置。 parameter_block_size 是如何 构建的?- 在 addResidualBlockInfo()函数中更新。需要注意的是,需要marg的状态的位置全部设置在最前面,这样构造的H矩阵中,需要marg的部分就在左上角。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int pos = 0; // 遍历需要marg的状态 要marg的状态放置与左上角 这里首先设置的是需要marg的变量 for (auto &it : parameter_block_idx) { it.second = pos; // 设置 该变量的id 即位置 pos += localSize(parameter_block_size[it.first]); // 通过 状态的地址 查找 该状态的 维度 localSize() 使Pose 由 7 变为 6 } // m 为 marg掉的维度 m = pos; // 然后 设置保留的状态的位置 for (const auto &it : parameter_block_size) { // 将未填加的状态添加到 parameter_block_idx if (parameter_block_idx.find(it.first) == parameter_block_idx.end()) { parameter_block_idx[it.first] = pos; pos += localSize(it.second); } } n = pos - m;

2、多线程构造A与b矩阵。(Ax = b)
先初始化A,b矩阵-

复制代码
1
2
3
4
5
6
TicToc t_summing; Eigen::MatrixXd A(pos, pos); // Pos即总状态维度 Eigen::VectorXd b(pos); A.setZero(); b.setZero();

创建四个线程 ,将所有的残差(factor)均分到threadsstruct数组的元素中,然后 每个线程负责一个threadsstruct的 ThreadsStruct。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
//multi thread 4个线程 TicToc t_thread_summing; pthread_t tids[NUM_THREADS]; // 4个线程 ThreadsStruct threadsstruct[NUM_THREADS]; // 每个元素表示 该线程需要处理的残差 int i = 0; // 遍历每一个残差 for (auto it : factors) { // 将每个残差分配到 threadsstruct threadsstruct[i].sub_factors.push_back(it); i++; i = i % NUM_THREADS; // 循环 }

多线程计算 A,b矩阵,这里线程的主函数是 ThreadsConstructA(),计算完毕将结果更新到A,b。

复制代码
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
// 遍历 threadsstruct 的每个元素 启动线程计算 for (int i = 0; i < NUM_THREADS; i++) { TicToc zero_matrix; threadsstruct[i].A = Eigen::MatrixXd::Zero(pos,pos); threadsstruct[i].b = Eigen::VectorXd::Zero(pos); threadsstruct[i].parameter_block_size = parameter_block_size; threadsstruct[i].parameter_block_idx = parameter_block_idx; // 启动线程 ThreadsConstructA 对 threadsstruct[i] 进行构造 int ret = pthread_create( &tids[i], NULL, ThreadsConstructA ,(void*)&(threadsstruct[i])); if (ret != 0) { ROS_WARN("pthread_create error"); ROS_BREAK(); } } // 更新 矩阵 A b for( int i = NUM_THREADS - 1; i >= 0; i--) { pthread_join( tids[i], NULL ); A += threadsstruct[i].A; b += threadsstruct[i].b; }

接着执行Marginalization, 就是那几个熟悉的公式,不多说:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//TODO 需要marg的部分 Eigen::MatrixXd Amm = 0.5 * (A.block(0, 0, m, m) + A.block(0, 0, m, m).transpose()); // 对称化 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> saes(Amm); // 求解特征值分解 //ROS_ASSERT_MSG(saes.eigenvalues().minCoeff() >= -1e-4, "min eigenvalue %f", saes.eigenvalues().minCoeff()); // PtAP = w => A = PwPt => A-1 = P w-1 Pt Eigen::MatrixXd Amm_inv = saes.eigenvectors() * Eigen::VectorXd((saes.eigenvalues().array() > eps).select(saes.eigenvalues().array().inverse(), 0)).asDiagonal() * saes.eigenvectors().transpose(); //printf("error1: %fn", (Amm * Amm_inv - Eigen::MatrixXd::Identity(m, m)).sum()); // marg 的过程 marg掉 m Eigen::VectorXd bmm = b.segment(0, m); Eigen::MatrixXd Amr = A.block(0, m, m, n); Eigen::MatrixXd Arm = A.block(m, 0, n, m); Eigen::MatrixXd Arr = A.block(m, m, n, n); Eigen::VectorXd brr = b.segment(m, n); A = Arr - Arm * Amm_inv * Amr; b = brr - Arm * Amm_inv * bmm;

然后 将A,b 结果反推 先验jacobian 与 先验残差。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将marg后的结果 A,b 转换为 先验jacobian与先验残差 // A = PSPt Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> saes2(A); // 对S中特征值小于 eps 的直接归0 处理 Eigen::VectorXd S = Eigen::VectorXd((saes2.eigenvalues().array() > eps).select(saes2.eigenvalues().array(), 0)); Eigen::VectorXd S_inv = Eigen::VectorXd((saes2.eigenvalues().array() > eps).select(saes2.eigenvalues().array().inverse(), 0)); Eigen::VectorXd S_sqrt = S.cwiseSqrt(); Eigen::VectorXd S_inv_sqrt = S_inv.cwiseSqrt(); // A = PSPt = (sqrt(S)*Pt)^t * (sqrt(S)*Pt) => A = JtJ => J = (sqrt(S)*Pt) linearized_jacobians = S_sqrt.asDiagonal() * saes2.eigenvectors().transpose(); // b = Jte => e = (Jt)^-1*b = sqrt(S)^-1 * P^t * b linearized_residuals = S_inv_sqrt.asDiagonal() * saes2.eigenvectors().transpose() * b;

参考下面公式:
在这里插入图片描述

ThreadsConstructA()

构造A矩阵线程的主函数, 各个残差在 preMarginalize()求解出来jacobian矩阵后,在这个函数中合成A矩阵,原理是 A += JtJ, b += Jt * r
这个函数 只对 分配到同一个线程的残差进行处理。
合并的顺序是:

在这里插入图片描述
补充
注意这里构建H矩阵的加速策略,将所有用来构建H矩阵的残差分成4组,每一组分配一个线程进行计算。

滑动窗口

void Estimator::slideWindow() (extimator.cpp中)
调用机制:
1、processImage()函数中,首先通过 addFeatureCheckParallax()判断marg的策略 ,
MARGIN_OLD:marg掉最老的一帧。
MARGIN_SECOND_NEW: marg掉次新帧。
2、初始化时,若初始化失败或者外参标定失败,需要调用 slideWindow() 将滑动窗口的一帧去除。
3、初始化成功后,以及之后进入NON_LINEAR模式,每一帧求解完solveOdometry()后都需要slideWindow()。
可以看到 slideWindow()是非常重要的!!
运行流程如下:
在这里插入图片描述
其中:
1、如果是 MARGIN_OLD 即marg掉最老的一帧

void FeatureManager::removeBackShiftDepth(Eigen::Matrix3d marg_R, Eigen::Vector3d marg_P, Eigen::Matrix3d new_R, Eigen::Vector3d new_P) ( feature_manager.cpp )
作用: 对feature容器进行更新。
因为最老帧被marg掉了:
遍历feature中所有特征点
1、如果该特征点的起始帧不是最老帧,则该特征点的起始帧start_frame要减一(前移)。
2、如果该特征点的起始帧为最老帧,则删除该特征点 feature_per_frame 最老帧的信息。
3、删除信息之后如果该特征点在滑动窗口中出现的个数小于2了,则feature直接删除该特征点,否则,更新该特征点的estimated_depth,之前estimated_depth等于特征点在start_frame也就是最老帧坐标系下的深度,现在要转换到新的start_frame下的深度。

在这里插入图片描述
2、如果是 MARGIN_SECOND_NEW 即marg掉次新帧
因为要将次新帧marg掉,那么当前最新帧就作为下一帧的次新帧,因此就需要将当前最新帧与次新帧的数据合并,即将当前帧的预积分传递到次新帧预积分中,当前帧的imu数据合并到次新帧.

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
for (unsigned int i = 0; i < dt_buf[frame_count].size(); i++) { // 获得时间 IMU测量值 记录了 从 frame_count-1 - frame_count帧之间的IMU数据 double tmp_dt = dt_buf[frame_count][i]; Vector3d tmp_linear_acceleration = linear_acceleration_buf[frame_count][i]; Vector3d tmp_angular_velocity = angular_velocity_buf[frame_count][i]; // 继续计算预积分 添加到 frame_count - 2 - frame_count - 1之间的预积分中 pre_integrations[frame_count - 1]->push_back(tmp_dt, tmp_linear_acceleration, tmp_angular_velocity); dt_buf[frame_count - 1].push_back(tmp_dt); linear_acceleration_buf[frame_count - 1].push_back(tmp_linear_acceleration); angular_velocity_buf[frame_count - 1].push_back(tmp_angular_velocity); }

下面数据 次新帧数据用最新帧的代替;

复制代码
1
2
3
4
5
6
7
8
// 次新帧的数据用最新帧代替 Headers[frame_count - 1] = Headers[frame_count]; Ps[frame_count - 1] = Ps[frame_count]; Vs[frame_count - 1] = Vs[frame_count]; Rs[frame_count - 1] = Rs[frame_count]; Bas[frame_count - 1] = Bas[frame_count]; Bgs[frame_count - 1] = Bgs[frame_count];

然后比较重要的是,用最新imu测量值构建新的预积分;

复制代码
1
2
3
4
// 这里重新构造最新帧的预积分!!!!!! delete pre_integrations[WINDOW_SIZE]; pre_integrations[WINDOW_SIZE] = new IntegrationBase{acc_0, gyr_0, Bas[WINDOW_SIZE], Bgs[WINDOW_SIZE]};

最后

以上就是雪白唇彩最近收集整理的关于Vins-mono 源码笔记 (6) 紧耦合优化的全部内容,更多相关Vins-mono内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(74)

评论列表共有 0 条评论

立即
投稿
返回
顶部