[从零手写VIO|第五节]——后端优化实践——单目BA求解代码解析
长篇警告⚠⚠⚠
目录
- solver 全流程回顾
- Solver三要素
- Solver求解中的疑问
- 核心问题
- 代码解析
- 1. TestMonoBA.cpp
- 2. 后端部分:
- 2.1 顶点
- 2.2 边(残差)
- 2.2.1 边的框架
- 2.2.2 首先计算残差
- ① 视觉重投影残差
- ② IMU预积分残差
- 2.2.3 计算残差的雅可比
- ① `edge_reprojection.cc`视觉残差的雅可比
- ② IMU残差的雅可比矩阵
- 2.3 problem类
- 2.3.1 向problem中添加顶点
- 2.3.2 向problem中添加边
- 2.3.3 solve函数
- 2.3.3.1 统计优化变量的维数,为构建 H 矩阵做准备
- 2.3.3.2 通过MakeHessian()构建H矩阵
- 【补充】鲁棒核函数的H矩阵以及b
- 2.3.3.3 LM算法的初始化
- 2.3.3.4 LM算法基本流程
- 2.3.3.5 求解线性方程
- 2.3.3.6 更新状态量`UpdateStates()`:
- 2.3.3.7 判断当前步是否可行以及 LM 的 lambda 怎么更新
- 2.3.3.8 回滚操作
- 3. 边缘化的测试函数
solver 全流程回顾
Solver三要素
Solver求解中的疑问
- 信息矩阵 H 不满秩,那求解时如何操作?
• 使用 LM 算法,加阻尼因子使得系统满秩,可求解,但是求得的结果可能会往零空间变化。
• 添加先验约束,增加系统的可观性。比如 g2o tutorial 中对第一个pose 的信息矩阵加上单位阵 H[11]+ = I. - orbslam,svo 等求 mono BA 问题时,fix 一个相机 pose 和一个特征点,或者 fix 两个相机 pose,也是为了限定优化值不乱飘。那代码如何实现 fix 呢?
• 添加超强先验,使得对应的信息矩阵巨大(如,1015),就能使得∆x = 0;
• 设定对应雅克比矩阵为 0,意味着残差等于 0. 求解方程为(0 + λI) ∆x = 0,只能 ∆x = 0。
核心问题
矩阵块的对应关系,如何拼接信息矩阵。
代码解析
整个代码的结构如下:
app放置应用问题的函数,例如这里是slam的BA问题,backend里涵盖后端的相应库函数,先看BA问题(TestMonoBA.cpp):
1. TestMonoBA.cpp
产生世界坐标系下的虚拟数据: 相机姿态, 特征点, 以及每帧观测
/*
* Frame : 保存每帧的姿态和观测
* */
struct Frame { Frame(Eigen::Matrix3d R, Eigen::Vector3d t) : Rwc(R), qwc(R), twc(t) {}; Eigen::Matrix3d Rwc; //旋转矩阵 Eigen::Quaterniond qwc; //四元数表示的旋转和平移 Eigen::Vector3d twc; //平移向量unordered_map<int, Eigen::Vector3d> featurePerId; // 该帧观测到的特征以及特征id};
/* * 产生世界坐标系下的虚拟数据: 相机姿态, 特征点, 以及每帧观测 */
void GetSimDataInWordFrame(vector<Frame> &cameraPoses, vector<Eigen::Vector3d> &points) { int featureNums = 20; // 特征数目,假设每帧都能观测到所有的特征 int poseNums = 3; // 相机数目double radius = 8; for (int n = 0; n < poseNums; ++n) { double theta = n * 2 * M_PI / (poseNums * 4); // 1/4 圆弧 // 绕 z轴 旋转 Eigen::Matrix3d R; R = Eigen::AngleAxisd(theta, Eigen::Vector3d::UnitZ()); Eigen::Vector3d t = Eigen::Vector3d(radius * cos(theta) - radius, radius * sin(theta), 1 * sin(2 * theta)); cameraPoses.push_back(Frame(R, t)); }// 随机数生成三维特征点 std::default_random_engine generator; //创建随机数引擎 std::normal_distribution<double> noise_pdf(0., 1. / 1000.); // 2pixel / focal for (int j = 0; j < featureNums; ++j) { std::uniform_real_distribution<double> xy_rand(-4, 4.0); // 产生均匀分布的随机数,创建取值范围 std::uniform_real_distribution<double> z_rand(4., 8.);Eigen::Vector3d Pw(xy_rand(generator), xy_rand(generator), z_rand(generator)); points.push_back(Pw); //产生世界坐标系中的特征点// 在每一帧上的观测量 for (int i = 0; i < poseNums; ++i) { Eigen::Vector3d Pc = cameraPoses[i].Rwc.transpose() * (Pw - cameraPoses[i].twc); //得到相机坐标系中的特征点 Pc = Pc / Pc.z(); // 归一化图像平面 Pc[0] += noise_pdf(generator); // 加噪声 Pc[1] += noise_pdf(generator); cameraPoses[i].featurePerId.insert(make_pair(j, Pc)); // 三维特征点-帧特征点 } }}
【注】相关函数的解析
uniform_real_distribution:浮点数均匀分布模板类,产生均匀分布的随机数,,可以用float和double来实例化。参数说明了随机数的范围:半开范围[ )
main主函数:
int main() { // 准备数据 vector<Frame> cameras; vector<Eigen::Vector3d> points; GetSimDataInWordFrame(cameras, points); Eigen::Quaterniond qic(1, 0, 0, 0); Eigen::Vector3d tic(0, 0, 0);// 构建 problem(最小二乘) Problem problem(Problem::ProblemType::SLAM_PROBLEM);// 所有 Pose vector<shared_ptr<VertexPose> > vertexCams_vec; for (size_t i = 0; i < cameras.size(); ++i) { shared_ptr<VertexPose> vertexCam(new VertexPose()); Eigen::VectorXd pose(7); // 三维的平移+四维的四元数--6自由度 pose << cameras[i].twc, cameras[i].qwc.x(), cameras[i].qwc.y(), cameras[i].qwc.z(), cameras[i].qwc.w(); vertexCam->SetParameters(pose);
// if(i < 2)
// vertexCam->SetFixed();problem.AddVertex(vertexCam); vertexCams_vec.push_back(vertexCam); }// 所有 Point 及 edge(残差) std::default_random_engine generator; std::normal_distribution<double> noise_pdf(0, 1.); double noise = 0; vector<double> noise_invd; vector<shared_ptr<VertexInverseDepth> > allPoints; for (size_t i = 0; i < points.size(); ++i) { //假设所有特征点的起始帧为第0帧, 逆深度容易得到 Eigen::Vector3d Pw = points[i]; Eigen::Vector3d Pc = cameras[0].Rwc.transpose() * (Pw - cameras[0].twc); noise = noise_pdf(generator); double inverse_depth = 1. / (Pc.z() + noise);
// double inverse_depth = 1. / Pc.z(); noise_invd.push_back(inverse_depth);// 初始化特征 vertex shared_ptr<VertexInverseDepth> verterxPoint(new VertexInverseDepth()); VecX inv_d(1); inv_d << inverse_depth; verterxPoint->SetParameters(inv_d); problem.AddVertex(verterxPoint); allPoints.push_back(verterxPoint);// 每个特征点j对应的投影误差edge, 第 0 帧为起始帧 for (size_t j = 1; j < cameras.size(); ++j) { Eigen::Vector3d pt_i = cameras[0].featurePerId.find(i)->second; Eigen::Vector3d pt_j = cameras[j].featurePerId.find(i)->second; shared_ptr<EdgeReprojection> edge(new EdgeReprojection(pt_i, pt_j)); edge->SetTranslationImuFromCamera(qic, tic);std::vector<std::shared_ptr<Vertex> > edge_vertex; edge_vertex.push_back(verterxPoint); edge_vertex.push_back(vertexCams_vec[0]); edge_vertex.push_back(vertexCams_vec[j]); edge->SetVertex(edge_vertex);problem.AddEdge(edge); }
}
problem.Solve(5); // 求解,迭代5步 // 三维特征点,原始数据、加噪声后数据、优化后数据
std::cout << "\nCompare MonoBA results after opt..." << std::endl;
for (size_t k = 0; k < allPoints.size(); k+=1) { std::cout << "after opt, point " << k << " : gt " << 1. / points[k].z() << " ,noise " << noise_invd[k] << " ,opt " << allPoints[k]->Parameters() << std::endl; }
std::cout<<"------------ pose translation ----------------"<<std::endl;
for (int i = 0; i < vertexCams_vec.size(); ++i) { std::cout<<"translation after opt: "<< i <<" :"<< vertexCams_vec[i]->Parameters().head(3).transpose() << " || gt: "<<cameras[i].twc.transpose()<<std::endl; }
/// 优化完成后,第一帧相机的 pose 平移(x,y,z)不再是原点 0,0,0. 说明向零空间发生了漂移。
/// 解决办法: fix 第一帧和第二帧,固定 7 自由度。 或者加上非常大的先验值。 80、81 fixproblem.TestMarginalize();return 0;
}
可以看到这里的顶点有verterxPose
和vertexCam
两种,分别代表特征点(逆深度)和相机位姿。
2. 后端部分:
2.1 顶点
顶点、边、求解器都有对应的文件,所有顶点对应的父类为vertex,它有一个VecX来表示它的值,然后还要指定其维度。以姿态的顶点VertexPose为例:
vertex_pose.h
/**
* Pose vertex * parameters: tx, ty, tz, qx, qy, qz, qw, 7 DoF
* optimization is perform on manifold, so update is 6 DoF, left multiplication
*
* pose is represented as Twb in VIO case
*/
class VertexPose : public Vertex {public: EIGEN_MAKE_ALIGNED_OPERATOR_NEW;VertexPose() : Vertex(7, 6) {} // 传进7个,但只有六自由度的变量/// 加法,可重定义 /// 默认是向量加 virtual void Plus(const VecX &delta) override;std::string TypeInfo() const { return "VertexPose"; }};
构造函数的两项参数为其变量的维数和实际自由度,比如姿态有6个自由度但是由于使用了四元数表示所以一共7维,这里就是7和6。同时对加法进行了override:
vertex_pose.cc
// 定义广义的加法void
VertexPose::Plus(const VecX &delta) { VecX ¶meters = Parameters(); parameters.head<3>() += delta.head<3>(); Qd q(parameters[6], parameters[3], parameters[4], parameters[5]); q = q * Sophus::SO3d::exp(Vec3(delta[3], delta[4], delta[5])).unit_quaternion(); // right multiplication with so3 q.normalized(); // 归一化操作 parameters[3] = q.x(); parameters[4] = q.y(); parameters[5] = q.z(); parameters[6] = q.w();
}
平移部分直接相加,旋转部分需要先将四元数的虚部转换为SO3下的更新量,再右乘原来的四元数,得到更新后的四元数,之后对四元数进行了归一化。
2.2 边(残差)
边负责计算残差,残差=预测-观测,维度在构造函数中定义代价函数是 (残差×信息×残差),是一个数值,由后端求和后最小化。edge类的构造函数如下:
edge.h
/** * 构造函数,会自动化配雅可比的空间 * @param residual_dimension 残差维度 * @param num_verticies 顶点数量 * @param verticies_types 顶点类型名称,可以不给,不给的话check中不会检查 */
explicit Edge(int residual_dimension, int num_verticies, const std::vector<std::string> &verticies_types = std::vector<std::string>());
...
/// 计算残差,由子类实现
virtual void ComputeResidual() = 0;/// 计算雅可比,由子类实现
/// 本后端不支持自动求导,需要实现每个子类的雅可比计算方法
virtual void ComputeJacobians() = 0;
2.2.1 边的框架
框架:
edge_reprojection.h
/*** 此边是视觉重投影误差,此边为三元边,与之相连的顶点有:* 路标点的逆深度InveseDepth、第一次观测到该路标点的source Camera的位姿T_World_From_Body1, * 和观测到该路标点的mearsurement Camera位姿T_World_From_Body2。* 注意:verticies_顶点顺序必须为InveseDepth、T_World_From_Body1、T_World_From_Body2。*/
class EdgeReprojection : public Edge {public: EIGEN_MAKE_ALIGNED_OPERATOR_NEW;EdgeReprojection(const Vec3 &pts_i, const Vec3 &pts_j) : Edge(2, 3, std::vector<std::string>{"VertexInverseDepth", "VertexPose", "VertexPose"}) { pts_i_ = pts_i; pts_j_ = pts_j; }/// 返回边的类型信息 virtual std::string TypeInfo() const override { return "EdgeReprojection"; }/// 计算残差 virtual void ComputeResidual() override;/// 计算雅可比 virtual void ComputeJacobians() override;void SetTranslationImuFromCamera(Eigen::Quaterniond &qic_, Vec3 &tic_);private: //Translation imu from camera Qd qic; Vec3 tic;//measurements Vec3 pts_i_, pts_j_;
};/**
* 此边是视觉重投影误差,此边为二元边,与之相连的顶点有:
* 路标点的世界坐标系XYZ、观测到该路标点的 Camera 的位姿T_World_From_Body1
* 注意:verticies_顶点顺序必须为 XYZ、T_World_From_Body1
*/
class EdgeReprojectionXYZ : public Edge {public: EIGEN_MAKE_ALIGNED_OPERATOR_NEW;EdgeReprojectionXYZ(const Vec3 &pts_i) : Edge(2, 2, std::vector<std::string>{"VertexXYZ", "VertexPose"}) { obs_ = pts_i;
}/// 返回边的类型信息 virtual std::string TypeInfo() const override { return "EdgeReprojectionXYZ"; }/// 计算残差 virtual void ComputeResidual() override;/// 计算雅可比 virtual void ComputeJacobians() override;void SetTranslationImuFromCamera(Eigen::Quaterniond &qic_, Vec3 &tic_);private: //Translation imu from camera Qd qic; Vec3 tic;//measurements Vec3 obs_;
};
这里传入的pts_i和pts_j,分别是当前观测和下一帧观测的重投影的像素坐标,并且在归一化平面上,即z=1。前面说到edge要负责计算出顶点残差和其对应的雅克比,这部分的函数如下:
edge_reprojection.cc
2.2.2 首先计算残差
① 视觉重投影残差
这里的残差是按照第三讲中的视觉重投影误差计算的,首先取得i帧下的逆深度,然后获得两帧相机的姿态,之后通过坐标系的转换将i帧下的像素点坐标pts_i_转换到j帧的坐标系下,最后将转换过来的像素坐标(pts_camera_j / dep_j)和j帧下的测量值取差得到残差。
【强调】verticies_顶点顺序确定为InveseDepth、T_World_From_Body1、T_World_From_Body2。
/* std::vector<std::shared_ptr<Vertex>> verticies_; // 该边对应的顶点 VecX residual_; // 残差 std::vector<MatXX> jacobians_; // 雅可比,每个雅可比维度是 residual x vertex[i] MatXX information_; // 信息矩阵 VecX observation_; // 观测信息 */void EdgeReprojection::ComputeResidual() { // 残差的计算 double inv_dep_i = verticies_[0]->Parameters()[0]; // 逆深度VecX param_i = verticies_[1]->Parameters(); // T_World_From_Body1 Qd Qi(param_i[6], param_i[3], param_i[4], param_i[5]); Vec3 Pi = param_i.head<3>(); VecX param_j = verticies_[2]->Parameters(); // T_World_From_Body2 Qd Qj(param_j[6], param_j[3], param_j[4], param_j[5]); Vec3 Pj = param_j.head<3>();// 公式(61)的实现 Vec3 pts_camera_i = pts_i_ / inv_dep_i; // 对当前观测i的重投影的像素坐标归一化 Vec3 pts_imu_i = qic * pts_camera_i + tic; // i帧到imu-i的转换 Vec3 pts_w = Qi * pts_imu_i + Pi; // imu-i到world的转换 Vec3 pts_imu_j = Qj.inverse() * (pts_w - Pj); // world到imu-j的转换 Vec3 pts_camera_j = qic.inverse() * (pts_imu_j - tic); // imu-j到j帧的转换 fc_jdouble dep_j = pts_camera_j.z(); residual_ = (pts_camera_j / dep_j).head<2>() - pts_j_.head<2>(); /// J^t * J * delta_x = - J^t * r
// residual_ = information_ * residual_; // remove information here, we multi information matrix in problem solver
}
【相关知识点】
- 视觉残差为:
其中,uci,vciu_{c_i},v_{c_i}uci,vci指的是代码中传入的pts_i:当前观测的重投影的像素坐标。为求解视觉残差,则需要求解对于第 i 帧中的特征点,它投影到第 j 帧相机坐标系下的值
,其计算公式为:
推导各类 Jacobian 之前,为了简化公式,先定义如下变量(即为代码中的求解过程):
求解出xcjx_{c_j}xcj,ycjy_{c_j}ycj,zcjz_{c_j}zcj代入残差公式即可求。
② IMU预积分残差
void EdgeImu::ComputeResidual()
{ VecX param_0 = verticies_[0]->Parameters(); Qd Qi(param_0[6], param_0[3], param_0[4], param_0[5]); Vec3 Pi = param_0.head<3>();VecX param_1 = verticies_[1]->Parameters(); Vec3 Vi = param_1.head<3>(); Vec3 Bai = param_1.segment(3, 3); Vec3 Bgi = param_1.tail<3>();VecX param_2 = verticies_[2]->Parameters(); Qd Qj(param_2[6], param_2[3], param_2[4], param_2[5]); Vec3 Pj = param_2.head<3>();VecX param_3 = verticies_[3]->Parameters(); Vec3 Vj = param_3.head<3>(); Vec3 Baj = param_3.segment(3, 3); Vec3 Bgj = param_3.tail<3>()residual_ = pre_integration_->evaluate(Pi, Qi, Vi, Bai, Bgi, Pj, Qj, Vj, Baj, Bgj); // 计算IMU残差SetInformation(pre_integration_->covariance.inverse());} // 得到IMU信息矩阵
关于IMU残差计算evaluate()
函数的详细代码解析和相关知识点可见我的另一篇博客——IMU预积分的第5节。
2.2.3 计算残差的雅可比
① edge_reprojection.cc
视觉残差的雅可比
此代码的雅可比计算为视觉误差对两个时刻(i,j)的状态量以及逆深度的求导,没有涉及对imu与imu之间参数的求导。根据链式法则,雅可比的计算可以分两步走,第一步是误差对fcjf_{c_j}fcj的求导(对应代码中的reduce
),第二步是fcjf_{c_j}fcj对各状态量的求导。详细解析已经在代码中注释。前面计算逆深度、各个变换矩阵的过程和计算残差与2.2.2相同.
void EdgeReprojection::ComputeJacobians() { double inv_dep_i = verticies_[0]->Parameters()[0]; // 目标点逆深度的参数化1维VecX param_i = verticies_[1]->Parameters(); Qd Qi(param_i[6], param_i[3], param_i[4], param_i[5]); // 第i时刻相机的POse Vec3 Pi = param_i.head<3>(); // 第i时刻相机的平移VecX param_j = verticies_[2]->Parameters(); Qd Qj(param_j[6], param_j[3], param_j[4], param_j[5]); // 第j时刻 Vec3 Pj = param_j.head<3>();// 公式(61)的实现 对于第 i 帧中的特征点,它投影到第 j 帧相机坐标系下的值 Vec3 pts_camera_i = pts_i_ / inv_dep_i; // 对当前观测i的重投影的像素坐标归一化 Vec3 pts_imu_i = qic * pts_camera_i + tic; Vec3 pts_w = Qi * pts_imu_i + Pi; Vec3 pts_imu_j = Qj.inverse() * (pts_w - Pj); Vec3 pts_camera_j = qic.inverse() * (pts_imu_j - tic); // fc_j = [xc_j,yc_j,zc_j]double dep_j = pts_camera_j.z();// 四元数变成矩阵 Mat33 Ri = Qi.toRotationMatrix(); // imu-i到world的转换 Mat33 Rj = Qj.toRotationMatrix(); // imu-j到world的转换 Mat33 ric = qic.toRotationMatrix(); // 相机到imu的转换// 视觉误差对pts_camera_j求导 Mat23 reduce(2, 3); // 公式(63)的实现 reduce << 1. / dep_j, 0, -pts_camera_j(0) / (dep_j * dep_j), 0, 1. / dep_j, -pts_camera_j(1) / (dep_j * dep_j);
// reduce = information_ * reduce;Eigen::Matrix<double, 2, 6> jacobian_pose_i; Eigen::Matrix<double, 3, 6> jaco_i; // 公式(64)实现 fc_j对i时刻位移的求导 放置到jaco_i的左3列 jaco_i.leftCols<3>() = ric.transpose() * Rj.transpose(); // 公式(67)实现 fc_j对i时刻角度增量的求导 放置到jaco_i的右3列 jaco_i.rightCols<3>() = ric.transpose() * Rj.transpose() * Ri * -Sophus::SO3d::hat(pts_imu_i); // hat为向量到其对应的反对称矩阵 imu到相机的转换 // 公式(62)的部分实现——视觉误差对i时刻状态量的求导 jacobian_pose_i.leftCols<6>() = reduce * jaco_i;Eigen::Matrix<double, 2, 6> jacobian_pose_j; Eigen::Matrix<double, 3, 6> jaco_j; // 公式(68)实现 fc_j对j时刻位移的求导 放置到jaco_j的左3列 jaco_j.leftCols<3>() = ric.transpose() * -Rj.transpose(); // 公式(70)实现 fc_j对j时刻角度增量的求导 放置到jaco_j的右3列 jaco_j.rightCols<3>() = ric.transpose() * Sophus::SO3d::hat(pts_imu_j); // 公式(62)的部分实现——视觉误差对j时刻状态量的求导 jacobian_pose_j.leftCols<6>() = reduce * jaco_j;Eigen::Vector2d jacobian_feature; // 公式(76)的实现——视觉误差对特征逆深度的求导 jacobian_feature = reduce * ric.transpose() * Rj.transpose() * Ri * ric * pts_i_ * -1.0 / (inv_dep_i * inv_dep_i);Eigen::Matrix<double, 2, 6> jacobian_ex_pose; Eigen::Matrix<double, 3, 6> jaco_ex; // 视觉误差对相机与imu之间的外参的求导 // 对位移的求导--公式(71)的实现 jaco_ex.leftCols<3>() = ric.transpose() * (Rj.transpose() * Ri - Eigen::Matrix3d::Identity()); // 对角度增量的求导 Eigen::Matrix3d tmp_r = ric.transpose() * Rj.transpose() * Ri * ric; jaco_ex.rightCols<3>() = -tmp_r * Utility::skewSymmetric(pts_camera_i) + Utility::skewSymmetric(tmp_r * pts_camera_i) +// 公式(74) Utility::skewSymmetric(ric.transpose() * (Rj.transpose() * (Ri * tic + Pi - Pj) - tic)); //公式(75) jacobian_ex_pose.leftCols<6>() = reduce * jaco_ex;// 雅可比矩阵 jacobians_[0] = jacobian_feature; jacobians_[1] = jacobian_pose_i; jacobians_[2] = jacobian_pose_j;jacobians_[3] = jacobian_ex_pose;
.【注】相关函数的解析
- Matrix3d M_so3_V1 = Sophus ::SO3 ::hat (so3_V1); //hat为向量到其对应的反对称矩阵
P.leftCols<cols>() // P(:, 1:cols) //前cols列
P.leftCols(cols) // P(:, 1:cols) //前cols列
P.middleCols<cols>(j) // P(:, j+1:j+cols) //中间cols列
P.middleCols(j, cols) // P(:, j+1:j+cols) //中间cols列
P.rightCols<cols>() // P(:, end-cols+1:end) //后cols列
P.rightCols(cols) // P(:, end-cols+1:end) //后cols列
参考资料
【相关知识点】
- 前文已经求解的
对于第i帧中的特征点,投影到第j帧相机坐标系下的三维坐标形式
:
雅可比矩阵的形式:
分别表示视觉误差对iii时刻的状态量(平移、旋转),对jjj时刻的状态量,对相机与Imu之间的外参,对逆深度的求导。根据链式法则,Jacobian 的计算可以分两步走,第一步误差对 fcjf_{c_j}fcj 求导,求解公式如下:
第二步 fcjf_{c_j}fcj 对各状态量求导:
(1) 对 i 时刻的状态量求导
a. 对 i 时刻位移求导,丢掉与平移无关的量(旋转部分),得到:
对i时刻位移求导有:
b. 对 i 时刻角度增量求导
为简化,直接丢掉不相关的部分:
雅可比矩阵为:
(2)对 j 时刻的状态量求导(原理同1类似)
a. 对位移求导:
b. 对角度增量求导,同上,简化公式,去掉无关量:
则雅可比矩阵为:
(3)视觉误差对相机与imu之间的外参的求导
a.对位移的求导
b.对角度增量求导,加式两部分都与RbcR_{bc}Rbc有关,比较复杂,所以分两部分求导,这里只给出两部分求导的结果,两部分相加就是视觉误差对外参中的角度增量的最终结果。
①第一部分的雅可比:
②第二部分的雅可比(结果是一个反对称矩阵):
(4)视觉误差对特征逆深度的求导,很容易:
将以上四部分整合进一个矩阵,最后得到整个视觉重投影误差的雅克比矩阵。
② IMU残差的雅可比矩阵
IMU残差的雅可比矩阵可以分为四个部分,分别是残差rp、rq、rvr_p、r_q、r_vrp、rq、rv分别对状态量RiR_iRi、pip_ipi的一阶导、对状态量RjR_jRj、pjp_jpj的一阶导、以及对两个时刻的速度和偏置的一阶导。
残差:
状态量:
具体求解过程可参考博客——后端非线性优化中的第三部分-IMU约束。
EdgeImu::ComputeJacobians()
详细代码略。
2.3 problem类
该类函数较多,所以从主函数用到的顺序来看,Problem problem(Problem::ProblemType::SLAM_PROBLEM);
problem框架:
problem.h
class Problem {public:/** * 问题的类型 * SLAM问题还是通用的问题 * * 如果是SLAM问题那么pose和landmark是区分开的,Hessian以稀疏方式存储 * SLAM问题只接受一些特定的Vertex和Edge * 如果是通用问题那么hessian是稠密的,除非用户设定某些vertex为marginalized */ enum class ProblemType { SLAM_PROBLEM, GENERIC_PROBLEM }; typedef unsigned long ulong;
// typedef std::unordered_map<unsigned long, std::shared_ptr<Vertex>> HashVertex; typedef std::map<unsigned long, std::shared_ptr<Vertex>> HashVertex; typedef std::unordered_map<unsigned long, std::shared_ptr<Edge>> HashEdge; typedef std::unordered_multimap<unsigned long, std::shared_ptr<Edge>> HashVertexIdToEdgeEIGEN_MAKE_ALIGNED_OPERATOR_NEW;Problem(ProblemType problemType);~Problem();bool AddVertex(std::shared_ptr<Vertex> vertex);/** * remove a vertex * @param vertex_to_remove */ bool RemoveVertex(std::shared_ptr<Vertex> vertex);bool AddEdge(std::shared_ptr<Edge> edge);bool RemoveEdge(std::shared_ptr<Edge> edge);/** * 取得在优化中被判断为outlier部分的边,方便前端去除outlier * @param outlier_edges */ void GetOutlierEdges(std::vector<std::shared_ptr<Edge>> &outlier_edges);/** * 求解此问题 * @param iterations * @return */ bool Solve(int iterations);/// 边缘化一个frame和以它为host的landmark bool Marginalize(std::shared_ptr<Vertex> frameVertex, const std::vector<std::shared_ptr<Vertex>> &landmarkVerticies);bool Marginalize(const std::shared_ptr<Vertex> frameVertex);//test compute prior void TestMarginalize();private:/// Solve的实现,解通用问题 bool SolveGenericProblem(int iterations);/// Solve的实现,解SLAM问题 bool SolveSLAMProblem(int iterations);/// 设置各顶点的ordering_index void SetOrdering();/// set ordering for new vertex in slam problem void AddOrderingSLAM(std::shared_ptr<Vertex> v);/// 构造大H矩阵 void MakeHessian();/// schur求解SBA void SchurSBA();/// 解线性方程 void SolveLinearSystem();/// 更新状态变量 void UpdateStates();void RollbackStates(); // 有时候 update 后残差会变大,需要退回去,重来/// 计算并更新Prior部分 void ComputePrior();/// 判断一个顶点是否为Pose顶点 bool IsPoseVertex(std::shared_ptr<Vertex> v);/// 判断一个顶点是否为landmark顶点 bool IsLandmarkVertex(std::shared_ptr<Vertex> v);/// 在新增顶点后,需要调整几个hessian的大小 void ResizePoseHessiansWhenAddingPose(std::shared_ptr<Vertex> v);/// 检查ordering是否正确 bool CheckOrdering();void LogoutVectorSize();/// 获取某个顶点连接到的边 std::vector<std::shared_ptr<Edge>> GetConnectedEdges(std::shared_ptr<Vertex> vertex);/// Levenberg /// 计算LM算法的初始Lambda void ComputeLambdaInitLM();/// Hessian 对角线加上或者减去 Lambda void AddLambdatoHessianLM();void RemoveLambdaHessianLM();/// LM 算法中用于判断 Lambda 在上次迭代中是否可以,以及Lambda怎么缩放 bool IsGoodStepInLM();/// PCG 迭代线性求解器 VecX PCGSolver(const MatXX &A, const VecX &b, int maxIter);double currentLambda_; double currentChi_; double stopThresholdLM_; // LM 迭代退出阈值条件 double ni_; //控制 Lambda 缩放大小ProblemType problemType_;/// 整个信息矩阵 MatXX Hessian_; VecX b_; VecX delta_x_;/// 先验部分信息 MatXX H_prior_; VecX b_prior_; MatXX Jt_prior_inv_; VecX err_prior_;/// SBA的Pose部分 MatXX H_pp_schur_; VecX b_pp_schur_; // Heesian 的 Landmark 和 pose 部分 MatXX H_pp_; VecX b_pp_; MatXX H_ll_; VecX b_ll_;/// all vertices HashVertex verticies_;/// all edges HashEdge edges_;/// 由vertex id查询edge HashVertexIdToEdge vertexToEdge_;/// Ordering related ulong ordering_poses_ = 0; ulong ordering_landmarks_ = 0; ulong ordering_generic_ = 0; std::map<unsigned long, std::shared_ptr<Vertex>> idx_pose_vertices_; // 以ordering排序的pose顶点 std::map<unsigned long, std::shared_ptr<Vertex>> idx_landmark_vertices_; // 以ordering排序的landmark顶点// verticies need to marg. <Ordering_id_, Vertex> HashVertex verticies_marg_;bool bDebug = false; double t_hessian_cost_ = 0.0; double t_PCGsovle_cost_ = 0.0;};
2.3.1 向problem中添加顶点
problem.cc
bool Problem::AddVertex(std::shared_ptr<Vertex> vertex) {if (verticies_.find(vertex->Id()) != verticies_.end()) {// LOG(WARNING) << "Vertex " << vertex->Id() << " has been added before";return false;} else {verticies_.insert(pair<unsigned long,shared_ptr<Vertex>>(vertex->Id(), vertex));}if (problemType_ == ProblemType::SLAM_PROBLEM) {if (IsPoseVertex(vertex)) { // 判断一个顶点是否为Pose顶点 ResizePoseHessiansWhenAddingPose(vertex); // 在新增顶点后,需要调整几个hessian的大小}}return true;}// 在新增顶点后,需要调整几个hessian的大小
void Problem::ResizePoseHessiansWhenAddingPose(shared_ptr<Vertex> v) {int size = H_prior_.rows() + v->LocalDimension(); H_prior_.conservativeResize(size, size); b_prior_.conservativeResize(size);b_prior_.tail(v->LocalDimension()).setZero(); H_prior_.rightCols(v->LocalDimension()).setZero(); H_prior_.bottomRows(v->LocalDimension()).setZero();
}// 判断一个顶点是否为Pose顶点
bool Problem::IsPoseVertex(std::shared_ptr<myslam::backend::Vertex> v) { string type = v->TypeInfo(); return type == string("VertexPose");}
以添加顶点为例,函数会给顶点添加一个序号,同时根据顶点是否为pose来生成H矩阵,例如第一个pose在左上角0X0位置,第二个应该左上角就为6X6。
2.3.2 向problem中添加边
problem.cc
bool Problem::AddEdge(shared_ptr<Edge> edge) {if (edges_.find(edge->Id()) == edges_.end()) {edges_.insert(pair<ulong, std::shared_ptr<Edge>>(edge->Id(), edge));} else {// LOG(WARNING) << "Edge " << edge->Id() << " has been added before!";return false; }for (auto &vertex: edge->Verticies()) {vertexToEdge_.insert(pair<ulong, shared_ptr<Edge>>(vertex->Id(), edge)); // 由vertex id查询edge}return true;
}
2.3.3 solve函数
添加完顶点和边后,主函数直接调用solve方法problem.Solve(5);
就结束了.
bool Problem::Solve(int iterations) {if (edges_.size() == 0 || verticies_.size() == 0) {std::cerr << "\nCannot solve problem without edges or verticies" << std::endl;return false;}TicToc t_solve; // 统计优化变量的维数,为构建 H 矩阵做准备 SetOrdering(); // 遍历edge, 构建 H 矩阵 MakeHessian(); // LM 初始化 ComputeLambdaInitLM(); // LM 算法迭代求解 bool stop = false; int iter = 0; while (!stop && (iter < iterations)) { std::cout << "iter: " << iter << " , chi= " << currentChi_ << " , Lambda= " << currentLambda_ << std::endl; bool oneStepSuccess = false; int false_cnt = 0; while (!oneStepSuccess) // 不断尝试 Lambda, 直到成功迭代一步 { // setLambda
// AddLambdatoHessianLM(); // 第四步,解线性方程 SolveLinearSystem(); // 阻尼项在此 //
// RemoveLambdaHessianLM();// 优化退出条件1: delta_x_ 很小则退出 if (delta_x_.squaredNorm() <= 1e-6 || false_cnt > 10) { stop = true; break;}// 更新状态量 UpdateStates(); // 判断当前步是否可行以及 LM 的 lambda 怎么更新 oneStepSuccess = IsGoodStepInLM(); // 后续处理, if (oneStepSuccess) { // 在新线性化点 构建 hessian MakeHessian(); // TODO:: 这个判断条件可以丢掉,条件 b_max <= 1e-12 很难达到,这里的阈值条件不应该用绝对值,而是相对值
// double b_max = 0.0;
// for (int i = 0; i < b_.size(); ++i) {// b_max = max(fabs(b_(i)), b_max);
// }
// // 优化退出条件2: 如果残差 b_max 已经很小了,那就退出
// stop = (b_max <= 1e-12); false_cnt = 0; } else { false_cnt ++; RollbackStates(); // 误差没下降,回滚 } } iter++;// 优化退出条件3: currentChi_ 跟第一次的chi2相比,下降了 1e6 倍则退出 if (sqrt(currentChi_) <= stopThresholdLM_) stop = true;} std::cout << "problem solve cost: " << t_solve.toc() << " ms" << std::endl; std::cout << " makeHessian cost: " << t_hessian_cost_ << " ms" << std::endl; return true;
}
2.3.3.1 统计优化变量的维数,为构建 H 矩阵做准备
void Problem::SetOrdering() {// 每次重新计数 ordering_poses_ = 0; ordering_generic_ = 0; ordering_landmarks_ = 0; int debug = 0;// Note:: verticies_ 是 map 类型的, 顺序是按照 id 号排序的 for (auto vertex: verticies_) { ordering_generic_ += vertex.second->LocalDimension(); // 所有的优化变量总维数if (IsPoseVertex(vertex.second)) { debug += vertex.second->LocalDimension();}if (problemType_ == ProblemType::SLAM_PROBLEM) // 如果是 slam 问题,还要分别统计 pose 和 landmark 的维数,后面会对他们进行排序{ AddOrderingSLAM(vertex.second); // set ordering for new vertex in slam problem }if (IsPoseVertex(vertex.second)) { std::cout << vertex.second->Id() << " order: " << vertex.second->OrderingId() << std::endl;}}std::cout << "\n ordered_landmark_vertices_ size : " << idx_landmark_vertices_.size() << std::endl; if (problemType_ == ProblemType::SLAM_PROBLEM) { // 这里要把 landmark 的 ordering 加上 pose 的数量,就保持了 landmark 在后,而 pose 在前 ulong all_pose_dimension = ordering_poses_; for (auto landmarkVertex : idx_landmark_vertices_) { landmarkVertex.second->SetOrderingId( landmarkVertex.second->OrderingId() + all_pose_dimension );}}
// CHECK_EQ(CheckOrdering(), true);
}
2.3.3.2 通过MakeHessian()构建H矩阵
遍历所有的edge的代码内部构建H矩阵的过程,对于每一条边都要进行这样两个for循环,分别是遍历方阵H的行和列,第一个i的for循环是行数,当遍历到行列相等,也就是对角的时候,只用放入一次hessian,而其他情况下都要放入关于对角对称的两个hessian,这是由于这两个位置的hessian是转置的关系。而b则在行遍历的时候一行行填入。
void Problem::MakeHessian() { TicToc t_h; // 直接构造大的 H 矩阵 ulong size = ordering_generic_; MatXX H(MatXX::Zero(size, size)); VecX b(VecX::Zero(size));for (auto &edge: edges_) { // 遍历所有残差项,求解所有残差edge.second->ComputeResidual(); edge.second->ComputeJacobians();auto jacobians = edge.second->Jacobians(); // 取出 auto verticies = edge.second->Verticies(); assert(jacobians.size() == verticies.size()); for (size_t i = 0; i < verticies.size(); ++i) { auto v_i = verticies[i]; if (v_i->IsFixed()) continue; // Hessian 里不需要添加它的信息,也就是它的雅克比为 0auto jacobian_i = jacobians[i]; // 放入 ulong index_i = v_i->OrderingId(); ulong dim_i = v_i->LocalDimension(); // 告诉维度MatXX JtW = jacobian_i.transpose() * edge.second->Information(); for (size_t j = i; j < verticies.size(); ++j) { auto v_j = verticies[j];if (v_j->IsFixed()) continue;auto jacobian_j = jacobians[j]; ulong index_j = v_j->OrderingId(); ulong dim_j = v_j->LocalDimension();assert(v_j->OrderingId() != -1); MatXX hessian = JtW * jacobian_j; // 所有的信息矩阵叠加起来 H.block(index_i,index_j, dim_i, dim_j).noalias() += hessian; if (j != i) { // 对称的下三角 H.block(index_j,index_i, dim_j, dim_i).noalias() += hessian.transpose(); } } b.segment(index_i, dim_i).noalias() -= JtW * edge.second->Residual(); }} Hessian_ = H; b_ = b; t_hessian_cost_ += t_h.toc();// Eigen::JacobiSVD<Eigen::MatrixXd> svd(H, Eigen::ComputeThinU | Eigen::ComputeThinV);
// std::cout << svd.singularValues() <<std::endl;if (err_prior_.rows() > 0) { b_prior_ -= H_prior_ * delta_x_.head(ordering_poses_); // update the error_prior } Hessian_.topLeftCorner(ordering_poses_, ordering_poses_) += H_prior_; b_.head(ordering_poses_) += b_prior_;delta_x_ = VecX::Zero(size); // initial delta_x = 0_n;
}
这里补充一下关于使用鲁棒核函数修改残差和信息矩阵的代码解析\red{这里补充一下关于使用鲁棒核函数修改残差和信息矩阵的代码解析}这里补充一下关于使用鲁棒核函数修改残差和信息矩阵的代码解析
【补充】鲁棒核函数的H矩阵以及b
鲁棒核函数会修改残差和信息矩阵,如果没有设置 robust cost function,就会返回原来的。
若使用鲁棒核函数,则下边的函数:
edge.second->Information()
将变为:
edge.second->RobustInfo(drho,robustInfo);
对于信息矩阵的修改实际就是在原先信息矩阵的基础上增加(乘以)权重信息。
相关知识如下:
相关代码如下:
void Edge::RobustInfo(double &drho, MatXX &info) const{ if(lossfunction_) // 核函数?? {// double e2 = residual_.transpose() * information_ * residual_; double e2 = this->Chi2(); // s (对应于上方的公式)Eigen::Vector3d rho; lossfunction_->Compute(e2,rho); // 计算鲁棒核函数以及相对于s的一、二阶导 VecX weight_err = sqrt_information_ * residual_;// 对W矩阵的求解,完全对应公式17MatXX robust_info(information_.rows(), information_.cols()); robust_info.setIdentity(); robust_info *= rho[1]; if(rho[1] + 2 * rho[2] * e2 > 0.) { robust_info += 2 * rho[2] * weight_err * weight_err.transpose(); // W权重因子 }info = robust_info * information_; // 核函数的信息矩阵 drho = rho[1]; }else { drho = 1.0; info = information_; }
}
① lossfunction_->Compute(e2,rho);
这里只列举柯西鲁棒核函数的定义以及对s的一阶导的代码,完全对应上方公式,delta_ 指协方差矩阵的逆,也就是信息矩阵∑−1\sum^{-1}∑−1。:
void CauchyLoss::Compute(double err2, Eigen::Vector3d& rho) const { double dsqr = delta_ * delta_; // c^2 double dsqrReci = 1. / dsqr; // 1/c^2 double aux = dsqrReci * err2 + 1.0; // 1 + e^2/c^2 rho[0] = dsqr * log(aux); // c^2 * log( 1 + e^2/c^2 ) rho[1] = 1. / aux; // rho' rho[2] = -dsqrReci * std::pow(rho[1], 2); // rho''
}
② 若使用鲁棒核函数则H矩阵和b向量都会相应改变,代码如下(完全对应上方知识点的公式):
MatXX JtW = jacobian_i.transpose() * robustInfo; for (size_t j = i; j < verticies.size(); ++j) { auto v_j = verticies[j];if (v_j->IsFixed()) continue;auto jacobian_j = jacobians[j]; ulong index_j = v_j->OrderingId(); ulong dim_j = v_j->LocalDimension();assert(v_j->OrderingId() != -1); MatXX hessian = JtW * jacobian_j;// 所有的信息矩阵叠加起来 H.block(index_i, index_j, dim_i, dim_j).noalias() += hessian; if (j != i) { // 对称的下三角 H.block(index_j, index_i, dim_j, dim_i).noalias() += hessian.transpose(); } }// 对应公式(17)等式的右侧 b.segment(index_i, dim_i).noalias() -= drho * jacobian_i.transpose()* edge.second->Information() * edge.second->Residual();
注意关于H先验信息的处理,留有后用\red{注意关于H先验信息的处理,留有后用}注意关于H先验信息的处理,留有后用
主要操作为将相机帧和路标点的信息块设置为0。代码如下:
if(H_prior_.rows() > 0)
{ MatXX H_prior_tmp = H_prior_; VecX b_prior_tmp = b_prior_;/// 遍历所有 POSE 顶点,然后设置相应的先验维度为 0 . fix 外参数, SET PRIOR TO ZERO /// landmark 没有先验 for (auto vertex: verticies_) { if (IsPoseVertex(vertex.second) && vertex.second->IsFixed() ) { int idx = vertex.second->OrderingId(); int dim = vertex.second->LocalDimension();//相机和路标点的信息删除 H_prior_tmp.block(idx,0, dim, H_prior_tmp.cols()).setZero(); H_prior_tmp.block(0,idx, H_prior_tmp.rows(), dim).setZero(); b_prior_tmp.segment(idx,dim).setZero(); } } Hessian_.topLeftCorner(ordering_poses_, ordering_poses_) += H_prior_tmp; b_.head(ordering_poses_) += b_prior_tmp;
}
2.3.3.3 LM算法的初始化
ComputeLambdaInitLM()
函数用于计算阻尼因子的初始值
/// LM
void Problem::ComputeLambdaInitLM() { ni_ = 2.; currentLambda_ = -1.; currentChi_ = 0.0; // TODO:: robust cost chi2 for (auto edge: edges_) { currentChi_ += edge.second->Chi2();} if (err_prior_.rows() > 0) // marg prior residual currentChi_ += err_prior_.norm();stopThresholdLM_ = 1e-6 * currentChi_; // 迭代条件为 误差下降 1e-6 倍double maxDiagonal = 0; ulong size = Hessian_.cols(); assert(Hessian_.rows() == Hessian_.cols() && "Hessian is not square"); for (ulong i = 0; i < size; ++i) { maxDiagonal = std::max(fabs(Hessian_(i, i)), maxDiagonal); // fabs取绝对值} double tau = 1e-5; currentLambda_ = tau * maxDiagonal;
}
其中τ\tauτ是一个系数,按工程经验取值(程序里取10−510^{-5}10−5)double tau = 1e-5;
,最后阻尼因子的计算为currentLambda_ = tau * maxDiagonal;
maxdiagonal是H矩阵对角线上最大的块。
【相关知识点】
加入阻尼因子的改进版高斯牛顿——LM
阻尼因子初始值的选取(本代码采取的策略)
程序里取10−510^{-5}10−5补充
Chi2()
代码:
double Edge::Chi2() { // TODO:: we should not Multiply information here, because we have computed Jacobian = sqrt_info * Jacobian return residual_.transpose() * information_ * residual_
// return residual_.transpose() * residual_; // 当计算 residual 的时候已经乘以了 sqrt_info, 这里不要再乘
}
初始化完成之后就进入LM迭代求解的部分,迭代次数由solve函数的入口参数确定,非 SLAM 问题直接取逆求解( H不是稀疏的)。SLAM 问题采用舒尔补的计算方式求解Hδx=bH\delta x = bHδx=b。
2.3.3.4 LM算法基本流程
这一部分在前面2.3.3 solve函数部分
的代码中已经展示,所以省略。
2.3.3.5 求解线性方程
SolveLinearSystem();
void Problem::SolveLinearSystem() {if (problemType_ == ProblemType::GENERIC_PROBLEM) {// 非 SLAM 问题直接求解 // PCG solver MatXX H = Hessian_; for (ulong i = 0; i < Hessian_.cols(); ++i) { H(i, i) += currentLambda_; // H = J^t * J + Lambda * I }
// delta_x_ = PCGSolver(H, b_, H.rows() * 2); delta_x_ = Hessian_.inverse() * b_; // H不是稀疏的,直接取逆} else {// SLAM 问题采用舒尔补的计算方式 // step1: schur marginalization --> Hpp, bpp int reserve_size = ordering_poses_; int marg_size = ordering_landmarks_;// TODO:: home work. 完成矩阵块取值,Hmm,Hpm,Hmp,bpp,bmm:landmark MatXX Hmm = Hessian_.block(reserve_size,reserve_size, marg_size, marg_size); MatXX Hmp = Hessian_.block(reserve_size,0, marg_size, reserve_size); MatXX Hpm = Hessian_.block(0,reserve_size,reserve_size, marg_size); VecX bmm = b_.segment(reserve_size,marg_size ); VecX bpp = b_.segment(0,reserve_size);// Hmm 是对角线矩阵,它的求逆可以直接为对角线块分别求逆,如果是逆深度,对角线块为1维的,则直接为对角线的倒数,这里可以加速 MatXX Hmm_inv(MatXX::Zero(marg_size, marg_size)); for (auto landmarkVertex : idx_landmark_vertices_) { int idx = landmarkVertex.second->OrderingId() - reserve_size; // landmark在后,pose在前 int size = landmarkVertex.second->LocalDimension(); Hmm_inv.block(idx, idx, size, size) = Hmm.block(idx, idx, size, size).inverse(); }// TODO:: home work. 完成舒尔补 Hpp, bpp 代码 MatXX tempH = Hpm * Hmm_inv; H_pp_schur_ = Hessian_.block(0,0,reserve_size,reserve_size) - tempH * Hmp; b_pp_schur_ = bpp - tempH * bmm;// step2: solve Hpp * delta_x = bpp VecX delta_x_pp(VecX::Zero(reserve_size)); // PCG Solver for (ulong i = 0; i < ordering_poses_; ++i) { H_pp_schur_(i, i) += currentLambda_; }int n = H_pp_schur_.rows() * 2; // 迭代次数 delta_x_pp = PCGSolver(H_pp_schur_, b_pp_schur_, n); // 哈哈,小规模问题,搞 pcg 花里胡哨 delta_x_.head(reserve_size) = delta_x_pp; // xp:相机位姿放置在指定位置 // std::cout << delta_x_pp.transpose() << std::endl;// TODO:: home work. step3: solve landmark VecX delta_x_ll(marg_size); delta_x_ll = Hmm_inv * (bmm - Hmp * delta_x_pp) ; delta_x_.tail(marg_size) = delta_x_ll; // x_ll 路标点信息}
}
【相关知识点】
SLAM问题的求解
将问题转换成normal equation,其中,HppH_{pp}Hpp表示相机位姿之间的关系,下图中HllH_{ll}Hll对应代码中的HmmH_{mm}Hmm表示路标点之间的关系,HplH_{pl}Hpl对应代码中的HpmH_{pm}Hpm表示相机和路标之间的关系。
首先使用SolveLinearSystem()
;来求解ΔxΔxΔx,如果求解后ΔxΔxΔx的值够小则认为迭代成功,令stop = true;退出while循环,否则更新状态量并判断当前步是否可行,可行的话就在当前的状态量下求新的H和b,否则当发现误差没有下降的时候就进行回滚。同时如果当残差下降得够多也可以退出迭代(2.3.3 solve函数部分
的代码中已经展示)。
2.3.3.6 更新状态量UpdateStates()
:
void Problem::UpdateStates()
{// update vertex for (auto vertex: verticies_) { // 备份参数 vertex.second->BackUpParameters(); // 保存上次的估计值ulong idx = vertex.second->OrderingId(); ulong dim = vertex.second->LocalDimension(); VecX delta = delta_x_.segment(idx, dim); // H delta_x = b vertex.second->Plus(delta); // += }// update prior if (err_prior_.rows() > 0) { // BACK UP b_prior_ b_prior_backup_ = b_prior_; err_prior_backup_ = err_prior_;/// update with first order Taylor, b' = b + \frac{\delta b}{\delta x} * \delta x /// \delta x = Computes the linearized deviation from the references (linearization points) b_prior_ -= H_prior_ * delta_x_.head(ordering_poses_); // update the error_prior err_prior_ = -Jt_prior_inv_ * b_prior_.head(ordering_poses_ - 15);}
}
2.3.3.7 判断当前步是否可行以及 LM 的 lambda 怎么更新
Problem::IsGoodStepInLM()
此函数的详细代码解析以及知识点可见我的另一篇博客——阻尼因子更新策略(Nielsen)
2.3.3.8 回滚操作
void Problem::RollbackStates()
{// update vertex for (auto vertex: verticies_) { vertex.second->RollBackParameters(); }// Roll back prior_ if (err_prior_.rows() > 0) { b_prior_ = b_prior_backup_; err_prior_ = err_prior_backup_; }
}
void RollBackParameters() { parameters_ = parameters_backup_; }
求解完problem整个问题BA就结束了!
3. 边缘化的测试函数
其例子为:
设x2x_2x2为室外的温度x1,x3x_1, x_3x1,x3分别为房间1和房间 3的室内温度,三个变量有以下关系:
图例为:
其中,viv_ivi相互独立,且各自服从协方差为σi2σ^2_iσi2 的高斯分布。
则其信息矩阵求解为:
现在要从这个例子中marg掉x2x_2x2(绿色部分),求解之后的信息矩阵。需要将绿色部分移动到信息矩阵的右下角,具体操作为先二、三行互换,再二、三列互换。然后进行舒尔补运算,marg掉x2x_2x2。程序如下:
void Problem::TestMarginalize() { // marg第二个变量// Add marg test int idx = 1; // marg 中间那个变量 int dim = 1; // marg 变量的维度 int reserve_size = 3; // 总共变量的维度 double delta1 = 0.1 * 0.1; double delta2 = 0.2 * 0.2; double delta3 = 0.3 * 0.3;int cols = 3; MatXX H_marg(MatXX::Zero(cols, cols)); H_marg << 1./delta1, -1./delta1, 0,-1./delta1, 1./delta1 + 1./delta2 + 1./delta3, -1./delta3, 0., -1./delta3, 1/delta3; std::cout << "---------- TEST Marg: before marg------------"<< std::endl; std::cout << H_marg << std::endl;// TODO:: home work. 将变量移动到右下角 /// 准备工作: move the marg pose to the Hmm bottown right // 将 row i 移动矩阵最下面 Eigen::MatrixXd temp_rows = H_marg.block(idx, 0, dim, reserve_size); //第二行 Eigen::MatrixXd temp_botRows = H_marg.block(idx + dim, 0, reserve_size - idx - dim, reserve_size); // 第三行 H_marg.block(idx, 0,reserve_size - idx - dim, reserve_size) = temp_botRows; // 二、三行颠倒 H_marg.block(reserve_size - dim, 0, dim, reserve_size) = temp_rows;// 将 col i 移动矩阵最右边 Eigen::MatrixXd temp_cols = H_marg.block(0, idx, reserve_size, dim); // 第二列 Eigen::MatrixXd temp_rightCols = H_marg.block(0, idx + dim, reserve_size, reserve_size - idx - dim); // 第三列 H_marg.block(0, idx, reserve_size, reserve_size - idx - dim) = temp_rightCols; // 二、三列颠倒 H_marg.block(0, reserve_size - dim, reserve_size, dim) = temp_cols;std::cout << "---------- TEST Marg: 将变量移动到右下角------------"<< std::endl; std::cout<< H_marg <<std::endl;/// 开始 marg : schur double eps = 1e-8; int m2 = dim; int n2 = reserve_size - dim; // 剩余变量的维度 Eigen::MatrixXd Amm = 0.5 * (H_marg.block(n2, n2, m2, m2) + H_marg.block(n2, n2, m2, m2).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();// TODO:: home work. 完成舒尔补操作 Eigen::MatrixXd Arm = H_marg.block(0,n2,n2,m2); Eigen::MatrixXd Amr = H_marg.block(n2,0,m2,n2); Eigen::MatrixXd Arr = H_marg.block(0,0,n2,n2);Eigen::MatrixXd tempB = Arm * Amm_inv; Eigen::MatrixXd H_prior = Arr - tempB * Amr;std::cout << "---------- TEST Marg: after marg------------"<< std::endl; std::cout << H_prior << std::endl;
}
[从零手写VIO|第五节]——后端优化实践——单目BA求解代码解析相关推荐
- 从零手写VIO(7)
从零手写VIO(7) 文章目录 从零手写VIO(7) 前言 一.VINS-Course代码解析 二.作业(7) 1.simulation-test.cpp修改 2.Sysytem.cpp修改 3.co ...
- 《视觉SLAM进阶:从零开始手写VIO》第三讲 基于优化的IMU预积分与视觉信息融合 作业
<视觉SLAM进阶:从零开始手写VIO>第三讲 基于优化的IMU预积分与视觉信息融合 作业 文章目录 <视觉SLAM进阶:从零开始手写VIO>第三讲 基于优化的IMU预积分与视 ...
- 【VIO笔记(学习VINS的必备基础)】第五讲(1/2) 手写VIO后端
文章目录 非线性最小二乘问题求解:solver 单目BA代码部分 系列教程来自某学院,侵权删除. 学习完这一系列课程再去看VINS才能做到不吃力,不然直接撸网上的各种VINS解析完全云里雾里-_-! ...
- 《视觉SLAM进阶:从零开始手写VIO》第二讲作业-IMU仿真、MU imu_utils标定
<视觉SLAM进阶:从零开始手写VIO>第二讲作业-IMU仿真.MU imu_utils标定 作业题目: 1 仿真代码解析 仿真代码地址:https://github.com/HeYiji ...
- 手写VIO --学习笔记 - Part1
目录 一.VIO文献阅读 二.四元数和李代数更新 三.其他导数 参考 一.VIO文献阅读 阅读VIO 相关综述文献,回答以下问题: 1.视觉与IMU进行融合之后有何优势? IMU:(Inertial ...
- 【SLAM】LIO-SAM解析——后端优化mapOptimization(5)
系列文章链接: [SLAM]LIO-SAM解析--流程图(1) [SLAM]LIO-SAM解析--数据预处理imageProjection(2) [SLAM]LIO-SAM解析--特征提取featur ...
- 写给中高级前端关于性能优化的9大策略和6大指标 | 网易四年实践
「链接和长图失效,请大家点击阅读原文查看详情」 前言 笔者近半年一直在参与项目重构,在重构过程中大量应用「性能优化」和「设计模式」两方面的知识.「性能优化」和「设计模式」两方面的知识不管在工作还是面试 ...
- 深蓝学院《从零开始手写VIO》作业五
深蓝学院<从零开始手写VIO>作业五 1. 完成Bundle Adjustment求解器 2. 完成测试函数 3. 论文总结 1. 完成Bundle Adjustment求解器 完成单目 ...
- 从零写VIO|第二节——作业:使用Allen方差工具标定IMU
这里写目录标题 作业内容 1 安装im_utils 1.1. 安装依赖: 1.2 编译 1.3 可能出的错误 2. 运行 ~~2.1 采集IMU数据~~ 2.2 生成imu.bag 2.3 新建imu ...
最新文章
- 3月18 周作业题解
- ubuntu网卡问题
- 快速搭建实验环境:使用 Terraform 部署 Proxmox 虚拟机
- scanf()中的%c 不能正常输入的问题
- 【转】关于大型网站技术演进的思考(七)--存储的瓶颈(7)
- .Net中的并行编程-6.常用优化策略
- apache无权限访问(You don't have permission to access /docs/index.html on this server)
- 矩阵键盘行列扫描c语言,单片机矩阵键盘按钮行列逐级扫描法
- 几种凹凸贴图(Bump Mapping)的学习记录
- AGPBI: {“kind“:“error“,“text“:“Program type already present: android.support.v4.os.ResultReceiver$1“
- 设置gvim中横竖光标_VIM的配置:高亮光标所在的行列
- canvas绘制表盘时钟
- 读《写给大家看的色彩书1》.设计配色基础1
- python获取摄像头型号_python opencv设置摄像头分辨率以及各个参数的方法_python
- c语言打印n个连续的字符tzz,C/C++编程笔记:C语言实现连连看游戏,小白练手项目(源码分享)...
- Camera Resolution vs Screen Resolution
- android文字转语音文件格式,Android文字转语音
- 解读两篇最新多元时间序列预测工作
- java制作SM2证书
- 35.给定的字符串中字母顺序前移,其他字符顺序后移。
热门文章
- apk闪退_安卓手机经常闪退怎么办?安卓手机闪退解决办法
- php 时间配置,php 配置正确的时间
- C语言实现,古典问题(兔子生崽):有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?(输出前40个月即可)
- ESXI自动关机 ping值检测关机脚本
- 五个网络游戏植入商品营销的案例
- airtest常用按键
- 时序建模:时间戳与时序特征衍生思路汇总
- 微信小程序加入(长按识别)群聊(群二维码)
- 第3周学习:ResNet+ResNeXt
- mysql 特性之一 double write (双写)