1. 坐标系基础

做SLAM这么多年,我越来越觉得坐标系这东西,就像空气一样——平时感觉不到它的存在,但一旦出问题,整个系统就崩了。今天咱们就来聊聊刚体运动和旋转矩阵,这是整个坐标变换的基石。

1.1 刚体运动到底是什么?

说白了,刚体运动就是物体在空间里又转又移,但形状大小一点不变。你想想看,你手里的手机从桌上拿到耳边,这个过程就是刚体运动——手机没变形,只是位置和朝向变了。

在SLAM里,相机、机器人、路标点,我们都把它们当成刚体来处理。这样做的好处是:我们只需要关心6个自由度——3个平移 + 3个旋转。

核心要点:刚体运动 = 旋转 + 平移,两者互不干扰,可以分开处理。

1.2 旋转矩阵——我见过最多的坑

旋转矩阵是个3×3的方阵,它满足两个性质:

  • 正交性:RTR = I,也就是说矩阵的逆等于它的转置
  • 行列式为+1:保证了是纯旋转,没有镜像

我在项目中遇到过好几次,有人把旋转矩阵写错了,结果点云全飞到天上去。嗯,这里要注意:旋转矩阵的每一列,代表的是原坐标系的基向量在新坐标系下的表示。

1.3 绕坐标轴的旋转

最基本的三种旋转:

旋转轴 旋转矩阵(绕θ角)
X轴 Rx(θ) = [[1, 0, 0], [0, cosθ, -sinθ], [0, sinθ, cosθ]]
Y轴 Ry(θ) = [[cosθ, 0, sinθ], [0, 1, 0], [-sinθ, 0, cosθ]]
Z轴 Rz(θ) = [[cosθ, -sinθ, 0], [sinθ, cosθ, 0], [0, 0, 1]]

我个人习惯用右手定则来判断正方向——拇指指向轴的正方向,四指弯曲的方向就是正角度方向。这个习惯帮我避免了很多低级错误。

1.4 复合旋转与顺序问题

这里有个大坑:旋转不满足交换律。也就是说,先绕X转再绕Y转,跟先绕Y转再绕X转,结果完全不一样。

警告:复合旋转时,矩阵乘法顺序是从右往左读。比如 R = Rz · Ry · Rx,表示先绕X转,再绕Y转,最后绕Z转。

我曾经在标定外参时,把乘法顺序搞反了,结果标出来的外参误差大得离谱。排查了两天才发现是这个问题——从那以后,我每次写旋转矩阵乘法都会在注释里标清楚顺序。

1.5 旋转矩阵的代码实现

用Python和NumPy实现起来很简单:

import numpy as np

def rotation_x(theta):
    """绕X轴旋转矩阵"""
    c, s = np.cos(theta), np.sin(theta)
    return np.array([
        [1, 0, 0],
        [0, c, -s],
        [0, s, c]
    ])

def rotation_y(theta):
    """绕Y轴旋转矩阵"""
    c, s = np.cos(theta), np.sin(theta)
    return np.array([
        [c, 0, s],
        [0, 1, 0],
        [-s, 0, c]
    ])

def rotation_z(theta):
    """绕Z轴旋转矩阵"""
    c, s = np.cos(theta), np.sin(theta)
    return np.array([
        [c, -s, 0],
        [s, c, 0],
        [0, 0, 1]
    ])

# 复合旋转:先绕X转30°,再绕Z转45°
theta_x = np.radians(30)
theta_z = np.radians(45)
R = rotation_z(theta_z) @ rotation_x(theta_x)
print("复合旋转矩阵:\n", R)

小技巧:调试时可以用 R @ R.T 检查是否为单位矩阵,用 np.linalg.det(R) 检查行列式是否为1。这两个检查能帮你快速发现旋转矩阵的拼写错误。

1.6 刚体变换与齐次坐标

把旋转和平移合在一起,就得到了刚体变换:

p' = R · p + t

为了表示方便,我们引入齐次坐标,把3D点变成4D向量:

# 齐次变换矩阵
T = np.array([
    [R[0,0], R[0,1], R[0,2], t[0]],
    [R[1,0], R[1,1], R[1,2], t[1]],
    [R[2,0], R[2,1], R[2,2], t[2]],
    [0,     0,      0,      1    ]
])

# 点p的齐次坐标
p_homo = np.array([p[0], p[1], p[2], 1.0])

# 变换后的点
p_prime_homo = T @ p_homo
p_prime = p_prime_homo[:3] / p_prime_homo[3]

为什么用齐次坐标?说白了就是为了把旋转和平移统一成一个矩阵乘法,方便连续变换和求逆。这在SLAM后端优化里特别有用。

1.7 知识体系总览

下面这张图帮你理清本章的核心逻辑:

刚体运动与旋转矩阵知识体系 刚体运动 旋转 (3自由度) 平移 (3自由度) 旋转矩阵 R 绕X/Y/Z轴旋转 平移向量 t 齐次变换矩阵 T = [R | t; 0 | 1] 应用:坐标变换 · 外参标定 · 位姿估计

1.8 避坑指南

最后分享几个我踩过的坑:

  • 旋转矩阵的维度:我曾经在C++里把3×3矩阵当成4×4来用,结果内存越界,程序直接崩了。检查了半小时才发现。
  • 角度单位:NumPy的三角函数默认用弧度,但很多人习惯用角度。我建议在代码开头就统一用弧度,或者在函数名里标明单位。
  • 矩阵乘法顺序:R1 @ R2 和 R2 @ R1 结果完全不同。写代码时一定要在注释里写清楚旋转顺序。

我的习惯:每次写完旋转矩阵相关代码,都会用单位向量做一次测试——把(1,0,0)转一下,看看结果对不对。这个小测试帮我省了无数调试时间。

好了,这一章就到这里。旋转矩阵是坐标变换的基石,理解透了后面学四元数、李代数都会轻松很多。记住:多动手写代码,多画图验证,比死记公式管用一万倍。