计算激光雷达与相机的外参 - 教程
一、概述
在自动驾驶、机器人导航和三维重建等领域,我们经常需要将激光雷达(LiDAR)采集的三维点云数据与相机拍摄的二维图像进行融合。这种多传感器融合能让我们同时获得环境的三维几何信息(来自激光雷达)和丰富的纹理颜色信息(来自相机),为后续的目标检测、地图构建等任务提供更全面的数据。
然而,这里存在一个根本问题:激光雷达和相机位于不同的位置,它们看到的是同一个世界的不同“视角”。激光雷达在自己的坐标系中记录每个点的三维位置(x, y, z),而相机在自己的坐标系中记录像素位置(u, v)。要将它们准确对齐,我们需要知道这两个坐标系之间的精确变换关系——这就是相机外参(Extrinsic Parameters),也称为雷达到相机的变换矩阵。
简单来说,相机外参回答了这个问题:“如果我知道激光雷达坐标系中的一个3D点,那么它在相机坐标系中的位置是什么?”通过这个变换矩阵,我们可以将激光雷达点投影到图像上,实现两种数据的精确叠加。
二、功能与原理
1、核心功能
本文介绍的方法能够通过手动标注少量对应点,计算激光雷达与相机之间的精确变换关系。整个过程包括:
- 从点云和图像中选取同一物体上的对应点
- 使用计算机视觉算法计算最优变换矩阵
- 验证计算结果并可视化
2、工作原理简述
想象一下这个场景:你在一张照片上看到一个路灯,同时也在激光雷达点云中看到了同一个路灯。如果你能在两个数据源中都识别出这个路灯的相同特征点(比如灯柱的顶部、底部等),那么这些点对就建立了激光雷达坐标系和图像坐标系之间的对应关系。
通过数学方法(如PnP算法),我们可以找到最佳变换矩阵,使得:
- 激光雷达中的3D点 → 通过变换 → 相机坐标系中的3D点 → 通过投影 → 图像上的2D点
- 这些投影后的2D点与实际图像上标注的2D点尽可能接近
这个“尽可能接近”的程度用重投影误差来衡量,误差越小,说明我们的外参计算越准确。
三、准备工作:收集和查看数据
1. 数据来源与结构
首先,我们需要一组配对的传感器数据:
- 激光雷达点云:
.pcd格式文件,记录了场景中每个点的三维坐标 - 相机图像:
.jpg格式文件,同一时刻拍摄的彩色图像 - 相机配置文件:
.json文件,包含相机内参和初始外参
这些数据通常来自自动驾驶数据集或标注平台(如Xtreme1)。文件组织如下:
数据集/
├── lidar_point_cloud_0/00000000.pcd
├── camera_image_front/00000000.jpg
└── camera_config/00000000.json
2. 理解相机配置文件
相机配置文件包含了标定的关键信息。让我们详细看一下各个参数的意义:
{
"camera_internal": {
"fx": 569.61, // 焦距在x方向(像素单位)
"fy": 576.66, // 焦距在y方向
"cx": 787.62, // 主点(光心)在x方向
"cy": 362.80 // 主点(光心)在y方向
},
"width": 1600, // 图像宽度(像素)
"height": 900, // 图像高度(像素)
"camera_external": [...] // 相机外参(4×4变换矩阵)
}
2.1、相机内参(Intrinsic Parameters)
- 焦距(fx, fy):决定了物体在图像中的大小。数值越大,相同的物体在图像中显得越大
- 主点(cx, cy):相机光轴与图像平面的交点,通常是图像中心
- 这些参数由相机制造和安装决定,一般不会变化
2.2、相机外参(Extrinsic Parameters)
这是一个4×4的变换矩阵,表示从世界坐标系(这里指激光雷达坐标系)到相机坐标系的刚体变换。它包含两个部分:
- 旋转部分(前3×3):描述坐标系的方向差异
- 平移部分(第4列前3行):描述坐标系原点的位置差异
3. 数据可视化与检查
在实际操作前,我们需要确认数据质量:
- 用CloudCompare查看点云:这个免费开源软件可以可视化
.pcd文件,让你看到三维空间中的点分布 - 用图片查看器预览图像:确认图像清晰,没有明显畸变
- 寻找共同区域:在点云和图像中找到相同的、特征明显的区域,如建筑物的角落、车辆的边缘等。这些区域将是我们选取对应点的目标
三、操作步骤:从数据到结果
步骤1:从点云拾取3D点
工具:CloudCompare
目标:在点云中选取至少4个特征明显的点,并记录它们的3D坐标
操作流程:
- 在CloudCompare中打开点云文件
- 使用"Point picking"工具在点云上点击
- 每个点击的点会被标记,其坐标会自动记录
- 导出所有选取的点到
picking_list.txt文件
为什么至少需要4个点?
从数学上讲,解算6自由度的变换(3个旋转+3个平移)至少需要3个点。但实际中由于测量误差,我们使用更多点(通常4-8个)来获得更稳定的解。


步骤2:从图像拾取对应的2D点
工具:pick_points.py(自定义Python工具)
目标:在图像上选取与步骤1中3D点对应的2D像素坐标
操作流程:
- 运行
pick_points.py,打开对应的图像 - 仔细对比点云和图像,找到相同的特征位置
- 在图像上点击对应位置,程序会自动记录像素坐标
- 坐标保存到
2d_points.txt文件

重要提示:
- 点的顺序必须一致!第一个3D点对应第一个2D点,以此类推
- 选择特征明显的点,如角点、边缘交点等
- 尽量让点分布在整个图像区域,而不是集中在局部
步骤3:运行主程序计算外参
工具:main.py(核心计算程序)
3.1 初始投影验证
程序首先使用配置文件中的原始外参,将你选取的3D点投影到图像上:

3.2 重新求解外参矩阵
这是整个流程的核心。程序使用PnP(Perspective-n-Point)算法,基于3D-2D点对关系求解最优变换矩阵:
PnP算法原理:
给定n个3D点(激光雷达坐标系)和对应的2D点(图像像素坐标),以及相机内参,PnP算法寻找旋转矩阵R和平移向量t,使得:
s [ u v 1 ] = K [ R t ] [ X Y Z 1 ] s \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = K \begin{bmatrix} R & t \end{bmatrix} \begin{bmatrix} X \\ Y \\ Z \\ 1 \end{bmatrix} suv1=K[Rt]XYZ1
其中:
- (u, v)是2D像素坐标
- (X, Y, Z)是3D点坐标
- K是相机内参矩阵
- s是尺度因子
程序提供了三种求解方法:
- PnP方法:使用OpenCV的
solvePnP函数,最常用 - SVD方法:基于奇异值分解的解析解
- PnP+BA方法:PnP后接Bundle Adjustment优化,精度最高
3.3 结果验证与可视化
计算完成后,程序会:
- 显示新计算的外参矩阵
- 与原始外参比较,计算差异
- 使用新外参重新投影3D点
- 计算重投影误差(衡量精度)

运行日志
3D点数量: 11, 2D点数量: 11
使用 pnp 方法求解...
重投影误差: 4.3088 像素
camera_external[0.008220822461211474, -0.1945781906022533, 0.9808525606940188, 0.0, -0.9982181674710671, 0.056376268754928405, 0.019550101073302795, 0.0, -0.05910083086358048, -0.9792655636053097, -0.19376802555635775, 0.0, -0.10691420765306425, -0.06395132918058577, -0.5668387212982573, 1.0]
估计的雷达到相机变换矩阵 (lidar2cam_rt):
[[ 0.00822082 -0.19457819 0.98085256 0. ]
[-0.99821817 0.05637627 0.0195501 0. ]
[-0.05910083 -0.97926556 -0.19376803 0. ]
[-0.10691421 -0.06395133 -0.56683872 1. ]]
原始变换矩阵 (来自配置文件):
[[ 0.00499474 -0.20121306 0.97953475 0. ]
[-0.9969654 0.07509539 0.02050949 0. ]
[-0.07768532 -0.9766647 -0.2002274 0. ]
[-0.09239691 0.05560648 -0.643189 1. ]]
旋转差异: 1.15 度
平移差异: 0.0000 米
验证估计的变换矩阵...
点0: 估计位置(820.1, 403.3), 真实位置(827.0, 405.0), 误差: 7.14像素
点1: 估计位置(824.8, 350.4), 真实位置(822.0, 348.0), 误差: 3.72像素
点2: 估计位置(803.7, 293.1), 真实位置(800.0, 294.0), 误差: 3.82像素
点3: 估计位置(1025.2, 341.7), 真实位置(1032.0, 344.0), 误差: 7.19像素
点4: 估计位置(687.3, 275.4), 真实位置(688.0, 274.0), 误差: 1.59像素
点5: 估计位置(579.9, 254.6), 真实位置(575.0, 256.0), 误差: 5.12像素
点6: 估计位置(591.9, 277.7), 真实位置(592.0, 279.0), 误差: 1.30像素
点7: 估计位置(530.2, 282.2), 真实位置(532.0, 282.0), 误差: 1.80像素
点8: 估计位置(527.7, 260.6), 真实位置(529.0, 260.0), 误差: 1.43像素
点9: 估计位置(434.7, 264.9), 真实位置(430.0, 262.0), 误差: 5.50像素
点10: 估计位置(401.5, 266.2), 真实位置(393.0, 264.0), 误差: 8.79像素
平均投影误差: 4.31像素
最大投影误差: 8.79像素
误差解读:
- 1-2像素:非常精确,满足大多数应用需求
- 3-5像素:可接受,但可能需要优化
- >5像素:需要检查数据质量或增加点数量
四、注意事项与常见问题
1. 数据质量是关键
- 点云质量:确保点云密度足够,噪声小
- 图像质量:避免过曝、过暗或运动模糊
- 时间同步:点云和图像应尽可能同时采集
2. 选点技巧
- 选择稳定特征:避免选择树叶、行人等易变物体
- 分布均匀:点在图像中应分散分布,而不是集中一处
- 避免共面:如果所有点都在同一平面上,PnP可能有多解
3. 精度提升方法
- 增加点数量:通常6-8个点效果最佳
- 使用棋盘格标定板:提供更精确的对应点
- 多次标定取平均:减少随机误差
- Bundle Adjustment优化:进一步优化结果
4. 特殊情况处理
- 相机畸变较大:需要在PnP中考虑畸变系数
- 点云与图像分辨率差异大:可能需要先对图像进行下采样
- 外参随时间变化:需要考虑温度、振动等因素的影响,定期重新标定
七、总结
通过本文介绍的方法,你可以精确计算激光雷达与相机之间的外参矩阵。这个过程虽然需要手动标注对应点,但提供了很高的灵活性和精度,特别适合:
- 新传感器安装后的初始标定
- 现有标定结果的验证和修正
- 研究环境中的快速原型开发
八、代码
1、pick_points.py
import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk
import os
class ImageViewer:
def __init__(self, root):
self.root = root
self.root.title("图片查看器 - 支持缩放和坐标保存")
# 图像相关变量
self.original_image = None
self.display_image = None
self.tk_image = None
self.image_id = None # 用于跟踪画布上的图像对象
# 缩放和位置变量
self.scale = 1.0
self.min_scale = 0.1
self.max_scale = 10.0
self.zoom_factor = 1.2
self.offset_x = 0
self.offset_y = 0
self.drag_start_x = 0
self.drag_start_y = 0
self.is_dragging = False
self.last_click_coords = [] # 存储所有点击的坐标
# 创建界面
self.create_widgets()
# 绑定事件
self.bind_events()
# 状态栏
self.status_var = tk.StringVar()
self.status_label = tk.Label(root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W)
self.status_label.pack(side=tk.BOTTOM, fill=tk.X)
self.update_status()
def create_widgets(self):
# 菜单栏
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="文件", menu=file_menu)
file_menu.add_command(label="打开图片", command=self.open_image)
file_menu.add_command(label="清除标记", command=self.clear_marks)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)
# 工具栏
toolbar = tk.Frame(self.root, bd=1, relief=tk.RAISED)
toolbar.pack(side=tk.TOP, fill=tk.X)
tk.Button(toolbar, text="打开", command=self.open_image).pack(side=tk.LEFT, padx=2, pady=2)
tk.Button(toolbar, text="重置", command=self.reset_view).pack(side=tk.LEFT, padx=2, pady=2)
tk.Button(toolbar, text="保存所有坐标", command=self.save_coordinates).pack(side=tk.LEFT, padx=2, pady=2)
tk.Button(toolbar, text="清除标记", command=self.clear_marks).pack(side=tk.LEFT, padx=2, pady=2)
# 显示区域
self.canvas = tk.Canvas

浙公网安备 33010602011771号