VINS_by

图中流程图部分与代码对应密切,这里不做具体阐述,对应processImg()中的代码,这里不做罗列。重点介绍视觉惯导联合初始化部分的代码。

image-20220701110546094

VINS_by

这部分是视觉惯导联合初始化的核心部分,主要涉及单目纯视觉的重建(SFM问题),以及视觉与IMU的对齐操作。本节主要分析SFM问题。

一、SFM中的特征点管理

首先是通过计算线加速度的标准差,判断 IMU 是否有充分运动激励,以进行初始化,然后是进行SFM所需要的特征点存入结构体SFMFeature tmp_feature;进行维护。这里涉及SFM中特征点管理方式,如下图所示:

image-20220701111128164

如图所示,滑窗中的路标点被多个图像帧观测到,比图路标点1被图像帧0,1,2观测到,则将每一帧的id以及在该帧中路标点的属性以vector的形式存储在 observation中。

struct SFMFeature
{
    bool state; //状态(是否被三角化)
    int id;        //路标点id
    vector<pair<int, Vector2d>> observation; //所有观测到该特征点的图像帧ID和图像坐标
    double position[3];                         // 3d坐标
    double depth;                             //深度
};

二、选取参考帧并恢复相机运动

如图所示,滑窗中一共有0-10共10帧图像帧,而选取参考帧是比较关键的,参考帧是之后SFM的基础,参考帧需要和当前帧保持足够的视差,又要含有足够的内点,即共视点够多距离有够远,两者权衡之下,选择参考帧 l,选取参考帧的工作主要是由 relativePose() 函数完成的。 第l 帧是从滑动窗口中第1帧开始遍历,第一个满足与当前帧的平均视差足够大的帧;如下图所示,假设我们选取的参考帧是第5帧;

image-20220701142248919

函数relativePose() 的作用,一方面是找到第 l 帧(参考帧),另一方面则是使用对极约束恢复参考帧到当前帧的对极约束,从而当前帧相对参考帧的位姿 $R,T$,为之后的纯视觉的三维重建奠定基础;

  • 根据视差确定参考帧

image-20220701152841318

/**
 * @brief 保证有足够的视差,由E矩阵回复R t
 * 
 * @param relative_R[out] 旋转矩阵
 * @param relative_T[out] 变换矩阵
 * @param l 索引:第 l 帧
 * @return true 
 * @return false 
 */
bool Estimator::relativePose(Matrix3d &relative_R, Vector3d &relative_T, int &l)
{
    // find previous frame which contians enough correspondance and parallex with newest frame
    // 优先从最前面开始
    for (int i = 0; i < WINDOW_SIZE; i++)
    {
        //寻找第i帧(枢纽帧/参考帧)到窗口最后一帧的对应特征点
        vector<pair<Vector3d, Vector3d>> corres;
        corres = f_manager.getCorresponding(i, WINDOW_SIZE);
        // 要求共视的特征点足够多
        if (corres.size() > 20)
        {
            //计算平均视差
            double sum_parallax = 0;
            double average_parallax;
            for (int j = 0; j < int(corres.size()); j++)
            {
                //第j个对应点在第i帧和最后一帧的(x,y)
                Vector2d pts_0(corres[j].first(0), corres[j].first(1));
                Vector2d pts_1(corres[j].second(0), corres[j].second(1));
                double parallax = (pts_0 - pts_1).norm();
                sum_parallax = sum_parallax + parallax;
            }
            average_parallax = 1.0 * sum_parallax / int(corres.size());
            //判断是否满足初始化条件:视差>30和内点数满足要求(大于12)
            //solveRelativeRT()通过基础矩阵计算当前帧与第l帧之间的R和T,并判断内点数目是否足够
            //同时返回窗口最后一帧(当前帧)到第l帧(参考帧)的relative_R,relative_T
            if (average_parallax * 460 > 30 && m_estimator.solveRelativeRT(corres, relative_R, relative_T))
            {
                l = i;
                ROS_DEBUG("average_parallax %f choose l %d and newest frame to triangulate the whole structure", average_parallax * 460, l);
                return true;
            }
        }
    }
    return false;
}

代码解析

程序设置一个 for循环,对滑窗中的所有帧进行遍历,使用getCorresponding() 函数求解每一帧和当前帧的共视点对,当共视点对大于 20 方可满足条件,满足共视点数足够多的条件之后,开始判断是视差,这里计算的是平均视差,因为两帧之间的共视点对不止一对,所以,遍历所有的共视点对,对每个共视点对求得视差取平均当平均视差大于 30 则满足称为参考帧的条件,此时我们就得到了参考帧 l;


  • 使用对极约束,根据基本矩阵求解相机位姿
/**
 * @brief 根据两帧匹配对求解R和带尺度的t
 * 
 * @param corres[in] 匹配点对 
 * @param Rotation[out] 
 * @param Translation[out]  
 * @return true 
 * @return false 
 */
bool MotionEstimator::solveRelativeRT(const vector<pair<Vector3d, Vector3d>> &corres, Matrix3d &Rotation, Vector3d &Translation)
{
    if (corres.size() >= 15)
    {
        vector<cv::Point2f> ll, rr;
        for (int i = 0; i < int(corres.size()); i++)
        {
            ll.push_back(cv::Point2f(corres[i].first(0), corres[i].first(1)));
            rr.push_back(cv::Point2f(corres[i].second(0), corres[i].second(1)));
        }
        cv::Mat mask;
        /**
         *  Mat cv::findFundamentalMat(  返回通过RANSAC算法求解两幅图像之间的本质矩阵E
         *      nputArray  points1,             第一幅图像点的数组
         *      InputArray  points2,            第二幅图像点的数组
         *      int     method = FM_RANSAC,     RANSAC 算法
         *      double  param1 = 3.,            点到对极线的最大距离,超过这个值的点将被舍弃
         *      double  param2 = 0.99,          矩阵正确的可信度
         *      OutputArray mask = noArray()    输出在计算过程中没有被舍弃的点
         *  ) 
         */   
        cv::Mat E = cv::findFundamentalMat(ll, rr, cv::FM_RANSAC, 0.3 / 460, 0.99, mask);
        // 已经是归一化相机坐标系了,因此内参阵用单位阵
        cv::Mat cameraMatrix = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
        cv::Mat rot, trans;

        /**
         *  int cv::recoverPose (   通过本质矩阵得到Rt,返回通过手性校验的内点个数
         *      InputArray  E,              本质矩阵
         *      InputArray  points1,        第一幅图像点的数组
         *      InputArray  points2,        第二幅图像点的数组
         *      InputArray  cameraMatrix,   相机内参
         *      OutputArray     R,          第一帧坐标系到第二帧坐标系的旋转矩阵
         *      OutputArray     t,          第一帧坐标系到第二帧坐标系的平移向量
         *      InputOutputArray    mask = noArray()  在findFundamentalMat()中没有被舍弃的点
         *  )  
         */
        int inlier_cnt = cv::recoverPose(E, ll, rr, cameraMatrix, rot, trans, mask);
        //cout << "inlier_cnt " << inlier_cnt << endl;

        Eigen::Matrix3d R;
        Eigen::Vector3d T;
        // 把CV:Mat格式转换为Eigen
        for (int i = 0; i < 3; i++)
        {   
            T(i) = trans.at<double>(i, 0);
            for (int j = 0; j < 3; j++)
                R(i, j) = rot.at<double>(i, j);
        }
        // Opencv得到的是T21,这里换成T12 
        Rotation = R.transpose();
        Translation = -R.transpose() * T;
        // 内点数满足要求(大于12)
        if(inlier_cnt > 12)
            return true;
        else
            return false;
    }
    return false;
}

代码解析

得到参考帧 l 之后,还需要利用函数 solveRelativeRT() 求取参考帧到当前帧的位姿 $R,T$ ,并且使用函数cv::recoverPose(),他它的作用是通过本质矩阵得到 $R,T$ ,返回通过手性校验的内点个数,并且内点个数也同时是 l 称为参考帧的必要条件,只有内点个数大于 12 才能有资格成为参考帧。

已知在参考帧的点为 $P^1$,当前帧点为$P^2$,则

$$ \begin{align*} P^1&=R_{12}\cdot P^2+T_{12}\\ P^2&=R_{21}\cdot P^1+T_{21} \end{align*}\tag{1} $$

其中,$R_{12},T_{12}$ 表示当前帧到参考帧的位姿,$R_{21},T_{21}$ 表示参考帧到当前帧的位姿,对公式$(1)$中的第二个式子两边同乘 $R_{21}^T$

$$ \begin{align*} R_{21}^TP^2&=P^1+R_{21}^TT_{21}\\ \Rightarrow P^1&=R_{21}^TP^2-R_{21}^TT_{21} \end{align*}\tag{2} $$

对应相等:

$$ \begin{align*} P^1&=R_{12}\cdot P^2+T_{12}\\ P^1&=R_{21}^TP^2-R_{21}^TT_{21} \end{align*}\tag{3} $$

因此可得:

$$ \begin{align*} R_{12}&=R_{21}^T\\ T_{12}&=-R_{21}^TT_{21} \end{align*}\tag{4} $$

由于,上面程序求得的是,当前帧相对参考帧的位姿$R_{21},T_{21}$,因此,参考帧相对当前帧的相对位姿就可以使用上述公式求取。

至此,我们通过对极约束求得了当前帧相对参考帧的位姿 $R,T$;但是,我们知道,对于单目相机来说,存在尺度不确定性,因此这个 $T$ 任意放缩,对极约束都是成立的,所以单目对极几何仅仅起到初始化的作用。

image-20220701153535546

这一步为后面的 PnP过程奠定了基础,因为单目视觉在使用PnP之前,必须使用对极约束,PnP的过程中使用了初始化得到的三角点,这样PnP求出来的 $R,T$ 是唯一的,其尺度是和初始化的时候的 $T$ 是一样的。

三、SFM重建

铺垫了这么多,终于可以进行三维重建了:

image-20220701165135521

/**
 * @description: 纯视觉 sfm,求解窗口中所有图像帧的位姿 QT(相对于第 l 帧)和特征点坐标 sfm_tracked_points
 * @param {int} frame_num             frame_num=frame_count + 1=11,frame_num-1 表示滑窗内的关键帧的总数
 * @param {Quaterniond*} q                恢复出来的滑窗中的各个姿态
 * @param {Vector3d*} T                    恢复出来的滑窗中的各个平移
 * @param {int} l                        参考帧的ids 
 * @param {Matrix3d} relative_R            当前帧到第 l 帧的 relative_R(旋转)
 * @param {Vector3d} relative_T            当前帧到第 l 帧的 relative_T(平移)
 * @param {vector<SFMFeature>} &sfm_f    用来做sfm特征点的集合
 * @param {map<int, Vector3d>} &sfm_tracked_points 恢复出来的地图点
 * @return {*}
 */
bool GlobalSFM::construct(int frame_num, Quaterniond* q, Vector3d* T, int l,
              const Matrix3d relative_R, const Vector3d relative_T,
              vector<SFMFeature> &sfm_f, map<int, Vector3d> &sfm_tracked_points)
{}

在详细讲解视觉重建的过程之前,需要对VINS自定义的三角化函数以及求解PnP的函数做一定的了解,因为两者是三维重建的基础,这部分函数的位置:vins_estimator\src\initial\initial_sfm.cpp

3.1 三角化函数

/**
 * @brief 根据两帧索引以及位姿计算对应特征点的三角化位置
 * 
 * @param frame0 参考帧索引
 * @param Pose0  参考帧位姿    
 * @param frame1 当前帧索引
 * @param Pose1  当前帧位姿    
 * @param sfm_f  三角化的结果    
 */
void GlobalSFM::triangulateTwoFrames(int frame0, Eigen::Matrix<double, 3, 4> &Pose0, 
                                     int frame1, Eigen::Matrix<double, 3, 4> &Pose1,
                                     vector<SFMFeature> &sfm_f)
{
    assert(frame0 != frame1);// 保证两帧图像是不同的
    for (int j = 0; j < feature_num; j++)
    {
        if (sfm_f[j].state == true) //已经三角化了
            continue;
        bool has_0 = false, has_1 = false;
        Vector2d point0;
        Vector2d point1;
        // 遍历该特征点的观测, 判断是否能被当前帧和参考帧同时看到
        for (int k = 0; k < (int)sfm_f[j].observation.size(); k++)
        {
            // 被参考帧看到 has_0 = true
            if (sfm_f[j].observation[k].first == frame0)
            {
                point0 = sfm_f[j].observation[k].second; // 去除特征点在该帧的观测
                has_0 = true;
            }
            // 被当前帧看到 has_1 = true
            if (sfm_f[j].observation[k].first == frame1)
            {
                point1 = sfm_f[j].observation[k].second;
                has_1 = true;
            }
        }
        // 如果被两帧同时看到
        if (has_0 && has_1)
        {
            Vector3d point_3d;
            // 将这个特征点进行三角化
            triangulatePoint(Pose0, Pose1, point0, point1, point_3d);
            sfm_f[j].state = true; // 三角化之后,三角化标志位置为true
            sfm_f[j].position[0] = point_3d(0);
            sfm_f[j].position[1] = point_3d(1);
            sfm_f[j].position[2] = point_3d(2);
            //cout << "trangulated : " << frame1 << "  3d point : "  << j << "  " << point_3d.transpose() << endl;
        }                              
    }
}
/**
 * @brief VINS三角化
 * 
 * @param Pose0 第一帧的位姿    
 * @param Pose1 第二帧的位姿
 * @param point0 第一帧的相机归一化坐标
 * @param point1 第二帧的相机归一化坐标
 * @param point_3d 路标点的世界坐标
 */
void GlobalSFM::triangulatePoint(Eigen::Matrix<double, 3, 4> &Pose0, Eigen::Matrix<double, 3, 4> &Pose1,
                        Vector2d &point0, Vector2d &point1, Vector3d &point_3d)
{
    // 通过奇异值分解求解一个Ax = 0得到
    Matrix4d design_matrix = Matrix4d::Zero();
    design_matrix.row(0) = point0[0] * Pose0.row(2) - Pose0.row(0);
    design_matrix.row(1) = point0[1] * Pose0.row(2) - Pose0.row(1);
    design_matrix.row(2) = point1[0] * Pose1.row(2) - Pose1.row(0);
    design_matrix.row(3) = point1[1] * Pose1.row(2) - Pose1.row(1);
    Vector4d triangulated_point;
    triangulated_point =
              design_matrix.jacobiSvd(Eigen::ComputeFullV).matrixV().rightCols<1>();
    // 齐次向量归一化
    point_3d(0) = triangulated_point(0) / triangulated_point(3);
    point_3d(1) = triangulated_point(1) / triangulated_point(3);
    point_3d(2) = triangulated_point(2) / triangulated_point(3);
}

3.2 求解PnP

/**
 * @brief 对相邻帧求解PnP
 * 
 * @param R_initial 上一帧的位姿作为i帧的初值,并在函数最后做更新,赋初值的意义在于,能够加速求解
 * @param P_initial 
 * @param i 当前索引
 * @param sfm_f 用来做sfm特征点的集合
 * @return true 
 * @return false 
 */
bool GlobalSFM::solveFrameByPnP(Matrix3d &R_initial, Vector3d &P_initial, int i,
                                vector<SFMFeature> &sfm_f)
{
    vector<cv::Point2f> pts_2_vector;     //世界坐标系下的控制点的坐标
    vector<cv::Point3f> pts_3_vector;    //图像坐标系下对应的控制点的坐标    
    for (int j = 0; j < feature_num; j++) // 遍历j个路标点
    {
        if (sfm_f[j].state != true) // false就是没有三角化的点,PnP是3d-2d,因此需要3d点
            continue;
        Vector2d point2d;
        for (int k = 0; k < (int)sfm_f[j].observation.size(); k++) // 遍历路标点j 被观测到的所有observation
        {
            // vector<pair<int, Vector2d> > observation 保存的是Id和图像坐标
            if (sfm_f[j].observation[k].first == i) // 判断与当前帧i有共同观测的帧
            {
                //记录图像坐标
                Vector2d img_pts = sfm_f[j].observation[k].second; 
                cv::Point2f pts_2(img_pts(0), img_pts(1));    
                pts_2_vector.push_back(pts_2);
                // 记录3d坐标
                cv::Point3f pts_3(sfm_f[j].position[0], sfm_f[j].position[1], sfm_f[j].position[2]);
                pts_3_vector.push_back(pts_3);
                break;
            }
        }
    }
    if (int(pts_2_vector.size()) < 15)
    {
        printf("unstable features tracking, please slowly move you device!\n");
        if (int(pts_2_vector.size()) < 10)
            return false;
    }
    // eigen转cv
    cv::Mat r, rvec, t, D, tmp_r;
    cv::eigen2cv(R_initial, tmp_r);
    cv::Rodrigues(tmp_r, rvec);
    cv::eigen2cv(P_initial, t);
    cv::Mat K = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
    bool pnp_succ;
    /**
     * @brief OpenCV求解PnP
     * 
     * @param objectPoints pts_3_vector  世界坐标系下的控制点的坐标
     * @param imagePoints  pts_2_vector  在图像坐标系下对应的控制点的坐标    
     * @param cameraMatrix K 相机的内参矩阵
     * @param distCoeffs   D 相机的畸变系数     
     * @param rvec         rvec 输出的旋转向量。使坐标点从世界坐标系旋转到相机坐标系
     * @param tvec            t 输出的平移向量。使坐标点从世界坐标系平移到相机坐标系
     * @param flags          默认使用CV_ITERATIV迭代法
    */
    pnp_succ = cv::solvePnP(pts_3_vector, pts_2_vector, K, D, rvec, t, 1);
    if(!pnp_succ)
    {
        return false;
    }
    // cv转eigen
    cv::Rodrigues(rvec, r);
    //cout << "r " << endl << r << endl;
    MatrixXd R_pnp;
    cv::cv2eigen(r, R_pnp);
    MatrixXd T_pnp;
    cv::cv2eigen(t, T_pnp);
    R_initial = R_pnp;
    P_initial = T_pnp;
    return true;
}

3.3 SFM重建过程

第一步:先三角化第 l 帧(参考帧)与第 frame_num-1 帧(当前帧)的路标点

第二步:pnp 求解参考帧 $l$ 到第 $l+1$ 开始的每一帧的变换矩阵 R_initial, P_initial,保存在 Pose 中

image-20220701171131507

for (int i = l; i < frame_num - 1 ; i++)
{
    // solve pnp
    if (i > l)
    {
        // 这是依次求解,因此上一帧的位姿是已知量
        Matrix3d R_initial = c_Rotation[i - 1];
        Vector3d P_initial = c_Translation[i - 1];
        if(!solveFrameByPnP(R_initial, P_initial, i, sfm_f))
            return false;
        c_Rotation[i] = R_initial;
        c_Translation[i] = P_initial;
        c_Quat[i] = c_Rotation[i];
        Pose[i].block<3, 3>(0, 0) = c_Rotation[i]; //相对当前帧[frame_num-1]的旋转
        Pose[i].block<3, 1>(0, 3) = c_Translation[i];//相对当前帧[frame_num-1]的平移
    }
    //G_ Step1: 先三角化第 l 帧(参考帧)与第 frame_num-1 帧(当前帧)的路标点
    // triangulate point based on the solve pnp result
    // 根据PnP的结果,参考与当前帧进行三角化处理
    triangulateTwoFrames(i, Pose[i], frame_num - 1, Pose[frame_num - 1], sfm_f);
}

第三步:对第 l 帧与 l+1 l+2 ... frame_num -2 的每一帧再进行三角化

考虑到前两步执行之后,会有些特征点不被最后一帧(当前帧)看到,因此,这一步将参考帧与之后的帧再次进行三角化。418133364418133364418133364

image-20220701171143259

for (int i = l + 1; i < frame_num - 1; i++)
    triangulateTwoFrames(l, Pose[l], i, Pose[i], sfm_f);

第四步:PNP 求解参考帧到从第 l-1 到第 0 帧的每一帧之间的变换矩阵,并进行三角化

image-20220701171158992

for (int i = l - 1; i >= 0; i--)
{
    //solve pnp
    // 这种情况就是最后一帧先求解出来
    Matrix3d R_initial = c_Rotation[i + 1];
    Vector3d P_initial = c_Translation[i + 1];
    if(!solveFrameByPnP(R_initial, P_initial, i, sfm_f))
        return false;
    c_Rotation[i] = R_initial;
    c_Translation[i] = P_initial;
    c_Quat[i] = c_Rotation[i];
    Pose[i].block<3, 3>(0, 0) = c_Rotation[i];
    Pose[i].block<3, 1>(0, 3) = c_Translation[i];
    //triangulate
    triangulateTwoFrames(i, Pose[i], l, Pose[l], sfm_f);
}

第五步:三角化其他未恢复的特征点。至此得到了滑动窗口中所有图像帧的位姿以及特征点的 3d 坐标

image-20220701171208947

for (int j = 0; j < feature_num; j++)
    {
        if (sfm_f[j].state == true)
            continue;
        if ((int)sfm_f[j].observation.size() >= 2)
        {
            Vector2d point0, point1;
            int frame_0 = sfm_f[j].observation[0].first;
            point0 = sfm_f[j].observation[0].second;
            int frame_1 = sfm_f[j].observation.back().first;
            point1 = sfm_f[j].observation.back().second;
            Vector3d point_3d;
            triangulatePoint(Pose[frame_0], Pose[frame_1], point0, point1, point_3d);
            sfm_f[j].state = true;
            sfm_f[j].position[0] = point_3d(0);
            sfm_f[j].position[1] = point_3d(1);
            sfm_f[j].position[2] = point_3d(2);
            //cout << "trangulated : " << frame_0 << " " << frame_1 << "  3d point : "  << j << "  " << point_3d.transpose() << endl;
        }        
    }

第六步:使用 ceres 进行全局 BA 优化

前面六步都通过三角化以及求解PnP的方式,得到一些3d点以及每一帧的位姿,这一步使用ceres对全局进行一次优化,使得所得到的3d点以及位姿更加准确。

在高博的《视觉SLAM十四讲》中,提到PnP中BA问题的求解,这里是个人参考十四讲记录的笔记:ch7_求解PnP中的BA问题,之后可以参考:VSLAM中的非线性优化了解整个非线性优化的过程。

这里需要逐一几个问题:

  1. ceres中待优化的参数是使用 double数组进行维护的,因此需要将待优化参数转换为 double数组
// 平移
c_translation[i][0] = c_Translation[i].x();
c_translation[i][1] = c_Translation[i].y();
c_translation[i][2] = c_Translation[i].z();
// 旋转四元数
c_rotation[i][0] = c_Quat[i].w();
c_rotation[i][1] = c_Quat[i].x();
c_rotation[i][2] = c_Quat[i].y();
c_rotation[i][3] = c_Quat[i].z();
  1. 四元数的加法是广义的加法,因此需要进行局部参数化
// 局部参数化,定义四元数的特殊加法
ceres::LocalParameterization* local_parameterization = new ceres::QuaternionParameterization();
problem.AddParameterBlock(c_rotation[i], 4, local_parameterization); // 这里使用local_parameterization声明这是旋转四元数的广义的加法
problem.AddParameterBlock(c_translation[i], 3);
  1. 由于视觉SLAM存在零空间漂移的问题,因此需要 fix 一些帧吗,这里 fix 的是参考帧的旋转和平移,以及当前帧的平移
// 由于时单目视觉SLAM,有七个自由度不可观,因此,fix(固定)一些参数块,避免在零空间出现漂移
// fix设置的世界坐标系地l帧的位姿,同时fix最后一帧的位移用来fix尺度
if (i == l)
{
    problem.SetParameterBlockConstant(c_rotation[i]); // 参考帧l的旋转不参与优化
}
if (i == l || i == frame_num - 1)
{
    problem.SetParameterBlockConstant(c_translation[i]);// 参考帧平移以及最后一帧的旋转不参与优化
}
  1. 构建BA的时候,由于都存在视觉重投影误差约束,因此可以直接遍历所有路标点
// 只有视觉重投影构成的约束,因此遍历所有的特征点,构建约束
for (int i = 0; i < feature_num; i++) // 遍历所有的路标点
{
    if (sfm_f[i].state != true) // 必须经过三角化
        continue;
    // 遍历所有观测帧,对这些帧建立约束
    for (int j = 0; j < int(sfm_f[i].observation.size()); j++) //每一个路标点被观测的所有帧
    {
        int l = sfm_f[i].observation[j].first;    // 路标点被观测帧的id
        ceres::CostFunction* cost_function = ReprojectionError3D::Create(
            sfm_f[i].observation[j].second.x(),  //路标点被观测帧的归一化坐标
            sfm_f[i].observation[j].second.y());
        // 约束了这一帧位姿和3d坐标点
        // AddResidualBlock: 代价函数模块、损失函数模块和参数模块
        problem.AddResidualBlock(cost_function, NULL, c_rotation[l], c_translation[l], 
                                 sfm_f[i].position);     
    }

}
  1. 这里的优化使用的是ceres的自动求导,自动求导要求使用者自定义一个仿函数,其实是一个类,只不过这个类的作用像函数,所以叫仿函数。原理就是类实现了operator()函数。该函数的作用就是告诉ceres怎么求,然后ceres根据用户需求求出最终的雅可比。

image-20220708163843928

struct ReprojectionError3D
{
    ReprojectionError3D(double observed_u, double observed_v)
        : observed_u(observed_u), observed_v(observed_v)
    {
    }
    // 定义仿函数
    template <typename T>
    bool operator()(const T *const camera_R, const T *const camera_T, const T *point, T *residuals) const
    {
        T p[3];
        ceres::QuaternionRotatePoint(camera_R, point, p); // 旋转这个点
        // Rcw * Pw + tcw
        p[0] += camera_T[0];
        p[1] += camera_T[1];
        p[2] += camera_T[2];
        // 归一化处理
        T xp = p[0] / p[2];
        T yp = p[1] / p[2];
        // 跟现在的观测形成残差
        residuals[0] = xp - T(observed_u);
        residuals[1] = yp - T(observed_v);
        return true;
    }
    // 构造CostFunction
    static ceres::CostFunction *Create(const double observed_x,
                                       const double observed_y)
    {
        return (new ceres::AutoDiffCostFunction<
                ReprojectionError3D, 2, 4, 3, 3>(
            new ReprojectionError3D(observed_x, observed_y)));
    }

    double observed_u;
    double observed_v;
};

AutoDiffCostFunction的模板参数:

  • 第1个参数是仿函数ReprojectionError3D
  • 第2个参数是残差块中残差的数量,这里指的就是归一化坐标
  • 第3个参数是第一个参数块中参数的数量,4表示camera_R的长度,3 表示位移camera_T的长度,3表示 3D点的长度
  1. 进行ceres 自动求导
ceres::Solver::Options options;
options.linear_solver_type = ceres::DENSE_SCHUR;
//options.minimizer_progress_to_stdout = true;
options.max_solver_time_in_seconds = 0.2; // 最大求解时间
ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);

四、对所有图像帧进行PnP

通过三维重建的操作,得到了窗口中所有图像帧的位姿 QT(相对于第 l 帧)和特征点坐标 sfm_tracked_points。至此,对滑窗中的关键帧的位姿以及3D点坐标进行了求解,但是,初始化由于需要所有帧的参与,因此还需要对所有帧all_image_frame(包括非关键帧)进行PnP 的求取,求取的方式就是根据之前求取的关键帧的位姿来得到。

image-20220708172915407

// 下面通过KF来求解其他的非KF的位姿
map<double, ImageFrame>::iterator frame_it;
map<int, Vector3d>::iterator it;
frame_it = all_image_frame.begin();
// i代表跟这个帧最近的KF索引
for (int i = 0; frame_it != all_image_frame.end(); frame_it++) // 遍历所有帧
{
    // provide initial guess
    cv::Mat r, rvec, t, D, tmp_r;
    // 如果时间戳一致,该帧本来就是KF,因此可以直接得到位姿
    if ((frame_it->first) == Headers[i].stamp.toSec()) 
    {
        frame_it->second.is_key_frame = true;// 是否是关键帧标志位职true
        frame_it->second.R = Q[i].toRotationMatrix() * RIC[0].transpose(); // 得到Rwi
        frame_it->second.T = T[i];  // 初始化不顾及平移外参
        i++;
        continue;
    }
    // 如果时间戳大于关键帧,则寻找下一个关键帧,即非关键帧使用时间戳小于自己的关键帧的位姿
    if ((frame_it->first) > Headers[i].stamp.toSec())
    {
        i++;
    }

    // Twc->Tcw
    // 注意这里的 Q和 T是图像帧的位姿,而不是求解PNP时所用的坐标系变换矩阵,两者具有对称关系
    Matrix3d R_inital = (Q[i].inverse()).toRotationMatrix();
    Vector3d P_inital = -R_inital * T[i];
    cv::eigen2cv(R_inital, tmp_r);
    cv::Rodrigues(tmp_r, rvec);//罗德里格斯公式将旋转矩阵转换成旋转向量
    cv::eigen2cv(P_inital, t);

    frame_it->second.is_key_frame = false;

    //获取 pnp需要用到的存储每个特征点三维点和图像坐标的 vector
    vector<cv::Point3f> pts_3_vector;
    vector<cv::Point2f> pts_2_vector;
    for (auto &id_pts : frame_it->second.points)
    {
        int feature_id = id_pts.first;
        for (auto &i_p : id_pts.second)
        {
            it = sfm_tracked_points.find(feature_id);
            if (it != sfm_tracked_points.end())
            {
                // 取出3D点
                Vector3d world_pts = it->second;
                cv::Point3f pts_3(world_pts(0), world_pts(1), world_pts(2));
                pts_3_vector.push_back(pts_3);
                // 取出2D点
                Vector2d img_pts = i_p.second.head<2>();
                cv::Point2f pts_2(img_pts(0), img_pts(1));
                pts_2_vector.push_back(pts_2);
            }
        }
    }
    //保证特征点数大于 5
    cv::Mat K = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
    if (pts_3_vector.size() < 6)
    {
        cout << "pts_3_vector size " << pts_3_vector.size() << endl;
        ROS_DEBUG("Not enough points for solve pnp !");
        return false;
    }
    /** 
         *bool cv::solvePnP(   求解 pnp问题
         *   InputArray  objectPoints,   特征点的3D坐标数组
         *   InputArray  imagePoints,    特征点对应的图像坐标
         *   InputArray  cameraMatrix,   相机内参矩阵
         *   InputArray  distCoeffs,     失真系数的输入向量
         *   OutputArray     rvec,       旋转向量
         *   OutputArray     tvec,       平移向量
         *   bool    useExtrinsicGuess = false, 为真则使用提供的初始估计值
         *   int     flags = SOLVEPNP_ITERATIVE 采用LM优化
         *)   
         */
    if (!cv::solvePnP(pts_3_vector, pts_2_vector, K, D, rvec, t, 1))
    {
        ROS_DEBUG("solve pnp fail!");
        return false;
    }
    // cv->eigen 同时Tcw -> Twc
    cv::Rodrigues(rvec, r);
    MatrixXd R_pnp, tmp_R_pnp;
    cv::cv2eigen(r, tmp_R_pnp);

    // Twc -> Twi
    //这里也同样需要将坐标变换矩阵转变成图像帧位姿,并转换为IMU坐标系的位姿
    R_pnp = tmp_R_pnp.transpose(); 
    MatrixXd T_pnp;
    cv::cv2eigen(t, T_pnp);
    T_pnp = R_pnp * (-T_pnp);
    frame_it->second.R = R_pnp * RIC[0].transpose();
    frame_it->second.T = T_pnp;
}
🧐 本文作者:
😉 本文链接:https://lukeyalvin.site/archives/80.html
😊 版权说明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
最后修改:2022 年 07 月 08 日
赏杯咖啡