ARTICLE DETAIL

资讯详情

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

KITTI数据集适配lego-loam:点云格式、标定与评测全解析

KITTI数据集适配lego-loam:点云格式、标定与评测全解析 简介这份资源是面向LiDAR SLAM研究与自动驾驶感知学习者的LEGO-LOAM适配版本针对Kitti数据集在数据读取、时间戳同步、特征提取与点云匹配等环节做了针对性修改可缓解原版算法在Kitti场景下效率偏低、定位漂移等问题适合具备一定SLAM基础、希望在真实激光雷达数据上验证建图与里程计效果的研究者。压缩包共23个文件约26.74MB以cpp源码与h头文件为核心辅以launch启动配置、msg消息定义、rviz可视化配置、xml参数文件并附有md说明、pdf论文预印本及少量jpg、png、gif示意图便于快速理解算法结构与运行方式。目前已有648人学习下载。读者可据此在Kitti数据集上复现并评估LEGO-LOAM的定位与建图表现结合源码与论文对照分析特征提取、回环检测等模块的改动思路并在此基础上做进一步定制实验。1. 为适配 KITTI 数据集修改的 lego-loam一次把点云格式、标定与评测链路讲透KITTI 数据集里的点云是 Velodyne HDL-64E 采的.bin文件按x,y,z,intensity四个 float32 紧凑排列没有 ring、没有时间戳字段而 lego-loam 默认吃的是 Velodyne 驱动吐出来的velodyne_points带 ring 通道和时间戳。直接拿 KITTI 的.bin喂进去第一帧就可能因为 ring 缺失导致地面分割和聚类全乱跑出来的轨迹和真值差出几十米。这个标题要解决的就是这条链路把 KITTI 的原始点云、标定文件、时间戳对齐到 lego-loam 的输入假设上让它能跑通、能出轨迹、能跟 KITTI 的 ground truth 做对比。适合做 SLAM 课程作业、激光里程计对比实验、或者想把 lego-loam 当 baseline 跑 KITTI 的从业者。下面按「数据长什么样 → 怎么改 → 怎么跑 → 坑在哪 → 怎么验证」的顺序拆开讲。2. KITTI 点云与 lego-loam 输入假设的差异先搞清楚为什么直接跑会翻车2.1 KITTI 的 .bin 到底存了什么KITTI 的 velodyne 点云文件是二进制流每个点 16 字节四个 float32x、y、z、intensity。intensity 是反射率归一化到 0~1 的值不是 ring ID也不是时间偏移。整个文件没有 header没有点数量字段读的时候只能靠文件大小除以 16 反推点数。这意味着你拿到的点云是「无序」的——没有扫描线编号也没有每个点相对于帧起始的时间戳。lego-loam 的 feature extraction 里地面点分割依赖扫描线角度做扇区划分聚类依赖相邻点的时间差做运动补偿这两个信息在 KITTI 原始 bin 里都不存在。2.2 lego-loam 对输入点云的三个硬性假设lego-loam 的imageProjection节点在收到sensor_msgs/PointCloud2后会按 ring 字段把点分到不同扫描线上每条线再按水平角度排序。它假设点云里有一个ring字段uint16值从 0 到 N-1N 是线数还假设有一个time或t字段float32表示该点相对帧起始的秒级偏移。没有这两个字段imageProjection要么直接报字段缺失要么把整帧当成一条线处理地面分割彻底失效。第三个假设是点云坐标系lego-loam 默认 lidar 坐标系是 x 向前、y 向左、z 向上而 KITTI 的 velodyne 坐标系是 x 向前、y 向左、z 向上——这个倒是一致的不用转。2.3 标定文件与 TF 树的对应关系KITTI 的标定文件calib.txt里有Tr_velo_to_cam、R_rect、P_rect等矩阵描述的是 velodyne 到相机、相机到图像平面的变换。lego-loam 本身不直接读这些文件它只关心base_link到velodyne的 TF。如果你只跑激光里程计不需要相机标定只需要把base_link到velodyne设成单位变换或者按车辆安装位置设一个固定外参。但如果你想把 lego-loam 的轨迹投影到图像上做验证就需要把Tr_velo_to_cam转成 TF 或直接在代码里做矩阵乘法。常见做法是写一个静态 TF publisher把Tr_velo_to_cam的逆矩阵发成velodyne到camera的变换。3. 改造 lego-loam 读 KITTI从 .bin 解析到 ring 与时间戳重建3.1 写一个 KITTI .bin 到 PointCloud2 的转换节点最直接的办法是写一个 ROS 节点读.bin文件解析成pcl::PointXYZI再补上 ring 和时间戳最后以sensor_msgs/PointCloud2发出来。ring 的估算可以用垂直角度对每个点算atan2(z, sqrt(x*xy*y))然后按 HDL-64E 的垂直角分辨率约 0.4 度量化到 0~63。时间戳可以用水平角度近似假设雷达转速 10Hz一帧 0.1 秒按水平角从 -180 到 180 线性映射到 0~0.1 秒。这个近似不完美但足够让 lego-loam 的运动补偿跑起来。import numpy as np import rospy from sensor_msgs.msg import PointCloud2, PointField import sensor_msgs.point_cloud2 as pc2 def read_kitti_bin(bin_path): # KITTI bin: 每个点 4 个 float32 (x, y, z, intensity) scan np.fromfile(bin_path, dtypenp.float32).reshape(-1, 4) return scan # shape (N, 4) def add_ring_and_time(scan): x, y, z scan[:, 0], scan[:, 1], scan[:, 2] # 垂直角度量化到 64 线 vert_angle np.arctan2(z, np.sqrt(x**2 y**2)) # 弧度 ring np.clip(((vert_angle np.deg2rad(24.8)) / np.deg2rad(0.4)).astype(np.int32), 0, 63) # 水平角度映射到 0~0.1 秒 horiz_angle np.arctan2(y, x) # -pi ~ pi time_offset (horiz_angle np.pi) / (2 * np.pi) * 0.1 return ring.astype(np.uint16), time_offset.astype(np.float32) def publish_cloud(pub, scan, ring, time_offset, frame_id): 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), PointField(ring, 16, PointField.UINT16, 1), PointField(time, 18, PointField.FLOAT32, 1), ] # 手动打包因为 pc2.create_cloud 不支持混合类型 import struct points [] for i in range(scan.shape[0]): points.append(struct.pack(ffffHf, scan[i,0], scan[i,1], scan[i,2], scan[i,3], ring[i], time_offset[i])) cloud PointCloud2() cloud.header.frame_id frame_id cloud.height 1 cloud.width scan.shape[0] cloud.fields fields cloud.is_bigendian False cloud.point_step 22 # 4*4 2 4 22 字节 cloud.row_step cloud.point_step * cloud.width cloud.data b.join(points) pub.publish(cloud)这段代码的关键在point_step和字段偏移x 在 0y 在 4z 在 8intensity 在 12ring 在 16uint16 占 2 字节time 在 18float32 占 4 字节总步长 22。如果偏移写错lego-loam 读出来的 ring 全是垃圾。ring 的量化公式里24.8是因为 HDL-64E 的垂直视场是 2 到 -24.8 度先平移到 0 起点再除以 0.4 度分辨率。时间戳用水平角线性映射误差在几毫秒量级对 10Hz 的帧率够用。3.2 修改 lego-loam 的 imageProjection 以兼容无 ring 情况如果你不想写转换节点也可以直接改 lego-loam 源码在imageProjection.cpp里加一个分支当点云没有 ring 字段时用垂直角度现算。找到copyPointCloud函数在pcl::fromROSMsg之后加一段// 如果 ring 字段缺失用垂直角度估算 if (cloud_in-points[0].ring 0 cloud_in-points[1].ring 0) { for (auto pt : cloud_in-points) { float vert_angle atan2(pt.z, sqrt(pt.x*pt.x pt.y*pt.y)); pt.ring static_castuint16_t(std::max(0.0f, std::min(63.0f, (vert_angle 24.8f * M_PI / 180.0f) / (0.4f * M_PI / 180.0f)))); } }这个改法的风险是如果点云本身有 ring 但第一两个点恰好是 0会误判。更稳妥的判断是检查cloud_in-fields里有没有ring字段名。另外lego-loam 的imageProjection里还有一段用ring做groundMat和rangeMat填充的逻辑如果 ring 是现算的填充顺序会跟原始驱动不一样但地面分割本身是按角度扇区做的影响不大。3.3 时间戳对齐与 TF 配置KITTI 的每帧点云在times.txt里有时间戳但那个是帧级时间不是点级。lego-loam 的运动补偿需要点级时间所以上面用水平角近似是必要的。TF 方面如果你只跑里程计launch 文件里把base_link到velodyne设成单位矩阵即可node pkgtf typestatic_transform_publisher namebase_to_velo args0 0 0 0 0 0 base_link velodyne 100 /如果你要跟 KITTI 的 ground truth 对比ground truth 的位姿是相机坐标系下的需要把 lego-loam 输出的camera_init到base_link的轨迹经过Tr_velo_to_cam变换到相机坐标系再跟真值算误差。这个变换矩阵在 KITTI 的calib.txt里格式是 3x4 的Tr_velo_to_cam补成 4x4 后取逆得到cam_to_velo再乘到轨迹上。4. 跑通 KITTI 序列的完整命令与参数调优4.1 数据准备与目录结构KITTI 的 odometry 数据集下载后是dataset/sequences/00/velodyne/下的一堆.bin还有times.txt和calib.txt。lego-loam 的 launch 文件里需要指定点云话题名默认是/velodyne_points。你的转换节点发出来的话题名要跟这个对上或者改 launch 里的lidarTopic参数。常见做法是写一个 launch 文件同时启动转换节点和 lego-loamlaunch node pkgkitti2lego typebin_player namebin_player outputscreen param namebin_dir value/data/kitti/sequences/00/velodyne/ param nameframe_rate value10/ /node include file$(find lego_loam)/launch/run.launch/ /launchbin_player节点按 10Hz 定时读下一帧.bin调用上面的read_kitti_bin和publish_cloud。注意frame_rate要跟 KITTI 的实际采集频率一致00 序列大约是 10Hz但有些序列有丢帧最好按times.txt里的时间戳来发而不是固定间隔。4.2 lego-loam 关键参数对 KITTI 的适配lego-loam 的run.launch里有几个参数直接影响 KITTI 上的表现参数默认值KITTI 建议值说明scanPeriod0.10.110Hz跟 KITTI 一致sensorvelodynevelodyneHDL-64E 用 velodyneN_SCAN1664线数必须改成 64Horizon_SCAN18002083水平分辨率64 线约 0.18 度lowerBound-30-24.8垂直视场下限upperBound102垂直视场上限N_SCAN和Horizon_SCAN在imageProjection.cpp里是硬编码的宏改 launch 参数没用得直接改源码重新编译。lowerBound和upperBound在featureExtraction.cpp里也有硬编码同样要改。这是 lego-loam 设计上的一个坑它假设你用的是 16 线雷达参数写死在代码里换 64 线必须动源码。4.3 编译与运行时的常见报错编译时如果报ring字段相关错误检查PointXYZI的定义——lego-loam 用的是自定义的PointXYZIR还是 PCL 的PointXYZI。有些版本的 lego-loam 用pcl::PointXYZI但代码里访问.ring这会导致编译失败。解决办法是换成pcl::PointXYZIR或者自己加一个带 ring 的结构体。运行时如果报Failed to find match for field ring说明转换节点发的 PointCloud2 里 ring 字段的偏移或类型不对用rosrun pcl_ros pcl_viewer看一下点云能不能正常显示再检查point_step。5. 避坑与排查KITTI 上跑 lego-loam 的五个血泪教训5.1 现象轨迹漂移严重Z 轴上下跳原因KITTI 的.bin里 intensity 是 0~1 的浮点但 lego-loam 的 feature extraction 里用 intensity 做地面点筛选时阈值是按 Velodyne 驱动的 0~255 设的。intensity 全小于 1 会导致地面点被误判为障碍物地面分割失效Z 轴估计跟着乱跳。解决在转换节点里把 intensity 乘 255或者改featureExtraction.cpp里的 intensity 阈值。我一般直接在转换节点里乘不动源码。5.2 现象跑几十帧后跟丢imageProjection报 rangeMat 越界原因Horizon_SCAN设成 1800 但 KITTI 的 64 线雷达水平分辨率约 2083角度量化时超出数组边界。lego-loam 里rangeMat是cv::Mat(N_SCAN, Horizon_SCAN, CV_32F)Horizon_SCAN 小了就会越界写。解决把Horizon_SCAN改成 2083 或更大同时检查N_SCAN是不是 64。改完必须catkin_make重新编译只改 launch 没用。5.3 现象时间戳近似导致高速场景运动补偿失效原因用水平角线性映射时间戳在车辆高速转弯时同一帧内不同点的实际时间差跟线性假设偏差大运动补偿把点云扭曲了。解决如果 KITTI 序列车速不高城市工况这个近似够用如果是高速场景最好用 KITTI 的times.txt做帧级对齐点级时间戳用匀速旋转模型重新算或者直接关掉运动补偿把time字段全设 0牺牲一点精度换稳定。5.4 现象跟 ground truth 对比时误差计算出来是几百米原因lego-loam 输出的轨迹是camera_init到base_link而 KITTI 的 ground truth 是相机坐标系下的位姿。直接拿两个轨迹算欧氏距离坐标系没对齐误差自然大。解决用Tr_velo_to_cam把 lego-loam 的轨迹变换到相机坐标系再做时间戳对齐KITTI 的 ground truth 是 10Hzlego-loam 输出也是 10Hz但起始帧可能差几帧。对齐后用evo工具算 ATE注意evo的--align参数要选origin或none别用sim3。5.5 现象编译通过但运行时点云为空原因转换节点发的 PointCloud2 的row_step或point_step算错lego-loam 的pcl::fromROSMsg解析出空点云。解决用rostopic echo /velodyne_points --noarr看 header 和字段再用rosrun pcl_ros pcl_viewer看能不能显示。如果 viewer 显示正常但 lego-loam 收不到检查话题名和 queue size。常见的是转换节点发的话题名是/kitti_points但 lego-loam 订阅的是/velodyne_points。6. 验证轨迹精度与进阶技巧用 evo 算 ATE 并做帧级对齐跑通之后验证精度是绕不开的一步。我一般用evo工具先把 lego-loam 输出的轨迹存成 TUM 格式timestamp x y z qx qy qz qw再把 KITTI 的 ground truth 也转成 TUM。KITTI 的 ground truth 在poses/00.txt里每行 12 个数是 3x4 的变换矩阵按行展开。转 TUM 的脚本大概这样import numpy as np from scipy.spatial.transform import Rotation as R def kitti_pose_to_tum(pose_file, times_file, output_file): poses np.loadtxt(pose_file) times np.loadtxt(times_file) with open(output_file, w) as f: for i, pose in enumerate(poses): mat pose.reshape(3, 4) t mat[:, 3] rot R.from_matrix(mat[:, :3]) q rot.as_quat() # x, y, z, w f.write(f{times[i]:.6f} {t[0]:.6f} {t[1]:.6f} {t[2]:.6f} f{q[0]:.6f} {q[1]:.6f} {q[2]:.6f} {q[3]:.6f}\n)注意 KITTI 的 ground truth 是相机坐标系lego-loam 的是 lidar 坐标系算 ATE 之前要用Tr_velo_to_cam把 lego-loam 的轨迹转到相机系。evo的命令是evo_ape kitti ground_truth.txt lego_loam_traj.txt --align --plot--align会做 SE3 对齐但如果你已经手动对齐了坐标系可以用--align origin只对齐起点。跑完看rmseKITTI 00 序列上 lego-loam 调好的话 ATE 大概在 1% 左右比 LOAM 略差但比纯 ICP 好很多。进阶技巧如果你想让 lego-loam 在 KITTI 上更稳可以关掉它的回环检测loopClosure设 false因为 KITTI 的序列里回环不多回环检测反而容易引入错误约束。另外featureExtraction里的edgeThreshold和surfThreshold可以适当调大KITTI 的 64 线点云比 16 线密特征点太多会拖慢后端优化。我一般把edgeThreshold从 0.1 调到 0.2surfThreshold从 0.1 调到 0.15跑起来帧率能到 10Hz 实时。这些参数没有理论最优都是我在几台机器上试出来的换数据集得重新调。希望帮到你。本文还有配套的精品资源点击获取
返回列表