longer95479@home:~$

[VINS-Fusion] Feature Manager


feature manager 的主要数据结构为:

class FeatureManager
    class FeaturePerId
        class FeaturePerFrame

FeaturePerFrame 包括某帧内某个路标被观测的信息,如归一化平面坐标及速度、像素坐标、是否是双目、时间偏移。

FeaturePerId 则主要包括 vector<FeaturePerFrame> feature_per_frame,以及 frame_idstart_frame 等路标点个体级别的信息。值得注意的是,vins假设特征点是被连续观测到的,因此只记录开始的帧,即可推断出 vector 内其他观测所属的帧。

  • 传入 start_frame的变量则是外部的 frameCntframeCnt 指明了滑窗内的哪一帧。

主体函数解释

bool FeatureManager::addFeatureCheckParallax(int frame_count, const map<int, vector<pair<int, Eigen::Matrix<double, 7, 1»» &image, double td)

输入包括:

  • frame_count :指明当前帧将会是滑窗内的哪一帧
  • image:map<int, vector<pair<int, Eigen::Matrix<double, 7, 1>>>> 的数据结构,记录了当前获得的新特征点集的信息,分级索引,分别是 id、相机id,信息则是点的二维几何信息。
  • td:与 imu 的时间戳延迟

可分为两大步骤:

  • 添加 image 到 list<FeaturePerId>类型的数据结构中,并作一些统计
  • 判断是否为关键帧

第一步:遍历输入 image 里的点,将其构造成 FeaturePerFrame 类型数据结构,判断该点是否被双目观测,若是则将右目信息也初始化进 FeaturePerFrame。判断 list<FeaturePerId>中是否有当前 image 的 id,不存在则 push 进新 id 并初始化对应的内存,存在则在对应 id 下新增二维几何信息等信息。

for (auto &id_pts : image)
    {
        FeaturePerFrame f_per_fra(id_pts.second[0].second, td);
        assert(id_pts.second[0].first == 0);
        if(id_pts.second.size() == 2)
        {
            f_per_fra.rightObservation(id_pts.second[1].second);
            assert(id_pts.second[1].first == 1);
        }

        int feature_id = id_pts.first;
        auto it = find_if(feature.begin(), feature.end(), [feature_id](const FeaturePerId &it)
                          {
            return it.feature_id == feature_id;
                          });

        if (it == feature.end())
        {
            feature.push_back(FeaturePerId(feature_id, frame_count));
            feature.back().feature_per_frame.push_back(f_per_fra);
            new_feature_num++;
        }
        else if (it->feature_id == feature_id)
        {
            it->feature_per_frame.push_back(f_per_fra);
            last_track_num++;
            if( it-> feature_per_frame.size() >= 4)
                long_track_num++;
        }
    }

第二步:若是窗口内前两帧,则直接视为关键帧;若追踪点数少于20个,视为关键帧;若连续4帧以上追踪的点少于 40 个,也视为关键帧;若新特征点数目大于追踪特征点的一半,亦认为是关键帧。

之后检查第二新和第三新的帧的特征点视差(前提是这个特征点有第二新和第三新的被观测帧),若平均视差大于阈值,则设置为关键帧。

20 和 40 是不是该调换一下,目前认为不太合理,因为 long_track_num <= last_track_num 是一定成立的(参考上个代码块的最后几行)。

如果面向一面白墙,一帧的特征点追踪个数始终小于阈值,则会被一直判定为关键帧,但这并不是我们的本意。我们的本意是,由于视角移动较大,因此会有一些被追踪的特征点消失在视野内,因此特征点追踪的数目下降,但新增的特征点会多一些。

    if (frame_count < 2 || 
        last_track_num < 20 || 
        long_track_num < 40 || 
        new_feature_num > 0.5 * last_track_num)
        return true;
    
    for (auto &it_per_id : feature)
    {
        if (it_per_id.start_frame <= frame_count - 2 &&
            it_per_id.start_frame + int(it_per_id.feature_per_frame.size()) - 1 >= frame_count - 1)
        {
            parallax_sum += compensatedParallax2(it_per_id, frame_count);
            parallax_num++;
        }
    }

    if (parallax_num == 0)
    {
        return true;
    }
    else
    {
        ROS_DEBUG("parallax_sum: %lf, parallax_num: %d", parallax_sum, parallax_num);
        ROS_DEBUG("current parallax: %lf", parallax_sum / parallax_num * FOCAL_LENGTH);
        last_average_parallax = parallax_sum / parallax_num * FOCAL_LENGTH;
        return parallax_sum / parallax_num >= MIN_PARALLAX;
    }

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

遍历特征点,若已有深度则不需要再进行三角,跳过该点。若无深度,则需要进行三角化,且无论双目还是单目,都使用了点被观测的第一帧坐标。若是单目,且特征点只有一个观测,无法三角化,因此也什么都不执行。

若是双目观测,则直接使用双目特征点三角化,不考虑追踪的时间长短

若是单目观测,且至少被观测两次,则用 start_frame 和 start_frame+1 这两帧的点作三角化(此时这两帧的 $P$ 也都已知了 ),也不考虑追踪的时间长短(追踪连续四帧及以上谓长)

代码的核心语句是 triangulatePoint(leftPose, rightPose, point0, point1, point3d);。而前后的代码都是由 imu_T_w 构造出 cam_T_w 作为该函数的输入。

void FeatureManager::triangulatePoint(Eigen::Matrix<double, 3, 4> &Pose0, Eigen::Matrix<double, 3, 4> &Pose1, Eigen::Vector2d &point0, Eigen::Vector2d &point1, Eigen::Vector3d &point_3d)

齐次坐标表示,等号本质上表示等价,成比例,并不是数值上的相等,因此一个点提供两个约束 $p_i$ 是 $P$ 的行,$P_{3\times 4}$ 是世界系到相机系的投影矩阵,到此函数已经把 2D 特征点表示在 归一化平面 了,因此 $P = K[R|t] = [R|t]$。列出投影的齐次方程: \(x = PX, x'=P'X\)

\(\begin{bmatrix} x\\ y\\ 1 \end{bmatrix} = \begin{bmatrix} p_1\\ p_2\\ p_3\\ \end{bmatrix} X , \begin{bmatrix} x'\\ y'\\ 1 \end{bmatrix} = \begin{bmatrix} p'_1\\ p'_2\\ p'_3\\ \end{bmatrix} X\) 得到真正的约束 \(x = \frac{p_1X}{p_3X}, y = \frac{p_2X}{p_3X}\) \(x' = \frac{p'_1X}{p'_3X}, y' = \frac{p'_2X}{p'_3X}\)

分母乘过去,合并同类项

\[(xp_3-p_1)X = 0\\ (yp_3-p_2)X = 0\\ (x'p'_3-p'_1)X = 0\\ (y'p'_3-p'_2)X = 0\\\]

写成矩阵形式

\[\begin{bmatrix} (xp_3-p_1)\\ (yp_3-p_2)\\ (x'p'_3-p'_1)\\ (y'p'_3-p'_2)\\ \end{bmatrix} X = 0\]

之后使用 SVD 方法求解最小二乘解,即最小奇异值对应的右奇异向量。

对应的代码如下:

void FeatureManager::triangulatePoint(Eigen::Matrix<double, 3, 4> &Pose0, Eigen::Matrix<double, 3, 4> &Pose1,
                        Eigen::Vector2d &point0, Eigen::Vector2d &point1, Eigen::Vector3d &point_3d)
{
    Eigen::Matrix4d design_matrix = Eigen::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);
    Eigen::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);
}

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

首先确保当前帧不是滑窗内的第一帧,因为单目的话,第一帧无法三角化点,也就无法 pnp 了。之后遍历所有特征点 id,选取有深度的点(已被三角化),并判断这个点的被观测帧是否包含当前帧,如果包含,则将其 3D 坐标由世界系转化到相机系,并分别将点在当前帧的 2D 坐标和 3D 坐标放入 vector 容器内。

之后将这些 2D-3D 点对传入函数 if(solvePoseByPnP(RCam, PCam, pts2D, pts3D)),点对数不够则 pnp 失败,返回 false,成功则放回 true,RcamPCam 会被赋值,表示世界系到相机系的变换,需要再作处理,得到机体系到世界系的变换矩阵。并且 pnp 求解过程会用到 R、t 的初始值,由 imu 提供。

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

核心语句是 pnp_succ = cv::solvePnP(pts3D, pts2D, K, D, rvec, t, 1);,其前后都是做一些合法判断、矩阵的变换。需要做一些矩阵变换的原因是,最外部输入和最终的输出,均是 w_T_cam(机体坐标相对于世界坐标,cam2w),而 opencv 的 pnp 函数需要的位姿代表的是 cam_T_w。

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

    //printf("pnp size %d \n",(int)pts2D.size() );
    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::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;
    pnp_succ = cv::solvePnP(pts3D, pts2D, K, D, rvec, t, 1);
    //pnp_succ = solvePnPRansac(pts3D, pts2D, K, D, rvec, t, true, 100, 8.0 / focalLength, 0.99, inliers);

    if(!pnp_succ)
    {
        printf("pnp failed ! \n");
        return false;
    }
    cv::Rodrigues(rvec, r);
    //cout << "r " << endl << r << endl;
    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;
}

void FeatureManager::removeBackShiftDepth(Eigen::Matrix3d marg_R, Eigen::Vector3d marg_P, Eigen::Matrix3d new_R, Eigen::Vector3d new_P)

滑窗进行边缘化后,特别是边缘化最旧的一帧时, 在内存上的体现是,依次将后序的内容移动到相邻的前一块,这意味着帧的序号发生了变化,此时也要对 feature_manager 里的 start_frame 进行更新。

因此以 id 索引遍历特征点,如果 start_frame 不是第一帧,则减 1;如果 start_frame 是第一帧,则丢弃该 2D 观测,并检查剩余的观测数量是否小于2,若小于则把该点抹去,否则为新的 start_frame 计算深度,方法为重投影:

\[X=P^{-1}x \\ x'=P'X\]
void FeatureManager::removeBackShiftDepth(Eigen::Matrix3d marg_R, Eigen::Vector3d marg_P, Eigen::Matrix3d new_R, Eigen::Vector3d new_P)
{
    for (auto it = feature.begin(), it_next = feature.begin();
         it != feature.end(); it = it_next)
    {
        it_next++;

        if (it->start_frame != 0)
            it->start_frame--;
        else
        {
            Eigen::Vector3d uv_i = it->feature_per_frame[0].point;  
            it->feature_per_frame.erase(it->feature_per_frame.begin());
            if (it->feature_per_frame.size() < 2)
            {
                feature.erase(it);
                continue;
            }
            else
            {
                Eigen::Vector3d pts_i = uv_i * it->estimated_depth;
                Eigen::Vector3d w_pts_i = marg_R * pts_i + marg_P;
                Eigen::Vector3d pts_j = new_R.transpose() * (w_pts_i - new_P);
                double dep_j = pts_j(2);
                if (dep_j > 0)
                    it->estimated_depth = dep_j;
                else
                    it->estimated_depth = INIT_DEPTH;
            }
        }
    }
}

double FeatureManager::compensatedParallax2(const FeaturePerId &it_per_id, int frame_count)

在 gpu 版本里冗余了,并没有实际计算补偿的归一化坐标,因此代码后半段是重复冗余计算的,可改进

补充说明一下补偿的思路:有 i, j 两帧,由于仅有旋转运动不利于对极几何求相对位姿,因此补偿的具体操作是把 j 的相机姿态对齐到 i,然后计算出此时的特征点坐标如何,再与上一帧的特征点坐标做差。

有了补偿,考虑两个极端情况:

  • 只有旋转,此时补偿后,特征点在两帧里基本没有移动
  • 只有平移,此时补偿后与补偿前的结果一致
    double FeatureManager::compensatedParallax2(const FeaturePerId &it_per_id, int frame_count)
    {
      //check the second last frame is keyframe or not
      //parallax betwwen seconde last frame and third last frame
      const FeaturePerFrame &frame_i = it_per_id.feature_per_frame[frame_count - 2 - it_per_id.start_frame];
      const FeaturePerFrame &frame_j = it_per_id.feature_per_frame[frame_count - 1 - it_per_id.start_frame];
    
      double ans = 0;
      Vector3d p_j = frame_j.point;
    
      double u_j = p_j(0);
      double v_j = p_j(1);
    
      Vector3d p_i = frame_i.point;
      Vector3d p_i_comp;
    
      //int r_i = frame_count - 2;
      //int r_j = frame_count - 1;
      //p_i_comp = ric[camera_id_j].transpose() * Rs[r_j].transpose() * Rs[r_i] * ric[camera_id_i] * p_i;
      p_i_comp = p_i;
      double dep_i = p_i(2);
      double u_i = p_i(0) / dep_i;
      double v_i = p_i(1) / dep_i;
      double du = u_i - u_j, dv = v_i - v_j;
    
      double dep_i_comp = p_i_comp(2);
      double u_i_comp = p_i_comp(0) / dep_i_comp;
      double v_i_comp = p_i_comp(1) / dep_i_comp;
      double du_comp = u_i_comp - u_j, dv_comp = v_i_comp - v_j;
    
      ans = max(ans, sqrt(min(du * du + dv * dv, du_comp * du_comp + dv_comp * dv_comp)));
    
      return ans;
    }
    
Total views. cowboys. Hits