【SLAM】VINS-MONO解析——初始化(代码部分)

2023-05-16

6.2 代码解析

在这里插入图片描述这部分代码在estimator::processImage()最后面。初始化部分的代码虽然生命周期比较短,但是,代码量巨大!主要分成2部分,第一部分是纯视觉SfM优化滑窗内的位姿,然后在融合IMU信息,按照理论部分优化各个状态量。

    if (solver_flag == INITIAL)//进行初始化
    {
        if (frame_count == WINDOW_SIZE)
        {
            bool result = false;
            if( ESTIMATE_EXTRINSIC != 2 && (header.stamp.toSec() - initial_timestamp) > 0.1)
            {//确保有足够的frame参与初始化,有外参,且当前帧时间戳大于初始化时间戳+0.1秒
               result = initialStructure();//执行视觉惯性联合初始化
               initial_timestamp = header.stamp.toSec();//更新初始化时间戳
            }
            if(result)//初始化成功则进行一次非线性优化
            {
                solver_flag = NON_LINEAR;//进行非线性优化
                solveOdometry();//执行非线性优化具体函数solveOdometry()
                slideWindow();
                f_manager.removeFailures();
                ROS_INFO("Initialization finish!");
                last_R = Rs[WINDOW_SIZE];//得到当前帧与第一帧的位姿
                last_P = Ps[WINDOW_SIZE];
                last_R0 = Rs[0];
                last_P0 = Ps[0];                
            }
            else
                slideWindow();//不成功则进行滑窗操作
        }
        else
       //TODO 再看看这个值是怎么变的
            ;//图像帧数量+1
    }



6.3 initialStructure()

这是一个相当大的函数,而且多层套娃。而且原理上讲的初始化,包括纯视觉SfM,视觉和IMU的松耦合,都是在这个部分里面。
在这里插入图片描述

6.3.1 确保IMU有足够的excitation(可选)

这一部分的思想就是通过计算滑窗内所有帧的线加速度的标准差,判断IMU是否有充分运动激励,以判断是否进行初始化。

一上来就出现all_image_frame这个数据结构,见5.2-2,它包含了滑窗内所有帧的视觉和IMU信息,它是一个hash,以时间戳为索引。

(1)第一次循环,求出滑窗内的平均线加速度

Vector3d sum_g;
for (frame_it = all_image_frame.begin(), frame_it++; frame_it != all_image_frame.end(); frame_it++)
{
    double dt = frame_it->second.pre_integration->sum_dt;//time for bk to bk+1
    Vector3d tmp_g = frame_it->second.pre_integration->delta_v / dt;
    sum_g += tmp_g;
}
Vector3d aver_g;
aver_g = sum_g * 1.0 / ((int)all_image_frame.size() - 1);


(2)第二次循环,求出滑窗内的线加速度的标准差

double var = 0;
for (frame_it = all_image_frame.begin(), frame_it++; frame_it != all_image_frame.end(); frame_it++)
{
    double dt = frame_it->second.pre_integration->sum_dt;
    Vector3d tmp_g = frame_it->second.pre_integration->delta_v / dt;
    var += (tmp_g - aver_g).transpose() * (tmp_g - aver_g);//计算加速度的方差
    //cout << "frame g " << tmp_g.transpose() << endl;
}
var = sqrt(var / ((int)all_image_frame.size() - 1));//计算加速度的标准差
//ROS_WARN("IMU variation %f!", var);
if(var < 0.25)
{
    ROS_INFO("IMU excitation not enouth!");
    //return false;
}
   


6.3.2 将f_manager中的所有feature在所有帧的归一化坐标保存到vector sfm_f中(辅助)

(1)一上来就定义了几个容器,分别是:

Quaterniond Q[frame_count + 1];
Vector3d T[frame_count + 1];
map<int, Vector3d> sfm_tracked_points;
vector<SFMFeature> sfm_f;

这块有2个需要注意的地方,
就是为什么容量是frame_count + 1?因为滑窗的容量是10,再加上当前最新帧,所以需要储存11帧的值!
然后出现了一个新的数据结构,我们看一下:

数据结构: vector<SFMFeature> sfm_f
它定义在initial/initial_sfm.h中,
    struct SFMFeature
{
    bool state;//状态(是否被三角化)
    int id;
    vector<pair<int,Vector2d>> observation;//所有观测到该特征点的 图像帧ID 和 特征点在这个图像帧的归一化坐标
    double position[3];//在帧l下的空间坐标
    double depth;//深度
};   


可以发现,它存放着一个特征点的所有信息。容器定义完了,接下来就是往容器里放数据。

(2) 往容器里放数据

for (auto &it_per_id : f_manager.feature)//对于滑窗中出现的 所有特征点
{
    int imu_j = it_per_id.start_frame - 1;//从start_frame开始帧编号
    SFMFeature tmp_feature;
    tmp_feature.state = false;//状态(是否被三角化)
    tmp_feature.id = it_per_id.feature_id;//特征点id
    for (auto &it_per_frame : it_per_id.feature_per_frame)//对于当前特征点 在每一帧的坐标
    {
        imu_j++;//帧编号+1
        Vector3d pts_j = it_per_frame.point;//当前特征在编号为imu_j++的帧的归一化坐标
        tmp_feature.observation.push_back(make_pair(imu_j, Eigen::Vector2d{pts_j.x(), pts_j.y()}));//把当前特征在当前帧的坐标和当前帧的编号配上对
    }//tmp_feature.observation里面存放着的一直是同一个特征,每一个pair是这个特征在 不同帧号 中的 归一化坐标
    sfm_f.push_back(tmp_feature);//sfm_f里面存放着是不同特征
} 

在这里,为什么要多此一举构造一个sfm_f而不是直接使用f_manager呢?
我的理解,是因为f_manager的信息量大于SfM所需的信息量(f_manager包含了大量的像素信息),而且不同的数据结构是为了不同的业务服务的,所以在这里作者专门为SfM设计了一个全新的数据结构sfm_f,专业化服务。

6.3.3 在滑窗(0-9)中找到第一个满足要求的帧(第l帧),它与最新一帧(frame_count=10)有足够的共视点和平行度,并求出这两帧之间的相对位置变化关系
(1)定义容器

Matrix3d relative_R; 
Vector3d relative_T;
int l; //滑窗中满足与最新帧视差关系的那一帧的帧号

(2)两帧之间的视差判断,并得到两帧之间的相对位姿变化关系

if (!relativePose(relative_R, relative_T, l))
{//这里的第L帧是从第一帧开始到滑动窗口中第一个满足与当前帧的平均视差足够大的帧l,会作为 参考帧 到下面的全局sfm使用,得到的Rt为当前帧到第l帧的坐标系变换Rt
    ROS_INFO("Not enough features or parallax; Move device around");
    return false;
}

这里,又出现了个新的函数relativePose(),这个函数也是6.3.3的主要功能,进去看一下:

首先,搞清楚这个函数是要干什么事情?
a.计算滑窗内的每一帧(0-9)与最新一帧(10)之间的视差,直到找出第一个满足要求的帧,作为我们的第l帧;

 Estimator::relativePose(Matrix3d &relative_R, Vector3d &relative_T, int &l)
{                                   //output array R,t
    // find previous frame which contians enough correspondance and parallex with newest frame
    for (int i = 0; i < WINDOW_SIZE; i++)
    {//滑窗内的所有帧都和最新一帧进行视差比较
        vector<pair<Vector3d, Vector3d>> corres; 
        corres = f_manager.getCorresponding(i, WINDOW_SIZE);//寻找第i帧到窗口最后一帧(当前帧)的对应特征点归一化坐标
        if (corres.size() > 20)                             //归一化坐标point(x,y,不需要z)
        {
            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));//改成3,4呢(对应像素坐标,1-3是归一化xyz坐标)
                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


getCorresponding()的作用就是找到当前帧与最新一帧所有特征点在对应2帧下分别的归一化坐标,并配对,以供求出相对位姿时使用。

b.计算l帧与最新一帧的相对位姿关系

            if(average_parallax * 460 > 30 && m_estimator.solveRelativeRT(corres, relative_R, relative_T))
            { //solveRelativeRT()通过基础矩阵计算当前帧与第l帧之间的R和T,并判断内点数目是否足够
                l = i; //同时返回窗口最后一帧(当前帧)到第l帧(参考帧)的relative_R,relative_T
                ROS_DEBUG("average_parallax %f choose l %d and newest frame to triangulate the whole structure", average_parallax * 460, l);
                return true;
//一旦这一帧与当前帧视差足够大了,那么就不再继续找下去了(再找只会和当前帧的视差越来越小)
            }
            

这里最核心的公式就是m_estimator.solveRelativeRT(),这部分非常地关键。这里面代码很简单,就是把对应的点传进入,然后套cv的公式,但是求出来的R和T是谁转向谁的比较容易迷糊。
根据对以前学习内容和回忆和对后面公式的阅读,这个relative_R和relative_T是把最新一帧旋转到第l帧的旋转平移!

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;            //因为这里的ll,rr是归一化坐标,所以得到的是本质矩阵
        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 inlier_cnt = cv::recoverPose(E, ll, rr, cameraMatrix, rot, trans, mask);
        //cout << "inlier_cnt " << inlier_cnt << endl;

        Eigen::Matrix3d R;                             
        Eigen::Vector3d T;
        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);
        }
        Rotation = R.transpose();
        Translation = -R.transpose() * T;
        if(inlier_cnt > 12)
            return true;
        else
            return false;
    }
    return false;
}
/**
 *  Mat cv::findFundamentalMat(  返回通过RANSAC算法求解两幅图像之间的本质矩阵E
 *      nputArray  points1,             第一幅图像点的数组
 *      InputArray  points2,            第二幅图像点的数组
 *      int     method = FM_RANSAC,     RANSAC 算法
 *      double  param1 = 3.,            点到对极线的最大距离,超过这个值的点将被舍弃
 *      double  param2 = 0.99,          矩阵正确的可信度
 *      OutputArray mask = noArray()    输出在计算过程中没有被舍弃的点
 *  ) 
 */  
/**
 *  int cv::recoverPose (   通过本质矩阵得到Rt,返回通过手性校验的内点个数
 *      InputArray  E,              本质矩阵
 *      InputArray  points1,        第一幅图像点的数组
 *      InputArray  points2,        第二幅图像点的数组
 *      InputArray  cameraMatrix,   相机内参
 *      OutputArray     R,          第一帧坐标系到第二帧坐标系的旋转矩阵
 *      OutputArray     t,          第一帧坐标系到第二帧坐标系的平移向量
 *      InputOutputArray    mask = noArray()  在findFundamentalMat()中没有被舍弃的点
 *  )  
 */ 

而且这里出现了个新的数据结构m_estimator,分析一下。

数据结构: MotionEstimator m_estimator
它定义在initial/solve_5pts.h中,这个类没有数据成员,只有函数功能。所以说,TODO 把这个h文件干掉,融到estimator.cpp里面去!

class MotionEstimator
{
  public:

    bool solveRelativeRT(const vector<pair<Vector3d, Vector3d>> &corres, Matrix3d &R, Vector3d &T);

  private:
    double testTriangulation(const vector<cv::Point2f> &l,
                             const vector<cv::Point2f> &r,
                             cv::Mat_<double> R, cv::Mat_<double> t);
    void decomposeE(cv::Mat E,
                    cv::Mat_<double> &R1, cv::Mat_<double> &R2,
                    cv::Mat_<double> &t1, cv::Mat_<double> &t2);
};   
                                                                                                              

这一部分的原理对应如下,
在这里插入图片描述
Assuming P =[X, Y, Z]T is the 3D coordinates of one feature in camera coordinate system at time bk. K is the intrinsic parameter matrix of camera. p1 and p2 are pixel coordinates of the feature on image bk and bk+1. Then it is,
s1p1 = KP, s2x2 = K (RP + t).
At normalized plane, the coordinates are,
x1 = K-1p1, x2 = K-1p2.
Then they can get,
s2x2 = s1Rx1 + t.
The equation left multiplies t^ which is,
t^s2x2 = s1t^Rx1.
Then left multiplies x2T then gets,
0 = x2T (t^R) x1, or 0 = p2T (K-Tt^RK-1) p1,

c.没有满足要求的帧,整个初始化initialStructure()失败。

return false

6.3.4 construct():对窗口中每个图像帧求解sfm问题
得到所有图像帧相对于参考帧的旋转四元数Q、平移向量T和特征点坐标(核心!)

首先,还是定义了一个容器,看一下这个容器的数据结构。

GlobalSFM sfm;
数据结构: GlobalSFM sfm
它定义在initial/initial_sfm.h中,
    class GlobalSFM
{
public:
    GlobalSFM();
    bool 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);
private:
    bool solveFrameByPnP(Matrix3d &R_initial, Vector3d &P_initial, int i, vector<SFMFeature> &sfm_f);
    void triangulatePoint(Eigen::Matrix<double, 3, 4> &Pose0, Eigen::Matrix<double, 3, 4> &Pose1                        ,Vector2d &point0, Vector2d &point1, Vector3d &point_3d);
    void triangulateTwoFrames(int frame0, Eigen::Matrix<double, 3, 4> &Pose0, 
                              int frame1, Eigen::Matrix<double, 3, 4> &Pose1,
                              vector<SFMFeature> &sfm_f);
    int feature_num;
};       

所以说这个数据结构和MotionEstimator是类似的,主要是实现函数功能,他只有一个数据成员feature_num。

if(!sfm.construct(frame_count + 1, Q, T, l, //Q ck->c0?
          relative_R, relative_T,
          sfm_f, sfm_tracked_points))
{
    ROS_DEBUG("global SFM failed!");
    marginalization_flag = MARGIN_OLD;
    return false;
}

这里主要干了2件事,首先SfM,传入了frame_count + 1,l, relative_R, relative_T, sfm_f这几个参数,得到了Q, T, sfm_tracked_points,这三个量的都是基于l帧上表示的!

第二件事就是marginalization_flag = MARGIN_OLD。这说明了在初始化后期的第一次slidingwindow() marg掉的是old帧。

看一下*construct()*里面干了什么。见initial/initial_sfm.cpp文件。

(1)把第l帧作为参考坐标系,获得最新一帧在参考坐标系下的位姿

feature_num = sfm_f.size(); 
q[l].w() = 1; //参考帧的四元数,平移为1和0
q[l].x() = 0;
q[l].y() = 0;
q[l].z() = 0;
T[l].setZero(); //1、这里把第l帧看作参考坐标系,根据当前帧到第l帧的relative_R,relative_T,得到当前帧在参考坐标系下的位姿,之后的pose[i]表示第l帧到第i帧的变换矩阵[R|T]
q[frame_num - 1] = q[l] * Quaterniond(relative_R); //frame_num-1表示当前帧* relative c0_->ck
T[frame_num - 1] = relative_T;

(2)构造容器,存储滑窗内 第l帧 相对于 其它帧 和 最新一帧 的位姿

Matrix3d c_Rotation[frame_num]; 
Vector3d c_Translation[frame_num];
Quaterniond c_Quat[frame_num];
double c_rotation[frame_num][4];
double c_translation[frame_num][3]; 
Eigen::Matrix<double, 3, 4> Pose[frame_num];

注意,这些容器存储的都是相对运动,大写的容器对应的是l帧旋转到各个帧。
小写的容器是用于全局BA时使用的,也同样是l帧旋转到各个帧。之所以在这两个地方要保存这种相反的旋转,是因为三角化求深度的时候需要这个相反旋转的矩阵!

为了表示区别,称这两类容器叫 坐标系变换矩阵,而不能叫 位姿 !

(3)对于第l帧和最新一帧,它们的相对运动是已知的,可以直接放入容器

//从l帧旋转到各个帧的旋转平移
c_Quat[l] = q[l].inverse(); 
c_Rotation[l] = c_Quat[l].toRotationMatrix(); 
c_Translation[l] = -1 * (c_Rotation[l] * T[l]); 
Pose[l].block<3, 3>(0, 0) = c_Rotation[l];
Pose[l].block<3, 1>(0, 3) = c_Translation[l];
c_Quat[frame_num - 1] = q[frame_num - 1].inverse();
c_Rotation[frame_num - 1] = c_Quat[frame_num - 1].toRotationMatrix();
c_Translation[frame_num - 1] = 1 * (c_Rotation[frame_num - 1] * T[frame_num - 1]);
Pose[frame_num - 1].block<3, 3>(0, 0) = c_Rotation[frame_num - 1];
Pose[frame_num - 1].block<3, 1>(0, 3) = c_Translation[frame_num - 1];

注意,这块有一个取相反旋转的操作,因为三角化的时候需要这个相反的旋转!

(4)三角化l帧和最新帧,获得他们的共视点在l帧上的空间坐标
注意,三角化的前提有1个:两帧的(相对)位姿已知。这样才能把他们的共视点的三维坐标还原出来。

triangulateTwoFrames(l, Pose[l], frame_num - 1, Pose[frame_num - 1], 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++)
        {
            if (sfm_f[j].observation[k].first == frame0)//如果这个特征在frame0出现过
            {
                point0 = sfm_f[j].observation[k].second;//把他的归一化坐标提取出来
                has_0 = true;
            }
            if (sfm_f[j].observation[k].first == frame1)//如果这个特征在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);//根据他们的位姿和归一化坐标,输出在参考系l下的的空间坐标
            sfm_f[j].state = true;已经完成三角化,状态更改为true
            sfm_f[j].position[0] = point_3d(0);//把参考系l下的的空间坐标赋值给这个特征点的对象
            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;
        }                             
    }
}

这个函数的核心还是triangulatePoint(Pose0, Pose1, point0, point1, point_3d)。
首先他把sfm_f的特征点取出来,一个个地检查看看这个特征点是不是被2帧都观测到了,如果被观测到了,再执行三角化操作。那么再看看triangulatePoint()是怎么写的。

void GlobalSFM::triangulatePoint(Eigen::Matrix<double, 3, 4> &Pose0, Eigen::Matrix<double, 3, 4> &Pose1,
                        Vector2d &point0, Vector2d &point1, Vector3d &point_3d)
{//https://blog.csdn.net/jsf921942722/article/details/95511167 
    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);
}


这一部分代码涉及到了三角化求深度,对应《SLAM14讲》7.5部分,对应《手写VIO》第6章。
原理如下,对于我们要求的3D坐标,可以表示成齐次形式,
在这里插入图片描述
世界坐标系到相机坐标系的变换矩阵,在这里,是l帧到各个帧的转换(因为所有位姿和特征点目前都是在l帧表示的),注意,这块的表示方法和我们习惯的表示方法(一般都是相机到世界的转换)是相反的!
在这里插入图片描述
x为相机归一化平面坐标:
在这里插入图片描述
λ为深度值,已知以上条件有:
在这里插入图片描述
展开上式得:
在这里插入图片描述
对于等号右边的那个矩阵,它的第一行 ×(−u)第二行 ×(−v),再相加,即可得到第三行,因此,其线性相关,保留前两行即可,有,
在这里插入图片描述
因此,已知一个归一化平面坐标x和变换矩阵T可以构建两个关于X的线性方程组。有两个以上的图像观测即可求出X,
在这里插入图片描述
由于得到矩阵的秩大于未知量的数,所以上式方程没有非零解,使用SVD求最小二乘解,最小奇异值对应的向量即为所需的解。这个思路和IMU外参在线标定的方法很像。注意,x向量是四维了,解出来的值最后一个数需要确保它是1,所以,

(5)对于在sliding window里在第l帧之后的每一帧,分别都和前一帧用PnP求它的位姿,得到位姿后再和最新一帧三角化得到它们共视点的3D坐标

//TODO 为什么从l帧开始,而不是从l+1帧开始?
for (int i = l; i < frame_num - 1 ; i++) 
{
    Matrix3d R_initial = c_Rotation[i - 1];
    Vector3d P_initial = c_Translation[i - 1]; 
    //已知第i帧上出现的一些特征点的l系上空间坐标,通过上一帧的旋转平移得到下一帧的旋转平移
    if(!solveFrameByPnP(R_initial, P_initial, i, sfm_f))
        return false;//SfM失败
    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];
    //头2个和中间2个参数相互换位置没有影响
    triangulateTwoFrames(i, Pose[i], frame_num - 1, Pose[frame_num - 1], sfm_f);
}

在这里,我修改了源码的结构,使得逻辑上更通畅一些。triangulateTwoFrames()之前讲过了,现在专门注意solveFrameByPnP()函数。
PnP在《SLAM14讲》的7.7部分,一般来讲,求位姿,2D-2D对极几何只是在第一次使用,也就是没有3D特征点坐标的时候使用,一旦有了特征点,之后都会用3D-2D的方式求位姿。然后会进入PnP求新位姿,然后三角化求新3D坐标的循环中。

solveFrameByPnP()代码逻辑可以分成4部分,

a.第一次筛选:把滑窗的所有特征点中,那些没有3D坐标的点pass掉。

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++)//feature_num = sfm_f.size() line121
    {//要把待求帧i上所有特征点的归一化坐标和3D坐标(l系上)都找出来
        if (sfm_f[j].state != true)//这个特征点没有被三角化为空间点,跳过这个点的PnP
            continue;
            

b.因为是对当前帧和上一帧进行PnP,所以这些有3D坐标的特征点,不仅得在当前帧被观测到,还得在上一帧被观测到。

    Vector2d point2d;
    for (int k=0; k < (int)sfm_f[j].observation.size(); k++)//依次遍历特征j在每一帧中的归一化坐标
    {
        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);//把在待求帧i上出现过的特征的归一化坐标放到容器中
            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);//把在待求帧i上出现过的特征在参考系l的空间坐标放到容器中
            break;//因为一个特征在帧i上只会出现一次,一旦找到了就没有必要再继续找了
        }
    }
}

c.如果这些有3D坐标的特征点,并且在当前帧和上一帧都出现了,数量却少于15,那么整个初始化全部失败。因为它的是层层往上传递。

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

d.套用openCV的公式,进行PnP求解。

    cv::Mat r, rvec, t, D, tmp_r;
    cv::eigen2cv(R_initial, tmp_r);//转换成solvePnP能处理的格式
    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(pts_3_vector, pts_2_vector, K, D, rvec, t, 1);//得到了第l帧到第i帧的旋转平移
    if(!pnp_succ)
    {
        return false;
    }
    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;
}

(6)从第l+1帧到滑窗的最后的每一帧再与第l帧进行三角化补充3D坐标
现在回到construct()函数,在上一步,求出了l帧后面的每一帧的位姿,也求出了它们相对于最后一帧的共视点的3D坐标,但是这是不够的,现在继续补充3D坐标,那么就和第l帧进行三角化。

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

(7)对于在sliding window里在第l帧之前的每一帧,分别都和后一帧用PnP求它的位姿,得到位姿后再和第l帧三角化得到它们共视点的3D坐标

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

l帧之后的帧都有着落了,现在解决之前的帧。这个过程和(5)完全一样。

(8) 三角化其他未恢复的特征点
至此得到了滑动窗口中所有图像帧的位姿以及特征点的3D坐标。

for (int j = 0; j < feature_num; j++)
{
    if (sfm_f[j].state == true)//estimator.cpp line258 是否已经三角化了
        continue;
    if ((int)sfm_f[j].observation.size() >= 2)//里面存放着第j个特征在滑窗所有帧里的归一化坐标
    {
        Vector2d point0, point1;
        int frame_0 = sfm_f[j].observation[0].first;//第j个特征在滑窗第一次被观测到的帧的ID
        point0 = sfm_f[j].observation[0].second;//第j个特征在滑窗第一次被观测到的帧的归一化坐标
        int frame_1 = sfm_f[j].observation.back().first;//第j个特征在滑窗最后一次被观测到的帧的ID
        point1 = sfm_f[j].observation.back().second;//第j特征在滑窗最后一次被观测到的帧的归一化坐标
        Vector3d point_3d;//在帧l下的空间坐标
        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);
    }       
}

(9)采用ceres进行全局BA
a.声明problem
注意,因为四元数是四维的,但是自由度是3维的,因此需要引入LocalParameterization。

ceres::Problem problem;
    ceres::LocalParameterization* local_parameterization = new ceres::QuaternionParameterization();//解决

b.加入待优化量:全局位姿

for (int i = 0; i < frame_num; i++)//7、使用cares进行全局BA优化
    {
        //double array for ceres
        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();
        problem.AddParameterBlock(c_rotation[i], 4, local_parameterization);
        problem.AddParameterBlock(c_translation[i], 3);//value,size

在这里,可以发现,仅仅是位姿被优化了,特征点的3D坐标没有被优化!

c.固定先验值
因为l帧是参考系,最新帧的平移也是先验,如果不固定住,原本可观的量会变的不可观。

        if (i == l)
    {
        problem.SetParameterBlockConstant(c_rotation[i]);
    }
    if (i == l || i == frame_num - 1)
    {
        problem.SetParameterBlockConstant(c_translation[i]);
    }
}

d.加入残差块
这里采用的仍然是最小化重投影误差的方式,所以需要2D-3D信息,注意这块没有加loss function。

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;
        ceres::CostFunction* cost_function = ReprojectionError3D::Create(
        sfm_f[i].observation[j].second.x(),
        sfm_f[i].observation[j].second.y());

        problem.AddResidualBlock(cost_function, NULL, c_rotation[l], c_translation[l], 
                                sfm_f[i].position);  
    }

}

e.shur消元求解

ceres::Solver::Options options;
options.linear_solver_type = ceres::DENSE_SCHUR;
options.max_solver_time_in_seconds = 0.2;
ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
    if (!(summary.termination_type == ceres::CONVERGENCE && summary.final_cost < 5e-03))
{       
    return false;
}

这块出现了shur消元知识点,复习一下。shur消元有2大作用,一个是在最小二乘中利用H矩阵稀疏的性质进行加速求解,另一个是在sliding window时求解marg掉老帧后的先验信息矩阵。这块是shur消元的第一个用法。

d.返回特征点l系下3D坐标和优化后的全局位姿

    for (int i = 0; i < frame_num; i++)
    {
        q[i].w() = c_rotation[i][0]; 
        q[i].x() = c_rotation[i][1]; 
        q[i].y() = c_rotation[i][2]; 
        q[i].z() = c_rotation[i][3]; 
        q[i] = q[i].inverse();      
    }
    for (int i = 0; i < frame_num; i++)
    {
        T[i] = -1 * (q[i] * Vector3d(c_translation[i][0], c_translation[i][1], c_translation[i][2]));
    }
    for (int i = 0; i < (int)sfm_f.size(); i++)
    {
        if(sfm_f[i].state)
            sfm_tracked_points[sfm_f[i].id] = Vector3d(sfm_f[i].position[0], sfm_f[i].position[1], sfm_f[i].position[2]);
    }
    return true;
}

优化完成后,需要获得各帧在帧l系下的位姿(也就是各帧到l帧的旋转平移),所以需要inverse操作,然后把特征点在帧l系下的3D坐标传递出来。

至此,construct()函数全部完成!

6.3.5 给滑窗外的图像帧提供初始的RT估计,然后solvePnP进行优化


目的:求出所有帧对应的IMU坐标系bk到l帧的旋转和ck到l帧的平移,和论文中初始化部分头两个公式对应上!


现在再次回到estimator.cpp文件,看下一步流程。在之前的流程里,求出了滑窗内所有帧和对应特征点在l帧下的状态,现在要求出所有帧,也就是滑窗外的,也求解出来。
这部分代码最外层有一个for循环,也就是遍历所有的图像帧:

map<double, ImageFrame>::iterator frame_it;
map<int, Vector3d>::iterator it;
frame_it = all_image_frame.begin( );
for (int i = 0; frame_it != all_image_frame.end( ); frame_it++)
{
    // provide initial guess
    cv::Mat r, rvec, t, D, tmp_r;
    //对当前图像帧的操作
    ...
}

TODO 我感到费解的是,这部分为什么没有对i<WINDOW_SIZE+1这个判断呢?

a.边界判断:对于滑窗内的帧,把它们设为关键帧,并获得它们对应的IMU坐标系到l系的旋转平移

if((frame_it->first) == Headers[i].stamp.toSec())
{
    frame_it->second.is_key_frame = true;
    frame_it->second.R = Q[i].toRotationMatrix() * RIC[0].transpose();
    frame_it->second.T = T[i];
    i++;
    continue;
}

这部分代码虽然看起来简单,实际上有点绕,涉及到的数据结构很多。这里,要注意一下,Headers,Q和T,它们的size都是WINDOW_SIZE+1!它们存储的信息都是滑窗内的,尤其是Q和T,它们都是当前视觉帧到l帧(也是视觉帧)系到旋转平移。
所以一开始,通过时间戳判断是不是滑窗内的帧;
如果是,那么设置为关键帧;
接下来的两行,非常重要,注意一下ImageFrame这个数据结构,见5.2-2,它不仅包括了图像信息,还包括了对应的IMU的位姿信息和IMU预积分信息,而这里,是这些帧第一次获得它们对应的IMU的位姿信息的位置!也就是bk->l帧的旋转平移!

b.边界判断:如果当前帧的时间戳大于滑窗内第i帧的时间戳,那么i++

//TODO delete this judgement
if((frame_it->first) > Headers[i].stamp.toSec())
    i++; //TODO change to ++i

代码好懂,但是不明白为什么这样写?这一部分有点像kmp算法中的2把尺子,一把大一点的尺子是all_image_frame,另一把小尺子是Headers,分别对应着所有帧的长度和滑窗长度;

小尺子固定,大尺子在上面滑动;
每次循环,大尺子滑动一格;
因为小尺子比较靠后,所以开始的时候只有大尺子在动,小尺子不动;
如果大尺子和小尺子刻度一样的时候,小尺子也走一格;
如果大尺子的刻度比小尺子大,小尺子走一格;

但是问题是,我觉得小尺子的刻度,对应在大尺子的最后面!TODO把这一部分注释掉,看看效果!

c.对滑窗外的所有帧,求出它们对应的IMU坐标系到l帧的旋转平移

    //注意这里的 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;
    vector<cv::Point3f> pts_3_vector;//获取 pnp需要用到的存储每个特征点三维点和图像坐标的 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())
            {
                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);
                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;
    }
    if (! cv::solvePnP(pts_3_vector, pts_2_vector, K, D, rvec, t, 1))
    {
        ROS_DEBUG("solve pnp fail!");
        return false;
    }
    cv::Rodrigues(rvec, r);
    MatrixXd R_pnp,tmp_R_pnp;
    cv::cv2eigen(r, tmp_R_pnp);
    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;
}

这部分和之前讲的的部分思路一样,唯一需要注意的是最后两行,这块明显是告诉我们传入的是bk->l的旋转平移。

6.3.6 visualInitialAlign() (核心!)

    if (visualInitialAlign())
        return true;
    else
        return false;
}

6.4 visualInitialAlign()

基本上,初始化的理论部分都在visualInitialAlign()函数里。

6.4.1计算陀螺仪偏置,尺度,重力加速度和速度

TicToc t_g;
VectorXd x;
if(!VisualIMUAlignment(all_image_frame, Bgs, g, x))
    return false;

进去看一下VisualIMUAlignment()的具体内容,它在initial/initial_alignment.h里面。

bool VisualIMUAlignment(map<double, ImageFrame> &all_image_frame, Vector3d* Bgs, Vector3d &g, VectorXd &x)
{
    solveGyroscopeBias(all_image_frame, Bgs);//陀螺仪的偏置进行标定

    if(LinearAlignment(all_image_frame, g, x))//估计尺度、重力以及速度
        return true;
    else 
        return false;
}

solveGyroscopeBias()对应在6.1.3部分。
LinearAlignment()对应在6.1.4和6.1.5部分。

6.4.2 传递所有图像帧的位姿Ps、Rs,并将其置为关键帧

for (int i = 0; i <= frame_count; i++)
{
    Matrix3d Ri = all_image_frame[Headers[i].stamp.toSec()].R;
    Vector3d Pi = all_image_frame[Headers[i].stamp.toSec()].T;
    Ps[i] = Pi;
    Rs[i] = Ri;
    all_image_frame[Headers[i].stamp.toSec()].is_key_frame = true;
}

6.4.3重新计算所有f_manager的特征点深度

VectorXd dep = f_manager.getDepthVector();
for (int i = 0; i < dep.size(); i++)
    dep[i] = -1;//将所有特征点的深度置为-1
f_manager.clearDepth(dep);

//triangulat on cam pose , no tic //重新计算特征点的深度
Vector3d TIC_TMP[NUM_OF_CAM];
for(int i = 0; i < NUM_OF_CAM; i++)
    TIC_TMP[i].setZero();
ric[0] = RIC[0];
f_manager.setRic(ric);
f_manager.triangulate(Ps, &(TIC_TMP[0]), &(RIC[0]));

这里有2个ric,小ric是vins自己计算得到的,大RIC是从yaml读取的,我觉得没什么区别。
这里需要注意一下,这个triangulate()和之前出现的并不是同一个函数。
它是定义在feature_manager.cpp里的,之前的是在initial_sfm.cpp里面。它们服务的对象是不同的数据结构!

6.4.4 IMU的bias改变,重新计算滑窗内的预积分

 for (int i = 0; i <= WINDOW_SIZE; i++)
    {
        pre_integrations[i]->repropagate(Vector3d::Zero(), Bgs[i]);
    }

TODO 我觉得这块操作在6.4.1的solveGyroscopeBias()部分重复了,注释掉看看
现在又想了一下,感觉并不是重复了,因为6.4.1是更新完角速度bias后重新算了一下提升了精度,现在再算一下,又提升了一次精度,就算是注释掉,也是注释掉6.4.1里面的那个?试一下。

6.4.5 将Ps、Vs、depth尺度s缩放后从l帧转变为相对于c0帧图像坐标系下

    double s = (x.tail<1>())(0);
   
    for (int i = frame_count; i >= 0; i--)
        Ps[i] = s * Ps[i] - Rs[i] * TIC[0] - (s * Ps[0] - Rs[0] * TIC[0]);
        
    int kv = -1;
map<double, ImageFrame>::iterator frame_i;

    for (frame_i = all_image_frame.begin(); frame_i != all_image_frame.end(); frame_i++)
    {
        if(frame_i->second.is_key_frame)
        {
            kv++;
            Vs[kv] = frame_i->second.R * x.segment<3>(kv * 3);
        }
    }
    for (auto &it_per_id : f_manager.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 *= s;
    }

6.4.6 通过将重力旋转到z轴上,得到世界坐标系与摄像机坐标系c0之间的旋转矩阵rot_diff

Matrix3d R0 = Utility::g2R(g);
double yaw = Utility::R2ypr(R0 * Rs[0]).x();
R0 = Utility::ypr2R(Eigen::Vector3d{-yaw, 0, 0}) * R0;
g = R0 * g;
//Matrix3d rot_diff = R0 * Rs[0].transpose();
Matrix3d rot_diff = R0;

6.4.7 所有变量从参考坐标系c0旋转到世界坐标系w

    for (int i = 0; i <= frame_count; i++)
    {
        Ps[i] = rot_diff * Ps[i];
        Rs[i] = rot_diff * Rs[i];
        Vs[i] = rot_diff * Vs[i];
    }
    return true;
}

至此,初始化的工作全部完成!!
代码量巨长,一半的工作在于视觉SfM(这部分作用仅仅负责求相机pose),另一半才是论文里说的松耦合初始化!在initialStructure()的前半部分里,新建了一组sfm_f这样一批特征点,虽然和f_manager有数据重复,但它们的作用仅仅是用来配合求pose的,而且这个数据结构是建在栈区的,函数结束后统统销毁。而且找l帧这个操作很巧妙。
后半部分,就是论文里的内容。见初始化(理论部分)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【SLAM】VINS-MONO解析——初始化(代码部分) 的相关文章

  • C++ 用socket封装成http

    HttpSocket h interface for the CHttpSocket class if defined AFX HTTPSOCKET H F49A8F82 A933 41A8 AF47 68FBCAC4ADA6 INCLUD
  • echarts更换主题

    前言 本篇文章基于上一篇文章 xff1a vue工程整合echarts xff0c 因此这里主要讲的是主题是如何配置的 xff0c 其他代码在这里不再赘述 官网中已经说明了echarts除了有默认的主题外 xff0c 并且内置了dark主题
  • Java final修饰符详解

    final 在 Java 中的意思是最终 xff0c 也可以称为完结器 xff0c 表示对象是最终形态的 xff0c 不可改变的意思 final 应用于类 方法和变量时意义是不同的 xff0c 但本质是一样的 xff0c 都表示不可改变 x
  • Python Requests库安装和使用

    Python 提供了多个用来编写爬虫程序的库 xff0c 除了前面已经介绍的 urllib 库之外 xff0c 还有一个很重的 Requests 库 xff0c 这个库的宗旨是 让 HTTP 服务于人类 Requests 是 Python
  • C语言 十进制转十六进制

    问题描述 十六进制数是在程序设计时经常要使用到的一种整数的表示方式 它有0 1 2 3 4 5 6 7 8 9 A B C D E F共16个符号 xff0c 分别表示十进制数的0至15 十六进制的计数方法是满16进1 xff0c 所以十进
  • Pandas知识点超全总结

    Pandas知识点超全总结 一 数据结构1 Series1 创建2 切片 修改3 其他属性 2 DataFrame1 创建2 切片3 增加 修改4 删除5 查看 二 读写数据1 读数据1 excel文件2 csv文件3 sql文件 2 写数
  • socket通信小结

    1 网络中的进程之间如何进行通信 区别于本地的进程间通信 xff0c 我们首要解决的问题是如何唯一标识一个进程 xff0c 否则通信无从谈起 xff01 在本地可以通过进程PID来唯一标识一个进程 xff0c 但是在网络中这是行不通的 其实
  • eclipse alt+/代码智能提示总是报错:problems during content assist

    解决办法如下图 xff1a 依次点击红框标记的地方即可解决问题
  • linux C 遍历目录及其子目录 opendir -> readdir -> closedir

    在 linux 下遍历某一目录下内容 LINUX 下历遍目录的方法一般是这样的 xff1a 打开目录 gt 读取 gt 关闭目录 相关函数是 opendir gt readdir gt closedir xff0c 其原型如下 xff1a
  • 最全面的 linux 信号量解析

    一 xff0e 什么是信号量 信号量的使用主要是用来保护共享资源 xff0c 使得资源在一个时刻只有一个进程 xff08 线程 xff09 所拥有 信号量的值为正的时候 xff0c 说明它空闲 所测试的线程可以锁定而使用它 若为 0 xff
  • vue3学习一:let和const

    在函数的内部 xff0c 定义name变量 xff0c 当i等于false时 xff0c 按照Java语言 xff0c else里是拿不到name的 xff0c 并且会显示报错 xff0c 但是却不会报错 xff0c 这是因为javascr
  • PHP 中最全的设计模式(23种)

    PhpDesignPatterns PHP 中的设计模式 一 Introduction 介绍 设计模式 xff1a 提供了一种广泛的可重用的方式来解决我们日常编程中常常遇见的问题 设计模式并不一定就是一个类库或者第三方框架 xff0c 它们
  • 异常详细信息: System.NullReferenceException: 未将对象引用设置到对象的实例。

    我遇到的出现这种错误的原因一般是以下几种情况 xff1a 1 在绑定数据控件的时候 xff0c 建立数据库连接 OleDbConnection conn 61 new OleDbConnection 34 provider 61 micro
  • 连接不上Github,网络超时的检查和解决办法

    先附上图片吧 连接不上github但是其它网站都是正常上网 解决办法 win 43 r打开运行输入cmd 再命令行里输入 ping github com 看看返回值 我的全是请求超时 xff0c 发送的数据包全丢 xff08 我先ping了
  • 2018校招笔试真题汇总

    2018校招笔试真题汇总 最近看好多牛油贡献了很多考试的真题 xff0c 我把他们汇总在一起给到大家 xff0c 也感谢这些牛油的贡献 xff0c 只要进这个汇总贴的 xff0c 你们都将每人获得一份牛客送出的礼物一份 科大讯飞 xff1a
  • 【2】Docker的启动与停止

    1 xff09 启动 docker systemctl start docker 2 xff09 查看 docker 状态 systemctl status docker 3 xff09 查看 docker 概要信息 docker info
  • 【8】Docker中部署Redis

    1 xff09 拉取镜像 docker pull redis 这是我在 VMware 的 CentOS 中装过的 redis 版本 xff0c 拉取该指定版本使用 docker pull redis 5 0 12 命令 xff0c 不过下面
  • 操作系统学习之系统调用

    目录 一 操作系统学习之系统调用 1 什么是系统调用 2 系统调用有什么用 3 为什么需要系统调用 4 系统调用的具体流程 1 xff09 执行过程 2 如何实现用户态与内态之间的切换 3 系统调用常见名词 4 系统调用如何返回 传递返回值
  • 22-Docker-常用命令详解-docker pull

    常用命令详解 docker pull 前言docker pull语法格式options说明 使用示例未指定tag a 拉取所有 tagged 镜像 前言 本篇来学习docker pull命令 docker pull 作用 xff1a 从镜像
  • 720p,1080p对应像素解释

    720P是1280 720 61 921600 xff0c 即 分辨率为921600 xff0c 即大约92万像素 xff0c 921600接近100万像素 xff08 1280是按照16 9算出来的 xff0c 4 3的另算 xff0c

随机推荐

  • vue3学习二:模板字符串

    模板字符串是为了解决字符串拼接问题 xff0c 在es5中 xff0c 字符串拼接是这样的 xff1a let name 61 34 wjdsg 34 console log 34 您好 34 43 name 而在es6中可以使用模板字符串
  • Canal 读取 mysql bin_log

    场景 xff1a 在微服务开发的过程中多个项目协同完成一个功能 xff0c 工程与工程之间存在数据上的解耦 xff0c 底层服务为上层服务提供数据 而底层服务有需要对数据进行管理 解决方案 xff1a 基本底层服务 通过 canal 获取
  • PuTTY连接Linux服务器被拒绝问题

    PuTTY连接Linux服务器被拒绝问题 1 使用命令 xff1a ssh localhost 查看是否安装ssh1 2需要手动安装ssh1 2 1 输入命令 xff1a 1 2 2 若是出现下图所示 xff1a 1 2 3 查看进程 xf
  • 实时数据同步工具<Maxwell 操作案例>

    文章目录 案例一 xff1a 监控MySQL中的数据并输出到控制台案例二 xff1a Maxwell监控mysql的数据输出到kafka案例三 xff1a 监控MySQL指定表的数据并输出到kafka 案例一 xff1a 监控MySQL中的
  • Docker 镜像 Tag 管理

    Author xff1a rab 良好的镜像版本命名习惯能让我们更好的管理和使用镜像 xff08 如项目上线失败后可有效的进行版本回退等 xff09 xff0c 以下是 Docker 社区常用的 tag 方案 比如我现在已经构建了一个 co
  • APM与Pixhawk间的关系

    1 APM 本文APM指代 xff1a https github com ArduPilot ardupilot 2 Pixhawk 本文Pixhawk指代 xff1a https github com PX4 Firmware 3 关系
  • Pixhawk串口名称与硬件接口对应关系

    Pixhawk提供的串口较多 xff0c 通过ls dev 可以看到有如下7个tty设备 xff1a ttyACM0 ttyS0 ttyS1 ttyS2 ttyS3 ttyS4 ttyS5 ttyS6 但每个串口名称对应到Pixhawk硬件
  • Linux系统大小端判断

    大端模式 大端模式 xff0c 是指数据的低位保存在内存的高地址中 xff0c 而数据的高位保存在内存的低地址中 小端模式 小端模式 xff0c 是指数据的低位保存在内存的低地址中 xff0c 而数据的高位保存在内存的高地址中 判断程序 文
  • C preprocessor fails sanity check

    编译某一产品固件时 xff0c 遇到如下现象 xff1a checking how to run the C preprocessor opt mipsel 24kec linux uclibc bin mipsel 24kec linux
  • VLC同时开启播放多个视频流BAT脚本

    工作中 xff0c 难免会遇到要用同一个程序连续打开多个URL资源 路径的情况 xff0c 一个窗口一个窗口的启动效率太低 这里以VLC同时播放多个码流图像为例 xff0c 写个简单的BAT脚本 xff0c 供需要者参考 PS 1 使用方式
  • 【AI】Ubuntu14.04安装OpenCV3.2.0

    在ubuntu14 04系统上安装OpenCV3 2 0 环境要求 GCC 4 4 x or later CMake 2 8 7 or higher Git if failed you can replace it with git cor
  • 若依代码生成器(mybatis-plus)

    看这篇文章之前 xff0c 先去看一下我前面的文章 xff1a 若依前后端分离整合mybatis plus wjdsg的博客 CSDN博客 用过若依都知道 xff0c 若依自带的代码生成器 xff0c 是下载下来 xff0c 然后自己粘贴到
  • 【AI】基于OpenCV开发自定义程序编译方法

    基于OpenCV开发自定义程序编译方法 OpenCV自带的程序 xff0c 编译均采用cmake统一编译 若我们要基于OpenCV开发自己的程序 xff0c 如何快速编译 xff1f 本文以OpenCV库自带的facedetect cpp程
  • H3C SNMPv3 配置

    1 xff09 H3C SNMPv3 配置 snmp agent mib view included MIB 2 mib 2 noAuthNoPriv xff1a snmp agent group v3 mygroup read view
  • 【SLAM】VINS-MONO解析——综述

    目前网上有很多分析文章 xff0c 但是都只是一些比较基础的原理分析 xff0c 而且很多量 xff0c 虽然有推倒 xff0c 但是往往没有讲清楚这些量是什么 xff0c 为什么要有这些量 xff0c 这些量是从哪来的 xff0c 也没有
  • 【SLAM】VINS-MONO解析——前端

    各个部分的讲解如下链接 xff1a SLAM VINS MONO解析 综述 SLAM VINS MONO解析 feature tracker SLAM VINS MONO解析 IMU预积分 SLAM VINS MONO解析 vins est
  • 【SLAM】VINS-MONO解析——IMU预积分

    4 IMU预积分 IMU预积分主要干了2件事 xff0c 第一个是IMU预积分获得 值 xff0c 另一个是误差传递函数的获取 本部分的流程图如下图所示 各个部分的讲解如下链接 xff1a SLAM VINS MONO解析 综述 SLAM
  • 【SLAM】VINS-MONO解析——vins_estimator流程

    5 vins estimator 基本上VINS里面绝大部分功能都在这个package下面 xff0c 包括IMU数据的处理 前端 xff0c 初始化 我觉得可能属于是前端 xff0c 滑动窗口 后端 xff0c 非线性优化 后端 xff0
  • 【SLAM】VINS-MONO解析——初始化(理论部分)

    6 初始化 第一个问题 xff0c 为什么要初始化 xff1f 对于单目系统而言 xff0c 1 视觉系统只能获得二维信息 xff0c 损失了一维信息 深度 所以需要动一下 xff0c 也就是三角化才能重新获得损失的深度信息 xff1b 2
  • 【SLAM】VINS-MONO解析——初始化(代码部分)

    6 2 代码解析 这部分代码在estimator processImage 最后面 初始化部分的代码虽然生命周期比较短 xff0c 但是 xff0c 代码量巨大 xff01 主要分成2部分 xff0c 第一部分是纯视觉SfM优化滑窗内的位姿