算法流程分三步:
几个点在一条直线上,则容易知道该协方差矩阵的秩为1;而当这几个点近似在一条直线上时,则协方差矩阵的一个特征值会明显大于另外一个。当大致均匀分布时,两个特征值会大小类似。所以说点云能够反映点云的局部形态。
NDT 改进方向
目前的nod_omp
是改进版本,使用OpenMP多线程,以及提升了搜索速度
参考这位博主的文章,工程optimized_ICP
,看完这位大姐写的,再去看ALOAM,感觉容易理解了很多。ALOAM就是两种特征点的ICP匹配,比本例稍复杂,改用ceres优化(张楫是手推的雅格比)
pcl::transformPointCloud
获得变换后的待匹配点云。nearestKSearch
搜索距离待匹配点云最近的一个点,记录下此点的索引和二者之间的距离的平方resultant_distances
resultant_distances
和参数max_correspond_distance_
的大小,大于则continueerror
目标函数
i 为通过最近邻搜索匹配上的点对, 和 分别为目标点云和待匹配点云中的点
R, t 分别为两个点云之间的旋转和平移变换。目标函数对R和t求导,文章写的是左扰动模型
但是代码里用的是右扰动模型
1 | for() |
安装kitt2bag
1 | sudo pip install pandas==0.23.0 -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com |
使用kitti-lego-loam
,结果报错 Failed to find match for field intensity ,结果发现是kitti转换出的bag中的点云fields是:x, y, z, i
。偏偏legoloam需要的是x, y, z, intensity.
参考kitt2bag 解决“Failed to find match for field intensity”问题
将PointField('i', 12, PointField.FLOAT32, 1)]
改为 PointField('intensity', 12, PointField.FLOAT32, 1)]
同步后的数据中,imu的频率在10Hz左右,如果需要100Hz的IMU数据,可以参考LIO-SAM【项目地址】中作者改过的kitti2bag文件。
该方法同时使用原始数据(extract)和同步数据(synced)制作数据包。
真值pose是左相机坐标系相对于其第1帧的transform
数据集KITTI-00的真值pose.txt
,行数就是帧数,每行是12列数据,12列数据很容易想到是3个平移量和一个3x3的旋转矩阵,但是其排列方式是一个3*4
的增广矩阵[R|t]
,说白了就是4x4的变换矩阵T去掉了最后一行的[0 0 0 1]
,数据存储顺序是从行到列
第一行数据如下
1 | 1.000000e+00 9.043680e-12 2.326809e-11 5.551115e-17 9.043683e-12 1.000000e+00 2.392370e-10 3.330669e-16 2.326810e-11 2.392370e-10 9.999999e-01 -4.440892e-16 |
还是Qt的环境好,很多时候,不用编译直接就提示有错误。
有的编译器也会报错 error: allocating an object of abstract class type ‘child’。说明基类中有纯虚函数没有实现,必须在派生类实现所有纯虚函数,不使用派生类时是看不出来的。
1 | class Parent |
派生类的同名函数和基类的参数类型不同,而且多了const
。如果没有override
,那么是派生类新加的函数,但是添加override
,编译器认为基类有同名函数,但是没发现,所以报错。
如果是重写基类方法,建议使用override标识符,让编译器检查出可能的错误,从而避免无意的错误。
安装evo: pip install evo --upgrade --no-binary evo
。最新版本的evo只支持Python3.6+,因此如果你在用Python 2.7,最高能安装的evo版本是1.12.0,可以输入pip install evo==1.12.0
安装。安装完之后,终端输入evo
看看有没有结果。
evo支持TUM、KITTI、EuRoC MAV、Bag格式的轨迹/姿态文件。可用的命令:
1 | Metrics: |
kitti
,否则报错: evo_rpe: error: argument subcommand: invalid choice: ‘abc’ (choose from ‘kitti’, ‘tum’, ‘euroc’, ‘bag’)
绘制多个轨迹
1 | evo_traj kitti ground_truth.txt laser_odom.txt -p --plot_mode=xyz |
--ref
似乎就是把对应轨迹设置为虚线,其他没什么特别。--plot_mode
如果设置为xyz,那么绘制的轨迹就是3D的。如果加入--save_results results.zip
,那么把数据存入了zip文件。
evo_traj相当于定性评价了精度,定量评价用到evo_ape
命令和evo_rpe
,前者评价的是每段距离内的误差,后者评价的是绝对误差随路程的累计。
评价每段距离内的误差可以使用指令: evo_rpe kitti ground_truth.txt laser_odom.txt -r trans_part --delta 100 --plot --plot_mode xyz
。 其中--delta 100
表示的是每隔100米统计一次误差,这样统计的其实就是误差的百分比,和kitti的odometry榜单中的距离误差指标就可以直接对应了。
评价总累计误差可以用指令: evo_ape kitti ground_truth.txt laser_odom.txt -r full --plot --plot_mode xyz
还有个evo_res
,以后用到再说。
1 |
|
三个迭代终止条件设置:
setMaximumIterations(30)
: 设置最大的迭代次数。 一般来说在这个限制值之前,优化程序会在epsilon(最大容差)变换阀值下终止,添加最大迭代次数限制能够增加程序的鲁棒性阻止了它在错误的方向上运行时间过长setTransformationEpsilon
: 终止条件设置最小转换差异,设置前后两次迭代的转换矩阵的最大容差epsilion
,在变换中Epsilon参数分别从长度和弧度,定义了变换矢量[x, y, z, roll, pitch, yaw]
的最小许可的递增量,一旦两次迭代小于这个最大容差,则认为已经收敛到最优解,默认值为:0 setEuclideanFitnessEpsilon
: 设置前后两次迭代的点对的欧式距离均值的最大容差,迭代终止条件之三,默认值为:-std::numeric_limits::max
把setTransformationEpsilon
的参数从0.01改为0.001,FitnessScore 只有细微变化,而且相对之前有时变大有时变小。但是改为0.1,FitnessScore与0.01时相比,也是有大有小。迭代次数不管怎么调整,FitnessScore毫无变化,可见确实是以epsilon为优先终止条件。
setStepSize(0.1)
: More-Thuente
线搜索设置最大步长,即设置牛顿法优化的最大步长。当你靠近最优解时该算法会缩短迭代步长,在最大步长将会在较少的迭代次数下遍历较大的距离,但是却有过度迭代和在不符合要求的局部最小值处结束的风险。 调试的经验 :step的话可以先放大后变小。
setResolution(1.0);
: 设置网格化时立方体的边长,网格大小设置在NDT中非常重要,太大会导致精度不高,太小导致内存过高,并且只有两幅点云相差不大的情况才能匹配。它需要足够大,每个体素包含与点有关的统计数据,平均值,协方差等。每个体素至少包含6个点,但小到足以唯一地描述环境。 调试的经验 :resolution在追求精度的情况下最好与点密度相等或者低一个数量级,这里是激光点云,所以设了1米
getFitnessScore()
:欧几里得适合度得分 FitnessScore ,该分数计算为从输出云到目标云中最近点的距离的平方。分数越大,准确率越低,配准效果也就越差。
align
函数中,output_cloud
保存变换后的input_cloud
,它和target_cloud
其实很相近了。但是它不能作为最终的源点云变换,因为上面对input_cloud
进行了滤波处理,所以又经过pcl::transformPointCloud(*input_cloud, *output_cloud, ndt_ptr->getFinalTransformation() );
获得未经滤波的output_cloud
注意的地方:
output_cloud
和target_cloud
看着不太契合参考: 参考程序
CMakeLists.txt:
1 | find_package (GeographicLib REQUIRED) |
直接看代码
1 |
|
local_E: 111297 local_N: 110569 local_U: -1935.19
,也就是说移动经纬1°,对应地面上的距离为11万米多。
这就涉及IMU的测量模型,包含两种:加速度计和陀螺仪。加速度计用来测量加速度的大小,陀螺仪用来测量角速度的大小。
IMU的测量值是基于自身坐标系(body坐标系)的三轴加速度和三轴角速度数据,也就是 ,由于受重力加速度的影响,IMU 的测量值并不直接反映其本身的加速度,并且其测量的加速度的坐标是在 Body 系下表示的,与世界坐标系之间存在一个旋转关系,所以需要转换为世界坐标系下的数据,而角速度不需要转换。
IMU的姿态解算选用的旋转顺序为ZYX,即IMU坐标系初始时刻与世界坐标系重合,然后依次绕自己的Z、Y、X轴进行旋转,这样坐标系就会改变,也可以称新的坐标系为body坐标系
IMU加速度计测量的是其感受到的加速度,在静止的时候,其本身是没有加速运动的,但因为重力加速度的作用,其感受的加速度与重力加速度正好相反,所以读到的线加速度z是9.8
,而不是-9.8
当加速度计水平放置,即Z轴竖直向上,而且在静止时,读到的加速度可以记作 (0, 0, 9.8)
(世界坐标系)
当加速度计旋转一定的姿态时,重力加速度会在加速度的3个轴上产生相应的分量,其本质是世界坐标系下的(0, 0, 9.8)
在新的加速度计坐标系(body坐标系)下的坐标,此时读到的是三轴加速度的新坐标。数学关系如下
这个结果是在body坐标系
在世界坐标系下,
转换到body坐标系, ,即 ,上面图中的计算结果就是 , 是IMU直接输出的。
那么按说真实加速度应该是这样的
1 | float accX = imuIn->linear_acceleration.x + sin(pitch) * 9.81; |
IMU不能随意安装时,其坐标系和载体坐标系是对齐的,都是右手坐标系,所以rosrun tf echo imu base_link
的结果中,角度应该都是0,这样IMU得到的角度就是IMU当前姿态向载体坐标系对齐的过程。LOAM框架作者为了与其他视觉SLAM的工作进行匹配统一,将点云坐标系进行了坐标变换。
在这张图里,只看IMU和camera坐标系的关系,IMU的x变成Camera的z,y—->x, z—->y。比如车上装相机识别二维码时,镜头总是朝前的,也就是图中的朝向。
这样一来,实际三轴加速度就成了源码里的
1 | float accX = imuIn->linear_acceleration.y - sin(roll) * cos(pitch) * 9.81; |
这导致了后面的处理中看似旋转顺序的表示产生了变化,而本质上旋转顺序按物理意义的偏航-俯仰-滚转保持了一致。
参考:IMU去除重力影响
这个话题是显示所有的约束,包括同一轨迹和不同轨迹的Inter约束,Intra约束。
以Inter constraints, different trajectories
和 Inter residuals, different trajectories
为例进行分析,内容如下:
1 | # 每个点的 z为0.0,省略 |
LINE_LIST
类型的visualization_msgs::Marker
,这里要提一下Rviz显示的线类型,如下LINE_LIST
是两两连接而成的线段,在carto这里,点数一定为偶数。把上面消息内容,没两个点的坐标连线,就可以得到Rviz里的显示看MapBuilderBridge::GetConstraintList
源码部分有些跳过了
1 | const auto trajectory_node_poses = |
constraint_pose
,map
坐标系下。