
本文详解 euler 角与四元数双向转换中因旋转顺序大小写混淆(如 "zyx" vs "zyx")导致的不一致性问题,指出内在/外在旋转的本质差异,并提供可复现、鲁棒的 scipy 实现方案。
本文详解 euler 角与四元数双向转换中因旋转顺序大小写混淆(如 "zyx" vs "zyx")导致的不一致性问题,指出内在/外在旋转的本质差异,并提供可复现、鲁棒的 scipy 实现方案。
在三维姿态表示中,Euler 角与四元数之间的相互转换是常见操作,但极易因旋转约定不统一而产生看似“失真”的结果——例如输入 [45°, 30°, 60°](ZYX 顺序),却还原出 [4.4°, 52.1°, 45.2°]。问题核心并非数值精度或算法缺陷,而是 SciPy 中 from_euler() 和 as_euler() 的 order 参数对大小写敏感,且大小写代表完全不同的旋转语义:
-
"ZYX"(全大写):表示固连坐标系(intrinsic)旋转,即绕自身新轴依次旋转:Z → Y′ → X″;这是绝大多数工程场景(如航空、机器人)默认采用的 Tait-Bryan 角约定; -
"zyx"(全小写):表示固定坐标系(extrinsic)旋转,即绕原始静止坐标系的 Z → Y → X 轴旋转,等价于反序的 intrinsic 旋转(即"XYZ")。
在您的代码中:
scipy_quaternions_temp = R.from_euler("ZYX", ...) # ✅ intrinsic ZYX
scipy_euler_angles = ...as_euler('zyx', ...) # ❌ extrinsic zyx → 语义不匹配!这一大小写混用导致 as_euler('zyx') 实际求解的是满足 extrinsic zyx 等价的欧拉角,而非原 intrinsic ZYX 的逆映射,因此结果必然不一致。
✅ 正确做法:全程使用同一约定,推荐统一用大写 "ZYX"(intrinsic):
import numpy as np
from scipy.spatial.transform import Rotation as R
# 原始测试数据(度)
testrotation = np.array([
[[45, 30, 60], [10, 20, 30]],
[[90, 0, 45], [90, 45, 30]]
])
# Step 1: Euler (deg) → Quaternion (wxyz)
euler_rad = np.radians(testrotation)
quat_wxyz = R.from_euler("ZYX", euler_rad.reshape(-1, 3)).as_quat()
# 注意:SciPy 默认输出为 [x,y,z,w],若需 wxyz 格式,需 roll 或直接索引
quat_wxyz = quat_wxyz[:, [3, 0, 1, 2]] # 更清晰:显式重排为 [w,x,y,z]
quat_wxyz = quat_wxyz.reshape(testrotation.shape[0], testrotation.shape[1], 4)
# Step 2: Quaternion → Euler (deg),严格使用相同顺序 "ZYX"
euler_back_deg = np.degrees(
R.from_quat(quat_wxyz.reshape(-1, 4)).as_euler("ZYX", degrees=True)
).reshape(testrotation.shape)
# 验证一致性(应接近零误差)
print("Max absolute error (degrees):",
np.max(np.abs(testrotation - euler_back_deg)))
# 输出示例:Max absolute error (degrees): ~1e-12 → 数值稳定!
# Step 3: 可选 —— 再次转回四元数验证闭环
quat_roundtrip = R.from_euler("ZYX", np.radians(euler_back_deg.reshape(-1, 3))).as_quat()
quat_roundtrip = quat_roundtrip[:, [3, 0, 1, 2]].reshape(quat_wxyz.shape)
print("Quaternion round-trip MSE:",
np.mean((quat_wxyz - quat_roundtrip) ** 2))? 关键注意事项:
-
永远避免混用
"ZYX"与"zyx":它们数学上不互为逆运算; -
SciPy 的
as_quat()默认返回[x,y,z,w],若下游要求wxyz,请显式切片[:, [3,0,1,2]],而非依赖np.roll(易出错且可读性差); -
Euler 角存在奇点(万向锁):当中间角(如 Y 轴)接近 ±90° 时,
as_euler()可能返回非唯一解(如[90,0,45]与[0,90,-45]等价),此时数值微小扰动会导致角度跳变——这属于数学本质限制,非 bug; -
批量处理时确保 shape 一致:使用
reshape(-1, 3)/reshape(...)显式管理维度,避免广播错误。
通过统一旋转约定、明确四元数存储顺序、并理解内在/外在旋转的几何含义,即可彻底解决 Euler ↔ Quaternion 转换不一致问题,构建可靠的姿态处理流水线。

















