cgse-coordinates 0.17.3__py3-none-any.whl → 0.18.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.
- cgse_coordinates/settings.yaml +0 -16
- {cgse_coordinates-0.17.3.dist-info → cgse_coordinates-0.18.0.dist-info}/METADATA +1 -1
- cgse_coordinates-0.18.0.dist-info/RECORD +16 -0
- {cgse_coordinates-0.17.3.dist-info → cgse_coordinates-0.18.0.dist-info}/entry_points.txt +0 -3
- egse/coordinates/__init__.py +27 -334
- egse/coordinates/avoidance.py +33 -41
- egse/coordinates/cslmodel.py +48 -57
- egse/coordinates/laser_tracker_to_dict.py +16 -25
- egse/coordinates/point.py +544 -418
- egse/coordinates/pyplot.py +117 -105
- egse/coordinates/reference_frame.py +1417 -0
- egse/coordinates/refmodel.py +311 -203
- egse/coordinates/rotation_matrix.py +95 -0
- egse/coordinates/transform3d_addon.py +292 -228
- cgse_coordinates-0.17.3.dist-info/RECORD +0 -16
- egse/coordinates/referenceFrame.py +0 -1251
- egse/coordinates/rotationMatrix.py +0 -82
- {cgse_coordinates-0.17.3.dist-info → cgse_coordinates-0.18.0.dist-info}/WHEEL +0 -0
|
@@ -1,65 +1,64 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
Created on Mon Jun 25 16:25:33 2018
|
|
5
|
-
|
|
6
|
-
@author: pierre
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import numpy
|
|
10
1
|
import numpy as np
|
|
11
2
|
import math
|
|
12
3
|
import transforms3d as t3
|
|
13
4
|
|
|
14
|
-
from egse.coordinates.
|
|
5
|
+
from egse.coordinates.rotation_matrix import RotationMatrix
|
|
15
6
|
|
|
16
7
|
|
|
17
|
-
def
|
|
18
|
-
"""
|
|
19
|
-
Tests if a matrix is a pure solid-body euclidian rotation + translation (no shear or scaling)
|
|
8
|
+
def affine_is_euclidian(matrix: np.ndarray) -> bool:
|
|
9
|
+
"""Checks if the given matrix is a pure solid-body Euclidian rotation + translation (no shear or scaling).
|
|
20
10
|
|
|
21
|
-
We only need to check that
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
We only need to check that:
|
|
12
|
+
- The rotation part is orthogonal : R @ R.T = I
|
|
13
|
+
- The det(R) = 1 (to check that the matrix does not represent a reflection)
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
matrix (np.ndarray): Matrix to check.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
True if the matrix is a pure solid-body Euclidian rotation + translation, False otherwise.
|
|
24
20
|
"""
|
|
21
|
+
|
|
25
22
|
rotation = matrix[:3, :3]
|
|
23
|
+
|
|
26
24
|
return np.allclose((rotation @ rotation.T), np.identity(3)) & np.allclose(np.linalg.det(matrix), 1)
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
def affine_inverse(matrix):
|
|
28
|
+
"""Returns the inverse of the given affine matrix.
|
|
29
|
+
|
|
30
|
+
We assume that the given matrix is an affine transformation matrix that only involves rotation and translation,
|
|
31
|
+
no zoom, no shear! That is why we can invert it by simply transposing the rotation part and negating the
|
|
32
|
+
translation part.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
matrix (np.ndarray): Augmented matrix to invert.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Inverted affine matrix.
|
|
30
39
|
"""
|
|
31
|
-
affine_inverse(matrix)
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
if affine_is_euclidian(matrix):
|
|
42
|
+
# Separate the given augmented matrix into rotation and translation
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
rotation = matrix[:3, :3]
|
|
45
|
+
translation = matrix[:3, 3]
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
in the affine transformation, no zoom, no shear!
|
|
47
|
+
# Invert the rotation and the translation
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
inverse_rotation = rotation.T
|
|
50
|
+
inverse_translation = -translation
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
# Invert the rotation and the translation
|
|
53
|
-
Rinv = R.T
|
|
54
|
-
tinv = -t
|
|
55
|
-
#
|
|
56
|
-
# The inverse affine is composed from R^-1 for the rotation and -(R^-1 . t) for the translation
|
|
57
|
-
result = np.identity(4)
|
|
58
|
-
result[:3, :3] = Rinv
|
|
59
|
-
result[:3, 3] = np.dot(Rinv, tinv)
|
|
52
|
+
# The inverse affine matrix is composed of
|
|
53
|
+
# - Rotation: R^-1
|
|
54
|
+
# - Translation: -(R^-1 . t)
|
|
55
|
+
|
|
56
|
+
result = np.identity(4)
|
|
57
|
+
result[:3, :3] = inverse_rotation
|
|
58
|
+
result[:3, 3] = np.dot(inverse_rotation, inverse_translation)
|
|
60
59
|
|
|
61
|
-
if affine_isEuclidian(result):
|
|
62
60
|
return result
|
|
61
|
+
|
|
63
62
|
else:
|
|
64
63
|
print("WARNING: This is not a rigid-body transformation matrix")
|
|
65
64
|
# print(f"R.T-based (.6f) = \n {np.round(result,6)}")
|
|
@@ -67,9 +66,12 @@ def affine_inverse(matrix):
|
|
|
67
66
|
return np.linalg.inv(matrix)
|
|
68
67
|
|
|
69
68
|
|
|
70
|
-
def affine_matrix_from_points(
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
def affine_matrix_from_points(
|
|
70
|
+
v0: np.ndarray, v1: np.ndarray, shear: bool = False, scale: bool = False, use_svd: bool = True
|
|
71
|
+
) -> np.ndarray:
|
|
72
|
+
"""Computes the homogeneous affine transform matrix that best maps one set of points to another.
|
|
73
|
+
|
|
74
|
+
Returns affine transform matrix to register two point sets.
|
|
73
75
|
|
|
74
76
|
v0 and v1 are shape (ndims, \*) arrays of at least ndims non-homogeneous
|
|
75
77
|
coordinates, where ndims is the dimensionality of the coordinate space.
|
|
@@ -78,7 +80,7 @@ def affine_matrix_from_points(v0, v1, shear=False, scale=False, usesvd=True):
|
|
|
78
80
|
If also scale is False, a rigid/Euclidean transformation matrix
|
|
79
81
|
is returned.
|
|
80
82
|
|
|
81
|
-
By default the algorithm by Hartley and Zissermann [15] is used.
|
|
83
|
+
By default, the algorithm by Hartley and Zissermann [15] is used.
|
|
82
84
|
If usesvd is True, similarity and Euclidean transformation matrices
|
|
83
85
|
are calculated by minimizing the weighted sum of squared deviations
|
|
84
86
|
(RMSD) according to the algorithm by Kabsch [8].
|
|
@@ -94,76 +96,89 @@ def affine_matrix_from_points(v0, v1, shear=False, scale=False, usesvd=True):
|
|
|
94
96
|
array([[ 0.14549, 0.00062, 675.50008],
|
|
95
97
|
[ 0.00048, 0.14094, 53.24971],
|
|
96
98
|
[ 0. , 0. , 1. ]])
|
|
97
|
-
>>> T = translation_matrix(
|
|
98
|
-
>>> R = random_rotation_matrix(
|
|
99
|
+
>>> T = translation_matrix(np.random.random(3)-0.5)
|
|
100
|
+
>>> R = random_rotation_matrix(np.random.random(3))
|
|
99
101
|
>>> S = scale_matrix(random.random())
|
|
100
102
|
>>> M = concatenate_matrices(T, R, S)
|
|
101
|
-
>>> v0 = (
|
|
103
|
+
>>> v0 = (np.random.rand(4, 100) - 0.5) * 20
|
|
102
104
|
>>> v0[3] = 1
|
|
103
|
-
>>> v1 =
|
|
104
|
-
>>> v0[:3] +=
|
|
105
|
+
>>> v1 = np.dot(M, v0)
|
|
106
|
+
>>> v0[:3] += np.random.normal(0, 1e-8, 300).reshape(3, -1)
|
|
105
107
|
>>> M = affine_matrix_from_points(v0[:3], v1[:3])
|
|
106
|
-
>>>
|
|
108
|
+
>>> np.allclose(v1, np.dot(M, v0))
|
|
107
109
|
True
|
|
108
110
|
|
|
109
111
|
More examples in superimposition_matrix()
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
|
|
113
|
+
References: This function was extracted from the original transformations.py written by Christoph Golke:
|
|
114
|
+
https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
Args:
|
|
117
|
+
v0 (np.ndarray): Set of points to transform.
|
|
118
|
+
v1 (np.ndarray): Set of points to transform to.
|
|
119
|
+
shear (bool): Indicates whether a full affine transform is allowed (i.e. incl. shear).
|
|
120
|
+
scale (bool): Indicates whether a full affine transform is allowed (i.e. incl. zoom).
|
|
121
|
+
use_svd (bool): Indicates whether to use SVD-base solution (Singular Value Decomposition) for
|
|
122
|
+
rigid/similarity transformations. If False and ndims = 3, the quaternion-based solution is used.
|
|
117
123
|
|
|
124
|
+
Returns:
|
|
125
|
+
Best affine transformation matrix to map `v0` to `v1`.
|
|
118
126
|
"""
|
|
119
|
-
import numpy
|
|
120
127
|
|
|
121
|
-
v0 =
|
|
122
|
-
v1 =
|
|
128
|
+
v0 = np.array(v0, dtype=np.float64, copy=True)
|
|
129
|
+
v1 = np.array(v1, dtype=np.float64, copy=True)
|
|
123
130
|
|
|
124
|
-
|
|
125
|
-
if
|
|
126
|
-
print(
|
|
131
|
+
num_dimensions = v0.shape[0]
|
|
132
|
+
if num_dimensions < 2 or v0.shape[1] < num_dimensions or v0.shape != v1.shape:
|
|
133
|
+
print(
|
|
134
|
+
f"num_dimensions {num_dimensions} v0/1.shape {v0.shape} {v1.shape} v0/1 class {v0.__class__} {v1.__class__}"
|
|
135
|
+
)
|
|
127
136
|
raise ValueError("input arrays are of wrong shape or type")
|
|
128
137
|
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
# First set of coordinates
|
|
139
|
+
|
|
140
|
+
t0 = -np.mean(v0, axis=1) # Move centroids to origin
|
|
141
|
+
v0 += t0.reshape(num_dimensions, 1)
|
|
142
|
+
matrix_0 = np.identity(num_dimensions + 1)
|
|
143
|
+
matrix_0[:num_dimensions, num_dimensions] = t0 # (I | t0)
|
|
144
|
+
|
|
145
|
+
# Second set of coordinates
|
|
146
|
+
|
|
147
|
+
t1 = -np.mean(v1, axis=1) # Move centroids to origin
|
|
148
|
+
v1 += t1.reshape(num_dimensions, 1)
|
|
149
|
+
matrix_1 = np.identity(num_dimensions + 1)
|
|
150
|
+
matrix_1[:num_dimensions, num_dimensions] = t1 # (I | t1)
|
|
138
151
|
|
|
139
152
|
if shear:
|
|
140
153
|
# Affine transformation
|
|
141
|
-
A =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
B =
|
|
145
|
-
C =
|
|
146
|
-
t =
|
|
147
|
-
t =
|
|
148
|
-
M =
|
|
149
|
-
|
|
154
|
+
A = np.concatenate((v0, v1), axis=0)
|
|
155
|
+
svd_u, svd_s, svd_vh = np.linalg.svd(A.T) # Singular Value Decomposition -> U, S, Vh
|
|
156
|
+
svd_vh = svd_vh[:num_dimensions].T
|
|
157
|
+
B = svd_vh[:num_dimensions]
|
|
158
|
+
C = svd_vh[num_dimensions : 2 * num_dimensions]
|
|
159
|
+
t = np.dot(C, np.linalg.pinv(B))
|
|
160
|
+
t = np.concatenate((t, np.zeros((num_dimensions, 1))), axis=1)
|
|
161
|
+
M = np.vstack((t, ((0.0,) * num_dimensions) + (1.0,)))
|
|
162
|
+
|
|
163
|
+
elif use_svd or num_dimensions != 3:
|
|
150
164
|
# Rigid transformation via SVD of covariance matrix
|
|
151
|
-
|
|
165
|
+
svd_u, svd_s, svd_vh = np.linalg.svd(np.dot(v1, v0.T))
|
|
152
166
|
# rotation matrix from SVD orthonormal bases
|
|
153
|
-
R =
|
|
154
|
-
if
|
|
155
|
-
# R does not constitute right
|
|
156
|
-
R -=
|
|
157
|
-
|
|
167
|
+
R = np.dot(svd_u, svd_vh)
|
|
168
|
+
if np.linalg.det(R) < 0.0:
|
|
169
|
+
# R does not constitute right-handed system
|
|
170
|
+
R -= np.outer(svd_u[:, num_dimensions - 1], svd_vh[num_dimensions - 1, :] * 2.0)
|
|
171
|
+
svd_s[-1] *= -1.0
|
|
158
172
|
# homogeneous transformation matrix
|
|
159
|
-
M =
|
|
160
|
-
M[:
|
|
173
|
+
M = np.identity(num_dimensions + 1)
|
|
174
|
+
M[:num_dimensions, :num_dimensions] = R
|
|
175
|
+
|
|
161
176
|
else:
|
|
162
177
|
# Rigid transformation matrix via quaternion
|
|
163
178
|
# compute symmetric matrix N
|
|
164
|
-
xx, yy, zz =
|
|
165
|
-
xy, yz, zx =
|
|
166
|
-
xz, yx, zy =
|
|
179
|
+
xx, yy, zz = np.sum(v0 * v1, axis=1)
|
|
180
|
+
xy, yz, zx = np.sum(v0 * np.roll(v1, -1, axis=0), axis=1)
|
|
181
|
+
xz, yx, zy = np.sum(v0 * np.roll(v1, -2, axis=0), axis=1)
|
|
167
182
|
N = [
|
|
168
183
|
[xx + yy + zz, 0.0, 0.0, 0.0],
|
|
169
184
|
[yz - zy, xx - yy - zz, 0.0, 0.0],
|
|
@@ -171,42 +186,43 @@ def affine_matrix_from_points(v0, v1, shear=False, scale=False, usesvd=True):
|
|
|
171
186
|
[xy - yx, zx + xz, yz + zy, zz - xx - yy],
|
|
172
187
|
]
|
|
173
188
|
# quaternion: eigenvector corresponding to most positive eigenvalue
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
#
|
|
178
|
-
M =
|
|
189
|
+
eigenvalues, eigenvectors = np.linalg.eigh(N)
|
|
190
|
+
quaternion = eigenvectors[:, np.argmax(eigenvalues)]
|
|
191
|
+
quaternion /= _vector_norm(quaternion) # Normalised quaternion
|
|
192
|
+
# Quaternion -> Homogeneous rotation matrix
|
|
193
|
+
M = quaternion_matrix(quaternion)
|
|
179
194
|
|
|
180
195
|
if scale and not shear:
|
|
181
196
|
# Affine transformation; scale is ratio of RMS deviations from centroid
|
|
182
197
|
v0 *= v0
|
|
183
198
|
v1 *= v1
|
|
184
|
-
M[:
|
|
199
|
+
M[:num_dimensions, :num_dimensions] *= math.sqrt(np.sum(v1) / np.sum(v0))
|
|
200
|
+
|
|
201
|
+
# Move centroids back
|
|
202
|
+
M = np.dot(np.linalg.inv(matrix_1), np.dot(M, matrix_0))
|
|
203
|
+
M /= M[num_dimensions, num_dimensions]
|
|
185
204
|
|
|
186
|
-
# move centroids back
|
|
187
|
-
M = numpy.dot(numpy.linalg.inv(M1), numpy.dot(M, M0))
|
|
188
|
-
M /= M[ndims, ndims]
|
|
189
205
|
return M
|
|
190
206
|
|
|
191
207
|
|
|
192
|
-
def _vector_norm(data, axis=None, out=None):
|
|
193
|
-
"""
|
|
208
|
+
def _vector_norm(data: np.ndarray, axis: str | None = None, out=None):
|
|
209
|
+
"""Returns the length, i.e. Euclidean norm, of the given array along the given axis.
|
|
194
210
|
|
|
195
|
-
>>> v =
|
|
211
|
+
>>> v = np.random.random(3)
|
|
196
212
|
>>> n = vector_norm(v)
|
|
197
|
-
>>>
|
|
213
|
+
>>> np.allclose(n, np.linalg.norm(v))
|
|
198
214
|
True
|
|
199
|
-
>>> v =
|
|
215
|
+
>>> v = np.random.rand(6, 5, 3)
|
|
200
216
|
>>> n = vector_norm(v, axis=-1)
|
|
201
|
-
>>>
|
|
217
|
+
>>> np.allclose(n, np.sqrt(np.sum(v*v, axis=2)))
|
|
202
218
|
True
|
|
203
219
|
>>> n = vector_norm(v, axis=1)
|
|
204
|
-
>>>
|
|
220
|
+
>>> np.allclose(n, np.sqrt(np.sum(v*v, axis=1)))
|
|
205
221
|
True
|
|
206
|
-
>>> v =
|
|
207
|
-
>>> n =
|
|
222
|
+
>>> v = np.random.rand(5, 4, 3)
|
|
223
|
+
>>> n = np.empty((5, 3))
|
|
208
224
|
>>> vector_norm(v, axis=1, out=n)
|
|
209
|
-
>>>
|
|
225
|
+
>>> np.allclose(n, np.sqrt(np.sum(v*v, axis=1)))
|
|
210
226
|
True
|
|
211
227
|
>>> vector_norm([])
|
|
212
228
|
0.0
|
|
@@ -215,53 +231,63 @@ def _vector_norm(data, axis=None, out=None):
|
|
|
215
231
|
|
|
216
232
|
This function is called by affine_matrix_from_points when usesvd=False
|
|
217
233
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
|
|
234
|
+
References: This function was extracted from the original transformations.py written by Christoph Golke:
|
|
235
|
+
https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
|
|
221
236
|
|
|
222
237
|
"""
|
|
223
|
-
|
|
238
|
+
|
|
239
|
+
data = np.array(data, dtype=np.float64, copy=True)
|
|
240
|
+
|
|
224
241
|
if out is None:
|
|
225
242
|
if data.ndim == 1:
|
|
226
|
-
return math.sqrt(
|
|
243
|
+
return math.sqrt(np.dot(data, data))
|
|
227
244
|
data *= data
|
|
228
|
-
out =
|
|
229
|
-
|
|
245
|
+
out = np.atleast_1d(np.sum(data, axis=axis))
|
|
246
|
+
np.sqrt(out, out)
|
|
247
|
+
|
|
230
248
|
return out
|
|
231
249
|
else:
|
|
232
250
|
data *= data
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
np.sum(data, axis=axis, out=out)
|
|
252
|
+
return np.sqrt(out, out)
|
|
235
253
|
|
|
236
254
|
|
|
237
|
-
def
|
|
238
|
-
"""
|
|
255
|
+
def quaternion_matrix(quaternion: np.ndarray):
|
|
256
|
+
"""Returns the homogeneous rotation matrix from the given quaternion.
|
|
239
257
|
|
|
240
258
|
>>> M = quaternion_matrix([0.99810947, 0.06146124, 0, 0])
|
|
241
|
-
>>>
|
|
259
|
+
>>> np.allclose(M, rotation_matrix(0.123, [1, 0, 0]))
|
|
242
260
|
True
|
|
243
261
|
>>> M = quaternion_matrix([1, 0, 0, 0])
|
|
244
|
-
>>>
|
|
262
|
+
>>> np.allclose(M, np.identity(4))
|
|
245
263
|
True
|
|
246
264
|
>>> M = quaternion_matrix([0, 1, 0, 0])
|
|
247
|
-
>>>
|
|
265
|
+
>>> np.allclose(M, np.diag([1, -1, -1, 1]))
|
|
248
266
|
True
|
|
249
267
|
|
|
250
268
|
This function is called by affine_matrix_from_points when usesvd=False
|
|
251
269
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
270
|
+
References: This function was extracted from the original transformations.py written by Christoph Golke:
|
|
271
|
+
https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
quaternion (np.ndarray): Quaternion to convert to a rotation matrix.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Homogeneous rotation matrix corresponding to the given quaternion.
|
|
255
278
|
"""
|
|
279
|
+
|
|
256
280
|
_EPS = np.finfo(float).eps * 5
|
|
257
281
|
|
|
258
|
-
q =
|
|
259
|
-
n =
|
|
282
|
+
q = np.array(quaternion, dtype=np.float64, copy=True)
|
|
283
|
+
n = np.dot(q, q)
|
|
284
|
+
|
|
260
285
|
if n < _EPS:
|
|
261
|
-
return
|
|
286
|
+
return np.identity(4)
|
|
287
|
+
|
|
262
288
|
q *= math.sqrt(2.0 / n)
|
|
263
|
-
q =
|
|
264
|
-
return
|
|
289
|
+
q = np.outer(q, q)
|
|
290
|
+
return np.array(
|
|
265
291
|
[
|
|
266
292
|
[1.0 - q[2, 2] - q[3, 3], q[1, 2] - q[3, 0], q[1, 3] + q[2, 0], 0.0],
|
|
267
293
|
[q[1, 2] + q[3, 0], 1.0 - q[1, 1] - q[3, 3], q[2, 3] - q[1, 0], 0.0],
|
|
@@ -271,167 +297,205 @@ def _quaternion_matrix(quaternion):
|
|
|
271
297
|
)
|
|
272
298
|
|
|
273
299
|
|
|
274
|
-
def
|
|
275
|
-
"""
|
|
276
|
-
|
|
277
|
-
INPUT
|
|
278
|
-
Afrom, Bto 3xn arrays = xyz coords of n points to be registered
|
|
300
|
+
def rigid_transform_3d(dataset_a: np.ndarray, dataset_b: np.ndarray):
|
|
301
|
+
"""Returns best translation and rotation to align points in dataset A to points in dataset B.
|
|
279
302
|
|
|
280
|
-
|
|
281
|
-
|
|
303
|
+
Args:
|
|
304
|
+
dataset_a (np.ndarray): First 3xn dataset of points (dataset A).
|
|
305
|
+
dataset_b (np.ndarray): Second 3xn dataset of points (dataset B).
|
|
282
306
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
307
|
+
References: Nghia Ho - 2013 - http://nghiaho.com/?page_id=671
|
|
308
|
+
"Finding optimal rotation and translation between corresponding 3D points"
|
|
309
|
+
Based on "A Method for Registration of 3-D Shapes", by Besl and McKay, 1992.
|
|
286
310
|
|
|
287
311
|
This is based on Singular Value Decomposition (SVD)
|
|
288
|
-
|
|
312
|
+
-> It is equivalent to affine_matrix_from_points with parameter use_svd=True
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Transformation matrix to align points in dataset A to points in dataset B.
|
|
289
316
|
"""
|
|
290
|
-
A = fromA.T
|
|
291
|
-
B = toB.T
|
|
292
317
|
|
|
293
|
-
|
|
318
|
+
dataset_a_transposed = dataset_a.T
|
|
319
|
+
dataset_b_transposed = dataset_b.T
|
|
294
320
|
|
|
295
|
-
|
|
321
|
+
assert len(dataset_a_transposed) == len(dataset_b_transposed)
|
|
296
322
|
|
|
297
|
-
|
|
298
|
-
centroid_B = np.mean(B, axis=0)
|
|
323
|
+
num_points = dataset_a_transposed.shape[0] # Total points
|
|
299
324
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
325
|
+
centroid_a = np.mean(dataset_a_transposed, axis=0)
|
|
326
|
+
centroid_b = np.mean(dataset_b_transposed, axis=0)
|
|
327
|
+
|
|
328
|
+
# Centre the points
|
|
329
|
+
a_centered = dataset_a_transposed - np.tile(centroid_a, (num_points, 1))
|
|
330
|
+
b_centered = dataset_b_transposed - np.tile(centroid_b, (num_points, 1))
|
|
303
331
|
|
|
304
332
|
# @ is matrix multiplication for array
|
|
305
|
-
|
|
333
|
+
covariance_matrix = np.transpose(a_centered) @ b_centered # Covariance matrix H
|
|
334
|
+
|
|
335
|
+
svd_u, svd_s, svd_vh = np.linalg.svd(covariance_matrix) # SVD(H) = [U, S, V]
|
|
306
336
|
|
|
307
|
-
|
|
337
|
+
rotation = svd_vh.T @ svd_u.T # Rotation matrix R
|
|
308
338
|
|
|
309
|
-
|
|
339
|
+
# Special reflection case
|
|
340
|
+
# There’s a special case when finding the rotation matrix that you have to take care of. Sometimes the SVD will
|
|
341
|
+
# return a "reflection" matrix, which is numerically correct but is actually nonsense in real life. This is
|
|
342
|
+
# addressed by checking the determinant of R (from SVD above) and seeing if it’s negative (-1). If it is then the
|
|
343
|
+
# 3rd column of V is multiplied by -1.
|
|
310
344
|
|
|
311
|
-
|
|
312
|
-
if np.linalg.det(R) < 0:
|
|
345
|
+
if np.linalg.det(rotation) < 0:
|
|
313
346
|
print("Reflection detected")
|
|
314
|
-
|
|
315
|
-
|
|
347
|
+
svd_vh[2, :] *= -1
|
|
348
|
+
rotation = svd_vh.T @ svd_u.T
|
|
316
349
|
|
|
317
|
-
|
|
350
|
+
translation = -rotation @ centroid_a.T + centroid_b.T
|
|
318
351
|
|
|
319
352
|
result = np.identity(4)
|
|
320
|
-
result[:3, :3] =
|
|
321
|
-
result[:3, 3] =
|
|
353
|
+
result[:3, :3] = rotation
|
|
354
|
+
result[:3, 3] = translation
|
|
322
355
|
|
|
323
356
|
return result
|
|
324
357
|
|
|
325
358
|
|
|
326
|
-
def
|
|
327
|
-
translation
|
|
359
|
+
def translation_rotation_to_transformation(
|
|
360
|
+
translation: np.ndarray,
|
|
361
|
+
rotation: np.ndarray,
|
|
362
|
+
rotation_config: str = "sxyz",
|
|
363
|
+
active: bool = True,
|
|
364
|
+
degrees: bool = True,
|
|
365
|
+
translation_first: bool = False,
|
|
328
366
|
):
|
|
367
|
+
"""Returns the transformation matrix from the given translation and rotation.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
translation (np.ndarray): Translation vector.
|
|
371
|
+
rotation (np.ndarray): Rotation vector.
|
|
372
|
+
rotation_config (str): Order in which the rotation about the three axes are chained.
|
|
373
|
+
active (bool): Indicates if the rotation is active (object rotates IN a fixed coordinate system) or passive
|
|
374
|
+
(coordinate system rotates AROUND a fixed object). Even if two angles are zero, the match
|
|
375
|
+
between angle orders and rot_config is still critical
|
|
376
|
+
degrees (bool): Indicates whether the rotation angles are specified in degrees, rather than radians.
|
|
377
|
+
translation_first (bool): Indicates the order of the translation and rotation in the transformation matrix.
|
|
378
|
+
False if the first three rows of the transformation matrix are (R t). This is the
|
|
379
|
+
usual convention.
|
|
380
|
+
True if the first three rows of the transformation matrix are (R Rt). This is used
|
|
381
|
+
in the hexapod.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Transformation matrix corresponding to the given translation and rotation.
|
|
329
385
|
"""
|
|
330
|
-
translationRotationToTransformation(translation,rotation,rot_config="sxyz",active=True,degrees=True,translationFirst=False)
|
|
331
386
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
True first 3 rows of transformation matrix = (R Rt) [used in the hexapod]
|
|
335
|
-
"""
|
|
336
|
-
import transforms3d as t3
|
|
337
|
-
import numpy as np
|
|
338
|
-
|
|
339
|
-
# Zoom - unit
|
|
340
|
-
zdef = np.array([1, 1, 1])
|
|
341
|
-
# Shear
|
|
342
|
-
sdef = np.array([0, 0, 0])
|
|
387
|
+
zoom = np.array([1, 1, 1])
|
|
388
|
+
shear = np.array([0, 0, 0])
|
|
343
389
|
translation = np.array(translation)
|
|
344
|
-
# if degrees: rotation = np.deg2rad(np.array(rotation))
|
|
345
390
|
if degrees:
|
|
346
391
|
rotation = np.array([np.deg2rad(item) for item in rotation])
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if
|
|
392
|
+
rotation_x, rotation_y, rotation_z = rotation
|
|
393
|
+
rotation_matrix = RotationMatrix(rotation_x, rotation_y, rotation_z, rotation_config=rotation_config, active=active)
|
|
394
|
+
|
|
395
|
+
if translation_first:
|
|
351
396
|
result = np.identity(4)
|
|
352
|
-
result[:3, :3] =
|
|
353
|
-
result[:3, 3] =
|
|
397
|
+
result[:3, :3] = rotation_matrix.rotation_matrix
|
|
398
|
+
result[:3, 3] = rotation_matrix.rotation_matrix @ translation
|
|
354
399
|
else:
|
|
355
|
-
result = t3.affines.compose(translation,
|
|
400
|
+
result = t3.affines.compose(translation, rotation_matrix.rotation_matrix, Z=zoom, S=shear)
|
|
401
|
+
|
|
356
402
|
return result
|
|
357
403
|
|
|
358
404
|
|
|
359
|
-
def
|
|
360
|
-
transformation
|
|
405
|
+
def translation_rotation_from_transformation(
|
|
406
|
+
transformation: np.ndarray,
|
|
407
|
+
rotation_config: str = "sxyz",
|
|
408
|
+
active: bool = True,
|
|
409
|
+
degrees: bool = True,
|
|
410
|
+
translation_first: bool = False,
|
|
361
411
|
):
|
|
412
|
+
"""Extracts the translation and rotation vector from the given transformation matrix.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
transformation (np.ndarray): Transformation matrix.
|
|
416
|
+
rotation_config (str): Order in which the rotation about the three axes are chained.
|
|
417
|
+
active (bool): Indicates if the rotation is active (object rotates IN a fixed coordinate system) or passive
|
|
418
|
+
(coordinate system rotates AROUND a fixed object). Even if two angles are zero, the match
|
|
419
|
+
between angle orders and rot_config is still critical
|
|
420
|
+
degrees (bool): Indicates whether the rotation angles are specified in degrees, rather than radians.
|
|
421
|
+
translation_first (bool): Indicates the order of the translation and rotation in the transformation matrix.
|
|
422
|
+
False if the first three rows of the transformation matrix are (R t). This is the
|
|
423
|
+
usual convention.
|
|
424
|
+
True if the first three rows of the transformation matrix are (R Rt). This is used
|
|
425
|
+
in the hexapod.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Translation and rotation vector for the given transformation matrix.
|
|
362
429
|
"""
|
|
363
|
-
translationRotationFromTransformation(transformation,rot_config="sxyz",active=True,degrees=True,translationFirst=False)
|
|
364
430
|
|
|
365
|
-
translationFirst : translation first
|
|
366
|
-
False first 3 rows of transformation matrix = (R t) [usual convention and default here]
|
|
367
|
-
True first 3 rows of transformation matrix = (R Rt) [used in the hexapod]
|
|
368
|
-
"""
|
|
369
431
|
translation = transformation[:3, 3]
|
|
370
|
-
rotation = t3.euler.mat2euler(transformation, axes=
|
|
432
|
+
rotation = t3.euler.mat2euler(transformation, axes=rotation_config)
|
|
371
433
|
if degrees:
|
|
372
434
|
rotation = np.array([np.rad2deg(item) for item in rotation])
|
|
373
|
-
if
|
|
435
|
+
if translation_first:
|
|
374
436
|
translation = transformation[:3, :3].T @ translation
|
|
437
|
+
|
|
375
438
|
return translation, rotation
|
|
376
439
|
|
|
377
440
|
|
|
378
|
-
|
|
379
|
-
|
|
441
|
+
tr2t = translation_rotation_to_transformation
|
|
442
|
+
t2tr = translation_rotation_from_transformation
|
|
380
443
|
|
|
381
444
|
|
|
382
|
-
def
|
|
383
|
-
"""
|
|
384
|
-
|
|
445
|
+
def vector_plane_intersection(vector, frame, epsilon=1.0e-6):
|
|
446
|
+
"""Returns the coordinates of the insection of a vector with a plane.
|
|
447
|
+
|
|
448
|
+
The origin of the input vector is:
|
|
449
|
+
vector.reference_frame.get_origin().coordinates[:3]
|
|
450
|
+
|
|
451
|
+
The direction of the input vector is:
|
|
452
|
+
vector.coordinates[:3]
|
|
385
453
|
|
|
386
|
-
|
|
387
|
-
vector origin = pt.ref.getOrigin().coordinates[:3]
|
|
388
|
-
vector direction = pt.coordinates[:3]
|
|
389
|
-
frame = input plane. ReferenceFrame object whose x-y plane is the target plane for intersection
|
|
454
|
+
In all cases, the coordinates of the intersection point are provided as a Point object, in "frame" coordinates
|
|
390
455
|
|
|
391
|
-
|
|
456
|
+
Args:
|
|
457
|
+
vector (Vector): Input vector.
|
|
458
|
+
frame (ReferenceFrame): Reference frame for which the xy-plane is the target plane for intersection.
|
|
459
|
+
epsilon (float):
|
|
392
460
|
|
|
393
|
-
In all cases, the coordinates of the interesection point are provided as a Point object, in "frame" coordinates
|
|
394
461
|
|
|
395
|
-
|
|
396
|
-
|
|
462
|
+
References:
|
|
463
|
+
https://stackoverflow.com/questions/5666222/3d-line-plane-intersection
|
|
397
464
|
"""
|
|
398
465
|
|
|
399
466
|
from egse.coordinates.point import Point
|
|
400
467
|
|
|
401
|
-
if
|
|
468
|
+
if vector.reference_frame == frame:
|
|
402
469
|
# The point is defined in frame => the origin of the vector is the origin of the target plane.
|
|
403
470
|
return np.array([0, 0, 0])
|
|
404
471
|
else:
|
|
405
|
-
# Express all inputs in
|
|
472
|
+
# Express all inputs in the given reference frame
|
|
406
473
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
# Vector
|
|
410
|
-
|
|
411
|
-
# Vector (u)
|
|
412
|
-
vec = vec_end - vec_orig
|
|
474
|
+
vector_origin = Point(
|
|
475
|
+
vector.reference_frame.get_origin().coordinates[:3], reference_frame=vector.reference_frame, name="ptorig"
|
|
476
|
+
).express_in(frame)[:3] # Vector Origin (p0)
|
|
477
|
+
vector_end = vector.express_in(frame)[:3] # Vector end (p1)
|
|
413
478
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
# Normal to the plane (
|
|
418
|
-
plane_normal = frame.getAxis("z").coordinates[:3]
|
|
479
|
+
vector_u = vector_end - vector_origin # Vector (u)
|
|
480
|
+
|
|
481
|
+
plane_origin = frame.get_origin().coordinates[:3] # Origin of the reference frame (p_co)
|
|
482
|
+
plane_normal = frame.get_axis("z").coordinates[:3] # Normal to the plane (p_no)
|
|
419
483
|
|
|
420
484
|
# Vector to normal 'angle'
|
|
421
|
-
|
|
485
|
+
dot = np.dot(vector_u, plane_normal) # dot = p_no * u
|
|
422
486
|
|
|
423
487
|
# Test if there is an intersection (and if it's unique)
|
|
424
|
-
#
|
|
425
|
-
|
|
426
|
-
if np.allclose(
|
|
488
|
+
# -> input vector and normal mustn't be perpendicular, else the vector is // to the plane or inside it
|
|
489
|
+
|
|
490
|
+
if np.allclose(dot, 0.0, atol=epsilon):
|
|
427
491
|
print("The input vector is // to the plane normal (or inside the plane)")
|
|
428
|
-
print("
|
|
492
|
+
print("-> There exists no intersection (or an infinity of them)")
|
|
429
493
|
return None
|
|
430
494
|
else:
|
|
431
495
|
# Vector from the point in the plane to the origin of the vector (w)
|
|
432
|
-
|
|
496
|
+
plane_to_vector = vector_origin - plane_origin # w = p0 - p_co
|
|
433
497
|
|
|
434
|
-
# Solution ("how many 'vectors' away is the
|
|
435
|
-
|
|
498
|
+
# Solution ("how many 'vectors' away is the intersection ?")
|
|
499
|
+
factor = -np.dot(plane_normal, plane_to_vector) / dot # fac = -(plane * w) / fac
|
|
436
500
|
|
|
437
|
-
return Point(
|
|
501
|
+
return Point(vector_origin + (vector_u * factor), reference_frame=frame)
|