笔者在学习LIO_SAM时踩了不少坑,在此记录从开始到最后整个踩坑过程。文中参考了很多大佬的文章,我只是个搬运工。可以直接跳到第二部分从0到1实现 有疑问可以随时联系我,欢迎交流。
⼀种激光惯导紧耦合的SLAM框架,可在室内和室外实现效果不错的建图。
(1) ImageProjection 激光运动畸变校正
1、利用当前激光帧起止时刻间的imu数据计算旋转增量,IMU里程计数据(来自ImuPreintegration)计算平移增量,进而对该帧激光每一时刻的激光点进行运动畸变校正(利用相对于激光帧起始时刻的位姿增量,变换当前激光点到起始时刻激光点的坐标系下,实现校正);
(激光帧的起始数据——由IMU得到旋转增量,由IMU里程计得到平移增量,通过位姿增量,变换当前激光点到原始激光点,实现校正。)
2、同时用IMU数据的姿态角(RPY,roll、pitch、yaw)、IMU里程计数据的的位姿,对当前帧激光位姿进行粗略初始化。
订阅原始IMU数据;
订阅IMU里程计数据,来自ImuPreintegration,表示每一时刻对应的位姿;
订阅原始激光点云数据。
发布当前帧激光运动畸变校正之后的有效点云,用于rviz展示;
发布当前帧激光运动畸变校正之后的点云信息,包括点云数据、初始位姿、姿态角、有效点云数据等,发布给FeatureExtraction进行特征提取。
(2) FeatureExtraction 点云特征提取
对经过运动畸变校正之后的当前帧激光点云,计算每个点的曲率,进而提取角点、平面点(用曲率的大小进行判定)。
订阅当前激光帧运动畸变校正后的点云信息,来自ImageProjection。
发布当前激光帧提取特征之后的点云信息,包括的历史数据有:运动畸变校正,点云数据,初始位姿,姿态角,有效点云数据,角点点云,平面点点云等,发布给MapOptimization;
发布当前激光帧提取的角点点云,用于rviz展示;
发布当前激光帧提取的平面点点云,用于rviz展示。
(3) ImuPreintegration IMU预积分
TransformFusion类
主要功能是订阅激光里程计(来自MapOptimization)和IMU里程计,根据前一时刻激光里程计,和该时刻到当前时刻的IMU里程计变换增量,计算当前时刻IMU里程计;rviz展示IMU里程计轨迹(局部)。
订阅激光里程计,来自MapOptimization;
订阅imu里程计,来自ImuPreintegration。
发布IMU里程计,用于rviz展示;
发布IMU里程计轨迹,仅展示最近一帧激光里程计时刻到当前时刻之间的轨迹。
用激光里程计,两帧激光里程计之间的IMU预计分量构建因子图,优化当前帧的状态(包括位姿、速度、偏置);
以优化后的状态为基础,施加IMU预计分量,得到每一时刻的IMU里程计。
订阅IMU原始数据,以因子图优化后的激光里程计为基础,施加两帧之间的IMU预计分量,预测每一时刻(IMU频率)的IMU里程计;
订阅激光里程计(来自MapOptimization),用两帧之间的IMU预计分量构建因子图,优化当前帧位姿(这个位姿仅用于更新每时刻的IMU里程计,以及下一次因子图优化)。
发布imu里程计;
(4) MapOptimization 因子图优化
1、scan-to-map匹配:提取当前激光帧特征点(角点、平面点),局部关键帧map的特征点,执行scan-to-map迭代优化,更新当前帧位姿;
2、关键帧因子图优化:关键帧加入因子图,添加激光里程计因子、GPS因子、闭环因子,执行因子图优化,更新所有关键帧位姿;
3、闭环检测:在历史关键帧中找距离相近,时间相隔较远的帧设为匹配帧,匹配帧周围提取局部关键帧map,同样执行scan-to-map匹配,得到位姿变换,构建闭环因子数据,加入因子图优化。
1、订阅当前激光帧点云信息,来自FeatureExtraction;
2、订阅GPS里程计;
3、订阅来自外部闭环检测程序提供的闭环数据,本程序没有提供,这里实际没用上。
1、发布历史关键帧里程计;
2、发布局部关键帧map的特征点云;
3、发布激光里程计,rviz中表现为坐标轴;
4、发布激光里程计;
5、发布激光里程计路径,rviz中表现为载体的运行轨迹;
6、发布地图保存服务;
7、发布闭环匹配局部关键帧map;
8、发布当前关键帧经过闭环优化后的位姿变换之后的特征点云;
9、发布闭环边,rviz中表现为闭环帧之间的连线;
10、发布局部map的降采样平面点集合;
11、发布历史帧(累加的)的角点、平面点降采样集合;
12、发布当前帧原始点云配准之后的点云;
特点:
补充说明:
IMU通常由三个单轴的加速度计和三个单轴的陀螺仪组成,加速度计检测载体在坐标系统中独立三轴的加速度信号,而陀螺仪检测载体相对于坐标系的角速度信号,对这些信号进行处理之后,便可解算出载体的姿态。IMU的更新频率较高,一般可达几百至1KHz。使用三个加速度值,通过两次积分可获得位移,以此实现位置定位,有角速度值积分可以获取姿态信息,结合在一起可获得物体的实际状态。需要注意的是IMU提供的是相对的原始定位信息,它的作用是测量相对于起点所运动的路线,所以它并不能提供你所在的具体位置的信息。
(9轴IMU有三个单轴加速度计,三个单轴陀螺仪和三个单轴磁力计)
具体的理论学习,源码解读,大家自行去网上搜索(这一部分公开的资料很多),笔者在此只着重介绍如何利用LIO_SAM运行自己的数据集。
先放张效果图,大概能看出个运行出的效果,还是不错的,笔者当时录包的时候录了所有话题的消息,占用了大量内存,短短的40s视频占了4个G。这是没有必要的哈,我们使用激光雷达+IMU ,真正操作过程中我们只需要录雷达和imu话题(/velodyne_points和/imu/data)就可以了。
笔者用的是Velodyne16线激光雷达(简称VLP16)+9轴imu(N100)。
网上很多博主介绍跑LIO_SAM要用9轴IMU,6轴IMU也是可以运行的哈,听高博介绍LIO_SAM也是用的6轴。如果需要融合GPS或者资金允许可以采用9轴IMU。为了防止有争议,最好还是用9轴。
LIO-SAM算法对激光雷达的数据格式有着较为严格的要求,以往的单激光雷达建图的算法没注意到这一点,一般要求的是XYZI(x, y, z, intensity ) 格式即可,但是LIO-SAM要求的是 XYZIRT(x, y, z, intensity, ring, timestamp) 格式,即算法内使用了激光雷达的通道数ring参数和时间戳timestep参数,启动算法时会检查是否具有这两个参数。那么如何获得这两个参数呢? 如果你同笔者使用的激光雷达一样,你可以将激光雷达的驱动更新到RSLidar-SDK版本即可采集,参考大佬另一篇教程安装速腾最新的驱动:(44条消息) Ubuntu18.04 安装速腾聚创最新驱动RSLidar_SDK采集XYZIRT格式的激光点云数据 --SLAM不学无术小问题_摆渡人的博客-CSDN博客
但问题是采集到的数据仍然有不可直接用,速腾格式的点和Velodyne格式的点还是有问题的。这又如何解决呢?大佬在某hub上找到了一位大佬的转化节点,这一节点可将速腾格式的点转化为Velodyne格式的点,具体安装步骤参考笔者另一篇教程链接: (44条消息) Ubuntu18 安装ROS节点解决----速腾聚创雷达点云格式转换为Velodyne雷达点云格式 --SLAM不学无术小问题_摆渡人的博客-CSDN博客_速腾雷达转velodyne(此处搬运大佬的文章,侵删)
如果你使用的激光雷达和笔者是同一款,则不需要考虑这个问题。VLP16的格式就是 XYZIRT(x, y, z, intensity, ring, timestamp) 格式,即算法内使用了激光雷达的通道数ring参数和时间戳timestep参数。
(此处搬运大佬的文章,侵删)(44条消息) LIO-SAM运行自己数据包遇到的问题解决–SLAM不学无数术小问题_摆渡人的博客-CSDN博客简单说就是对 frame_id 有要求,可以找到对应的源码修改,不想改源代码只能在外部使劲了,这里面涉及到了内部坐标的映射关系,不一样的话可能ERROR 导致无法建图,原作给出的数据集中的 frame_id是这样对应的:
//激光雷达数据
/points_raw-----------frame_id:"velodyne"--------
//IMU数据
/imu_raw--------------frame_id:"imu_link"--------
//GPS数据
/gps/fix--------------frame_id:"navset_link"-----
查看自己数据包的frame_id:
// 在播放数据包的时候使用如下指令查看某一Topic的frame_id:
rostopic echo /Topic | grep frame_id
如果与原作的不一样的话需要修改,具体修改方法可以在ROS的官方wiki下载一个工具包bag_tools,里面包含了许多关于数据包的操作工具,官方附带了使用教程,可自行百度下载解决。这里给一条印象中的转化指令:
rosrun bag_tools change_frame_id.py -t /需要要改的topic -f 新的frame_id -i 旧.bag -o 新.bag
imu的频率 最好大于100HZ,笔者在用小于100HZ时导致了在跑数据包时,在转弯的时候,地图跟着车转动(准确来说跟着imu转动),一开始没有注意到imu频率问题,一直以为是外参设置的不够准确。其实看过原作者论文的话应该会发现,作者直接把外参设置为单位矩阵,如下图蓝色圈出的部分:平移矩阵和旋转矩阵
但是我们可以从下图官方配置的图看出,vlp16和imu的平移不可能是0,可见跑lio_sam不需要特别精准的外参,如果不相信的话,可以用标定工具标出的结果对比一下看看是不是接近单位矩阵。
笔者在一开始跑的时候,一直对外参和imu的内参的设置要精准坚信不疑,当我发现即使我的外参很精准,跑图还是很飘,就意识到事情的不对了,在此特别感谢两位大佬的提点。
尽量将imu和雷达的位置放置的近一点,这样平移矩阵设置为0也没问题。
imu和雷达的坐标系最好完全一致,如果不能,则尽量保证坐标系相差90°或者180°;
在此插一句题外话,笔者在安装时,一开始没有搞清楚imu的坐标系,整了个乌龙,后来询问商家发现imu的物理坐标系在ROS中输出时已经被调整,而我还按照imu物理坐标系设置,跑出来的图飘的离谱。所以一定要弄清楚获取到的imu数据和雷达坐标系是什么方向的。在此放一张笔者安装图:
笔者的imu和lidar坐标系是安装的完全一致的(Z轴竖直向上),而且靠的也很近,所以笔者后来就直接将平移矩阵设置为0,旋转矩阵设置为单位矩阵(这样是最省事的)。外参设置的也就是imu和lidar的转换关系,如果从源头就解决这个问题,那就会方便很多。
在lio-sam参数文件中,在imu内参下面的有一个参数是imu的重力加速度,正负表示方向:
如果imu和vlp16的Z轴同时向上则设置为正,反之为负。
还需要注意一下波特率和imu频率匹配,115200对应的频率应该是不能超过50HZ,961200可以设置100HZ往上。
可以直接跳到链接:(44条消息) LIO-SAM运行自己数据包遇到的问题解决–SLAM不学无数术小问题_摆渡人的博客-CSDN博客_lio_sam这个博主写的很详细。
网上有很多开源的标定方法,笔者使用lidar_align标定成功。
按上述来说不需要特别精准的外参,如果允许程序时发现 警告large bias…large velocity…,那就是外参设置错了,也就是转换关系错了,可以使用这个标定一下,看一下转换关系
cd 自己的工作空间/src
git clone https://github.com/ethz-asl/lidar_align.git
catkin_make
建议下载修正过后的代码:lidar_align_wwtx
新建自己的ROS工作空间,将该源码放入,catkin_make即可。
首次编译会因为缺少NLOPT库报错
编译时出现Could not find NLOPTConfig.cmake
解决方法:
sudo apt-get install libnlopt-dev
找到并将NLOPTConfig.cmake文件移动到 lidar_align/src/下(此处可以直接编译),如果还是报上面错误的话则在CMakeLists.txt里加上这样一句话:
list(APPEND CMAKE_FIND_ROOT_PATH ${PROJECT_SOURCE_DIR})
set (CMAKE_PREFIX_PATH "/usr/local/lib/cmake/nlopt")
如图:因为身在外地,无法演示,就放一张大佬的图吧 (侵删)(44条消息) 激光雷达和IMU联合标定并运行LIOSAM_浪客_剑心的博客-CSDN博客_liosam
然后进行第二次编译
二次编译还是会报错,解决冲突问题(按照顺序,运行下面的命令即可)
sudo mv /usr/include/flann/ext/lz4.h /usr/include/flann/ext/lz4.h.bak
sudo mv /usr/include/flann/ext/lz4hc.h /usr/include/flann/ext/lz4.h.bak
sudo ln -s /usr/include/lz4.h /usr/include/flann/ext/lz4.h
sudo ln -s /usr/include/lz4hc.h /usr/include/flann/ext/lz4hc.h
catkin_make
第三次编译通过
原文链接:https://blog.csdn.net/weixin_42141088/article/details/118000544 (侵删)
这一工具原本不是用来标定激光雷达和IMU的而是用来标定激光雷达和里程计的。所以需要改写IMU接口来替换掉里程计接口。所以这一工具一定程度上并不是精确标定上述两种传感器的,但不精确不代表不可用。改写这一接口的大佬没找着,没法连接了所以侵删联系。以下是做法:
打开loader.cpp文件
找到以下odom部分注释删掉都可
/* types.push_back(std::string("geometry_msgs/TransformStamped"));
rosbag::View view(bag, rosbag::TypeQuery(types));
size_t tform_num = 0;
for (const rosbag::MessageInstance& m : view) {
std::cout << " Loading transform: \e[1m" << tform_num++
<< "\e[0m from ros bag" << '\r' << std::flush;
geometry_msgs::TransformStamped transform_msg =
*(m.instantiate());
Timestamp stamp = transform_msg.header.stamp.sec * 1000000ll +
transform_msg.header.stamp.nsec / 1000ll;
Transform T(Transform::Translation(transform_msg.transform.translation.x,
transform_msg.transform.translation.y,
transform_msg.transform.translation.z),
Transform::Rotation(transform_msg.transform.rotation.w,
transform_msg.transform.rotation.x,
transform_msg.transform.rotation.y,
transform_msg.transform.rotation.z));
odom->addTransformData(stamp, T);
}
*/
将以上部分替换为:
types.push_back(std::string("sensor_msgs/Imu"));
rosbag::View view(bag, rosbag::TypeQuery(types));
size_t imu_num = 0;
double shiftX=0,shiftY=0,shiftZ=0,velX=0,velY=0,velZ=0;
ros::Time time;
double timeDiff,lastShiftX,lastShiftY,lastShiftZ;
for (const rosbag::MessageInstance& m : view){
std::cout <<"Loading imu: \e[1m"<< imu_num++<<"\e[0m from ros bag"<<'\r'<< std::flush;
sensor_msgs::Imu imu=*(m.instantiate());
Timestamp stamp = imu.header.stamp.sec * 1000000ll +imu.header.stamp.nsec / 1000ll;
if(imu_num==1){
time=imu.header.stamp;
Transform T(Transform::Translation(0,0,0),Transform::Rotation(1,0,0,0));
odom->addTransformData(stamp, T);
}
else{
timeDiff=(imu.header.stamp-time).toSec();
time=imu.header.stamp;
velX=velX+imu.linear_acceleration.x*timeDiff;
velY=velX+imu.linear_acceleration.y*timeDiff;
velZ=velZ+(imu.linear_acceleration.z-9.801)*timeDiff;
lastShiftX=shiftX;
lastShiftY=shiftY;
lastShiftZ=shiftZ;
shiftX=lastShiftX+velX*timeDiff+imu.linear_acceleration.x*timeDiff*timeDiff/2;
shiftY=lastShiftY+velY*timeDiff+imu.linear_acceleration.y*timeDiff*timeDiff/2;
shiftZ=lastShiftZ+velZ*timeDiff+(imu.linear_acceleration.z-9.801)*timeDiff*timeDiff/2;
Transform T(Transform::Translation(shiftX,shiftY,shiftZ),
Transform::Rotation(imu.orientation.w,
imu.orientation.x,
imu.orientation.y,
imu.orientation.z));
odom->addTransformData(stamp, T);
}
}
并在开头添加头文件:
#include
第四次编译
在此之前还需要安装一些其他的库Eigen,Metis,Gtsam,ceres等,总之缺什么就去网上找一下对应的库安装即可,在此需要注意(一定一定要注意版本匹配)。
只需同时录制激光雷达和IMU(/velodyne_points和/imu/data)两分钟即可,最好数据量不要超过2G,超出2G之外可能出现未知错误无法标定。两分钟的数据应该尽量包含较大的旋转量和平移量(也就是先直线跑一段再转两圈),这样标出来的误差结果(据说)会收敛到几百以下(已经相当可以了),(标定过程较为漫长,耐心等待)。最好周围不要有透明的玻璃和移动障碍物。
下面这句命令意思就是录制只/velodyne_points /imu/data两个话题的数据包
cd 数据包存放的地方(不运行此句,则默认在home下)
rosbag record -O lidar_imu.bag /velodyne_points /imu/data
打开lidar_align.launch文件,将两分钟的数据包路经copy到下方:
开始标定
cd lidar_align
source devel/setup.bash
roslaunch lidar_align lidar_align.launch
标定过程大概一个小时左右,耐心等待,迭代200~300次。
标定好之后会在终端显示出标定结果,也会在result文件夹下面生成对应的标定文件。
这个是标定imu内参的,如果商家提供了内参,可以跳过这个标定步骤确保内参是正确的。这个步骤需要录制2小时。笔者就直接采用的商家提供的内参。
具体操作步骤这里参考了大佬文章,可直接跳过去按照大佬教程安装用imu_utils标定IMU,之后用于kalibr中相机和IMU的联合标定.
这里要录制两个小时的IMU数据,注意!!!录制数据时必须保持IMU的静止状态,录制全程不可晃动只录制IMU的Topic即可,不过别担心内存,两小时下来只要300M多一点。并且这个数据包最好是放到你imu_utils的工作空间下(与src并列的地方)。最好是IMU上电10分钟之后在开始录制,上电瞬间开始录制会有较大误差。
录制指令
rosbag play -r 200 imu_utils/imu.bag
需要注意的是在运行的终端标定时,终端是不会显示标定信息的,不要担心,它其实在正常运行,而且很快就能标定完成。
在此记录一个问题:
error: cannot convert 'int*' to'idx_t*'{aka long int*}' for argument '1' to 'int' METIS_NodeND(idx_t*, idx_t*, idx_t*,idx_t*, idx_t*, idx_t*,idx_t*)'
output_error = METIS_NodeND(&m, m_indexPtr.data(), m_innerIndices.data(), NULL, NULL, perm.data(), iperm.data());
此问题最后是因为ceres和Eigen版本不匹配造成的(耽搁了我好久!!)适当降低一下版本。
根据上面标定的外参和内参对lio-sam参数文件进行修改:
具体修改的话,不再赘述,跳过去看大佬的教程https://blog.csdn.net/weixin_42141088/article/details/118000544,讲的挺详细的
在这我放上自己的参数文件,仅供参考
lio_sam:
# Topics
pointCloudTopic: "velodyne_points" # Point cloud data(替换成自己的点云话题)
imuTopic: "imu_data" # IMU data(替换自己的imu话题)
odomTopic: "odometry/imu" # IMU pre-preintegration odometry, same frequency as IMU
gpsTopic: "odometry/gpsz"
# GPS odometry topic from navsat, see(没有GPS就不需要更改) module_navsat.launch file
# Frames
lidarFrame: "base_link"
baselinkFrame: "base_link"
odometryFrame: "odom"
mapFrame: "map"
# GPS Settings
useImuHeadingInitialization: false # if using GPS data, set to "true"
useGpsElevation: false # if GPS elevation is bad, set to "false"
gpsCovThreshold: 2.0 # m^2, threshold for using GPS data
poseCovThreshold: 25.0 # m^2, threshold for using GPS data
# Export settings
savePCD: true # https://github.com/TixiaoShan/LIO-SAM/issues/3
savePCDDirectory: "/rosbag/pcd" # in your home folder, starts and ends with "/". Warning: the code deletes "LOAM" folder then recreates it. See "mapOptimization" for implementation
# Sensor Settings
sensor: velodyne # lidar sensor type, 'velodyne' or 'ouster' or 'livox'
N_SCAN: 16 # number of lidar channel (i.e., Velodyne/Ouster: 16, 32, 64, 128, Livox Horizon: 6)
Horizon_SCAN: 1800 # lidar horizontal resolution (Velodyne:1800, Ouster:512,1024,2048, Livox Horizon: 4000)
downsampleRate: 1 # default: 1. Downsample your data if too many points. i.e., 16 = 64 / 4, 16 = 16 / 1
lidarMinRange: 1.0 # default: 1.0, minimum lidar range to be used
lidarMaxRange: 1000.0 # default: 1000.0, maximum lidar range to be used
# IMU Settings 对应imu的内参 我这里是直接用的商家提供的
imuAccNoise: 1.0909715156015328e-02
imuGyrNoise: 8.0226069504443656e-04
imuAccBiasN: 4.0347767978459928e-04
imuGyrBiasN: 4.4153147263889589e-05
imuGravity: 9.80511
imuRPYWeight: 0.01
# Extrinsics: T_lb (lidar -> imu)
extrinsicTrans: [0.0, 0.0, 0.0] # 平移矩阵 我这里直接设置为0 因为我imu和雷达靠的很近 在此不做赘述了 上面解释过了。
extrinsicRot: [1, 0, 0, # 旋转矩阵 我这里直接是单位矩阵,可以用上面标定的外参对比一下
0, 1, 0,
0, 0, 1]
extrinsicRPY: [1, 0, 0,
0, 1, 0,
0, 0, 1]
# LOAM feature threshold
edgeThreshold: 1.0
surfThreshold: 0.1
edgeFeatureMinValidNum: 10
surfFeatureMinValidNum: 100
我这里只列举了主要修改的地方,其他的参数根据注释和个人需要进行修改。
前面的工作做完了之后,现在就可以运行看效果了。
打开底层
roslaunch xxx_bringup xxx.launch //每个人都不一样,这里是比较常见的方式,运行雷达,imu和底盘。
录制数据包
//放到对应的路径下
rosbag record -O xxx.bag /velodyne_points /imu/data
控制小车运动
录制包结束之后,开始跑lio-sam
roslaunch lio-sam run.launch
cd xxxx//数据包的路径
rosbag play xxx.bag
这个时候就能看到rviz中的效果了。在跑完数据包后会自己保存pcd文件。
5.最后在bag包的路径下有一个pcl文件夹,这里面是保存的点云地图
//安装pcl_tools,安装过则跳过
sudo apt-get install pcl-tools
//到pcl路径下打开一个终端
pcl_viewer xxx.pcd
上图是我的一张pcd图。
(44条消息) LIO-SAM运行自己数据包遇到的问题解决–SLAM不学无数术小问题_摆渡人的博客-CSDN博客_lio_sam
(44条消息) LIO-SAM:配置环境、安装测试、适配自己采集数据集_有待成长的小学生的博客-CSDN博客_lio-sam