VINS_FUSION入门系列---GPS与VIO融合

2023-12-18 04:58

本文主要是介绍VINS_FUSION入门系列---GPS与VIO融合,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转载自:https://blog.csdn.net/pj_find/article/details/106566838

VINS_FUSION入门系列---GPSVIO融合

pj_find 2020-06-05 14:32:28 878 收藏 7

文章标签: 自动驾驶

版权

参考的博客:https://blog.csdn.net/subiluo/article/details/105429471
http://www.luyixian.cn/news_show_313718.aspx
在这里插入图片描述

state: 状态量,位姿,速度,bias等
Local Factor: 局部观测约束,VIO相对位姿变换, 计算的是相邻两帧之间位姿的残差

1.代码的输入输出

输入输出的定义都在globalOptNode.cpp这个文件里面

    ros::Subscriber sub_GPS = n.subscribe("/gps", 100, GPS_callback);ros::Subscriber sub_vio = n.subscribe("/vins_estimator/odometry", 100, vio_callback);pub_global_path = n.advertise<nav_msgs::Path>("global_path", 100);pub_global_odometry = n.advertise<nav_msgs::Odometry>("global_odometry", 100);pub_car = n.advertise<visualization_msgs::MarkerArray>("car_model", 1000);

vio的输入函数

void vio_callback(const nav_msgs::Odometry::ConstPtr &pose_msg)
{//printf("vio_callback! \n");double t = pose_msg->header.stamp.toSec();last_vio_t = t;//获取VIO输出的位置(三维向量),姿态(四元数)Eigen::Vector3d vio_t(pose_msg->pose.pose.position.x, pose_msg->pose.pose.position.y, pose_msg->pose.pose.position.z);Eigen::Quaterniond vio_q;vio_q.w() = pose_msg->pose.pose.orientation.w;vio_q.x() = pose_msg->pose.pose.orientation.x;vio_q.y() = pose_msg->pose.pose.orientation.y;vio_q.z() = pose_msg->pose.pose.orientation.z;//位姿传入global Estimator中globalEstimator.inputOdom(t, vio_t, vio_q);m_buf.lock();//寻找与VIO时间戳相对应的GPS信息while(!gpsQueue.empty()){//获得最老的GPS数据和其时间sensor_msgs::NavSatFixConstPtr GPS_msg = gpsQueue.front();double gps_t = GPS_msg->header.stamp.toSec();printf("vio t: %f, gps t: %f \n", t, gps_t);// 10ms sync toleranceif(gps_t >= t - 0.01 && gps_t <= t + 0.01){//printf("receive GPS with timestamp %f\n", GPS_msg->header.stamp.toSec());//GPS的经纬度,海拔高度double latitude = GPS_msg->latitude;double longitude = GPS_msg->longitude;double altitude = GPS_msg->altitude;//int numSats = GPS_msg->status.service;//GPS数据的方差double pos_accuracy = GPS_msg->position_covariance[0];if(pos_accuracy <= 0)pos_accuracy = 1;//printf("receive covariance %lf \n", pos_accuracy);//if(GPS_msg->status.status > 8)//向globalEstimator中输入GPS数据globalEstimator.inputGPS(t, latitude, longitude, altitude, pos_accuracy);gpsQueue.pop();//此处break,意味只存储了一个GPS数据后就break了。后来想明白了GPS不同于imu,是绝对位break;}else if(gps_t < t - 0.01)gpsQueue.pop();else if(gps_t > t + 0.01)break;}m_buf.unlock();Eigen::Vector3d global_t;Eigen:: Quaterniond global_q;globalEstimator.getGlobalOdom(global_t, global_q);nav_msgs::Odometry odometry;odometry.header = pose_msg->header;odometry.header.frame_id = "world";odometry.child_frame_id = "world";odometry.pose.pose.position.x = global_t.x();odometry.pose.pose.position.y = global_t.y();odometry.pose.pose.position.z = global_t.z();odometry.pose.pose.orientation.x = global_q.x();odometry.pose.pose.orientation.y = global_q.y();odometry.pose.pose.orientation.z = global_q.z();odometry.pose.pose.orientation.w = global_q.w();pub_global_odometry.publish(odometry);pub_global_path.publish(*global_path);publish_car_model(t, global_t, global_q);// write result to filestd::ofstream foutC("/home/tony-ws1/output/vio_global.csv", ios::app);foutC.setf(ios::fixed, ios::floatfield);foutC.precision(0);foutC << pose_msg->header.stamp.toSec() * 1e9 << ",";foutC.precision(5);foutC << global_t.x() << ","<< global_t.y() << ","<< global_t.z() << ","<< global_q.w() << ","<< global_q.x() << ","<< global_q.y() << ","<< global_q.z() << endl;foutC.close();
}

再看一下inputOdom函数,这个函数把vio的结果存储在localPoseMap中,然后使用外参 WGPS_T_WVIO把VIO的结果转换到GPS坐标系下存储在globalPoseMap中。注意,此时我们考虑刚开始gps没对齐时,此时外参WGPS_T_WVIO是单位矩阵,所以globalPoseMap里的位姿和VIO的结果一样。globalPoseMap是存储融合优化后的位姿的,这也符合逻辑:在没有gps数据时,融合结果完全和VIO一样。我的理解就是这个值也是放在GPS里面进行优化状态量的初始值,然后进行迭代优化的.
在这里插入图片描述

void GlobalOptimization::inputOdom(double t, Eigen::Vector3d OdomP, Eigen::Quaterniond OdomQ)
{mPoseMap.lock();// 把vio直接输出的位姿存入 localPoseMap 中vector<double> localPose{OdomP.x(), OdomP.y(), OdomP.z(), OdomQ.w(), OdomQ.x(), OdomQ.y(), OdomQ.z()};localPoseMap[t] = localPose;// 把VIO转换到GPS坐标系下,准确的说是转换到以第一帧GPS为原点的坐标系下// 转换之后的位姿插入到globalPoseMap 中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;lastP = globalP;lastQ = globalQ;//把vio转换坐标系后的结果赋值给global_path,给最新传入的一个初始值。geometry_msgs::PoseStamped pose_stamped;pose_stamped.header.stamp = ros::Time(t);pose_stamped.header.frame_id = "world";pose_stamped.pose.position.x = lastP.x();pose_stamped.pose.position.y = lastP.y();pose_stamped.pose.position.z = lastP.z();pose_stamped.pose.orientation.x = lastQ.x();pose_stamped.pose.orientation.y = lastQ.y();pose_stamped.pose.orientation.z = lastQ.z();pose_stamped.pose.orientation.w = lastQ.w();global_path.header = pose_stamped.header;global_path.poses.push_back(pose_stamped);mPoseMap.unlock();
}

gps信号转换坐标系
这里是输入的VO数据的10ms内的GPS数据进行坐标系转换,GPS2XYZ函数将GPS的经纬度坐标转换为ENU坐标,并且第一帧的GPS数据作为原点[0,0,0]转换之后的gps数据和协方差一起存入到GPSPositionMap中

void GlobalOptimization::inputGPS(double t, double latitude, double longitude, double altitude, double posAccuracy)
{double xyz[3];// 因为经纬度表示的是地球上的坐标,而地球是一个球形,// 需要首先把经纬度转化到平面坐标系上// 值得一提的是,GPS2XYZ()并非把经纬度转化到世界坐标系下(以0经度,0纬度为原点),// 而是以第一帧GPS数据为坐标原点,这一点需要额外注意GPS2XYZ(latitude, longitude, altitude, xyz);// 存入经纬度计算出的平面坐标,存入GPSPositionMap中vector<double> tmp{xyz[0], xyz[1], xyz[2], posAccuracy};//printf("new gps: t: %f x: %f y: %f z:%f \n", t, tmp[0], tmp[1], tmp[2]);GPSPositionMap[t] = tmp;newGPS = true;}

接着就是进行优化了,先讲VIO的优化,加载优化的相关参数.

map<double, vector<double>>::iterator iter;iter = globalPoseMap.begin();for (int i = 0; i < length; i++, iter++){t_array[i][0] = iter->second[0];t_array[i][1] = iter->second[1];t_array[i][2] = iter->second[2];q_array[i][0] = iter->second[3];q_array[i][1] = iter->second[4];q_array[i][2] = iter->second[5];q_array[i][3] = iter->second[6];problem.AddParameterBlock(q_array[i], 4, local_parameterization);problem.AddParameterBlock(t_array[i], 3);}

将观测值两帧VIO的差传入到优化框架中

                //vio factoriterVIONext = iterVIO;iterVIONext++;// 添加VIO残差,观测量是两帧VIO数据之差,是相对的。而下面的GPS是绝对的if(iterVIONext != localPoseMap.end()){Eigen::Matrix4d wTi = Eigen::Matrix4d::Identity();Eigen::Matrix4d wTj = Eigen::Matrix4d::Identity();wTi.block<3, 3>(0, 0) = Eigen::Quaterniond(iterVIO->second[3], iterVIO->second[4], iterVIO->second[5], iterVIO->second[6]).toRotationMatrix();wTi.block<3, 1>(0, 3) = Eigen::Vector3d(iterVIO->second[0], iterVIO->second[1], iterVIO->second[2]);wTj.block<3, 3>(0, 0) = Eigen::Quaterniond(iterVIONext->second[3], iterVIONext->second[4], iterVIONext->second[5], iterVIONext->second[6]).toRotationMatrix();wTj.block<3, 1>(0, 3) = Eigen::Vector3d(iterVIONext->second[0], iterVIONext->second[1], iterVIONext->second[2]);Eigen::Matrix4d iTj = wTi.inverse() * wTj;Eigen::Quaterniond iQj;iQj = iTj.block<3, 3>(0, 0);Eigen::Vector3d iPj = iTj.block<3, 1>(0, 3);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]);

,而VIO的数据和状态量的残差定义为:(j时刻的状态量-i时刻的状态量)得到的增量-vio增量。其意味着融合后的位置必须和GPS位置尽可能重合,而两帧间的增量要和VIO尽可能相等。这对理解坐标转换至关重要,这样的处理意味着vio的数据并不对融合后的绝对位置做约束,只要求融合后的位置增量和vio的增量误差尽可能小。所以融合后的位置会在GPS坐标系下。

	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];//世界坐标系下ij帧的位置增量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);T t_i_ij[3];//i帧坐标系下,ij帧的位置增量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];//传入观测的四元数增量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;}

GPS因子的优化.
加载GPS观测值

//gps factor// GPS残差,这个观测量直接就是GPS的测量数据,// 残差计算的是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]);//printf("inverse weight %f \n", iterGPS->second[3]);problem.AddResidualBlock(gps_function, loss_function, t_array[i]);

这个里的状态量就是前面从VIO坐标系转到GPS坐标系作为初值进行优化.

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){}//首先是GPS的数据和状态量定义的残差:状态量位置-gps算出来的位置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;}

在优化求解完成以后,会遍历globalPoseMap,对其更新。即globalPoseMap里面存储着优化后的位姿。同时,之后会用在VIO坐标系下的位置(localPoseMap里)和优化后在GPS坐标系下的位置(globalPose)对外参WGPS_T_WVIO进行更新。还是考虑第一次同步的gps数据传入,优化完成后。外参WGPS_T_WVIO之前是单位矩阵,而第一次更新,会算出真正和实际相符的外参。

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();double t = iter->first;WVIO_T_body.block<3, 3>(0, 0) = Eigen::Quaterniond(localPoseMap[t][3], localPoseMap[t][4], localPoseMap[t][5], localPoseMap[t][6]).toRotationMatrix();WVIO_T_body.block<3, 1>(0, 3) = Eigen::Vector3d(localPoseMap[t][0], localPoseMap[t][1], localPoseMap[t][2]);WGPS_T_body.block<3, 3>(0, 0) = Eigen::Quaterniond(globalPose[3], globalPose[4], globalPose[5], globalPose[6]).toRotationMatrix();WGPS_T_body.block<3, 1>(0, 3) = Eigen::Vector3d(globalPose[0], globalPose[1], globalPose[2]);WGPS_T_WVIO = WGPS_T_body * WVIO_T_body.inverse();}}updateGlobalPath();

这篇关于VINS_FUSION入门系列---GPS与VIO融合的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/507115

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

poj 2104 and hdu 2665 划分树模板入门题

题意: 给一个数组n(1e5)个数,给一个范围(fr, to, k),求这个范围中第k大的数。 解析: 划分树入门。 bing神的模板。 坑爹的地方是把-l 看成了-1........ 一直re。 代码: poj 2104: #include <iostream>#include <cstdio>#include <cstdlib>#include <al

MySQL-CRUD入门1

文章目录 认识配置文件client节点mysql节点mysqld节点 数据的添加(Create)添加一行数据添加多行数据两种添加数据的效率对比 数据的查询(Retrieve)全列查询指定列查询查询中带有表达式关于字面量关于as重命名 临时表引入distinct去重order by 排序关于NULL 认识配置文件 在我们的MySQL服务安装好了之后, 会有一个配置文件, 也就

韦季李输入法_输入法和鼠标的深度融合

在数字化输入的新纪元,传统键盘输入方式正悄然进化。以往,面对实体键盘,我们常需目光游离于屏幕与键盘之间,以确认指尖下的精准位置。而屏幕键盘虽直观可见,却常因占据屏幕空间,迫使我们在操作与视野间做出妥协,频繁调整布局以兼顾输入与界面浏览。 幸而,韦季李输入法的横空出世,彻底颠覆了这一现状。它不仅对输入界面进行了革命性的重构,更巧妙地将鼠标这一传统外设融入其中,开创了一种前所未有的交互体验。 想象

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0