1. 为什么偏偏是PandarXT-16和LpmsIG1这套组合
先交代一下背景。我手头有一台履带式移动底盘,之前一直用的16线老雷达配合纯激光里程计,到了室内外过渡场景就开始飘。后来换上了禾赛PandarXT-16做主激光雷达,又收了阿努比LpmsIG1这个工业级IMU,目标是跑通LIO-SAM,把定位精度拉上去。
这套组合之所以值得写,是因为它比很多人熟悉的Velodyne + Xsens方案要"折腾"一些,但实际跑起来之后精度和稳定性并不差。PandarXT-16的测距能力在16线里算很能打的,垂直视场也够宽,而LpmsIG1作为一颗体积小巧的工业IMU,输出频率高、噪声控制不错,恰好能满足LIO-SAM预积分对高频惯性数据的需求。
当然,光看账面参数是不够的。真正让我决定写这篇东西的原因,是这套组合在工程对接上有不少暗坑:禾赛驱动的消息类型和LIO-SAM默认接收的PointCloud2不一致、LpmsIG1官方没有现成的ROS1驱动、四元数排序和ROS规范对不上、时间同步问题等等。这些坑我在实际调试中一个个趟过来了,今天把完整过程整理出来,给后面想用这套硬件跑LIO-SAM的人留一份可以直接照做的笔记。
2. 硬件层面的关键参数与对接逻辑
2.1 禾赛PandarXT-16的性能边界和配置要点
PandarXT-16是一颗16线机械旋转激光雷达,水平360°扫描,垂直视场角约30°,测距范围在80%反射率下能到120米,典型测距精度±3cm。数据接口是百兆以太网,UDP方式输出点云,帧率支持5Hz、10Hz、20Hz三档可配。
| 参数 | 数值 |
|---|---|
| 线束 | 16线 |
| 水平视场角 | 360° |
| 垂直视场角 | 约30°(-15° ~ +15°) |
| 测距范围 | 0.3m ~ 120m(80%反射率) |
| 测距精度 | ±3cm典型值 |
| 帧率 | 5Hz / 10Hz / 20Hz |
| 水平角分辨率 | 约0.18° |
| 供电 | DC 9V ~ 36V |
LIO-SAM对雷达点云的建模方式是"环-点"二维网格,它需要知道雷达有多少线(N_SCAN)以及一圈水平方向有多少个点(Horizon_SCAN)。PandarXT-16的水平角分辨率约0.18°,对应一圈约2000个点。LIO-SAM默认配置是Horizon_SCAN=1800,如果你直接拿默认配置跑,网格右侧会多出一部分空列,特征提取环节虽然能跳过空列,但会造成排序浪费。我建议在params.yaml里把Horizon_SCAN改成2000,让网格和雷达实际输出对齐。
垂直角度方面,XT-16的16根线束不是均匀分布的,上下边界约±15°。安装时如果雷达带俯仰角,sensorMountAngle参数一定要跟着改,否则地面分割会把墙面误判成地面,特征提取出来的角点、面点分布会乱掉。
2.2 阿努比LpmsIG1的通信与数据特性
阿努比LpmsIG1是LP-RESEARCH面向工业场景的一款九轴IMU,内置三轴加速度计、三轴陀螺仪和三轴磁力计,支持串口(UART)和USB两种通信方式,最高输出频率400Hz。它设备端可以做姿态解算,直接输出四元数、欧拉角,也可以输出原始角速度和线加速度。
| 参数 | 数值 |
|---|---|
| 加速度计量程 | ±8g(可配置) |
| 陀螺仪满量程 | 450°/s(可配置) |
| 输出频率 | 最高400Hz |
| 通信方式 | 串口/USB |
| 供电 | 5V USB或3.3V串口 |
IG1对LIO-SAM最关键的贡献是高频惯性数据。LIO-SAM在雷达两帧点云之间,用IMU做运动补偿和预积分,IMU输出频率越高,预积分对帧间运动的刻画越连续。实际使用中我建议把IG1配成200Hz输出,这个频率下串口数据稳定,CPU占用也不夸张。
但需要注意一个坑:IG1官方手册里四元数输出的顺序是w,x,y,z,而ROS标准消息sensor_msgs/Imu里orientation字段要求是x,y,z,w。如果直接把手册里的四元数顺序填进ROS消息,LIO-SAM的预积分会整体错乱,轨迹呈现一种对称性扭曲。这个点后面我会在踩坑清单里展开。
3. 软件链路搭建:从驱动到LIO-SAM
3.1 编译LIO-SAM及前置依赖
我使用的环境是Ubuntu 20.04 + ROS Noetic。LIO-SAM的依赖主要有GTSAM、PCL、Eigen、OpenCV。其中GTSAM建议源码编译4.0.2版本,apt源里的版本太老,编译LIO-SAM时会遇到接口不匹配的问题。
cd ~ git clone https://github.com/borglab/gtsam.git cd gtsam git checkout 4.0.2 mkdir build && cd build cmake -DGTSAM_BUILD_TESTS=OFF -DGTSAM_BUILD_UNSTABLE=OFF .. make -j8 sudo make install然后是LIO-SAM本体:
cd ~/catkin_ws/src git clone https://github.com/TixiaoShan/LIO-SAM.git cd ~/catkin_ws catkin_make -j8Noetic下编译可能会遇到usleep未声明的报错,这是因为Noetic默认C++标准更严格。解决办法是在include/utility.h里补上#include <unistd.h>,重新编译即可。
3.2 禾赛ROS驱动的安装与点云话题转换
禾赛官方的ROS驱动仓库叫HesaiLidar_ROS,它支持PandarXT系列。安装方式:
cd ~/catkin_ws/src git clone https://github.com/HesaiTechnology/HesaiLidar_ROS.git cd ~/catkin_ws catkin_make -j8驱动启动文件里需要配置雷达IP、电脑IP、端口等参数。下面是针对PandarXT-16的launch配置:
<launch> <node name="hesai_lidar" pkg="hesai_lidar" type="hesai_lidar_node" output="screen"> <param name="server_ip" value="192.168.1.222"/> <param name="lidar_ip" value="192.168.1.201"/> <param name="lidar_port" value="2368"/> <param name="pc_port" value="8321"/> <param name="frame_id" value="hesai_link"/> <param name="model" value="PandarXT-16"/> <param name="lidar_type" value="PandarXT-16"/> <param name="ptc_file" value="$(find hesai_lidar)/config/PandarXT-16.csv"/> <param name="coordinate" value="0"/> </node> </launch>ptc_file指向的是角度校准文件,PandarXT-16每台设备出厂时有一个专属的角度文件,问供应商要或者在雷达外壳标签上找编号,千万别混用不同设备的文件。
驱动启动后,默认发布的点云话题类型是自定义的hesai_pandar/PointCloud,不是LIO-SAM能直接消费的sensor_msgs/PointCloud2。这是整套方案里最容易卡住的第一步。解决办法有两个:
第一种,新版Hesai驱动有些版本会额外发布一个标准PointCloud2话题,检查launch文件里有没有类似publish_pointcloud2或pointcloud_topic的参数,打开后话题名一般是/hesai/pointcloud。
第二种,写一个轻量转换节点。我采用的是第二种,顺手在里面加了一道滤波,把PandarXT-16点云里距离小于0.5m的噪声点滤掉:
#!/usr/bin/env python3 import rospy import sensor_msgs.point_cloud2 as pc2 from sensor_msgs.msg import PointCloud2 from hesai_pandar.msg import PointCloud pub = rospy.Publisher('/points_raw', PointCloud2, queue_size=10) def callback(msg): points = [] for p in msg.points: if p.distance < 0.5: continue points.append([p.x, p.y, p.z, p.intensity]) header = msg.header header.frame_id = 'hesai_link' cloud = pc2.create_cloud(header, fields, points) pub.publish(cloud) rospy.init_node('pandar_to_pc2') sub = rospy.Subscriber('/hesai/pandar', PointCloud, callback) rospy.spin()这里fields需要按PointCloud2的格式定义x,y,z,intensity四个字段,具体写法是:
from sensor_msgs.msg import PointField fields = [ PointField('x', 0, PointField.FLOAT32, 1), PointField('y', 4, PointField.FLOAT32, 1), PointField('z', 8, PointField.FLOAT32, 1), PointField('intensity', 12, PointField.FLOAT32, 1) ]转换后的/points_raw再喂给LIO-SAM,这一步就通了。
3.3 阿努比LpmsIG1的ROS驱动封装
IG1官方没有现成的ROS1驱动,需要自己写一个串口读取节点。好在它的二进制协议有公开文档,帧头、帧尾固定,解析起来并不复杂。
sudo apt install ros-noetic-serial节点核心逻辑:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import rospy import serial from sensor_msgs.msg import Imu def parse_lpms_frame(data): # 按Lpms二进制协议解析 # 帧头0x3A 0x02,帧尾0x0D 0x0A # 解析出accel_x, accel_y, accel_z, gyr_x, gyr_y, gyr_z, quat_w, quat_x, quat_y, quat_z pass def main(): rospy.init_node('lpms_ig1_driver') pub = rospy.Publisher('/imu/data', Imu, queue_size=200) ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1) rate = rospy.Rate(200) while not rospy.is_shutdown(): data = ser.read(ser.in_waiting) if data: imu = Imu() imu.header.stamp = rospy.Time.now() imu.header.frame_id = 'imu_link' accel, gyro, quat = parse_lpms_frame(data) imu.orientation.x = quat[1] imu.orientation.y = quat[2] imu.orientation.z = quat[3] imu.orientation.w = quat[0] imu.linear_acceleration.x = accel[0] imu.linear_acceleration.y = accel[1] imu.linear_acceleration.z = accel[2] imu.angular_velocity.x = gyro[0] imu.angular_velocity.y = gyro[1] imu.angular_velocity.z = gyro[2] imu.orientation_covariance = [0.0001, 0, 0, 0, 0.0001, 0, 0, 0, 0.0001] imu.angular_velocity_covariance = [0.0001, 0, 0, 0, 0.0001, 0, 0, 0, 0.0001] imu.linear_acceleration_covariance = [0.0001, 0, 0, 0, 0.0001, 0, 0, 0, 0.0001] pub.publish(imu) rate.sleep() if __name__ == '__main__': main()有两个细节必须强调:
第一,orientation_covariance、angular_velocity_covariance、linear_acceleration_covariance这三个协方差矩阵不能全为零。LIO-SAM在因子图优化里要用它们构造噪声模型,全零会直接触发GTSAM的异常,程序启动即崩溃。对角线给一个较小的正值,比如0.0001,是安全的。
第二,四元数送进ROS消息前,一定确认顺序转换正确。IG1输出w,x,y,z,ROS要求x,y,z,w,上面的代码里已经做了这个顺序调整。
4. LIO-SAM参数配置:针对XT-16与IG1的适配细节
4.1 点云输入与扫描参数
params.yaml里的核心参数如下:
pointCloudTopic: /points_raw imuTopic: /imu/data lidarFrame: hesai_link baselinkFrame: base_link mapFrame: map odometryFrame: odom sensor: hesai N_SCAN: 16 Horizon_SCAN: 2000 downsampleRate: 1 lidarMinRange: 1.0 lidarMaxRange: 100.0 groundScan: trueHorizon_SCAN=2000这个改动对应前面说的PandarXT-16水平角分辨率0.18°。lidarMinRange设为1.0是因为XT-16在0.5m以内的近距离点云噪声偏大,而且近距离点在特征提取里容易产生不稳定角点,直接滤掉更干净。
4.2 IMU参数与外参
imu.yaml里的配置分成两类,一类是噪声参数,另一类是外参。
# 雷达坐标系到IMU坐标系的变换 extrinsicTrans: [0.0, 0.0, 0.0] extrinsicRot: [1, 0, 0, 0, 1, 0, 0, 0, 1] # 加速度计噪声密度,单位 m/s^2 / sqrt(Hz) imuAccNoise: 1.0e-3 # 陀螺仪噪声密度,单位 rad/s / sqrt(Hz) imuGyrNoise: 1.0e-3 # IMU零偏随机游走 imuAccBiasN: 1.0e-4 imuGyrBiasN: 1.0e-4外参这块很多人理解反了。extrinsicRot和extrinsicTrans描述的是雷达坐标系到IMU坐标系的变换,不是IMU到base_link的变换。实际操作中,我先把IMU固定在雷达正上方的支架上,量出IMU中心在雷达坐标系下的坐标,然后用标定板和RViz做手动对齐。精度不用做到毫米级,但旋转部分一定要准,否则建出来的地图会在转弯时出现明显的扭曲。
注意:LIO-SAM并不会使用IMU消息里的
orientation做初始化姿态。它启动后靠静止状态下IMU加速度计均值去估计重力方向。所以在雷达和IMU都启动之后,设备必须静止至少3~5秒,再开始移动,否则初始重力对齐出错,整个轨迹会整体倾斜。
4.3 帧率与时间同步
IMU频率我用200Hz,雷达帧率固定在10Hz。LIO-SAM的IMU回调里会计算相邻两帧IMU的时间差dt用于预积分,如果IMU频率超过500Hz,dt过小会导致数值稳定性下降;如果低于50Hz,两帧雷达之间的运动补偿会粗糙很多。200Hz是一个比较稳妥的中间值。
时间戳方面,雷达和IMU节点都必须使用系统时间,use_sim_time保持false。如果工控机有GPS授时或者NTP服务,尽量让系统和UTC对齐,这样雷达PTP授时后的时间戳和IMU时间戳天然一致,LIO-SAM在imageProjection.cpp里缓存点云和IMU数据做时间对齐时不会出现时间倒退的问题。
5. 启动流程与验证方法
5.1 推荐启动顺序
# 终端1:禾赛驱动 roslaunch hesai_lidar hesai_lidar.launch # 终端2:IMU节点 rosrun lpms_ig1_driver lpms_ig1_node.py # 终端3:点云转换节点 rosrun pandar_to_pc2 pandar_to_pc2_node.py # 终端4:LIO-SAM roslaunch lio_sam run.launch启动后先检查话题频率:
rostopic hz /points_raw rostopic hz /imu/data点云频率稳定在10Hz左右、IMU频率稳定在200Hz左右,说明底层链路没问题。
5.2 静止误差验证
把设备放在地上静止60秒,记录/odometry的x、y、z输出。理想情况下输出幅度应小于2cm。如果超过10cm,优先检查IMU零偏和雷达到IMU的外参是否准确。我在第一次测试时静止漂移到了20cm,后来定位到问题是IMU没有做零偏校正,IG1在通电后需要等待一段时间让陀螺仪温漂稳定,之后漂移降到2cm以内。
5.3 小范围往返验证
在5m x 5m区域走一个正方形,回到起点后看/odometry终点与起点的差值。小于0.1m说明系统基本可用;如果超过0.5m,大概率是外参标定不准或者雷达和IMU时间戳没对齐。
5.4 回环检测验证
在大楼走廊走一圈回到原点,LIO-SAM的Scan Context回环检测触发后,终端会打印回环检测成功的日志。如果始终不触发,先看雷达点云是否有强度信息。Scan Context依赖强度生成描述子,如果强度字段全为零,描述子退化成纯几何特征,识别率会明显下降。PandarXT-16默认是输出强度信息的,转换节点里一定要保留intensity字段。
6. 我踩过的坑:XT-16与LpmsIG1的组合实战问题清单
6.1 坑一:点云消息类型不匹配导致LIO-SAM崩溃
这是第一个遇到的坑。禾赛驱动默认发布hesai_pandar/PointCloud自定义消息,直接rostopic remap到LIO-SAM的/points_raw,程序在反序列化阶段直接崩溃。原因很简单,自定义消息的字段布局和PointCloud2不一样,LIO-SAM的imageProjection.cpp把它当成标准PointCloud2解析,必然出错。
我的解决办法就是前面写的转换节点。如果你不想写转换节点,也有取巧的办法:去Hesai驱动源码里找到发布点云的函数,把消息类型改成sensor_msgs::PointCloud2。但我建议不要这么干,官方驱动后续升级会覆盖本地改动,而且自己改驱动源码容易引入隐藏问题。转换节点虽然多一层拷贝,但逻辑清晰,调试方便。
6.2 坑二:IG1四元数顺序错误导致预积分扭曲
这个问题我排查了整整一个下午。IG1手册里四元数顺序是w,x,y,z,ROS标准是x,y,z,w。我当时想当然地按照手册顺序填进Imu消息,结果LIO-SAM的预积分出来的轨迹呈现一种对称性的扭曲,转弯时尤其明显,看起来像地图被拧了个麻花。
判断方法很简单:静止状态下把IMU的orientation转成欧拉角,Roll/Pitch/Yaw应该接近(0,0,0)或(0,0,±π)。如果转出来是一组奇怪的角度,优先检查四元数顺序。
6.3 坑三:协方差全零导致GTSAM异常
LIO-SAM在因子图优化时会用IMU消息里的协方差矩阵构造噪声模型。初次写IMU驱动节点时,我把三个协方差矩阵都设成了零,结果程序一启动就报GTSAM的异常,定位不到原因。后来查了LIO-SAM的源码才发现,GTSAM里构造noiseModel::Gaussian时协方差必须正定,全零矩阵直接触发断言失败。
解决办法是在驱动节点里给三个协方差矩阵的对角线填一个较小的正值:
orientation_covariance: [0.0001, 0, 0, 0, 0.0001, 0, 0, 0, 0.0001] angular_velocity_covariance: [0.0001, 0, 0, 0, 0.0001, 0, 0, 0, 0.0001] linear_acceleration_covariance: [0.0001, 0, 0, 0, 0.0001, 0, 0, 0, 0.0001]6.4 坑四:雷达安装方向与坐标轴未对齐
PandarXT-16默认安装方向是线缆朝后、雷达自身x轴朝前。如果你为了走线方便把雷达转了90°安装,但没有在params.yaml里改外参或者加静态坐标变换,LIO-SAM拿到的点云和IMU外参完全对不上,建图结果像被拧过一样。
我习惯在run.launch里增加一个静态坐标变换节点,确保base_link和雷达坐标系在启动时就对齐:
<node pkg="tf2_ros" type="static_transform_publisher" name="lidar_to_base" args="0 0 0.3 0 0 0 base_link hesai_link"/>这里0 0 0.3表示雷达安装在base_link上方0.3m处,三个旋转角为0。如果雷达有偏转角度,在args里填对应的roll/pitch/yaw即可。
6.5 坑五:启动顺序导致IMU时间戳晚于点云
有一次我改变了启动顺序,IMU节点比雷达节点晚启动了几秒,结果LIO-SAM的IMU回调里频繁出现dt <= 0的警告。原因是IMU消息的时间戳晚于雷达点云帧的时间戳,imageProjection.cpp在时间对齐时把点云缓存和IMU数据比较后得到负的时间差。
解决方法是把雷达和IMU的时间戳统一到ROS系统时间,并保证IMU节点先于LIO-SAM启动。如果雷达开了PTP授时,要确保PTP主时钟和IMU节点所在工控机的系统时间一致,否则会出现周期性的时间跳变。
7. 跑通之后的几点体会
整套系统稳定运行之后,我最深的感受是:硬件组合本身的潜力往往被忽略,而工程对接细节决定成败。PandarXT-16和LpmsIG1在账面参数上完全够用,真正拉开差距的是消息格式、时间戳、外参标定这些"看不见"的地方。
如果你也在做类似的组合,我建议优先把IMU外参标定做扎实。LIO-SAM对雷达到IMU外参的误差容忍度很低,旋转偏差哪怕只有2~3°,建图质量就会明显下降。标定这一步偷懒,后面所有调试都是在给前面的偷懒交学费。
另外一个小建议:在params.yaml里把savePCD设为true,这样每次跑完都能留下一份/laser_cloud_map的PCD文件,方便离线复盘。我调试过程中遇到诡异问题时,都是靠这些离线点云逐帧定位问题点的。
这套方案目前在我自己的底盘上已经稳定运行了两个多月,室内外过渡场景的定位精度相比纯激光里程计提升非常明显。希望这篇记录能帮你少走一些弯路。