transform-base 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transform_base-0.0.2/MANIFEST.in +19 -0
- transform_base-0.0.2/PKG-INFO +60 -0
- transform_base-0.0.2/README.md +47 -0
- transform_base-0.0.2/pyproject.toml +36 -0
- transform_base-0.0.2/setup.cfg +4 -0
- transform_base-0.0.2/transform_base/__init__.py +30 -0
- transform_base-0.0.2/transform_base/transform.py +778 -0
- transform_base-0.0.2/transform_base.egg-info/PKG-INFO +60 -0
- transform_base-0.0.2/transform_base.egg-info/SOURCES.txt +10 -0
- transform_base-0.0.2/transform_base.egg-info/dependency_links.txt +1 -0
- transform_base-0.0.2/transform_base.egg-info/requires.txt +6 -0
- transform_base-0.0.2/transform_base.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
prune test*
|
|
3
|
+
prune tests*
|
|
4
|
+
prune docs*
|
|
5
|
+
prune examples*
|
|
6
|
+
prune input*
|
|
7
|
+
prune output*
|
|
8
|
+
prune log*
|
|
9
|
+
prune logs*
|
|
10
|
+
prune tmp*
|
|
11
|
+
prune temp*
|
|
12
|
+
prune .git
|
|
13
|
+
prune .pytest_cache
|
|
14
|
+
prune .mypy_cache
|
|
15
|
+
prune __pycache__
|
|
16
|
+
global-exclude *.log
|
|
17
|
+
global-exclude *.pyc
|
|
18
|
+
global-exclude *.pyo
|
|
19
|
+
global-exclude .DS_Store
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: transform-base
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: 欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy>=1.20
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
Requires-Dist: build; extra == "dev"
|
|
12
|
+
Requires-Dist: twine; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# transform-base
|
|
15
|
+
|
|
16
|
+
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX)。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install transform-base
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 功能特性
|
|
25
|
+
|
|
26
|
+
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
27
|
+
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
28
|
+
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
29
|
+
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
30
|
+
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
31
|
+
|
|
32
|
+
## 快速开始
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from transform_base import (
|
|
36
|
+
normalize_angle,
|
|
37
|
+
standard_euler_to_rotation_matrix,
|
|
38
|
+
rotation_matrix_to_standard_euler,
|
|
39
|
+
ExtrinsicParam,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
43
|
+
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
44
|
+
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
45
|
+
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
46
|
+
|
|
47
|
+
# 外参:欧拉角 + 平移
|
|
48
|
+
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
49
|
+
p_out = ext.transform_point([1, 0, 0])
|
|
50
|
+
ext_inv = ext.inverse()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 依赖
|
|
54
|
+
|
|
55
|
+
- Python >= 3.10
|
|
56
|
+
- numpy >= 1.20
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# transform-base
|
|
2
|
+
|
|
3
|
+
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX)。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install transform-base
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 功能特性
|
|
12
|
+
|
|
13
|
+
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
14
|
+
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
15
|
+
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
16
|
+
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
17
|
+
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from transform_base import (
|
|
23
|
+
normalize_angle,
|
|
24
|
+
standard_euler_to_rotation_matrix,
|
|
25
|
+
rotation_matrix_to_standard_euler,
|
|
26
|
+
ExtrinsicParam,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
30
|
+
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
31
|
+
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
32
|
+
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
33
|
+
|
|
34
|
+
# 外参:欧拉角 + 平移
|
|
35
|
+
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
36
|
+
p_out = ext.transform_point([1, 0, 0])
|
|
37
|
+
ext_inv = ext.inverse()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 依赖
|
|
41
|
+
|
|
42
|
+
- Python >= 3.10
|
|
43
|
+
- numpy >= 1.20
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "transform-base"
|
|
7
|
+
version = "0.0.2"
|
|
8
|
+
description = "欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy>=1.20",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = ["pytest", "build", "twine"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
exclude = [
|
|
21
|
+
"test*",
|
|
22
|
+
"tests*",
|
|
23
|
+
"docs*",
|
|
24
|
+
"examples*",
|
|
25
|
+
"input*",
|
|
26
|
+
"output*",
|
|
27
|
+
"log*",
|
|
28
|
+
"logs*",
|
|
29
|
+
"*.log",
|
|
30
|
+
"tmp*",
|
|
31
|
+
"temp*",
|
|
32
|
+
".pytest_cache*",
|
|
33
|
+
".mypy_cache*",
|
|
34
|
+
"__pycache__*",
|
|
35
|
+
"*.pyc",
|
|
36
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# transform_base: 欧拉角、旋转矩阵、四元数、角轴及外参变换基础库
|
|
2
|
+
from transform_base.transform import (
|
|
3
|
+
normalize_angle,
|
|
4
|
+
standard_euler_to_rotation_matrix,
|
|
5
|
+
rotation_matrix_to_standard_euler,
|
|
6
|
+
rotation_matrix_to_quaternion,
|
|
7
|
+
quaternion_to_rotation_matrix,
|
|
8
|
+
rotation_matrix_to_axis_angle,
|
|
9
|
+
axis_angle_to_rotation_matrix,
|
|
10
|
+
standard_euler_to_quaternion,
|
|
11
|
+
quaternion_to_standard_euler,
|
|
12
|
+
standard_euler_to_axis_angle,
|
|
13
|
+
axis_angle_to_standard_euler,
|
|
14
|
+
ExtrinsicParam,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"normalize_angle",
|
|
19
|
+
"standard_euler_to_rotation_matrix",
|
|
20
|
+
"rotation_matrix_to_standard_euler",
|
|
21
|
+
"rotation_matrix_to_quaternion",
|
|
22
|
+
"quaternion_to_rotation_matrix",
|
|
23
|
+
"rotation_matrix_to_axis_angle",
|
|
24
|
+
"axis_angle_to_rotation_matrix",
|
|
25
|
+
"standard_euler_to_quaternion",
|
|
26
|
+
"quaternion_to_standard_euler",
|
|
27
|
+
"standard_euler_to_axis_angle",
|
|
28
|
+
"axis_angle_to_standard_euler",
|
|
29
|
+
"ExtrinsicParam",
|
|
30
|
+
]
|
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Tuple, Union, Optional
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ==================== 角度归一化 ====================
|
|
7
|
+
|
|
8
|
+
def normalize_angle(angle: float) -> float:
|
|
9
|
+
"""
|
|
10
|
+
将角度归一化到[-180, 180]区间
|
|
11
|
+
|
|
12
|
+
参数:
|
|
13
|
+
angle: 输入角度,单位度
|
|
14
|
+
|
|
15
|
+
返回:
|
|
16
|
+
归一化后的角度,范围[-180, 180]
|
|
17
|
+
"""
|
|
18
|
+
while angle > 180:
|
|
19
|
+
angle -= 360
|
|
20
|
+
while angle < -180:
|
|
21
|
+
angle += 360
|
|
22
|
+
return angle
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ==================== Order参数辅助函数 ====================
|
|
26
|
+
|
|
27
|
+
def _normalize_order(order: str) -> str:
|
|
28
|
+
"""
|
|
29
|
+
规范化order参数(转大写,验证有效性,默认'ZYX')
|
|
30
|
+
|
|
31
|
+
参数:
|
|
32
|
+
order: 旋转顺序字符串,支持 'XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'(不区分大小写)
|
|
33
|
+
order表示从右往左读矩阵乘法的顺序,如ZYX表示 Rx @ Ry @ Rz
|
|
34
|
+
|
|
35
|
+
返回:
|
|
36
|
+
规范化后的order字符串(大写),不识别的输入默认返回'ZYX'
|
|
37
|
+
"""
|
|
38
|
+
if order is None:
|
|
39
|
+
return 'ZYX'
|
|
40
|
+
|
|
41
|
+
order_upper = order.upper()
|
|
42
|
+
valid_orders = ['XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX']
|
|
43
|
+
|
|
44
|
+
if order_upper not in valid_orders:
|
|
45
|
+
warnings.warn(f"无效的order值'{order}',默认使用'ZYX'。有效值为: {valid_orders}", UserWarning)
|
|
46
|
+
return 'ZYX'
|
|
47
|
+
|
|
48
|
+
return order_upper
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_basic_rotation_matrices(roll_rad: float, pitch_rad: float, yaw_rad: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
52
|
+
"""
|
|
53
|
+
获取基础旋转矩阵 Rx, Ry, Rz
|
|
54
|
+
|
|
55
|
+
参数:
|
|
56
|
+
roll_rad: 横滚角,单位弧度
|
|
57
|
+
pitch_rad: 俯仰角,单位弧度
|
|
58
|
+
yaw_rad: 航向角,单位弧度
|
|
59
|
+
|
|
60
|
+
返回:
|
|
61
|
+
(Rx, Ry, Rz): 三个基础旋转矩阵
|
|
62
|
+
"""
|
|
63
|
+
# 绕X轴旋转(roll)
|
|
64
|
+
Rx = np.array([
|
|
65
|
+
[1, 0, 0],
|
|
66
|
+
[0, np.cos(roll_rad), -np.sin(roll_rad)],
|
|
67
|
+
[0, np.sin(roll_rad), np.cos(roll_rad)]
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
# 绕Y轴旋转(pitch)
|
|
71
|
+
Ry = np.array([
|
|
72
|
+
[np.cos(pitch_rad), 0, np.sin(pitch_rad)],
|
|
73
|
+
[0, 1, 0],
|
|
74
|
+
[-np.sin(pitch_rad), 0, np.cos(pitch_rad)]
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
# 绕Z轴旋转(yaw)
|
|
78
|
+
Rz = np.array([
|
|
79
|
+
[np.cos(yaw_rad), -np.sin(yaw_rad), 0],
|
|
80
|
+
[np.sin(yaw_rad), np.cos(yaw_rad), 0],
|
|
81
|
+
[0, 0, 1]
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
return Rx, Ry, Rz
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _euler_to_rotation_matrix_by_order(roll: float, pitch: float, yaw: float, order: str) -> np.ndarray:
|
|
88
|
+
"""
|
|
89
|
+
根据order参数生成对应旋转顺序的旋转矩阵
|
|
90
|
+
|
|
91
|
+
order表示从右往左读矩阵乘法的顺序:
|
|
92
|
+
- 'ZYX': Rx @ Ry @ Rz (先绕Z轴,再绕Y轴,最后绕X轴) - 与v0.1.2相同
|
|
93
|
+
- 'XYZ': Rz @ Ry @ Rx (先绕X轴,再绕Y轴,最后绕Z轴)
|
|
94
|
+
|
|
95
|
+
参数:
|
|
96
|
+
roll: 横滚角,单位度
|
|
97
|
+
pitch: 俯仰角,单位度
|
|
98
|
+
yaw: 航向角,单位度
|
|
99
|
+
order: 旋转顺序,'XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'
|
|
100
|
+
|
|
101
|
+
返回:
|
|
102
|
+
旋转矩阵,shape (3, 3)
|
|
103
|
+
"""
|
|
104
|
+
order = _normalize_order(order)
|
|
105
|
+
|
|
106
|
+
roll_rad = np.deg2rad(roll)
|
|
107
|
+
pitch_rad = np.deg2rad(pitch)
|
|
108
|
+
yaw_rad = np.deg2rad(yaw)
|
|
109
|
+
|
|
110
|
+
Rx, Ry, Rz = _get_basic_rotation_matrices(roll_rad, pitch_rad, yaw_rad)
|
|
111
|
+
|
|
112
|
+
# 根据order组合旋转矩阵
|
|
113
|
+
# order表示从右往左读的顺序,如ZYX表示从右往左是Z-Y-X,即 Rx @ Ry @ Rz
|
|
114
|
+
rotation_map = {
|
|
115
|
+
'XYZ': Rz @ Ry @ Rx, # 从右往左读: X-Y-Z
|
|
116
|
+
'XZY': Ry @ Rz @ Rx, # 从右往左读: X-Z-Y
|
|
117
|
+
'YXZ': Rz @ Rx @ Ry, # 从右往左读: Y-X-Z
|
|
118
|
+
'YZX': Rx @ Rz @ Ry, # 从右往左读: Y-Z-X
|
|
119
|
+
'ZXY': Ry @ Rx @ Rz, # 从右往左读: Z-X-Y
|
|
120
|
+
'ZYX': Rx @ Ry @ Rz, # 从右往左读: Z-Y-X (与v0.1.2相同)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return rotation_map[order]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _rotation_matrix_to_euler_by_order(R: np.ndarray, order: str) -> Tuple[float, float, float]:
|
|
127
|
+
"""
|
|
128
|
+
根据order参数从旋转矩阵提取对应顺序的欧拉角
|
|
129
|
+
|
|
130
|
+
参数:
|
|
131
|
+
R: 旋转矩阵,shape (3, 3)
|
|
132
|
+
order: 旋转顺序,'XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'
|
|
133
|
+
|
|
134
|
+
返回:
|
|
135
|
+
(roll, pitch, yaw): 欧拉角三元组,单位度,范围[-180, 180]
|
|
136
|
+
"""
|
|
137
|
+
order = _normalize_order(order)
|
|
138
|
+
|
|
139
|
+
if order == 'ZYX':
|
|
140
|
+
# R = Rx @ Ry @ Rz (与v0.1.2完全相同的提取公式)
|
|
141
|
+
sy = np.sqrt(R[0, 0]**2 + R[0, 1]**2)
|
|
142
|
+
singular = sy < 1e-6
|
|
143
|
+
|
|
144
|
+
if not singular:
|
|
145
|
+
roll = np.arctan2(-R[1, 2], R[2, 2])
|
|
146
|
+
pitch = np.arctan2(R[0, 2], sy)
|
|
147
|
+
yaw = np.arctan2(-R[0, 1], R[0, 0])
|
|
148
|
+
else:
|
|
149
|
+
roll = np.arctan2(R[2, 1], R[1, 1])
|
|
150
|
+
pitch = np.arctan2(R[0, 2], sy)
|
|
151
|
+
yaw = 0
|
|
152
|
+
|
|
153
|
+
elif order == 'XYZ':
|
|
154
|
+
# R = Rz @ Ry @ Rx
|
|
155
|
+
sin_pitch = -R[2, 0]
|
|
156
|
+
cos_pitch = np.sqrt(R[0, 0]**2 + R[1, 0]**2)
|
|
157
|
+
|
|
158
|
+
if cos_pitch < 1e-6: # 万向锁
|
|
159
|
+
pitch = np.arctan2(sin_pitch, cos_pitch)
|
|
160
|
+
if sin_pitch > 0:
|
|
161
|
+
yaw = np.arctan2(-R[0, 1], R[1, 1])
|
|
162
|
+
roll = 0
|
|
163
|
+
else:
|
|
164
|
+
yaw = np.arctan2(R[0, 1], R[1, 1])
|
|
165
|
+
roll = 0
|
|
166
|
+
else:
|
|
167
|
+
pitch = np.arctan2(sin_pitch, cos_pitch)
|
|
168
|
+
roll = np.arctan2(R[2, 1], R[2, 2])
|
|
169
|
+
yaw = np.arctan2(R[1, 0], R[0, 0])
|
|
170
|
+
|
|
171
|
+
elif order == 'XZY':
|
|
172
|
+
# R = Ry @ Rz @ Rx
|
|
173
|
+
sin_yaw = R[1, 0]
|
|
174
|
+
cos_yaw = np.sqrt(R[0, 0]**2 + R[2, 0]**2)
|
|
175
|
+
|
|
176
|
+
if cos_yaw < 1e-6: # 万向锁
|
|
177
|
+
yaw = np.arctan2(sin_yaw, cos_yaw)
|
|
178
|
+
if sin_yaw > 0:
|
|
179
|
+
roll = np.arctan2(R[0, 2], R[0, 0])
|
|
180
|
+
pitch = 0
|
|
181
|
+
else:
|
|
182
|
+
roll = np.arctan2(-R[0, 2], R[0, 0])
|
|
183
|
+
pitch = 0
|
|
184
|
+
else:
|
|
185
|
+
yaw = np.arctan2(sin_yaw, cos_yaw)
|
|
186
|
+
roll = np.arctan2(-R[1, 2], R[1, 1])
|
|
187
|
+
pitch = np.arctan2(-R[2, 0], R[0, 0])
|
|
188
|
+
|
|
189
|
+
elif order == 'YXZ':
|
|
190
|
+
# R = Rz @ Rx @ Ry
|
|
191
|
+
sin_roll = R[2, 1]
|
|
192
|
+
cos_roll = np.sqrt(R[0, 1]**2 + R[1, 1]**2)
|
|
193
|
+
|
|
194
|
+
if cos_roll < 1e-6: # 万向锁
|
|
195
|
+
roll = np.arctan2(sin_roll, cos_roll)
|
|
196
|
+
if sin_roll > 0:
|
|
197
|
+
yaw = np.arctan2(R[0, 2], R[0, 0])
|
|
198
|
+
pitch = 0
|
|
199
|
+
else:
|
|
200
|
+
yaw = np.arctan2(-R[0, 2], R[0, 0])
|
|
201
|
+
pitch = 0
|
|
202
|
+
else:
|
|
203
|
+
roll = np.arctan2(sin_roll, cos_roll)
|
|
204
|
+
pitch = np.arctan2(-R[2, 0], R[2, 2])
|
|
205
|
+
yaw = np.arctan2(-R[0, 1], R[1, 1])
|
|
206
|
+
|
|
207
|
+
elif order == 'YZX':
|
|
208
|
+
# R = Rx @ Rz @ Ry
|
|
209
|
+
sin_yaw = -R[0, 1]
|
|
210
|
+
cos_yaw = np.sqrt(R[1, 1]**2 + R[2, 1]**2)
|
|
211
|
+
|
|
212
|
+
if cos_yaw < 1e-6: # 万向锁
|
|
213
|
+
yaw = np.arctan2(sin_yaw, cos_yaw)
|
|
214
|
+
if sin_yaw > 0:
|
|
215
|
+
pitch = np.arctan2(-R[2, 0], R[2, 2])
|
|
216
|
+
roll = 0
|
|
217
|
+
else:
|
|
218
|
+
pitch = np.arctan2(R[2, 0], R[2, 2])
|
|
219
|
+
roll = 0
|
|
220
|
+
else:
|
|
221
|
+
yaw = np.arctan2(sin_yaw, cos_yaw)
|
|
222
|
+
roll = np.arctan2(R[2, 1], R[1, 1])
|
|
223
|
+
pitch = np.arctan2(R[0, 2], R[0, 0])
|
|
224
|
+
|
|
225
|
+
elif order == 'ZXY':
|
|
226
|
+
# R = Ry @ Rx @ Rz
|
|
227
|
+
sin_roll = -R[1, 2]
|
|
228
|
+
cos_roll = np.sqrt(R[0, 2]**2 + R[2, 2]**2)
|
|
229
|
+
|
|
230
|
+
if cos_roll < 1e-6: # 万向锁
|
|
231
|
+
roll = np.arctan2(sin_roll, cos_roll)
|
|
232
|
+
if sin_roll > 0:
|
|
233
|
+
pitch = np.arctan2(-R[0, 1], R[0, 0])
|
|
234
|
+
yaw = 0
|
|
235
|
+
else:
|
|
236
|
+
pitch = np.arctan2(R[0, 1], R[0, 0])
|
|
237
|
+
yaw = 0
|
|
238
|
+
else:
|
|
239
|
+
roll = np.arctan2(sin_roll, cos_roll)
|
|
240
|
+
pitch = np.arctan2(R[0, 2], R[2, 2])
|
|
241
|
+
yaw = np.arctan2(R[1, 0], R[1, 1])
|
|
242
|
+
|
|
243
|
+
# 转换为度并归一化
|
|
244
|
+
roll_deg = normalize_angle(np.rad2deg(roll))
|
|
245
|
+
pitch_deg = normalize_angle(np.rad2deg(pitch))
|
|
246
|
+
yaw_deg = normalize_angle(np.rad2deg(yaw))
|
|
247
|
+
|
|
248
|
+
return roll_deg, pitch_deg, yaw_deg
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ==================== 标准欧拉角(支持order参数) ====================
|
|
252
|
+
|
|
253
|
+
def standard_euler_to_rotation_matrix(roll: float, pitch: float, yaw: float, order: str = 'ZYX') -> np.ndarray:
|
|
254
|
+
"""
|
|
255
|
+
标准欧拉角转旋转矩阵(支持多种旋转顺序)
|
|
256
|
+
|
|
257
|
+
默认order='ZYX':R = Rx @ Ry @ Rz (先绕Z轴,再绕Y轴,最后绕X轴)
|
|
258
|
+
|
|
259
|
+
参数:
|
|
260
|
+
roll: 横滚角,单位度
|
|
261
|
+
pitch: 俯仰角,单位度
|
|
262
|
+
yaw: 航向角,单位度
|
|
263
|
+
order: 旋转顺序,默认'ZYX',支持 'XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'(不区分大小写)
|
|
264
|
+
|
|
265
|
+
返回:
|
|
266
|
+
旋转矩阵,shape (3, 3)
|
|
267
|
+
"""
|
|
268
|
+
order = _normalize_order(order)
|
|
269
|
+
return _euler_to_rotation_matrix_by_order(roll, pitch, yaw, order)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def rotation_matrix_to_standard_euler(R: np.ndarray, order: str = 'ZYX') -> Tuple[float, float, float]:
|
|
273
|
+
"""
|
|
274
|
+
旋转矩阵转标准欧拉角(支持多种旋转顺序)
|
|
275
|
+
|
|
276
|
+
参数:
|
|
277
|
+
R: 旋转矩阵,shape (3, 3)
|
|
278
|
+
order: 旋转顺序,默认'ZYX',支持 'XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX'(不区分大小写)
|
|
279
|
+
|
|
280
|
+
返回:
|
|
281
|
+
(roll, pitch, yaw): 欧拉角三元组,单位度,范围[-180, 180]
|
|
282
|
+
"""
|
|
283
|
+
order = _normalize_order(order)
|
|
284
|
+
return _rotation_matrix_to_euler_by_order(R, order)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ==================== 旋转矩阵与四元数转换 ====================
|
|
288
|
+
|
|
289
|
+
def rotation_matrix_to_quaternion(R: np.ndarray) -> np.ndarray:
|
|
290
|
+
"""
|
|
291
|
+
将旋转矩阵转换为四元数 [x, y, z, w]
|
|
292
|
+
|
|
293
|
+
参数:
|
|
294
|
+
R: 旋转矩阵,shape (3, 3)
|
|
295
|
+
|
|
296
|
+
返回:
|
|
297
|
+
四元数 [x, y, z, w],shape (4,)
|
|
298
|
+
"""
|
|
299
|
+
trace = np.trace(R)
|
|
300
|
+
|
|
301
|
+
if trace > 0:
|
|
302
|
+
s = 0.5 / np.sqrt(trace + 1.0)
|
|
303
|
+
w = 0.25 / s
|
|
304
|
+
x = (R[2, 1] - R[1, 2]) * s
|
|
305
|
+
y = (R[0, 2] - R[2, 0]) * s
|
|
306
|
+
z = (R[1, 0] - R[0, 1]) * s
|
|
307
|
+
elif R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
|
|
308
|
+
s = 2.0 * np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2])
|
|
309
|
+
w = (R[2, 1] - R[1, 2]) / s
|
|
310
|
+
x = 0.25 * s
|
|
311
|
+
y = (R[0, 1] + R[1, 0]) / s
|
|
312
|
+
z = (R[0, 2] + R[2, 0]) / s
|
|
313
|
+
elif R[1, 1] > R[2, 2]:
|
|
314
|
+
s = 2.0 * np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2])
|
|
315
|
+
w = (R[0, 2] - R[2, 0]) / s
|
|
316
|
+
x = (R[0, 1] + R[1, 0]) / s
|
|
317
|
+
y = 0.25 * s
|
|
318
|
+
z = (R[1, 2] + R[2, 1]) / s
|
|
319
|
+
else:
|
|
320
|
+
s = 2.0 * np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1])
|
|
321
|
+
w = (R[1, 0] - R[0, 1]) / s
|
|
322
|
+
x = (R[0, 2] + R[2, 0]) / s
|
|
323
|
+
y = (R[1, 2] + R[2, 1]) / s
|
|
324
|
+
z = 0.25 * s
|
|
325
|
+
|
|
326
|
+
quaternion = np.array([x, y, z, w])
|
|
327
|
+
# 归一化
|
|
328
|
+
return quaternion / np.linalg.norm(quaternion)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def quaternion_to_rotation_matrix(q: Union[list, np.ndarray]) -> np.ndarray:
|
|
332
|
+
"""
|
|
333
|
+
将四元数转换为旋转矩阵
|
|
334
|
+
|
|
335
|
+
参数:
|
|
336
|
+
q: 四元数 [x, y, z, w],shape (4,)
|
|
337
|
+
|
|
338
|
+
返回:
|
|
339
|
+
旋转矩阵,shape (3, 3)
|
|
340
|
+
"""
|
|
341
|
+
q = np.array(q, dtype=float)
|
|
342
|
+
# 归一化
|
|
343
|
+
q = q / np.linalg.norm(q)
|
|
344
|
+
|
|
345
|
+
x, y, z, w = q
|
|
346
|
+
|
|
347
|
+
R = np.array([
|
|
348
|
+
[1 - 2*(y**2 + z**2), 2*(x*y - w*z), 2*(x*z + w*y)],
|
|
349
|
+
[2*(x*y + w*z), 1 - 2*(x**2 + z**2), 2*(y*z - w*x)],
|
|
350
|
+
[2*(x*z - w*y), 2*(y*z + w*x), 1 - 2*(x**2 + y**2)]
|
|
351
|
+
])
|
|
352
|
+
|
|
353
|
+
return R
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ==================== 旋转矩阵与角轴转换 ====================
|
|
357
|
+
|
|
358
|
+
def rotation_matrix_to_axis_angle(R: np.ndarray) -> Tuple[np.ndarray, float]:
|
|
359
|
+
"""
|
|
360
|
+
将旋转矩阵转换为角轴表示
|
|
361
|
+
|
|
362
|
+
参数:
|
|
363
|
+
R: 旋转矩阵,shape (3, 3)
|
|
364
|
+
|
|
365
|
+
返回:
|
|
366
|
+
axis: 旋转轴单位向量,shape (3,)
|
|
367
|
+
angle: 旋转角度,单位弧度
|
|
368
|
+
"""
|
|
369
|
+
# 计算旋转角度
|
|
370
|
+
trace = np.trace(R)
|
|
371
|
+
angle = np.arccos(np.clip((trace - 1) / 2, -1, 1))
|
|
372
|
+
|
|
373
|
+
# 处理特殊情况
|
|
374
|
+
if np.abs(angle) < 1e-10:
|
|
375
|
+
# 无旋转
|
|
376
|
+
return np.array([1.0, 0.0, 0.0]), 0.0
|
|
377
|
+
elif np.abs(angle - np.pi) < 1e-10:
|
|
378
|
+
# 180度旋转
|
|
379
|
+
# 找到对角线最大元素对应的轴
|
|
380
|
+
diag = np.diag(R)
|
|
381
|
+
k = np.argmax(diag)
|
|
382
|
+
axis = np.zeros(3)
|
|
383
|
+
axis[k] = np.sqrt((R[k, k] + 1) / 2)
|
|
384
|
+
if k == 0:
|
|
385
|
+
axis[1] = R[0, 1] / (2 * axis[0])
|
|
386
|
+
axis[2] = R[0, 2] / (2 * axis[0])
|
|
387
|
+
elif k == 1:
|
|
388
|
+
axis[0] = R[1, 0] / (2 * axis[1])
|
|
389
|
+
axis[2] = R[1, 2] / (2 * axis[1])
|
|
390
|
+
else:
|
|
391
|
+
axis[0] = R[2, 0] / (2 * axis[2])
|
|
392
|
+
axis[1] = R[2, 1] / (2 * axis[2])
|
|
393
|
+
return axis, angle
|
|
394
|
+
else:
|
|
395
|
+
# 一般情况
|
|
396
|
+
axis = np.array([
|
|
397
|
+
R[2, 1] - R[1, 2],
|
|
398
|
+
R[0, 2] - R[2, 0],
|
|
399
|
+
R[1, 0] - R[0, 1]
|
|
400
|
+
])
|
|
401
|
+
axis = axis / (2 * np.sin(angle))
|
|
402
|
+
return axis, angle
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def axis_angle_to_rotation_matrix(axis: np.ndarray, angle: float) -> np.ndarray:
|
|
406
|
+
"""
|
|
407
|
+
将角轴表示转换为旋转矩阵(Rodrigues公式)
|
|
408
|
+
|
|
409
|
+
参数:
|
|
410
|
+
axis: 旋转轴单位向量,shape (3,)
|
|
411
|
+
angle: 旋转角度,单位弧度
|
|
412
|
+
|
|
413
|
+
返回:
|
|
414
|
+
旋转矩阵,shape (3, 3)
|
|
415
|
+
"""
|
|
416
|
+
# 归一化旋转轴
|
|
417
|
+
axis = np.array(axis, dtype=float)
|
|
418
|
+
axis = axis / np.linalg.norm(axis)
|
|
419
|
+
|
|
420
|
+
# Rodrigues公式
|
|
421
|
+
K = np.array([
|
|
422
|
+
[0, -axis[2], axis[1]],
|
|
423
|
+
[axis[2], 0, -axis[0]],
|
|
424
|
+
[-axis[1], axis[0], 0]
|
|
425
|
+
])
|
|
426
|
+
|
|
427
|
+
R = np.eye(3) + np.sin(angle) * K + (1 - np.cos(angle)) * (K @ K)
|
|
428
|
+
return R
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ==================== 标准欧拉角转换函数(支持order) ====================
|
|
432
|
+
|
|
433
|
+
def standard_euler_to_quaternion(roll: float, pitch: float, yaw: float, order: str = 'ZYX') -> np.ndarray:
|
|
434
|
+
"""
|
|
435
|
+
标准欧拉角转四元数
|
|
436
|
+
|
|
437
|
+
参数:
|
|
438
|
+
roll: 横滚角,单位度
|
|
439
|
+
pitch: 俯仰角,单位度
|
|
440
|
+
yaw: 航向角,单位度
|
|
441
|
+
order: 旋转顺序,默认'ZYX'
|
|
442
|
+
|
|
443
|
+
返回:
|
|
444
|
+
四元数 [x, y, z, w],shape (4,)
|
|
445
|
+
"""
|
|
446
|
+
R = standard_euler_to_rotation_matrix(roll, pitch, yaw, order)
|
|
447
|
+
return rotation_matrix_to_quaternion(R)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def quaternion_to_standard_euler(q: Union[list, np.ndarray], order: str = 'ZYX') -> Tuple[float, float, float]:
|
|
451
|
+
"""
|
|
452
|
+
四元数转标准欧拉角
|
|
453
|
+
|
|
454
|
+
参数:
|
|
455
|
+
q: 四元数 [x, y, z, w],shape (4,)
|
|
456
|
+
order: 旋转顺序,默认'ZYX'
|
|
457
|
+
|
|
458
|
+
返回:
|
|
459
|
+
(roll, pitch, yaw): 欧拉角三元组,单位度,范围[-180, 180]
|
|
460
|
+
"""
|
|
461
|
+
R = quaternion_to_rotation_matrix(q)
|
|
462
|
+
return rotation_matrix_to_standard_euler(R, order)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def standard_euler_to_axis_angle(roll: float, pitch: float, yaw: float, order: str = 'ZYX') -> Tuple[np.ndarray, float]:
|
|
466
|
+
"""
|
|
467
|
+
标准欧拉角转角轴表示
|
|
468
|
+
|
|
469
|
+
参数:
|
|
470
|
+
roll: 横滚角,单位度
|
|
471
|
+
pitch: 俯仰角,单位度
|
|
472
|
+
yaw: 航向角,单位度
|
|
473
|
+
order: 旋转顺序,默认'ZYX'
|
|
474
|
+
|
|
475
|
+
返回:
|
|
476
|
+
axis: 旋转轴单位向量,shape (3,)
|
|
477
|
+
angle: 旋转角度,单位弧度
|
|
478
|
+
"""
|
|
479
|
+
R = standard_euler_to_rotation_matrix(roll, pitch, yaw, order)
|
|
480
|
+
return rotation_matrix_to_axis_angle(R)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def axis_angle_to_standard_euler(axis: np.ndarray, angle: float, order: str = 'ZYX') -> Tuple[float, float, float]:
|
|
484
|
+
"""
|
|
485
|
+
角轴表示转标准欧拉角
|
|
486
|
+
|
|
487
|
+
参数:
|
|
488
|
+
axis: 旋转轴单位向量,shape (3,)
|
|
489
|
+
angle: 旋转角度,单位弧度
|
|
490
|
+
order: 旋转顺序,默认'ZYX'
|
|
491
|
+
|
|
492
|
+
返回:
|
|
493
|
+
(roll, pitch, yaw): 欧拉角三元组,单位度,范围[-180, 180]
|
|
494
|
+
"""
|
|
495
|
+
R = axis_angle_to_rotation_matrix(axis, angle)
|
|
496
|
+
return rotation_matrix_to_standard_euler(R, order)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ==================== 外参类(支持order) ====================
|
|
500
|
+
|
|
501
|
+
class ExtrinsicParam:
|
|
502
|
+
"""
|
|
503
|
+
外参类,存储旋转矩阵和平移向量
|
|
504
|
+
|
|
505
|
+
初始化方式:
|
|
506
|
+
1. ExtrinsicParam(euler=[roll, pitch, yaw], translation=[x, y, z], euler_order='ZYX')
|
|
507
|
+
2. ExtrinsicParam(rotation_matrix=R, translation=[x, y, z], euler_order='ZYX')
|
|
508
|
+
3. ExtrinsicParam(axis_angle=(axis, angle), translation=[x, y, z], euler_order='ZYX')
|
|
509
|
+
4. ExtrinsicParam(quaternion=[x, y, z, w], translation=[x, y, z], euler_order='ZYX')
|
|
510
|
+
5. ExtrinsicParam.from_homogeneous_matrix(T, euler_order='ZYX') # 从4x4矩阵创建
|
|
511
|
+
|
|
512
|
+
属性:
|
|
513
|
+
rotation_matrix: 旋转矩阵,shape (3, 3)
|
|
514
|
+
translation: 平移向量 [x, y, z],单位米
|
|
515
|
+
euler_order: 欧拉角旋转顺序,默认'ZYX'
|
|
516
|
+
standard_euler: 标准欧拉角 [roll, pitch, yaw],单位度,范围[-180, 180]
|
|
517
|
+
axis: 角轴旋转轴,shape (3,)
|
|
518
|
+
angle: 角轴旋转角,单位弧度
|
|
519
|
+
quaternion: 四元数 [x, y, z, w],shape (4,)
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
def __init__(self,
|
|
523
|
+
euler: Optional[Union[list, np.ndarray]] = None,
|
|
524
|
+
translation: Optional[Union[list, np.ndarray]] = None,
|
|
525
|
+
rotation_matrix: Optional[np.ndarray] = None,
|
|
526
|
+
axis_angle: Optional[Tuple[np.ndarray, float]] = None,
|
|
527
|
+
quaternion: Optional[Union[list, np.ndarray]] = None,
|
|
528
|
+
euler_order: str = 'ZYX'):
|
|
529
|
+
"""
|
|
530
|
+
初始化外参
|
|
531
|
+
|
|
532
|
+
参数:
|
|
533
|
+
euler: 欧拉角 [roll, pitch, yaw],单位度
|
|
534
|
+
translation: 平移向量 [x, y, z],单位米
|
|
535
|
+
rotation_matrix: 旋转矩阵,shape (3, 3)
|
|
536
|
+
axis_angle: 角轴表示 (axis, angle),axis为单位向量,angle单位弧度
|
|
537
|
+
quaternion: 四元数 [x, y, z, w],shape (4,)
|
|
538
|
+
euler_order: 欧拉角旋转顺序,默认'ZYX'(与v0.1.2相同)
|
|
539
|
+
"""
|
|
540
|
+
# 存储欧拉角顺序
|
|
541
|
+
self.euler_order = _normalize_order(euler_order)
|
|
542
|
+
|
|
543
|
+
if euler is not None:
|
|
544
|
+
self.rotation_matrix = standard_euler_to_rotation_matrix(*euler, self.euler_order)
|
|
545
|
+
elif rotation_matrix is not None:
|
|
546
|
+
self.rotation_matrix = np.array(rotation_matrix, dtype=float)
|
|
547
|
+
elif axis_angle is not None:
|
|
548
|
+
axis, angle = axis_angle
|
|
549
|
+
self.rotation_matrix = axis_angle_to_rotation_matrix(axis, angle)
|
|
550
|
+
elif quaternion is not None:
|
|
551
|
+
self.rotation_matrix = quaternion_to_rotation_matrix(quaternion)
|
|
552
|
+
else:
|
|
553
|
+
self.rotation_matrix = np.eye(3)
|
|
554
|
+
|
|
555
|
+
# 计算标准欧拉角表示(使用存储的euler_order)
|
|
556
|
+
self.standard_euler = np.array(rotation_matrix_to_standard_euler(self.rotation_matrix, self.euler_order))
|
|
557
|
+
|
|
558
|
+
# 计算角轴表示
|
|
559
|
+
self.axis, self.angle = rotation_matrix_to_axis_angle(self.rotation_matrix)
|
|
560
|
+
|
|
561
|
+
# 计算四元数表示 [x, y, z, w]
|
|
562
|
+
self.quaternion = rotation_matrix_to_quaternion(self.rotation_matrix)
|
|
563
|
+
|
|
564
|
+
self.translation = np.array(translation, dtype=float) if translation is not None else np.zeros(3)
|
|
565
|
+
|
|
566
|
+
@classmethod
|
|
567
|
+
def from_homogeneous_matrix(cls, T: np.ndarray, euler_order: str = 'ZYX') -> 'ExtrinsicParam':
|
|
568
|
+
"""
|
|
569
|
+
从4x4齐次变换矩阵创建外参对象
|
|
570
|
+
|
|
571
|
+
参数:
|
|
572
|
+
T: 4x4齐次变换矩阵,格式为:
|
|
573
|
+
[[R11, R12, R13, tx],
|
|
574
|
+
[R21, R22, R23, ty],
|
|
575
|
+
[R31, R32, R33, tz],
|
|
576
|
+
[ 0, 0, 0, 1]]
|
|
577
|
+
euler_order: 欧拉角旋转顺序,默认'ZYX'
|
|
578
|
+
|
|
579
|
+
返回:
|
|
580
|
+
ExtrinsicParam对象
|
|
581
|
+
"""
|
|
582
|
+
T = np.array(T, dtype=float)
|
|
583
|
+
if T.shape != (4, 4):
|
|
584
|
+
raise ValueError(f"齐次矩阵必须是4x4矩阵,当前shape: {T.shape}")
|
|
585
|
+
|
|
586
|
+
# 验证最后一行是否为 [0, 0, 0, 1]
|
|
587
|
+
if not np.allclose(T[3, :], [0, 0, 0, 1]):
|
|
588
|
+
raise ValueError(f"齐次矩阵最后一行必须是[0, 0, 0, 1],当前为: {T[3, :]}")
|
|
589
|
+
|
|
590
|
+
rotation_matrix = T[:3, :3]
|
|
591
|
+
translation = T[:3, 3]
|
|
592
|
+
|
|
593
|
+
return cls(rotation_matrix=rotation_matrix, translation=translation, euler_order=euler_order)
|
|
594
|
+
|
|
595
|
+
def __matmul__(self, other: 'ExtrinsicParam') -> 'ExtrinsicParam':
|
|
596
|
+
"""
|
|
597
|
+
外参复合运算(@ 运算符,推荐使用)
|
|
598
|
+
|
|
599
|
+
齐次变换复合: T_result = T_left @ T_right
|
|
600
|
+
对应: R_result = R_left @ R_right
|
|
601
|
+
t_result = R_left @ t_right + t_left
|
|
602
|
+
|
|
603
|
+
参数:
|
|
604
|
+
other: 右侧外参
|
|
605
|
+
|
|
606
|
+
返回:
|
|
607
|
+
复合后的外参(使用左侧外参的euler_order)
|
|
608
|
+
|
|
609
|
+
示例:
|
|
610
|
+
>>> ext1 = ExtrinsicParam(euler=[10, 20, 30], translation=[1, 0, 0])
|
|
611
|
+
>>> ext2 = ExtrinsicParam(euler=[5, 10, 15], translation=[0, 1, 0])
|
|
612
|
+
>>> ext_combined = ext1 @ ext2
|
|
613
|
+
"""
|
|
614
|
+
R_combined = self.rotation_matrix @ other.rotation_matrix
|
|
615
|
+
t_combined = self.rotation_matrix @ other.translation + self.translation
|
|
616
|
+
return ExtrinsicParam(rotation_matrix=R_combined, translation=t_combined, euler_order=self.euler_order)
|
|
617
|
+
|
|
618
|
+
def inverse(self) -> 'ExtrinsicParam':
|
|
619
|
+
"""
|
|
620
|
+
计算外参的逆变换
|
|
621
|
+
|
|
622
|
+
对于齐次变换矩阵:
|
|
623
|
+
T = [ R t ]
|
|
624
|
+
[ 0 1 ]
|
|
625
|
+
|
|
626
|
+
其逆矩阵为:
|
|
627
|
+
T^-1 = [ R^T -R^T·t ]
|
|
628
|
+
[ 0 1 ]
|
|
629
|
+
|
|
630
|
+
返回:
|
|
631
|
+
逆变换的外参对象(保持相同的euler_order)
|
|
632
|
+
|
|
633
|
+
示例:
|
|
634
|
+
>>> ext = ExtrinsicParam(euler=[30, 45, 60], translation=[1, 2, 3])
|
|
635
|
+
>>> ext_inv = ext.inverse()
|
|
636
|
+
>>> result = ext @ ext_inv # 应该接近单位变换
|
|
637
|
+
"""
|
|
638
|
+
inv_rotation = self.rotation_matrix.T # R^T
|
|
639
|
+
inv_translation = -inv_rotation @ self.translation # -R^T·t
|
|
640
|
+
return ExtrinsicParam(rotation_matrix=inv_rotation, translation=inv_translation, euler_order=self.euler_order)
|
|
641
|
+
|
|
642
|
+
def transform_point(self, point: Union[list, np.ndarray]) -> np.ndarray:
|
|
643
|
+
"""
|
|
644
|
+
坐标前向变换: P_out = R * P_in + T
|
|
645
|
+
|
|
646
|
+
参数:
|
|
647
|
+
point: 输入点坐标 [x, y, z]
|
|
648
|
+
|
|
649
|
+
返回:
|
|
650
|
+
变换后的点坐标,shape (3,)
|
|
651
|
+
|
|
652
|
+
示例:
|
|
653
|
+
>>> ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
654
|
+
>>> point_in = [1, 0, 0]
|
|
655
|
+
>>> point_out = ext.transform_point(point_in)
|
|
656
|
+
"""
|
|
657
|
+
point = np.array(point, dtype=float)
|
|
658
|
+
return self.rotation_matrix @ point + self.translation
|
|
659
|
+
|
|
660
|
+
def inverse_transform_point(self, point: Union[list, np.ndarray]) -> np.ndarray:
|
|
661
|
+
"""
|
|
662
|
+
坐标逆变换: P_in = R^T * (P_out - T)
|
|
663
|
+
|
|
664
|
+
参数:
|
|
665
|
+
point: 输出点坐标 [x', y', z']
|
|
666
|
+
|
|
667
|
+
返回:
|
|
668
|
+
逆变换后的点坐标,shape (3,)
|
|
669
|
+
|
|
670
|
+
示例:
|
|
671
|
+
>>> ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
672
|
+
>>> point_out = [1, 1, 0]
|
|
673
|
+
>>> point_in = ext.inverse_transform_point(point_out)
|
|
674
|
+
"""
|
|
675
|
+
point = np.array(point, dtype=float)
|
|
676
|
+
return self.rotation_matrix.T @ (point - self.translation)
|
|
677
|
+
|
|
678
|
+
def get_homogeneous_matrix(self) -> np.ndarray:
|
|
679
|
+
"""
|
|
680
|
+
获取4x4齐次变换矩阵
|
|
681
|
+
|
|
682
|
+
返回:
|
|
683
|
+
4x4齐次矩阵:
|
|
684
|
+
[[R11, R12, R13, tx],
|
|
685
|
+
[R21, R22, R23, ty],
|
|
686
|
+
[R31, R32, R33, tz],
|
|
687
|
+
[ 0, 0, 0, 1]]
|
|
688
|
+
|
|
689
|
+
示例:
|
|
690
|
+
>>> ext = ExtrinsicParam(euler=[30, 45, 60], translation=[1, 2, 3])
|
|
691
|
+
>>> T = ext.get_homogeneous_matrix()
|
|
692
|
+
>>> print(T.shape) # (4, 4)
|
|
693
|
+
"""
|
|
694
|
+
T = np.eye(4)
|
|
695
|
+
T[:3, :3] = self.rotation_matrix
|
|
696
|
+
T[:3, 3] = self.translation
|
|
697
|
+
return T
|
|
698
|
+
|
|
699
|
+
def get_axis_angle(self) -> Tuple[np.ndarray, float]:
|
|
700
|
+
"""
|
|
701
|
+
获取角轴表示
|
|
702
|
+
|
|
703
|
+
返回:
|
|
704
|
+
axis: 旋转轴单位向量,shape (3,)
|
|
705
|
+
angle: 旋转角度,单位弧度
|
|
706
|
+
|
|
707
|
+
示例:
|
|
708
|
+
>>> ext = ExtrinsicParam(euler=[30, 0, 0], translation=[0, 0, 0])
|
|
709
|
+
>>> axis, angle = ext.get_axis_angle()
|
|
710
|
+
>>> print(f"旋转轴: {axis}, 旋转角: {np.rad2deg(angle)}度")
|
|
711
|
+
"""
|
|
712
|
+
return self.axis, self.angle
|
|
713
|
+
|
|
714
|
+
def get_axis_angle_degrees(self) -> Tuple[np.ndarray, float]:
|
|
715
|
+
"""
|
|
716
|
+
获取角轴表示(角度单位为度)
|
|
717
|
+
|
|
718
|
+
返回:
|
|
719
|
+
axis: 旋转轴单位向量,shape (3,)
|
|
720
|
+
angle: 旋转角度,单位度,范围[-180, 180]
|
|
721
|
+
|
|
722
|
+
示例:
|
|
723
|
+
>>> ext = ExtrinsicParam(euler=[30, 0, 0], translation=[0, 0, 0])
|
|
724
|
+
>>> axis, angle = ext.get_axis_angle_degrees()
|
|
725
|
+
>>> print(f"旋转轴: {axis}, 旋转角: {angle}度")
|
|
726
|
+
"""
|
|
727
|
+
angle_deg = np.rad2deg(self.angle)
|
|
728
|
+
angle_deg = normalize_angle(angle_deg)
|
|
729
|
+
return self.axis, angle_deg
|
|
730
|
+
|
|
731
|
+
def get_quaternion(self) -> np.ndarray:
|
|
732
|
+
"""
|
|
733
|
+
获取四元数表示 [x, y, z, w]
|
|
734
|
+
|
|
735
|
+
返回:
|
|
736
|
+
四元数 [x, y, z, w],shape (4,)
|
|
737
|
+
|
|
738
|
+
示例:
|
|
739
|
+
>>> ext = ExtrinsicParam(euler=[30, 45, 60], translation=[0, 0, 0])
|
|
740
|
+
>>> quat = ext.get_quaternion()
|
|
741
|
+
>>> print(f"四元数: {quat}")
|
|
742
|
+
"""
|
|
743
|
+
return self.quaternion
|
|
744
|
+
|
|
745
|
+
def get_standard_euler(self, order: Optional[str] = None) -> np.ndarray:
|
|
746
|
+
"""
|
|
747
|
+
获取标准欧拉角
|
|
748
|
+
|
|
749
|
+
参数:
|
|
750
|
+
order: 旋转顺序,如果为None则使用self.euler_order,否则使用指定的order
|
|
751
|
+
|
|
752
|
+
返回:
|
|
753
|
+
欧拉角 [roll, pitch, yaw],单位度,范围[-180, 180]
|
|
754
|
+
|
|
755
|
+
示例:
|
|
756
|
+
>>> ext = ExtrinsicParam(quaternion=[0, 0, 0, 1], translation=[0, 0, 0])
|
|
757
|
+
>>> euler = ext.get_standard_euler()
|
|
758
|
+
>>> print(f"标准欧拉角(RPY): {euler}")
|
|
759
|
+
>>> euler_xyz = ext.get_standard_euler(order='XYZ')
|
|
760
|
+
>>> print(f"标准欧拉角(XYZ顺序): {euler_xyz}")
|
|
761
|
+
"""
|
|
762
|
+
if order is None:
|
|
763
|
+
return self.standard_euler
|
|
764
|
+
else:
|
|
765
|
+
return np.array(rotation_matrix_to_standard_euler(self.rotation_matrix, order))
|
|
766
|
+
|
|
767
|
+
def __repr__(self) -> str:
|
|
768
|
+
"""
|
|
769
|
+
字符串表示
|
|
770
|
+
|
|
771
|
+
返回:
|
|
772
|
+
对象的字符串描述
|
|
773
|
+
"""
|
|
774
|
+
return (f"ExtrinsicParam(\n"
|
|
775
|
+
f" euler_order='{self.euler_order}',\n"
|
|
776
|
+
f" standard_euler(RPY)={self.standard_euler},\n"
|
|
777
|
+
f" translation={self.translation}\n"
|
|
778
|
+
f")")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: transform-base
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: 欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy>=1.20
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
Requires-Dist: build; extra == "dev"
|
|
12
|
+
Requires-Dist: twine; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# transform-base
|
|
15
|
+
|
|
16
|
+
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX)。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install transform-base
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 功能特性
|
|
25
|
+
|
|
26
|
+
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
27
|
+
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
28
|
+
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
29
|
+
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
30
|
+
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
31
|
+
|
|
32
|
+
## 快速开始
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from transform_base import (
|
|
36
|
+
normalize_angle,
|
|
37
|
+
standard_euler_to_rotation_matrix,
|
|
38
|
+
rotation_matrix_to_standard_euler,
|
|
39
|
+
ExtrinsicParam,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
43
|
+
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
44
|
+
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
45
|
+
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
46
|
+
|
|
47
|
+
# 外参:欧拉角 + 平移
|
|
48
|
+
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
49
|
+
p_out = ext.transform_point([1, 0, 0])
|
|
50
|
+
ext_inv = ext.inverse()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 依赖
|
|
54
|
+
|
|
55
|
+
- Python >= 3.10
|
|
56
|
+
- numpy >= 1.20
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
transform_base/__init__.py
|
|
5
|
+
transform_base/transform.py
|
|
6
|
+
transform_base.egg-info/PKG-INFO
|
|
7
|
+
transform_base.egg-info/SOURCES.txt
|
|
8
|
+
transform_base.egg-info/dependency_links.txt
|
|
9
|
+
transform_base.egg-info/requires.txt
|
|
10
|
+
transform_base.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|