Vins-mono 源码笔记 (6) 紧耦合优化

优化主流程

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

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)、相机的外参数节点。

// 相机的外参
    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)、时间戳

// 时间戳       
if (ESTIMATE_TD)
{
    problem.AddParameterBlock(para_Td[0], 1);
    //problem.SetParameterBlockConstant(para_Td[0]);
}

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

   // 如果有先验信息   添加边缘化残差,丢弃帧的约束 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的预积分约束

// 添加各个相机的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)、添加视觉重投影误差

// 添加视觉的重投影误差  
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、对上述构建的优化问题进行求解。

// 优化设置   
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、消除相机位姿在零空间的变化,同时将优化的结果保存在 该函数稍后单独解析 !!!

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 则可以理解为利用残差构造的一条边,它的定义为:

// 残差信息块  
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类的定义为:

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 类对象中,函数如下:

// 给图优化添加一个残差   
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)、首先处理老先验信息

// 如果之前存在先验信息矩阵  则在对当前图优化问题边缘化前,要将老先验信息矩阵添加 
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 构建残差

  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);
然后构建残差 -

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 等数组中的位置。

// 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] ???
还望知道的朋友告知!!

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。

// 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的部分就在左上角。

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矩阵-

TicToc t_summing;
Eigen::MatrixXd A(pos, pos);    // Pos即总状态维度   
Eigen::VectorXd b(pos);
A.setZero();
b.setZero();

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

//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。

// 遍历  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, 就是那几个熟悉的公式,不多说:

//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: %f\n", (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 与 先验残差。

// 将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数据合并到次新帧.

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);
}

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

// 次新帧的数据用最新帧代替    
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测量值构建新的预积分;

 // 这里重新构造最新帧的预积分!!!!!!
delete pre_integrations[WINDOW_SIZE];
pre_integrations[WINDOW_SIZE] = new IntegrationBase{acc_0, gyr_0, Bas[WINDOW_SIZE], Bgs[WINDOW_SIZE]};
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值