ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

自定义激光雷达适配LIO-SAM:ring与time字段补全实战

自定义激光雷达适配LIO-SAM:ring与time字段补全实战 1. 为什么自定义激光雷达接LIO-SAM总会卡在ring和time上手里有一台非主流型号的激光雷达或者从某处拆下来的一台多线雷达想跑LIO-SAM做建图与定位结果第一步就卡住了——要么编译报错说找不到ring字段要么跑起来点云像一锅粥建图飘得没法看。这个问题在激光雷达SLAM圈子里非常典型尤其是当你用的不是Velodyne、Ouster、RoboSense这些LIO-SAM原生支持的雷达型号时ring和time这两个字段的缺失几乎是必然要面对的。LIO-SAM的全称是Tightly-coupled Lidar Inertial Odometry via Smoothing and Mapping它依赖激光雷达点云中的两个关键字段ring和time。ring标识每个点属于哪一根激光线束time记录每个点相对于该帧起始时刻的精确时间戳。这两个字段直接决定了LIO-SAM能否正确进行点云去畸变和特征提取。没有它们LIO-SAM要么直接崩溃要么输出的里程计漂到无法使用。这篇文章面向的是正在用自定义激光雷达跑LIO-SAM的开发者不管你是刚入门ROS的在校学生还是正在做产品原型的工程师只要你手头的雷达数据缺少ring和time这里的内容都能直接拿来用。我会从原理层面讲清楚为什么这两个字段如此关键然后给出完整的适配方案包括代码实现、参数配置和调试技巧最后把我自己踩过的坑和排查经验一并分享出来。2. LIO-SAM对点云格式的硬性要求拆解2.1 ring字段到底在做什么LIO-SAM内部使用的是velodyne风格的PointXYZIRT点类型其中R就是ringT就是time。ring的作用是标记每个点来自哪一根激光线束。以常见的16线雷达为例ring的取值范围就是0到15每个点都会被赋予一个线束编号。为什么LIO-SAM需要ring核心原因在于特征提取阶段。LIO-SAM会按照ring对点云进行分组在每一根线束内部计算曲率判断哪些点是边缘特征、哪些点是平面特征。如果没有ring字段所有点混在一起曲率计算就完全失去意义特征提取的结果会变得极其混乱。你可以把它想象成一支阅兵方队ring就是每个人的排号没有排号就没办法按行按列对齐整个方队就散了。在实际代码中LIO-SAM的imageProjection.cpp里有一段关键逻辑它遍历每个点通过ring来判断当前点是否属于同一根线束进而决定是否将其纳入当前线束的曲率计算。如果ring字段缺失或者全部为0所有点会被当作同一根线束处理曲率计算的结果就完全错误。2.2 time字段与运动畸变去除的关系time字段记录的是每个点相对于该帧起始时刻的时间偏移单位通常是秒。激光雷达在旋转扫描的过程中雷达本身可能正在运动比如搭载在移动平台上这就导致同一帧点云中不同时间采集到的点实际上处于不同的雷达位姿下。如果不做去畸变处理这帧点云就是扭曲的。LIO-SAM的去畸变流程是这样的它先通过IMU积分得到雷达在这帧扫描期间的位姿变化然后根据每个点的time值将该点投影到帧起始时刻的坐标系下。没有time字段这个投影就无从谈起去畸变步骤直接失效。建图飘、轨迹抖动、回环检测失败很多时候根源都在这里。我见过不少人把time全部填0想着“反正雷达转得快畸变不大”。短距离低速场景下可能勉强能跑但一旦平台速度上来或者场景变大累积误差会迅速发散。所以time字段必须认真对待不能糊弄。2.3 标准点云格式对比下面这张表列出了几种常见雷达点云格式与LIO-SAM要求的差异方便你快速定位自己的数据缺什么字段Velodyne标准Ouster标准自定义雷达常见情况LIO-SAM要求x, y, z有有有必须有intensity有有可能有可选ring有有列号经常缺失必须有time有有经常缺失必须有从表中可以看出自定义雷达最容易缺的就是ring和time。有些雷达驱动只输出x, y, z, intensity四个字段这种情况下你就需要自己动手补全。3. 从零适配自定义雷达数据的完整方案3.1 确认雷达数据源的真实结构动手改代码之前第一件事是搞清楚你的雷达到底输出了什么。用rostopic echo查看原始点云话题重点关注fields字段rostopic echo /your_lidar/points --noarr | head -50你会看到类似这样的输出fields: - name: x offset: 0 datatype: 7 - name: y offset: 4 datatype: 7 - name: z offset: 8 datatype: 7 - name: intensity offset: 16 datatype: 7如果这里没有ring和time那就确认了问题所在。接下来还要确认几件事雷达的线束数量是多少扫描频率是多少每帧点云的时间跨度是多少这些参数直接影响到后面ring和time的计算方式。另外有些雷达虽然在fields里没有ring但点云的排列顺序本身隐含了线束信息。比如按线束顺序逐行排列的点云你可以通过点的索引来推算ring值。这种情况下适配会简单很多。3.2 通过索引推算ring值的实现方法假设你的雷达是N线雷达每帧点云总数为total_points那么每根线束上的点数就是total_points / N。如果点云是按线束顺序排列的先第0线所有点再第1线所有点以此类推那么第i个点的ring值就是int ring i / (total_points / N);但实际情况往往更复杂。很多雷达的点云是按方位角排列的即每一列包含所有线束的点然后按方位角递增。这种情况下第i个点的ring值就是int ring i % N;具体是哪种排列方式需要你查看雷达的驱动文档或者通过可视化工具观察点云结构来判断。我通常的做法是在RViz里用不同颜色显示点云然后手动旋转视角看同一根线束的点是否连续排列。下面是一个完整的ROS节点示例演示如何订阅原始点云并发布带ring和time的点云#include ros/ros.h #include sensor_msgs/PointCloud2.h #include sensor_msgs/point_cloud2_iterator.h ros::Publisher pub; void cloudCallback(const sensor_msgs::PointCloud2ConstPtr msg) { int num_lines 16; // 根据你的雷达修改 int total_points msg-width * msg-height; int points_per_line total_points / num_lines; sensor_msgs::PointCloud2 output; output.header msg-header; output.height 1; output.width total_points; output.is_dense false; output.is_bigendian false; sensor_msgs::PointCloud2Modifier modifier(output); modifier.setPointCloud2Fields(6, x, 1, sensor_msgs::PointField::FLOAT32, y, 1, sensor_msgs::PointField::FLOAT32, z, 1, sensor_msgs::PointField::FLOAT32, intensity, 1, sensor_msgs::PointField::FLOAT32, ring, 1, sensor_msgs::PointField::UINT16, time, 1, sensor_msgs::PointField::FLOAT32); modifier.resize(total_points); sensor_msgs::PointCloud2Iteratorfloat iter_x(*msg, x); sensor_msgs::PointCloud2Iteratorfloat iter_y(*msg, y); sensor_msgs::PointCloud2Iteratorfloat iter_z(*msg, z); sensor_msgs::PointCloud2Iteratorfloat out_x(output, x); sensor_msgs::PointCloud2Iteratorfloat out_y(output, y); sensor_msgs::PointCloud2Iteratorfloat out_z(output, z); sensor_msgs::PointCloud2Iteratorfloat out_i(output, intensity); sensor_msgs::PointCloud2Iteratoruint16_t out_ring(output, ring); sensor_msgs::PointCloud2Iteratorfloat out_time(output, time); float scan_period 0.1; // 10Hz雷达根据实际情况修改 for (int i 0; i total_points; i, iter_x, iter_y, iter_z, out_x, out_y, out_z, out_i, out_ring, out_time) { *out_x *iter_x; *out_y *iter_y; *out_z *iter_z; *out_i 1.0; // 按方位角排列的情况 *out_ring static_castuint16_t(i % num_lines); // 按线束排列的情况用下面这行 // *out_ring static_castuint16_t(i / points_per_line); // time按方位角线性分配 *out_time scan_period * (i % points_per_line) / points_per_line; } pub.publish(output); } int main(int argc, char** argv) { ros::init(argc, argv, lidar_adapter); ros::NodeHandle nh; ros::Subscriber sub nh.subscribe(/your_lidar/points, 10, cloudCallback); pub nh.advertisesensor_msgs::PointCloud2(/adapted_points, 10); ros::spin(); return 0; }这段代码的核心逻辑就是遍历原始点云根据排列方式推算ring根据方位角比例推算time。time的计算方式是把扫描周期按每根线束上的点数均分第j个点的时间偏移就是scan_period * j / points_per_line。3.3 time字段的精确计算方法上面代码里time的计算用的是线性分配这在大多数情况下够用但如果你追求更高的精度可以考虑以下两种改进方案。第一种是利用雷达驱动提供的时间戳信息。有些雷达虽然不在点云里输出time字段但会在每帧的头部或者单独的topic里给出每个点的时间戳。这种情况下直接读取即可精度最高。第二种是利用方位角信息。如果你的点云里保留了方位角有些雷达会输出azimuth字段那么time可以更精确地计算float azimuth_rad point_azimuth; // 当前点的方位角 float time_offset scan_period * azimuth_rad / (2 * M_PI);这种方式比线性分配更准确因为它是按照实际角度比例来分配时间的。不过前提是你的点云里得有方位角信息。注意time字段的单位必须是秒不是毫秒也不是微秒。LIO-SAM内部按照秒来处理单位搞错了会导致去畸变完全失效而且这种错误很隐蔽因为程序不会报错只是建图效果差。3.4 修改LIO-SAM的配置以匹配自定义雷达数据适配好了之后还需要修改LIO-SAM的配置文件。打开params.yaml找到以下几个关键参数# 雷达线束数 N_SCAN: 16 # 水平角分辨率度 Horizon_SCAN: 1800 # 雷达到IMU的外参 extrinsicRot: [1, 0, 0, 0, 1, 0, 0, 0, 1] extrinsicRPY: [1, 0, 0, 0, 1, 0, 0, 0, 1]N_SCAN必须和你的雷达线束数一致Horizon_SCAN是一圈扫描的点数。这两个参数如果设错了特征提取会出问题。外参矩阵需要根据你的实际安装位置来标定如果雷达和IMU的坐标系不一致建图会直接飘走。另外在imageProjection.cpp中LIO-SAM默认使用velodyne_ros::Point类型。如果你用的是自定义点类型需要把相关代码改成对应的类型。具体来说找到所有使用velodyne_ros::Point的地方替换成你自己的点类型定义。4. 调试与验证确保适配后的数据能跑通4.1 用RViz做可视化验证数据适配完成后第一步是在RViz里检查点云是否正常。启动RViz添加PointCloud2显示订阅适配后的话题。重点观察以下几点点云是否呈现清晰的多线结构每根线束是否分明用颜色映射到ring字段看颜色是否按线束分层用颜色映射到time字段看颜色是否沿方位角渐变如果ring的颜色是均匀分布的说明线束编号正确。如果time的颜色从一帧的起始到结束呈现渐变说明时间分配合理。如果颜色混乱或者全部一样那就需要回去检查代码。4.2 用rosbag录制并回放测试可视化确认没问题后用rosbag录制一段数据然后回放给LIO-SAM跑# 录制 rosbag record /adapted_points /imu/data -O test.bag # 回放 rosbag play test.bag --clock回放的同时启动LIO-SAM观察输出的里程计话题和建图结果。如果一切正常你应该能看到点云地图逐渐构建出来轨迹平滑没有明显的漂移。4.3 常见异常现象与对应排查下面这张表整理了我在调试过程中遇到过的典型问题及解决方法异常现象可能原因排查方法建图严重飘移time字段单位错误或全为0检查time计算逻辑确认单位为秒点云呈放射状散开ring字段全部相同检查ring推算逻辑确认排列方式特征提取失败N_SCAN与实际线束数不符核对params.yaml中的N_SCAN里程计抖动剧烈外参矩阵不正确重新标定雷达与IMU的外参程序崩溃报段错误点类型不匹配检查LIO-SAM中的点类型定义提示如果建图飘移但不确定是不是time的问题可以先把time全部设为0跑一次再和正常time的结果对比。如果两者差异明显说明time字段确实在起作用。5. 实操心得与避坑经验5.1 排列方式判断的快捷方法判断点云是按线束排列还是按方位角排列我有个很实用的技巧取一帧点云打印前100个点的坐标观察它们的模式。如果前几个点的z值相近但x, y变化较大说明是按线束排列如果前几个点的x, y相近但z值变化较大说明是按方位角排列。这个方法比翻文档快得多。另外有些雷达的排列方式既不是纯线束也不是纯方位角而是混合的。这种情况下你需要更仔细地分析点云结构必要时可以联系雷达厂商获取驱动源码直接看数据打包的逻辑。5.2 time字段的精度取舍time字段的精度直接影响去畸变效果但也不是越精确越好。我实测下来对于10Hz的16线雷达time的精度到微秒级别就完全够用了。再高的精度对建图效果的提升微乎其微反而会增加计算量。所以在线性分配time的时候不需要搞得太复杂按方位角比例分配就能满足绝大多数场景。但有一种情况需要特别注意如果你的雷达扫描频率不是固定的比如某些固态雷达的扫描模式是变频率的那么time的计算就不能简单按固定周期来分配必须根据实际的时间戳来计算。这种情况下建议直接从雷达驱动获取每个点的时间戳。5.3 外参标定的重要性被严重低估很多人把注意力全放在ring和time上却忽略了外参标定。我见过不少案例ring和time都适配得好好的但建图就是飘最后发现是雷达和IMU的外参矩阵填错了。LIO-SAM对雷达和IMU之间的外参非常敏感旋转矩阵错一点点建图结果就会差很多。标定外参的方法有很多可以用lidar_align工具也可以手动标定。手动标定的基本思路是让平台做纯旋转运动观察IMU和雷达的角速度是否一致通过调整外参矩阵使两者对齐。这个过程需要耐心但一旦标定准确建图效果会有质的提升。5.4 不同雷达型号的适配差异不同品牌的雷达在数据格式上差异很大适配时需要注意以下几点Velodyne系列雷达通常自带ring和time字段适配最简单Ouster雷达的ring字段叫ring但time字段的单位可能是纳秒需要转换RoboSense雷达的time字段通常是相对于帧起始的偏移单位是秒国产某些雷达可能只输出x, y, z需要完全自己补全我建议在适配之前先花时间仔细阅读雷达的驱动文档和源码搞清楚数据的确切格式。这一步偷懒后面调试的时间会成倍增加。5.5 性能优化的几个实用技巧当雷达线束较多比如64线或128线时点云适配的计算量会比较大。以下几个优化技巧可以帮你提升性能第一使用PointCloud2Iterator而不是手动计算偏移量这样代码更安全编译器也更容易优化。第二如果不需要修改intensity字段可以直接复用原始值避免不必要的赋值操作。第三对于ring和time的计算尽量用位运算代替除法和取模尤其是在循环内部。第四如果雷达驱动支持尽量在驱动层面直接输出带ring和time的点云避免额外的转换节点。我在处理128线雷达数据时最初用了一个单独的转换节点结果CPU占用率很高。后来把转换逻辑直接集成到雷达驱动里性能提升了将近一倍。所以如果你的雷达驱动是开源的直接改驱动是更好的选择。6. 从适配到建图的完整流程回顾6.1 完整操作流程清单把整个适配流程梳理成一份可执行的清单方便你按步骤操作用rostopic echo确认原始点云的字段结构确定雷达的线束数、扫描频率和点云排列方式编写适配节点补全ring和time字段在RViz中可视化验证点云结构修改LIO-SAM的params.yaml设置正确的N_SCAN和Horizon_SCAN标定雷达与IMU的外参矩阵用rosbag录制测试数据并回放观察建图结果根据异常现象排查问题性能优化必要时将适配逻辑集成到雷达驱动这份清单看起来步骤不多但每一步都可能遇到坑。我的建议是不要跳步尤其是第4步和第6步很多人觉得可视化验证没必要外参标定差不多就行结果后面调试花的时间远超预期。6.2 验证适配成功的几个标志怎么判断适配已经成功了我总结几个标志点云在RViz中呈现清晰的多线结构没有明显的扭曲LIO-SAM启动后没有报错特征提取正常建图结果中墙壁、地面等平面结构清晰没有重影轨迹闭合误差在合理范围内回环检测能正常工作。如果这几个标志都满足说明适配基本到位了。如果只能满足其中一部分比如点云看起来正常但建图还是飘那问题可能出在外参或者LIO-SAM的参数配置上需要进一步排查。6.3 后续扩展方向适配完成后还有一些可以进一步优化的方向。比如可以尝试不同的特征提取参数针对你的雷达特性调优可以加入回环检测的优化提升大场景下的建图精度如果雷达支持运动补偿可以在驱动层面直接输出去畸变后的点云减轻LIO-SAM的计算负担。另外如果你后续要换用其他SLAM框架比如LIO-Livox或者FAST-LIOring和time的适配逻辑基本是通用的这套方案可以直接迁移过去。所以花时间把这两个字段搞清楚是一次投入长期受益的事情。我个人在实际操作中的体会是自定义雷达适配LIO-SAM最难的部分不是写代码而是搞清楚雷达数据的真实结构。一旦你完全理解了数据的排列方式和时间分配逻辑后面的适配就是水到渠成的事情。所以遇到问题不要急着改代码先花时间把数据本身研究透往往能事半功倍。
返回列表