4. 点云坐标变换:刚体变换、旋转矩阵与四元数、欧拉角与变换矩阵

点云处理里,坐标变换可以说是最基础也最绕不开的一环。我刚开始做SLAM那会儿,就被旋转矩阵和四元数搞得晕头转向。说白了,你拿激光雷达扫了一堆点,这些点都是在传感器坐标系下的。但你要做建图、做定位,就得把它们转到世界坐标系下。这个「转」的过程,就是坐标变换。

今天咱们就把这块硬骨头啃下来。我会结合自己踩过的坑,把刚体变换、旋转矩阵、四元数、欧拉角这些概念串起来讲清楚。

4.1 刚体变换:点云对齐的核心

先问个问题:一个点云怎么从一个坐标系挪到另一个坐标系?

答案是刚体变换。它包含两部分:旋转平移。旋转解决「朝向」问题,平移解决「位置」问题。刚体变换有个重要性质——变换前后,点与点之间的距离不变。你想想看,一个杯子从桌上拿到地上,杯子的形状没变,只是位置和朝向变了。这就是刚体变换。

数学表达:

P' = R * P + t

其中 P 是原始点,P' 是变换后的点,R 是旋转矩阵,t 是平移向量。

我在项目中遇到过一个问题:两个不同时刻采集的点云,怎么拼到一起?其实就是求它们之间的刚体变换。这个在SLAM里叫「帧间匹配」,后面我们会详细讲。

4.2 旋转矩阵:最直观的旋转表示

旋转矩阵是个3x3的矩阵,它描述了一个坐标系相对于另一个坐标系的旋转关系。我个人的习惯是,先理解二维旋转,再推广到三维。

二维旋转很简单:

// 绕原点逆时针旋转θ角度
R(θ) = [cosθ  -sinθ]
       [sinθ   cosθ]

三维旋转就复杂一些。绕X、Y、Z轴分别旋转,对应的矩阵是:

旋转轴 旋转矩阵
绕X轴 [1, 0, 0; 0, cosθ, -sinθ; 0, sinθ, cosθ]
绕Y轴 [cosθ, 0, sinθ; 0, 1, 0; -sinθ, 0, cosθ]
绕Z轴 [cosθ, -sinθ, 0; sinθ, cosθ, 0; 0, 0, 1]

注意:旋转矩阵是正交矩阵,行列式为+1。我曾经在代码里手写旋转矩阵,结果行列式算出来是-1,点云直接镜像了,找了半天bug才发现是符号写反了。

4.3 四元数:避免万向锁的利器

旋转矩阵有9个参数,但自由度只有3。这就有冗余,而且容易产生数值问题。欧拉角虽然直观,但有个致命问题——万向锁。我记得第一次做无人机姿态估计时,俯仰角到了90度,偏航和滚转突然就耦合了,控制直接乱掉。

四元数就是为了解决这个问题而生的。它用4个参数表示旋转,没有奇异性,插值也平滑。

四元数的形式:

q = w + xi + yj + zk
其中 w^2 + x^2 + y^2 + z^2 = 1

用四元数旋转一个点P:

P' = q * P * q^(-1)

这里P要写成纯四元数形式 (0, x, y, z)。

我的建议:在SLAM系统里,内部计算尽量用四元数。只在可视化或用户交互时,才转成欧拉角。这样可以避免很多坑。

4.4 欧拉角与变换矩阵

欧拉角用三个角度表示旋转:偏航(Yaw)、俯仰(Pitch)、滚转(Roll)。不同的旋转顺序会得到不同的结果。常用的顺序是ZYX,也就是先绕Z轴转Yaw,再绕Y轴转Pitch,最后绕X轴转Roll。

从欧拉角到旋转矩阵的转换:

R = Rz(Yaw) * Ry(Pitch) * Rx(Roll)

而完整的变换矩阵是4x4的齐次矩阵:

T = [R  t]
    [0  1]

这样就把旋转和平移统一到一个矩阵里了。点云的齐次坐标是 (x, y, z, 1),变换起来非常方便:

P' = T * P

4.5 知识体系总览

下面这张图是我自己总结的坐标变换知识结构,帮你理清思路:

点云坐标变换知识体系 刚体变换 旋转矩阵 (3x3) 四元数 (w,x,y,z) 欧拉角 (Yaw,Pitch,Roll) 正交矩阵 行列式=1 9参数,有冗余 无万向锁 插值平滑 4参数,单位范数 直观易理解 存在万向锁 旋转顺序敏感 变换矩阵 (4x4) 旋转 + 平移 = 完整的刚体变换

4.6 实战:点云变换的代码实现

说了这么多理论,咱们直接上代码。下面是用Python实现点云刚体变换的示例:

import numpy as np

def transform_point_cloud(points, R, t):
    """
    对点云进行刚体变换
    points: Nx3 的点云数组
    R: 3x3 旋转矩阵
    t: 3x1 平移向量
    """
    # 方法1:直接计算
    transformed = (R @ points.T).T + t.reshape(1, 3)
    
    # 方法2:使用齐次坐标(推荐)
    # 构建4x4变换矩阵
    T = np.eye(4)
    T[:3, :3] = R
    T[:3, 3] = t.flatten()
    
    # 转成齐次坐标
    ones = np.ones((points.shape[0], 1))
    homo_points = np.hstack([points, ones])
    
    # 变换
    transformed_homo = (T @ homo_points.T).T
    transformed = transformed_homo[:, :3]
    
    return transformed

# 示例:绕Z轴旋转45度,平移(1,2,3)
theta = np.pi / 4
R = np.array([
    [np.cos(theta), -np.sin(theta), 0],
    [np.sin(theta),  np.cos(theta), 0],
    [0,              0,             1]
])
t = np.array([1, 2, 3])

# 生成测试点云
points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])

# 变换
transformed = transform_point_cloud(points, R, t)
print("原始点云:\n", points)
print("变换后点云:\n", transformed)

避坑指南:我曾经在写变换代码时,忘记把平移向量reshape成(1,3),结果广播机制搞出了奇怪的形状。建议始终用齐次坐标的方式,不容易出错。

4.7 不同表示方式的相互转换

实际开发中,经常需要在旋转矩阵、四元数、欧拉角之间来回转换。这里给出常用的转换公式:

转换方向 方法
旋转矩阵 → 四元数 从矩阵的迹和元素值推导,注意处理特殊情况
四元数 → 旋转矩阵 直接用四元数元素构造矩阵
欧拉角 → 旋转矩阵 按顺序连乘三个基本旋转矩阵
旋转矩阵 → 欧拉角 从矩阵元素反解角度,注意万向锁判断

我个人建议直接用现成的库,比如scipy.spatial.transform或者Eigen(C++)。手写转换容易出bug,尤其是处理边界情况的时候。

嗯,坐标变换这块内容就讲到这里。记住一点:理解物理意义比死记公式更重要。你只要想清楚「点要从哪个坐标系转到哪个坐标系」,剩下的就是套公式的事。


公众号:蓝海资料掘金营,微信deep3321