cgse-coordinates 2023.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.
@@ -0,0 +1,85 @@
1
+ """
2
+ The RotationMatrix provides a number of convenience methods to define and apply rotations.
3
+
4
+ @author: pierre
5
+ """
6
+
7
+ import transforms3d as t3
8
+ import numpy as np
9
+
10
+ class RotationMatrix():
11
+ """
12
+ RotationMatrix(angle1, angle2, angle3, rot_config="sxyz", active=True)
13
+
14
+ The ``angle`` parameters provide the amplitude of rotation around an axis
15
+ according to the order of rotations corresponding to the ``rot_config`` parameter.
16
+
17
+ The ``rot_config`` parameter is a string that consists of four letter
18
+ indicating the rotation system and the order of applying the rotations. The default
19
+ for this parameter is ``"rxyz"`` where the first character can be 'r' or 's' and
20
+ the next three characters define the axes of rotation in the order specified, here 'xyz'.
21
+
22
+ * 'r' stands for rotating system (intrinsic rotations)
23
+ * 's' stands for static system (extrinsic rotations)
24
+ * "rxyz" imposes angle1 describes the rotation around X
25
+ * even if two angles = 0, the match between angle orders and rot_config is still critical
26
+
27
+ :param str rot_config: otation system to be used, 4 characters. Default = "sxyz"
28
+
29
+ :param float angle1: amplitude of rotations around the first axis
30
+
31
+ :param float angle2: amplitude of rotations around the second axis
32
+
33
+ :param float angle3: amplitude of rotations around the third axis
34
+
35
+ :param bool active: when True the object rotates **in** a fixed coord system,
36
+ when False the coord system rotates **around** a fixed object
37
+ The default for the transform3d.euleur package is to represent movements
38
+ of the coordinate system itself --> **around** the object <==> passive
39
+ """
40
+ _ROT_CONFIG_DEFAULT = 'sxyz'
41
+
42
+ def __init__(self,angle1,angle2,angle3,rot_config=_ROT_CONFIG_DEFAULT,active=True):
43
+ R = t3.euler.euler2mat(angle1,angle2,angle3,rot_config)
44
+ if active:
45
+ self.R = R
46
+ else:
47
+ self.R = R.T
48
+ rot_config_array = np.array(list(rot_config[1:]))
49
+ angles_array = np.array([angle1,angle2,angle3])
50
+ self.angles_hash = {}
51
+ for axis in ['x','y','z']:
52
+ self.angles_hash[axis] = angles_array[np.where(rot_config_array==axis)[0][0]]
53
+ self.active=active
54
+
55
+
56
+ def getRotationMatrix(self):
57
+ '''Return the Rotation Matrix'''
58
+ return self.R
59
+
60
+
61
+ def trace(self):
62
+ return np.sum([self.R[i,i] for i in range(3)])
63
+
64
+
65
+ def getAngle(self, axis):
66
+ """
67
+ Returns the angle
68
+
69
+ :param str axis: 'x', 'y', 'z'
70
+
71
+ :return: the angle
72
+ """
73
+ return self.angles_hash[axis]
74
+
75
+
76
+ def apply(self, vectors):
77
+ """
78
+ if self = active, the output = the coords of these vectors after rotation
79
+
80
+ if self = passive, the ouput = the coords of these vectors after transformation to the rotated coordinate system
81
+
82
+ :param vectors: an array of shape [3,N] gathering a set of vectors along its columns
83
+ """
84
+ return np.dot(self.R, vectors)
85
+
@@ -0,0 +1,419 @@
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
+ import numpy
9
+ import numpy as np
10
+ import math
11
+ import transforms3d as t3
12
+
13
+ from egse.coordinates.rotationMatrix import RotationMatrix
14
+
15
+ def affine_isEuclidian(matrix):
16
+ """
17
+ Tests if a matrix is a pure solid-body euclidian rotation + translation (no shear or scaling)
18
+
19
+ We only need to check that
20
+ . the rotation part is orthogonal : R @ R.T = I
21
+ . the det(R) = 1 (=> this is not a reflexion)
22
+ """
23
+ rotation = matrix[:3,:3]
24
+ return np.allclose((rotation @ rotation.T),np.identity(3)) & np.allclose(np.linalg.det(matrix),1)
25
+
26
+ def affine_inverse(matrix):
27
+ """
28
+ affine_inverse(matrix)
29
+
30
+ WARNING:
31
+ This is NOT a generic inversion of an affine transformation matrix
32
+
33
+ This returns the affine transformation inverting that produced by the input matrix,
34
+
35
+ ASSSUMING that only rotation and translation were involved
36
+ in the affine transformation, no zoom, no shear!
37
+
38
+ That preserves the fact that the orthogonal property of the part of the input matrix
39
+ corresponding to rotation => the inverse is simply the transpose.
40
+
41
+ Pierre Royer
42
+ """
43
+ #import numpy as np
44
+
45
+ # Extract Rotation matrix and translation vector from input affine transformation
46
+ R = matrix[:3,:3]
47
+ t = matrix[:3,3]
48
+ #
49
+ # Invert the rotation and the translation
50
+ Rinv = R.T
51
+ tinv = -t
52
+ #
53
+ # The inverse affine is composed from R^-1 for the rotation and -(R^-1 . t) for the translation
54
+ result = np.identity(4)
55
+ result[:3,:3] = Rinv
56
+ result[:3,3] = np.dot(Rinv,tinv)
57
+
58
+ if affine_isEuclidian(result):
59
+ return result
60
+ else:
61
+ print("WARNING: This is not a rigid-body transformation matrix")
62
+ #print(f"R.T-based (.6f) = \n {np.round(result,6)}")
63
+ #print(f"np.inverse (.6f) = \n {np.round(np.linalg.inv(matrix),6)}")
64
+ return np.linalg.inv(matrix)
65
+
66
+ def affine_matrix_from_points(v0, v1, shear=False, scale=False, usesvd=True):
67
+ """affine_matrix_from_points(v0, v1, shear=False, scale=False, usesvd=True)
68
+ Return affine transform matrix to register two point sets.
69
+
70
+ v0 and v1 are shape (ndims, \*) arrays of at least ndims non-homogeneous
71
+ coordinates, where ndims is the dimensionality of the coordinate space.
72
+
73
+ If shear is False, a similarity transformation matrix is returned.
74
+ If also scale is False, a rigid/Euclidean transformation matrix
75
+ is returned.
76
+
77
+ By default the algorithm by Hartley and Zissermann [15] is used.
78
+ If usesvd is True, similarity and Euclidean transformation matrices
79
+ are calculated by minimizing the weighted sum of squared deviations
80
+ (RMSD) according to the algorithm by Kabsch [8].
81
+ Otherwise, and if ndims is 3, the quaternion based algorithm by Horn [9]
82
+ is used, which is slower when using this Python implementation.
83
+
84
+ The returned matrix performs rotation, translation and uniform scaling
85
+ (if specified).
86
+
87
+ >>> v0 = [[0, 1031, 1031, 0], [0, 0, 1600, 1600]]
88
+ >>> v1 = [[675, 826, 826, 677], [55, 52, 281, 277]]
89
+ >>> affine_matrix_from_points(v0, v1)
90
+ array([[ 0.14549, 0.00062, 675.50008],
91
+ [ 0.00048, 0.14094, 53.24971],
92
+ [ 0. , 0. , 1. ]])
93
+ >>> T = translation_matrix(numpy.random.random(3)-0.5)
94
+ >>> R = random_rotation_matrix(numpy.random.random(3))
95
+ >>> S = scale_matrix(random.random())
96
+ >>> M = concatenate_matrices(T, R, S)
97
+ >>> v0 = (numpy.random.rand(4, 100) - 0.5) * 20
98
+ >>> v0[3] = 1
99
+ >>> v1 = numpy.dot(M, v0)
100
+ >>> v0[:3] += numpy.random.normal(0, 1e-8, 300).reshape(3, -1)
101
+ >>> M = affine_matrix_from_points(v0[:3], v1[:3])
102
+ >>> numpy.allclose(v1, numpy.dot(M, v0))
103
+ True
104
+
105
+ More examples in superimposition_matrix()
106
+
107
+ Author: this function was extracted from the original transformations.py
108
+ written by Christoph Golke:
109
+ https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
110
+
111
+ usesvd controls the use of a method based on Singular Value Decomposition (SVD)
112
+ --> when True, it is equivalent to rigid_transform_3D (see below)
113
+
114
+ """
115
+ import numpy
116
+ v0 = numpy.array(v0, dtype=numpy.float64, copy=True)
117
+ v1 = numpy.array(v1, dtype=numpy.float64, copy=True)
118
+
119
+ ndims = v0.shape[0]
120
+ if ndims < 2 or v0.shape[1] < ndims or v0.shape != v1.shape:
121
+ print(f"ndims {ndims} v0/1.shape {v0.shape} {v1.shape} v0/1 class {v0.__class__} {v1.__class__}")
122
+ raise ValueError('input arrays are of wrong shape or type')
123
+
124
+ # move centroids to origin
125
+ t0 = -numpy.mean(v0, axis=1)
126
+ M0 = numpy.identity(ndims+1)
127
+ M0[:ndims, ndims] = t0
128
+ v0 += t0.reshape(ndims, 1)
129
+ t1 = -numpy.mean(v1, axis=1)
130
+ M1 = numpy.identity(ndims+1)
131
+ M1[:ndims, ndims] = t1
132
+ v1 += t1.reshape(ndims, 1)
133
+
134
+ if shear:
135
+ # Affine transformation
136
+ A = numpy.concatenate((v0, v1), axis=0)
137
+ u, s, vh = numpy.linalg.svd(A.T)
138
+ vh = vh[:ndims].T
139
+ B = vh[:ndims]
140
+ C = vh[ndims:2*ndims]
141
+ t = numpy.dot(C, numpy.linalg.pinv(B))
142
+ t = numpy.concatenate((t, numpy.zeros((ndims, 1))), axis=1)
143
+ M = numpy.vstack((t, ((0.0,)*ndims) + (1.0,)))
144
+ elif usesvd or ndims != 3:
145
+ # Rigid transformation via SVD of covariance matrix
146
+ u, s, vh = numpy.linalg.svd(numpy.dot(v1, v0.T))
147
+ # rotation matrix from SVD orthonormal bases
148
+ R = numpy.dot(u, vh)
149
+ if numpy.linalg.det(R) < 0.0:
150
+ # R does not constitute right handed system
151
+ R -= numpy.outer(u[:, ndims-1], vh[ndims-1, :]*2.0)
152
+ s[-1] *= -1.0
153
+ # homogeneous transformation matrix
154
+ M = numpy.identity(ndims+1)
155
+ M[:ndims, :ndims] = R
156
+ else:
157
+ # Rigid transformation matrix via quaternion
158
+ # compute symmetric matrix N
159
+ xx, yy, zz = numpy.sum(v0 * v1, axis=1)
160
+ xy, yz, zx = numpy.sum(v0 * numpy.roll(v1, -1, axis=0), axis=1)
161
+ xz, yx, zy = numpy.sum(v0 * numpy.roll(v1, -2, axis=0), axis=1)
162
+ N = [[xx+yy+zz, 0.0, 0.0, 0.0],
163
+ [yz-zy, xx-yy-zz, 0.0, 0.0],
164
+ [zx-xz, xy+yx, yy-xx-zz, 0.0],
165
+ [xy-yx, zx+xz, yz+zy, zz-xx-yy]]
166
+ # quaternion: eigenvector corresponding to most positive eigenvalue
167
+ w, V = numpy.linalg.eigh(N)
168
+ q = V[:, numpy.argmax(w)]
169
+ q /= _vector_norm(q) # unit quaternion
170
+ # homogeneous transformation matrix
171
+ M = _quaternion_matrix(q)
172
+
173
+ if scale and not shear:
174
+ # Affine transformation; scale is ratio of RMS deviations from centroid
175
+ v0 *= v0
176
+ v1 *= v1
177
+ M[:ndims, :ndims] *= math.sqrt(numpy.sum(v1) / numpy.sum(v0))
178
+
179
+ # move centroids back
180
+ M = numpy.dot(numpy.linalg.inv(M1), numpy.dot(M, M0))
181
+ M /= M[ndims, ndims]
182
+ return M
183
+
184
+
185
+ def _vector_norm(data, axis=None, out=None):
186
+ """Return length, i.e. Euclidean norm, of ndarray along axis.
187
+
188
+ >>> v = numpy.random.random(3)
189
+ >>> n = vector_norm(v)
190
+ >>> numpy.allclose(n, numpy.linalg.norm(v))
191
+ True
192
+ >>> v = numpy.random.rand(6, 5, 3)
193
+ >>> n = vector_norm(v, axis=-1)
194
+ >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=2)))
195
+ True
196
+ >>> n = vector_norm(v, axis=1)
197
+ >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
198
+ True
199
+ >>> v = numpy.random.rand(5, 4, 3)
200
+ >>> n = numpy.empty((5, 3))
201
+ >>> vector_norm(v, axis=1, out=n)
202
+ >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
203
+ True
204
+ >>> vector_norm([])
205
+ 0.0
206
+ >>> vector_norm([1])
207
+ 1.0
208
+
209
+ This function is called by affine_matrix_from_points when usesvd=False
210
+
211
+ Author: this function was extracted from the original transformations.py
212
+ written by Christoph Golke:
213
+ https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
214
+
215
+ """
216
+ data = numpy.array(data, dtype=numpy.float64, copy=True)
217
+ if out is None:
218
+ if data.ndim == 1:
219
+ return math.sqrt(numpy.dot(data, data))
220
+ data *= data
221
+ out = numpy.atleast_1d(numpy.sum(data, axis=axis))
222
+ numpy.sqrt(out, out)
223
+ return out
224
+ else:
225
+ data *= data
226
+ numpy.sum(data, axis=axis, out=out)
227
+ numpy.sqrt(out, out)
228
+
229
+
230
+ def _quaternion_matrix(quaternion):
231
+ """Return homogeneous rotation matrix from quaternion.
232
+
233
+ >>> M = quaternion_matrix([0.99810947, 0.06146124, 0, 0])
234
+ >>> numpy.allclose(M, rotation_matrix(0.123, [1, 0, 0]))
235
+ True
236
+ >>> M = quaternion_matrix([1, 0, 0, 0])
237
+ >>> numpy.allclose(M, numpy.identity(4))
238
+ True
239
+ >>> M = quaternion_matrix([0, 1, 0, 0])
240
+ >>> numpy.allclose(M, numpy.diag([1, -1, -1, 1]))
241
+ True
242
+
243
+ This function is called by affine_matrix_from_points when usesvd=False
244
+
245
+ Author: this function was extracted from the original transformations.py
246
+ written by Christoph Golke:
247
+ https://www.lfd.uci.edu/~gohlke/code/transformations.py.html
248
+ """
249
+ _EPS = np.finfo(float).eps * 5
250
+
251
+ q = numpy.array(quaternion, dtype=numpy.float64, copy=True)
252
+ n = numpy.dot(q, q)
253
+ if n < _EPS:
254
+ return numpy.identity(4)
255
+ q *= math.sqrt(2.0 / n)
256
+ q = numpy.outer(q, q)
257
+ return numpy.array([
258
+ [1.0-q[2, 2]-q[3, 3], q[1, 2]-q[3, 0], q[1, 3]+q[2, 0], 0.0],
259
+ [ q[1, 2]+q[3, 0], 1.0-q[1, 1]-q[3, 3], q[2, 3]-q[1, 0], 0.0],
260
+ [ q[1, 3]-q[2, 0], q[2, 3]+q[1, 0], 1.0-q[1, 1]-q[2, 2], 0.0],
261
+ [ 0.0, 0.0, 0.0, 1.0]])
262
+
263
+
264
+ def rigid_transform_3D(fromA, toB, verbose=True):
265
+ """rigid_transform_3D(fromA, toB, verbose=True)
266
+
267
+ INPUT
268
+ Afrom, Bto 3xn arrays = xyz coords of n points to be registered
269
+
270
+ OUTPUT
271
+ Rotation + translation transformation matrix registering fromA into toB
272
+
273
+ Author : Nghia Ho - 2013 - http://nghiaho.com/?page_id=671
274
+ "Finding optimal rotation and translation between corresponding 3D points"
275
+ Based on "A Method for Registration of 3-D Shapes", by Besl and McKay, 1992.
276
+
277
+ This is based on Singular Value Decomposition (SVD)
278
+ --> it is equivalent to affine_matrix_from_points with parameter usesvd=True
279
+ """
280
+ A = fromA.T
281
+ B = toB.T
282
+
283
+ assert len(A) == len(B)
284
+
285
+ N = A.shape[0]; # total points
286
+
287
+ centroid_A = np.mean(A, axis=0)
288
+ centroid_B = np.mean(B, axis=0)
289
+
290
+ # centre the points
291
+ AA = A - np.tile(centroid_A, (N, 1))
292
+ BB = B - np.tile(centroid_B, (N, 1))
293
+
294
+ # @ is matrix multiplication for array
295
+ H = np.transpose(AA) @ BB
296
+
297
+ U, S, Vt = np.linalg.svd(H)
298
+
299
+ R = Vt.T @ U.T
300
+
301
+ # special reflection case
302
+ if np.linalg.det(R) < 0:
303
+ print ("Reflection detected")
304
+ Vt[2,:] *= -1
305
+ R = Vt.T @ U.T
306
+
307
+ t = -R @ centroid_A.T + centroid_B.T
308
+
309
+ result = np.identity(4)
310
+ result[:3,:3] = R
311
+ result[:3,3] = t
312
+
313
+ return result
314
+
315
+
316
+ def translationRotationToTransformation(translation,rotation,rot_config="sxyz",active=True,degrees=True,translationFirst=False):
317
+ """
318
+ translationRotationToTransformation(translation,rotation,rot_config="sxyz",active=True,degrees=True,translationFirst=False)
319
+
320
+ translationFirst : translation first
321
+ False first 3 rows of transformation matrix = (R t) [usual convention and default here]
322
+ True first 3 rows of transformation matrix = (R Rt) [used in the hexapod]
323
+ """
324
+ import transforms3d as t3
325
+ import numpy as np
326
+ # Zoom - unit
327
+ zdef = np.array([1,1,1])
328
+ # Shear
329
+ sdef = np.array([0,0,0])
330
+ translation = np.array(translation)
331
+ #if degrees: rotation = np.deg2rad(np.array(rotation))
332
+ if degrees: rotation = np.array([np.deg2rad(item) for item in rotation])
333
+ rotx,roty,rotz = rotation
334
+ rmat = RotationMatrix(rotx,roty,rotz,rot_config=rot_config,active=active)
335
+ #
336
+ if translationFirst:
337
+ result = np.identity(4)
338
+ result[:3,:3] = rmat.R
339
+ result[:3,3] = rmat.R @ translation
340
+ else:
341
+ result = t3.affines.compose(translation,rmat.R,Z=zdef,S=sdef)
342
+ return result
343
+
344
+ def translationRotationFromTransformation(transformation,rot_config="sxyz",active=True,degrees=True,translationFirst=False):
345
+ """
346
+ translationRotationFromTransformation(transformation,rot_config="sxyz",active=True,degrees=True,translationFirst=False)
347
+
348
+ translationFirst : translation first
349
+ False first 3 rows of transformation matrix = (R t) [usual convention and default here]
350
+ True first 3 rows of transformation matrix = (R Rt) [used in the hexapod]
351
+ """
352
+ translation = transformation[:3,3]
353
+ rotation = t3.euler.mat2euler(transformation,axes=rot_config)
354
+ if degrees: rotation = np.array([np.rad2deg(item) for item in rotation])
355
+ if translationFirst:
356
+ translation = transformation[:3,:3].T @ translation
357
+ return translation,rotation
358
+
359
+ tr2T = translationRotationToTransformation
360
+ T2tr = translationRotationFromTransformation
361
+
362
+
363
+ def vectorPlaneIntersection(pt, frame, epsilon=1.e-6):
364
+ """
365
+ return the coordinates of the intersection of a vector with a plane.
366
+
367
+ pt = input vector. Point object, expressing the vector
368
+ vector origin = pt.ref.getOrigin().coordinates[:3]
369
+ vector direction = pt.coordinates[:3]
370
+ frame = input plane. ReferenceFrame object whose x-y plane is the target plane for intersection
371
+
372
+ If the vector's own reference frame is 'frame', the problem is trivial
373
+
374
+ In all cases, the coordinates of the interesection point are provided as a Point object, in "frame" coordinates
375
+
376
+ Ref:
377
+ https://stackoverflow.com/questions/5666222/3d-line-plane-intersection
378
+ """
379
+
380
+ from egse.coordinates.point import Point
381
+
382
+ if pt.ref == frame:
383
+ # The point is defined in frame => the origin of the vector is the origin of the target plane.
384
+ return np.array([0,0,0])
385
+ else:
386
+ # Express all inputs in 'frame'
387
+
388
+ # Vector Origin (p0)
389
+ vec_orig = Point(pt.ref.getOrigin().coordinates[:3],ref=pt.ref, name='ptorig').expressIn(frame)[:3]
390
+ # Vector End (p1)
391
+ vec_end = pt.expressIn(frame)[:3]
392
+ # Vector (u)
393
+ vec = vec_end - vec_orig
394
+
395
+ # A point in Plane (pco)
396
+ #plane_orig = np.array([0,0,0],dtype=float)
397
+ plane_orig = frame.getOrigin().coordinates[:3]
398
+ # Normal to the plane (pno)
399
+ plane_normal = frame.getAxis('z').coordinates[:3]
400
+
401
+ # Vector to normal 'angle'
402
+ vec_x_normal = np.dot(vec, plane_normal)
403
+
404
+ # Test if there is an intersection (and if it's unique)
405
+ # --> input vector and normal mustn't be perpendicular, else the vector is // to the plane or inside it
406
+ #
407
+ if (np.allclose(vec_x_normal, 0., atol=epsilon)):
408
+ print('The input vector is // to the plane normal (or inside the plane)')
409
+ print('--> there exists no intersection (or an infinity of them)')
410
+ return None
411
+ else:
412
+ # Vector from the point in the plane to the origin of the vector (w)
413
+ plane_to_vec = vec_orig - plane_orig
414
+
415
+ # Solution ("how many 'vectors' away is the interesection ?")
416
+ vec_multiplicator = - np.dot(plane_normal, plane_to_vec) / vec_x_normal
417
+
418
+ return Point(vec_orig + (vec * vec_multiplicator), ref=frame)
419
+