robomath 0.1.0__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.
- robomath/__init__.py +221 -0
- robomath/_util.py +16 -0
- robomath/convert.py +68 -0
- robomath/euler.py +100 -0
- robomath/geodesic.py +148 -0
- robomath/quaternions.py +308 -0
- robomath/registration.py +60 -0
- robomath/rotations.py +82 -0
- robomath/se2.py +117 -0
- robomath/se23.py +138 -0
- robomath/se3.py +196 -0
- robomath/sim3.py +167 -0
- robomath/so2.py +40 -0
- robomath/so3.py +437 -0
- robomath/transform.py +141 -0
- robomath-0.1.0.dist-info/METADATA +607 -0
- robomath-0.1.0.dist-info/RECORD +20 -0
- robomath-0.1.0.dist-info/WHEEL +5 -0
- robomath-0.1.0.dist-info/licenses/LICENSE +201 -0
- robomath-0.1.0.dist-info/top_level.txt +1 -0
robomath/__init__.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Declare all public usable functions from robomath.
|
|
2
|
+
|
|
3
|
+
Groups: SO(2), SO(3), SE(2), SE(3), Sim(3) (pose with a scale, the scale last in the
|
|
4
|
+
tangent vector) and SE_2(3) (rotation, velocity and position). The Transform class
|
|
5
|
+
wraps SE(3) only.
|
|
6
|
+
|
|
7
|
+
Planned: kinematics (screw axes, product of exponentials, forward
|
|
8
|
+
and inverse kinematics, Jacobians), trajectories on SO(3) and SE(3), and covariance
|
|
9
|
+
propagation. Estimators, simulators and ROS code stay outside this package.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .convert import (
|
|
13
|
+
pq_to_trans,
|
|
14
|
+
quat_wxyz_to_xyzw,
|
|
15
|
+
quat_xyzw_to_wxyz,
|
|
16
|
+
swap_twist,
|
|
17
|
+
trans_to_pq,
|
|
18
|
+
)
|
|
19
|
+
from .euler import euler_zyx_to_matrix, matrix_to_euler_zyx
|
|
20
|
+
from .geodesic import (
|
|
21
|
+
distance_so3,
|
|
22
|
+
interp_se2,
|
|
23
|
+
interp_se3,
|
|
24
|
+
interp_so3,
|
|
25
|
+
mean_se2,
|
|
26
|
+
mean_se3,
|
|
27
|
+
mean_so3,
|
|
28
|
+
)
|
|
29
|
+
from .quaternions import (
|
|
30
|
+
axis_angle_to_quat,
|
|
31
|
+
euler_zyx_to_quat,
|
|
32
|
+
matrix_to_quat,
|
|
33
|
+
quat_conjugate,
|
|
34
|
+
quat_multiply,
|
|
35
|
+
quat_normalize,
|
|
36
|
+
quat_to_axis_angle,
|
|
37
|
+
quat_to_euler_zyx,
|
|
38
|
+
quat_to_matrix,
|
|
39
|
+
slerp,
|
|
40
|
+
)
|
|
41
|
+
from .registration import kabsch, umeyama
|
|
42
|
+
from .rotations import inverse, rotx, roty, rotz
|
|
43
|
+
from .se2 import (
|
|
44
|
+
adjoint_se2,
|
|
45
|
+
exp_vec_se2,
|
|
46
|
+
hat_se2,
|
|
47
|
+
lminus_se2,
|
|
48
|
+
log_vec_se2,
|
|
49
|
+
lplus_se2,
|
|
50
|
+
rminus_se2,
|
|
51
|
+
rplus_se2,
|
|
52
|
+
vee_se2,
|
|
53
|
+
)
|
|
54
|
+
from .se3 import (
|
|
55
|
+
adjoint,
|
|
56
|
+
angular,
|
|
57
|
+
exp_vec_se3,
|
|
58
|
+
hat_se3,
|
|
59
|
+
linear,
|
|
60
|
+
lminus_se3,
|
|
61
|
+
log_vec_se3,
|
|
62
|
+
lplus_se3,
|
|
63
|
+
rminus_se3,
|
|
64
|
+
rp_to_trans,
|
|
65
|
+
rplus_se3,
|
|
66
|
+
trans_inv,
|
|
67
|
+
trans_to_rp,
|
|
68
|
+
twist,
|
|
69
|
+
vee_se3,
|
|
70
|
+
)
|
|
71
|
+
from .se23 import (
|
|
72
|
+
adjoint_se23,
|
|
73
|
+
exp_vec_se23,
|
|
74
|
+
hat_se23,
|
|
75
|
+
lminus_se23,
|
|
76
|
+
log_vec_se23,
|
|
77
|
+
lplus_se23,
|
|
78
|
+
rminus_se23,
|
|
79
|
+
rplus_se23,
|
|
80
|
+
rvp_to_se23,
|
|
81
|
+
se23_inv,
|
|
82
|
+
se23_to_rvp,
|
|
83
|
+
vee_se23,
|
|
84
|
+
)
|
|
85
|
+
from .sim3 import (
|
|
86
|
+
adjoint_sim3,
|
|
87
|
+
exp_vec_sim3,
|
|
88
|
+
hat_sim3,
|
|
89
|
+
lminus_sim3,
|
|
90
|
+
log_vec_sim3,
|
|
91
|
+
lplus_sim3,
|
|
92
|
+
rminus_sim3,
|
|
93
|
+
rplus_sim3,
|
|
94
|
+
sim3_inv,
|
|
95
|
+
sim3_to_srt,
|
|
96
|
+
srt_to_sim3,
|
|
97
|
+
vee_sim3,
|
|
98
|
+
)
|
|
99
|
+
from .so2 import exp_so2, log_so2, wrap_angle
|
|
100
|
+
from .so3 import (
|
|
101
|
+
axis_angle_to_matrix,
|
|
102
|
+
exp_so3,
|
|
103
|
+
exp_vec,
|
|
104
|
+
hat,
|
|
105
|
+
jl,
|
|
106
|
+
jl_inv,
|
|
107
|
+
jr,
|
|
108
|
+
jr_inv,
|
|
109
|
+
lminus,
|
|
110
|
+
log_so3,
|
|
111
|
+
log_vec,
|
|
112
|
+
lplus,
|
|
113
|
+
matrix_to_axis_angle,
|
|
114
|
+
nearest_rotation,
|
|
115
|
+
rminus,
|
|
116
|
+
rplus,
|
|
117
|
+
vee,
|
|
118
|
+
)
|
|
119
|
+
from .transform import Transform
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
"axis_angle_to_matrix",
|
|
123
|
+
"axis_angle_to_quat",
|
|
124
|
+
"matrix_to_axis_angle",
|
|
125
|
+
"matrix_to_quat",
|
|
126
|
+
"quat_conjugate",
|
|
127
|
+
"quat_multiply",
|
|
128
|
+
"quat_normalize",
|
|
129
|
+
"quat_to_axis_angle",
|
|
130
|
+
"quat_to_matrix",
|
|
131
|
+
"slerp",
|
|
132
|
+
"euler_zyx_to_matrix",
|
|
133
|
+
"inverse",
|
|
134
|
+
"matrix_to_euler_zyx",
|
|
135
|
+
"rotx",
|
|
136
|
+
"roty",
|
|
137
|
+
"rotz",
|
|
138
|
+
"hat",
|
|
139
|
+
"vee",
|
|
140
|
+
"exp_vec",
|
|
141
|
+
"log_vec",
|
|
142
|
+
"exp_so3",
|
|
143
|
+
"log_so3",
|
|
144
|
+
"rplus",
|
|
145
|
+
"rminus",
|
|
146
|
+
"lplus",
|
|
147
|
+
"lminus",
|
|
148
|
+
"jr",
|
|
149
|
+
"jr_inv",
|
|
150
|
+
"jl",
|
|
151
|
+
"jl_inv",
|
|
152
|
+
"euler_zyx_to_quat",
|
|
153
|
+
"quat_to_euler_zyx",
|
|
154
|
+
"nearest_rotation",
|
|
155
|
+
"Transform",
|
|
156
|
+
"adjoint_sim3",
|
|
157
|
+
"exp_vec_sim3",
|
|
158
|
+
"hat_sim3",
|
|
159
|
+
"lminus_sim3",
|
|
160
|
+
"log_vec_sim3",
|
|
161
|
+
"lplus_sim3",
|
|
162
|
+
"rminus_sim3",
|
|
163
|
+
"rplus_sim3",
|
|
164
|
+
"sim3_inv",
|
|
165
|
+
"sim3_to_srt",
|
|
166
|
+
"srt_to_sim3",
|
|
167
|
+
"vee_sim3",
|
|
168
|
+
"adjoint_se23",
|
|
169
|
+
"exp_vec_se23",
|
|
170
|
+
"hat_se23",
|
|
171
|
+
"lminus_se23",
|
|
172
|
+
"log_vec_se23",
|
|
173
|
+
"lplus_se23",
|
|
174
|
+
"rminus_se23",
|
|
175
|
+
"rplus_se23",
|
|
176
|
+
"rvp_to_se23",
|
|
177
|
+
"se23_inv",
|
|
178
|
+
"se23_to_rvp",
|
|
179
|
+
"vee_se23",
|
|
180
|
+
"pq_to_trans",
|
|
181
|
+
"quat_wxyz_to_xyzw",
|
|
182
|
+
"quat_xyzw_to_wxyz",
|
|
183
|
+
"swap_twist",
|
|
184
|
+
"trans_to_pq",
|
|
185
|
+
"kabsch",
|
|
186
|
+
"umeyama",
|
|
187
|
+
"distance_so3",
|
|
188
|
+
"interp_se2",
|
|
189
|
+
"interp_se3",
|
|
190
|
+
"interp_so3",
|
|
191
|
+
"mean_se2",
|
|
192
|
+
"mean_se3",
|
|
193
|
+
"mean_so3",
|
|
194
|
+
"exp_so2",
|
|
195
|
+
"log_so2",
|
|
196
|
+
"wrap_angle",
|
|
197
|
+
"adjoint_se2",
|
|
198
|
+
"exp_vec_se2",
|
|
199
|
+
"hat_se2",
|
|
200
|
+
"lminus_se2",
|
|
201
|
+
"log_vec_se2",
|
|
202
|
+
"lplus_se2",
|
|
203
|
+
"rminus_se2",
|
|
204
|
+
"rplus_se2",
|
|
205
|
+
"vee_se2",
|
|
206
|
+
"adjoint",
|
|
207
|
+
"angular",
|
|
208
|
+
"exp_vec_se3",
|
|
209
|
+
"hat_se3",
|
|
210
|
+
"linear",
|
|
211
|
+
"lminus_se3",
|
|
212
|
+
"log_vec_se3",
|
|
213
|
+
"lplus_se3",
|
|
214
|
+
"rminus_se3",
|
|
215
|
+
"rp_to_trans",
|
|
216
|
+
"rplus_se3",
|
|
217
|
+
"trans_inv",
|
|
218
|
+
"trans_to_rp",
|
|
219
|
+
"twist",
|
|
220
|
+
"vee_se3",
|
|
221
|
+
]
|
robomath/_util.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Robomath - small helpers shared by the other modules."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def norm(v: np.ndarray) -> np.ndarray:
|
|
7
|
+
"""Length of the vectors along the last axis, shape (...).
|
|
8
|
+
|
|
9
|
+
Same result as np.linalg.norm(v, axis=-1)
|
|
10
|
+
"""
|
|
11
|
+
return np.sqrt(np.einsum("...i,...i->...", v, v))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def apply(a: np.ndarray, v: np.ndarray) -> np.ndarray:
|
|
15
|
+
"""Matrix-vector product for stacks: a (..., n, n) times v (..., n)."""
|
|
16
|
+
return (a @ v[..., None])[..., 0]
|
robomath/convert.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Robomath - Convert between robomath's layouts and those of other libraries.
|
|
2
|
+
|
|
3
|
+
Inside robomath a quaternion is (x, y, z, w) and a tangent vector is (translation,
|
|
4
|
+
rotation). Other libraries differ, and a mix-up still runs but gives wrong results.
|
|
5
|
+
Each function here changes only the layout, named by the layout and not the library.
|
|
6
|
+
Every function accepts leading batch dimensions.
|
|
7
|
+
|
|
8
|
+
Quaternion (x, y, z, w), scalar last: scipy, ROS messages, Eigen coeffs(), Pinocchio.
|
|
9
|
+
Quaternion (w, x, y, z), scalar first: Drake, MuJoCo, Ceres.
|
|
10
|
+
Tangent vector (translation, rotation): Sola's paper, Sophus, manif, Pinocchio.
|
|
11
|
+
Tangent vector (rotation, translation): Modern Robotics, Drake, MuJoCo, GTSAM Pose3.
|
|
12
|
+
|
|
13
|
+
Not here yet: poses with the camera convention
|
|
14
|
+
(OpenCV and COLMAP store world-to-camera, so the inverse of ours), other Euler
|
|
15
|
+
sequences, SE(2) layouts such as GTSAM's, and ROS message classes (this module takes
|
|
16
|
+
plain arrays, so it needs no ROS install).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from .quaternions import matrix_to_quat, quat_to_matrix
|
|
22
|
+
from .se3 import angular, linear, rp_to_trans, trans_to_rp, twist
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def quat_xyzw_to_wxyz(q: np.ndarray) -> np.ndarray:
|
|
26
|
+
"""Move the scalar part from last to first: (x, y, z, w) -> (w, x, y, z)."""
|
|
27
|
+
return np.roll(np.asarray(q, dtype=float), 1, axis=-1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def quat_wxyz_to_xyzw(q: np.ndarray) -> np.ndarray:
|
|
31
|
+
"""Move the scalar part from first to last: (w, x, y, z) -> (x, y, z, w)."""
|
|
32
|
+
return np.roll(np.asarray(q, dtype=float), -1, axis=-1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def swap_twist(tau: np.ndarray) -> np.ndarray:
|
|
36
|
+
"""Swap the halves of a tangent vector, (a, b) -> (b, a).
|
|
37
|
+
|
|
38
|
+
Turns (translation, rotation) into (rotation, translation) and back; the swap is
|
|
39
|
+
its own inverse. Works for a tangent vector or a spatial velocity.
|
|
40
|
+
"""
|
|
41
|
+
tau = np.asarray(tau, dtype=float)
|
|
42
|
+
return twist(angular(tau), linear(tau))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pq_to_trans(p: np.ndarray, q: np.ndarray) -> np.ndarray:
|
|
46
|
+
"""4x4 pose from a position and an (x, y, z, w) quaternion, as in a ROS Pose.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
p (np.ndarray): Positions, shape (..., 3).
|
|
50
|
+
q (np.ndarray): Unit quaternions, shape (..., 4).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
np.ndarray: The poses, shape (..., 4, 4).
|
|
54
|
+
"""
|
|
55
|
+
return rp_to_trans(quat_to_matrix(q), p)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def trans_to_pq(m: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
59
|
+
"""Position and (x, y, z, w) quaternion of a 4x4 pose, as in a ROS Pose.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
m (np.ndarray): Poses, shape (..., 4, 4).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
tuple[np.ndarray, np.ndarray]: (p, q), shapes (..., 3) and (..., 4).
|
|
66
|
+
"""
|
|
67
|
+
r, t = trans_to_rp(m)
|
|
68
|
+
return t, matrix_to_quat(r)
|
robomath/euler.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Robomath - ZYX Euler angles.
|
|
2
|
+
|
|
3
|
+
Every function accepts leading batch dimensions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
GIMBAL_TOL = 1e-15
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def euler_zyx_to_matrix(alpha: float, beta: float, gamma: float) -> np.ndarray:
|
|
14
|
+
"""Rotation matrix for ZYX Euler angles.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
alpha (float): Angle about z, in radians, scalar or shape (...).
|
|
18
|
+
beta (float): Angle about y, in radians; broadcasts with alpha.
|
|
19
|
+
gamma (float): Angle about x, in radians; broadcasts with alpha.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
np.ndarray: The rotation matrix, shape (..., 3, 3),
|
|
23
|
+
R = Rz(alpha) Ry(beta) Rx(gamma).
|
|
24
|
+
"""
|
|
25
|
+
alpha, beta, gamma = (np.asarray(a, dtype=float) for a in (alpha, beta, gamma))
|
|
26
|
+
if alpha.ndim == beta.ndim == gamma.ndim == 0:
|
|
27
|
+
ca, sa = math.cos(alpha), math.sin(alpha)
|
|
28
|
+
cb, sb = math.cos(beta), math.sin(beta)
|
|
29
|
+
cg, sg = math.cos(gamma), math.sin(gamma)
|
|
30
|
+
return np.array(
|
|
31
|
+
[
|
|
32
|
+
[ca * cb, ca * sb * sg - sa * cg, ca * sb * cg + sa * sg],
|
|
33
|
+
[sa * cb, sa * sb * sg + ca * cg, sa * sb * cg - ca * sg],
|
|
34
|
+
[-sb, cb * sg, cb * cg],
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
ca, sa = np.cos(alpha), np.sin(alpha)
|
|
38
|
+
cb, sb = np.cos(beta), np.sin(beta)
|
|
39
|
+
cg, sg = np.cos(gamma), np.sin(gamma)
|
|
40
|
+
r = np.empty(np.broadcast_shapes(alpha.shape, beta.shape, gamma.shape) + (3, 3))
|
|
41
|
+
r[..., 0, 0] = ca * cb
|
|
42
|
+
r[..., 0, 1] = ca * sb * sg - sa * cg
|
|
43
|
+
r[..., 0, 2] = ca * sb * cg + sa * sg
|
|
44
|
+
r[..., 1, 0] = sa * cb
|
|
45
|
+
r[..., 1, 1] = sa * sb * sg + ca * cg
|
|
46
|
+
r[..., 1, 2] = sa * sb * cg - ca * sg
|
|
47
|
+
r[..., 2, 0] = -sb
|
|
48
|
+
r[..., 2, 1] = cb * sg
|
|
49
|
+
r[..., 2, 2] = cb * cg
|
|
50
|
+
return r
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def matrix_to_euler_zyx(r: np.ndarray) -> tuple[float, float, float]:
|
|
54
|
+
"""ZYX Euler angles of a rotation matrix.
|
|
55
|
+
|
|
56
|
+
At gimbal lock (beta = +-pi/2) only one combination of alpha and gamma is
|
|
57
|
+
fixed by the matrix, so alpha is set to 0 and gamma takes the rest.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
r (np.ndarray): Rotation matrices, shape (..., 3, 3).
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
tuple[float, float, float]: (alpha, beta, gamma) in radians, each of
|
|
64
|
+
shape (...), with alpha and gamma in (-pi, pi] and beta in
|
|
65
|
+
[-pi/2, pi/2].
|
|
66
|
+
"""
|
|
67
|
+
r = np.asarray(r, dtype=float)
|
|
68
|
+
if r.ndim == 2:
|
|
69
|
+
(r11, r12, _), (r21, r22, _), (r31, r32, r33) = r.tolist()
|
|
70
|
+
if abs(r31 + 1) < GIMBAL_TOL:
|
|
71
|
+
return 0.0, math.pi / 2, math.atan2(r12, r22)
|
|
72
|
+
if abs(r31 - 1) < GIMBAL_TOL:
|
|
73
|
+
return 0.0, -math.pi / 2, -math.atan2(r12, r22)
|
|
74
|
+
return (
|
|
75
|
+
math.atan2(r21, r11),
|
|
76
|
+
math.atan2(-r31, math.hypot(r11, r21)),
|
|
77
|
+
math.atan2(r32, r33),
|
|
78
|
+
)
|
|
79
|
+
r11, r12 = r[..., 0, 0], r[..., 0, 1]
|
|
80
|
+
r21, r22 = r[..., 1, 0], r[..., 1, 1]
|
|
81
|
+
r31, r32, r33 = r[..., 2, 0], r[..., 2, 1], r[..., 2, 2]
|
|
82
|
+
|
|
83
|
+
# Both gimbal-lock cases and the general one are computed, then chosen by
|
|
84
|
+
# mask, so a batch can mix them.
|
|
85
|
+
lock_up = np.abs(r31 + 1) < GIMBAL_TOL
|
|
86
|
+
lock_down = np.abs(r31 - 1) < GIMBAL_TOL
|
|
87
|
+
locked_gamma = np.arctan2(r12, r22)
|
|
88
|
+
|
|
89
|
+
beta = np.where(
|
|
90
|
+
lock_up,
|
|
91
|
+
np.pi / 2,
|
|
92
|
+
np.where(lock_down, -np.pi / 2, np.arctan2(-r31, np.hypot(r11, r21))),
|
|
93
|
+
)
|
|
94
|
+
alpha = np.where(lock_up | lock_down, 0.0, np.arctan2(r21, r11))
|
|
95
|
+
gamma = np.where(
|
|
96
|
+
lock_up,
|
|
97
|
+
locked_gamma,
|
|
98
|
+
np.where(lock_down, -locked_gamma, np.arctan2(r32, r33)),
|
|
99
|
+
)
|
|
100
|
+
return alpha[()], beta[()], gamma[()]
|
robomath/geodesic.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Robomath - Distance, interpolation and mean on SO(3) and SE(3).
|
|
2
|
+
|
|
3
|
+
All of them follow the shortest path (the geodesic) given by the right plus and
|
|
4
|
+
minus of the group: the step from A to B is minus(B, A), and moving a fraction t
|
|
5
|
+
of it is plus(A, t * minus(B, A)). Every function accepts leading batch dimensions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from ._util import norm
|
|
13
|
+
from .quaternions import matrix_to_quat, quat_to_matrix
|
|
14
|
+
from .se2 import rminus_se2, rplus_se2
|
|
15
|
+
from .se3 import angular, rminus_se3, rp_to_trans, rplus_se3, trans_to_rp
|
|
16
|
+
from .so2 import exp_so2, log_so2
|
|
17
|
+
from .so3 import rminus, rplus
|
|
18
|
+
|
|
19
|
+
MEAN_TOL = 1e-13
|
|
20
|
+
MEAN_ITERATIONS = 100
|
|
21
|
+
UNIQUE_ANGLE = np.pi / 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def distance_so3(r1: np.ndarray, r2: np.ndarray) -> np.ndarray:
|
|
25
|
+
"""Angle in [0, pi] of the rotation that takes r1 to r2, shape (...)."""
|
|
26
|
+
return norm(rminus(r2, r1))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def interp_so3(r1: np.ndarray, r2: np.ndarray, t: np.ndarray) -> np.ndarray:
|
|
30
|
+
"""Rotation a fraction t of the way from r1 (t = 0) to r2 (t = 1)."""
|
|
31
|
+
t = np.asarray(t, dtype=float)
|
|
32
|
+
return rplus(r1, t[..., None] * rminus(r2, r1))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def interp_se2(m1: np.ndarray, m2: np.ndarray, t: np.ndarray) -> np.ndarray:
|
|
36
|
+
"""Planar pose a fraction t of the way from m1 (t = 0) to m2 (t = 1)."""
|
|
37
|
+
t = np.asarray(t, dtype=float)
|
|
38
|
+
return rplus_se2(m1, t[..., None] * rminus_se2(m2, m1))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def interp_se3(m1: np.ndarray, m2: np.ndarray, t: np.ndarray) -> np.ndarray:
|
|
42
|
+
"""Pose a fraction t of the way from m1 (t = 0) to m2 (t = 1)."""
|
|
43
|
+
t = np.asarray(t, dtype=float)
|
|
44
|
+
return rplus_se3(m1, t[..., None] * rminus_se3(m2, m1))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _weighted(w, x):
|
|
48
|
+
"""Weighted sum over the N items: w (..., N) times x (..., N, k) -> (..., k)."""
|
|
49
|
+
return np.einsum("...n,...nk->...k", w, x)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _start_so3(rotations, w):
|
|
53
|
+
"""Chordal mean: the top eigenvector of the weighted sum of q q^T (Markley).
|
|
54
|
+
|
|
55
|
+
No iteration and no local minima, and it does not depend on the item order.
|
|
56
|
+
Same mean as scipy's Rotation.mean, written here from the method.
|
|
57
|
+
"""
|
|
58
|
+
q = matrix_to_quat(rotations)
|
|
59
|
+
k = np.einsum("...n,...ni,...nj->...ij", w, q, q)
|
|
60
|
+
return quat_to_matrix(np.linalg.eigh(k)[1][..., -1])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _start_se3(poses, w):
|
|
64
|
+
r, t = trans_to_rp(poses)
|
|
65
|
+
return rp_to_trans(_start_so3(r, w), _weighted(w, t))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _start_se2(poses, w):
|
|
69
|
+
r, t = trans_to_rp(poses)
|
|
70
|
+
theta = log_so2(r)
|
|
71
|
+
mean_angle = np.arctan2(
|
|
72
|
+
np.einsum("...n,...n->...", w, np.sin(theta)),
|
|
73
|
+
np.einsum("...n,...n->...", w, np.cos(theta)),
|
|
74
|
+
)
|
|
75
|
+
return rp_to_trans(exp_so2(mean_angle), _weighted(w, t))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _angle_se3(tau):
|
|
79
|
+
"""Rotation angle of each SE(3) tangent vector."""
|
|
80
|
+
return norm(angular(tau))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _angle_se2(tau):
|
|
84
|
+
"""Rotation angle of each SE(2) tangent vector."""
|
|
85
|
+
return np.abs(tau[..., 2])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _mean(items, weights, start, plus, minus, angle):
|
|
89
|
+
"""Karcher mean: move the guess by the weighted mean of its offsets to the items.
|
|
90
|
+
|
|
91
|
+
items has shape (..., N, a, a); the N items are averaged. The first guess is
|
|
92
|
+
the chordal mean of the rotations, so the answer does not depend on the item
|
|
93
|
+
order. angle maps tangent offsets to the rotation angle of each. The mean is
|
|
94
|
+
unique when all items lie within 90 degrees of it. Beyond that the loop can
|
|
95
|
+
settle on a local answer, so it warns.
|
|
96
|
+
"""
|
|
97
|
+
items = np.asarray(items, dtype=float)
|
|
98
|
+
n = items.shape[-3]
|
|
99
|
+
if n == 0:
|
|
100
|
+
raise ValueError("the mean of no items is undefined")
|
|
101
|
+
w = np.full(n, 1.0 / n) if weights is None else np.asarray(weights, dtype=float)
|
|
102
|
+
if np.any(w < 0) or np.any(np.sum(w, axis=-1) <= 0):
|
|
103
|
+
raise ValueError("weights must be non-negative and not all zero")
|
|
104
|
+
w = w / np.sum(w, axis=-1, keepdims=True)
|
|
105
|
+
mean = start(items, w)
|
|
106
|
+
for _ in range(MEAN_ITERATIONS):
|
|
107
|
+
step = np.sum(w[..., None] * minus(items, mean[..., None, :, :]), axis=-2)
|
|
108
|
+
mean = plus(mean, step)
|
|
109
|
+
if np.max(np.abs(step), initial=0.0) < MEAN_TOL:
|
|
110
|
+
offsets = minus(items, mean[..., None, :, :])
|
|
111
|
+
if np.max(angle(offsets), initial=0.0) >= UNIQUE_ANGLE:
|
|
112
|
+
warnings.warn(
|
|
113
|
+
"an item is 90 degrees or more from the mean; "
|
|
114
|
+
"the mean may not be unique",
|
|
115
|
+
stacklevel=3,
|
|
116
|
+
)
|
|
117
|
+
return mean
|
|
118
|
+
raise ValueError("mean did not settle; the items are too spread out")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def mean_so3(rotations: np.ndarray, weights: np.ndarray | None = None) -> np.ndarray:
|
|
122
|
+
"""Mean of N rotations, shape (..., N, 3, 3) -> (..., 3, 3).
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
rotations (np.ndarray): The rotations to average.
|
|
126
|
+
weights (np.ndarray | None): Weights, shape (..., N). Equal if None.
|
|
127
|
+
"""
|
|
128
|
+
return _mean(rotations, weights, _start_so3, rplus, rminus, norm)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def mean_se3(poses: np.ndarray, weights: np.ndarray | None = None) -> np.ndarray:
|
|
132
|
+
"""Mean of N poses, shape (..., N, 4, 4) -> (..., 4, 4).
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
poses (np.ndarray): The poses to average.
|
|
136
|
+
weights (np.ndarray | None): Weights, shape (..., N). Equal if None.
|
|
137
|
+
"""
|
|
138
|
+
return _mean(poses, weights, _start_se3, rplus_se3, rminus_se3, _angle_se3)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def mean_se2(poses: np.ndarray, weights: np.ndarray | None = None) -> np.ndarray:
|
|
142
|
+
"""Mean of N planar poses, shape (..., N, 3, 3) -> (..., 3, 3).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
poses (np.ndarray): The poses to average.
|
|
146
|
+
weights (np.ndarray | None): Weights, shape (..., N). Equal if None.
|
|
147
|
+
"""
|
|
148
|
+
return _mean(poses, weights, _start_se2, rplus_se2, rminus_se2, _angle_se2)
|