前言

在上一篇博客(ALOAM:后端lasermapping通过Ceres进行帧到地图的位姿优化)中,通过Ceres优化得到了 当前帧到地图的最优位姿

下面要做的是更新地图模块中维护的一个位姿,这个位姿就是odom到map之间的位姿变换

为什么要更新这个位姿呢?
因为在前面这篇博客中(ALOAM:后端laserMapping代码结构与数据处理分析),在收到前端里程计数据后,会以前端里程计的频率,向外发布一个高频率当前帧到地图坐标系下的位姿。
在这里插入图片描述
这这里用到了T_map_odom,就是在后端通过Ceres得到当前帧到map的位姿后,再计算odom到map的位姿,所以要更新这个位姿,为下一帧做准备。

并且在进行栅格地图位置更新处理的时候,也通过上一帧维护的T_map_odom,得到当前帧的一个初值估计。所以得到当前帧到map的位姿后,需要更新odom到map的位姿,为下一帧的处理做准备。相关内容在这篇博客(ALOAM:后端lasermapping地图栅格化处理与提取)的这个地方:
在这里插入图片描述

odom到map之间的位姿更新原理

通过Ceres的优化 得到了,当前帧到map的位姿变换
在这里插入图片描述
前端里程计向后端发布的是,当前帧到odom的位姿变换
在这里插入图片描述
那么计算odom到map的位姿变换的公式为:
在这里插入图片描述
写成旋转+平移的齐次矩阵的形式,上面的公式则变为:
在这里插入图片描述
那么 odom到map之间旋转和平移则为
在这里插入图片描述

下面来看ALOAM中代码是如何实现的

代码解析

在前面通过ceres的优化得到了 当前帧到地图的位姿变换。该位姿变换存在 parameters 变量中。

这个parameters是一个double 的7维数组
在这里插入图片描述
前4位是旋转四元数,后三位是平移向量

然后程序则调用了

            //更新odom到map之间的位姿变换
            transformUpdate();

这个就是更新odom到map之间的位姿变换的函数。

里面的内容是:

//更新odom到map之间的位姿变换
void transformUpdate()
{
    q_wmap_wodom = q_w_curr * q_wodom_curr.inverse();
    t_wmap_wodom = t_w_curr - q_wmap_wodom * t_wodom_curr;
}

其中

  • q_wmap_wodom 就是 odom到map 的旋转 四元数
  • t_wmap_wodom 就是 odom到map 的平移

q_wodom_curr 是前端里程计发来的 curr到odom的 旋转四元数
t_wodom_curr 是前端里程计发来的 curr到odom的 平移

  • q_w_curr 是Ceres优化的 curr到map的旋转四元数
  • t_w_curr 是Ceres优化的 curr到map的平移

在整个程序中并没有发现q_w_curr 和 t_w_curr 被赋值的地方,尤其是ceres计算完后并没给这两个变量赋值。

那么这两个变量是如何被赋值的呢?
在它俩声明的地方:
在这里插入图片描述
是通过eigen的map声明的
当ceres 更新 parameters之后 会通过 eigen的map 实时更新q_w_curr和t_w_curr 不需要自己再去转换

这里简单介绍下 Eigen 的Map 类:
Eigen中定义了一系列的vector和matrix,相比copy数据,更一般的方式是复用数据的内存,将它们转变为Eigen类型。Map类很好地实现了这个功能。

简单理解Map就是将原始“连续内存存储”的数据,以矩阵形式重新组织。在使用Map时就需要原始数据,Map后的数据的维度形式,Map时使用的Stride设定。所谓Stride,既指矩阵中沿着矩阵列或行方向移动一个位置,内存中需要移动的位置数。这个需要移动的内存位置数与矩阵采用的存储方式有关(列或行主导)。

Eigen::Map原型

template<typename PlainObjectType, int MapOptions, typename StrideType> 
class Map: public MapBase<Map<PlainObjectType, MapOptions, StrideType> >
  • PlainObjectType :映射数据的等价矩阵类型
  • MapOptions:指定指针对齐方式,默认是未对齐的
  • StrideType:指定步长

这个类的作用就是让非Eigen数据结构变成Eigenj矩阵或者向量时,减少在复制过程中的开销,Eigen官网描述这样使用没有开销。

构造函数有以下

Map (PointerArgType dataPtr, const StrideType &stride=StrideType())
Map (PointerArgType dataPtr, Index size, const StrideType &stride=StrideType())
Map (PointerArgType dataPtr, Index rows, Index cols, const StrideType &stride=StrideType())

举个最简单的例子

    int array[9];
    for (int i = 0; i < 9; ++i)
        array[i] = i;
    std::cout << Eigen::Map<Eigen::Matrix3i>(array) << std::endl;

输出为:
0 3 6
1 4 7
2 5 8
再举一个和ALOAM使用情况一样的例子,将一个数组元素转换成Eigen数据结构

int data[] = {1,2,3,4,5,6,7,8,9};
Map<RowVectorXi> v(data,4);
cout << "The mapped vector v is: " << v << "\n";
new (&v) Map<RowVectorXi>(data+4,5);
cout << "Now v is: " << v << "\n

输出为:
The mapped vector v is: 1 2 3 4
Now v is: 5 6 7 8 9

看了这几个例子,就明白,在Ceres优化完成后,这两个变量(q_w_curr ,t_w_curr )就被更新值了。

  • q_w_curr 是Ceres优化的 curr到map的旋转四元数
  • t_w_curr 是Ceres优化的 curr到map的平移

transformUpdate() 函数中计算的公式就是上面推导的:

    q_wmap_wodom = q_w_curr * q_wodom_curr.inverse();
    t_wmap_wodom = t_w_curr - q_wmap_wodom * t_wodom_curr;

在这里插入图片描述

在后面后端会把 q_w_curr,t_w_curr做为里程计更新发布出来

            nav_msgs::Odometry odomAftMapped;
            odomAftMapped.header.frame_id = "/camera_init";
            odomAftMapped.child_frame_id = "/aft_mapped";
            odomAftMapped.header.stamp = ros::Time().fromSec(timeLaserOdometry);
            odomAftMapped.pose.pose.orientation.x = q_w_curr.x();
            odomAftMapped.pose.pose.orientation.y = q_w_curr.y();
            odomAftMapped.pose.pose.orientation.z = q_w_curr.z();
            odomAftMapped.pose.pose.orientation.w = q_w_curr.w();
            odomAftMapped.pose.pose.position.x = t_w_curr.x();
            odomAftMapped.pose.pose.position.y = t_w_curr.y();
            odomAftMapped.pose.pose.position.z = t_w_curr.z();
            pubOdomAftMapped.publish(odomAftMapped);

在这里插入图片描述
可以在rviz中添加一个odometry的选项,选择topic为/aft_mapped_to_init来显示。