VINS-Mono 代碼詳細解讀——feature_manager.cpp

LZ發現estimator.cpp中關鍵函數爲processImage(),裏面包含了IMU預積分、圖像處理特徵點跟蹤等一系列流程,上一節中對processIMU()以及預積分的integrationBase類進行解讀,本節繼續做基礎儲備,對與estimator.cpp中的feature_manager.cpp進行詳細介紹,主要是對特徵點管理。特徵點管理器主要就是FeatureManager類

目錄

  1. feature_manager.h中三個類FeatureManager、FeaturePerId、FeaturePerFrame的關係
  2. 類的成員函數
函數名 功能
FeaturePerId::endFrame() 返回最後一個觀測到這個特徵點的圖像幀ID
FeatureManager()::getFeatureCount() 窗口中被跟蹤的特徵點數量
FeatureManager()::addFeatureCheckParallax() 特徵點進入時檢查視差,是否爲關鍵幀
FeatureManager()::getCorresponding() 前後兩幀之間匹配特徵點3D座標
FeatureManager()::setDepth() 設置特徵點逆深度
FeatureManager()::triangulate() 特徵點三角化求深度(SVD分解)
FeatureManager()::removeOutlier() 移除外點
FeatureManager()::removeBackShiftDepth()  
FeatureManager()::removeBack() 邊緣化最老幀,直接將特徵點保存的幀號前移
FeatureManager()::removeFront() 邊緣化次新幀,對特徵點在次新幀的信息移除

一、FeatureManager、FeaturePerId、FeaturePerFrame

 f_manager是特徵管理器類的對象

FeatureManager f_manager;//特徵管理器類

這裏f_mangager.feature主要指的是一個list容器

feature_manager.h主要三個類:

FeatureManager管理所有特徵點,通過list容器存儲特徵點屬性

FeaturePerId指的是某feature_id下的所有FeaturePerFrame。常用feature_id和觀測第一幀start_frame、最後一幀endFrame()

FeaturePerFrame指的是每幀基本的數據:特徵點[x,y,z,u,v,vx,vy]和td IMU與cam同步時間差

在這裏插入圖片描述

FeaturePerId,某feature_id下的所有FeaturePerFrame 

feature_id 特徵點ID、start_frame 出現該角點的第一幀的id--start_frame

class FeaturePerId
{
  public:
    const int feature_id;// 特徵點ID索引
    int start_frame;// 首次被觀測到時,該幀的索引
    vector<FeaturePerFrame> feature_per_frame; // 能夠觀測到某個特徵點的所有相關幀

    int used_num;// 該特徵出現的次數
    bool is_outlier;// 是否外點
    bool is_margin;// 是否Marg邊緣化
    double estimated_depth; // 估計的逆深度
    int solve_flag; // 求解器 0 haven't solve yet; 1 solve succ; 2 solve fail;

    Vector3d gt_p; // ???

    FeaturePerId(int _feature_id, int _start_frame) 
        : feature_id(_feature_id), start_frame(_start_frame),
          used_num(0), estimated_depth(-1.0), solve_flag(0)
    {
    }

    int endFrame();// 返回最後一次觀測到這個特徵點的幀數ID
};

 FeaturePerFrame

 _point 每幀的特徵點[x,y,z,u,v,vx,vy], td IMU和cam同步時間差

class FeaturePerFrame
{
  public:
    FeaturePerFrame(const Eigen::Matrix<double, 7, 1> &_point, double td)
    {
        point.x() = _point(0);
        point.y() = _point(1);
        point.z() = _point(2);
        uv.x() = _point(3);
        uv.y() = _point(4);
        velocity.x() = _point(5); 
        velocity.y() = _point(6); 
        cur_td = td;
    }
    double cur_td;
    Vector3d point;
    Vector2d uv;
    Vector2d velocity;

    double z; // 特徵點的深度
    bool is_used;// 是否被用了
    double parallax;// 視差
    MatrixXd A; //變換矩陣
    VectorXd b;
    double dep_gradient; // ???
};

三者串聯最好的例子是:從f_manager到it_per_id再到底層的it_per_frame,就可以得到基本數據point了

for (auto &it_per_id : f_manager.feature)
{
   ......
   for (auto &it_per_frame : it_per_id.feature_per_frame)
   {
     Vector3d pts_j = it_per_frame.point;// 3D特徵點座標
 }
}

class FeatureManager中 list<FeaturePerId> feature;// 重要!! 通過FeatureManager可以得到滑動窗口內所有的角點信息

class FeaturePerId中 vector<FeaturePerFrame> feature_per_frame; // 能夠觀測到某個特徵點的所有相關幀


 二、主要函數

函數名 功能
FeaturePerId::endFrame() 返回最後一個觀測到這個特徵點的圖像幀ID
FeatureManager()::getFeatureCount() 窗口中被跟蹤的特徵點數量
FeatureManager()::addFeatureCheckParallax() 特徵點進入時檢查視差,是否爲關鍵幀
FeatureManager()::getCorresponding() 前後兩幀之間匹配特徵點3D座標
FeatureManager()::setDepth() 設置特徵點逆深度
FeatureManager()::triangulate() 特徵點三角化求深度(SVD分解)
FeatureManager()::removeOutlier() 移除外點
FeatureManager()::removeBackShiftDepth()  
FeatureManager()::removeBack() 邊緣化最老幀,直接將特徵點保存的幀號前移
FeatureManager()::removeFront() 邊緣化次新幀,對特徵點在次新幀的信息移除

endFrame()

返回最後一個觀測到這個特徵點的圖像幀ID

int FeaturePerId::endFrame()
{
    return start_frame + feature_per_frame.size() - 1;
}

其中,feature_per_frame的數量爲vector容器中,FeaturePerFrame基本類的數量。代表能夠觀測到某個特徵點的所有幀。

vector<FeaturePerFrame> feature_per_frame; // 能夠觀測到某個特徵點的所有相關幀

getFeatureCount()

窗口中被跟蹤特徵點的數量。

標準:該特徵點被兩幀以上觀測到了,且第一次觀測到的幀數不是在最後面。

int FeatureManager::getFeatureCount()
{
    int cnt = 0;
    for (auto &it : feature)// 遍歷feature
    {

        it.used_num = it.feature_per_frame.size(); // 所有特徵點被觀測到的幀數
        // 如果該特徵點有兩幀以上觀測到了  且第一次觀測到幀數不是在最後
        if (it.used_num >= 2 && it.start_frame < WINDOW_SIZE - 2)
        {
            cnt++;// 這個特徵點是有效的
        }
    }
    return cnt;
}

addFeatureCheckParallax()

爲什麼要檢查視差?

VINS中爲了控制優化計算量,只對當前幀之前某一部分幀進行優化,而不是全部歷史幀,局部優化幀數量的大小就是窗口大小。

爲了維持窗口大小,需要去除舊幀添加新幀,也就是邊緣化Marginalization。到底是刪去最舊的幀(MARGIN_OLD)還是刪去剛剛進來窗口倒數第二幀(MARGIN_SECOND_NEW),就需要對當前幀與之前幀進行視差比較,如果是當前幀變化很小,就會刪去倒數第二幀,如果變化很大,就刪去最舊的幀

通過檢測兩幀之間的視差以及特徵點數量決定 次新幀是否作爲關鍵幀

關鍵幀選取

1、當前幀相對最近的關鍵幀的特徵平均視差大於一個閾值就爲關鍵幀(因爲視差可以根據平移和旋轉共同得到,而純旋轉則導致不能三角化成功,所以這一步需要IMU預積分進行補償)

2、當前幀跟蹤到的特徵點數量小於閾值視爲關鍵幀

輸入的是特徵點,但是會把能觀測到這個特徵點的所有幀也都放進去,第一個索引是特徵點ID,第二個索引是觀測到該特徵點的相機幀 ID。

先把特徵點從image放入feature的list容器中,計算每一個點跟蹤次數小於閾值 和 它在次新幀和次次新幀間所有特徵點的平均視差大於閾值,返回是否是關鍵幀

bool FeatureManager::addFeatureCheckParallax(int frame_count, const map<int, vector<pair<int, Eigen::Matrix<double, 7, 1>>>> &image, double td)
{
    ROS_DEBUG("input feature: %d", (int)image.size());// 特徵點數量
    ROS_DEBUG("num of feature: %d", getFeatureCount()); // 能夠作爲特徵點的數量
    double parallax_sum = 0;// 所有特徵點視差總和
    int parallax_num = 0;
    last_track_num = 0;// 被跟蹤的個數

    // 1. 把image map中的所有特徵點放入feature list容器中
    // 遍歷特徵點,看該特徵點是否在特徵點的列表中,如果沒在,則將<FeatureID,Start_frame>存入到Feature列表中;否則統計數目
    for (auto &id_pts : image)// 遍歷所有特徵點
    {
        FeaturePerFrame f_per_fra(id_pts.second[0].second, td);// _point 每幀的特徵點[x,y,z,u,v,vx,vy]和td IMU和cam同步時間差

        // 1.1迭代器尋找feature list中是否有這feature_id
        int feature_id = id_pts.first;// 特徵點ID
        // 第三個參數是 Lambda表達式
        auto it = find_if(feature.begin(), feature.end(), [feature_id](const FeaturePerId &it)
                          {
            return it.feature_id == feature_id;
                          });

        // 1.2 如果沒有則新建一個,並在feature管理器的list容器最後添加:FeaturePerId、FeaturePerFrame
        if (it == feature.end())
        {
            feature.push_back(FeaturePerId(feature_id, frame_count));// (特徵點ID,首次觀測到特徵點的圖像幀ID)
            feature.back().feature_per_frame.push_back(f_per_fra);
        }
        // 1.3 之前有的話在FeaturePerFrame添加此特徵點在此幀的位置和其他信息,並統計數目。
        else if (it->feature_id == feature_id)
        {
            it->feature_per_frame.push_back(f_per_fra);
            last_track_num++; // 此幀有多少相同特徵點被跟蹤
        }
    }

    // 2. 追蹤次數小於20或者窗口內幀的數目小於2,是關鍵幀
    if (frame_count < 2 || last_track_num < 20)
        return true;

    // 3.計算每個特徵在次新幀和次次新幀中的視差
    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)
        {
            // 總視差:該特徵點在兩幀的歸一化平面上的座標點的距離ans
            parallax_sum += compensatedParallax2(it_per_id, frame_count);
            parallax_num++;// 個數
        }
    }
    // 4.1 第一次加進去的,是關鍵幀
    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);
        // 4.2 平均視差大於閾值的是關鍵幀
        return parallax_sum / parallax_num >= MIN_PARALLAX;
    }
}

其中,find_if函數可以參考 C++ STL find和find_if。意爲:遍歷feature list 容器看看之前是否出現過當前的feature_id

compensatedParallax2()

計算某個特徵點it_per_id在次新幀和次次新幀的視差ans

判斷觀測到該特徵點的frame中倒數第二幀和倒數第三幀的共視關係 實際是求取該特徵點在兩幀的歸一化平面上的座標點的距離ans

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;// 3D路標點(倒數第二幀j)
    double u_j = p_j(0);
    double v_j = p_j(1);

    Vector3d p_i = frame_i.point;// 3D路標點(倒數第三幀i)
    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;
}

其中,獲取倒數第三幀方式爲:

const FeaturePerFrame &frame_i = it_per_id.feature_per_frame[frame_count - 2 - it_per_id.start_frame];// 倒數第三幀

 getCorresponding()

得到給定兩幀之間的對應特徵點3D座標

vector<pair<Vector3d, Vector3d>> FeatureManager::getCorresponding(int frame_count_l, int frame_count_r)
{
    vector<pair<Vector3d, Vector3d>> corres;
    for (auto &it : feature)// 遍歷feature的list容器
    {
        // 要找特徵點的兩幀在窗口範圍內,可以直接取。窗口爲:觀測到當前特徵點的所有圖像幀
        if (it.start_frame <= frame_count_l && it.endFrame() >= frame_count_r)
        {
            Vector3d a = Vector3d::Zero(), b = Vector3d::Zero();
            int idx_l = frame_count_l - it.start_frame;// 當前幀-第一次觀測到特徵點的幀數
            int idx_r = frame_count_r - it.start_frame;

            a = it.feature_per_frame[idx_l].point;
            b = it.feature_per_frame[idx_r].point;
            
            corres.push_back(make_pair(a, b));
        }
    }
    return corres;
}

setDepth()

設置特徵點的逆深度估計值

void FeatureManager::setDepth(const VectorXd &x)
{
    int feature_index = -1;// 先給feature ID賦值-1
    for (auto &it_per_id : feature)// 遍歷所有特徵點
    {
        // 至少兩幀觀測得到這個特徵點  且  首次觀測到該特徵點的圖像幀在滑動窗範圍內
        it_per_id.used_num = it_per_id.feature_per_frame.size();// 能夠觀測到某個特徵點的所有相關幀數目
        if (!(it_per_id.used_num >= 2 && it_per_id.start_frame < WINDOW_SIZE - 2))
            continue;

        // 求解逆深度
        it_per_id.estimated_depth = 1.0 / x(++feature_index);
        //ROS_INFO("feature id %d , start_frame %d, depth %f ", it_per_id->feature_id, it_per_id-> start_frame, it_per_id->estimated_depth);
        // 深度小於0估計失敗
        if (it_per_id.estimated_depth < 0)
        {
            it_per_id.solve_flag = 2;//失敗估計
        }
        else
            it_per_id.solve_flag = 1;//成功估計
    }
}

問題:

// 求解逆深度

it_per_id.estimated_depth = 1.0 / x(++feature_index);

最後乘上的(++feature_index)是做什麼用的?

trangulate()

對特徵點進行三角化求深度(SVD分解)

void FeatureManager::triangulate(Vector3d Ps[], Vector3d tic[], Matrix3d ric[])
{
    for (auto &it_per_id : feature)
    {
        // 需要至少兩幀觀測到該特徵點 且 首次觀測到特徵點的幀不是倒數第三幀
        it_per_id.used_num = it_per_id.feature_per_frame.size();
        if (!(it_per_id.used_num >= 2 && it_per_id.start_frame < WINDOW_SIZE - 2))
            continue;

        if (it_per_id.estimated_depth > 0)
            continue;

        int imu_i = it_per_id.start_frame, imu_j = imu_i - 1;

        ROS_ASSERT(NUM_OF_CAM == 1);
        Eigen::MatrixXd svd_A(2 * it_per_id.feature_per_frame.size(), 4);
        int svd_idx = 0;

        //R0 t0爲第i幀相機座標系到世界座標系的變換矩陣Rwc
        Eigen::Matrix<double, 3, 4> P0;
        Eigen::Vector3d t0 = Ps[imu_i] + Rs[imu_i] * tic[0];
        Eigen::Matrix3d R0 = Rs[imu_i] * ric[0];
        P0.leftCols<3>() = Eigen::Matrix3d::Identity();
        P0.rightCols<1>() = Eigen::Vector3d::Zero();

        for (auto &it_per_frame : it_per_id.feature_per_frame)//遍歷
        {
            imu_j++;
            //R t爲第j幀相機座標系到第i幀相機座標系的變換矩陣,P爲i到j的變換矩陣
            Eigen::Vector3d t1 = Ps[imu_j] + Rs[imu_j] * tic[0];
            Eigen::Matrix3d R1 = Rs[imu_j] * ric[0];
            Eigen::Vector3d t = R0.transpose() * (t1 - t0);
            Eigen::Matrix3d R = R0.transpose() * R1;
            Eigen::Matrix<double, 3, 4> P;
            P.leftCols<3>() = R.transpose();
            P.rightCols<1>() = -R.transpose() * t;
            Eigen::Vector3d f = it_per_frame.point.normalized();
            //P = [P1 P2 P3]^T 
            //AX=0      A = [A(2*i) A(2*i+1) A(2*i+2) A(2*i+3) ...]^T
            //A(2*i)   = x(i) * P3 - z(i) * P1
            //A(2*i+1) = y(i) * P3 - z(i) * P2
            svd_A.row(svd_idx++) = f[0] * P.row(2) - f[2] * P.row(0);
            svd_A.row(svd_idx++) = f[1] * P.row(2) - f[2] * P.row(1);

            if (imu_i == imu_j)
                continue;
        }
        //對A的SVD分解得到其最小奇異值對應的單位奇異向量(x,y,z,w),深度爲z/w
        ROS_ASSERT(svd_idx == svd_A.rows());
        Eigen::Vector4d svd_V = Eigen::JacobiSVD<Eigen::MatrixXd>(svd_A, Eigen::ComputeThinV).matrixV().rightCols<1>();
        double svd_method = svd_V[2] / svd_V[3];
        //it_per_id->estimated_depth = -b / A;
        //it_per_id->estimated_depth = svd_V[2] / svd_V[3];

        it_per_id.estimated_depth = svd_method;
        //it_per_id->estimated_depth = INIT_DEPTH;

        if (it_per_id.estimated_depth < 0.1)
        {
            it_per_id.estimated_depth = INIT_DEPTH;
        }

    }
}

三個邊緣化函數

removeBackShiftDepth()

邊緣化最老幀時,處理特徵點保存的幀號,將起始幀是最老幀的特徵點的深度值進行轉移

//marg_R、marg_P爲被邊緣化的位姿,new_R、new_P爲在這下一幀的位姿
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
            {
                //pts_i爲特徵點在最老幀座標系下的三維座標
                //w_pts_i爲特徵點在世界座標系下的三維座標
                //將其轉換到在下一幀座標系下的座標pts_j
                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;
            }
        }
        // remove tracking-lost feature after marginalize
        /*
        if (it->endFrame() < WINDOW_SIZE - 1)
        {
            feature.erase(it);
        }
        */
    }
}

removeBack()

邊緣化最老幀時,直接將特徵點所保存的幀號向前滑動

void FeatureManager::removeBack()
{
    for (auto it = feature.begin(), it_next = feature.begin();
         it != feature.end(); it = it_next)
    {
        it_next++;
        //如果特徵點起始幀號start_frame不爲零則減一
        if (it->start_frame != 0)
            it->start_frame--;
        //如果start_frame爲0則直接移除feature_per_frame的第0幀FeaturePerFrame
        //如果feature_per_frame爲空則直接刪除特徵點
        else
        {
            it->feature_per_frame.erase(it->feature_per_frame.begin());
            if (it->feature_per_frame.size() == 0)
                feature.erase(it);
        }
    }
}

removeFront()

邊緣化次新幀時,對特徵點在次新幀的信息進行移除處理

void FeatureManager::removeFront(int frame_count)
{
    for (auto it = feature.begin(), it_next = feature.begin(); it != feature.end(); it = it_next)
    {
        it_next++;
        //起始幀爲最新幀的滑動成次新幀
        if (it->start_frame == frame_count)
        {
            it->start_frame--;
        }
        else
        {
            int j = WINDOW_SIZE - 1 - it->start_frame;
            //如果次新幀之前已經跟蹤結束則什麼都不做
            if (it->endFrame() < frame_count - 1)
                continue;
            //如果在次新幀仍被跟蹤,則刪除feature_per_frame中次新幀對應的FeaturePerFrame
            //如果feature_per_frame爲空則直接刪除特徵點
            it->feature_per_frame.erase(it->feature_per_frame.begin() + j);
            if (it->feature_per_frame.size() == 0)
                feature.erase(it);
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章