2021@SDUSC
2021年11月2日星期二——2021年11月4日星期四
2021年11月10日星期三——2021年11月11日星期四
附:发烧导致学习进度中断。
经过前几周对于雷达部分的学习,我对于ros以及LVI-SLAM包有了初步的认识。我们小组的下一个目标就是视觉部分,经过讨论和对于信息流的分析,我们决定按照visual_estimator,visual_feature和visual_loop的顺序来学习。
因为LVI-SAM的视觉部分是建立在VINS-MONO的基础之上的,而后者的包的分析资料网上有很多,所以我们可以以此作为参考来学习。但是同时也要重点注意LVI-SAM是融合了雷达和视觉系统的存在,在查阅资料时要注意区分不同点。
根据对于整个项目的规划,我计划利用十篇文章左右的篇幅来介绍这个visual_estimator。
在visual-estimator包下,我的第一个任务是对于initial部分的分析。
在学习的过程中,我发现在阅读代码时经常会陷入不知道在解释什么的困境,于是决定回到VIN-Mono这篇论文中去,通过阅读论文来为后续的学习铺垫基础。因此,在继续这部分的学习之前,我先阅读一下VIN-Mono的论文。
在visual_estimator下,主要有三个文件夹:factor,initial和utility及3个头文件与4个cpp文件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ERHJtNew-1636788956482)(C:\Users\Fan luke\Desktop\image-20211102103333149.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XvtC7tCi-1636788956486)(C:\Users\Fan luke\Desktop\image-20211110151329327.png)]
文件夹内部一共有四个cpp文件和四个与之对应的头文件。
这里参考了古月局上大佬的分析图,出处附在文末,文章中给出了详细的数学推导过程。
我正是因为卡在了这个地方,所以4号的时候不得不去看了看论文的讲解,并把学习的内容记录在了上一篇博客之中,这里就不再赘述了。
using namespace Eigen;
using namespace std;
class ImageFrame
{
public:
ImageFrame(){};
ImageFrame(const map<int, vector<pair<int, Eigen::Matrix<double, 8, 1>>>>& _points,
const vector<float> &_lidar_initialization_info,
double _t):
t{_t}, is_key_frame{false}, reset_id{-1}, gravity{9.805}
{
points = _points;
// reset id in case lidar odometry relocate
reset_id = (int)round(_lidar_initialization_info[0]);
// Pose
T.x() = _lidar_initialization_info[1];
T.y() = _lidar_initialization_info[2];
T.z() = _lidar_initialization_info[3];
// Rotation
Eigen::Quaterniond Q = Eigen::Quaterniond(_lidar_initialization_info[7],
_lidar_initialization_info[4],
_lidar_initialization_info[5],
_lidar_initialization_info[6]);
R = Q.normalized().toRotationMatrix();
// Velocity
V.x() = _lidar_initialization_info[8];
V.y() = _lidar_initialization_info[9];
V.z() = _lidar_initialization_info[10];
// Acceleration bias
Ba.x() = _lidar_initialization_info[11];
Ba.y() = _lidar_initialization_info[12];
Ba.z() = _lidar_initialization_info[13];
// Gyroscope bias
Bg.x() = _lidar_initialization_info[14];
Bg.y() = _lidar_initialization_info[15];
Bg.z() = _lidar_initialization_info[16];
// Gravity
gravity = _lidar_initialization_info[17];
};
map<int, vector<pair<int, Eigen::Matrix<double, 8, 1>> > > points;
double t;
IntegrationBase *pre_integration;
bool is_key_frame;
// Lidar odometry info
int reset_id;
Vector3d T;
Matrix3d R;
Vector3d V;
Vector3d Ba;
Vector3d Bg;
double gravity;
};
bool VisualIMUAlignment(map<double, ImageFrame> &all_image_frame, Vector3d* Bgs, Vector3d &g, VectorXd &x);
class odometryRegister
{
public:
ros::NodeHandle n;
tf::Quaternion q_lidar_to_cam;
Eigen::Quaterniond q_lidar_to_cam_eigen;
ros::Publisher pub_latest_odometry;
odometryRegister(ros::NodeHandle n_in):
n(n_in)
{
q_lidar_to_cam = tf::Quaternion(0, 1, 0, 0); // rotate orientation // mark: camera - lidar
q_lidar_to_cam_eigen = Eigen::Quaterniond(0, 0, 0, 1); // rotate position by pi, (w, x, y, z) // mark: camera - lidar
// pub_latest_odometry = n.advertise("odometry/test", 1000);
}
// convert odometry from ROS Lidar frame to VINS camera frame
vector<float> getOdometry(deque<nav_msgs::Odometry>& odomQueue, double img_time)
{
vector<float> odometry_channel;
odometry_channel.resize(18, -1); // reset id(1), P(3), Q(4), V(3), Ba(3), Bg(3), gravity(1)
nav_msgs::Odometry odomCur;
// pop old odometry msg
while (!odomQueue.empty())
{
if (odomQueue.front().header.stamp.toSec() < img_time - 0.05)
odomQueue.pop_front();
else
break;
}
if (odomQueue.empty())
{
return odometry_channel;
}
// find the odometry time that is the closest to image time
for (int i = 0; i < (int)odomQueue.size(); ++i)
{
odomCur = odomQueue[i];
if (odomCur.header.stamp.toSec() < img_time - 0.002) // 500Hz imu
continue;
else
break;
}
// time stamp difference still too large
if (abs(odomCur.header.stamp.toSec() - img_time) > 0.05)
{
return odometry_channel;
}
// convert odometry rotation from lidar ROS frame to VINS camera frame (only rotation, assume lidar, camera, and IMU are close enough)
tf::Quaternion q_odom_lidar;
tf::quaternionMsgToTF(odomCur.pose.pose.orientation, q_odom_lidar);
tf::Quaternion q_odom_cam = tf::createQuaternionFromRPY(0, 0, M_PI) * (q_odom_lidar * q_lidar_to_cam); // global rotate by pi // mark: camera - lidar
tf::quaternionTFToMsg(q_odom_cam, odomCur.pose.pose.orientation);
// convert odometry position from lidar ROS frame to VINS camera frame
Eigen::Vector3d p_eigen(odomCur.pose.pose.position.x, odomCur.pose.pose.position.y, odomCur.pose.pose.position.z);
Eigen::Vector3d v_eigen(odomCur.twist.twist.linear.x, odomCur.twist.twist.linear.y, odomCur.twist.twist.linear.z);
Eigen::Vector3d p_eigen_new = q_lidar_to_cam_eigen * p_eigen;
Eigen::Vector3d v_eigen_new = q_lidar_to_cam_eigen * v_eigen;
odomCur.pose.pose.position.x = p_eigen_new.x();
odomCur.pose.pose.position.y = p_eigen_new.y();
odomCur.pose.pose.position.z = p_eigen_new.z();
odomCur.twist.twist.linear.x = v_eigen_new.x();
odomCur.twist.twist.linear.y = v_eigen_new.y();
odomCur.twist.twist.linear.z = v_eigen_new.z();
// odomCur.header.stamp = ros::Time().fromSec(img_time);
// odomCur.header.frame_id = "vins_world";
// odomCur.child_frame_id = "vins_body";
// pub_latest_odometry.publish(odomCur);
odometry_channel[0] = odomCur.pose.covariance[0];
odometry_channel[1] = odomCur.pose.pose.position.x;
odometry_channel[2] = odomCur.pose.pose.position.y;
odometry_channel[3] = odomCur.pose.pose.position.z;
odometry_channel[4] = odomCur.pose.pose.orientation.x;
odometry_channel[5] = odomCur.pose.pose.orientation.y;
odometry_channel[6] = odomCur.pose.pose.orientation.z;
odometry_channel[7] = odomCur.pose.pose.orientation.w;
odometry_channel[8] = odomCur.twist.twist.linear.x;
odometry_channel[9] = odomCur.twist.twist.linear.y;
odometry_channel[10] = odomCur.twist.twist.linear.z;
odometry_channel[11] = odomCur.pose.covariance[1];
odometry_channel[12] = odomCur.pose.covariance[2];
odometry_channel[13] = odomCur.pose.covariance[3];
odometry_channel[14] = odomCur.pose.covariance[4];
odometry_channel[15] = odomCur.pose.covariance[5];
odometry_channel[16] = odomCur.pose.covariance[6];
odometry_channel[17] = odomCur.pose.covariance[7];
return odometry_channel;
}
};
头文件定义了两个类和一个函数。
文件开头是利用相机的旋转来标定imu的bias的函数。
#include ……………
void solveGyroscopeBias(map<double, ImageFrame> &all_image_frame, Vector3d* Bgs)
{
//接受参数,包括帧、矩阵、滑窗内的帧、以及三维信息方位bgs,
Matrix3d A;
Vector3d b;
Vector3d delta_bg;
A.setZero();
b.setZero();
map<double, ImageFrame>::iterator frame_i;
map<double, ImageFrame>::iterator frame_j;
//下边是利用公式,根据滑窗和帧来计算,for循环对应了公式当中的求和部分。根据介绍的公式是对应了Ax=B
for (frame_i = all_image_frame.begin(); next(frame_i) != all_image_frame.end(); frame_i++)
{
frame_j = next(frame_i);
MatrixXd tmp_A(3, 3);
tmp_A.setZero();
VectorXd tmp_b(3);
tmp_b.setZero();
Eigen::Quaterniond q_ij(frame_i->second.R.transpose() * frame_j->second.R);
tmp_A = frame_j->second.pre_integration->jacobian.template block<3, 3>(O_R, O_BG);
tmp_b = 2 * (frame_j->second.pre_integration->delta_q.inverse() * q_ij).vec();
A += tmp_A.transpose() * tmp_A;
b += tmp_A.transpose() * tmp_b;
}
//A是一个矩阵,可以LDLT分解,这里的ldlt函数正是这个功能,利用了IDLT分解求解线性方程。
delta_bg = A.ldlt().solve(b);
//传递ROS信息流
ROS_INFO_STREAM("gyroscope bias initial calibration " << delta_bg.transpose());
//给滑窗内的imu预积分加上角速度bias
for (int i = 0; i <= WINDOW_SIZE; i++)
Bgs[i] += delta_bg;
//重新计算预积分。
for (frame_i = all_image_frame.begin(); next(frame_i) != all_image_frame.end( ); frame_i++)
{
frame_j = next(frame_i);
frame_j->second.pre_integration->repropagate(Vector3d::Zero(), Bgs[0]);
}
}
利用gw的模长已知这个先验条件进一步优化gc0
说明:这里的模长就是gw我们所测得真实的重力大小,
void RefineGravity(map<double, ImageFrame> &all_image_frame, Vector3d &g, VectorXd &x)
{
Vector3d g0 = g.normalized() * G.norm();
Vector3d lx, ly;
//VectorXd x;
int all_frame_count = all_image_frame.size();
int n_state = all_frame_count * 3 + 2 + 1;
MatrixXd A{n_state, n_state};
A.setZero();
VectorXd b{n_state};
b.setZero();
map<double, ImageFrame>::iterator frame_i;
map<double, ImageFrame>::iterator frame_j;
//一开始也是设置参数,接受容器,这里需要注意下这个n维的Vector,根据论文中的公式可知,这里是用来建立切向空间的:> 因此,我们在其切线空间上用两个变量重新参数化重力
for(int k = 0; k < 4; k++)
{
MatrixXd lxly(3, 2);
//建立切向空间的函数
lxly = TangentBasis(g0);
int i = 0;
//代入公式,求解答案
for (frame_i = all_image_frame.begin(); next(frame_i) != all_image_frame.end(); frame_i++, i++)
{
frame_j = next(frame_i);
MatrixXd tmp_A(6, 9);
tmp_A.setZero();
VectorXd tmp_b(6);
tmp_b.setZero();
double dt = frame_j->second.pre_integration->sum_dt;
tmp_A.block<3, 3>(0, 0) = -dt * Matrix3d::Identity();
tmp_A.block<3, 2>(0, 6) = frame_i->second.R.transpose() * dt * dt / 2 * Matrix3d::Identity() * lxly;
tmp_A.block<3, 1>(0, 8) = frame_i->second.R.transpose() * (frame_j->second.T - frame_i->second.T) / 100.0;
tmp_b.block<3, 1>(0, 0) = frame_j->second.pre_integration->delta_p + frame_i->second.R.transpose() * frame_j->second.R * TIC[0] - TIC[0] - frame_i->second.R.transpose() * dt * dt / 2 * g0;
tmp_A.block<3, 3>(3, 0) = -Matrix3d::Identity();
tmp_A.block<3, 3>(3, 3) = frame_i->second.R.transpose() * frame_j->second.R;
tmp_A.block<3, 2>(3, 6) = frame_i->second.R.transpose() * dt * Matrix3d::Identity() * lxly;
tmp_b.block<3, 1>(3, 0) = frame_j->second.pre_integration->delta_v - frame_i->second.R.transpose() * dt * Matrix3d::Identity() * g0;
Matrix<double, 6, 6> cov_inv = Matrix<double, 6, 6>::Zero();
//cov.block<6, 6>(0, 0) = IMU_cov[i + 1];
//MatrixXd cov_inv = cov.inverse();
cov_inv.setIdentity();
MatrixXd r_A = tmp_A.transpose() * cov_inv * tmp_A;
VectorXd r_b = tmp_A.transpose() * cov_inv * tmp_b;
A.block<6, 6>(i * 3, i * 3) += r_A.topLeftCorner<6, 6>();
b.segment<6>(i * 3) += r_b.head<6>();
A.bottomRightCorner<3, 3>() += r_A.bottomRightCorner<3, 3>();
b.tail<3>() += r_b.tail<3>();
A.block<6, 3>(i * 3, n_state - 3) += r_A.topRightCorner<6, 3>();
A.block<3, 6>(n_state - 3, i * 3) += r_A.bottomLeftCorner<3, 6>();
}
//ldlt法求解线性方程,并得到最终优化结果
A = A * 1000.0;
b = b * 1000.0;
x = A.ldlt().solve(b);
VectorXd dg = x.segment<2>(n_state - 3);
g0 = (g0 + lxly * dg).normalized() * G.norm();
//double s = x(n_state - 1);
}
g = g0;
}
这个函数利用imu的平移来估计重力、各个bk帧的速度以及尺度的函数。
bool LinearAlignment(map<double, ImageFrame> &all_image_frame, Vector3d &g, VectorXd &x)
{ //开头依旧是接受参数,设置容器
int all_frame_count = all_image_frame.size();
int n_state = all_frame_count * 3 + 3 + 1;
MatrixXd A{n_state, n_state};
A.setZero();
VectorXd b{n_state};
b.setZero();
map<double, ImageFrame>::iterator frame_i;
map<double, ImageFrame>::iterator frame_j;
int i = 0;
//下边的for循环也是利用公式来计算求解的形式
for (frame_i = all_image_frame.begin(); next(frame_i) != all_image_frame.end(); frame_i++, i++)
{
frame_j = next(frame_i);
MatrixXd tmp_A(6, 10);
tmp_A.setZero();
VectorXd tmp_b(6);
tmp_b.setZero();
double dt = frame_j->second.pre_integration->sum_dt;
tmp_A.block<3, 3>(0, 0) = -dt * Matrix3d::Identity();
tmp_A.block<3, 3>(0, 6) = frame_i->second.R.transpose() * dt * dt / 2 * Matrix3d::Identity();
tmp_A.block<3, 1>(0, 9) = frame_i->second.R.transpose() * (frame_j->second.T - frame_i->second.T) / 100.0;
tmp_b.block<3, 1>(0, 0) = frame_j->second.pre_integration->delta_p + frame_i->second.R.transpose() * frame_j->second.R * TIC[0] - TIC[0];
//cout << "delta_p " << frame_j->second.pre_integration->delta_p.transpose() << endl;
tmp_A.block<3, 3>(3, 0) = -Matrix3d::Identity();
tmp_A.block<3, 3>(3, 3) = frame_i->second.R.transpose() * frame_j->second.R;
tmp_A.block<3, 3>(3, 6) = frame_i->second.R.transpose() * dt * Matrix3d::Identity();
tmp_b.block<3, 1>(3, 0) = frame_j->second.pre_integration->delta_v;
//cout << "delta_v " << frame_j->second.pre_integration->delta_v.transpose() << endl;
Matrix<double, 6, 6> cov_inv = Matrix<double, 6, 6>::Zero();
//cov.block<6, 6>(0, 0) = IMU_cov[i + 1];
//MatrixXd cov_inv = cov.inverse();
cov_inv.setIdentity();
MatrixXd r_A = tmp_A.transpose() * cov_inv * tmp_A;
VectorXd r_b = tmp_A.transpose() * cov_inv * tmp_b;
A.block<6, 6>(i * 3, i * 3) += r_A.topLeftCorner<6, 6>();
b.segment<6>(i * 3) += r_b.head<6>();
A.bottomRightCorner<4, 4>() += r_A.bottomRightCorner<4, 4>();
b.tail<4>() += r_b.tail<4>();
A.block<6, 4>(i * 3, n_state - 4) += r_A.topRightCorner<6, 4>();
A.block<4, 6>(n_state - 4, i * 3) += r_A.bottomLeftCorner<4, 6>();
}
//为什么要乘上一千呢?
A = A * 1000.0;
b = b * 1000.0;
//求解,依旧是LDLT分解
x = A.ldlt().solve(b);
//校正,校正这偏大的100?
double s = x(n_state - 1) / 100.0;
ROS_INFO("estimated scale: %f", s);
//评估预测的重力是否正确,偏差大于一或是负数都表示计算出现了错误。
g = x.segment<3>(n_state - 4);
ROS_INFO_STREAM(" result g " << g.norm() << " " << g.transpose());
if(fabs(g.norm() - G.norm()) > 1.0 || s < 0)
{
return false;
}
RefineGravity(all_image_frame, g, x);
s = (x.tail<1>())(0) / 100.0;
(x.tail<1>())(0) = s;
ROS_INFO_STREAM(" refine " << g.norm() << " " << g.transpose());
if(s < 0.0 )
return false;
else
return true;
}
这个是建立切向空间的函数,在重力优化时用到了。
MatrixXd TangentBasis(Vector3d &g0)
{
Vector3d b, c;
Vector3d a = g0.normalized();
Vector3d tmp(0, 0, 1);
if(a == tmp)
tmp << 1, 0, 0;
b = (tmp - a * (a.transpose() * tmp)).normalized();
c = a.cross(b);
MatrixXd bc(3, 2);
bc.block<3, 1>(0, 0) = b;
bc.block<3, 1>(0, 1) = c;
return bc;
}
调用上述的函数,得到最终结果。
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;
}
ROS中的数据操作需要线性代数,Eigen库是C++中的线性代数计算库。
Eigen库独立于ROS,但是在ROS中可以使用。
Eigen库可以参见http://eigen.tuxfamily.org
可以理解成是python中的numpy。
其中Eigen下不仅有矩阵,还有向量。
向量是矩阵的特殊情况,也是用矩阵定义的。定义如下:
typedef Matrix Vector3f;
typedef Matrix RowVector2i;12
这里的Vector是几维的就是几d。
我们见到的vector3d是3维的。
而vector3是另一个包下的,有geometry_msgs/Vector3 Message。
g表示的是gravity重力,w,c0都是我们之前提到的坐标系的名字,分别表示的是真实世界和参考c0.
我们在论文中介绍了重力精细化的步骤,表现在代码中就是RefineGravity这个函数。
代码中涉及到Vector的地方有很多,不了解这些常用函数的话,看起来寸步难行,有必要学习一下。
读了文章之后有了初步的印象,把代码中出现的几个摘录下来:
C.setZero(rows,cols) // C = zeros(rows,cols) //全零矩阵
x.segment(i, n) // x(i+1 : i+n) //切片
x.segment(i) // x(i+1 : i+n) //切片R.transpose() // R.’ or conj(R’) // 可读/写 转置
x.norm() // norm(x). //范数(注意:Eigen中没有norm®)
在Eigen中最基本的快操作运算是用
.block()
完成的。提取的子矩阵同样分为动态大小和固定大小。x.cross(y) // cross(x, y) //交叉积,需要头文件 #include
VINS-Mono
VINS-Mono翻译_Pancheng1的博客-CSDN博客_vins-mono
Visual-Inertial Odometry - 知乎 (zhihu.com)
【SLAM】VINS-MONO解析——综述_iwanderu的博客-CSDN博客
从零手写VIO——(一)Introduction - 古月居 (guyuehome.com)
vi_sfm - ROS Wiki
详解SLAM中的两种常用的三角化求解地标点的方法_yg838457845的博客-CSDN博客_slam三角化
视觉SLAM中的对极约束、三角测量、PnP、ICP问题 - 古月居 (guyuehome.com)
例说姿态解算与导航18-IMU误差源 - 知乎 (zhihu.com)
SFM原理简介_lpj822的专栏-CSDN博客
[ROS中使用Eigen库不定期更新]_陈显森的博客-CSDN博客
EIgen:Matricx和vector类的定义和使用_飘零过客-CSDN博客
Eigen常用函数_yanzhiwen2的博客-CSDN博客
Eigen教程2----MatrixXd和VectorXd的用法_MaybeTnT的博客-CSDN博客