概述

VINS Fusion在VINS Mono的基础上,添加了GPS等可以获取全局观测信息的传感器,使得VINS可以利用全局信息消除累计误差,进而减小闭环依赖。

局部传感器(如相机,IMU,激光雷达等)被广泛应用于建图和定位算法。尽管这些传感器能够在没有GPS信息的区域,实现良好的局部定位和局部建图效果,但是这些传感器只能提供局部观测,限制了其应用场景:

第一个问题是局部观测数据缺乏全局约束,当我们每次在不同的位置运行算法时,都会得到不同坐标系下的定位和建图结果,因而难以将这些测量结果结合起来,形成全局效果。
第二个问题是基于局部观测的估计算法必然存在累计漂移,尽管回环检测可以一定程度上消除漂移,但是对于数据量较大的大范围场景,算法依然难以处理。

相比于局部传感器,全局传感器(如GPS,气压计和磁力计等)可以提供全局观测。这些传感器使用全局统一坐标系,并且输出的观测数据的方差不随时间累计而增加。但这些传感器也存在一些问题,导致无法直接用于精确定位和建图。以GPS为例,GPS数据通常不平滑,存在噪声,且输出速率低。

因此,一个简单而直观的想法是将局部传感器和全局传感器结合起来,以达到局部精确全局零漂的效果。这也就是VINS Fusion这篇论文的核心内容。

融合原理

VINS Fusion的算法架构如图所示:

下图以因子图的方式表示观测和状态之间的约束:

其中圆形为状态量(如位姿,速度,偏置等),黄色正方形为局部观测的约束,即来自VO/VIO的相对位姿变换;而其他颜色的正方形为全局观测的约束,比如紫色正方形为来自GPS的约束。

局部约束(残差)的构建参考vins mono论文,计算的是相邻两帧之间的位姿残差。这里只讨论GPS带来的全局约束。首先当然是将GPS坐标,也就是经纬度转换为大地坐标系。习惯上选择右手坐标系,x,y,z轴正方向分别是北东地或东北天方向。接下来就可以计算得到全局约束的残差:

其中z为GPS测量值,X为状态预测,h方程为观测方程。X可以通过上一时刻状态以及当前时刻与上一时刻的位姿变换计算出来。具体到本文的方法,就是利用VIO得到当前时刻和上一时刻的相对位姿dX,加到上一时刻的位姿上X(i-1),得到当前时刻的位姿Xi。需要注意的是,这里的X以第一帧为原点。通过观测方程h,将当前状态的坐标转换到GPS坐标系下。这样就构建了一个全局约束。

之后的优化就交给BA优化器进行迭代优化,vins fusion沿用了ceres作为优化器。

代码实现

1. GPS和VIO数据输入

需要明确的一点是,VIO的输出是相对于第一帧的累计位姿变换,也就是以第一帧为原点。VINS Fusion接收vio输出的局部位姿变换(相对于第一帧),以及gps输出的经纬度坐标,进行融合。接受数据输入的接口在global_fusion/src/globaOptNode.cpp文件中,接口定义的关键代码如下:

void GPS_callback(const sensor_msgs::NavSatFixConstPtr &GPS_msg)
{m_buf.lock();gpsQueue.push(GPS_msg); // 每次接收到的gps消息添加到gpsQueue队列中m_buf.unlock();
}void vio_callback(const nav_msgs::Odometry::ConstPtr &pose_msg)
{double t = pose_msg->header.stamp.toSec();last_vio_t = t;Eigen::Vector3d vio_t; // 平移矩阵,转换的代码省略Eigen::Quaterniond vio_q; // 旋转四元数,转换的代码省略globalEstimator.inputOdom(t, vio_t, vio_q); // 将时间,平移,四元数作为预测添加进globalEstimatorm_buf.lock();while(!gpsQueue.empty()){sensor_msgs::NavSatFixConstPtr GPS_msg = gpsQueue.front();double gps_t = GPS_msg->header.stamp.toSec();// 找到和vio里程计数据相差在10ms以内的gps数据,作为匹配数据if(gps_t >= t - 0.01 && gps_t <= t + 0.01){double latitude = GPS_msg->latitude;double longitude = GPS_msg->longitude;double altitude = GPS_msg->altitude;double pos_accuracy = GPS_msg->position_covariance[0];globalEstimator.inputGPS(t, latitude, longitude, altitude, pos_accuracy); // 关键,将gps数据作为观测输入到globalEstimator中gpsQueue.pop(); // 满足条件的gps信息弹出break;}else if(gps_t < t - 0.01)gpsQueue.pop(); // 将过时的gps信息弹出else if(gps_t > t + 0.01)break; // 说明gps信息是后来的,就不要改动gps队列了,退出}m_buf.unlock();// ...其余省略
}

2. GPS和VIO融合

VINS Fusion融合GPS和VIO数据的代码在global_fusion/src/globalOpt.cpp文件中,下面进行详细介绍。

a. 接收GPS数据,接收VIO数据并转到GPS坐标系

// 接收上面输入的vio数据
void GlobalOptimization::inputOdom(double t, Eigen::Vector3d OdomP, Eigen::Quaterniond OdomQ)
{vector<double> localPose{OdomP.x(), OdomP.y(), OdomP.z(), OdomQ.w(), OdomQ.x(), OdomQ.y(), OdomQ.z()};localPoseMap[t] = localPose;// 利用vio的局部坐标进行坐标变换,得到当前帧的全局位姿Eigen::Quaterniond globalQ;globalQ = WGPS_T_WVIO.block<3, 3>(0, 0) * OdomQ;Eigen::Vector3d globalP = WGPS_T_WVIO.block<3, 3>(0, 0) * OdomP + WGPS_T_WVIO.block<3, 1>(0, 3);vector<double> globalPose{globalP.x(), globalP.y(), globalP.z(),globalQ.w(), globalQ.x(), globalQ.y(), globalQ.z()};globalPoseMap[t] = globalPose;
}// 接收上面输入的gps数据
void GlobalOptimization::inputGPS(double t, double latitude, double longitude, double altitude, double posAccuracy)
{double xyz[3];GPS2XYZ(latitude, longitude, altitude, xyz); // 将GPS的经纬度转到地面笛卡尔坐标系vector<double> tmp{xyz[0], xyz[1], xyz[2], posAccuracy};GPSPositionMap[t] = tmp;newGPS = true;
}

上面出现了VIO数据的局部坐标转到GPS坐标的计算过程,其公式如下:

这个公式中的GPS2VIO出现在后面的优化过程中,计算方法为:

b. 融合优化

这里是融合的关键代码,可以看出其流程如下:

1.构建t_array和q_array,用来存入平移和旋转变量,方便输入优化方程,以及在优化后取出。
2.利用RelativeRTError::Create()构建VIO两帧之间的约束,输入优化方程
3.利用TError::Create()构建GPS构成的全局约束,输入优化方程
4.取出优化后的数据

void GlobalOptimization::optimize()
{while(true){if(newGPS){newGPS = false;// ceres定义部分略去// add parammPoseMap.lock();int length = localPoseMap.size();// ********************************************************// ***  1. 构建t_array, q_array用来存优化变量,等优化后取出  ***// ********************************************************double t_array[length][3];double q_array[length][4];map<double, vector<double>>::iterator iter;iter = globalPoseMap.begin();for (int i = 0; i < length; i++, iter++){// 取出数据部分省略// 添加了parameterblockproblem.AddParameterBlock(q_array[i], 4, local_parameterization);problem.AddParameterBlock(t_array[i], 3);}map<double, vector<double>>::iterator iterVIO, iterVIONext, iterGPS;int i = 0;for (iterVIO = localPoseMap.begin(); iterVIO != localPoseMap.end(); iterVIO++, i++){// ********************************************************// *********************   2. VIO约束   *******************// ********************************************************iterVIONext = iterVIO;iterVIONext++;if(iterVIONext != localPoseMap.end()){Eigen::Matrix4d wTi = Eigen::Matrix4d::Identity(); // 第i帧的变换矩阵Eigen::Matrix4d wTj = Eigen::Matrix4d::Identity(); // 第j帧的变换矩阵// 取出数据部分省略Eigen::Matrix4d iTj = wTi.inverse() * wTj; // 第j帧到第i帧的变换矩阵Eigen::Quaterniond iQj; // 第j帧到第i帧的旋转iQj = iTj.block<3, 3>(0, 0); Eigen::Vector3d iPj = iTj.block<3, 1>(0, 3);  // 第j帧到第i帧的平移ceres::CostFunction* vio_function = RelativeRTError::Create(iPj.x(), iPj.y(), iPj.z(),iQj.w(), iQj.x(), iQj.y(), iQj.z(),0.1, 0.01);problem.AddResidualBlock(vio_function, NULL, q_array[i], t_array[i], q_array[i+1], t_array[i+1]);}// ********************************************************// *********************   3. GPS约束   *******************// ********************************************************double t = iterVIO->first;iterGPS = GPSPositionMap.find(t);if (iterGPS != GPSPositionMap.end()){ceres::CostFunction* gps_function = TError::Create(iterGPS->second[0], iterGPS->second[1], iterGPS->second[2], iterGPS->second[3]);problem.AddResidualBlock(gps_function, loss_function, t_array[i]);}}ceres::Solve(options, &problem, &summary);// ********************************************************// *******************   4. 取出优化结果   *****************// ********************************************************iter = globalPoseMap.begin();for (int i = 0; i < length; i++, iter++){// 取出优化结果vector<double> globalPose{t_array[i][0], t_array[i][1], t_array[i][2],q_array[i][0], q_array[i][1], q_array[i][2], q_array[i][3]};iter->second = globalPose;if(i == length - 1){Eigen::Matrix4d WVIO_T_body = Eigen::Matrix4d::Identity(); Eigen::Matrix4d WGPS_T_body = Eigen::Matrix4d::Identity();// 剩余的存入部分省略,得到WGPS_T_WVIOWGPS_T_WVIO = WGPS_T_body * WVIO_T_body.inverse();}}updateGlobalPath();mPoseMap.unlock();}std::chrono::milliseconds dura(2000);std::this_thread::sleep_for(dura);}return;
}

上述代码中出现了一个关键的部分,即WGPS_T_WVIO的计算。从之前的代码中知道,这个4*4矩阵是用来做VIO到GPS坐标系的变换的。

具体的程序在 Factors.h 里面的结构体,如下:

struct RelativeRTError
{//结构体初始化RelativeRTError(double t_x, double t_y, double t_z, double q_w, double q_x, double q_y, double q_z,double t_var, double q_var):t_x(t_x), t_y(t_y), t_z(t_z), q_w(q_w), q_x(q_x), q_y(q_y), q_z(q_z),t_var(t_var), q_var(q_var){}//构建代价函数template <typename T>bool operator()(const T* const w_q_i, const T* ti, const T* w_q_j, const T* tj, T* residuals) const{T t_w_ij[3]; //VIO和GPS优化后,世界坐标系 i、j帧的位置增量t_w_ij[0] = tj[0] - ti[0];t_w_ij[1] = tj[1] - ti[1];t_w_ij[2] = tj[2] - ti[2];T i_q_w[4]; //i帧的四元数逆QuaternionInverse(w_q_i, i_q_w);//世界坐标系下,i、j帧的位置增量t_w_ij,经i_q_w 转换到i帧的坐标系,与 t_x 求差!T t_i_ij[3];ceres::QuaternionRotatePoint(i_q_w, t_w_ij, t_i_ij);//四元素旋转,得到姿态//残差定义为增量差residuals[0] = (t_i_ij[0] - T(t_x)) / T(t_var);residuals[1] = (t_i_ij[1] - T(t_y)) / T(t_var);residuals[2] = (t_i_ij[2] - T(t_z)) / T(t_var);T relative_q[4]; //传入VIO观测的四元数增量relative_q[0] = T(q_w);relative_q[1] = T(q_x);relative_q[2] = T(q_y);relative_q[3] = T(q_z);T q_i_j[4]; //状态量计算的四元数增量ceres::QuaternionProduct(i_q_w, w_q_j, q_i_j);T relative_q_inv[4];QuaternionInverse(relative_q, relative_q_inv);T error_q[4]; //状态量计算的增量乘上测量量的逆,定义了残差ceres::QuaternionProduct(relative_q_inv, q_i_j, error_q); residuals[3] = T(2) * error_q[1] / T(q_var);residuals[4] = T(2) * error_q[2] / T(q_var);residuals[5] = T(2) * error_q[3] / T(q_var);return true;}static ceres::CostFunction* Create(const double t_x, const double t_y, const double t_z,const double q_w, const double q_x, const double q_y, const double q_z,const double t_var, const double q_var) {return (new ceres::AutoDiffCostFunction<RelativeRTError, 6, 4, 3, 4, 3>(new RelativeRTError(t_x, t_y, t_z, q_w, q_x, q_y, q_z, t_var, q_var)));}double t_x, t_y, t_z, t_norm;double q_w, q_x, q_y, q_z;double t_var, q_var;};

注意:哪些是VIO的 两帧间的变化,哪些是 优化后 globalPoseMap 的位姿态!

思路:VIO和GPS优化后的POS增量 投影到 VIO下,然后和VIO下的POS 增量作差,求残差!

我以前看这些代码很迷糊,但是你把Ceres熟悉之后,在明白VIO和GPS的优化原理,就很简单,下图是示意图:
VIO的数据和状态量的残差定义为:(j时刻的状态量 - i时刻的状态量)得到的增量-vio增量; 其意味着融合后的位置必须

和GPS位置尽可能重合,而两帧间的增量要和VIO尽可能相等。这对理解坐标转换至关重要,这样的处理意味着vio的数据

并不对融合后的绝对位置做约束,只要求融合后的位置增量和vio的增量误差尽可能小, 所以融合后的位置会在GPS坐标系下。

再来看一下 struct TError 结构体:

struct TError
{//定义数据的 t_x 、t_y 、 t_z 和 var 的析构函数TError(double t_x, double t_y, double t_z, double var):t_x(t_x), t_y(t_y), t_z(t_z), var(var){}template <typename T>bool operator()(const T* tj, T* residuals) const{residuals[0] = (tj[0] - T(t_x)) / T(var);residuals[1] = (tj[1] - T(t_y)) / T(var);residuals[2] = (tj[2] - T(t_z)) / T(var);return true;}//损失函数static ceres::CostFunction* Create(const double t_x, const double t_y, const double t_z, const double var) {return (new ceres::AutoDiffCostFunction<TError, 3, 3>(new TError(t_x, t_y, t_z, var)));}double t_x, t_y, t_z, var;};

理解:根据程序可知,残差是:VIO/GPS融合后的位置(优化变量) - gps观测值, 到此,优化部分结束!

注意:

WGPS_T_WVIO = WGPS_T_body * WVIO_T_body.inverse()

这导致了WGPS_T_WVIO这个外参包括了VIO计算的误差。什么意思呢,本来当vio足够精准时,

这样算出的WGPS_T_WVIO肯定是一个固定值。实际把WGPS_T_WVIO输出看,会随着时间一直变化。

变化原因是因为vio的结果和融合后的结果均带误差,WGPS_T_body * WVIO_T_body.inverse() 使得误差包含在里面了。

总结

优化流程:

首先呢,判断是否有gps数据,整体的算法就是在ceres架构下的优化算法。
所以总体的步骤就是ceres优化的步骤,首先添加优化参数块(AddParameterBlock函数),参数为globalPoseMap中的6维位姿(代码中旋转用四元数表示,所以共7维)。
之后添加CostFunction,通过构建factor重载operator方法实现(具体实现需要学习ceres库)。该部分有两个factor,一个是位姿图优化,另一个则是利用gps进行位置约束。
将factor添加后,进行ceres求解,更新此时gps和vio间的坐标系转换参数,之后再利用updateGlobalPath函数更新位姿。

https://blog.csdn.net/weixin_41843971/article/details/86748719

残差项:

接着开始添加残差项,总共有两个残差项分别是:vio factor以及gps factor

– vio factor:

残差项的costfunction创建由 relativeRTError来提供。观测值由vio的结果提供。此时计算的是以i时刻作为参考,从i到j这两个时刻的位移值以及四元数的旋转值作为观测值传递进入代价函数中。 此时iPj代表了i到j的位移,iQj代表了i到j的四元数变换。添加残差项的时候,需要添加当前i时刻的位姿以及j时刻的位姿。即用观测值来估计i时刻的位姿以及j时刻的位姿。

– gps factor:

残差项的costfunction创建由 TError来提供。观测值由Gps数据的结果提供。添加残差项的时候,只需要添加当前i时刻的位姿。

TError 及 RelativeRTError

直观上理解:
{0, 1,2,3,4, 5,6…}
要估计出这些时刻,每个时刻的位姿。
我有的是两个方面的观测值,一方面是GPS得到的每个时刻的位置(x,y,z)(并且GPS信号可以提供在该时刻得到这个位置的精度posAccuracy),没有累计误差,精度较低。另一方面是VIO得到的每个时刻的位置(x,y,z)以及对应的姿态四元数(w,x,y,z),存在累计误差,短时间内精度较高。为了得到更好的一个融合结果,因此我们采用这个策略:观测值中,初始位置由GPS提供,并且vio观测值信任的是i到j时刻的位移以及姿态变化量。 并不信任vio得到的一个绝对位移量以及绝对的旋转姿态量。只信任短期的i到j的变化量,用这个变化量作为整个代价函数的观测值,进行求解。
因此两个残差项TError及RelativeRTError分别对应的就是GPS位置信号以及vio短时间内估计的i到j时刻的位姿变化量对最终结果的影响。迭代求解的过程中均采用了AutoDiffCostFunction自动求解微分来进行迭代。
(1)TError
TError(x,y,z,accuracy),最后一项是定位精度,可以由GPS系统提供。残差除了直接观测值与真值相减以外,还除了这个accuracy作为分母。意味着精度越高,accuracy越小,对结果的影响就越大。
(2)RelativeRTError
RelativeRTError(dx,dy,dz,dqw,dqx,dqy,dqz,t_var,q_var),最后两项是位移以及旋转之间的权重分配比例,并且为了使得与TError对应。在程序中,应该是根据经验把最后两项设置成一个常值,分别对应0.1以及0.01。残差的得到就根据实际值与观测值之间的偏差直接得出。

原文链接:https://blog.csdn.net/huanghaihui_123/article/details/87183055

Local Factor: 局部观测约束,VIO相对位姿变换, 计算的是相邻两帧之间位姿的残差

VINS Fusion GPS融合部分相关推荐

  1. VINS - Fusion GPS/VIO 融合 二、数据融合

    https://zhuanlan.zhihu.com/p/75492883 一.简介 源代码:VINS - Fusion 数据集:KITTI 数据 程序入口:globalOptNode.cpp 二.程 ...

  2. vins中imu融合_双目版 VINS 项目发布,小觅双目摄像头作为双目惯导相机被推荐...

    继 VINS-Mono 和 VINS-Mobile 之后,香港科技大学沈劭劼老师实验室正式发布了双目版 VINS 开源项目 VINS-Fusion .项目推荐了小觅双目摄像头标准版(MYNT EYE ...

  3. vins中imu融合_小觅智能 | VINS 学习笔记(持续更新中)

    VINS 基本介绍 VINS-Mono 和 VINS-Mobile 是香港科技大学沈劭劼老师开源的单目视觉惯导 SLAM 方案.2017年发表于<IEEE Transactions on Rob ...

  4. vins-fusion gps融合相关总结

    1 . 简介: VINS Fusion在VINS Mono的基础上,添加了GPS等可以获取全局观测信息的传感器,使得VINS可以利用全局信息消除累计误差,进而减小闭环依赖. 相比于局部传感器,全局传感 ...

  5. 小觅相机D系列跑vins fusion

    有幸使用一款D系列小觅相机.今天测试了下它跑vins fusion的表现.其中有一个注意点,官方文档没有说明.这里记录一下. D系列小觅相机添加了IR散斑投射器,用来恢复没有纹理细节时的深度.这与老版 ...

  6. vins中imu融合_基于非线性优化算法—当视觉SLAM遇到VINS会碰撞出怎样的火花?

    今天来给大家分享一个视觉SLAM中比较综合且复杂的系统-VINS.VINS旨在通过融合两个传感器测量数据获得移动机器人的位姿和特征点在空间中的位置,在现代控制理论学科中属于最优估计问题.在移动智能机器 ...

  7. 北斗导航 | 惯性导航中的IMU与GPS融合之无人机位姿估计(IMU+GPS:附Matlab源代码)

    ============================================================ 博主github:https://github.com/MichaelBeec ...

  8. Vins-fusion GPS融合部分测试(自己的数据ZED+RTK)

    经过前一段时间的积累,目前暂时成功实现了用自己的数据测试实现Vins-fusion+GPS融合,其实放在数据采集处理上的时间比较多,踩了很多坑,效果在一些部分还不是很好,后期持续优化吧 其中黑色轨迹为 ...

  9. VINS-FUSION GPS融合坐标转换细节分析

    先贴两个对GPS融合的代码结构分析的博文,感谢博主们的分享. https://zhuanlan.zhihu.com/p/75492883 https://blog.csdn.net/weixin_41 ...

最新文章

  1. android EditText 修改光标的颜色值
  2. 算法-------二分法查找
  3. Android书籍推荐
  4. mysql matlab for循环嵌套循环语句吗_C++: for_each 和 基于范围的for循环
  5. Linux 内存管理 | 地址映射:分段、分页、段页
  6. 缓存三大问题及解决方案
  7. 掌握 Ajax,第 4 部分: 利用 DOM 进行 Web 响应
  8. STL - 底层实现
  9. 转载------------java equals 方法
  10. html语义化有哪些优点,语义化的HTML结构到底有什么好处?
  11. BZOJ1433 ZJOI2009 假期的宿舍 二分图匹配
  12. 对接GA/T1400协议注册流程简易demo【Java版】
  13. 程序员计算器使用方法介绍(快速计算十六进制、八进制的方法)
  14. 制作温馨浪漫爱心表白动画特效HTML5+jQuery【附源码】
  15. 【运筹学】线性规划数学模型 ( 求解基矩阵示例 | 矩阵的可逆性 | 线性规划表示为 基矩阵 基向量 非基矩阵 非基向量 形式 )
  16. 最近整理的面试笔试区别题
  17. 2312、卖木头块 | 面试官与狂徒张三的那些事(leetcode,附思维导图 + 全部解法)
  18. [MyBatis] 通过代码配置+XML文件构建SqlSessionFactory
  19. 程序人生之九:2012,回首这7年
  20. IronPython中使用Cecil类库指南

热门文章

  1. IBM中国武汉全球服务执行中心正式落成
  2. 浪潮服务器2003系统,浪潮服务器安装WINDOWS 2003操作系统课件.ppt
  3. 基于linux的web自动化(selenium+jenkins+linux+firefox)
  4. RedisCluster redis集群配置
  5. 【Android】之【延时执行的几种方法】
  6. 保姆级 | ChatGPT接入微信教程
  7. sql文字转换全拼_SQL汉字转拼音函数-支持首字母、全拼
  8. 力扣算法学习计划打卡:第一天
  9. CF1455A 题解
  10. VScode必备插件大全