transforms3d库入门指南:Python三维空间变换全解析
这个库是干什么的?
简单来说,transforms3d解决的核心问题是:如何在代码中描述和组合三维空间中的旋转、平移、缩放操作。
比如你想让一个3D模型绕某个轴旋转45度,然后再平移一段距离,最后投影到相机平面上——这些操作都需要用到空间变换的数学工具。手动实现这些矩阵运算既容易出错,又不便于维护,transforms3d就是来帮你解决这个痛点的。
几个显著特点:
- 和numpy深度绑定,输入输出都是ndarray,无缝衔接科学计算生态
- 纯Python实现,安装简单,不依赖C扩展
- 覆盖了工程领域最常用的旋转表示方法及其相互转换
安装
pip install transforms3d
验证是否安装成功:
import transforms3d as t3d
print(t3d.__version__)
模块总览
打开transforms3d的源码目录,你会发现它是按变换类型来划分模块的:
| 子模块 | 负责什么 | 核心数据结构 |
|---|---|---|
axangles | 轴角表示法相关转换 | 旋转轴 + 旋转角度 |
euler | 欧拉角相关转换 | 三个旋转角 |
quaternions | 四元数相关运算 | 四元数(w, x, y, z) |
affines | 齐次变换矩阵的构建与拆解 | 4x4变换矩阵 |
utils | 辅助工具函数 | — |
理解这个模块划分很重要——当你需要做某种特定转换时,直接去对应模块找就行。
四种旋转表示法
三维旋转在数学上有好几种等效的表示方式,各有各的适用场景。transforms3d最大的价值就在于它把这些表示法之间的互相转换封装成了简单的一行函数调用。
旋转矩阵
旋转矩阵是最直观的一种表示——一个3x3的正交矩阵,乘以某个坐标就能得到旋转后的坐标。
把轴角转成旋转矩阵:
import numpy as np
from transforms3d.axangles import axangle2mat
# 绕Z轴旋转90度(π/2弧度)
R = axangle2mat([0, 0, 1], np.pi / 2)
print(R)
# 输出大概长这样:
# [[ 0. -1. 0.]
# [ 1. 0. 0.]
# [ 0. 0. 1.]]
反过来,从旋转矩阵提取轴和角度:
from transforms3d.axangles import mat2axangle
axis, angle = mat2axangle(R)
四元数
四元数用4个数字(w, x, y, z)来描述旋转,最大优势是不会出现万向节锁问题,而且在做旋转插值(平滑过渡)时非常方便。
from transforms3d.quaternions import mat2quat, quat2mat
# 旋转矩阵 → 四元数
q = mat2quat(R)
# 结果形如 [0.707, 0, 0, 0.707]
# 四元数 → 旋转矩阵
R_back = quat2mat(q)
两个旋转的组合可以通过四元数乘法实现:
from transforms3d.quaternions import quatmultiply
# 注意:四元数乘法不满足交换律,顺序很重要
q_combined = quatmultiply(q2, q1) # 先应用q1,再应用q2
欧拉角
欧拉角是最符合直觉的表示——分别绕三个坐标轴旋转一定角度,就像飞机的俯仰、偏航、滚转。
from transforms3d.euler import euler2mat, mat2euler
# 定义三个旋转角(弧度)
ai, aj, ak = np.pi/6, np.pi/4, np.pi/3
# 欧拉角 → 旋转矩阵
# axes参数控制旋转顺序和坐标系类型:
# 'sxyz' = 静坐标系,按X→Y→Z顺序旋转
# 'rzyx' = 旋转坐标系,按Z→Y→X顺序旋转
R = euler2mat(ai, aj, ak, axes='sxyz')
# 旋转矩阵 → 欧拉角
angles = mat2euler(R, axes='sxyz')
踩坑提醒:欧拉角有万向节锁问题——当某个旋转角达到90度时,会丢失一个自由度。如果应用场景中角度变化范围大,建议用四元数代替。
转换关系速查表
| 从 \ 往 | 轴角 | 旋转矩阵 | 四元数 | 欧拉角 |
|---|---|---|---|---|
| 轴角 | — | axangle2mat | 先转矩阵再转 | 先转矩阵再转 |
| 旋转矩阵 | mat2axangle | — | mat2quat | mat2euler |
| 四元数 | 先转矩阵再转 | quat2mat | — | 先转矩阵再转 |
| 欧拉角 | 先转矩阵再转 | euler2mat | 先转矩阵再转 | — |
核心思路:旋转矩阵可以作为所有表示法之间的"中转站"。
齐次变换矩阵(4x4)
在实际工程中,我们通常需要同时描述旋转和平移。齐次变换矩阵把这两者(以及缩放)打包到一个4x4矩阵中,这样多个变换就可以通过矩阵乘法来组合。
构建变换矩阵
from transforms3d.affines import compose
# 定义各分量
translation = [1.0, 2.0, 3.0] # 平移量
rotation = axangle2mat([0, 0, 1], np.pi/2) # 旋转(绕Z轴90度)
scaling = [2.0, 1.0, 1.0] # 缩放(X方向放大2倍)
# 合成一个4x4齐次变换矩阵
T = compose(translation, rotation, scaling)
拆解变换矩阵
拿到一个4x4矩阵后,可以反向提取出各个分量:
from transforms3d.affines import decompose44
t, r, z, s = decompose44(T)
# t = 平移向量
# r = 旋转矩阵(3x3)
# z = 缩放向量
# s = 剪切向量(通常为0)
应用变换到点集
from transforms3d.affines import apply_transform
# 定义一批3D点
points = np.array([
[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0],
[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]
])
# 对所有点应用同一个变换
new_points = apply_transform(points, T)
坐标系链:多级变换的组合
在机器人学中,经常需要处理多个坐标系之间的嵌套关系。比如"世界坐标系→基座坐标系→末端执行器坐标系",每一级都有自己的旋转和平移。
transforms3d本身没有提供专门的坐标系链管理类,但借助numpy的矩阵乘法,这件事变得很简单:
from transforms3d.affines import compose
from transforms3d.axangles import axangle2mat
# 世界坐标系 → 基座坐标系
T_world_base = compose(
translation=[0, 0, 0.5], # 基座安装高度0.5m
rotation=axangle2mat([0, 0, 1], 0), # 无旋转
scaling=[1, 1, 1]
)
# 基座坐标系 → 末端执行器
T_base_tool = compose(
translation=[0.3, 0, 0.2], # 末端位置
rotation=axangle2mat([0, 1, 0], np.pi/4), # 绕Y轴转45度
scaling=[1, 1, 1]
)
# 世界坐标系 → 末端执行器(矩阵左乘,顺序从右到左)
T_world_tool = T_base_tool @ T_world_base
关键点:矩阵乘法的顺序是从右到左应用的。
T_final = T2 @ T1表示先应用T1,再应用T2。搞反这个顺序是最常见的bug来源之一。
旋转插值:SLERP
在动画和轨迹规划场景中,经常需要在两个姿态之间做平滑过渡。四元数的球面线性插值(Spherical Linear Interpolation,简称SLERP)就是干这个的。
from transforms3d.quaternions import quat_slerp
# 起始姿态:无旋转
q_start = [1, 0, 0, 0]
# 结束姿态:绕Z轴转90度
q_end = [0.707, 0, 0, 0.707]
# 取中间时刻的插值
q_mid = quat_slerp(q_start, q_end, t=0.5)
# 生成整条轨迹(10个离散点)
import numpy as np
trajectory = [quat_slerp(q_start, q_end, t) for t in np.linspace(0, 1, 10)]
平移的插值就更简单了,直接用numpy的线性插值:
start = np.array([0, 0, 0])
end = np.array([1, 2, 3])
# t=0.5处的中间位置
mid = start + 0.5 * (end - start)
实战:二维机械臂正运动学
下面用一个简单的二连杆平面机械臂来演示transforms3d的实际用法:
import numpy as np
import transforms3d as t3d
def compute_end_effector(joint_angles, link_lengths):
"""
二连杆平面机械臂的正运动学求解
joint_angles: [theta1, theta2] 各关节角度(弧度)
link_lengths: [l1, l2] 各连杆长度
返回末端执行器的(x, y)坐标
"""
T = np.eye(4) # 4x4单位矩阵作为初始变换
for theta, length in zip(joint_angles, link_lengths):
# 每个关节贡献一个旋转+平移
R = t3d.axangles.axangle2mat([0, 0, 1], theta)
T_link = t3d.affines.compose([length, 0, 0], R, [1, 1, 1])
T = T @ T_link # 累积变换
return T[:3, 3] # 提取平移分量
# 计算示例:关节角45度和30度,连杆长1.0和0.8
pos = compute_end_effector([np.pi/4, np.pi/6], [1.0, 0.8])
print(f"末端位置: x={pos[0]:.3f}, y={pos[1]:.3f}")
实战:3D点投影到2D图像平面
相机标定中一个基本操作就是把世界坐标系中的3D点投影到图像像素坐标上:
def project_to_image(points_3d, intrinsic, extrinsic):
"""
3D世界坐标 → 2D图像像素坐标
intrinsic: 3x3 相机内参矩阵
extrinsic: 4x4 世界到相机的外参矩阵
"""
# 转齐次坐标
pts_h = np.hstack([points_3d, np.ones((len(points_3d), 1))])
# 世界坐标 → 相机坐标
pts_cam = (extrinsic @ pts_h.T).T
# 相机坐标 → 像素坐标
pts_img_h = (intrinsic @ pts_cam[:, :3].T).T
# 归一化得到像素坐标
pts_img = pts_img_h[:, :2] / pts_img_h[:, 2:3]
return pts_img
和numpy搭配使用的高级技巧
transforms3d的所有函数都返回numpy数组,这意味着你可以充分利用numpy的向量化能力来处理大规模数据。
批量转换多个旋转:
from transforms3d.quaternions import quat2mat
# 10000个随机四元数
quats = np.random.randn(10000, 4)
quats /= np.linalg.norm(quats, axis=1, keepdims=True) # 归一化
# 批量转旋转矩阵
all_matrices = [quat2mat(q) for q in quats]
不同旋转表示的选择建议:
| 场景 | 推荐表示 | 原因 |
|---|---|---|
| 做姿态插值/动画 | 四元数 | SLERP保证最短路径,无万向节锁 |
| 组合多个变换 | 旋转矩阵 | 矩阵乘法天然支持组合 |
| 接收用户输入/显示 | 欧拉角 | 直观,非专业人员也能理解 |
| 存储和传输 | 轴角 | 只需4个数,紧凑 |
| 数值计算/优化 | 旋转矩阵 | 与线性代数工具链无缝衔接 |
常见报错及解决办法
| 症状 | 根本原因 | 修复方法 |
|---|---|---|
| 旋转方向反了 | 左手系和右手系混用 | 确认统一使用右手坐标系 |
| 某些角度计算结果异常 | 万向节锁 | 换用四元数表示 |
| 变换结果和预期差很远 | 矩阵乘法顺序搞反 | 记住"从右到左"原则:T = T2 @ T1 |
| 多次变换后结果漂移 | 浮点误差累积 | 定期对旋转矩阵做正交化处理 |
总结
transforms3d虽然功能专一,但在三维几何变换这个垂直领域做得非常扎实。掌握它的核心就是理解四种旋转表示法以及它们之间的转换关系,然后在实际项目中选择最适合当前场景的表示方式。配合numpy的向量化能力,处理大规模空间变换数据也非常高效。
更多推荐
所有评论(0)