“ 在优化完成后,滑动窗口前,必须进行边缘化项的操作”

一、理论部分

VINS 细节系列 - 窗口优化_努力努力努力-CSDN博客

一个边缘化的例子

下面我们用一个具体例子来形象说明边缘化过程及其导致的矩阵稠密现象(fill-in)。设有 四个相机位姿 xpi ,

以及  6 个路标点xmk (路标点用 xyz 的参数化), 相机与路标点的边表示一次观测, 相邻相机之间的边表示 IMU 约束,

相互关系如下:

下面试图将 xp1  给 marg 掉,然后再将 xm1 给 marg 掉, 看看 H 矩阵会如何变化

图(12-a) 表示原始的 H 矩阵,注意这里的 左上角为路标点相关部分, 而右下角是 pose 相关部分

图(12-b)是把 H 矩阵中跟x p1 相关的部分移动到 H 矩阵左上角

其中各矩阵的维度已在上式标明,也就是说图(12-c)的矩阵大小为 32×32。我们可以看到,图(c)相对于图(b)变得更稠密了,

即 marg 掉一个 pose,会使得剩余的 H 矩阵有 3 个地方被 fill-in,如图(c)中颜色加重区域。

这时图关系则变为:

注意,观察图 16 可发现这时的 xm1 、xm2 和 xm3 彼此之间已经产生了新的约束关系,且xp2 也与xm1 产生了新的约束关系。

图(12-d)是 marg 掉xm1 后的 H 矩阵,详细如下所示:

对应的图关系如下:

我们发现, marg 掉xm1 后,并没有是 H 矩阵更稠密,这是因为xm1 之前并未与其他pose 有约束关系,即并未被观察到,

因此如果 marg 掉那些不被其他帧观测到的路标点,不会显著地使 H 矩阵变得稠密。而要 marg 掉的路标点中,对于那

些被其他帧观测到路标点,要么就别设置为 marg, 要么就宁愿丢弃,这就是 OKVIS 中用到的策略

简单介绍

 1、优化变量:

序号             变量                                  维度

   0          para_Pose                 (6维,相机位姿)
   1          para_SpeedBias    (9维,相机速度、加速度偏置、角速度偏置)、
   2          para_Ex_Pose         (6维、相机IMU外参)、
   3          para_Feature           (1维,特征点深度)、
   4          para_Td                      (1维,标定同步时间)
五部分组成,在后面进行边缘化操作时这些优化变量都是当做整体看待

last_marginalization_parameter_blocks :Xb变量,也是我们要优化的变量;
last_marginalization_info                                Xm与Xb对应的测量Zb,也是先验残差(细品一下)

2、思路:

边缘化策略

边缘化分两种情况,每种情况有各自的流程

a. 如果次新帧是关键帧,则将边缘化最老帧,及其看到的路标点和IMU数据,转化为先验。具体流程为:

1)将上一次先验残差项传递给marginalization_info

2)将第0帧和第1帧间的IMU因子IMUFactor(pre_integrations[1]),添加到marginalization_info中

3)将第一次观测为第0帧的所有路标点对应的视觉观测,添加到marginalization_info中

4)计算每个残差,对应的Jacobian,并将各参数块拷贝到统一的内存(parameter_block_data)中

5)多线程构造先验项舒尔补AX=b的结构,在X0处线性化计算Jacobian和残差

6)调整参数块在下一次窗口中对应的位置(往前移一格),注意这里是指针,后面slideWindow中会赋新值,这里只是提前占座

b. 如果次新帧不是关键帧,此时具体流程为:

1)保留次新帧的IMU测量,丢弃该帧的视觉测量,将上一次先验残差项传递给marginalization_info

2)premargin:计算每个残差,对应的Jacobian,并更新parameter_block_data

3)marginalize:构造先验项舒尔补AX=b的结构,计算Jacobian和残差

4)调整参数块在下一次窗口中对应的位置(去掉次新帧)

3、marginalization_factor.cpp 代码中有几个变量需要提前说明:

举例说明,当第一次 marg 掉最老帧时,parameter_block_size 为所有变量及其对应的
localSize,  parameter_block_data 为对应的数据(double*类型)

 

二、程序部分

2.1  把下面这个图搞明白!

2.2 看  "marginalization_factor.h"  中类和结构体的定义

 1、class ResidualBlockInfo

类描述:这个类保存了待marg变量(xm)与相关联变量(xb)之间的一个约束因子关系  -  Zm

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) {}void Evaluate();//调用cost_function的evaluate函数计算残差 和 雅克比矩阵ceres::CostFunction *cost_function;ceres::LossFunction *loss_function;std::vector<double *> parameter_blocks;std::vector<int> drop_set;double **raw_jacobians;std::vector<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> jacobians;Eigen::VectorXd residuals;int localSize(int size){return size == 7 ? 6 : size;}
};
成员变量 描述
cost_function 对应ceres中表示约束因子的类
parameter_blocks 该约束因子相关联的参数块变量
drop_set parameter_blocks中待marg变量的索引

2、class MarginalizationInfo

类描述:这个类保存了优化时上一步边缘化后保留下来的先验信息

class MarginalizationInfo
{public:~MarginalizationInfo();int localSize(int size) const;int globalSize(int size) const;void addResidualBlockInfo(ResidualBlockInfo *residual_block_info);void preMarginalize();void marginalize();std::vector<double *> getParameterBlocks(std::unordered_map<long, double *> &addr_shift);std::vector<ResidualBlockInfo *> factors;//这里将参数块分为Xm,Xb,Xr,Xm表示被marg掉的参数块,Xb表示与Xm相连接的参数块,Xr表示剩余的参数块//那么m=Xm的localsize之和,n为Xb的localsize之和,pos为(Xm+Xb)localsize之和int m, n;      std::unordered_map<long, int> parameter_block_size; //global size 将被marg掉的约束边相关联的参数块,即将被marg掉的参数块以及与它们直接相连的参数快int sum_block_size;std::unordered_map<long, int> parameter_block_idx; //local size  std::unordered_map<long, double *> parameter_block_data;std::vector<int> keep_block_size; //global sizestd::vector<int> keep_block_idx;  //local sizestd::vector<double *> keep_block_data;Eigen::MatrixXd linearized_jacobians;Eigen::VectorXd linearized_residuals;const double eps = 1e-8;};
成员变量 描述
factors xm与xb之间的约束因子,Zb
m 所有将被marg掉变量的localsize之和,如上图中xm的localsize
n 所有与将被marg掉变量有约束关系的变量的localsize之和,如上图中xb的localsize
parameter_block_size <参数块地址,参数块的global size>,参数块包括xm和xb
parameter_block_idx <参数块地址,参数块排序好后的索引>,对参数块进行排序,xm排在前面,xb排成后面,使用localsize
parameter_block_data <参数块地址,参数块数据>,需要注意的是这里保存的参数块数据是原始参数块数据的一个拷贝,不再变化,用于记录这些参数块变量在marg时的状态
keep_block_size <保留下来的参数块地址,参数块的globalsize>
keep_block_idx <保留下来的参数块地址,参数块的索引>,保留下来的参数块是xb

成员函数信息:

成员函数 描述
preMarginalize 得到每次IMU和视觉观测对应的参数块,雅克比矩阵,残差值
marginalize 开启多线程构建信息矩阵H和b ,同时从H,b中恢复出线性化雅克比和残差

3、class MarginalizationFactor

类描述:该类是优化时表示上一步边缘化后保留下来的先验信息代价因子,变量marginalization_info保存了类似约束测量信息

class MarginalizationFactor : public ceres::CostFunction
{public:MarginalizationFactor(MarginalizationInfo* _marginalization_info);virtual bool Evaluate(double const *const *parameters, double *residuals, double **jacobians) const;MarginalizationInfo* marginalization_info;
};

2.3 程序讲解

边缘化部分在  optimization 函数里面,当执行完后端优化后,紧接着就会执行边缘化,两个过程都需要添加优化变量和残差,

但区别是边缘化的变量少一点!主要是构建模块的过程!

 

一、如果边缘化 “老帧”

【1】首先 把上一次先验项中的残差项(尺寸为 n)传递给当前先验项,并从中去除需要丢弃 的状态量;

即从旧的先验残差项 last_marginalization_info 新建一个新的 marginalization_factor,参与这个残差项的优化变量是:last_marginalization_parameter_blocks 里面的内容:para_Pose、para_SpeedBias、para_Ex_Pose、para_Feature、para_Td 都是按顺序排好的,所以要把 要丢弃 的状态量  para_Pose[0]、para_SpeedBias[0] 对应的序号用 drop_set 记录下来

// --------------------------------------marginalization-----------------------------------------//添加残差以及优化变量的方式和后端线性优化中添加的方式相似,因为边缘化类应该就是仿照ceres写的TicToc t_whole_marginalization;//一、边缘化老帧,那就要考虑老帧的情况!
//要边缘化的参数块是 para_Pose[0] para_SpeedBias[0] 以及 para_Feature[feature_index](滑窗内的第feature_index个点的逆深度)if (marginalization_flag == MARGIN_OLD){MarginalizationInfo *marginalization_info = new MarginalizationInfo();//先验信息vector2double();  //【1】首先添加上一次先验残差项if (last_marginalization_info && last_marginalization_info->valid){vector<int> drop_set;for (int i = 0; i < static_cast<int>(last_marginalization_parameter_blocks.size()); i++){//查询last_marginalization_parameter_blocks中是首帧状态量的序号if (last_marginalization_parameter_blocks[i] == para_Pose[0] ||last_marginalization_parameter_blocks[i] == para_SpeedBias[0])drop_set.push_back(i);}MarginalizationFactor *marginalization_factor = new MarginalizationFactor(last_marginalization_info);ResidualBlockInfo *residual_block_info = new ResidualBlockInfo(marginalization_factor, NULL,last_marginalization_parameter_blocks,drop_set); //调用addResidualBlockInfo()函数将各个残差以及残差涉及的优化变量添加入优化变量中marginalization_info->addResidualBlockInfo(residual_block_info);}

【2】添加IMU的marg信息到 marginalization_info 中:第0帧和第1帧之间的IMU预积分值以及第0帧和第1帧相关优化变量

if(USE_IMU){if (pre_integrations[1]->sum_dt < 10.0){IMUFactor* imu_factor = new IMUFactor(pre_integrations[1]);//0和1是para_Pose[0], para_SpeedBias[0],需要marg的变量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});marginalization_info->addResidualBlockInfo(residual_block_info);}}

【3】最后添加第一次观测滑窗中第0帧的路标点以及其他相关的滑窗中的帧的相关的优化变量 到 marginalization_info 中

 {int feature_index = -1;//这里是遍历滑窗所有的特征点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 < 4)continue;++feature_index;int imu_i = it_per_id.start_frame, imu_j = imu_i - 1;//特征点的起始帧必须是首帧,因此后面用来构建marg矩阵的都是和第一帧有共视关系的滑窗帧,因为 marginalization_flag == MARGIN_OLDif (imu_i != 0)continue;//得到该Feature在起始帧下的归一化坐标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)//不需要起始观测帧{Vector3d pts_j = it_per_frame.point;ProjectionTwoFrameOneCamFactor *f_td = new ProjectionTwoFrameOneCamFactor(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);ResidualBlockInfo *residual_block_info = new ResidualBlockInfo(f_td, loss_function, vector<double *>{para_Pose[imu_i], para_Pose[imu_j], para_Ex_Pose[0], para_Feature[feature_index], para_Td[0]},vector<int>{0, 3});marginalization_info->addResidualBlockInfo(residual_block_info);}if(STEREO && it_per_frame.is_stereo){Vector3d pts_j_right = it_per_frame.pointRight;if(imu_i != imu_j){ProjectionTwoFrameTwoCamFactor *f = new ProjectionTwoFrameTwoCamFactor(pts_i, pts_j_right, it_per_id.feature_per_frame[0].velocity, it_per_frame.velocityRight,it_per_id.feature_per_frame[0].cur_td, it_per_frame.cur_td);ResidualBlockInfo *residual_block_info = new ResidualBlockInfo(f, loss_function,vector<double *>{para_Pose[imu_i], para_Pose[imu_j], para_Ex_Pose[0], para_Ex_Pose[1], para_Feature[feature_index], para_Td[0]},vector<int>{0, 4});marginalization_info->addResidualBlockInfo(residual_block_info);}else{ProjectionOneFrameTwoCamFactor *f = new ProjectionOneFrameTwoCamFactor(pts_i, pts_j_right, it_per_id.feature_per_frame[0].velocity, it_per_frame.velocityRight,it_per_id.feature_per_frame[0].cur_td, it_per_frame.cur_td);ResidualBlockInfo *residual_block_info = new ResidualBlockInfo(f, loss_function,vector<double *>{para_Ex_Pose[0], para_Ex_Pose[1], para_Feature[feature_index], para_Td[0]},vector<int>{2});marginalization_info->addResidualBlockInfo(residual_block_info);}}}}}

【4】marginalization_info->preMarginalize():得到每次 IMU 和视觉观测(cost_function)对 应的参数块(parameter_blocks),雅可比矩阵(jacobians),残差值(residuals);

 marginalization_info->preMarginalize();

【4】marginalization_info->marginalize():多线程计整个先验项的参数块,雅可比矩阵和 残差值,其中与代码对应关系为:

marginalization_info->marginalize();

【5】最后移交了优化项需要得到的两个变量:last_marginalization_info和last_marginalization_parameter_blocks

std::unordered_map<long, double *> addr_shift;for (int i = 1; i <= WINDOW_SIZE; i++){addr_shift[reinterpret_cast<long>(para_Pose[i])] = para_Pose[i - 1];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];}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;

重要的三个结构变量:

  • MarginalizationFactor *marginalization_factor
  • *MarginalizationInfo marginalization_info
  • ResidualBlockInfo *residual_block_info

MarginalizationFactor *marginalization_factor 两个作用:(1)构建ceres的残差项,即计算residual (2)构建ResidualBlockInfo *residual_block_info

MarginalizationInfo *marginalization_info 由他可以获取边缘化信息,用它来构建MarginalizationFactor *marginalization_factor以及得到对应的参数块

ResidualBlockInfo *residual_block_info 用它来丰富marginalization_info项的信息

从头到位都是marginalization_info这个变量来进行统筹安排进行边缘化。

通过marginalization_info->addResidualBlockInfo()来添加约束,有三个方面的来源:(1)旧的(2)imu预积分项(3)特征点

理解:

上面三步通过调用 addResidualBlockInfo() 已经确定优化变量的数量、存储位置、长度以及待优化变量的数量以及存储位置;

为什么要添加这些量呀?因为在k时刻这些量是我们要边缘化掉的变量,残差;但是k+1时刻就是我们的先验,这不正是我们初衷嘛!

需要注意的是这些都是和第一帧图像有关的!因为我们边缘化的是最老帧!三部分的流程都是一样的,如下:

第一步 定义损失函数,对于先验残差就是MarginalizationFactor,对于IMU就是IMUFactor,对于视觉就是ProjectionTdFactor,

这三个损失函数的类都是继承自ceres的损失函数类ceres::CostFunction,里面都重载了函数; 函数通过传入的优化变量值 parameters,

以及先验值(对于先验残差就是上一时刻的先验残差last_marginalization_info,对于IMU就是预计分值pre_integrations[1],对于视觉

就是空间的的像素坐标pts_i, pts_j)可以计算出各项残差值residuals,以及残差对应个优化变量的雅克比矩阵jacobians;

bool MarginalizationFactor::Evaluate(double const *const *parameters, double *residuals, double **jacobians) const
{int n = marginalization_info->n;int m = marginalization_info->m;Eigen::VectorXd dx(n);//遍历本次边缘化保留下的参数块:keep_block_sizefor (int i = 0; i < static_cast<int>(marginalization_info->keep_block_size.size()); i++){//得到该参数块的大小和序号int size = marginalization_info->keep_block_size[i];int idx = marginalization_info->keep_block_idx[i] - m;//x 感觉是边缘化之前 BA优化后的值 ?Eigen::VectorXd x = Eigen::Map<const Eigen::VectorXd>(parameters[i], size);//x0表示marg时参数块变量的值(即xb)Eigen::VectorXd x0 = Eigen::Map<const Eigen::VectorXd>(marginalization_info->keep_block_data[i], size);if (size != 7)//speed_bias 9项dx.segment(idx, size) = x - x0;//变量的更新else//位置,姿态项, 姿态不能直接加减了!要变成四元素的 ✖{//使用四元数的表达方式dx.segment<3>(idx + 0) = x.head<3>() - x0.head<3>();dx.segment<3>(idx + 3) = 2.0 * Utility::positify(Eigen::Quaterniond(x0(6), x0(3), x0(4), x0(5)).inverse() * Eigen::Quaterniond(x(6), x(3), x(4), x(5))).vec();if (!((Eigen::Quaterniond(x0(6), x0(3), x0(4), x0(5)).inverse() * Eigen::Quaterniond(x(6), x(3), x(4), x(5))).w() >= 0)){dx.segment<3>(idx + 3) = 2.0 * -Utility::positify(Eigen::Quaterniond(x0(6), x0(3), x0(4), x0(5)).inverse() * Eigen::Quaterniond(x(6), x(3), x(4), x(5))).vec();}}}//计算残差,理解,泰勒展开求的公式Eigen::Map<Eigen::VectorXd>(residuals, n) = marginalization_info->linearized_residuals + marginalization_info->linearized_jacobians * dx;//仅在边缘化残差这一块,雅克比矩阵要固定?if (jacobians){for (int i = 0; i < static_cast<int>(marginalization_info->keep_block_size.size()); i++){if (jacobians[i]){int size = marginalization_info->keep_block_size[i], local_size = marginalization_info->localSize(size);int idx = marginalization_info->keep_block_idx[i] - m;//Eigen::Map<Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>> jacobian(jacobians[i], n, size);jacobian.setZero();jacobian.leftCols(local_size) = marginalization_info->linearized_jacobians.middleCols(idx, local_size);}}}return true;
}

上面这段代码是在优化过程中,对先验约束项的更新,主要是对先验残差的更新,雅克比不再发生变化。
代码中的x0 表示marg xm状态时 xb变量的值; 优化过程中先验项的雅克比保持不变,但是残差需要变化,其变化公式推导如下:

第二步定义ResidualBlockInfo,其构造函数如下:

ResidualBlockInfo(ceres::CostFunction *_cost_function, ceres::LossFunction *_loss_function, std::vector<double *> _parameter_blocks, std::vector<int> _drop_set)
----------------------------------------------------------------------------------------------------
void ResidualBlockInfo::Evaluate()
{//获取残差的个数: IMU + 视觉residuals.resize(cost_function->num_residuals());//优化变量参数块的变量大小:para_Pose、para_SpeedBias、para_Ex_Pose、para_Feature、para_Tdstd::vector<int> block_sizes = cost_function->parameter_block_sizes();//数组外围的大小,也就是参数块的个数raw_jacobians = new double *[block_sizes.size()];jacobians.resize(block_sizes.size());//分配每一行的大小,残差的维数*每个参数块中参数的个数block_sizes[i],J矩阵大小的确认!想一下//比如:两个残差f1,f2;5个变量x1,x2,,,x5, 则J矩阵是2行5列呀for (int i = 0; i < static_cast<int>(block_sizes.size()); i++){jacobians[i].resize(cost_function->num_residuals(), block_sizes[i]);raw_jacobians[i] = jacobians[i].data();}//利用各自残差的Evaluate函数计算残差和雅克比矩阵。cost_function->Evaluate(parameter_blocks.data(), residuals.data(), raw_jacobians);//好像明白了,这个是视觉里面有的Huber核函数;有鲁棒核函数的残差部分,重写雅克比与残差if (loss_function){double residual_scaling_, alpha_sq_norm_;double sq_norm, rho[3];sq_norm = residuals.squaredNorm();loss_function->Evaluate(sq_norm, rho);double sqrt_rho1_ = sqrt(rho[1]);if ((sq_norm == 0.0) || (rho[2] <= 0.0)){residual_scaling_ = sqrt_rho1_;alpha_sq_norm_ = 0.0;}else//这一部分公式没有看懂!{const double D = 1.0 + 2.0 * sq_norm * rho[2] / rho[1];const double alpha = 1.0 - sqrt(D);residual_scaling_ = sqrt_rho1_ / (1 - alpha);  alpha_sq_norm_ = alpha / sq_norm;  }//感觉像根据先验残差,推出新的残差和雅可比公式一样!for (int i = 0; i < static_cast<int>(parameter_blocks.size()); i++){jacobians[i] = sqrt_rho1_ * (jacobians[i] - alpha_sq_norm_ * residuals * (residuals.transpose() * jacobians[i]));}residuals *= residual_scaling_;}
}

这一步是为了将不同的损失函数_cost_function以及优化变量_parameter_blocks统一起来再一起添加到marginalization_info中。

变量_loss_function是核函数,在VINS-mono的边缘化中仅仅视觉残差有用到核函数,另外会设置需要被边缘话的优化变量的位置

_drop_set,这里对于不同损失函数又会有不同:对于先验损失,其待边缘化优化变量是根据是否等于para_Pose[0]或者

para_SpeedBias[0],也就是说和第一帧相关的优化变量都作为边缘化的对象。对于IMU,其输入的_drop_set是 vector{0, 1},

也就是说其待边缘化变量是para_Pose[0], para_SpeedBias[0],也是第一帧相关的变量都作为边缘化的对象,

里值得注意的是和后端优化不同,这里只添加了第一帧和第二帧的相关变量作为优化变量,因此边缘化构造的信息矩阵会比

后端优化构造的信息矩阵要小对于视觉,其输入的_drop_set是vector{0, 3},也就是说其待边缘化变量是para_Pose[imu_i]和

para_Feature[feature_index],从这里可以看出来在VINS-mono的边缘化操作中会不仅仅会边缘化第一帧相关的优化变量,

还会边缘化掉以第一帧为起始观察帧的路标点。

问1:为什么输入的_drop_set是 vector{0, 1},也就是说其待边缘化变量是para_Pose[0], para_SpeedBias[0];

输入的_drop_set是vector{0, 3},也就是说其待边缘化变量是para_Pose[imu_i]和para_Feature[feature_index];

答1:如前面所示,优化变量是按照固定的顺序排列的

序号                 变量                                

   0             para_Pose                 
   1             para_SpeedBias    
   2             para_Ex_Pose       
   3             para_Feature           
   4             para_Td

问2:最后一部分为什么要重写残差和雅可比公式?公式怎么来的?

答2:看过论文的童鞋应该知道,在优化部分,视觉是加了核函数的,这样带来的问题是其对应的残差和雅可比是变的,这个应该很好理解:

比如: f(x) 的雅可比是J, 那么 ρ(f(x))的雅可比肯定变了! ρ 是核函数! 公式我没有推出来,还请知道的兄弟告诉一下!

经过努力,得一丝头绪! 其中核函数如下:

第三步是将定义的residual_block_info添加到marginalization_info中,通过下面这一句

marginalization_info->addResidualBlockInfo(residual_block_info);

addResidualBlockInfo() 这个函数的实现如下:

void MarginalizationInfo::addResidualBlockInfo(ResidualBlockInfo *residual_block_info)
{factors.emplace_back(residual_block_info);//总残差快std::vector<double *> &parameter_blocks = residual_block_info->parameter_blocks;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;//将指针强转为数据的地址}//这里应该是待边缘化的变量for (int i = 0; i < static_cast<int>(residual_block_info->drop_set.size()); i++){//这个是待边缘化的变量的iddouble *addr = parameter_blocks[residual_block_info->drop_set[i]];//将需要marg的变量的id存入parameter_block_idxparameter_block_idx[reinterpret_cast<long>(addr)] = 0;}
}

目的:

将不同损失函数对应的优化变量、边缘化位置存入到 parameter_block_sizes 和 parameter_block_idx中,这里注意的是执行到这一步,

parameter_block_idx 中仅仅有待边缘化的优化变量的内存地址的key,而且其对应value全部为0;

【4】调用 preMarginalize() 进行预处理

上面通过调用 addResidualBlockInfo() 已经确定优化变量的数量、存储位置、长度以及待优化变量的数量以及存储位置,

下面就需要调用 preMarginalize() 进行预处理, 得到每次 IMU 和视觉观测 (cost_function) 对 应的参数块(parameter_blocks),

雅可比矩阵(jacobians),残差值(residuals);

void MarginalizationInfo::preMarginalize()
{//遍历所有factor,在前面的addResidualBlockInfo中会将不同的残差块加入到factor中。for (auto it : factors){it->Evaluate();//利用多态性分别计算所有状态变量构成的残差和雅克比矩阵//遍历所有参数块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];//将factor中的参数块复制到parameter_block_data中,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); //重新开辟一块内存parameter_block_data[addr] = data;//通过之前优化变量的数据的地址和新开辟的内存数据进行关联}}}
}

it->Evaluate()  这一句里面其实就是调用各个损失函数中的重载函数Evaluate(),这个函数前面有提到过:

【5】调用marginalize()

开启多线程构建信息矩阵H和b ,同时从H,b中恢复出线性化雅克比和残差

前面已经将数据都准备好了,下面通过调用marginalize()函数就要正式开始进行边缘化操作了,实现如下:

void MarginalizationInfo::marginalize()
{int pos = 0;for (auto &it : parameter_block_idx)//遍历待marg的优化变量的内存地址{it.second = pos;pos += localSize(parameter_block_size[it.first]);}m = pos;//需要marg掉的变量个数for (const auto &it : parameter_block_size)//计算除了边缘化之外要保留的参数块{//如果这个变量不是是待marg的优化变量。if (parameter_block_idx.find(it.first) == parameter_block_idx.end()){parameter_block_idx[it.first] = pos;//就将这个待marg的变量id设为pospos += localSize(it.second);        //pos加上这个变量的长度}}//上面的操作就会将所有的优化变量进行一个伪排序 n = pos - m;//要保留下来的变量个数if(m == 0){valid = false;printf("unstable tracking...\n");return;}TicToc t_summing;Eigen::MatrixXd A(pos, pos);//整个矩阵大小 --- 没有边缘化之前的矩阵Eigen::VectorXd b(pos);A.setZero();b.setZero();TicToc t_thread_summing;pthread_t tids[NUM_THREADS];ThreadsStruct threadsstruct[NUM_THREADS];//携带每个线程的输入输出信息//为每个线程均匀分配factor。int i = 0;for (auto it : factors){threadsstruct[i].sub_factors.push_back(it);i++;i = i % NUM_THREADS;}//构造4个线程,并确定线程的主程序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;//分别构造矩阵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;}/*代码中求Amm的逆矩阵时,为了保证数值稳定性,做了Amm=1/2*(Amm+Amm^T)的运算,Amm本身是一个对称矩阵,所以  等式成立。接着对Amm进行了特征值分解,再求逆,更加的快速*/Eigen::MatrixXd Amm = 0.5 * (A.block(0, 0, m, m) + A.block(0, 0, m, m).transpose());Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> saes(Amm);Eigen::MatrixXd Amm_inv = saes.eigenvectors() * Eigen::VectorXd((saes.eigenvalues().array() > eps).select(saes.eigenvalues().array().inverse(), 0)).asDiagonal() * saes.eigenvectors().transpose();// 设x_{m}为要被marg掉的状态量,x_{r}是与x_{m}相关的状态量,所以在最后我们要保存的是x_{r}的信息////      |      |    |          |   |//      |  Amm | Amr|  m       |bmm|        |x_{m}|//  A = |______|____|      b = |__ |       A|x_{r}| = b//      |  Arm | Arr|  n       |brr|//      |      |    |          |   |//  使用舒尔补://  C = Arr - Arm*Amm^{-1}Amr//  d = brr - Arm*Amm^{-1}bmm//舒尔补,上面这段代码边缘化掉xm变量,保留xb变量 Eigen::VectorXd bmm = b.segment(0, m);Eigen::MatrixXd Amr = A.block(0, m, m, n);//0,m是开始的位置,m,m是开始位置后的大小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是marg过的A和b,大小是发生了变化的//下面就是更新先验残差项Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> saes2(A);//求更新后 A特征值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和b中恢复出来雅克比矩阵和残差向量;//两者会作为先验残差带入到下一轮的先验残差的雅克比和残差的计算当中去linearized_jacobians = S_sqrt.asDiagonal() * saes2.eigenvectors().transpose();linearized_residuals = S_inv_sqrt.asDiagonal() * saes2.eigenvectors().transpose() * b;
}

部分程序:

    int pos = 0;    //pos表示所有的被marg掉的参数块以及它们的相连接参数块的localsize之和for (auto &it : parameter_block_idx){it.second = pos;pos += localSize(parameter_block_size[it.first]);}m = pos;       for (const auto &it : parameter_block_size){if (parameter_block_idx.find(it.first) == parameter_block_idx.end()){//将被marg掉参数的相连接参数块添加道parameter_block_idx中parameter_block_idx[it.first] = pos;pos += localSize(it.second);}}n = pos - m;

这里对 参数块变量进行排序,待marg的参数块变量放在前面,其他参数块变量放在后面,

并将每个参数块的对应的下标放在parameter_block_idx中; pos = m + n

    TicToc t_thread_summing;pthread_t tids[NUM_THREADS];//携带每个线程的输入输出信息ThreadsStruct threadsstruct[NUM_THREADS];int i = 0;//将先验约束因子平均分配到线程中for (auto it : factors){threadsstruct[i].sub_factors.push_back(it);i++;i = i % NUM_THREADS;}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;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;}

这段代码开启多线程来构建信息矩阵A和残差b;将所有的先验约束因子平均分配到NUM_THREADS个线程中,每个线程分别构建一个A和b;

函数会通过多线程快速构造各个残差对应的各个优化变量的信息矩阵(雅克比和残差前面都已经求出来了),如下图所示:

这里构造信息矩阵时采用的正是 parameter_block_idx 作为构造顺序,自然而然地将待边缘化的变量构造在矩阵的左上方;

子函数   void* ThreadsConstructA(void* threadsstruct)             

void* ThreadsConstructA(void* threadsstruct)
{ThreadsStruct* p = ((ThreadsStruct*)threadsstruct);//遍历该线程分配的所有factors,所有观测项for (auto it : p->sub_factors){//遍历该factor中的所有参数块,五个参数块,分别计算,理解!for (int i = 0; i < static_cast<int>(it->parameter_blocks.size()); i++){//得到参数块的大小int idx_i = p->parameter_block_idx[reinterpret_cast<long>(it->parameter_blocks[i])];int size_i = p->parameter_block_size[reinterpret_cast<long>(it->parameter_blocks[i])];if (size_i == 7)//对于pose来说,是7维的,最后一维为0,这里取左边6size_i = 6;//只提取local size部分,对于pose来说,是7维的,最后一维为0,这里取左边6维//P.leftCols(cols) = P(:, 1:cols),取出从1列开始的cols列Eigen::MatrixXd jacobian_i = it->jacobians[i].leftCols(size_i);for (int j = i; j < static_cast<int>(it->parameter_blocks.size()); j++){int idx_j = p->parameter_block_idx[reinterpret_cast<long>(it->parameter_blocks[j])];int size_j = p->parameter_block_size[reinterpret_cast<long>(it->parameter_blocks[j])];if (size_j == 7)size_j = 6;Eigen::MatrixXd jacobian_j = it->jacobians[j].leftCols(size_j);//对应对角区域,H*X=b, A代表H矩阵if (i == j)p->A.block(idx_i, idx_j, size_i, size_j) += jacobian_i.transpose() * jacobian_j;else{//对应非对角区域p->A.block(idx_i, idx_j, size_i, size_j) += jacobian_i.transpose() * jacobian_j;p->A.block(idx_j, idx_i, size_j, size_i) = p->A.block(idx_i, idx_j, size_i, size_j).transpose();}}//求取g,Hx=g,都是根据公式来写程序的!p->b.segment(idx_i, size_i) += jacobian_i.transpose() * it->residuals;}}return threadsstruct;
}

这部分代码是用来实现构建A和b的:

    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());Eigen::MatrixXd Amm_inv = saes.eigenvectors() * Eigen::VectorXd((saes.eigenvalues().array() > eps).select(saes.eigenvalues().array().inverse(), 0)).asDiagonal() * saes.eigenvectors().transpose();//printf("error1: %f\n", (Amm * Amm_inv - Eigen::MatrixXd::Identity(m, m)).sum());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;

上面这段代码边缘化掉xm变量,保留xb变量,利用的方法是舒尔补

注意:上面这个等式就是先验信息,程序中又变成了 AX = b 的形式,从A和b中恢复出雅克比矩阵和残差,作为下一时刻的先验信息;

代码中求Amm的逆矩阵时,为了保证数值稳定性,做了Amm=1/2*(Amm+Amm^T)的运算,Amm本身是一个对称矩阵,

所以等式成立。接着对Amm进行了特征值分解,再求逆,更加的快速。

    Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> saes2(A);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();linearized_jacobians = S_sqrt.asDiagonal() * saes2.eigenvectors().transpose();linearized_residuals = S_inv_sqrt.asDiagonal() * saes2.eigenvectors().transpose() * b;

上面这段代码是从A和b中恢复出雅克比矩阵和残差

【6】滑窗预移动

//这里仅仅将指针进行了一次移动,指针对应的数据还是旧数据,调用的 slideWindow() 才能实现真正的滑窗移动std::unordered_map<long, double *> addr_shift;for (int i = 1; i <= WINDOW_SIZE; i++)//从1开始,因为第一帧的状态不要了{//这一步的操作指的是第i的位置存放的的是i-1的内容,这就意味着窗口向前移动了一格addr_shift[reinterpret_cast<long>(para_Pose[i])] = para_Pose[i - 1];if(USE_IMU)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];addr_shift[reinterpret_cast<long>(para_Td[0])] = para_Td[0];//根据地址来得到保留的参数块vector<double *> parameter_blocks = marginalization_info->getParameterBlocks(addr_shift);if (last_marginalization_info)//Zbdelete last_marginalization_info;                     //删除掉上一次的marg相关的内容last_marginalization_info = marginalization_info;         //marg相关内容的递归last_marginalization_parameter_blocks = parameter_blocks; //优化变量的递归,这里面仅仅是指针}

这一部分应该很容易明白;当边缘化帧的 “新帧”就不说了,把上面的看懂了,估计那个也会了

/------------------------------------------------------------------程序-----------------------------------------------------------------------/

VINS-Mono 代码解析六、边缘化(3)相关推荐

  1. FAST-LIO2代码解析(六)

    0. 简介 上一节我们将while内部的IKD-Tree部分全部讲完,下面将是最后一部分,关于后端优化更新的部分. 1. 迭代更新 最后一块主要做的就是,拿当前帧与IKD-Tree建立的地图算出的残差 ...

  2. [从零手写VIO|第五节]——后端优化实践——单目BA求解代码解析

    长篇警告⚠⚠⚠ 目录 solver 全流程回顾 Solver三要素 Solver求解中的疑问 核心问题 代码解析 1. TestMonoBA.cpp 2. 后端部分: 2.1 顶点 2.2 边(残差) ...

  3. 视觉SLAM开源算法ORB-SLAM3 原理与代码解析

    来源:深蓝学院,文稿整理者:何常鑫,审核&修改:刘国庆 本文总结于上交感知与导航研究所科研助理--刘国庆关于[视觉SLAM开源算法ORB-SLAM3 原理与代码解析]的公开课. ORB-SLA ...

  4. Linux-0.00 代码解析(四)

    Linux 0.00 的编译.运行.源码下载: http://blog.csdn.net/longintchar/article/details/78757065 Linux 0.00 Makefil ...

  5. iOS 编写高质量Objective-C代码(六)

    级别: ★★☆☆☆ 标签:「iOS」「Block」「Objective-C」 作者: MrLiuQ 审校: QiShare团队 前言: 这几篇文章是小编在钻研<Effective Objecti ...

  6. Celery 源码解析六:Events 的实现

    序列文章: Celery 源码解析一:Worker 启动流程概述 Celery 源码解析二:Worker 的执行引擎 Celery 源码解析三: Task 对象的实现 Celery 源码解析四: 定时 ...

  7. 番外篇15:libevent简单理解(附libevent官方代码解析,和跨平台服务器、客户端链接代码)

    文章目录 一.事件event和事件管理器event_base介绍 二.libevent流程简介(注册->检测->分派) 三.libevent的好处 四.代码比较 4.1 原来reactor ...

  8. 【Vue】Vue中传值的几种方法,案例代码解析

    目录 一.反向传值(子组件传值给父组件) 二.$refs 三.$parent 四.$children 五.$attrs/$listeners -----多层传值 六.$root ----根组件 七.依 ...

  9. 基于单层决策树的adaBoost算法思想分析和源代码解析

    基于单层决策树的AdaBoost算法思想分析和源代码解析 前言: 上一篇SVM可是废了我好鼻子劲,这一篇咱们来点愉快的东西.我们一定听说过这句俗语:"三个臭皮匠,顶个诸葛亮!" 大 ...

  10. LVI-SAM imuPreintegration代码解析

    写在前面:这个部分imu预积分,具体是啥,大家可以自行网络搜索,我的理解就是在进行优化的时候为了方便计算,不至于从头开始计算,于是提出了预积分的概念,然后相关公式大家可以在网上自行查阅.还有预积分的话 ...

最新文章

  1. 算子find_shpe_model参数详解
  2. Callable和Future
  3. type lambda
  4. v-viewer图片打不开一直在刷新_WordPress 上传图片时 async-upload.php出现520 Bug的原因及解决方案...
  5. 从苹果换回安卓是什么体验?
  6. Spark自定义排序
  7. 为什么要关闭数据库连接,可以不关闭吗?
  8. 用二次探测法建立hash表
  9. pdca实施的流程图_PDCA实战案例详解:PDCA的 4个阶段 8个步骤及应用详解
  10. 【艾琪出品】《计算机应用基础》【试题汇总9】
  11. 2.模仿小米通讯录的快速索引demo
  12. shiro中anon配置不生效
  13. JS实现PDF文件下载
  14. 代号记忆之数字和英文总结
  15. 微信公众平台模拟登陆和发送消息详解
  16. DiskGenius清除分区空闲空间后硬盘满了
  17. Sklearn-GBDT(GradientBoostingDecisionTree)梯度提升树
  18. Linux内核转储---Kdump,Crash使用介绍
  19. Dave Oracle 学习 手册 第一版 下载 说明
  20. 征服账号服务器,最新中文征服服务端(带架设教程+客户端补丁+需要的工具)10.13日更新...

热门文章

  1. vscode 突然无法切换输入法(切换中文输入法)
  2. 最优控制理论 八、CasADi求解路径约束轨迹优化的多重打靶法
  3. IE浏览器弹框提示脚本发生错误
  4. SVN Eclipse插件Subclipse安装和配置
  5. xp的ie显示无服务器,WinXP系统IE无法打开站点怎么办?
  6. python mqtt publish_mqtt异步publish方法
  7. ROS源代码之Publish底层实现(一)
  8. python 3 5的值_杨桃Python基础教程第5章:Python数据类型(3)列表s[M:n]值,的,三,smn,取值...
  9. 什么是windows用户账户
  10. 直接管理和维护计算机系统的程序称为,全国2008年04月自学考试计算机原理试题及答案.doc...