VINS 细节系列 - Pnp (3D-2D)

一、理论部分

 

1. DLT,直接线性变换

注意:原版的DLT中无须已知摄像机内参(请参考 MVG
鉴于在大部分情况下,我们是已知摄像机内参的,因此这里的推导考虑了摄像机内参数。

 1.1 理论推导

3D参考点在世界坐标系下的齐次坐标记为

 2D投影点在图像坐标系下的齐次坐标记为

 相机的内参数为

 那么3D点到2D点投影可表示为

 

 

 

 

连接: https://blog.csdn.net/hltt3838/article/details/109464869

三、程序解析

void solvePnP(  objectPoints,   imagePoints,   cameraMatrix,   distCoeffs,   rvec,   tvec, false,  flags  )

objectPoints     -   世界坐标系下的特征点的坐标(注意
imagePoints     -   在图像坐标系下对应的特征点的坐标 (注意
cameraMatrix  -   相机的内参矩阵
distCoeffs          -   相机的畸变系数
rvec                      -   输出的旋转向量,使坐标点从世界坐标系旋转到相机坐标系
tvec                      -   输出的平移向量,使坐标点从世界坐标系平移到相机坐标系
flags                     -   默认使用CV_ITERATIV迭代法

程序入口在

       if (solver_flag == INITIAL)
    {
        //stereo + IMU initilization,双目初始化
        if(STEREO && USE_IMU)
        {
            f_manager.initFramePoseByPnP(frame_count, Ps, Rs, tic, ric);
            f_manager.triangulate(frame_count, Ps, Rs, tic, ric);

其中 f_manager.initFramePoseByPnP(frame_count, Ps, Rs, tic, ric) 如下:

  • frame_count:当前窗口帧数;
  • Ps,Rs: 世界坐标系下,当前 bk的位置 和姿态
  • tic, ric:相机坐标系和bk间的转换
void FeatureManager::initFramePoseByPnP(int frameCnt, Vector3d Ps[], Matrix3d Rs[], Vector3d tic[], Matrix3d ric[])
{

    if(frameCnt > 0)
    {
        vector<cv::Point2f> pts2D;
        vector<cv::Point3f> pts3D;
        
        for (auto &it_per_id : feature)        //遍历滑窗内所有的特征点
        {
            if (it_per_id.estimated_depth > 0) //满足特征点深度大于0
            {
                int index = frameCnt - it_per_id.start_frame;
                if((int)it_per_id.feature_per_frame.size() >= index + 1)
                {
                    Vector3d ptsInCam = ric[0] * (it_per_id.feature_per_frame[0].point * it_per_id.estimated_depth) + tic[0];
 
                    Vector3d ptsInWorld = Rs[it_per_id.start_frame] * ptsInCam + Ps[it_per_id.start_frame];

                    cv::Point3f point3d(ptsInWorld.x(), ptsInWorld.y(), ptsInWorld.z());
                    cv::Point2f point2d(it_per_id.feature_per_frame[index].point.x(), it_per_id.feature_per_frame[index].point.y());
                    
                    pts3D.push_back(point3d);
                    pts2D.push_back(point2d); 
                }
            }
        }
        
        Eigen::Matrix3d RCam;
        Eigen::Vector3d PCam;
        
        RCam = Rs[frameCnt - 1] * ric[0];
        PCam = Rs[frameCnt - 1] * tic[0] + Ps[frameCnt - 1];

        if(solvePoseByPnP(RCam, PCam, pts2D, pts3D))
        {
            // Rs[frameCnt] = Rw_c * Rb_c.transpose() = Rw_b
            Rs[frameCnt] =  RCam * ric[0].transpose(); 
            Ps[frameCnt] = -RCam * ric[0].transpose() * tic[0] + PCam;

            //对应的四元素
            Eigen::Quaterniond Q(Rs[frameCnt]);
        }
    }
}

 

对上面的程序进行一步步解析:

第一部分:

void FeatureManager::initFramePoseByPnP(int frameCnt, Vector3d Ps[], Matrix3d Rs[], Vector3d tic[], Matrix3d ric[])
{

    if(frameCnt > 0)
    {
        vector<cv::Point2f> pts2D;
        vector<cv::Point3f> pts3D;
        
        for (auto &it_per_id : feature)        //遍历滑窗内所有的特征点
        {
            if (it_per_id.estimated_depth > 0) //满足特征点深度大于0
            {
                int index = frameCnt - it_per_id.start_frame;
                if((int)it_per_id.feature_per_frame.size() >= index + 1)
                {
                    Vector3d ptsInCam = ric[0] * (it_per_id.feature_per_frame[0].point * it_per_id.estimated_depth) + tic[0];
                    Vector3d ptsInWorld = Rs[it_per_id.start_frame] * ptsInCam + Ps[it_per_id.start_frame];

                    cv::Point3f point3d(ptsInWorld.x(), ptsInWorld.y(), ptsInWorld.z());
                    cv::Point2f point2d(it_per_id.feature_per_frame[index].point.x(), it_per_id.feature_per_frame[index].point.y());
                    
                    pts3D.push_back(point3d);
                    pts2D.push_back(point2d); 
                }
            }
 
                    

该程序只有在滑动窗口中有图像才进入,对应  if(frameCnt > 0) ,废话!首先定义了两个 变量 pts2D, pts3D;  分别用于存放世界坐标系下的3D点和其对应的相机归一化坐标系下的2D点;整个过程就是图像帧一帧一帧的进来,会对周围特征点进行观测, 假如 滑动窗口大小为 10,当前进来了5帧;VINS的策略是当滑动窗口不满时,一直进来图像,提取特征点,三角化,这些帧都是看作关键帧,直到满足滑窗帧数;

 变量解释:

 frameCnt:当前滑动窗口中的帧数;

it_per_id.start_frame:特征点被第一次观测到时,对应滑动窗口的帧号

feature_per_frame.size() :表示观测到某个路标点 图像的个数

feature_per_frame[0].point: 某个路标点被第一张图像观测到的信息,所谓的第一张图片不一定是滑窗内的第一张

ric[0]:相机参考坐标系的点 到 对应的bk坐标系上点的转换 矩阵- Rbk_ck, 这个值在在线调节外参的时候就定了,不再变了!

Rb0_c0 = Rb1_c1 = Rb2_c2 ... Rbn_cn

tic[0]:  是两者的平移

Ps、Rs、Vs: bk相对 世界坐标系w 的 位置、姿态 和速度; k代表的是与bk对应的帧,记住啦!

这个图是我自己想的,也有可能不对!

 记住啦,图像是一帧一帧进来的!我们以第五帧 (C5)为讲解,假设对于特征点P5

it_per_id.feature_per_frame.size()  =  3 满足下面的条件

if((int)it_per_id.feature_per_frame.size() >= index + 1)

仔细想一下,为什么会有这个判断,再看看其他特征点,都会满足这个条件,那不是多此一举嘛?

但是我们的滑动窗口大小是10,而当前帧才是5,还会进入图像,都满足上面的判断,但是当滑动窗口满时,我们进来新的帧需要边缘化,上面的条件可能就不满足了,你们可以考虑一下!比如最新帧被边缘化调,那这个时候就没有必要求最新帧的PnP了,这个应该不难理解!

 注意啦!感觉这样处理的原因是因为 Opencv中 cv::solvePnP 这个函数,C5进来的时候我们观测到了P5,3D点便是前面帧获得的P5 世界坐标系的点,相对C3而言; 2D点 却不是C3中的坐标,而是C5观察到 P5 下的归一化坐标!然后根据已知的C3到世界坐标系w的转换矩阵,带入 Opencv中 cv::solvePnP  中,得到 C5到w的转换矩阵。

第二部分:

        Eigen::Matrix3d RCam;
        Eigen::Vector3d PCam;

        RCam = Rs[frameCnt - 1] * ric[0];
        PCam = Rs[frameCnt - 1] * tic[0] + Ps[frameCnt - 1];

       
  因为滑动窗口进来帧嘛,知道倒数第二帧 frameCnt - 1 相机坐标系的点 到 世界坐标系点 的 转换矩阵;

RCam = Rs[frameCnt - 1] * ric[0]  //  RCam = Rw_bk-1 * Rbk-1_ck-1 = Rw_ck-1
  想求 新进来帧的 Rs[frameCnt]、Ps[frameCnt]

第三部分:


        if(solvePoseByPnP(RCam, PCam, pts2D, pts3D))
        {
            // Rs[frameCnt] = Rw_c * Rb_c.transpose() = Rw_b
            Rs[frameCnt] =  RCam * ric[0].transpose(); 
            Ps[frameCnt] = -RCam * ric[0].transpose() * tic[0] + PCam;

            //对应的四元素
            Eigen::Quaterniond Q(Rs[frameCnt]);
        }
    }
}

solvePoseByPnP(RCam, PCam, pts2D, pts3D)  如下:

bool FeatureManager::solvePoseByPnP(Eigen::Matrix3d &R, Eigen::Vector3d &P, 
                                      vector<cv::Point2f> &pts2D, vector<cv::Point3f> &pts3D)
{
    Eigen::Matrix3d R_initial;
    Eigen::Vector3d P_initial;

    //坐标系之间的转换:w_T_cam ---> cam_T_w,注意和点之间转换的区别
    R_initial = R.inverse();
    P_initial = -(R_initial * P);

   
    if (int(pts2D.size()) < 4)
    {
        printf("feature tracking not enough, please slowly move you device! \n");
        return false;
    }
    cv::Mat r, rvec, t, D, tmp_r;
    cv::eigen2cv(R_initial, tmp_r);//cv::Mat 与 Eigen 转换
    cv::Rodrigues(tmp_r, rvec);    //罗德里格斯公式
    cv::eigen2cv(P_initial, t);     //cv::Mat 与 Eigen 转换
    
    cv::Mat K = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);  
    bool pnp_succ;
    pnp_succ = cv::solvePnP(pts3D, pts2D, K, D, rvec, t, 1);//用的是相似三角形方法!
   

    if(!pnp_succ)
    {
        printf("pnp failed ! \n");
        return false;
    }
    
    cv::Rodrigues(rvec, r);
    
     
    Eigen::MatrixXd R_pnp;
    cv::cv2eigen(r, R_pnp);
    Eigen::MatrixXd T_pnp;
    cv::cv2eigen(t, T_pnp);

    // cam_T_w ---> w_T_cam
    R = R_pnp.transpose();
    P = R * (-T_pnp);

    return true;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

他人是一面镜子,保持谦虚的态度

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值