akai-transform 0.0.1__py3-none-any.whl
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.
- akai_transform/__init__.py +31 -0
- akai_transform/const.py +51 -0
- akai_transform/log_quat.py +84 -0
- akai_transform/quat.py +447 -0
- akai_transform/se3.py +260 -0
- akai_transform/tf2d.py +170 -0
- akai_transform/tf3d.py +742 -0
- akai_transform-0.0.1.dist-info/METADATA +127 -0
- akai_transform-0.0.1.dist-info/RECORD +15 -0
- akai_transform-0.0.1.dist-info/WHEEL +5 -0
- akai_transform-0.0.1.dist-info/top_level.txt +2 -0
- image/akai_3d_big.jpg +0 -0
- image/akai_wechat_qrcode.png +0 -0
- image//346/226/207/346/241/243/346/210/252/345/233/276.png +0 -0
- image//351/230/277/345/207/257/347/251/272/351/227/264/345/217/230/346/215/242/345/272/223ICON-V2.png +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'''
|
|
2
|
+
akai_transform — 空间变换工具库
|
|
3
|
+
----------------------------------------------------------------
|
|
4
|
+
提供 2D/3D 空间变换、四元数运算、SE(3) 李群/李代数、对数四元数等功能。
|
|
5
|
+
|
|
6
|
+
子模块:
|
|
7
|
+
- tf2d: 2D 空间变换(3×3 齐次变换矩阵)
|
|
8
|
+
- tf3d: 3D 空间变换(4×4 齐次变换矩阵)
|
|
9
|
+
- quat: 四元数运算([w, x, y, z] 标量在前)
|
|
10
|
+
- se3: SE(3) 李群/李代数(指数/对数映射)
|
|
11
|
+
- log_quat: 对数四元数(四元数 ↔ R³ 旋转向量)
|
|
12
|
+
- const: 常量和枚举定义
|
|
13
|
+
|
|
14
|
+
作者: 阿凯爱玩机器人 | 微信: xingshunkai | QQ: 244561792
|
|
15
|
+
官网: deepsenserobot.com
|
|
16
|
+
B站: https://space.bilibili.com/40344504
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
from . import const
|
|
20
|
+
from . import tf2d
|
|
21
|
+
from . import tf3d
|
|
22
|
+
from . import quat
|
|
23
|
+
from . import se3
|
|
24
|
+
from . import log_quat
|
|
25
|
+
|
|
26
|
+
# 常量便捷导出
|
|
27
|
+
from .const import (
|
|
28
|
+
PI, DEG2RAD, RAD2DEG, PI_2, PI_D_2,
|
|
29
|
+
TranslationUnit, AngleUnit,
|
|
30
|
+
MM, M, DEG, RAD,
|
|
31
|
+
)
|
akai_transform/const.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'''
|
|
2
|
+
常量与枚举定义
|
|
3
|
+
----------------------------------------------------------------
|
|
4
|
+
提供空间变换库所需的数学常量和单位枚举。
|
|
5
|
+
|
|
6
|
+
作者: 阿凯爱玩机器人 | 微信: xingshunkai | QQ: 244561792
|
|
7
|
+
官网: deepsenserobot.com
|
|
8
|
+
B站: https://space.bilibili.com/40344504
|
|
9
|
+
'''
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
from enum import IntEnum
|
|
13
|
+
|
|
14
|
+
# ============================================================
|
|
15
|
+
# 数学常量
|
|
16
|
+
# ============================================================
|
|
17
|
+
|
|
18
|
+
PI = math.pi
|
|
19
|
+
'''圆周率'''
|
|
20
|
+
|
|
21
|
+
DEG2RAD = math.pi / 180.0
|
|
22
|
+
'''角度转弧度系数'''
|
|
23
|
+
|
|
24
|
+
RAD2DEG = 180.0 / math.pi
|
|
25
|
+
'''弧度转角度系数'''
|
|
26
|
+
|
|
27
|
+
PI_2 = math.pi * 2.0
|
|
28
|
+
'''两倍圆周率 (2π)'''
|
|
29
|
+
|
|
30
|
+
PI_D_2 = math.pi / 2.0
|
|
31
|
+
'''圆周率的一半 (π/2)'''
|
|
32
|
+
|
|
33
|
+
# ============================================================
|
|
34
|
+
# 单位枚举
|
|
35
|
+
# ============================================================
|
|
36
|
+
|
|
37
|
+
class TranslationUnit(IntEnum):
|
|
38
|
+
'''平移量单位枚举'''
|
|
39
|
+
MM = 0 # 毫米
|
|
40
|
+
M = 1 # 米
|
|
41
|
+
|
|
42
|
+
class AngleUnit(IntEnum):
|
|
43
|
+
'''角度单位枚举'''
|
|
44
|
+
DEG = 0 # 度
|
|
45
|
+
RAD = 1 # 弧度
|
|
46
|
+
|
|
47
|
+
# 便捷别名
|
|
48
|
+
MM = TranslationUnit.MM
|
|
49
|
+
M = TranslationUnit.M
|
|
50
|
+
DEG = AngleUnit.DEG
|
|
51
|
+
RAD = AngleUnit.RAD
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'''
|
|
2
|
+
对数四元数模块
|
|
3
|
+
----------------------------------------------------------------
|
|
4
|
+
提供四元数与对数四元数(R³ 旋转向量)之间的映射,
|
|
5
|
+
用于姿态插值和优化。
|
|
6
|
+
|
|
7
|
+
理论基础:
|
|
8
|
+
- 对数映射: SO(3) → so(3), q → log(q)
|
|
9
|
+
- 指数映射: so(3) → SO(3), v → exp(v)
|
|
10
|
+
- 应用场景: 姿态插值、轨迹规划、姿态优化等
|
|
11
|
+
|
|
12
|
+
作者: 阿凯爱玩机器人 | 微信: xingshunkai | QQ: 244561792
|
|
13
|
+
官网: deepsenserobot.com
|
|
14
|
+
B站: https://space.bilibili.com/40344504
|
|
15
|
+
'''
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def quat2log(q):
|
|
21
|
+
'''单位四元数 → 对数四元数(R³ 旋转向量)
|
|
22
|
+
|
|
23
|
+
使用对数映射将 SO(3) 群映射到其李代数 so(3) ≅ R³。
|
|
24
|
+
支持单个或批量四元数输入。
|
|
25
|
+
|
|
26
|
+
:param q: 单位四元数 [w, x, y, z],形状为 (4,) 或 (..., 4)
|
|
27
|
+
:return: 对数四元数(旋转向量) [vx, vy, vz],形状为 (3,) 或 (..., 3)
|
|
28
|
+
'''
|
|
29
|
+
q = np.asarray(q, dtype=np.float64)
|
|
30
|
+
original_shape = q.shape
|
|
31
|
+
|
|
32
|
+
if q.ndim == 1:
|
|
33
|
+
q = q.reshape(1, -1)
|
|
34
|
+
|
|
35
|
+
# 归一化
|
|
36
|
+
q = q / np.linalg.norm(q, axis=-1, keepdims=True)
|
|
37
|
+
|
|
38
|
+
w = q[..., 0]
|
|
39
|
+
vec = q[..., 1:]
|
|
40
|
+
norm_vec = np.linalg.norm(vec, axis=-1)
|
|
41
|
+
|
|
42
|
+
log_q = np.zeros((*q.shape[:-1], 3), dtype=np.float64)
|
|
43
|
+
|
|
44
|
+
mask = norm_vec >= 1e-6
|
|
45
|
+
if np.any(mask):
|
|
46
|
+
theta = 2.0 * np.arctan2(norm_vec[mask], w[mask])
|
|
47
|
+
axis = vec[mask] / norm_vec[mask, np.newaxis]
|
|
48
|
+
log_q[mask] = axis * theta[:, np.newaxis]
|
|
49
|
+
|
|
50
|
+
if len(original_shape) == 1:
|
|
51
|
+
return log_q[0]
|
|
52
|
+
return log_q
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def log2quat(v):
|
|
56
|
+
'''对数四元数(R³ 旋转向量) → 单位四元数
|
|
57
|
+
|
|
58
|
+
使用指数映射将李代数 so(3) 映射回 SO(3) 群。
|
|
59
|
+
支持单个或批量输入。
|
|
60
|
+
|
|
61
|
+
:param v: 对数四元数(旋转向量) [vx, vy, vz],形状为 (3,) 或 (..., 3)
|
|
62
|
+
:return: 单位四元数 [w, x, y, z],形状为 (4,) 或 (..., 4)
|
|
63
|
+
'''
|
|
64
|
+
v = np.asarray(v, dtype=np.float64)
|
|
65
|
+
original_shape = v.shape
|
|
66
|
+
|
|
67
|
+
if v.ndim == 1:
|
|
68
|
+
v = v.reshape(1, -1)
|
|
69
|
+
|
|
70
|
+
theta = np.linalg.norm(v, axis=-1)
|
|
71
|
+
|
|
72
|
+
q = np.zeros((*v.shape[:-1], 4), dtype=np.float64)
|
|
73
|
+
q[..., 0] = 1.0
|
|
74
|
+
|
|
75
|
+
mask = theta >= 1e-6
|
|
76
|
+
if np.any(mask):
|
|
77
|
+
axis = v[mask] / theta[mask, np.newaxis]
|
|
78
|
+
half_theta = 0.5 * theta[mask]
|
|
79
|
+
q[mask, 0] = np.cos(half_theta)
|
|
80
|
+
q[mask, 1:] = axis * np.sin(half_theta)[:, np.newaxis]
|
|
81
|
+
|
|
82
|
+
if len(original_shape) == 1:
|
|
83
|
+
return q[0]
|
|
84
|
+
return q
|
akai_transform/quat.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
'''
|
|
2
|
+
四元数运算模块
|
|
3
|
+
----------------------------------------------------------------
|
|
4
|
+
提供四元数的基础运算、向量旋转、插值和统计功能。
|
|
5
|
+
四元数格式统一为 [w, x, y, z](标量在前)。
|
|
6
|
+
|
|
7
|
+
作者: 阿凯爱玩机器人 | 微信: xingshunkai | QQ: 244561792
|
|
8
|
+
官网: deepsenserobot.com
|
|
9
|
+
B站: https://space.bilibili.com/40344504
|
|
10
|
+
'''
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ============================================================
|
|
16
|
+
# 基础操作
|
|
17
|
+
# ============================================================
|
|
18
|
+
|
|
19
|
+
def identity():
|
|
20
|
+
'''返回单位四元数 [1, 0, 0, 0]
|
|
21
|
+
|
|
22
|
+
:return: 单位四元数 (4,)
|
|
23
|
+
'''
|
|
24
|
+
return np.array([1.0, 0.0, 0.0, 0.0])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def norm(q):
|
|
28
|
+
'''计算四元数的模长
|
|
29
|
+
|
|
30
|
+
:param q: 四元数 [w, x, y, z]
|
|
31
|
+
:return: 模长标量
|
|
32
|
+
'''
|
|
33
|
+
return np.linalg.norm(q)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_unit(q, tol=1e-6):
|
|
37
|
+
'''判断是否为单位四元数
|
|
38
|
+
|
|
39
|
+
:param q: 四元数 [w, x, y, z]
|
|
40
|
+
:param tol: 误差容限
|
|
41
|
+
:return: 是否为单位四元数
|
|
42
|
+
'''
|
|
43
|
+
return abs(norm(q) - 1.0) < tol
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize(q):
|
|
47
|
+
'''归一化为单位四元数
|
|
48
|
+
|
|
49
|
+
:param q: 四元数 [w, x, y, z]
|
|
50
|
+
:return: 单位四元数 (4,)
|
|
51
|
+
'''
|
|
52
|
+
q = np.asarray(q, dtype=float)
|
|
53
|
+
return q / np.linalg.norm(q)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def conjugate(q):
|
|
57
|
+
'''四元数共轭
|
|
58
|
+
|
|
59
|
+
:param q: 四元数 [w, x, y, z]
|
|
60
|
+
:return: 共轭四元数 [w, -x, -y, -z]
|
|
61
|
+
'''
|
|
62
|
+
q = np.asarray(q, dtype=float)
|
|
63
|
+
return np.array([q[0], -q[1], -q[2], -q[3]])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def reverse(q):
|
|
67
|
+
'''四元数取反
|
|
68
|
+
|
|
69
|
+
:param q: 四元数 [w, x, y, z]
|
|
70
|
+
:return: 取反后的四元数 [-w, -x, -y, -z]
|
|
71
|
+
'''
|
|
72
|
+
return -np.asarray(q, dtype=float)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def inv(q):
|
|
76
|
+
'''四元数求逆
|
|
77
|
+
|
|
78
|
+
:param q: 四元数 [w, x, y, z]
|
|
79
|
+
:return: 逆四元数 (4,)
|
|
80
|
+
'''
|
|
81
|
+
q = np.asarray(q, dtype=float)
|
|
82
|
+
q_conj = conjugate(q)
|
|
83
|
+
n2 = np.dot(q, q)
|
|
84
|
+
return q_conj / n2
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ============================================================
|
|
88
|
+
# 运算
|
|
89
|
+
# ============================================================
|
|
90
|
+
|
|
91
|
+
def dot(q1, q2):
|
|
92
|
+
'''四元数点乘
|
|
93
|
+
|
|
94
|
+
:param q1: 四元数 [w, x, y, z]
|
|
95
|
+
:param q2: 四元数 [w, x, y, z]
|
|
96
|
+
:return: 点乘标量
|
|
97
|
+
'''
|
|
98
|
+
return float(np.dot(q1, q2))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def multiply(q1, q2):
|
|
102
|
+
'''四元数 Grabmann 乘积(Hamilton 积)
|
|
103
|
+
|
|
104
|
+
:param q1: 四元数 [w, x, y, z]
|
|
105
|
+
:param q2: 四元数 [w, x, y, z]
|
|
106
|
+
:return: 乘积四元数 (4,)
|
|
107
|
+
'''
|
|
108
|
+
a, b, c, d = q1[0], q1[1], q1[2], q1[3]
|
|
109
|
+
mat_left = np.array([
|
|
110
|
+
[ a, -b, -c, -d],
|
|
111
|
+
[ b, a, -d, c],
|
|
112
|
+
[ c, d, a, -b],
|
|
113
|
+
[ d, -c, b, a]
|
|
114
|
+
])
|
|
115
|
+
return mat_left @ np.asarray(q2, dtype=float)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ============================================================
|
|
119
|
+
# 向量旋转
|
|
120
|
+
# ============================================================
|
|
121
|
+
|
|
122
|
+
def rotate_vec(q, v):
|
|
123
|
+
'''用四元数旋转向量
|
|
124
|
+
|
|
125
|
+
:param q: 单位四元数 [w, x, y, z]
|
|
126
|
+
:param v: 3D 向量 (3,)
|
|
127
|
+
:return: 旋转后的向量 (3,)
|
|
128
|
+
'''
|
|
129
|
+
q_v = np.array([0.0, v[0], v[1], v[2]])
|
|
130
|
+
q_conj = conjugate(q)
|
|
131
|
+
result = multiply(multiply(q, q_v), q_conj)
|
|
132
|
+
return result[1:]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def rotate_vec_nq(q_list, v):
|
|
136
|
+
'''批量旋转:多个四元数旋转同一个向量
|
|
137
|
+
|
|
138
|
+
:param q_list: 四元数列表,每个 [w, x, y, z]
|
|
139
|
+
:param v: 3D 向量 (3,)
|
|
140
|
+
:return: 旋转后的向量列表
|
|
141
|
+
'''
|
|
142
|
+
return [rotate_vec(q, v) for q in q_list]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def rotate_vec_nv(q, v_list):
|
|
146
|
+
'''批量旋转:同一个四元数旋转多个向量
|
|
147
|
+
|
|
148
|
+
:param q: 单位四元数 [w, x, y, z]
|
|
149
|
+
:param v_list: 向量列表,每个 (3,)
|
|
150
|
+
:return: 旋转后的向量列表
|
|
151
|
+
'''
|
|
152
|
+
return [rotate_vec(q, v) for v in v_list]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ============================================================
|
|
156
|
+
# 旋转度量
|
|
157
|
+
# ============================================================
|
|
158
|
+
|
|
159
|
+
def angle_diff(q1, q2, select_min=True):
|
|
160
|
+
'''计算两个四元数在超球面上的角度差
|
|
161
|
+
|
|
162
|
+
:param q1: 单位四元数 [w, x, y, z]
|
|
163
|
+
:param q2: 单位四元数 [w, x, y, z]
|
|
164
|
+
:param select_min: True 选最小角度 [0, π/2],False 选 [π/2, π]
|
|
165
|
+
:return: (角度差值, 是否需要将 q2 取反)
|
|
166
|
+
'''
|
|
167
|
+
q1_u = normalize(q1)
|
|
168
|
+
q2_u = normalize(q2)
|
|
169
|
+
|
|
170
|
+
# 判断相等或相反
|
|
171
|
+
if np.allclose(q1_u, q2_u, atol=1e-8):
|
|
172
|
+
return 0.0, False
|
|
173
|
+
if np.allclose(q1_u, -q2_u, atol=1e-8):
|
|
174
|
+
return 0.0, True
|
|
175
|
+
|
|
176
|
+
cos_theta = float(np.dot(q1_u, q2_u))
|
|
177
|
+
is_q_reverse = False
|
|
178
|
+
|
|
179
|
+
if (select_min and cos_theta < 0) or (not select_min and cos_theta > 0):
|
|
180
|
+
cos_theta = -cos_theta
|
|
181
|
+
is_q_reverse = True
|
|
182
|
+
|
|
183
|
+
cos_theta = np.clip(cos_theta, -1.0, 1.0)
|
|
184
|
+
theta = np.arccos(cos_theta)
|
|
185
|
+
return float(theta), is_q_reverse
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def relative_rotation(q1, q2):
|
|
189
|
+
'''求解两个四元数的相对旋转 q_delta
|
|
190
|
+
|
|
191
|
+
满足 q_delta * q1 = q2,即 q_delta = q2 * q1^(-1)
|
|
192
|
+
|
|
193
|
+
:param q1: 起始四元数 [w, x, y, z]
|
|
194
|
+
:param q2: 目标四元数 [w, x, y, z]
|
|
195
|
+
:return: 相对旋转四元数 (4,)
|
|
196
|
+
'''
|
|
197
|
+
q1_conj = conjugate(q1)
|
|
198
|
+
return multiply(q2, q1_conj)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ============================================================
|
|
202
|
+
# 插值
|
|
203
|
+
# ============================================================
|
|
204
|
+
|
|
205
|
+
def lerp(q1, q2, t):
|
|
206
|
+
'''四元数线性插值(结果非单位四元数)
|
|
207
|
+
|
|
208
|
+
:param q1: 起始四元数 [w, x, y, z]
|
|
209
|
+
:param q2: 终止四元数 [w, x, y, z]
|
|
210
|
+
:param t: 插值参数 [0, 1]
|
|
211
|
+
:return: 插值四元数 (4,)
|
|
212
|
+
'''
|
|
213
|
+
q1 = np.asarray(q1, dtype=float)
|
|
214
|
+
q2 = np.asarray(q2, dtype=float)
|
|
215
|
+
return (1.0 - t) * q1 + t * q2
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def nlerp(q1, q2, t):
|
|
219
|
+
'''四元数正规化线性插值(结果为单位四元数)
|
|
220
|
+
|
|
221
|
+
:param q1: 起始四元数 [w, x, y, z]
|
|
222
|
+
:param q2: 终止四元数 [w, x, y, z]
|
|
223
|
+
:param t: 插值参数 [0, 1]
|
|
224
|
+
:return: 单位四元数 (4,)
|
|
225
|
+
'''
|
|
226
|
+
return normalize(lerp(q1, q2, t))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def slerp(q1, q2, t):
|
|
230
|
+
'''四元数球面线性插值
|
|
231
|
+
|
|
232
|
+
:param q1: 起始四元数 [w, x, y, z]
|
|
233
|
+
:param q2: 终止四元数 [w, x, y, z]
|
|
234
|
+
:param t: 插值参数 [0, 1]
|
|
235
|
+
:return: 单位四元数 (4,)
|
|
236
|
+
'''
|
|
237
|
+
q1 = np.asarray(q1, dtype=float)
|
|
238
|
+
q2 = np.asarray(q2, dtype=float)
|
|
239
|
+
cos_theta = np.clip(float(np.dot(q1, q2)), -1.0, 1.0)
|
|
240
|
+
# 过于接近时退化为 NLerp
|
|
241
|
+
if abs(cos_theta) > 0.9995:
|
|
242
|
+
return nlerp(q1, q2, t)
|
|
243
|
+
theta = np.arccos(cos_theta)
|
|
244
|
+
sin_theta = np.sin(theta)
|
|
245
|
+
q_interp = (q1 * np.sin((1.0 - t) * theta) + q2 * np.sin(t * theta)) / sin_theta
|
|
246
|
+
return normalize(q_interp)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def interp_p2p(q1, q2, t_list, short_path=True):
|
|
250
|
+
'''两点间四元数批量插值(默认使用 Slerp)
|
|
251
|
+
|
|
252
|
+
:param q1: 起始四元数 [w, x, y, z]
|
|
253
|
+
:param q2: 终止四元数 [w, x, y, z]
|
|
254
|
+
:param t_list: 插值参数列表
|
|
255
|
+
:param short_path: 是否选择最短路径
|
|
256
|
+
:return: 四元数列表
|
|
257
|
+
'''
|
|
258
|
+
q1_u = normalize(q1)
|
|
259
|
+
q2_u = normalize(q2)
|
|
260
|
+
|
|
261
|
+
# 判断相等或相反
|
|
262
|
+
if np.allclose(q1_u, q2_u, atol=1e-3) or np.allclose(q1_u, -q2_u, atol=1e-3):
|
|
263
|
+
return [q1_u.copy() for _ in t_list]
|
|
264
|
+
|
|
265
|
+
cos_theta = float(np.dot(q1_u, q2_u))
|
|
266
|
+
cos_theta = np.clip(cos_theta, -1.0, 1.0)
|
|
267
|
+
|
|
268
|
+
# 选择最短/最长路径
|
|
269
|
+
if (short_path and cos_theta < 0) or (not short_path and cos_theta > 0):
|
|
270
|
+
cos_theta = -cos_theta
|
|
271
|
+
q2_u = -q2_u
|
|
272
|
+
|
|
273
|
+
is_close = abs(cos_theta) > 0.9995
|
|
274
|
+
theta = np.arccos(cos_theta)
|
|
275
|
+
|
|
276
|
+
result = []
|
|
277
|
+
for t in t_list:
|
|
278
|
+
if is_close:
|
|
279
|
+
result.append(nlerp(q1_u, q2_u, t))
|
|
280
|
+
else:
|
|
281
|
+
sin_theta = np.sin(theta)
|
|
282
|
+
q_interp = (q1_u * np.sin((1.0 - t) * theta) + q2_u * np.sin(t * theta)) / sin_theta
|
|
283
|
+
result.append(normalize(q_interp))
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def squad(q0, q1, s0, s1, t):
|
|
288
|
+
'''四元数 Squad 插值(Spherical Quadrangle)
|
|
289
|
+
|
|
290
|
+
Squad 是 Slerp 的高阶扩展,用于多途径点的平滑插值。
|
|
291
|
+
类似于位置空间的圆弧过渡,Squad 在姿态空间提供 C1 连续性。
|
|
292
|
+
|
|
293
|
+
:param q0: 起始四元数 [w, x, y, z]
|
|
294
|
+
:param q1: 终止四元数 [w, x, y, z]
|
|
295
|
+
:param s0: 起始控制四元数 [w, x, y, z]
|
|
296
|
+
:param s1: 终止控制四元数 [w, x, y, z]
|
|
297
|
+
:param t: 插值参数 [0, 1]
|
|
298
|
+
:return: 插值四元数 (4,)
|
|
299
|
+
'''
|
|
300
|
+
# Squad(q0, q1, s0, s1, t) = Slerp(Slerp(q0, q1, t), Slerp(s0, s1, t), 2t(1-t))
|
|
301
|
+
a = slerp(q0, q1, t)
|
|
302
|
+
b = slerp(s0, s1, t)
|
|
303
|
+
return slerp(a, b, 2.0 * t * (1.0 - t))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def compute_control_quat(q_prev, q_curr, q_next):
|
|
307
|
+
'''计算 Squad 插值的控制四元数
|
|
308
|
+
|
|
309
|
+
控制四元数用于保证多途径点插值的平滑性(C1 连续)。
|
|
310
|
+
计算公式基于对数映射:
|
|
311
|
+
s_i = q_i * exp(-0.25 * (log(q_i^-1 * q_{i-1}) + log(q_i^-1 * q_{i+1})))
|
|
312
|
+
|
|
313
|
+
:param q_prev: 前一个四元数 [w, x, y, z]
|
|
314
|
+
:param q_curr: 当前四元数 [w, x, y, z]
|
|
315
|
+
:param q_next: 后一个四元数 [w, x, y, z]
|
|
316
|
+
:return: 当前点的控制四元数 (4,)
|
|
317
|
+
'''
|
|
318
|
+
q_prev = normalize(q_prev)
|
|
319
|
+
q_curr = normalize(q_curr)
|
|
320
|
+
q_next = normalize(q_next)
|
|
321
|
+
|
|
322
|
+
# 计算相对旋转
|
|
323
|
+
q_curr_inv = inv(q_curr)
|
|
324
|
+
q_rel_prev = multiply(q_curr_inv, q_prev)
|
|
325
|
+
q_rel_next = multiply(q_curr_inv, q_next)
|
|
326
|
+
|
|
327
|
+
# 转换为对数空间(旋转向量)
|
|
328
|
+
def quat_log(q):
|
|
329
|
+
'''四元数对数映射:q -> log(q)'''
|
|
330
|
+
q = normalize(q)
|
|
331
|
+
w = q[0]
|
|
332
|
+
v = q[1:]
|
|
333
|
+
v_norm = np.linalg.norm(v)
|
|
334
|
+
if v_norm < 1e-8:
|
|
335
|
+
return np.array([0.0, 0.0, 0.0])
|
|
336
|
+
theta = 2.0 * np.arctan2(v_norm, w)
|
|
337
|
+
return (theta / v_norm) * v
|
|
338
|
+
|
|
339
|
+
def quat_exp(omega):
|
|
340
|
+
'''四元数指数映射:log(q) -> q'''
|
|
341
|
+
theta = np.linalg.norm(omega)
|
|
342
|
+
if theta < 1e-8:
|
|
343
|
+
return np.array([1.0, 0.0, 0.0, 0.0])
|
|
344
|
+
half_theta = theta / 2.0
|
|
345
|
+
s = np.sin(half_theta) / theta
|
|
346
|
+
return np.array([np.cos(half_theta), s * omega[0], s * omega[1], s * omega[2]])
|
|
347
|
+
|
|
348
|
+
# 对数空间平均
|
|
349
|
+
log_prev = quat_log(q_rel_prev)
|
|
350
|
+
log_next = quat_log(q_rel_next)
|
|
351
|
+
log_avg = -0.25 * (log_prev + log_next)
|
|
352
|
+
|
|
353
|
+
# 转回四元数空间
|
|
354
|
+
q_delta = quat_exp(log_avg)
|
|
355
|
+
s = multiply(q_curr, q_delta)
|
|
356
|
+
return normalize(s)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def interp_multi_waypoints(q_list, t_list, method='squad'):
|
|
360
|
+
'''多途径点四元数插值
|
|
361
|
+
|
|
362
|
+
:param q_list: 四元数途径点列表,每个 [w, x, y, z],至少 2 个点
|
|
363
|
+
:param t_list: 插值参数列表,范围 [0, n-1],n 为途径点数量
|
|
364
|
+
:param method: 插值方法,'slerp' 或 'squad'
|
|
365
|
+
:return: 插值四元数列表
|
|
366
|
+
'''
|
|
367
|
+
q_list = [normalize(q) for q in q_list]
|
|
368
|
+
n = len(q_list)
|
|
369
|
+
|
|
370
|
+
if n < 2:
|
|
371
|
+
raise ValueError("至少需要 2 个途径点")
|
|
372
|
+
|
|
373
|
+
if method == 'slerp':
|
|
374
|
+
# Slerp 分段插值(折线效果)
|
|
375
|
+
result = []
|
|
376
|
+
for t in t_list:
|
|
377
|
+
t = np.clip(t, 0.0, n - 1.0)
|
|
378
|
+
idx = int(np.floor(t))
|
|
379
|
+
if idx >= n - 1:
|
|
380
|
+
result.append(q_list[-1].copy())
|
|
381
|
+
else:
|
|
382
|
+
local_t = t - idx
|
|
383
|
+
result.append(slerp(q_list[idx], q_list[idx + 1], local_t))
|
|
384
|
+
return result
|
|
385
|
+
|
|
386
|
+
elif method == 'squad':
|
|
387
|
+
# Squad 平滑插值
|
|
388
|
+
if n < 3:
|
|
389
|
+
# 少于 3 个点时退化为 Slerp
|
|
390
|
+
return interp_multi_waypoints(q_list, t_list, method='slerp')
|
|
391
|
+
|
|
392
|
+
# 计算所有控制四元数
|
|
393
|
+
s_list = []
|
|
394
|
+
for i in range(n):
|
|
395
|
+
if i == 0:
|
|
396
|
+
# 首点:使用镜像策略
|
|
397
|
+
q_prev = multiply(q_list[0], inv(multiply(inv(q_list[0]), q_list[1])))
|
|
398
|
+
s = compute_control_quat(q_prev, q_list[0], q_list[1])
|
|
399
|
+
elif i == n - 1:
|
|
400
|
+
# 末点:使用镜像策略
|
|
401
|
+
q_next = multiply(q_list[-1], inv(multiply(inv(q_list[-1]), q_list[-2])))
|
|
402
|
+
s = compute_control_quat(q_list[-2], q_list[-1], q_next)
|
|
403
|
+
else:
|
|
404
|
+
# 中间点:正常计算
|
|
405
|
+
s = compute_control_quat(q_list[i - 1], q_list[i], q_list[i + 1])
|
|
406
|
+
s_list.append(s)
|
|
407
|
+
|
|
408
|
+
# 执行 Squad 插值
|
|
409
|
+
result = []
|
|
410
|
+
for t in t_list:
|
|
411
|
+
t = np.clip(t, 0.0, n - 1.0)
|
|
412
|
+
idx = int(np.floor(t))
|
|
413
|
+
if idx >= n - 1:
|
|
414
|
+
result.append(q_list[-1].copy())
|
|
415
|
+
else:
|
|
416
|
+
local_t = t - idx
|
|
417
|
+
q_interp = squad(q_list[idx], q_list[idx + 1], s_list[idx], s_list[idx + 1], local_t)
|
|
418
|
+
result.append(q_interp)
|
|
419
|
+
return result
|
|
420
|
+
|
|
421
|
+
else:
|
|
422
|
+
raise ValueError(f"未知的插值方法: {method}")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# ============================================================
|
|
426
|
+
# 统计
|
|
427
|
+
# ============================================================
|
|
428
|
+
|
|
429
|
+
def mean(q_list):
|
|
430
|
+
'''四元数均值(最大特征值法)
|
|
431
|
+
|
|
432
|
+
:param q_list: 四元数列表,每个 [w, x, y, z]
|
|
433
|
+
:return: 均值四元数 (4,)
|
|
434
|
+
'''
|
|
435
|
+
q_arr = np.array(q_list, dtype=float)
|
|
436
|
+
n = q_arr.shape[0]
|
|
437
|
+
# 外积累加
|
|
438
|
+
A = np.zeros((4, 4))
|
|
439
|
+
for i in range(n):
|
|
440
|
+
q = q_arr[i]
|
|
441
|
+
A += np.outer(q, q)
|
|
442
|
+
A /= n
|
|
443
|
+
# 特征值分解,取最大特征值对应的特征向量
|
|
444
|
+
eigenvalues, eigenvectors = np.linalg.eigh(A)
|
|
445
|
+
# eigh 返回升序排列,最大特征值在最后
|
|
446
|
+
q_mean = eigenvectors[:, -1]
|
|
447
|
+
return normalize(q_mean)
|