cgse-coordinates 0.17.2__py3-none-any.whl → 0.17.4__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.
@@ -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.rotationMatrix import RotationMatrix
5
+ from egse.coordinates.rotation_matrix import RotationMatrix
15
6
 
16
7
 
17
- def affine_isEuclidian(matrix):
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
- . the rotation part is orthogonal : R @ R.T = I
23
- . the det(R) = 1 (=> this is not a reflexion)
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
- WARNING:
34
- This is NOT a generic inversion of an affine transformation matrix
41
+ if affine_is_euclidian(matrix):
42
+ # Separate the given augmented matrix into rotation and translation
35
43
 
36
- This returns the affine transformation inverting that produced by the input matrix,
44
+ rotation = matrix[:3, :3]
45
+ translation = matrix[:3, 3]
37
46
 
38
- ASSSUMING that only rotation and translation were involved
39
- in the affine transformation, no zoom, no shear!
47
+ # Invert the rotation and the translation
40
48
 
41
- That preserves the fact that the orthogonal property of the part of the input matrix
42
- corresponding to rotation => the inverse is simply the transpose.
49
+ inverse_rotation = rotation.T
50
+ inverse_translation = -translation
43
51
 
44
- Pierre Royer
45
- """
46
- # import numpy as np
47
-
48
- # Extract Rotation matrix and translation vector from input affine transformation
49
- R = matrix[:3, :3]
50
- t = matrix[:3, 3]
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(v0, v1, shear=False, scale=False, usesvd=True):
71
- """affine_matrix_from_points(v0, v1, shear=False, scale=False, usesvd=True)
72
- Return affine transform matrix to register two point sets.
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(numpy.random.random(3)-0.5)
98
- >>> R = random_rotation_matrix(numpy.random.random(3))
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 = (numpy.random.rand(4, 100) - 0.5) * 20
103
+ >>> v0 = (np.random.rand(4, 100) - 0.5) * 20
102
104
  >>> v0[3] = 1
103
- >>> v1 = numpy.dot(M, v0)
104
- >>> v0[:3] += numpy.random.normal(0, 1e-8, 300).reshape(3, -1)
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
- >>> numpy.allclose(v1, numpy.dot(M, v0))
108
+ >>> np.allclose(v1, np.dot(M, v0))
107
109
  True
108
110
 
109
111
  More examples in superimposition_matrix()
110
112
 
111
- Author: this function was extracted from the original transformations.py
112
- written by Christoph Golke:
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
- usesvd controls the use of a method based on Singular Value Decomposition (SVD)
116
- --> when True, it is equivalent to rigid_transform_3D (see below)
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 = numpy.array(v0, dtype=numpy.float64, copy=True)
122
- v1 = numpy.array(v1, dtype=numpy.float64, copy=True)
128
+ v0 = np.array(v0, dtype=np.float64, copy=True)
129
+ v1 = np.array(v1, dtype=np.float64, copy=True)
123
130
 
124
- ndims = v0.shape[0]
125
- if ndims < 2 or v0.shape[1] < ndims or v0.shape != v1.shape:
126
- print(f"ndims {ndims} v0/1.shape {v0.shape} {v1.shape} v0/1 class {v0.__class__} {v1.__class__}")
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
- # move centroids to origin
130
- t0 = -numpy.mean(v0, axis=1)
131
- M0 = numpy.identity(ndims + 1)
132
- M0[:ndims, ndims] = t0
133
- v0 += t0.reshape(ndims, 1)
134
- t1 = -numpy.mean(v1, axis=1)
135
- M1 = numpy.identity(ndims + 1)
136
- M1[:ndims, ndims] = t1
137
- v1 += t1.reshape(ndims, 1)
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 = numpy.concatenate((v0, v1), axis=0)
142
- u, s, vh = numpy.linalg.svd(A.T)
143
- vh = vh[:ndims].T
144
- B = vh[:ndims]
145
- C = vh[ndims : 2 * ndims]
146
- t = numpy.dot(C, numpy.linalg.pinv(B))
147
- t = numpy.concatenate((t, numpy.zeros((ndims, 1))), axis=1)
148
- M = numpy.vstack((t, ((0.0,) * ndims) + (1.0,)))
149
- elif usesvd or ndims != 3:
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
- u, s, vh = numpy.linalg.svd(numpy.dot(v1, v0.T))
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 = numpy.dot(u, vh)
154
- if numpy.linalg.det(R) < 0.0:
155
- # R does not constitute right handed system
156
- R -= numpy.outer(u[:, ndims - 1], vh[ndims - 1, :] * 2.0)
157
- s[-1] *= -1.0
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 = numpy.identity(ndims + 1)
160
- M[:ndims, :ndims] = R
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 = numpy.sum(v0 * v1, axis=1)
165
- xy, yz, zx = numpy.sum(v0 * numpy.roll(v1, -1, axis=0), axis=1)
166
- xz, yx, zy = numpy.sum(v0 * numpy.roll(v1, -2, axis=0), axis=1)
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
- w, V = numpy.linalg.eigh(N)
175
- q = V[:, numpy.argmax(w)]
176
- q /= _vector_norm(q) # unit quaternion
177
- # homogeneous transformation matrix
178
- M = _quaternion_matrix(q)
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[:ndims, :ndims] *= math.sqrt(numpy.sum(v1) / numpy.sum(v0))
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
- """Return length, i.e. Euclidean norm, of ndarray along axis.
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 = numpy.random.random(3)
211
+ >>> v = np.random.random(3)
196
212
  >>> n = vector_norm(v)
197
- >>> numpy.allclose(n, numpy.linalg.norm(v))
213
+ >>> np.allclose(n, np.linalg.norm(v))
198
214
  True
199
- >>> v = numpy.random.rand(6, 5, 3)
215
+ >>> v = np.random.rand(6, 5, 3)
200
216
  >>> n = vector_norm(v, axis=-1)
201
- >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=2)))
217
+ >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=2)))
202
218
  True
203
219
  >>> n = vector_norm(v, axis=1)
204
- >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
220
+ >>> np.allclose(n, np.sqrt(np.sum(v*v, axis=1)))
205
221
  True
206
- >>> v = numpy.random.rand(5, 4, 3)
207
- >>> n = numpy.empty((5, 3))
222
+ >>> v = np.random.rand(5, 4, 3)
223
+ >>> n = np.empty((5, 3))
208
224
  >>> vector_norm(v, axis=1, out=n)
209
- >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
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
- Author: this function was extracted from the original transformations.py
219
- written by Christoph Golke:
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
- data = numpy.array(data, dtype=numpy.float64, copy=True)
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(numpy.dot(data, data))
243
+ return math.sqrt(np.dot(data, data))
227
244
  data *= data
228
- out = numpy.atleast_1d(numpy.sum(data, axis=axis))
229
- numpy.sqrt(out, out)
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
- numpy.sum(data, axis=axis, out=out)
234
- numpy.sqrt(out, out)
251
+ np.sum(data, axis=axis, out=out)
252
+ return np.sqrt(out, out)
235
253
 
236
254
 
237
- def _quaternion_matrix(quaternion):
238
- """Return homogeneous rotation matrix from quaternion.
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
- >>> numpy.allclose(M, rotation_matrix(0.123, [1, 0, 0]))
259
+ >>> np.allclose(M, rotation_matrix(0.123, [1, 0, 0]))
242
260
  True
243
261
  >>> M = quaternion_matrix([1, 0, 0, 0])
244
- >>> numpy.allclose(M, numpy.identity(4))
262
+ >>> np.allclose(M, np.identity(4))
245
263
  True
246
264
  >>> M = quaternion_matrix([0, 1, 0, 0])
247
- >>> numpy.allclose(M, numpy.diag([1, -1, -1, 1]))
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
- Author: this function was extracted from the original transformations.py
253
- written by Christoph Golke:
254
- https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
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 = numpy.array(quaternion, dtype=numpy.float64, copy=True)
259
- n = numpy.dot(q, q)
282
+ q = np.array(quaternion, dtype=np.float64, copy=True)
283
+ n = np.dot(q, q)
284
+
260
285
  if n < _EPS:
261
- return numpy.identity(4)
286
+ return np.identity(4)
287
+
262
288
  q *= math.sqrt(2.0 / n)
263
- q = numpy.outer(q, q)
264
- return numpy.array(
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 rigid_transform_3D(fromA, toB, verbose=True):
275
- """rigid_transform_3D(fromA, toB, verbose=True)
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
- OUTPUT
281
- Rotation + translation transformation matrix registering fromA into toB
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
- Author : Nghia Ho - 2013 - http://nghiaho.com/?page_id=671
284
- "Finding optimal rotation and translation between corresponding 3D points"
285
- Based on "A Method for Registration of 3-D Shapes", by Besl and McKay, 1992.
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
- --> it is equivalent to affine_matrix_from_points with parameter usesvd=True
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
- assert len(A) == len(B)
318
+ dataset_a_transposed = dataset_a.T
319
+ dataset_b_transposed = dataset_b.T
294
320
 
295
- N = A.shape[0] # total points
321
+ assert len(dataset_a_transposed) == len(dataset_b_transposed)
296
322
 
297
- centroid_A = np.mean(A, axis=0)
298
- centroid_B = np.mean(B, axis=0)
323
+ num_points = dataset_a_transposed.shape[0] # Total points
299
324
 
300
- # centre the points
301
- AA = A - np.tile(centroid_A, (N, 1))
302
- BB = B - np.tile(centroid_B, (N, 1))
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
- H = np.transpose(AA) @ BB
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
- U, S, Vt = np.linalg.svd(H)
337
+ rotation = svd_vh.T @ svd_u.T # Rotation matrix R
308
338
 
309
- R = Vt.T @ U.T
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
- # special reflection case
312
- if np.linalg.det(R) < 0:
345
+ if np.linalg.det(rotation) < 0:
313
346
  print("Reflection detected")
314
- Vt[2, :] *= -1
315
- R = Vt.T @ U.T
347
+ svd_vh[2, :] *= -1
348
+ rotation = svd_vh.T @ svd_u.T
316
349
 
317
- t = -R @ centroid_A.T + centroid_B.T
350
+ translation = -rotation @ centroid_a.T + centroid_b.T
318
351
 
319
352
  result = np.identity(4)
320
- result[:3, :3] = R
321
- result[:3, 3] = t
353
+ result[:3, :3] = rotation
354
+ result[:3, 3] = translation
322
355
 
323
356
  return result
324
357
 
325
358
 
326
- def translationRotationToTransformation(
327
- translation, rotation, rot_config="sxyz", active=True, degrees=True, translationFirst=False
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
- translationFirst : translation first
333
- False first 3 rows of transformation matrix = (R t) [usual convention and default here]
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
- rotx, roty, rotz = rotation
348
- rmat = RotationMatrix(rotx, roty, rotz, rot_config=rot_config, active=active)
349
- #
350
- if translationFirst:
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] = rmat.R
353
- result[:3, 3] = rmat.R @ translation
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, rmat.R, Z=zdef, S=sdef)
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 translationRotationFromTransformation(
360
- transformation, rot_config="sxyz", active=True, degrees=True, translationFirst=False
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=rot_config)
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 translationFirst:
435
+ if translation_first:
374
436
  translation = transformation[:3, :3].T @ translation
437
+
375
438
  return translation, rotation
376
439
 
377
440
 
378
- tr2T = translationRotationToTransformation
379
- T2tr = translationRotationFromTransformation
441
+ tr2t = translation_rotation_to_transformation
442
+ t2tr = translation_rotation_from_transformation
380
443
 
381
444
 
382
- def vectorPlaneIntersection(pt, frame, epsilon=1.0e-6):
383
- """
384
- return the coordinates of the intersection of a vector with a plane.
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
- pt = input vector. Point object, expressing the vector
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
- If the vector's own reference frame is 'frame', the problem is trivial
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
- Ref:
396
- https://stackoverflow.com/questions/5666222/3d-line-plane-intersection
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 pt.ref == frame:
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 'frame'
472
+ # Express all inputs in the given reference frame
406
473
 
407
- # Vector Origin (p0)
408
- vec_orig = Point(pt.ref.getOrigin().coordinates[:3], ref=pt.ref, name="ptorig").expressIn(frame)[:3]
409
- # Vector End (p1)
410
- vec_end = pt.expressIn(frame)[:3]
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
- # A point in Plane (pco)
415
- # plane_orig = np.array([0,0,0],dtype=float)
416
- plane_orig = frame.getOrigin().coordinates[:3]
417
- # Normal to the plane (pno)
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
- vec_x_normal = np.dot(vec, plane_normal)
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
- # --> input vector and normal mustn't be perpendicular, else the vector is // to the plane or inside it
425
- #
426
- if np.allclose(vec_x_normal, 0.0, atol=epsilon):
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("--> there exists no intersection (or an infinity of them)")
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
- plane_to_vec = vec_orig - plane_orig
496
+ plane_to_vector = vector_origin - plane_origin # w = p0 - p_co
433
497
 
434
- # Solution ("how many 'vectors' away is the interesection ?")
435
- vec_multiplicator = -np.dot(plane_normal, plane_to_vec) / vec_x_normal
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(vec_orig + (vec * vec_multiplicator), ref=frame)
501
+ return Point(vector_origin + (vector_u * factor), reference_frame=frame)