[自动驾驶-传感器融合] 多激光雷达的外参标定

文章目录

  • 引言
  • 外参标定原理
  • ICP匹配示例
  • 参考文献

引言

多激光雷达系统通常用于自动驾驶或机器人,每个雷达的位置和姿态不同,需要将它们的数据统一到同一个坐标系下。多激光雷达外参标定的核心目标是通过计算不同雷达坐标系之间的刚性变换关系(旋转矩阵 R R R 和平移向量 t t t),将多个雷达的点云数据统一到同一坐标系下。具体需求包括:

  • 数据融合:消除多雷达间的位姿差异,生成全局一致的点云。
  • 减少累积误差:避免多传感器数据因坐标系不统一导致的定位与建图误差。
  • 提升感知精度:为自动驾驶或机器人提供更可靠的环境感知能力。

外参标定原理

外参标定本质是求解两个坐标系之间的最优变换参数,设雷达A的坐标系为源坐标系,雷达B为目标坐标系。对于同一物理点 P P P,其在两个坐标系下的坐标分别为 P A P_A PA P B P_B PB,满足:
P B = R ⋅ P A + t P_B = R \cdot P_A + t PB=RPA+t
其中 R ∈ S O ( 3 ) R \in SO(3) RSO(3) 为旋转矩阵, t ∈ R 3 t \in \mathbb{R}^3 tR3 为平移向量。

一般来说,多激光雷达的主流的外参标定方法有手动标定调参法、自动标定法、基于车辆运动轨迹的标定法。

  • 手动标定法:
    使用已知几何形状的标定物(如立方体、棋盘格),通过人工测量或标定物特征点计算外参。优点:精度高,适合实验室环境。 缺点:依赖标定物,效率低。
    基本步骤是:
    1. 将标定物放置在雷达共同视场内;
    2. 提取标定物的角点或平面;
    3. 基于最小二乘法求解 R R R t t t
  • 自动标定法:
    基本方式是利用环境中的稳定特征(如地面、建筑物边缘)自动对齐点云,优点在于无需标定物、适应性较强,但易受动态物体干扰,且要求场景特征丰富。
    多采用 ICP(Iterative Closest PointNDT(Normal Distributions Transform) 求解 R R R t t t
    ICP 通过迭代最近点匹配,最小化点对距离:
    min ⁡ R , t ∑ i = 1 N ∥ ( R ⋅ P A , i + t ) − P B , i ∥ 2 \min_{R,t} \sum_{i=1}^N \| (R \cdot P_{A,i} + t) - P_{B,i} \|^2 R,tmini=1N(RPA,i+t)PB,i2
    NDT(Normal Distributions Transform) 则将点云转换为概率密度函数,通过优化概率分布相似性求解变换。

激光雷达外参标定

  • 基于运动轨迹的标定:
    利用雷达在运动过程中采集的数据,通过里程计或SLAM生成轨迹约束。一般是联合优化多个雷达的外参和运动轨迹,再使用因子图优化(Factor Graph Optimization)等方式进行优化,求解雷达之间的运动轨迹所匹配的 R R R t t t
    这种方式适合动态环境却可在线标定,但计算复杂度高,需高精度里程计支持。

注:实际工程落地时,可不用追求高难度的优化算法,可根据具体场景选择合适的方式来进行标定

ICP匹配示例

在这篇文章中,我们可结合手动标定法与自动标定法联调方式来确保标定精度,简单操作是假设我们通过手动标定方式初步得到基准激光雷达与附属激光雷达的相对坐标变换参数 R 0 R_0 R0 t 0 t_0 t0 ,在此基础上采用 G I C P GICP GICP I C P ICP ICP等基础算法进行多次配准提高标定精度,得到最终的 R R R t t t

这里展示一个 I C P ICP ICP的使用示例:
按" 空格 "即可观察迭代匹配的效果。

#include <pcl/console/time.h>  
#include <pcl/io/ply_io.h>
#include <pcl/point_types.h>
#include <pcl/registration/icp.h>
#include <pcl/visualization/pcl_visualizer.h>#include <string>typedef pcl::PointXYZ PointT;
typedef pcl::PointCloud<PointT> PointCloudT;bool next_iteration = false;void print4x4Matrix(const Eigen::Matrix4d& matrix) {printf("Rotation matrix :\n");printf("    | %6.3f %6.3f %6.3f | \n", matrix(0, 0), matrix(0, 1),matrix(0, 2));printf("R = | %6.3f %6.3f %6.3f | \n", matrix(1, 0), matrix(1, 1),matrix(1, 2));printf("    | %6.3f %6.3f %6.3f | \n", matrix(2, 0), matrix(2, 1),matrix(2, 2));printf("Translation vector :\n");printf("t = < %6.3f, %6.3f, %6.3f >\n\n", matrix(0, 3), matrix(1, 3),matrix(2, 3));
}
/*** 此函数是查看器的回调。 当查看器窗口位于顶部时,只要按任意键,就会调用此函数。* 如果碰到“空格”; 将布尔值设置为true。* @param event* @param nothing*/
void keyboardEventOccurred(const pcl::visualization::KeyboardEvent& event,void* nothing) {if (event.getKeySym() == "space" && event.keyDown()) next_iteration = true;
}int main(int argc, char* argv[]) {// The point clouds we will be usingPointCloudT::Ptr cloud_in(new PointCloudT);   // Original point cloudPointCloudT::Ptr cloud_tr(new PointCloudT);   // Transformed point cloudPointCloudT::Ptr cloud_icp(new PointCloudT);  // ICP output point cloud//    我们检查程序的参数,设置初始ICP迭代的次数,然后尝试加载PLY文件。// Checking program argumentsif (argc < 2) {printf("Usage :\n");printf("\t\t%s file.ply number_of_ICP_iterations\n", argv[0]);PCL_ERROR("Provide one ply file.\n");return (-1);}int iterations = 1;  // Default number of ICP iterationsif (argc > 2) {// If the user passed the number of iteration as an argumentiterations = atoi(argv[2]);if (iterations < 1) {PCL_ERROR("Number of initial iterations must be >= 1\n");return (-1);}}pcl::console::TicToc time;time.tic();if (pcl::io::loadPLYFile(argv[1], *cloud_in) < 0) {PCL_ERROR("Error loading cloud %s.\n", argv[1]);return (-1);}std::cout << "\nLoaded file " << argv[1] << " (" << cloud_in->size()<< " points) in " << time.toc() << " ms\n"<< std::endl;// 我们使用刚性矩阵变换来变换原始点云。// cloud_in包含原始点云。// cloud_tr和cloud_icp包含平移/旋转的点云。// cloud_tr是我们将用于显示的备份(绿点云)。// Defining a rotation matrix and translation vectorEigen::Matrix4d transformation_matrix = Eigen::Matrix4d::Identity();// A rotation matrix (see https://en.wikipedia.org/wiki/Rotation_matrix)double theta = M_PI / 8;  // The angle of rotation in radianstransformation_matrix(0, 0) = std::cos(theta);transformation_matrix(0, 1) = -sin(theta);transformation_matrix(1, 0) = sin(theta);transformation_matrix(1, 1) = std::cos(theta);// A translation on Z axis (0.4 meters)transformation_matrix(2, 3) = 0.4;// Display in terminal the transformation matrixstd::cout << "Applying this rigid transformation to: cloud_in -> cloud_icp"<< std::endl;print4x4Matrix(transformation_matrix);// Executing the transformationpcl::transformPointCloud(*cloud_in, *cloud_icp, transformation_matrix);*cloud_tr = *cloud_icp;  // We backup cloud_icp into cloud_tr for later use// 这是ICP对象的创建。 我们设置ICP算法的参数。// setMaximumIterations(iterations)设置要执行的初始迭代次数(默认值为1)。// 然后,我们将点云转换为cloud_icp。// 第一次对齐后,我们将在下一次使用该ICP对象时(当用户按下“空格”时)将ICP最大迭代次数设置为1。// The Iterative Closest Point algorithmtime.tic();pcl::IterativeClosestPoint<PointT, PointT> icp;icp.setMaximumIterations(iterations);icp.setInputSource(cloud_icp);icp.setInputTarget(cloud_in);icp.align(*cloud_icp);icp.setMaximumIterations(1);  // We set this variable to 1 for the next time// we will call .align () functionstd::cout << "Applied " << iterations << " ICP iteration(s) in " << time.toc()<< " ms" << std::endl;// 检查ICP算法是否收敛; 否则退出程序。// 如果返回true,我们将转换矩阵存储在4x4矩阵中,然后打印刚性矩阵转换。if (icp.hasConverged()) {// std::cout << "\nICP has converged, score is " << icp.getFitnessScore()//           << std::endl;// std::cout << "\nICP transformation " << iterations//           << " : cloud_icp -> cloud_in" << std::endl;transformation_matrix = icp.getFinalTransformation().cast<double>();print4x4Matrix(transformation_matrix);} else {PCL_ERROR("\nICP has not converged.\n");return (-1);}// Visualizationpcl::visualization::PCLVisualizer viewer("ICP demo");// Create two vertically separated viewportsint v1(0);int v2(1);viewer.createViewPort(0.0, 0.0, 0.5, 1.0, v1);viewer.createViewPort(0.5, 0.0, 1.0, 1.0, v2);// The color we will be usingfloat bckgr_gray_level = 0.0;  // Blackfloat txt_gray_lvl = 1.0 - bckgr_gray_level;// Original point cloud is whitepcl::visualization::PointCloudColorHandlerCustom<PointT> cloud_in_color_h(cloud_in, (int)255 * txt_gray_lvl, (int)255 * txt_gray_lvl,(int)255 * txt_gray_lvl);viewer.addPointCloud(cloud_in, cloud_in_color_h, "cloud_in_v1", v1);viewer.addPointCloud(cloud_in, cloud_in_color_h, "cloud_in_v2", v2);// Transformed point cloud is greenpcl::visualization::PointCloudColorHandlerCustom<PointT> cloud_tr_color_h(cloud_tr, 20, 180, 20);viewer.addPointCloud(cloud_tr, cloud_tr_color_h, "cloud_tr_v1", v1);// ICP aligned point cloud is redpcl::visualization::PointCloudColorHandlerCustom<PointT> cloud_icp_color_h(cloud_icp, 180, 20, 20);viewer.addPointCloud(cloud_icp, cloud_icp_color_h, "cloud_icp_v2", v2);// Adding text descriptions in each viewportviewer.addText("White: Original point cloud\nGreen: Matrix transformed point cloud", 10,15, 16, txt_gray_lvl, txt_gray_lvl, txt_gray_lvl, "icp_info_1", v1);viewer.addText("White: Original point cloud\nRed: ICP aligned point cloud",10, 15, 16, txt_gray_lvl, txt_gray_lvl, txt_gray_lvl,"icp_info_2", v2);std::stringstream ss;ss << iterations;std::string iterations_cnt = "ICP iterations = " + ss.str();viewer.addText(iterations_cnt, 10, 60, 16, txt_gray_lvl, txt_gray_lvl,txt_gray_lvl, "iterations_cnt", v2);// Set background colorviewer.setBackgroundColor(bckgr_gray_level, bckgr_gray_level,bckgr_gray_level, v1);viewer.setBackgroundColor(bckgr_gray_level, bckgr_gray_level,bckgr_gray_level, v2);// Set camera position and orientationviewer.setCameraPosition(-3.68332, 2.94092, 5.71266, 0.289847, 0.921947,-0.256907, 0);viewer.setSize(1280, 1024);  // Visualiser window size// Register keyboard callback :viewer.registerKeyboardCallback(&keyboardEventOccurred, (void*)NULL);// Display the visualiserwhile (!viewer.wasStopped()) {viewer.spinOnce();// The user pressed "space" :if (next_iteration) {// The Iterative Closest Point algorithmtime.tic();// 如果用户按下键盘上的任意键,则会调用keyboardEventOccurred函数。// 此功能检查键是否为“空格”。// 如果是,则全局布尔值next_iteration设置为true,从而允许查看器循环输入代码的下一部分:调用ICP对象以进行对齐。// 记住,我们已经配置了该对象输入/输出云,并且之前通过setMaximumIterations将最大迭代次数设置为1。icp.align(*cloud_icp);std::cout << "Applied 1 ICP iteration in " << time.toc() << " ms"<< std::endl;// 和以前一样,我们检查ICP是否收敛,如果不收敛,则退出程序。if (icp.hasConverged()) {// printf(“ 033 [11A”);// 在终端增加11行以覆盖显示的最后一个矩阵是一个小技巧。// 简而言之,它允许替换文本而不是编写新行; 使输出更具可读性。// 我们增加迭代次数以更新可视化器中的文本值。printf("\033[11A");  // Go up 11 lines in terminal output.printf("\nICP has converged, score is %+.0e\n", icp.getFitnessScore());// 这意味着,如果您已经完成了10次迭代,则此函数返回矩阵以将点云从迭代10转换为11。std::cout << "\nICP transformation " << ++iterations<< " : cloud_icp -> cloud_in" << std::endl;// 函数getFinalTransformation()返回在迭代过程中完成的刚性矩阵转换(此处为1次迭代)。transformation_matrix *=icp.getFinalTransformation().cast<double>();  // WARNING /!\ This is not accurate! For// "educational" purpose only!print4x4Matrix(transformation_matrix);  // Print the transformation between// original pose and current posess.str("");ss << iterations;std::string iterations_cnt = "ICP iterations = " + ss.str();viewer.updateText(iterations_cnt, 10, 60, 16, txt_gray_lvl,txt_gray_lvl, txt_gray_lvl, "iterations_cnt");viewer.updatePointCloud(cloud_icp, cloud_icp_color_h, "cloud_icp_v2");} else {PCL_ERROR("\nICP has not converged.\n");return (-1);}//这不是我们想要的。//如果我们将最后一个矩阵与新矩阵相乘,那么结果就是从开始到当前迭代的转换矩阵。}next_iteration = false;}return (0);
}

ICP效果示例

参考文献

[1] 姜聿于——自动驾驶感知【激光雷达】:一、标定

[2] Segal A , Hhnel D , Thrun S .Generalized-ICP[J]. 2009.DOI:10.15607/RSS.2009.V.021.

[3] Kulmer D , Tahiraj I , Chumak A ,et al.Multi-LiCa: A Motion and Targetless Multi LiDAR-to-LiDAR Calibration Framework[J].IEEE, 2025.DOI:10.1109/MFI62651.2024.10705773.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/71387.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Blazor-路由模板(下)

路由约束 类型约束 我们这里使用{id:int}限制路由&#xff0c;id为int类型&#xff0c;并且路由参数 id 对应的 Id 属性也必须是 int 类型。我们试试能否正常访问 page "/demoPage/{id:int}" <h3>demoPage</h3> <h2>路由参数Id&#xff1a;Id&l…

多线程-JUC源码

简介 JUC的核心是AQS&#xff0c;大部分锁都是基于AQS扩展出来的&#xff0c;这里先结合可重入锁和AQS&#xff0c;做一个讲解&#xff0c;其它的锁的实现方式也几乎类似 ReentrantLock和AQS AQS的基本结构 AQS&#xff0c;AbstractQueuedSynchronizer&#xff0c;抽象队列…

通过多线程获取RV1126的AAC码流

目录 一RV1126多线程获取音频编码AAC码流的流程 1.1AI模块的初始化并使能 1.2AENC模块的初始化 ​​​​​​​1.3绑定AI模块和AENC模块 ​​​​​​​1.4多线程获取每一帧AAC码流 ​​​​​​​1.5每个AAC码流添加ADTSHeader头部 ​​​​​​​1.6写入具体每一帧AAC的…

JVM常用概念之对象初始化的成本

在JVM常用概念之新对象实例化博客中我讲到了对象的实例化&#xff0c;主要包含分配&#xff08;TLAB&#xff09;、系统初始化、用户初始化&#xff0c;而我在JVM常用概念之线程本地分配缓冲区&#xff08;ThreadLocal Allocation Buffer&#xff0c;TLAB&#xff09;博客中也讲…

java后端开发day27--常用API(二)正则表达式爬虫

&#xff08;以下内容全部来自上述课程&#xff09; 1.正则表达式&#xff08;regex&#xff09; 可以校验字符串是否满足一定的规则&#xff0c;并用来校验数据格式的合法性。 1.作用 校验字符串是否满足规则在一段文本中查找满足要求的内容 2.内容定义 ps&#xff1a;一…

AI---DevOps常备工具(‌AI-Integrated DevOps Essential Tools)

AI---DevOps常备工具 技术领域正在迅速发展&#xff0c;随着我们步入 2025 年&#xff0c;有一点是明确的&#xff1a;人工智能&#xff08;AI&#xff09;不再只是一个流行词&#xff0c;它是每个 DevOps 工程师都需要掌握的工具。随着云环境的复杂性增加、对更快部署的需求以…

Pytorch中的主要函数

目录 一、torch.manual_seed(seed)二、torch.cuda.manual_seed(seed)三、torch.rand(*size, outNone, dtypeNone, layouttorch.strided, deviceNone, requires_gradFalse)四、给大家写一个常用的自动选择电脑cuda 或者cpu 的小技巧五、torch.version.cuda&#xff1b;torch.bac…

Spring Boot中对接Twilio以实现发送验证码和验证短信码

Twilio介绍 Twilio是一家提供云通信服务的公司&#xff0c;旨在帮助开发者和企业通过简单的API实现各种通信功能。以下是Twilio的一些主要特点和服务介绍&#xff1a; 核心功能 短信服务&#xff08;SMS&#xff09;&#xff1a;允许用户通过API发送和接收短信&#xff0c;支…

VSCode详细安装步骤,适用于 Windows/macOS/Linux 系统

以下是 Visual Studio Code (VSCode) 的详细安装步骤&#xff0c;适用于 Windows/macOS/Linux 系统&#xff1a; VSCode 的详细安装步骤 一、Windows 系统安装1. 下载安装包2. 运行安装程序3. 验证安装 二、macOS 系统安装1. 方法一&#xff1a;官网下载安装包2. 方法二&#x…

基于PyTorch的深度学习3——基于autograd的反向传播

反向传播&#xff0c;可以理解为函数关系的反向传播。

设备管理系统功能与.NET+VUE(IVIEW)技术实现

在现代工业和商业环境中&#xff0c;设备管理系统&#xff08;Equipment Management System&#xff0c;简称EMS&#xff09;是确保设备高效运行和维护的关键工具。本文采用多租户设计的设备管理系统&#xff0c;基于.NET后端和VUE前端&#xff08;使用IVIEW UI框架&#xff09…

PHP之特性

在你有别的编程语言的基础下&#xff0c;你想学习PHP&#xff0c;可能要了解的PHP特有的东西。 定界符 使用<<<TT(可以是任意字符&#xff0c;但是不可以在别的地方使用过)和TT&#xff0c;会解析html格式和变量&#xff0c;如果在<<<后面加上单引号就会不…

9-Agent大模型中工作流的使用方法分析

目录 关键词 摘要 速览 配置插件进行新闻内容查找的工作流设置 自动化调用用户输入变量的插件配置教程 配置大模型以整理并简要输出新闻内容 新闻内容总结功能调试与优化 搭建与发布工作流优化布局的流程详解 创建和配置智能体工作流程 调试页面与工作流配置演示 思…

记一次:泛微OA集成Mybatis后 insert/update执行成功,但未真正插入或修改数据

背景&#xff1a;通过Mybatis插入数据或更新数据&#xff0c;显示插入/更新成功&#xff0c;查询数据库&#xff0c;发现并未插入成功、数据也没更新成功。下面是Mapper文件 public interface TestOrmMapper {int insertByTest(Param("requestId") Integer requestI…

使用 Spring Boot 实现前后端分离的海康威视 SDK 视频监控

使用 Spring Boot 实现前后端分离的海康威视 SDK 视频监控系统&#xff0c;可以分为以下几个步骤&#xff1a; 1. 系统架构设计 前端&#xff1a;使用 Vue.js、React 或 Angular 等前端框架实现用户界面。后端&#xff1a;使用 Spring Boot 提供 RESTful API&#xff0c;负责与…

【大模型系列篇】国产开源大模型DeepSeek-V3技术报告解析

DeepSeek-V3技术报告 目录 DeepSeek-V3技术报告 1. 摘要 2. 引言 3. DeepSeek V3 架构 3.1 基础架构 3.1.1. 多头潜在注意力 3.1.2. DeepSeekMoE和无辅助损失的负载均衡 3.2 多令牌预测 4. 基础设施 4.1 计算集群 4.2 训练框架 4.2.1. DualPipe算法与计算通信协同优…

负载均衡 - 一致性hash算法

构建场景 假如我们有三台缓存服务器编号node0、node1、node2&#xff0c;现在有3000万个key&#xff0c;希望可以将这些个key均匀的缓存到三台机器上&#xff0c;你会想到什么方案呢&#xff1f; 我们可能首先想到的方案&#xff0c;是取模算法hash&#xff08;key&#xff0…

pdfplumber 解析 PDF 表格的原理

&#x1f4cc; pdfplumber 解析 PDF 表格的原理 pdfplumber 处理表格的原理是基于几何分析&#xff08;geometric analysis&#xff09;&#xff0c;它通过分析 PDF 页面中的线条、单元格间距和文本分布&#xff0c;提取表格数据。它主要利用 垂直线&#xff08;vertical line…

洛谷P1334

题目如下 思路&#xff1a; 每次选择最短的两块木板进行合并&#xff0c;直到只剩下一块木板。使用最小堆&#xff08;优先队列&#xff09;来实现这一过程。使用最小堆&#xff1a; 将所有木板的长度放入最小堆&#xff08;优先队列&#xff09; 每次从堆中取出两块最短的木…

JVM(Java Virtual Machine,Java 虚拟机)的作用

JVM&#xff08;Java Virtual Machine&#xff0c;Java 虚拟机&#xff09;的作用至关重要&#xff0c;它是 Java 语言“一次编写&#xff0c;到处运行”&#xff08;Write Once, Run Anywhere&#xff0c;WORA&#xff09;特性的基石&#xff0c;也是 Java 平台的核心组成部分…