资讯详情

资讯详情

lidar_imu_calib:激光雷达与IMU外参自动标定工具实现解析

简介面向基于3D激光雷达SLAM的开发者尤其适合无人车、机器人与自动驾驶定位场景解决了激光雷达与IMU之间外参姿态分量的自动校准问题可为ICP、NDT等匹配算法提供准确的位姿先验。代码以C为主利用ndt_omp完成点云配准通过ROS Bag录制imu与lidar点云话题并按launch文件配置即可运行适合具备ROS与PCL基础的中级开发者。整个压缩包共7个文件、约10KB包含2个cpp核心实现、1个h头文件、launch启动配置、XML包配置及说明文档目录将核心代码、启动配置与说明拆分存放结构精简且定位明确便于直接查看关键算法和快速改动。已有5898人学习下载资料虽小但直击3D激光雷达SLAM中的标定痛点读者可获得自动校准节点源码、运行配置思路以及如何用ndt_omp完成外参姿态标定的可复用流程并迁移到自己的数据集上。1. 这个项目解决什么问题装好传感器却对不准外参的痛如果你手里有一套 3D 激光雷达加 IMU 的传感器组合装上车后第一件麻烦事不是选什么算法而是激光雷达和 IMU 之间的外参到底是多少。手工量出来的结果往往差个几厘米建图直接重影跑定位变成玄学。这个 C 写的 lidar_imu_calib 项目就是专治这个问题的它把一段正常行驶中的点云和 IMU 数据扔进去自动算出激光雷达到 IMU 坐标系的旋转矩阵和平移向量全程不需要人工放置标定板也不需要高精度转台。机器人、自动驾驶、无人机姿态估计方向的开发者都能直接把它当成外参标定的离线工具先跑一遍。2. 技术栈与整体架构C、ROS、PCL、Eigen、Ceres 组成的标定管线2.1 项目定位与核心思路拆这个项目之前我一直以为激光雷达和 IMU 的外参标定是个纯粹的数学问题动手以后才发现更像个系统工程。lidar_imu_calib 采用离线批处理而不是在线实时估计。整条标定流程分成三个阶段数据预处理、特征提取、联合优化。核心思路是把外参当成优化变量用激光雷达点云在 IMU 积分位姿下的对齐误差作为代价。换句话说完成外参变换之后激光雷达每一帧点云应该落在 IMU 预测的连续轨迹上。这个目标函数可以同时约束旋转和平移比分开标定更稳。从数学形式上看外参是一个 4x4 齐次变换矩阵由 3x3 旋转和 3x1 平移组成。旋转部分项目用四元数保存因为四元数在优化时只有 4 个分量而且天然在单位球面上方便做归一化约束。平移部分就是三个浮点数。整个优化器最后输出的就是这两个参数块。常见做法是先用手眼标定 AXXB 初始化旋转再用点到线、点到面的残差精修平移。这个项目也走这条路。好处是旋转初始化不依赖平移估计旋转对了以后平移基本变成一堆线性方程的问题不容易把误差搅在一起。因为面向的是离线标定它还可以做更多数据清洗工作比如滤掉低反射强度点、投影深度图后再提特征这在在线标定里很难做到。2.2 目录结构每个文件夹在管线里的角色拿到源码后我先把目录结构梳理了一遍项目不大分得却相当清楚lidar_imu_calib/ ├── include/lidar_imu_calib/ │ ├── data_preprocess.h # 点云与IMU时间同步、下采样 │ ├── feature_extract.h # 提取边缘点与平面点 │ ├── optimizer.h # Ceres 优化器封装 │ └── utils.h # 四元数、旋转矩阵、文件IO工具 ├── src/ │ ├── data_preprocess.cpp │ ├── feature_extract.cpp │ ├── optimizer.cpp │ └── main.cpp # ROS 节点入口串联整条管线 ├── config/ │ ├── calib_config.yaml # 标定参数、阈值、收敛条件 │ └── sensor_param.yaml # 传感器分辨率、IMU噪声参数 ├── launch/ │ ├── calib.launch # 跑标定的 launch 文件 │ └── record.launch # 录制数据用的 launch 文件 ├── scripts/ │ ├── run_calib.sh # 一键标定脚本 │ └── verify_result.py # 标定结果验证脚本 └── CMakeLists.txt这个结构对二次开发非常友好。data_preprocess 只负责把数据变成干净的、带时间戳的流feature_extract 只负责从点云中抽出两类特征optimizer 只管残差和增量更新main.cpp 把这几步串起来。如果你想替换自己的预处理算法只要改 data_preprocess 的实现不需要动 optimizer。从模块边界上能看出作者是有意把它做成一个可替换管线的。标定这类代码最怕所有逻辑挤在一个 main 文件里改一个参数要全局搜一遍。拆成这样以后每个类都可以单独写单元测试我后来把预处理和优化分开测省了不少事。utils.h 里放着四元数与欧拉角互转的接口同时处理 TF 广播逻辑它不参与优化计算但你做可视化时会经常用到。2.3 关键依赖与版本选择C 版本要求 C11 或者 C14我拆的这份用的是 C14。ROS 方面melodic 和 noetic 都能编译前提是装好 PCL 和 Eigen。PCL 用于点云滤波和特征提取Eigen 处理矩阵运算Ceres Solver 负责非线性优化。我在这里踩过坑Ceres 和 ROS 自带的版本偶尔会冲突尤其是直接用 apt 装的 Ceres 太老时自动求导会出现莫名其妙的incomplete type错误。我一般从源码编译 Ceres 1.14 或更高版本安装到 /usr/local然后让 CMake 优先找它而不是 ROS 带的那一份。依赖清单大致如下依赖作用版本建议ROS消息通信、TF、launchmelodic / noeticEigen矩阵、四元数运算3.3PCL点云滤波、特征提取1.8Ceres Solver非线性优化1.14这里要特别说明为什么用 Ceres 而不是 g2o 或 GTSAM。这个项目的残差是点到线和点到面的距离属于典型的非线性最小二乘问题Ceres 的自动求导加上鲁棒核函数用起来最直接。固定规模优化用 Ceres滑窗后端才需要考虑 g2o不是越大的框架越合适。如果你不想依赖 ROSmain.cpp 里其实只用了时间戳和 TF可以把输入换成文本文件每行是一个点云的 PCD 路径加对应的 IMU 四元数这在单元测试里非常方便。但项目默认还是走 ROS因为激光雷达驱动和 IMU 采集在机器人系统里本来就以 topic 形式存在直接接进去成本最低。不熟悉 CMake 的话可以先把 include 和 src 都加进target_include_directories依赖再逐个补不建议跳过依赖检查。2.4 数据流与模块调用关系整条管线跑起来后的数据流是这样的rosbag 里的点云和 IMU 消息通过 main.cpp 读进来先进入 data_preprocess输出一个按时间排序的 IMU 位姿队列然后每帧点云去 feature_extract 提取特征最后 optimizer 用这些特征和位姿队列构造残差迭代更新外参。每次迭代后优化器会打印当前的旋转角和平移量方便在终端观察收敛情况。调用链可以简写为 main → DataPreprocess::sync → FeatureExtract::extract → Optimizer::solve。中间的订阅回调、缓存队列都封装在类内部不在外层暴露。这种设计让单元测试也好写我直接把预处理结果写成独立单元不用跑 ROS。参数块在模块之间传递顺序是固定的预处理只产生归一化之后的时间戳和点位姿流特征提取只产生特征索引和几何信息优化器只接收参数块和残差。做到这一点后你想把预处理换成多线程流水线也很安全只要接口不变其他模块完全不感知。3. 核心模块拆解从原始点云到外参矩阵的完整链路3.1 数据预处理时间同步与点云过滤第一阶段把原始传感器数据变成干净的输入。激光雷达一般 10HzIMU 可能有 100Hz 到 200Hz两者到达节点的时刻对不齐。项目用一个滑窗缓存每次来点云帧时从 IMU 缓存里取前后最近的若干帧做线性插值算出点云时间戳对应的 IMU 位姿。// data_preprocess.cpp —— 找出与点云时间最接近的IMU位姿 bool DataPreprocess::getInterpolatedPose( double t_pc, const ImuBuffer buffer, Eigen::Quaterniond q, Eigen::Vector3d t) { if (buffer.size() 2) return false; // 找到第一个时间戳大于 t_pc 的 IMU 帧索引 size_t i 0; while (i buffer.size() buffer[i].time t_pc) i; if (i 0 || i buffer.size()) return false; double t0 buffer[i - 1].time; double t1 buffer[i].time; double s (t_pc - t0) / (t1 - t0); // 旋转在四元数空间用球面线性插值位移直接线性插值 q buffer[i - 1].q.slerp(s, buffer[i].q); t buffer[i - 1].p s * (buffer[i].p - buffer[i - 1].p); return true; }这里参数说清楚t_pc是点云帧的 header.stampbuffer 保存最近一段时间 IMU 的旋转和位移。s是插值比例必须在 0 到 1 之间。四元数插值用slerp直接对四元数做线性归一化在旋转角大的时候会产生非线性误差处理大转角容易翻车。预处理还包括点云下采样。项目默认用 PCL 的 VoxelGrid叶子大小 0.1m。对于 64 线雷达可以开到 0.2m16 线雷达建议 0.05m。叶子大了特征会被抹掉小了优化太慢。这个参数在calib_config.yaml的voxel_leaf字段里调不用改代码。3.2 特征提取边缘点和平面点是怎么挑出来的特征提取的思路和 LOAM 一致计算每个点的局部曲率曲率大的作为边缘点曲率小的作为平面点。为什么要这样因为外参优化主要靠两个几何约束点到直线的距离和点到平面的距离。边缘点天然提供线约束平面点提供面约束两者单独优化容易互相耦合合在一起标定才稳定。// feature_extract.cpp 曲率计算与特征分类 void FeatureExtract::extract( const pcl::PointCloudpcl::PointXYZI::Ptr cloud, std::vectorint edge, std::vectorint plane) { edge.clear(); plane.clear(); int n cloud-size(); for (int i 5; i n - 6; i) { float diff 0.0f; const pcl::PointXYZI p cloud-points[i]; for (int j -5; j 6; j) { if (j 0) continue; const pcl::PointXYZI q cloud-points[i j]; diff (q.x - p.x) * (q.x - p.x) (q.y - p.y) * (q.y - p.y) (q.z - p.z) * (q.z - p.z); } float curvature diff / 10.0f; if (curvature edge_threshold_) edge.push_back(i); else if (curvature plane_threshold_) plane.push_back(i); } }索引从 5 开始、到 n-6 结束是为了留出至少 5 个邻域点参与计算。edge_threshold_和plane_threshold_是配置文件里的两个阈值默认分别是 0.7 和 0.1。你可以把曲率打印出来看看分布再调不要盲改。有一个细节edge 和 plane 是两个独立分支如果一个点同时满足两个条件它只会进 edge因为在找角点的时候混入平面点会污染线约束。特征提取前还要做一次体素滤波因为同一线束上的相邻点距离很近直接算曲率会受噪声影响。体素滤波之后相邻点距离更均匀曲率才稳定。3.3 旋转外参初始化手眼标定 AXXB非线性优化很怕初值给得差。项目先用几分钟数据做一次手眼标定拿到旋转初值。手眼标定的思想是激光雷达和 IMU 固定在一个刚体上刚体旋转时两边的旋转增量存在相似变换关系。记 IMU 从位姿 A 到 B 的旋转变换为 R_AB激光雷达从位姿 C 到 D 的旋转变换为 R_CD激光雷达到 IMU 的外参为 R_LC那么满足 R_AB * R_LC R_LC * R_CD。这是一个 AXXB 问题项目用四元数形式做线性最小二乘得到闭式解。得到旋转初值后平移初值由几组离散点云做简单 ICP 先估一个粗糙值。这个初始化能做到多准手眼标定对运动激励要求比较高旋转轴必须足够丰富。只绕一个轴转的话标定结果会退化成只标了部分角度。作者在文档里也强调采集数据时要绕 8 字型多转几个角度不要只做平移动。3.4 平移外参优化Ceres 代价函数与参数块初始化之后进入精修阶段。优化器有两个参数块一个四元数旋转一个三维平移。残差是激光雷达坐标系下的特征点在变换到 IMU 坐标系后与 IMU 轨迹上对应位置的几何距离。对于平面特征残差是点到平面的距离。核心结构长这样// optimizer.cpp 平面点到目标平面的残差 struct PlanarResidual { PlanarResidual(const Eigen::Vector3d point, const Eigen::Vector3d normal, const Eigen::Vector3d centroid) : point_(point), normal_(normal), centroid_(centroid) {} template typename T bool operator()(const T* const q, const T* const t, T* residual) const { T p[3]; // 先用外参旋转点再平移 ceres::QuaternionRotatePoint(q, point_.data(), p); p[0] t[0]; p[1] t[1]; p[2] t[2]; // 计算变换后的点到目标平面的有符号距离 residual[0] normal_(0) * (p[0] - centroid_(0)) normal_(1) * (p[1] - centroid_(1)) normal_(2) * (p[2] - centroid_(2)); return true; } };ceres::QuaternionRotatePoint是 Ceres 提供的四元数旋转函数它接收的点坐标是 double但在模板编译时会替换成 Jet 类型从而实现自动求导。t是平移参数块q是旋转参数块。残差是点到平面的距离优化目标为所有平面残差平方和最小。边缘点残差类似只是把目标点换成直线上的两个点计算点到线的距离。求解时用ceres::LocalParameterization把四元数强制归一化不然旋转参数超过四个自由度后优化会发散。平移参数不需要额外约束。参数块初始值从 3.3 节得到。迭代次数默认 50收敛阈值 1e-6。当平移变化量连续五次小于 0.001m 时 Ceres 会提前退出。输出结果保存在config/calib_result.yaml同时会发布一个静态 TF。4. 编译与运行从空白工作区到跑通标定4.1 编译环境准备建议用 Ubuntu 18.04 melodic 或者 Ubuntu 20.04 noetic。先把依赖装齐。PCL 在 ROS 桌面版里通常已经带了自己编译 Ceres 能避免版本冲突。顺序是装 Eigen、编译安装 Ceres、创建 catkin 工作区、把 lidar_imu_calib 放到 src 下、catkin_make。# 从源码编译 Ceres避免系统包太老 sudo apt-get install -y libeigen3-dev libgoogle-glog-dev libgflags-dev libatlas-base-dev git clone https://github.com/ceres-solver/ceres-solver.git cd ceres-solver mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease .. make -j$(nproc) sudo make install # 编译这个项目 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_init_workspace git clone 你实际拿到的源码包路径 cd ~/catkin_ws catkin_make source devel/setup.bash注意这里不要直接用 apt 的 libceres-dev因为仓库里的 Ceres 版本可能低于 1.13对模板化残差支持不完整。git 地址换成你实际拿到的源码路径如果是从 zip 解压出来的直接用绝对路径放到 src 下也行。编译过程中最常见的是找不到 Ceres 头文件需要在 CMakeLists.txt 里加set(CMAKE_PREFIX_PATH /usr/local)确保find_package(Ceres REQUIRED)能定位到你自己编译的那份。CMakeLists.txt 的关键项大致如下cmake_minimum_required(VERSION 3.0.2) project(lidar_imu_calib) find_package(catkin REQUIRED COMPONENTS roscpp sensor_msgs pcl_ros tf2_ros) find_package(PCL 1.8 REQUIRED) find_package(Eigen3 REQUIRED) find_package(Ceres REQUIRED) add_executable(lidar_imu_calib_node src/main.cpp ...) target_include_directories(lidar_imu_calib_node PRIVATE include) target_link_libraries(lidar_imu_calib_node ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${CERES_LIBRARIES})Eigen 是 header-only 的放在 include_directories 里即可不需要链接。Ceres 的路径如果不在系统默认搜索范围必须手动加CMAKE_PREFIX_PATH否则find_package找不到会报红。装完后可以用pcl-config --version和一条简单的 c 预处理命令确认 Eigen 版本避免用了过老的头文件接口。4.2 录制数据与配置文件说明项目标定需要一段 rosbag。录制时激光雷达和 IMU 消息必须同时采集建议车辆或机器人以 8 字型行驶 3 到 5 分钟速度控制在 1m/s 到 3m/s转弯方向要多元化。避免长时间直线因为纯平移对旋转标定没有激励。录制命令直接用 rosbag 即可rosbag record -O calib.bag /imu/data /lidar/points话题名要改成你驱动实际发布的 topic。用rosbag info确认两个话题都有足够的消息时间跨度覆盖全程。看一下每个 topic 的频率记录在案后面填sensor_param.yaml要用。接下来是配置文件。calib_config.yaml里控制标定流程的参数我都整理成了表参数默认值作用voxel_leaf0.1点云体素滤波叶子大小edge_threshold0.7曲率大于该值视为边缘点plane_threshold0.1曲率小于该值视为平面点max_iterations50优化最大迭代次数min_step_change1e-4平移更新量低于该值提前退出gravity_aligntrue是否先用重力对齐 IMU 坐标系sensor_param.yaml主要放 IMU 噪声密度、加速度计零偏、激光雷达线数和水平分辨率。这些参数会影响预处理中的 IMU 积分结果设错会让优化残差虚高。如果你没有 IMU 内参可以参考 datasheet 填或者先用 Allan 方差跑一组数据出来。# sensor_param.yaml 示例 imu: rate: 200.0 # IMU 频率 gyro_noise: 0.003 # 陀螺仪噪声密度 rad/s/sqrt(Hz) acc_noise: 0.05 # 加速度计噪声密度 m/s^2/sqrt(Hz) lidar: lines: 16 # 雷达线数 horizontal_res: 0.2 # 水平角度分辨率度 range_min: 0.5 range_max: 100.0rate偏差会影响插值位置的准确性。如果 IMU 实际是 100Hz 却填 200轨迹在时间轴上会被压缩一倍残差会明显变大。我用rosbag info核实了实际频率后才填的。4.3 执行标定并解读输出启动之前先把配置按传感器改成自己的。然后跑启动文件roslaunch lidar_imu_calib calib.launchlaunch 文件里会依次启动数据回放节点和标定节点默认是从 bag 离线读入。需要指定 bag 路径例如launch arg namebag_path default/path/to/calib.bag/ node pkgrosbag typeplay namerosbag_play args$(arg bag_path)/ node pkglidar_imu_calib typelidar_imu_calib_node namelidar_imu_calib_node outputscreen/ /launch标定节点会在所有消息回放完后进入优化流程。终端会打印每次迭代的残差、旋转欧拉角和平移向量。当看到残差从几百降到个位数并且连续多次下降小于 0.01说明收敛了。输出文件calib_result.yaml长这样rotation: w: 0.9999 x: 0.0123 y: -0.0045 z: 0.0008 translation: x: -0.401 y: 0.032 z: 0.108 tf_prefix: imu这是从激光雷达到 IMU 坐标系的变换。把点云通过这个变换转到 IMU 坐标系后应该和 IMU 轨迹重合。如果你需要反向变换把四元数取逆、平移按旋转逆后再取负即可。验证结果最简单的办法是用项目自带的verify_result.py跑一遍数据集把变换后的点云发布成 topic和未校正点云叠在一起看重影是否消失。脚本里统计的是重投影误差如果你看到误差中位数在 0.1m 以上先不要急着改可视化回去检查数据质量。4.4 常见参数调优经验再补一些调参手感。voxel_leaf设太大会把细小的角点特征抹掉优化结果会很平太小时特征提取要算满整帧点云CPU 占用直接拉满。我一般先设 0.1看特征点数量在每帧 300 到 1000 之间就差不多。IMU 噪声参数如果没标定可以把积分噪声放大一点让优化更相信点云几何约束。反过来如果点云本身很噪则减小 IMU 噪声权重让位姿轨迹更平滑。这个 trade-off 在全自动标定里很难绕开多试几组就有手感。还有一点如果雷达驱动发出的点云带运动畸变要记得在上游打开补偿否则特征点在边界处会弯曲残差永远是弯的标定结果偏移几十厘米也降不下来。运动补偿开关一般在驱动配置里不在这份源码里跑之前先确认。5. 避坑指南标定结果飘忽不定的五种典型原因跑通这个项目不难难的是跑通之后数据也录了、参数也改了结果每次标出来的还不一样。我拆这项目时把五种翻车原因都撞了一遍下面按现象、原因、解决写清楚。5.1 时间戳没对齐结果随机现象第一次标定旋转在 20 度左右第二次给我 5 度第三次又跳到 18 度用的是同一份 bag。原因激光雷达和 IMU 话题的时间戳没有统一来源。激光雷达驱动给的是点云完帧时间IMU 给的是各自的板载时钟两者没有做任何同步缓存插值就用上了错位的 IMU 数据。时间戳偏差只要几十毫秒外参结果就会乱跳这是全项目里最隐蔽的黑匣子。解决确保同一台机器上所有话题由同一个时钟驱动。没有硬件同步时先rosbag record录制再做软同步。缓存滑窗长度要能覆盖 IMU 帧间隔同时检查点云时间戳是不是 GPS 时间是的话要转成 Unix 时间。我后来在预处理里加了一个time_offset变量在插值前对 IMU 时间戳加一个常数偏移把时间对齐的代价函数也放进优化器结果才压稳。如果你只改这一个变量记得记录 bag 的起始时间和结束时间用rosbag info验证。5.2 运动激励不够优化退化现象标定结果只更新了平移旋转一直停在初始值残差曲线到最后是平的。原因运动激励不足。手眼标定需要绕多个旋转轴转动只围着 yaw 轴转x、y 方向的旋转约束几乎没有优化问题退化旋转被压在初始位置。这个问题在手持设备上不明显装在车上容易被忽略。解决采集数据时按 8 字型绕圈或者手拿着设备在三个轴方向各转几圈。回放数据后可以用脚本统计各轴角速度标准差如果最小那个轴的标准差小于 0.05 rad/s建议重新采集。旋转激励越丰富旋转外参的置信度才越高。同时把gravity_align打开让 IMU 先对齐重力方向能减少一个自由度的盲目搜索。5.3 IMU 噪声方差设错结果收敛到错误值现象优化残差下降很快但外参离手量结果差得很远换一段数据结果又变。原因sensor_param.yaml里的gyro_noise和acc_noise是占位默认值没做 Allan 方差分析。优化器里数据项误差和先验误差的权重由这两个参数决定设太大 IMU 轨迹被噪声迷惑设太小优化器几乎不听 IMU 约束。解决用 30 分钟以上的静止数据跑 Allan 方差把真实噪声密度填进去。没有条件的时候先把噪声放大十倍再用点云约束回拉观察结果比之前稳再收紧。这是仅次于时间戳的第二大坑。你还可以把优化器的代价函数打印到文件看残差按数据项还是先验项分布如果先验占大头说明噪声参数压得太死点云约束起不了作用。5.4 点云畸变不处理标定后地图重影现象标定后的平移看起来在合理范围但把点云拼到 IMU 轨迹后转弯处出现前后两帧交叉重影。原因旋转雷达一帧数据不是瞬时完成的从第一个到最后一个点之间有几十毫秒甚至上百毫秒时间差车在这期间已经走了几十厘米。外参标定默认所有点时间戳相同导致点云不同位置的点被投影到同一时刻的 IMU 位姿产生畸变。解决在雷达驱动或上游节点对点云做运动补偿用 IMU 积分把每帧点按各自相对时间插值到起始时间。项目在预处理里预留了comp_vel参数默认关闭。打开后需要 IMU 频率不低于 100Hz否则插值出来的姿态抖动反而更重。补偿公式很简单p_comp p_origin - T * (t_point - t_start) * v关键是你得有可靠的瞬时速度和姿态增量。5.5 初始外参太差掉进局部最优现象手动量了一个初值误差大约 5cm 和 2 度跑出来的结果和预期接近但就差一点无论怎么调迭代步长都回不来。原因手眼标定初始化在数据质量差时会失败项目有时会退回到读默认初值。默认旋转大多设为 0如果实际安装角接近 90 度Ceres 的雅可比在四元数 0 附近有奇异点优化直接卡在局部最优。解决先按机械设计图把初始旋转调到 10 度以内再交给项目精修。或者在配置里把init_rot_deg填上三个轴的粗略角度不要用默认零值。我的经验是旋转初值误差不超过 15 度、平移初值误差不超过 5cm 时这个项目基本都能收敛到差 0.01 度级别。如果还不放心可以在优化器里开启 Ceres 的 debug 输出观察每次迭代后的梯度范数梯度范数长时间不变就是陷在局部最优里了。6. 二次开发从固定标定到自适应参数调优6.1 换传感器的第一步改配置而不是改代码换传感器时先看接口是否能复用。激光雷达从 16 线换成 64 线主要改horizontal_res和range_max算法本身不变。IMU 换型号优先改噪声参数。只有时间同步和话题名变化才需要动 main.cpp 的订阅回调。6.2 给 Ceres 加一个 Huber 鲁棒核默认最小二乘里一个离群点会让残差爆炸。我在优化器里加了一个ceres::HuberLoss(0.5)把距离超过 0.5m 的残差当成异常值压住。改动只有两行ceres::Problem problem; ceres::LossFunction* loss new ceres::HuberLoss(0.5); problem.AddResidualBlock(cost_function, loss, rotation_, translation_);这样动态障碍物边缘的离群点不会拖垮外参。鲁棒核的阈值要和点云噪声匹配设太大起不到作用设太小会把正常的几何约束也压掉。6.3 把结果发布成静态 TF为了让定位模块直接使用标定结果我在优化结束之后加了一个静态 TF 发布函数tf2_ros::StaticTransformBroadcaster br; geometry_msgs::TransformStamped ts; ts.header.frame_id imu; ts.child_frame_id lidar; ts.transform.rotation result.rotation; ts.transform.translation result.translation; br.sendTransform(ts);注意 frame_id 是 imuchild_frame_id 是 lidar代表从 lidar 到 imu 的变换。别把两个坐标系写反否则定位模块拿到后感知会反向错一半。6.4 自检脚本每次标定完都强制跑一遍verify_result.py 会把标定结果加载进来统计重投影误差的中位数和 95% 分位如果中位数大于 0.1m 直接打印警告。从那以后我每次标完都强制走一遍自检脚本误差大了先回去查时间戳和运动激励而不是继续试参数。这套习惯救了我好几次希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →