简介:面向激光SLAM学习与KITTI数据评测需求,这份kitti-lego-loam资源提供经修改的LeGO-LOAM代码,可在KITTI数据集上直接运行并评估轨迹,适合不熟悉ROS与LOAM、希望快速复现激光里程计实验的学生和工程师。压缩包共35个文件、约26.84MB,以5个cpp和3个头文件承载核心算法,2个launch与1个rviz配置负责启动和可视化,另有README、PDF说明、msg消息定义及txt/xml等工程文件,并配有少量图片、动图辅助理解目录结构与运行效果。当前已有6361人学习下载。使用后可获得KITTI地面格式轨迹,并能借助EVO-eval套件对照真值完成精度评估;代码同时给出GTSAM等依赖提示,便于搭建实验环境、复现LeGO-LOAM流程、定位编译运行中的常见问题。
1. 把 KITTI 的 velodyne 点云直接喂进 LeGO-LOAM,跑出来的轨迹和 ground-truth 一叠,ATE 动不动几十米——多数时候不是算法趴窝,是数据链路在骗你
kitti-lego-loam 这套东西解决的就是这条链路:KITTI 数据怎么变成 LeGO-LOAM 认的点云、launch 里那几个参数在 KITTI 上该改成什么、跑完的轨迹怎么和 ground-truth 用 evo 对齐打分。适合已经能编过 ROS 工作空间、想拿公开数据集验证激光里程计精度的人:新手照着步骤能跑出第一张 ATE 表,熟手能对着参数表和坐标变换看边界。整套流程里最玄学的不是建图,是格式对齐——点云的 ring 字段、逐点时间、bag 的时间基准、输出坐标系,哪一环没对上,最后的数字都没有意义。先把这层捅破,后面 evo 报出来的 RMSE 才值得信。
2. 数据落盘与格式对齐:从 KITTI bin 到 ROS bag 的转换链路
KITTI odometry 的一帧点云是一段裸二进制:每点 4 个 float32,x、y、z、intensity,强度归一化到 0 到 1 之间。它没有 ring(线号),也没有逐点相对时间。而 LeGO-LOAM 的 imageProjection 恰恰依赖这两样东西做球面投影和分簇:没有 ring,相邻扫描线的关系就断了;没有相对时间,去畸变那段就只能瞎猜。这一章把「补齐字段、打 bag、转 GT」三件事拆开做。
2.1 KITTI odometry 目录里哪些文件是硬需求
先把目录看明白,别一上来就写转换脚本。odometry 数据集里跟激光里程计相关的文件其实就那么几个,缺哪个、后果是什么,心里得有个表。
| 路径 | 内容 | 缺了会怎样 |
|---|---|---|
sequences/00/velodyne/*.bin | 每帧 4×float32 的点云 | 没有数据可播,直接卡死在这一步 |
sequences/00/times.txt | 每帧时间戳(秒,浮点) | bag 的时间戳只能自己造,evo 配对时全是错位 |
sequences/00/calib.txt | 相机与雷达的标定矩阵 | 纯激光模式可暂缺,做点云投影或多传感器融合时必须有 |
poses/00.txt | 3×4 位姿矩阵,每行 12 个数 | 没有 ground-truth,只能看地图形态,没法定量 |
sequences/00/image_0、image_1 | 灰度双目图 | LeGO-LOAM 纯激光不用,可以整目录忽略 |
有两个细节容易翻车。第一,只有 00 到 10 这 11 个序列带 ground-truth,11 到 21 是纯测试集,别拿 11 号去算 ATE 然后怀疑人生。第二,times.txt的时间往往是从 0 开始的相对秒,直接塞进 ROS 时间会得到 stamp 为 0 的消息,部分节点对零时间戳的处理并不友好,实操里习惯加一个固定偏移(比如 1.0 秒)再发。
2.2 用脚本把 bin 点云补齐 ring 与相对时间后打成 rosbag
转换脚本的核心是三件事:读 bin、算 ring、算逐点时间,然后打包成sensor_msgs/PointCloud2。不要用现成的通用转换包一把梭,字段顺序和 datatype 一旦不对,rviz 里看着有点,LeGO-LOAM 里就是一堆散点。
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import glob import numpy as np import rosbag from sensor_msgs.msg import PointCloud2, PointField N_SCAN = 64 ANG_BOTTOM = 24.8 # 最低线与水平面的夹角(度),HDL-64E 约 +2 ~ -24.8 ANG_RES_Y = 26.8 / (N_SCAN - 1) LINE_DURATION = 0.1 # 一圈 10Hz,转一圈 0.1 秒 def read_bin(path): # KITTI 每点 4 个 float32:x y z intensity return np.fromfile(path, dtype=np.float32).reshape(-1, 4) def build_fields(): return [ 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), ] def build_cloud(scan, stamp): x, y, z = scan[:, 0], scan[:, 1], scan[:, 2] # 1) 俯仰角量化成线号,clip 兜住边界,避免越界 pitch = np.degrees(np.arctan2(z, np.sqrt(x * x + y * y))) ring = np.clip(((ANG_BOTTOM - pitch) / ANG_RES_Y).astype(np.int32), 0, N_SCAN - 1) # 2) 逐点相对时间用方位角线性插值,转一圈 0.1s az = np.arctan2(y, x) t = ((az + np.pi) / (2 * np.pi) * LINE_DURATION).astype(np.float32) # 3) 按 x y z intensity ring time 顺序拼成结构化数组,packed 后逐点 memcpy arr = np.zeros(len(scan), dtype=[ ('x', np.float32), ('y', np.float32), ('z', np.float32), ('intensity', np.float32), ('ring', np.uint16), ('time', np.float32)]) arr['x'], arr['y'], arr['z'] = x, y, z arr['intensity'] = scan[:, 3] arr['ring'], arr['time'] = ring.astype(np.uint16), t msg = PointCloud2() msg.header.stamp = stamp msg.header.frame_id = "velodyne" msg.height, msg.width = 1, len(scan) msg.fields = build_fields() msg.is_bigendian = False msg.point_step = 22 msg.row_step = 22 * len(scan) msg.is_dense = True msg.data = arr.tobytes() return msg def main(seq_dir, bag_path, time_offset=1.0): with rosbag.Bag(bag_path, 'w') as bag: times = np.loadtxt(os.path.join(seq_dir, "times.txt")) bins = sorted(glob.glob(os.path.join(seq_dir, "velodyne", "*.bin"))) for i, b in enumerate(bins): stamp = times[i] + time_offset msg = build_cloud(read_bin(b), rospy_time(stamp)) bag.write("/velodyne_points", msg, msg.header.stamp)逻辑上分三层。第一层read_bin只负责把二进制摊平成 N×4 的数组,reshape(-1, 4)里的 4 是硬约定,KITTI 没有别的格式。第二层算字段,ring 用俯仰角反推而不是查表:HDL-64E 的垂直角分辨率约 26.8/63 度,把俯仰角代入这个线性关系取整,就能得到 0 到 63 的线号。这个近似在 KITTI 上足够用,因为它的点云本身是理想化的。逐点时间用的是方位角线性插值,本质是在假设一圈匀速旋转——KITTI 没给真实时间,这是业界常见的近似,够拿来跑通、不够拿来发表。
第三层的几个参数值得单独说。point_step = 22是 4 个 float32 加 1 个 uint16 加 1 个 float32,字节数必须严格等于字段偏移量的总和,写错一个字节 rviz 会直接花屏。time_offset就是我上面说的那个偏移,默认给 1.0 秒。frame_id用velodyne只是习惯,最终要和 launch 里的 TF 对上。
2.3 ground-truth 位姿转成 evo 能直接读的两类格式
KITTI 的poses/00.txt每行是 3×4 位姿矩阵的 12 个数,行主序。evo 支持直接读这种格式(evo_ape kitti),但如果要做时间重采样或和别的轨迹插值,转成 TUM 的timestamp tx ty tz qx qy qz qw更方便。
import numpy as np from scipy.spatial.transform import Rotation as Rot def kitti_to_tum(pose_file, time_file, out_file, t_offset=1.0): poses = np.loadtxt(pose_file).reshape(-1, 3, 4) times = np.loadtxt(time_file) with open(out_file, "w") as f: for i in range(len(poses)): T = poses[i] t = T[:, 3] q = Rot.from_matrix(T[:, :3]).as_quat() # 返回顺序正好是 x y z w f.write("%.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f\n" % (times[i] + t_offset, t[0], t[1], t[2], q[0], q[1], q[2], q[3]))注意Rot.as_quat()返回的是(x, y, z, w),和 TUM 要求的顺序一致,这一点很多人会下意识按(w, x, y, z)写反,结果 evo 报出来的误差大得离谱却查不出原因。t_offset必须和点云那边用同一个值,否则按时间关联时整条轨迹会错位。如果不想转 TUM,直接用 kitti 格式也行,代价是 evo 只能按行号配对,帧数必须严格相等——这个坑在第五章展开。
3. LeGO-LOAM 的四个参数块:地面分割、聚类、特征、回环,在 KITTI 上各该动哪里
LeGO-LOAM 相对原始 LOAM 的改动集中在两处:先用地面分割把点云分成地面和非地面,再对非地面做聚类,最后在簇内提特征。这套设计在结构化道路场景收益很大,而 KITTI 恰好就是典型的城市与公路场景。但默认参数是照着 16 线雷达和室内外混合场景调的,搬到 64 线的 KITTI 上,好几个地方必须改,否则要么地面提不出来,要么分簇碎成一地。
3.1 N_SCAN、Horizon_SCAN 与 groundScanInd:球面投影的分箱基础
这三个参数都在utility.h里,是整条流水线的地基。N_SCAN是线数,KITTI 是 HDL-64E,填 64。ang_res_y是垂直角分辨率,用26.8/(N_SCAN-1)算。ang_bottom是最低线相对水平面的角度,HDL-64E 取 24.8。这三个基本没得商量。
真正需要现场量的是Horizon_SCAN,也就是水平方向一圈切多少个箱。切少了,同一列里会挤进方位角差很远的点,投影图糊成一片;切多了,大部分箱是空的,特征提取时邻域搜索大量落空。KITTI 每圈的实际点数在 2000 上下浮动,具体值跟场景有关,常见做法是先统计再定:
import numpy as np scan = np.fromfile("000000.bin", dtype=np.float32).reshape(-1, 4) az = np.degrees(np.arctan2(scan[:, 1], scan[:, 0])) print("一帧点数:", len(scan)) print("0.2 度分箱后唯一列数:", len(np.unique(np.round(az / 0.2))))跑出来如果唯一列数接近 2000,Horizon_SCAN就别用默认的 1800,往上提到 2080 或 2250。这不是玄学,投影时列索引超出会被回绕,回绕就意味着两个不同方向的点被塞进同一个箱,地面分割的连通性直接崩掉,表现出来是地面被切成碎片、建图飘。宁大勿小是这个参数唯一的原则。
groundScanInd决定哪几条线被当成地面。KITTI 的车顶安装高度和俯仰角决定了地面点主要落在下面 18 到 22 条线上,我一般先填 20,然后在 rviz 里看/ground_cloud话题:地面点如果一直延伸到远处的墙面,说明取大了;如果近处地面都缺一块,说明取小了,往下调两条线再看。
| 参数 | 位置 | KITTI 取值 | 调整依据 |
|---|---|---|---|
N_SCAN | utility.h | 64 | 雷达线数,固定 |
Horizon_SCAN | utility.h | 2080 ~ 2250 | 每圈实际点数,宁大勿小 |
ang_res_y | utility.h | 26.8/(N_SCAN-1) | 由线数和垂直视场推导 |
ang_bottom | utility.h | 24.8 | 最低线俯角 |
groundScanInd | utility.h | 18 ~ 22 | 看/ground_cloud调 |
useCloudRing | utility.h | true | 点云带 ring 字段时必须开 |
3.2 地面分割与聚类:两个阈值决定了特征点的质量
分割逻辑在imageProjection.cpp里。同一列相邻两点,如果它们之间的连线跟水平面的夹角小于某个阈值,就判为同一类(地面);否则断开。聚类则是在非地面点上做基于距离的连通域生长,相邻点距离超过阈值就分到不同簇。这两个阈值在源码里是写死的常量,KITTI 上一般不用动,但有两件事要确认。
第一,去畸变要不要开。KITTI odometry 的点云在发布时已经做过一定程度的运动补偿,而 LeGO-LOAM 默认会拿每点的time字段再做一遍。开与不开的差别在直道上几乎看不出来,在转弯和加减速段能差出几十厘米。我一般的做法是跑两遍,同一段序列比 ATE,取小的那个,别凭感觉拍板。
第二,聚类的输出在featureAssociation里被用来限制特征提取的范围:每个簇里只保留曲率最大的若干个边缘点和若干个平面点,簇太小就直接跳过。KITTI 的远处点很稀,一个簇可能只有三五个点,阈值稍微一严就被整段丢掉,表现出来是远处墙面没有特征、位姿在长直道上慢慢漂。这时候与其去改聚类阈值,不如先把Horizon_SCAN调对,很多问题会自己消失。
3.3 回环检测与 GTSAM 后端:车载高速场景要放宽搜索半径
回环部分在mapOptimization.cpp,核心是历史关键帧的搜索。几个参数在 KITTI 上需要重新掂量:
| 参数 | 含义 | 常用取值 | KITTI 上的考量 |
|---|---|---|---|
surroundingkeyframeAddingDistThreshold | 新增关键帧的位移阈值(米) | 1.0 | 车速约 10m/s,1 米大约十帧一次,合理 |
surroundingkeyframeAddingAngleThreshold | 新增关键帧的角度阈值(弧度) | 0.2 | 直道上几乎不触发,转弯时主导 |
surroundingKeyframeSearchRadius | 局部地图的搜索半径(米) | 50 | 场景大时可以提到 80 |
historyKeyframeSearchRadius | 回环候选的半径(米) | 15 | 车载场景建议 20 ~ 25 |
historyKeyframeSearchTimeDiff | 回环候选的最小时间差(秒) | 30 | 防止把相邻帧错认成回环 |
historyKeyframeFitnessScore | ICP 配准的接受阈值 | 0.3 | 点云稀疏时可放宽到 0.5 |
historyKeyframeSearchNum | 每次搜索的历史帧数 | 25 | 越大越慢,先别动 |
回环半径 15 米在室内或者园区里够用,放到 KITTI 上就偏保守:车开得快,几秒钟就出去几十米,回到同一地点的两次经过在时间上隔了很久,空间上却可能差二十多米。把半径提到 20 到 25,配合时间差阈值,回环触发率会明显上来。但historyKeyframeFitnessScore别放松过头,0.5 以上就容易把长得像的路口错配成回环,GTSAM 优化完之后整张图会拧一下,这种错误在地图上看不出来,在 ATE 上是一根尖刺。
3.4 launch 文件与话题重映射:KITTI 上要砍掉 IMU 这条支路
原版的启动文件会把 IMU 预积分节点一起拉起来,它在等/imu/data。KITTI 没有 IMU,节点起不来就发不出对应的 TF,下游的transformFusion一直等,表现出来是 odom 完全不动,或者偶尔跳一下。纯激光跑 KITTI,启动文件只留四个节点:
<launch> <param name="/use_sim_time" value="true"/> <node pkg="lego_loam" type="imageProjection" name="imageProjection" output="screen"/> <node pkg="lego_loam" type="featureAssociation" name="featureAssociation" output="screen"/> <node pkg="lego_loam" type="mapOptimization" name="mapOptimization" output="screen"/> <node pkg="lego_loam" type="transformFusion" name="transformFusion" output="screen"/> <!-- bag 里的话题名不是 /velodyne_points 时在这里改 --> <remap from="/velodyne_points" to="/velodyne_points"/> </launch>/use_sim_time必须为 true,原因是 bag 回放用的是录制时的时间基准,节点如果还按墙钟走,TF 的查询会一直报外推失败。这一条几乎是所有离线回放问题的源头,先确认它,再去查别的。remap那一行看着像废话,但它的存在是提醒你:bag 里的话题名、launch 里订阅的话题名、rviz 里加的点云话题,三处必须一致,转换脚本里写的是/velodyne_points,那就以它为准。
4. 跑通与量化:从 rosbag play 到 evo 出 ATE/RPE 的完整命令链
数据和参数都对齐之后,剩下的就是把流程跑起来。这一段最容易出的问题不是算法,是节奏:LeGO-LOAM 单帧处理时间在普通工作站上可能超过 100 毫秒,而 KITTI 是 10Hz 数据,按原速播一定会丢帧。丢帧之后轨迹行数和 GT 对不上,后面所有数字都白算。
4.1 启动顺序与回放节奏
先起 launch,再放 bag,两个终端分开,别图省事写一个脚本里。launch 起来之后在 rviz 里确认 TF 树里map、camera_init、velodyne都在,再放数据。
# 终端一:启动纯激光节点 source devel/setup.bash roslaunch lego_loam kitti_run.launch # 终端二:以半速回放并发布仿真时钟 rosbag play kitti_00.bag -r 0.5 --clock --pause--clock配合/use_sim_time,把 bag 的时间发布成/clock,所有节点跟着它走。--pause让 bag 先停在第一帧,这时候去 rviz 里看一眼:点云是不是成圈的线状分布、有没有明显的飞点、/ground_cloud是不是贴着地面。确认没问题再按空格开始。-r 0.5是半速,先保证不丢帧,跑通之后再试 1.0,看处理延迟还能不能跟上。
4.2 订阅里程计话题并落盘成 TUM 轨迹
LeGO-LOAM 的位姿发布在/aft_mapped_to_init,类型是nav_msgs/Odometry,坐标系是camera_init。用 rosbag record 记下来再离线转是一种办法,但更省事的是直接写个订阅节点落盘,时间戳和位姿一起写,省掉二次转换。
#!/usr/bin/env python # -*- coding: utf-8 -*- import rospy from nav_msgs.msg import Odometry out = open("lego_loam_00_cam.txt", "w") def cb(msg): t = msg.header.stamp.to_sec() p = msg.pose.pose.position q = msg.pose.pose.orientation out.write("%.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f\n" % (t, p.x, p.y, p.z, q.x, q.y, q.z, q.w)) out.flush() # 中途崩了也不至于一条都没留下 rospy.init_node("traj_saver") rospy.Subscriber("/aft_mapped_to_init", Odometry, cb, queue_size=50) rospy.spin()flush()这一行是血泪经验:跑长序列时节点偶尔因为内存或段错误挂掉,没有 flush 的话缓冲区里的位姿全丢,白跑半小时。queue_size给 50 是留点余量,订阅端处理不过来的话消息会被丢弃,轨迹出现时间跳变,后面按时间关联就对不上了。写完之后检查一下行数,和poses/00.txt的行数比一比,差太多就说明中间丢帧了,回去把-r降下来重跑。
4.3 把 camera 系位姿转回 KITTI 系:不转这一步,ATE 永远差好几倍
/aft_mapped_to_init在camera_init坐标系下,轴向是 x 向右、y 向下、z 向前。KITTI 的 ground-truth 是 x 向前、y 向左、z 向上。这两套系之间差一个固定的旋转,不是简单换个轴名,必须用矩阵转。
import numpy as np from scipy.spatial.transform import Rotation as Rot # kitti 系基向量在 camera 系下的表示,按列拼起来 R_ck = np.array([[0, -1, 0], [0, 0, -1], [1, 0, 0]], dtype=float) def convert(src, dst): rows = np.loadtxt(src) with open(dst, "w") as f: for r in rows: t, p, q = r[0], r[1:4], r[4:8] T = np.eye(4) T[:3, :3] = Rot.from_quat(q).as_matrix() T[:3, 3] = p # 相似变换:T_kitti = R^T * T_cam * R T_k = R_ck.T @ T @ R_ck q_k = Rot.from_matrix(T_k[:3, :3]).as_quat() f.write("%.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f\n" % (t, T_k[0, 3], T_k[1, 3], T_k[2, 3], q_k[0], q_k[1], q_k[2], q_k[3])) convert("lego_loam_00_cam.txt", "lego_loam_00_kitti.txt")R_ck的三列分别是 KITTI 的前、左、上三个方向在 camera 系里的坐标:前对应 camera 的 z,左对应负 x,上对应负 y。相似变换写成R^T @ T @ R而不是R @ T,是因为位姿是作用在坐标系上的,左右两边都要换基。漏掉右边那个@ R的后果很隐蔽:位置看着大致对,姿态全错,ATE 的平移部分可能还行,一到旋转相关的指标就离谱。转完之后拿前几帧对一下,车应该沿一个轴单调前进,不是斜着走。
4.4 evo 的两条命令与三个必须搞清的对齐选项
# ATE:绝对位姿误差,看整体轨迹和真值差多远 evo_ape kitti gt/00.txt est/lego_loam_00_kitti.txt \ -a --plot --save_results res_00_ape.zip # RPE:相对位姿误差,按每 10 米一段算局部位移漂移 evo_rpe kitti gt/00.txt est/lego_loam_00_kitti.txt \ -a --delta 10 --delta_unit m --plot-a是--align,用 Umeyama 算法做 SE3 对齐,只估旋转和平移,不动尺度。这是评估激光里程计时应该用的选项,因为同一台雷达、同一套坐标系,尺度本来就该一致。千万不要顺手加--correct_scale:它会额外估一个尺度因子,把尺度误差吸收进对齐里,ATE 数字立刻好看一大截,但那是自欺欺人,真实系统里的尺度问题被掩盖了。
--delta 10 --delta_unit m是 RPE 的窗口,按每 10 米位移算一段相对误差,这个指标比 ATE 更能反映「走得越远漂得越多」。--save_results存的 zip 后面可以用evo_res重新出表,做多组参数对比时不用重跑。另外 kitti 格式的轨迹是按行号配对的,两张表的行数必须相等,这是最容易被忽略的前置条件——下一章第一条就是这个。
5. 避坑与排查:五个把 ATE 拉爆的典型问题,按现象、原因、解决拆开
跑通一次不难,难的是数字不对劲的时候知道往哪查。下面这几条都是同一类问题的不同表现:数据本身没错,错的是数据进入算法之前的形态。每条都按「现象 → 原因 → 解决」写,方便对照自己遇到的症状。
5.1 点云在 rviz 里成形,但特征点碎成渣,节点偶尔直接崩
现象是/laser_cloud_sharp和/laser_cloud_flat两个话题上的点稀稀拉拉、没有沿结构分布,跑久一点featureAssociation会段错误退出。原因基本锁定在useCloudRing上:这个开关打开时,代码会去读点云里的ring字段做相邻线判断,而 bin 转 bag 时如果没有补这个字段,读到的就是未定义内存。转 bag 脚本里字段顺序、point_step、datatype三者中任何一个写错,都会走到这个结果。解决分两步:先用rostopic echo /velodyne_points/fields确认ring和time都在,offset和datatype对得上;确认无误后如果还崩,临时把useCloudRing设成false跑一遍,能跑通就说明问题确实在字段上,回头修脚本,别就这么放着——关掉 ring 之后相邻线关系丢失,精度会掉一截。
5.2 前端几十米正常,越走越弯,ATE 到几十米量级
现象是轨迹开头和 GT 基本重合,走出一两百米之后整体向一侧弯,最后回到起点时闭合误差巨大。原因通常是两个叠加:一是Horizon_SCAN偏小,每圈点被压缩后分箱回绕,导致远处点的方位被算错,位姿解算时被一点点带偏;二是没做 4.3 里的坐标变换,两套轴向混着比,误差随距离线性增长。排查顺序是先看坐标:把估计轨迹和 GT 画在同一张俯视图上,如果形状相似只是整体旋转了,那是轴向问题,几十行代码就能修好;如果形状本身就变形,回到Horizon_SCAN,用 3.1 里的统计脚本量一遍每圈点数,把值提上去重跑。
5.3 完整 launch 起得来,但 odom 完全不动或者间歇性乱跳
现象是 rviz 里点云正常刷新,/aft_mapped_to_init却一直是同一个值,或者偶尔跳一下又停住。原因几乎都是 IMU 预积分节点在等一个永远不会来的/imu/data,它不发 TF,下游的transformFusion拿不到变换就卡住。KITTI 是纯激光数据,没有 IMU,这条支路必须砍掉。解决就是用 3.4 里那个四节点的 launch,把imuPreintegration从启动文件里删干净。删完之后如果 odom 还是不动,去看rosnode list里mapOptimization是否还活着,它挂掉的话通常伴随GTSAM的断言失败,那是另一个问题,回去看 3.3 里的回环阈值是不是放得太松。
5.4 evo 报两张轨迹长度不一致,直接拒绝计算
现象是evo_ape报trajectories have different number of poses,或者算出来的结果莫名其妙。原因是 kitti 格式按行号配对,LeGO-LOAM 在某些帧点太少、解不出位姿时会跳过输出,估计轨迹比 GT 少几行。解决有两条路:一是按时间最近邻重采样到同一时间轴再比,二是直接截断到较短的一侧。前者更严谨,后者更快。我一般在保存轨迹的节点里就把queue_size和 flush 做好,尽量不丢帧,同时跑完先wc -l对一下行数,行数不等就别急着跑 evo,先查丢在哪一段。
5.5 ATE 只有零点几米,RPE 随距离爆炸
现象是 ATE 数字漂亮得不像话,一看 RPE 曲线,每 10 米段的误差随里程稳步上升。原因八成是 evo 命令里加了--correct_scale,或者用了某种带尺度估计的对齐方式,把尺度误差吸收到对齐变换里了。纯激光、同传感器
本文还有配套的精品资源,点击获取