MSCKF7讲:特征管理与优化

MSCKF7讲:特征管理与优化


​ 写在前面,MSCKF里面维度一个feature.hpp,用来管理特征

std::map<StateIDType, Eigen::Vector4d, std::less<StateIDType>,
        Eigen::aligned_allocator<std::pair<const StateIDType, Eigen::Vector4d>>>
    observations;	// 存放了观测到当前特征(路标点)的所有帧id,以及对应坐标系下的归一化坐标,因为这里是双目相机,所以是Vector4d

// 3d postion of the feature in the world frame.
Eigen::Vector3d position;

msckf是一个vio系统,他不会对路标点进行优化更新,也不关注全局map,不会保存地图信息,只会把要删除的点去做更新,所以下面MapServer中只是保存了一段时间的特征(路标点)

// 地图----特征(路标点)管理
typedef Feature::FeatureIDType FeatureIDType;	// int
typedef std::map<FeatureIDType, Feature, std::less<int>,	// 存储了特征的map容器
        Eigen::aligned_allocator<std::pair<const FeatureIDType, Feature>>>
    MapServer;
状态量含义
MapServer存储满足要求的特征点,键-特征id,值-对应特征(路标点)
FeatureIDType长整型long long int
Feature特征结构体,描述一个路标点应该有的东西

​ MSCKF中特征参数使用了逆深度的形式,即 λ = [ α β ρ ] ⊤ \boldsymbol{\lambda}=\begin{bmatrix}\alpha&\beta&\rho\end{bmatrix}^{\top} λ=[αβρ]。(X/Z, Y/Z, 1/Z)

使用逆深度

好处:能够表示无穷远点,当特征点较远时, z 数值比较大,直接估计 z 会存在数值问题

坏处:当z趋于0时,ρ趋于无穷大,存在数值奇异,只能用在相机坐标系中,因为相机系下坐标深度都是大于0的,不会趋于0;特别是在非相机坐标系中,因为在那里点的深度可以是负值或零。

1 Feature.h

​ 记录观测特征(路标点)的类,一个特征对应的一些成员(特征id、三维点坐标)、三角化函数等

/** 
 * @brief Feature Salient part of an image.
 * 一个特征可以理解为一个三维点,由多帧观测到
 */
struct Feature
{
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    typedef long long int FeatureIDType;


    struct OptimizationConfig{};	// 优化参数配置

    // 构造函数:初始化特征id,空间位置
    Feature() : id(0), position(Eigen::Vector3d::Zero()),is_initialized(false) {}

    Feature(const FeatureIDType &new_id): id(new_id),position(Eigen::Vector3d::Zero()),is_initialized(false) {}

    /**
     * @brief 计算三维点的重投影误差
     * @param T_c0_c1 即Tcic0
     * @param x The current estimation.要投影的三维点
     * @param z The ith measurement of the feature j in ci frame.观测
     * @return e The cost of this observation.重投影误差
     */
    inline void cost(
        const Eigen::Isometry3d &T_c0_ci,
        const Eigen::Vector3d &x, const Eigen::Vector2d &z,
        double &e) const;

    /**
     * @brief 计算观测z对三维点的雅可比(这里的三维点不是传统意义上的三维点,具体看程序吧)
     * @param T_c0_c1 即Tcic0
     * @param x The current estimation.要投影的三维点
     * @param z 第j个特征在第ci帧中的观测
     * @return J The computed Jacobian.
     * @return r The computed residual.残差
     * @return w Weight induced by huber kernel.高斯核
     */
    inline void jacobian(
        const Eigen::Isometry3d &T_c0_ci,
        const Eigen::Vector3d &x, const Eigen::Vector2d &z,
        Eigen::Matrix<double, 2, 3> &J, Eigen::Vector2d &r,
        double &w) const;

    /**
     * @brief 使用两帧(观测到该点的第一帧(左)核最后一帧(右))去三角化初始的三维点坐标
     * @param z1: feature observation in c1 frame.
     * @param z2: feature observation in c2 frame.
     * @return p: Computed feature position in c1 frame.
     */
    inline void generateInitialGuess(
        const Eigen::Isometry3d &T_c1_c2, const Eigen::Vector2d &z1,
        const Eigen::Vector2d &z2, Eigen::Vector3d &p) const;

    /**
     * @brief 检验三角化是否合理
     * @param cam_states : input camera poses.
     * @return True if the translation between the input camera
     *    poses is sufficient.
     */
    inline bool checkMotion(const CamStateServer &cam_states) const;

    /**
     * @brief InitializePosition Intialize the feature position
     *    based on all current available measurements.供外部调用
     * @param cam_states:std::map<StateIDType, CAMState>
     * @return The computed 3d position is used to set the position
     *    member variable. Note the resulted position is in world
     *    frame.
     * @return True if the estimated 3d position of the feature
     *    is valid.
     */
    inline bool initializePosition(
        const CamStateServer &cam_states);

    // 特征id
    FeatureIDType id;

    // id for next feature
    static FeatureIDType next_id;


    // 升序排序的map
    // 键:观测到这个特征点的 帧id
    // 值:注意这里4表示的是左右相机的特征点对应坐标
    std::map<StateIDType, Eigen::Vector4d, std::less<StateIDType>,
            Eigen::aligned_allocator<std::pair<const StateIDType, Eigen::Vector4d>>>
        observations;

    // 对应在世界系下的坐标
    Eigen::Vector3d position;

    bool is_initialized;

    // Noise for a normalized feature measurement.
    static double observation_noise;

    // Optimization configuration for solving the 3d position.
    static OptimizationConfig optimization_config;
};

2 OptimizationConfig

​ 主要设置了最小二乘法—列文伯格算法中的参数、平移量阈值等

/** 优化配置
     * @brief OptimizationConfig Configuration parameters
     *    for 3d feature position optimization.
     * 优化参数
     */
struct OptimizationConfig
{
    // 位移是否足够,用于判断点是否能做三角化
    double translation_threshold;
    // huber参数
    double huber_epsilon;
    // 修改量阈值,优化的每次迭代都会有更新量,这个量如果太小则表示与目标值接近
    double estimation_precision;
    // LM算法lambda的初始值
    double initial_damping;

    // 内外轮最大迭代次数
    int outer_loop_max_iteration;
    int inner_loop_max_iteration;

    OptimizationConfig(): translation_threshold(0.2),
    huber_epsilon(0.01),
    estimation_precision(5e-7),
    initial_damping(1e-3),
    outer_loop_max_iteration(10),
    inner_loop_max_iteration(10)
    {
        return;
    }
};

3 initializePosition三角化+LM优化

功能:feature主函数,利用观测到的特征点来进行三角化(利用左目不同时刻的帧,第一次观测帧、最后一次观测帧),返回是否三角化成功

/**
 * @brief initializePosition 三角化+LM优化
 * @param cam_states 所有参与计算的相机位姿 是一个map容器,键-id 值-相机状态
 * @return 是否三角化成功
 */
bool Feature::initializePosition(const CamStateServer &cam_states)
{
    // Organize camera poses and feature observations properly.
    // 存放每个观测以及每个对应相机的pos,注意这块是左右目独立存放
    std::vector<Eigen::Isometry3d, Eigen::aligned_allocator<Eigen::Isometry3d>> cam_poses(0);
    std::vector<Eigen::Vector2d, Eigen::aligned_allocator<Eigen::Vector2d>> measurements(0);

    // 1. 添加每一个观测对应的帧信息(左右双目)
    for (auto &m : observations)
    {
        // TODO: This should be handled properly. Normally, the
        //    required camera states should all be available in
        //    the input cam_states buffer.
        // 找到对应观测到该的特征点的帧
        auto cam_state_iter = cam_states.find(m.first);
        if (cam_state_iter == cam_states.end())
            continue;

        // Add the measurement.
        // 1.1 添加归一化坐标
        measurements.push_back(m.second.head<2>());
        measurements.push_back(m.second.tail<2>());

        // This camera pose will take a vector from this camera frame
        // to the world frame.
        // Twc
        Eigen::Isometry3d cam0_pose;
        cam0_pose.linear() =
            quaternionToRotation(cam_state_iter->second.orientation).transpose();
        cam0_pose.translation() = cam_state_iter->second.position;

        Eigen::Isometry3d cam1_pose;
        cam1_pose = cam0_pose * CAMState::T_cam0_cam1.inverse();

        // 1.2 添加相机位姿
        cam_poses.push_back(cam0_pose);
        cam_poses.push_back(cam1_pose);
    }

    // All camera poses should be modified such that it takes a
    // vector from the first camera frame in the buffer to this
    // camera frame.
    // 2. 中心化位姿,提高计算精度---实际上就是把第一帧当基准,找到第一帧到其它帧(包括了右目)的位姿变换
    Eigen::Isometry3d T_c0_w = cam_poses[0];        // Twc0
    for (auto &pose : cam_poses)	// 注意这里是引用
        pose = pose.inverse() * T_c0_w;  // Tcic0 = Tciw *Twc0

    // Generate initial guess
    // 3. 使用首末位姿粗略计算出一个三维点坐标----初始解
    // 这里计算了一个是第一帧坐标系下的三维坐标点初始解
    Eigen::Vector3d initial_position(0.0, 0.0, 0.0);
    generateInitialGuess(
        cam_poses[cam_poses.size() - 1], measurements[0],
        measurements[measurements.size() - 1], initial_position);
    // 弄成逆深度形式---归一化三维点--原则上和measurements[0]的前两维度是同样的值。因为估算的时候也是在最后用measurements[0]*depth,只不过这里第三维变成了深度的倒数,即逆深度形式
    Eigen::Vector3d solution(
        initial_position(0) / initial_position(2),
        initial_position(1) / initial_position(2),
        1.0 / initial_position(2));

    // 列文伯格算法的相关参数
    double lambda = optimization_config.initial_damping;
    int inner_loop_cntr = 0;
    int outer_loop_cntr = 0;
    bool is_cost_reduced = false;
    double delta_norm = 0;

    // Compute the initial cost.
    // 4. 利用初计算的三维点(有深度)计算在各个相机下的误差,作为初始误差
    double total_cost = 0.0;
    for (int i = 0; i < cam_poses.size(); ++i)
    {
        double this_cost = 0.0;
        // 计算投影误差(归一化坐标)
        cost(cam_poses[i], solution, measurements[i], this_cost);
        total_cost += this_cost;
    }

    // Outer loop.
    // 5. LM优化开始, 优化三维点坐标,不优化位姿,比较简单
    do
    {
        // A是  J^t * J  B是 J^t * r
        // 可能有同学疑问自己当初学的时候是 -J^t * r
        // 这个无所谓,因为这里是负的更新就是正的,而这里是正的,所以更新是负的
        // 总之就是有一个是负的,总不能误差越来越大吧
        Eigen::Matrix3d A = Eigen::Matrix3d::Zero();
        Eigen::Vector3d b = Eigen::Vector3d::Zero();

        // 5.1 计算AB矩阵
        for (int i = 0; i < cam_poses.size(); ++i)
        {
            Eigen::Matrix<double, 2, 3> J;
            Eigen::Vector2d r;
            double w;

            // 计算一目相机观测的雅可比与误差
            // J 归一化坐标误差相对于三维点的雅可比
            // r
            // w 权重,同信息矩阵
            jacobian(cam_poses[i], solution, measurements[i], J, r, w);

            // 鲁棒核约束
            if (w == 1)
            {
                A += J.transpose() * J;
                b += J.transpose() * r;
            }
            else
            {
                double w_square = w * w;
                A += w_square * J.transpose() * J;
                b += w_square * J.transpose() * r;
            }
        }

        // Inner loop.
        // Solve for the delta that can reduce the total cost.
        // 这层是在同一个雅可比下多次迭代,如果效果不好说明需要调整阻尼因子了,因为线性化不是很好
        // 如果多次一直误差不下降,退出循环重新计算雅可比
        do
        {
            // LM算法中的lambda
            Eigen::Matrix3d damper = lambda * Eigen::Matrix3d::Identity();
            Eigen::Vector3d delta = (A + damper).ldlt().solve(b);
            // 更新
            Eigen::Vector3d new_solution = solution - delta;
            // 统计本次修改量的大小,如果太小表示接近目标值或者陷入局部极小值,那么就没必要继续了
            delta_norm = delta.norm();

            // 计算更新后的误差
            double new_cost = 0.0;
            for (int i = 0; i < cam_poses.size(); ++i)
            {
                double this_cost = 0.0;
                cost(cam_poses[i], new_solution, measurements[i], this_cost);
                new_cost += this_cost;
            }

            // 如果更新后误差比之前小,说明确实是往好方向发展
            // 我们高斯牛顿的JtJ比较接近真实情况所以减少阻尼,增大步长,delta变大,加快收敛
            if (new_cost < total_cost)
            {
                is_cost_reduced = true;
                solution = new_solution;
                total_cost = new_cost;
                lambda = lambda / 10 > 1e-10 ? lambda / 10 : 1e-10;
            }
            // 如果不行,那么不要这次迭代的结果
            // 说明高斯牛顿的JtJ不接近二阶的海森矩阵
            // 那么增大阻尼,减小步长,delta变小
            // 并且算法接近一阶的最速下降法
            else
            {
                
                is_cost_reduced = false;
                lambda = lambda * 10 < 1e12 ? lambda * 10 : 1e12;
            }

        } while (inner_loop_cntr++ <
                        optimization_config.inner_loop_max_iteration &&
                    !is_cost_reduced);

        inner_loop_cntr = 0;

    // 直到迭代次数到了或者更新量足够小了
    } while (outer_loop_cntr++ <
                    optimization_config.outer_loop_max_iteration &&
                delta_norm > optimization_config.estimation_precision);

    // Covert the feature position from inverse depth
    // representation to its 3d coordinate.
    // 取出最后的结果
    Eigen::Vector3d final_position(solution(0) / solution(2),
                                    solution(1) / solution(2), 1.0 / solution(2));

    // Check if the solution is valid. Make sure the feature
    // is in front of every camera frame observing it.
    // 6. 深度验证
    bool is_valid_solution = true;
    for (const auto &pose : cam_poses)
    {
        Eigen::Vector3d position =
            pose.linear() * final_position + pose.translation();
        if (position(2) <= 0)
        {
            is_valid_solution = false;
            break;
        }
    }

    // 7. 更新结果
    // Convert the feature position to the world frame.
    position = T_c0_w.linear() * final_position + T_c0_w.translation();

    if (is_valid_solution)
        is_initialized = true;

    return is_valid_solution;
}

3.1 计算归一化坐标深度初值generateInitialGuess

这里和常规的三角化并不相同,它只是利用两个归一化坐标的一个旋转变换关系,求取某一个归一化坐标的深度。而不是真正的进行三角化,三角化路标点其中XYZ位置,需要两个匹配点投影才可以求取。

① 理论推导

在这里插入图片描述

② 代码分析

这里计算了一个是第一帧坐标系下的三维坐标点初始解

归一化坐标(非像素坐标)

/**
 * @brief generateInitialGuess 两帧做一次三角化
 * @param T_c1_c2 两帧间的相对位姿
 * @param z1 观测1(坐标系1下归一化坐标(不是像素坐标))
 * @param z2 观测2 都是归一化坐标
 * @param p 三位点坐标
 */
void Feature::generateInitialGuess(
    const Eigen::Isometry3d &T_c1_c2, const Eigen::Vector2d &z1,
    const Eigen::Vector2d &z2, Eigen::Vector3d &p) const
{
    // 列出方程
    // P2 = R21 * P1 + t21  下面省略21
    // 两边左乘P2的反对称矩阵
    // P2^ * (R * P1 + t) = 0
    // 其中左右可以除以P2的深度,这样P2就成了z2,且P1可以分成z1(归一化平面) 乘上我们要求的深度d
    // 令m = R * z1
    // | 0   -1   z2y |    ( | m0 |         )
    // | 1    0  -z2x | *  ( | m1 | * d + t )  =  0
    // |-z2y z2x   0  |    ( | m2 |         )
    // 会发现这三行里面两行是线性相关的,所以只取前两行
    // Construct a least square problem to solve the depth.
    Eigen::Vector3d m = T_c1_c2.linear() * Eigen::Vector3d(z1(0), z1(1), 1.0);

    Eigen::Vector2d A(0.0, 0.0);
    A(0) = m(0) - z2(0) * m(2);  // 对应第二行

    // 按照上面推导这里应该是负的但是不影响,因为我们下边b(1)也给取负了
    A(1) = m(1) - z2(1) * m(2);  // 对应第一行

    Eigen::Vector2d b(0.0, 0.0);
    b(0) = z2(0) * T_c1_c2.translation()(2) - T_c1_c2.translation()(0);
    b(1) = z2(1) * T_c1_c2.translation()(2) - T_c1_c2.translation()(1);

    // Solve for the depth.
    // 解方程得出p1的深度值
    double depth = (A.transpose() * A).inverse() * A.transpose() * b;
    p(0) = z1(0) * depth;
    p(1) = z1(1) * depth;
    p(2) = depth;		// 实际返回的是第一帧坐标系下的三维坐标点
    return;
}

3.2 计算归一化误差cost

函数功能:计算归一化坐标残差

计算c0系下路标点在每一个ci系下的归一化坐标(预测),并计算归一化坐标z(观测)与预测z_hat的残差e(函数功能参考原作者代码英文注释)

① 理论推导

在这里插入图片描述

② 代码分析

/**
 * @brief cost 计算投影误差(归一化坐标)
 * @param T_c0_ci 相对位姿,Tcic0 每一个单相机到第一个观测的相机的T
 * @param x 三维点坐标(x/z, y/z, 1/z)
 * @param z ci帧下的观测归一化坐标
 * @param e 误差
 */
void Feature::cost(
    const Eigen::Isometry3d &T_c0_ci,
    const Eigen::Vector3d &x, const Eigen::Vector2d &z,
    double &e) const
{
    // Compute hi1, hi2, and hi3 as Equation (37).
    const double &alpha = x(0);
    const double &beta = x(1);
    const double &rho = x(2);

    // h 等于  (R * P + t) * 1/Pz
    // h1    | R11 R12 R13    alpha / rho       t1    |
    // h2 =  | R21 R22 R23 *  beta  / rho   +   t2    |   *  rho
    // h3    | R31 R32 R33    1 / rho           t3    |
    Eigen::Vector3d h =
        T_c0_ci.linear() * Eigen::Vector3d(alpha, beta, 1.0) +		// T_c0_ci.linear()获取旋转矩阵
        rho * T_c0_ci.translation();
    double &h1 = h(0);
    double &h2 = h(1);
    double &h3 = h(2);

    // Predict the feature observation in ci frame.
    // 求出在另一个相机ci下的归一化坐标
    Eigen::Vector2d z_hat(h1 / h3, h2 / h3);

    // Compute the residual.
    // 两个归一化坐标算误差
    e = (z_hat - z).squaredNorm();
    return;
}

3.3 计算雅可比jacobian()

上面计算的是残差,而这里计算残差对α、β、ρ的雅可比,即对(x/z, y/z, 1/z)的雅可比。本质是求解对三维点的雅可比,但不是直接对xyz求雅可比

① 理论推导

在这里插入图片描述

在这里插入图片描述

② 代码分析

/**
 * @brief jacobian 求一个观测对应的雅可比
 * @param T_c0_ci 相对位姿,Tcic0 每一个单相机到第一个观测的相机的T
 * @param x 三维点坐标(x/z, y/z, 1/z)
 * @param z 归一化坐标
 * @param J 雅可比 归一化坐标误差相对于三维点的
 * @param r 误差
 * @param w 权重
 * @return 是否三角化成功
 */
void Feature::jacobian(
    const Eigen::Isometry3d &T_c0_ci,
    const Eigen::Vector3d &x, const Eigen::Vector2d &z,
    Eigen::Matrix<double, 2, 3> &J, Eigen::Vector2d &r,
    double &w) const
{

    // Compute hi1, hi2, and hi3 as Equation (37).
    const double &alpha = x(0);  // x/z
    const double &beta = x(1);  // y/z
    const double &rho = x(2);  // 1/z

    // h 等于 (R * P + t) * 1/Pz
    // h1    | R11 R12 R13    alpha / rho       t1    |
    // h2 =  | R21 R22 R23 *  beta  / rho   +   t2    |   *  rho
    // h3    | R31 R32 R33    1 / rho           t3    |
    Eigen::Vector3d h = T_c0_ci.linear() * Eigen::Vector3d(alpha, beta, 1.0) +
                        rho * T_c0_ci.translation();
    double &h1 = h(0);
    double &h2 = h(1);
    double &h3 = h(2);

    // Compute the Jacobian.
    // 首先明确一下误差与三维点的关系
    // 下面的r是误差 r = z_hat - z;  Eigen::Vector2d z_hat(h1 / h3, h2 / h3)
    // 我们要求r对三维点的雅可比,其中z是观测,与三维点坐标无关
    // 因此相当于求归一化坐标相对于 alpha beta rho的雅可比,首先要求出他们之间的关系
    // 归一化坐标设为x y
    // x = h1/h3 y = h2/h3
    // 先写出h与alpha beta rho的关系,也就是上面写的
    // 然后求h1 相对于alpha beta rho的导数 再求h2 的  在求 h3 的
    // R11 R12 t1
    // R21 R22 t2
    // R31 R32 t3
    // 链式求导法则
    // ∂x/∂alpha = ∂x/∂h1 * ∂h1/∂alpha + ∂x/∂h3 * ∂h3/∂alpha
    // ∂x/∂h1 = 1/h3       ∂h1/∂alpha = R11
    // ∂x/∂h3 = -h1/h3^2   ∂h3/∂alpha = R31
    // 剩下的就好求了
    Eigen::Matrix3d W;
    W.leftCols<2>() = T_c0_ci.linear().leftCols<2>();
    W.rightCols<1>() = T_c0_ci.translation();

    // h1 / h3 相对于 alpha beta rho的
    J.row(0) = 1 / h3 * W.row(0) - h1 / (h3 * h3) * W.row(2);
    // h1 / h3 相对于 alpha beta rho的
    J.row(1) = 1 / h3 * W.row(1) - h2 / (h3 * h3) * W.row(2);

    // Compute the residual.
    // 求取误差
    Eigen::Vector2d z_hat(h1 / h3, h2 / h3);
    r = z_hat - z;

    // Compute the weight based on the residual.
    // 使用鲁棒核函数约束
    double e = r.norm();
    if (e <= optimization_config.huber_epsilon)
        w = 1.0;
    // 如果误差大于optimization_config.huber_epsilon但是没超过他的2倍,那么会放大权重w>1
    // 如果误差大的离谱,超过他的2倍,缩小他的权重
    else
        w = std::sqrt(2.0 * optimization_config.huber_epsilon / e);

    return;
}

3.4 计算视差checkMotion—能否三角化

checkMotion Check the input camera poses to ensure there is enough translation to triangulate the feature positon

① 理论分析

​ 代码写的很简单,实际上就是说这角度和基线O1O2不能太小,也不能是180°,要不然达不到三角化条件。

注意:下面feature_direction经过旋转之后还是 O 1 P O_1P O1P方向,所以简单代替,实际转换后,其为世界系下坐标,不再等同 O 1 P O_1P O1P,但方向不变!因为 R w c P c = P w − t w c R_{wc}P_c = P_w - t_{wc} RwcPc=Pwtwc,所以方向还是不变的!(或者说一个空间矢量不会因为在不同坐标系下而改变,改变的只是其在不同系下坐标)。
还有这里为什么 O 1 P O_1P O1P要乘以一个旋转,因为 O 1 O 2 O_1O_2 O1O2也是世界系下的,我们要求的点P是世界系下的,即要求这个三角形矢量也都必须在世界系。

在这里插入图片描述

② 代码分析

/**
 * @brief checkMotion 查看是否有足够视差.供外部调用
 * @param cam_states 所有参与计算的相机位姿
 * @return 是否有足够视差
 */
bool Feature::checkMotion(const CamStateServer &cam_states) const
{
    // observations存储了所有观测到该特征的帧id和特征坐标
    // 1. 取出对应的始末帧id
    const StateIDType &first_cam_id = observations.begin()->first;
    // -- 是递减运算符,用于将迭代器向前移动一个位置。--observations.end() 用于获取容器中倒数第1个元素的迭代器
    // observations.end()指向了容器的末尾,不是最后一个元素!
    const StateIDType &last_cam_id = (--observations.end())->first;

    // 2. 分别赋值位姿
    Eigen::Isometry3d first_cam_pose;
    // Rwc---相机状态里面记录的是qcw,所以这里转R后取了转置
    first_cam_pose.linear() = quaternionToRotation(cam_states.find(first_cam_id)->second.orientation).transpose();
    
    // twc---相机状态原本记录即twc
    first_cam_pose.translation() = cam_states.find(first_cam_id)->second.position;

    Eigen::Isometry3d last_cam_pose;
    // Rwc
    last_cam_pose.linear() = quaternionToRotation(cam_states.find(last_cam_id)->second.orientation).transpose();
    // twc
    last_cam_pose.translation() = cam_states.find(last_cam_id)->second.position;

    // Get the direction of the feature when it is first observed.
    // This direction is represented in the world frame.
    // 这里的意思应该是:找到第一次观测到这个特征的帧,那么把这一帧作为世界系,然后根据计算求出观测的最后一帧,
    // 3. 求出投影射线在世界坐标系啊下的方向
    // 在第一帧的左相机上的归一化坐标
    Eigen::Vector3d feature_direction(observations.begin()->second(0),observations.begin()->second(1), 1.0);
    // 归一化保证了模长是1
    feature_direction = feature_direction / feature_direction.norm();
    // 转到世界坐标系下,求出了这个射线的方向
    feature_direction = first_cam_pose.linear() * feature_direction;

    // Compute the translation between the first frame
    // and the last frame. We assume the first frame and
    // the last frame will provide the largest motion to
    // speed up the checking process.
    // 4. 求出始末两帧在世界坐标系下的位移----始指向末的向量
    // -----就是两帧相机光心在世界系向量O1O2 = twc2-twc1
    Eigen::Vector3d translation = last_cam_pose.translation() - first_cam_pose.translation();     

    // 这里相当于两个向量点乘 这个结果等于夹角的cos值乘上位移的模
    // 也相当于translation 在 feature_direction上的投影
    // 其实就是translation在feature_direction方向上的长度
    double parallel_translation = translation.transpose() * feature_direction;

    // 这块直接理解比较抽象,使用带入法,分别带入 0° 180° 跟90°
    // 当两个向量的方向相同 0°,这个值是0
    // 两个方向相反时 180°,这个值也是0
    // 90°时, cos为0,也就是看translation是否足够大,后面项是0,如果translation小于阈值,也不行
    // 所以这块的判断即考虑了角度,同时考虑了位移。即使90°但是位移不够也不做三角化
    Eigen::Vector3d orthogonal_translation = translation - parallel_translation * feature_direction;

    if (orthogonal_translation.norm() > optimization_config.translation_threshold)
        return true;
    else
        return false;
}

3.5 初始化Position

/**
 * @brief InitializePosition Intialize the feature position
 *    based on all current available measurements.供外部调用
 * @param cam_states: A map containing the camera poses with its
 *    ID as the associated key value.
 * @return The computed 3d position is used to set the position
 *    member variable. Note the resulted position is in world
 *    frame.
 * @return True if the estimated 3d position of the feature
 *    is valid.
 */
inline bool initializePosition(
    const CamStateServer &cam_states);



/**
 * @brief initializePosition 三角化+LM优化
 * @param cam_states 所有参与计算的相机位姿
 * @return 是否三角化成功
 */
bool Feature::initializePosition(
    const CamStateServer &cam_states)
{
    // Organize camera poses and feature observations properly.
    // 存放每个观测以及每个对应相机的pos,注意这块是左右目独立存放
    std::vector<Eigen::Isometry3d, Eigen::aligned_allocator<Eigen::Isometry3d>> cam_poses(0);
    std::vector<Eigen::Vector2d, Eigen::aligned_allocator<Eigen::Vector2d>> measurements(0);

    // 1. 准备数据
    for (auto &m : observations)
    {
        // TODO: This should be handled properly. Normally, the
        //    required camera states should all be available in
        //    the input cam_states buffer.
        auto cam_state_iter = cam_states.find(m.first);
        if (cam_state_iter == cam_states.end())
            continue;

        // Add the measurement.
        // 1.1 添加归一化坐标
        measurements.push_back(m.second.head<2>());
        measurements.push_back(m.second.tail<2>());

        // This camera pose will take a vector from this camera frame
        // to the world frame.
        // Twc
        Eigen::Isometry3d cam0_pose;
        cam0_pose.linear() =
            quaternionToRotation(cam_state_iter->second.orientation).transpose();
        cam0_pose.translation() = cam_state_iter->second.position;

        Eigen::Isometry3d cam1_pose;
        cam1_pose = cam0_pose * CAMState::T_cam0_cam1.inverse();

        // 1.2 添加相机位姿
        cam_poses.push_back(cam0_pose);
        cam_poses.push_back(cam1_pose);
    }

    // All camera poses should be modified such that it takes a
    // vector from the first camera frame in the buffer to this
    // camera frame.
    // 2. 中心化位姿,提高计算精度
    Eigen::Isometry3d T_c0_w = cam_poses[0];
    for (auto &pose : cam_poses)
        pose = pose.inverse() * T_c0_w;

    // Generate initial guess
    // 3. 使用首末位姿粗略计算出一个三维点坐标
    Eigen::Vector3d initial_position(0.0, 0.0, 0.0);
    generateInitialGuess(
        cam_poses[cam_poses.size() - 1], measurements[0],
        measurements[measurements.size() - 1], initial_position);
    // 弄成逆深度形式
    Eigen::Vector3d solution(
        initial_position(0) / initial_position(2),
        initial_position(1) / initial_position(2),
        1.0 / initial_position(2));

    // Apply Levenberg-Marquart method to solve for the 3d position.
    double lambda = optimization_config.initial_damping;
    int inner_loop_cntr = 0;
    int outer_loop_cntr = 0;
    bool is_cost_reduced = false;
    double delta_norm = 0;

    // Compute the initial cost.
    // 4. 利用初计算的点计算在各个相机下的误差,作为初始误差
    double total_cost = 0.0;
    for (int i = 0; i < cam_poses.size(); ++i)
    {
        double this_cost = 0.0;
        // 计算投影误差(归一化坐标)
        cost(cam_poses[i], solution, measurements[i], this_cost);
        total_cost += this_cost;
    }

    // Outer loop.
    // 5. LM优化开始, 优化三维点坐标,不优化位姿,比较简单
    do
    {
        // A是  J^t * J  B是 J^t * r
        // 可能有同学疑问自己当初学的时候是 -J^t * r
        // 这个无所谓,因为这里是负的更新就是正的,而这里是正的,所以更新是负的
        // 总之就是有一个是负的,总不能误差越来越大吧
        Eigen::Matrix3d A = Eigen::Matrix3d::Zero();
        Eigen::Vector3d b = Eigen::Vector3d::Zero();

        // 5.1 计算AB矩阵
        for (int i = 0; i < cam_poses.size(); ++i)
        {
            Eigen::Matrix<double, 2, 3> J;
            Eigen::Vector2d r;
            double w;

            // 计算一目相机观测的雅可比与误差
            // J 归一化坐标误差相对于三维点的雅可比
            // r
            // w 权重,同信息矩阵
            jacobian(cam_poses[i], solution, measurements[i], J, r, w);

            // 鲁棒核约束
            if (w == 1)
            {
                A += J.transpose() * J;
                b += J.transpose() * r;
            }
            else
            {
                double w_square = w * w;
                A += w_square * J.transpose() * J;
                b += w_square * J.transpose() * r;
            }
        }

        // Inner loop.
        // Solve for the delta that can reduce the total cost.
        // 这层是在同一个雅可比下多次迭代,如果效果不好说明需要调整阻尼因子了,因为线性化不是很好
        // 如果多次一直误差不下降,退出循环重新计算雅可比
        do
        {
            // LM算法中的lambda
            Eigen::Matrix3d damper = lambda * Eigen::Matrix3d::Identity();
            Eigen::Vector3d delta = (A + damper).ldlt().solve(b);
            // 更新
            Eigen::Vector3d new_solution = solution - delta;
            // 统计本次修改量的大小,如果太小表示接近目标值或者陷入局部极小值,那么就没必要继续了
            delta_norm = delta.norm();

            // 计算更新后的误差
            double new_cost = 0.0;
            for (int i = 0; i < cam_poses.size(); ++i)
            {
                double this_cost = 0.0;
                cost(cam_poses[i], new_solution, measurements[i], this_cost);
                new_cost += this_cost;
            }

            // 如果更新后误差比之前小,说明确实是往好方向发展
            // 我们高斯牛顿的JtJ比较接近真实情况所以减少阻尼,增大步长,delta变大,加快收敛
            if (new_cost < total_cost)
            {
                is_cost_reduced = true;
                solution = new_solution;
                total_cost = new_cost;
                lambda = lambda / 10 > 1e-10 ? lambda / 10 : 1e-10;
            }
            // 如果不行,那么不要这次迭代的结果
            // 说明高斯牛顿的JtJ不接近二阶的海森矩阵
            // 那么增大阻尼,减小步长,delta变小
            // 并且算法接近一阶的最速下降法
            else
            {
                
                is_cost_reduced = false;
                lambda = lambda * 10 < 1e12 ? lambda * 10 : 1e12;
            }

        } while (inner_loop_cntr++ <
                        optimization_config.inner_loop_max_iteration &&
                    !is_cost_reduced);

        inner_loop_cntr = 0;

    // 直到迭代次数到了或者更新量足够小了
    } while (outer_loop_cntr++ <
                    optimization_config.outer_loop_max_iteration &&
                delta_norm > optimization_config.estimation_precision);

    // Covert the feature position from inverse depth
    // representation to its 3d coordinate.
    // 取出最后的结果
    Eigen::Vector3d final_position(solution(0) / solution(2),
                                    solution(1) / solution(2), 1.0 / solution(2));

    // Check if the solution is valid. Make sure the feature
    // is in front of every camera frame observing it.
    // 6. 深度验证
    bool is_valid_solution = true;
    for (const auto &pose : cam_poses)
    {
        Eigen::Vector3d position =
            pose.linear() * final_position + pose.translation();
        if (position(2) <= 0)
        {
            is_valid_solution = false;
            break;
        }
    }

    // 7. 更新结果
    // Convert the feature position to the world frame.
    position = T_c0_w.linear() * final_position + T_c0_w.translation();

    if (is_valid_solution)
        is_initialized = true;

    return is_valid_solution;
}

4 特征管理总结

4.1 特征投影

z m , k = h ( x k ) + n k = h d ( z n , k , ζ ) + n k = h d ( h p ( C k p f ) , ζ ) + n k = h d ( h p ( h t ( G p f , G C k R , G p C k ) ) , ζ ) + n k = h d ( h p ( h t ( h r ( λ , ⋯   ) , G C k R , G p C k ) ) , ζ ) + n k \begin{aligned} \mathbf{z}_{m,k}& =\mathbf{h}(\mathbf{x}_k)+\mathbf{n}_k \\ &=\mathbf{h}_d(\mathbf{z}_{n,k}, \boldsymbol{\zeta})+\mathbf{n}_k \\ &=\mathbf{h}_d(\mathbf{h}_p(^{C_k}\mathbf{p}_f), \boldsymbol{\zeta})+\mathbf{n}_k \\ &=\mathbf{h}_d(\mathbf{h}_p(\mathbf{h}_t(^G\mathbf{p}_f, _G^{C_k}\mathbf{R}, ^G\mathbf{p}_{C_k})), \boldsymbol{\zeta})+\mathbf{n}_k \\ &=\mathbf{h}_d(\mathbf{h}_p(\mathbf{h}_t(\mathbf{h}_r(\boldsymbol{\lambda},\cdots), _G^{C_k}\mathbf{R}, ^G\mathbf{p}_{C_k})), \boldsymbol{\zeta})+\mathbf{n}_k \end{aligned} zm,k=h(xk)+nk=hd(zn,k,ζ)+nk=hd(hp(Ckpf),ζ)+nk=hd(hp(ht(Gpf,GCkR,GpCk)),ζ)+nk=hd(hp(ht(hr(λ,),GCkR,GpCk)),ζ)+nk

  1. 特征参数转换为特征的全局坐标: G p f = h r ( λ ) ^Gp_f=h_r\left(\lambda\right) Gpf=hr(λ)
  2. 特征的全局坐标转到观测相机坐标系: C k p f = h t ( G p f ) {}^{C_k}p_f=h_t\left({}^Gp_f\right) Ckpf=ht(Gpf)
  3. 相机坐标系投影到normalize平面: z n , k = h p ( C k p f ) z_{n,k}=h_p\left({}^{C_k}p_f\right) zn,k=hp(Ckpf)
  4. normalize平面转到图像平面: z m , k = h d ( z n , k ) z_{m,k}=h_d\left(z_{n,k}\right) zm,k=hd(zn,k)

链式法则求导:
∂ z m , k ∂ λ = ∂ z m , k ∂ z n , k ∂ z n , k ∂ C k p f ∂ C k p f ∂ G p f ∂ G p f ∂ λ \frac{\partial z_{m,k}}{\partial\lambda}=\frac{\partial z_{m,k}}{\partial z_{n,k}}\frac{\partial z_{n,k}}{\partial^{{C_{k}}}p_{f}}\frac{\partial^{{C_{k}}}p_{f}}{\partial^{G}p_{f}}\frac{\partial^{G}p_{f}}{\partial\lambda} λzm,k=zn,kzm,kCkpfzn,kGpfCkpfλGpf

关于链式法则的详细推导,参考

4.2 特征参数

从上面的特征投影就能够看出,整个过程多了特征参数转换为特征全局坐标的一个步骤

对于不同的特征参数表示 λ \lambda λ,都先将其转换成世界系下的 X Y Z XYZ XYZ,即 G p f = h r ( λ ) ^Gp_f=h_r\left(\lambda\right) Gpf=hr(λ),再投影到图像上。这样做的好处在于 G p f ^{G}p_{f} Gpf z m , k z_{m,k} zm,k的转换关系和Jacobian保持不变,只需要构造不同的 h r ( λ ) h_r\left(\lambda\right) hr(λ)并推导 ∂ G p f ∂ λ \frac{\partial^{G}p_{f}}{\partial\lambda} λGpf,就能很快得到 ∂ z m , k ∂ λ \frac{\partial z_{m,k}}{\partial\lambda} λzm,k

特征参数的表示类型由5种(openVINS文档):

Global XYZ:$^Gp_f=h_r\left(\lambda\right)=\lambda ,推出 ,推出 ,推出\frac{\partial^{G}p_{f}}{\partial\lambda}=I$

Global Inverse Depth(球坐标):见4.2.2

Anchored XYZ λ = C a p f , G p f = h r ( λ ) = C a G R λ + G p C a \lambda={}^{C_a}p_f,{}^Gp_f=h_r\left(\lambda\right)={}_{C_a}^GR\left.\lambda+{}^Gp_{C_a}\right. λ=Capf,Gpf=hr(λ)=CaGRλ+GpCa,推出 ∂ G p f ∂ λ = C a G R \frac{\partial^{G}p_{f}}{\partial\lambda}=_{{C_{a}}}^{G}R λGpf=CaGR

Anchored Inverse Depth(球坐标):见4.2.2

Anchored Inverse Depth (MSCKF Version):见4.2.2

注意,只有球坐标逆深度可以用在全局坐标系,因为MSCKF的逆深度当z趋于0时ρ趋于无穷,其只能用在相机坐标系,只有相机系下的特征(路标点)的深度一定大于0!在全局坐标系下,它的深度并不一定大于0。

注意

openvins将世界系中的相机位姿拆分成了IMU位姿以及相机到IMU的外参,这样是为了分别对IMU位姿和相机外参求Jacobian

4.2.1 基于坐标系划分

Global vs Anchored:特征点的表示是全局坐标系的坐标还是局部相机坐标系的坐标

Anchored XYZ表示选择某个相机坐标系作为Anbchor坐标系 C a {C_a} Ca。对于MSCKF里面就使用了观测到当前特征的第一帧作为anchored坐标系

4.2.2 基于XYZ或者逆深度

XYZ即直接使用空间三维点坐标进行建模,当特征较远时, Z 数值比较大,直接估计Z会存在数值问题,逆深度优势在于能够建模无穷远点。

XYZ vs Inverse Depth:使用的XYZ还是逆深度

XYZ

对于世界系

G p f = h r ( λ ) = λ ^Gp_f=h_r\left(\lambda\right)=\lambda Gpf=hr(λ)=λ

对于局部相机坐标系

λ = C a p f , G p f = h r ( λ ) = C a G R λ + G p C a \lambda={}^{C_a}p_f,{}^Gp_f=h_r\left(\lambda\right)={}_{C_a}^GR\left.\lambda+{}^Gp_{C_a}\right. λ=Capf,Gpf=hr(λ)=CaGRλ+GpCa

逆深度1:球坐标逆深度

p f = [ x f y f z f ] = 1 ρ [ cos ⁡ ( θ ) sin ⁡ ( ϕ ) sin ⁡ ( θ ) sin ⁡ ( ϕ ) cos ⁡ ( ϕ ) ] λ = [ θ ϕ ρ ] = [ arctan ⁡ ( y f / x f ) arccos ⁡ ( z f / x f 2 + y f 2 + z f 2 ) 1 / x f 2 + y f 2 + z f 2 ] ∂ p f ∂ λ = [ − 1 ρ s i n ( θ ) sin ⁡ ( ϕ ) 1 ρ c o s ( θ ) cos ⁡ ( ϕ ) − 1 ρ 2 c o s ( θ ) sin ⁡ ( ϕ ) 1 ρ c o s ( θ ) sin ⁡ ( ϕ ) 1 ρ s i n ( θ ) cos ⁡ ( ϕ ) − 1 ρ 2 s i n ( θ ) sin ⁡ ( ϕ ) 0 − 1 ρ s i n ( ϕ ) − 1 ρ 2 c o s ( ϕ ) ] \begin{aligned} &p_{f}=\left[\begin{array}{c}{x_{f}}\\{y_{f}}\\{z_{f}}\end{array}\right]=\frac{1}{\rho}\left[\begin{array}{c}{\cos(\theta)\sin(\phi)}\\{\sin(\theta)\sin(\phi)}\\{\cos(\phi)}\end{array}\right] \\ &\lambda=\begin{bmatrix}\theta\\\phi\\\rho\end{bmatrix}=\begin{bmatrix}\arctan(y_f/x_f)\\\arccos\left(z_f/\sqrt{x_f^2+y_f^2+z_f^2}\right)\\1/\sqrt{x_f^2+y_f^2+z_f^2}\end{bmatrix} \\ &\left.\frac{\partial p_{f}}{\partial\lambda}=\left[\begin{array}{ccc}-\frac{1}{\rho}\mathrm{sin}(\theta)\sin(\phi)&\frac{1}{\rho}\mathrm{cos}(\theta)\cos(\phi)&-\frac{1}{\rho^{2}}\mathrm{cos}(\theta)\sin(\phi)\\\frac{1}{\rho}\mathrm{cos}(\theta)\sin(\phi)&\frac{1}{\rho}\mathrm{sin}(\theta)\cos(\phi)&-\frac{1}{\rho^{2}}\mathrm{sin}(\theta)\sin(\phi)\\0&-\frac{1}{\rho}\mathrm{sin}(\phi)&-\frac{1}{\rho^{2}}\mathrm{cos}(\phi)\end{array}\right.\right] \end{aligned} pf= xfyfzf =ρ1 cos(θ)sin(ϕ)sin(θ)sin(ϕ)cos(ϕ) λ= θϕρ = arctan(yf/xf)arccos(zf/xf2+yf2+zf2 )1/xf2+yf2+zf2 λpf= ρ1sin(θ)sin(ϕ)ρ1cos(θ)sin(ϕ)0ρ1cos(θ)cos(ϕ)ρ1sin(θ)cos(ϕ)ρ1sin(ϕ)ρ21cos(θ)sin(ϕ)ρ21sin(θ)sin(ϕ)ρ21cos(ϕ)

逆深度2:MSCKF逆深度

对应了MSCKF中α、β、ρ ----(x/z, y/z, 1/z)

但MSCKF逆深度有个缺点是当z→0时,ρ→∞,存在数值奇异,所以只能用在相机坐标系,因为相机系下特征点的深度都大于0。而球坐标逆深度仅在x,y,z都趋近于0时才存在数值奇异,所以能用在全局坐标系。

p f = [ x f y f z f ] = 1 ρ [ α β 1 ] λ = [ α β ρ ] = [ x f / z f y f / z f 1 / z f ] ∂ p f ∂ λ = [ 1 ρ 0 − α ρ 2 0 1 ρ − β ρ 2 0 0 − 1 ρ 2 ] \begin{gathered}p_f=\begin{bmatrix}x_f\\y_f\\z_f\end{bmatrix}=\frac1\rho\begin{bmatrix}\alpha\\\beta\\1\end{bmatrix}\\\lambda=\begin{bmatrix}\alpha\\\beta\\\rho\end{bmatrix}=\begin{bmatrix}x_f/z_f\\y_f/z_f\\1/z_f\end{bmatrix}\\\frac{\partial p_f}{\partial\lambda}=\begin{bmatrix}\frac1\rho&0&-\frac\alpha{\rho^2}\\0&\frac1\rho&-\frac\beta{\rho^2}\\0&0&-\frac1{\rho^2}\end{bmatrix}\end{gathered} pf= xfyfzf =ρ1 αβ1 λ= αβρ = xf/zfyf/zf1/zf λpf= ρ1000ρ10ρ2αρ2βρ21

如果是全局坐标系,那么上面的推到就是 ∂ G p f ∂ λ \frac{\partial^{G}p_{f}}{\partial\lambda} λGpf;如果是局部相机坐标系,那么我们还需要对构建局部坐标系到世界系的变换,然后进行链式法则求导(即左乘一个旋转矩阵 C a G R ^G_{C_a}R CaGR)。

G p f = h r ( λ ) = C a G R C a p f + G p C a ∂ G p f ∂ λ = ∂ G p f ∂ C a p f ∂ C a p f ∂ λ \begin{aligned} &^Gp_f=h_r\left(\lambda\right)=_{C_a}^GR^{C_a}p_f+^Gp_{C_a} \\ &\frac{\partial^Gp_f}{\partial\lambda}=\frac{\partial^Gp_f}{\partial^{C_a}p_f}\frac{\partial^{C_a}p_f}{\partial\lambda} \end{aligned} Gpf=hr(λ)=CaGRCapf+GpCaλGpf=CapfGpfλCapf

下面求导参考openvins推导

对球坐标

msckf

4.3 MSCKF逆深度

​ MSCKF中特征参数使用了逆深度的形式,即 λ = [ α β ρ ] ⊤ \boldsymbol{\lambda}=\begin{bmatrix}\alpha&\beta&\rho\end{bmatrix}^{\top} λ=[αβρ]

使用逆深度

好处:能够表示无穷远点,当特征点较远时, z 数值比较大,直接估计 z 会存在数值问题

坏处:当z趋于0时,ρ趋于无穷大,存在数值奇异,只能用在相机坐标系中,因为相机系下坐标深度都是大于0的,不会趋于0;特别是在非相机坐标系中,因为在那里点的深度可以是负值或零。

4.4 VINS逆深度

对于VINS:仅仅使用了深度的逆作为特征参数,

λ = [ ρ ] \lambda=\begin{bmatrix}\rho\end{bmatrix} λ=[ρ]

这对应了VINS里面的优化量:姿态、外参、逆深度

[ p b i w , q b i w ] , [ p b j w , q b j w ] , [ p c b , q c b ] , λ l ∘ \begin{bmatrix}p_{b_i}^w,q_{b_i}^w\end{bmatrix}, \begin{bmatrix}p_{b_j}^w,q_{b_j}^w\end{bmatrix}, [p_c^b,q_c^b], \lambda_l\circ [pbiw,qbiw],[pbjw,qbjw],[pcb,qcb],λl

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值