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
egse/coordinates/point.py
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
|
-
The point module provides two classes, a `Point` class which simply represents a point
|
|
3
|
-
|
|
2
|
+
The point module provides two classes, a `Point` class which simply represents a point in the 3D space, and a `Points`
|
|
3
|
+
class which is a collection of Point objects.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Point objects defined in the same reference frame can be combined with the natural `+`, `-` , `+=` and `-=` operations.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
In order to work with 4x4 transformation matrices, the 3D [x,y,z] coordinates are
|
|
11
|
-
automatically converted to a [x,y,z,1] coordinates array attribute.
|
|
12
|
-
|
|
13
|
-
@author: Pierre Royer
|
|
7
|
+
In order to work with 4x4 transformation matrices, the 3D [x, y, z] coordinates are automatically converted to a
|
|
8
|
+
[x, y, z, 1] coordinates array attribute.
|
|
14
9
|
"""
|
|
15
10
|
|
|
16
11
|
import logging
|
|
17
12
|
import random
|
|
18
13
|
import string
|
|
14
|
+
from typing import Any
|
|
19
15
|
|
|
16
|
+
import numpy
|
|
20
17
|
import numpy as np
|
|
18
|
+
from numpy import floating
|
|
21
19
|
|
|
22
20
|
import egse.coordinates.transform3d_addon as t3add
|
|
23
21
|
|
|
@@ -25,451 +23,549 @@ LOGGER = logging.getLogger(__name__)
|
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
class Point:
|
|
28
|
-
|
|
29
|
-
A Point object represents a point in 3D space and is defined with respect to
|
|
30
|
-
a given reference frame.
|
|
26
|
+
from egse.coordinates.reference_frame import ReferenceFrame
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
There is no check that the randomly generated name is unique, so two Point
|
|
34
|
-
objects can be different but have the same name.
|
|
28
|
+
"""Representation of a point in 3D space, defined w.r.t. a given reference frame."""
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
def __init__(self, coordinates: np.ndarray | list, reference_frame: ReferenceFrame, name: str = None):
|
|
31
|
+
"""Initialisation of a point in 3D space, defined w.r.t. a given reference frame.
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
In order to work with 4x4 transformation matrices, the 3D [x, y, z] coordinates are automatically converted to a
|
|
34
|
+
[x, y, z, 1] coordinates array attribute.
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
Args:
|
|
37
|
+
coordinates (np.ndarray, list): 1x3 or 1x4 matrix defining this system in the given reference frame
|
|
38
|
+
(1x3 being x, y, z + an additional 1 for the affine operations).
|
|
39
|
+
reference_frame (ReferenceFrame): Reference system in which this point object will be defined.
|
|
40
|
+
name (str): Name of the point. If not given, a random name will be generated. Note that there is no check
|
|
41
|
+
that the randomly generated name is unique, so two `Point` objects can be different but have
|
|
42
|
+
the same name.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If the reference frame is None.
|
|
41
46
|
"""
|
|
42
|
-
This initializes a Point object in a given reference frame.
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
coordinates (numpy.ndarray, list): 1x3 or 1x4 matrix defining this system in "ref" system
|
|
46
|
-
(1x3 being x,y,z + an additional 1 for the affine operations)
|
|
48
|
+
# Coordinates
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
self.x = None
|
|
51
|
+
self.y = None
|
|
52
|
+
self.z = None
|
|
53
|
+
self.coordinates = np.ndarray([])
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
self.set_coordinates(coordinates) # Format (x, y, z, 1)
|
|
52
56
|
|
|
53
|
-
#
|
|
54
|
-
self.setCoordinates(coordinates)
|
|
57
|
+
# Reference frame
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
raise ValueError("A Point shall be defined with a reference frame, ref can not be None.")
|
|
59
|
+
if reference_frame is None:
|
|
60
|
+
raise ValueError("The reference frame must not be None.")
|
|
59
61
|
else:
|
|
60
|
-
self.
|
|
62
|
+
self.reference_frame = reference_frame
|
|
63
|
+
|
|
64
|
+
# Name
|
|
61
65
|
|
|
62
|
-
self.
|
|
66
|
+
self.name = None
|
|
67
|
+
self.set_name(name)
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
# Definition
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
return f"{self.coordinates[:-1]} (ref {self.ref.name})"
|
|
71
|
+
self.definition = [self.coordinates[:-1], self.reference_frame, self.name]
|
|
68
72
|
|
|
69
|
-
def
|
|
70
|
-
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
"""Returns a representation the point.
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
Returns:
|
|
77
|
+
Representation of the point.
|
|
73
78
|
"""
|
|
74
|
-
Re-implements the == operator which by default checks if id(self) == id(other).
|
|
75
79
|
|
|
76
|
-
|
|
80
|
+
return f"{self.coordinates[:-1]} (ref {self.reference_frame.name})"
|
|
81
|
+
|
|
82
|
+
def __str__(self) -> str:
|
|
83
|
+
"""Returns a printable string representation of the point.
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* the name must not be equal
|
|
85
|
+
Returns:
|
|
86
|
+
Printable string representation of the point.
|
|
81
87
|
"""
|
|
88
|
+
|
|
89
|
+
return f"{self.coordinates[:-1]} (ref {self.reference_frame.name}), name {self.name}"
|
|
90
|
+
|
|
91
|
+
def __eq__(self, other) -> bool:
|
|
92
|
+
"""Implements the == operator.
|
|
93
|
+
|
|
94
|
+
Two points are equal when:
|
|
95
|
+
- Their coordinates are equal,
|
|
96
|
+
- The reference system in which they are defined is equal.
|
|
97
|
+
|
|
98
|
+
The name does not have to be equal.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
other (Point): Other point to compare with.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if the points are equal, False otherwise.
|
|
105
|
+
"""
|
|
106
|
+
|
|
82
107
|
if self is other:
|
|
83
108
|
return True
|
|
84
109
|
|
|
85
110
|
if isinstance(other, Point):
|
|
86
111
|
if not np.array_equal(self.coordinates, other.coordinates):
|
|
87
112
|
return False
|
|
88
|
-
if self.
|
|
113
|
+
if self.reference_frame != other.reference_frame:
|
|
89
114
|
return False
|
|
90
115
|
return True
|
|
91
|
-
return NotImplemented
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def __hash__(self) -> int:
|
|
120
|
+
"""Returns a unique integer hash value for the point.
|
|
95
121
|
|
|
96
|
-
|
|
122
|
+
Returns:
|
|
123
|
+
Unique integer hash value for the point.
|
|
97
124
|
"""
|
|
98
|
-
This checks if a Point is the same as another Point in a different reference frame.
|
|
99
125
|
|
|
100
|
-
|
|
101
|
-
after they have been expressed in the same reference frame.
|
|
126
|
+
return id(self.definition) // 16
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
128
|
+
def is_same(self, other) -> bool:
|
|
129
|
+
"""Checks whether two points are the same, even if they are defined in different reference frames.
|
|
105
130
|
|
|
106
|
-
:
|
|
131
|
+
Args:
|
|
132
|
+
other (Point): Other point to compare with.
|
|
107
133
|
|
|
108
|
-
:
|
|
109
|
-
|
|
134
|
+
Returns:
|
|
135
|
+
True if the points are the same, False otherwise.
|
|
110
136
|
"""
|
|
111
137
|
|
|
112
138
|
if isinstance(other, Point):
|
|
113
139
|
if self == other:
|
|
114
140
|
return True
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return NotImplemented
|
|
141
|
+
if np.array_equal(self.coordinates, other.express_in(self.reference_frame)):
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
return False
|
|
120
145
|
|
|
121
146
|
@staticmethod
|
|
122
|
-
def __coords__(coordinates):
|
|
123
|
-
"""
|
|
124
|
-
|
|
125
|
-
|
|
147
|
+
def __coords__(coordinates) -> np.ndarray:
|
|
148
|
+
"""Formats the input list into 1x4 np.array coordinates.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
coordinates (Point | np.ndarray | list):
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Coordinates formatted as a 1x4 np.ndarray.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If the input is not a list, numpy.ndarray, or Point.
|
|
126
158
|
"""
|
|
159
|
+
|
|
127
160
|
if isinstance(coordinates, Point):
|
|
128
161
|
return coordinates.coordinates
|
|
129
162
|
elif isinstance(coordinates, (np.ndarray, list)):
|
|
130
163
|
coordinates = list(coordinates)
|
|
131
164
|
if len(coordinates) == 3:
|
|
132
165
|
coordinates.append(1)
|
|
133
|
-
return coordinates
|
|
166
|
+
return np.ndarray(coordinates)
|
|
134
167
|
else:
|
|
135
|
-
raise ValueError("
|
|
136
|
-
|
|
137
|
-
def setName(self, name=None):
|
|
138
|
-
"""
|
|
139
|
-
Set or change the name of a Point object.
|
|
168
|
+
raise ValueError("Input must be a list, numpy.ndarray or Point")
|
|
140
169
|
|
|
141
|
-
|
|
170
|
+
def set_name(self, name: (str | None) = None) -> None:
|
|
171
|
+
"""Sets the name of the point.
|
|
142
172
|
|
|
143
|
-
|
|
144
|
-
|
|
173
|
+
Args:
|
|
174
|
+
name (str | None): Name to use for the point. If None, a random name will be generated.
|
|
145
175
|
"""
|
|
176
|
+
|
|
146
177
|
if name is None:
|
|
178
|
+
# TODO Should we care about the possibility the the generation of random names does not necessarily create
|
|
179
|
+
# a unique name for the point?
|
|
147
180
|
self.name = "p" + "".join(random.choices(string.ascii_lowercase, k=3))
|
|
148
181
|
else:
|
|
149
182
|
self.name = name
|
|
150
183
|
|
|
151
|
-
def
|
|
152
|
-
"""
|
|
153
|
-
|
|
184
|
+
def set_coordinates(self, coordinates: np.ndarray | list) -> None:
|
|
185
|
+
"""Sets the coordinates of the point.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
coordinates (np.ndarray | list): Coordinates to set.
|
|
154
189
|
"""
|
|
190
|
+
|
|
155
191
|
coordinates = Point.__coords__(coordinates)
|
|
156
|
-
self.coordinates = np.
|
|
192
|
+
self.coordinates = np.ndarray(coordinates)
|
|
157
193
|
|
|
158
194
|
self.x = self.coordinates[0]
|
|
159
195
|
self.y = self.coordinates[1]
|
|
160
196
|
self.z = self.coordinates[2]
|
|
161
197
|
|
|
162
|
-
def
|
|
163
|
-
"""
|
|
164
|
-
Returns the coordinates of this Points object.
|
|
198
|
+
def get_coordinates(self, reference_frame: ReferenceFrame | None = None) -> np.ndarray:
|
|
199
|
+
"""Returns the coordinates of the point.
|
|
165
200
|
|
|
166
|
-
|
|
167
|
-
|
|
201
|
+
Args:
|
|
202
|
+
reference_frame (ReferenceFrame | None): Reference frame in which the point coordinates are returned.
|
|
168
203
|
|
|
169
|
-
:
|
|
170
|
-
|
|
204
|
+
Returns:
|
|
205
|
+
Coordinates of the point in the given reference frame.
|
|
171
206
|
"""
|
|
172
|
-
|
|
207
|
+
|
|
208
|
+
if reference_frame is None:
|
|
173
209
|
return self.coordinates
|
|
174
210
|
else:
|
|
175
|
-
return self.
|
|
211
|
+
return self.express_in(reference_frame)
|
|
176
212
|
|
|
177
|
-
def
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
def distance_to(self, target) -> floating[Any]:
|
|
214
|
+
"""Returns the distance from this point to the target.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
target (Point | ReferenceFrame | np.ndarray | list): Target to compute the distance to.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Distance from this point to the target.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
ValueError: If the target is not a Point, ReferenceFrame, numpy.ndarray, or list.
|
|
181
224
|
"""
|
|
182
|
-
|
|
225
|
+
|
|
226
|
+
from egse.coordinates.reference_frame import ReferenceFrame
|
|
183
227
|
|
|
184
228
|
if isinstance(target, Point):
|
|
185
|
-
|
|
229
|
+
target_coordinates = target.express_in(self.reference_frame)[:3]
|
|
186
230
|
elif isinstance(target, ReferenceFrame):
|
|
187
|
-
return np.linalg.norm(self.
|
|
231
|
+
return np.linalg.norm(self.express_in(target)[:3])
|
|
188
232
|
elif isinstance(target, (np.ndarray, list)):
|
|
189
233
|
if len(target) > 3:
|
|
190
234
|
target = target[:3]
|
|
191
|
-
|
|
235
|
+
target_coordinates = target
|
|
192
236
|
else:
|
|
193
|
-
raise ValueError("
|
|
237
|
+
raise ValueError("Target must be a list, numpy.ndarray, Point, or ReferenceFrame")
|
|
194
238
|
|
|
195
|
-
LOGGER.info(f"self={self.coordinates[:-1]}, target={
|
|
239
|
+
LOGGER.info(f"self={self.coordinates[:-1]}, target={target_coordinates}")
|
|
196
240
|
|
|
197
|
-
return np.linalg.norm(self.coordinates[:3] -
|
|
241
|
+
return np.linalg.norm(self.coordinates[:3] - target_coordinates)
|
|
198
242
|
|
|
199
|
-
def
|
|
200
|
-
"""
|
|
201
|
-
Returns the distance of this Point object to the target, considering 2 coordinates only!
|
|
243
|
+
def in_plane_distance_to(self, target, plane: str = "xy") -> np.floating[Any]:
|
|
244
|
+
"""Returns the distance of this point object to the target, considering 2 coordinates only.
|
|
202
245
|
|
|
203
|
-
|
|
246
|
+
Note that this is not a commutative operation, because the plane used to project the points coordinates
|
|
247
|
+
before computing the distances is taken from the coordinate system of `self`.
|
|
204
248
|
|
|
205
|
-
|
|
249
|
+
Args:
|
|
250
|
+
target (Point | ReferenceFrame | np.ndarray | list): Target to compute the distance to.
|
|
251
|
+
plane (str): Plane to consider. Must be in ['xy', 'xz', 'yz'].
|
|
206
252
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
==> pointA.inPlaneDistanceTo(pointB) != pointB.inPlaneDistanceTo(pointA)
|
|
210
|
-
The first projects on the xy plane of pointA.ref, the second on the xy plane of pointB.ref
|
|
253
|
+
Returns:
|
|
254
|
+
Distance from this point to the target.
|
|
211
255
|
"""
|
|
212
|
-
|
|
256
|
+
|
|
257
|
+
from egse.coordinates.reference_frame import ReferenceFrame
|
|
213
258
|
|
|
214
259
|
if isinstance(target, Point):
|
|
215
|
-
|
|
260
|
+
target_coordinates = target.express_in(self.reference_frame)
|
|
216
261
|
elif isinstance(target, ReferenceFrame):
|
|
217
|
-
|
|
262
|
+
target_coordinates = target.get_origin().express_in(self)
|
|
218
263
|
elif isinstance(target, (np.ndarray, list)):
|
|
219
|
-
|
|
264
|
+
target_coordinates = target
|
|
220
265
|
else:
|
|
221
266
|
raise ValueError("input must be a list, numpy.ndarray, Point or ReferenceFrame")
|
|
222
267
|
|
|
223
|
-
LOGGER.info(f"self={self.coordinates[:-1]}, target={
|
|
268
|
+
LOGGER.info(f"self={self.coordinates[:-1]}, target={target_coordinates}")
|
|
224
269
|
|
|
225
|
-
|
|
270
|
+
plane_selection = {"xy": [0, 1], "xz": [0, 2], "yz": [1, 2]}
|
|
226
271
|
|
|
227
|
-
LOGGER.info(f"self.coordinates[planeSelect[plane]] {self.coordinates[
|
|
228
|
-
LOGGER.info(f"targetCoords[planeSelect[plane]] {
|
|
272
|
+
LOGGER.info(f"self.coordinates[planeSelect[plane]] {self.coordinates[plane_selection[plane]]}")
|
|
273
|
+
LOGGER.info(f"targetCoords[planeSelect[plane]] {target_coordinates[plane_selection[plane]]}")
|
|
229
274
|
LOGGER.info(
|
|
230
|
-
f"Difference {self.coordinates[
|
|
275
|
+
f"Difference {self.coordinates[plane_selection[plane]] - target_coordinates[plane_selection[plane]]}"
|
|
231
276
|
)
|
|
232
277
|
|
|
233
|
-
return np.linalg.norm(self.coordinates[
|
|
278
|
+
return np.linalg.norm(self.coordinates[plane_selection[plane]] - target_coordinates[plane_selection[plane]])
|
|
279
|
+
|
|
280
|
+
def distance_to_plane(self, plane: str = "xy", reference_frame: ReferenceFrame | None = None) -> float:
|
|
281
|
+
"""Calculates the distance from the point to a plane in a given reference frame.
|
|
234
282
|
|
|
235
|
-
|
|
283
|
+
Args:
|
|
284
|
+
plane (str): Target plane, must be in ["xy", "xz", "yz"]
|
|
285
|
+
reference_frame (ReferenceFrame | None, optional): Reference frame in which the distance is calculated.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Distance from the point to the plane.
|
|
236
289
|
"""
|
|
237
|
-
distanceToPlane(self,plane="xy",ref=None)
|
|
238
290
|
|
|
239
|
-
|
|
291
|
+
if (reference_frame is None) or (self.reference_frame == reference_frame):
|
|
292
|
+
coordinates = self.coordinates[:-1]
|
|
293
|
+
else:
|
|
294
|
+
coordinates = self.express_in(reference_frame)
|
|
295
|
+
|
|
296
|
+
out_of_plane_index = {"xy": 2, "xz": 1, "yz": 0}
|
|
297
|
+
|
|
298
|
+
return coordinates[out_of_plane_index[plane]]
|
|
240
299
|
|
|
241
|
-
|
|
242
|
-
|
|
300
|
+
def __sub__(self, point):
|
|
301
|
+
"""Implements the subtraction operator (-).
|
|
243
302
|
|
|
244
|
-
:
|
|
245
|
-
|
|
303
|
+
Args:
|
|
304
|
+
point (Point | np.ndarray | list): Point to subtract from `self`.
|
|
246
305
|
|
|
247
|
-
:
|
|
306
|
+
Returns:
|
|
307
|
+
Point: New point resulting from the subtraction.
|
|
248
308
|
"""
|
|
249
|
-
if (ref is None) or (self.ref == ref):
|
|
250
|
-
coordinates = self.coordinates[:-1]
|
|
251
|
-
elif self.ref != ref:
|
|
252
|
-
coordinates = self.expressIn(ref)
|
|
253
309
|
|
|
254
|
-
|
|
310
|
+
if isinstance(point, Point):
|
|
311
|
+
if point.reference_frame != self.reference_frame:
|
|
312
|
+
raise NotImplementedError("The points have different reference frames")
|
|
255
313
|
|
|
256
|
-
|
|
314
|
+
new_coordinates = self.coordinates - point.coordinates
|
|
257
315
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
Takes care for
|
|
261
|
-
newPoint = self + point
|
|
262
|
-
"""
|
|
263
|
-
if isinstance(apoint, Point):
|
|
264
|
-
try:
|
|
265
|
-
if apoint.ref != self.ref:
|
|
266
|
-
raise ValueError
|
|
267
|
-
except ValueError:
|
|
268
|
-
print("WARNING: The points have different reference frames, returning NotImplemented")
|
|
269
|
-
return NotImplemented
|
|
270
|
-
newCoordinates = self.coordinates - apoint.coordinates
|
|
316
|
+
elif isinstance(point, (np.ndarray, list)):
|
|
317
|
+
new_coordinates = self.coordinates - Point.__coords__(point)
|
|
271
318
|
|
|
272
|
-
|
|
273
|
-
|
|
319
|
+
else:
|
|
320
|
+
raise ValueError("The point must be a Point, numpy.ndarray, or list")
|
|
274
321
|
|
|
275
322
|
# For the affine transforms, the 4th digit must be set to 1 (it has been modified above)
|
|
276
|
-
newCoordinates[-1] = 1
|
|
277
323
|
|
|
278
|
-
|
|
324
|
+
new_coordinates[-1] = 1
|
|
279
325
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
326
|
+
return Point(coordinates=new_coordinates, reference_frame=self.reference_frame)
|
|
327
|
+
|
|
328
|
+
def __isub__(self, point):
|
|
329
|
+
"""Implements the subtraction assignment operator (-=).
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
point (Point | np.ndarray | list): Point to add to `self`.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Modified point.
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
ValueError: If the point is not a Point, numpy.ndarray, or list.
|
|
284
339
|
"""
|
|
285
|
-
if isinstance(apoint, Point):
|
|
286
|
-
try:
|
|
287
|
-
if apoint.ref != self.ref:
|
|
288
|
-
raise ValueError
|
|
289
|
-
except ValueError:
|
|
290
|
-
print("WARNING: The points have different reference frames, returning NotImplemented")
|
|
291
|
-
return NotImplemented
|
|
292
|
-
newCoordinates = self.coordinates - apoint.coordinates
|
|
293
340
|
|
|
294
|
-
|
|
295
|
-
|
|
341
|
+
if isinstance(point, Point):
|
|
342
|
+
if point.reference_frame != self.reference_frame:
|
|
343
|
+
raise NotImplementedError("The points have different reference frames")
|
|
344
|
+
|
|
345
|
+
new_coordinates = self.coordinates - point.coordinates
|
|
346
|
+
|
|
347
|
+
elif isinstance(point, (np.ndarray, list)):
|
|
348
|
+
new_coordinates = self.coordinates - Point.__coords__(point)
|
|
349
|
+
|
|
350
|
+
else:
|
|
351
|
+
raise ValueError("The point must be a Point, numpy.ndarray, or list")
|
|
296
352
|
|
|
297
353
|
# For the affine transforms, the 4th digit must be set to 1 (it has been modified above)
|
|
298
|
-
|
|
354
|
+
new_coordinates[-1] = 1
|
|
299
355
|
|
|
300
|
-
self.coordinates =
|
|
356
|
+
self.coordinates = new_coordinates
|
|
301
357
|
|
|
302
358
|
return self
|
|
303
359
|
|
|
304
|
-
def __add__(self,
|
|
305
|
-
"""
|
|
306
|
-
|
|
307
|
-
|
|
360
|
+
def __add__(self, point):
|
|
361
|
+
"""Implements the addition operator (+).
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
point (Point | np.ndarray | list): Point to add to `self`.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Point: New point resulting from the addition.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
ValueError: If the point is not a Point, numpy.ndarray, or list.
|
|
308
371
|
"""
|
|
309
|
-
if isinstance(apoint, Point):
|
|
310
|
-
try:
|
|
311
|
-
if apoint.ref != self.ref:
|
|
312
|
-
print(f"DEBUG: {apoint} = {apoint.expressIn(self.ref)}")
|
|
313
|
-
raise ValueError
|
|
314
|
-
except ValueError:
|
|
315
|
-
print("WARNING: The points have different reference frames, returning NotImplemented")
|
|
316
|
-
return NotImplemented
|
|
317
|
-
newCoordinates = self.coordinates + apoint.coordinates
|
|
318
372
|
|
|
319
|
-
|
|
320
|
-
|
|
373
|
+
if isinstance(point, Point):
|
|
374
|
+
if point.reference_frame != self.reference_frame:
|
|
375
|
+
raise NotImplementedError("The points have different reference frames")
|
|
376
|
+
|
|
377
|
+
new_coordinates = self.coordinates + point.coordinates
|
|
378
|
+
|
|
379
|
+
elif isinstance(point, (np.ndarray, list)):
|
|
380
|
+
new_coordinates = self.coordinates + Point.__coords__(point)
|
|
321
381
|
|
|
322
382
|
else:
|
|
323
|
-
|
|
383
|
+
raise ValueError("The point must be a Point, numpy.ndarray, or list")
|
|
324
384
|
|
|
325
385
|
# For the affine transforms, the 4th digit must be set to 1 (it has been modified above)
|
|
326
|
-
newCoordinates[-1] = 1
|
|
327
386
|
|
|
328
|
-
|
|
387
|
+
new_coordinates[-1] = 1
|
|
329
388
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
389
|
+
return Point(coordinates=new_coordinates, reference_frame=self.reference_frame)
|
|
390
|
+
|
|
391
|
+
def __iadd__(self, point):
|
|
392
|
+
"""Implements the addition assignment operator (+=).
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
point (Point | np.ndarray | list): Point to add to `self`.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Modified point.
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
ValueError: If the point is not a Point, numpy.ndarray, or list.
|
|
334
402
|
"""
|
|
335
|
-
if isinstance(apoint, Point):
|
|
336
|
-
try:
|
|
337
|
-
if apoint.ref != self.ref:
|
|
338
|
-
raise ValueError
|
|
339
|
-
except ValueError:
|
|
340
|
-
print("WARNING: The points have different reference frames, returning NotImplemented")
|
|
341
|
-
return NotImplemented
|
|
342
|
-
newCoordinates = self.coordinates + apoint.coordinates
|
|
343
403
|
|
|
344
|
-
|
|
345
|
-
|
|
404
|
+
if isinstance(point, Point):
|
|
405
|
+
if point.reference_frame != self.reference_frame:
|
|
406
|
+
raise NotImplementedError("The points have different reference frames")
|
|
407
|
+
|
|
408
|
+
new_coordinates = self.coordinates + point.coordinates
|
|
409
|
+
|
|
410
|
+
elif isinstance(point, (np.ndarray, list)):
|
|
411
|
+
new_coordinates = self.coordinates + Point.__coords__(point)
|
|
412
|
+
|
|
413
|
+
else:
|
|
414
|
+
raise ValueError("The point must be a Point, numpy.ndarray, or list")
|
|
346
415
|
|
|
347
416
|
# For the affine transforms, the 4th digit must be set to 1 (it has been modified above)
|
|
348
|
-
newCoordinates[-1] = 1
|
|
349
417
|
|
|
350
|
-
|
|
418
|
+
new_coordinates[-1] = 1
|
|
419
|
+
self.coordinates = new_coordinates
|
|
351
420
|
|
|
352
421
|
return self
|
|
353
422
|
|
|
354
|
-
def
|
|
355
|
-
"""
|
|
356
|
-
|
|
423
|
+
def express_in(self, target_frame: ReferenceFrame) -> np.ndarray:
|
|
424
|
+
"""Expresses the coordinates in another reference frame.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
target_frame (ReferenceFrame): Target reference frame.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Coordinates in the target reference frame.
|
|
357
431
|
"""
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
targetFrame == self.ref
|
|
361
|
-
We're after the definition of self
|
|
362
|
-
"""
|
|
432
|
+
|
|
433
|
+
if target_frame == self.reference_frame:
|
|
363
434
|
result = self.coordinates
|
|
435
|
+
|
|
364
436
|
else:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
We know the coordinates in self.ref
|
|
369
|
-
#We need to apply the transformation from targetFrame to self.ref
|
|
370
|
-
self.ref --> self (self.transformation)
|
|
371
|
-
"""
|
|
372
|
-
# transform = targetFrame.getTransformationFrom(self.ref)
|
|
373
|
-
transform = self.ref.getPassiveTransformationTo(targetFrame)
|
|
374
|
-
if self.debug:
|
|
375
|
-
print("transform \n{0}".format(transform))
|
|
437
|
+
# Apply coordinate transformation of self.coordinates from self.reference_frame to target_frame
|
|
438
|
+
transform = self.reference_frame.get_passive_transformation_to(target_frame)
|
|
376
439
|
result = np.dot(transform, self.coordinates)
|
|
377
|
-
return result
|
|
378
440
|
|
|
379
|
-
|
|
380
|
-
"""
|
|
381
|
-
We redefine self as attached to another reference frame
|
|
382
|
-
. calculate self's coordinates in the new reference frame
|
|
383
|
-
. update the definition
|
|
384
|
-
"""
|
|
385
|
-
newCoordinates = self.expressIn(targetFrame)
|
|
386
|
-
self.setCoordinates(newCoordinates)
|
|
387
|
-
self.ref = targetFrame
|
|
388
|
-
return
|
|
441
|
+
LOGGER.debug(f"transform: \n{transform}")
|
|
389
442
|
|
|
443
|
+
return result
|
|
390
444
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
A Points object is a collection of Point objects.
|
|
445
|
+
def change_reference_frame(self, target_frame: ReferenceFrame):
|
|
446
|
+
"""Re-defines `self` as attached to another reference frame.
|
|
394
447
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
448
|
+
This is done by:
|
|
449
|
+
- Calculating the coordinates of `self` in the target reference frame,
|
|
450
|
+
- Updating the definition of `self` in the new reference frame (with the newly calculated coordinates and the
|
|
451
|
+
target reference frame).
|
|
398
452
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
453
|
+
Args:
|
|
454
|
+
target_frame (ReferenceFrame): Target reference frame.
|
|
455
|
+
"""
|
|
402
456
|
|
|
403
|
-
|
|
457
|
+
new_coordinates = self.express_in(target_frame)
|
|
458
|
+
self.set_coordinates(new_coordinates)
|
|
404
459
|
|
|
405
|
-
|
|
460
|
+
self.reference_frame = target_frame
|
|
406
461
|
|
|
407
|
-
def __init__(self, coordinates, ref, name=None):
|
|
408
|
-
"""
|
|
409
|
-
Points.__init__(self, coordinates, ref, name=None)
|
|
410
462
|
|
|
411
|
-
|
|
463
|
+
class Points:
|
|
464
|
+
"""Representation of a collection of points in 3D space."""
|
|
465
|
+
|
|
466
|
+
from egse.coordinates.reference_frame import ReferenceFrame
|
|
412
467
|
|
|
413
|
-
|
|
414
|
-
* numpy.ndarray:
|
|
415
|
-
4xn matrix defining this system in "ref" system
|
|
416
|
-
(3 being x,y,z + an additional 1 for the affine operations)
|
|
417
|
-
* list of Point objects:
|
|
418
|
-
the coordinates of the Point objects are extracted in the order of the list
|
|
419
|
-
and concatenated in a numpy.ndarray
|
|
468
|
+
debug = 0
|
|
420
469
|
|
|
421
|
-
|
|
470
|
+
def __init__(self, coordinates: numpy.ndarray | list, reference_frame: ReferenceFrame, name: str = None):
|
|
471
|
+
"""Initialisation of a new set of points.
|
|
422
472
|
|
|
423
|
-
|
|
473
|
+
Args:
|
|
474
|
+
coordinates (numpy.ndarray | list): Either a 4xn matrix (3 being x, y, z + an additional 1 for the affine
|
|
475
|
+
operations), defining n coordinates in the given reference frame, or a
|
|
476
|
+
list of Point(s) objects.
|
|
477
|
+
reference_frame (ReferenceFrame): Reference frame in which the coordinates are defined.
|
|
478
|
+
name (str | None): Name of the Points object. When None, a name will be generated automatically, consisting
|
|
479
|
+
of a capital "P" followed by three lower case letters.
|
|
424
480
|
"""
|
|
425
481
|
|
|
426
|
-
#
|
|
482
|
+
# Coordinates
|
|
483
|
+
|
|
484
|
+
self.x = None
|
|
485
|
+
self.y = None
|
|
486
|
+
self.z = None
|
|
487
|
+
self.coordinates = np.ndarray([])
|
|
427
488
|
|
|
428
489
|
if isinstance(coordinates, list):
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
490
|
+
coordinate_list = []
|
|
491
|
+
|
|
492
|
+
for item in coordinates:
|
|
493
|
+
if isinstance(item, Point):
|
|
494
|
+
coordinate_list.append(item.express_in(reference_frame))
|
|
495
|
+
elif isinstance(item, Points):
|
|
496
|
+
coordinate_list.extend(item.express_in(reference_frame))
|
|
497
|
+
else:
|
|
432
498
|
raise ValueError("If the input is a list, all items in it must be Point(s) objects")
|
|
433
|
-
|
|
434
|
-
self.setCoordinates(np.array(coordinateList).T)
|
|
499
|
+
self.set_coordinates(np.array(coordinate_list).T)
|
|
435
500
|
elif isinstance(coordinates, np.ndarray):
|
|
436
|
-
self.
|
|
501
|
+
self.set_coordinates(coordinates)
|
|
437
502
|
else:
|
|
438
|
-
raise ValueError("The input must be either a numpy.ndarray or a list of Point objects")
|
|
503
|
+
raise ValueError("The input must be either a numpy.ndarray or a list of Point/Points objects")
|
|
439
504
|
|
|
440
|
-
|
|
505
|
+
# Reference frame
|
|
441
506
|
|
|
442
|
-
|
|
507
|
+
if reference_frame is None:
|
|
508
|
+
raise ValueError("The reference frame must not be None.")
|
|
509
|
+
else:
|
|
510
|
+
self.reference_frame = reference_frame
|
|
511
|
+
|
|
512
|
+
# Name
|
|
513
|
+
|
|
514
|
+
self.name = None
|
|
515
|
+
self.set_name(name)
|
|
516
|
+
|
|
517
|
+
def __repr__(self) -> str:
|
|
518
|
+
"""Returns a representation the points.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Representation of the points.
|
|
522
|
+
"""
|
|
443
523
|
|
|
444
|
-
return
|
|
524
|
+
return "{0} (ref {1})".format(self.coordinates[:-1], self.reference_frame.name)
|
|
445
525
|
|
|
446
|
-
def
|
|
447
|
-
|
|
526
|
+
def __str__(self) -> str:
|
|
527
|
+
"""Returns a printable string representation of the point.
|
|
448
528
|
|
|
449
|
-
|
|
450
|
-
|
|
529
|
+
Returns:
|
|
530
|
+
Printable string representation of the point.
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
return "{1} (ref {2}), name {0}".format(self.name, self.coordinates[:-1], self.reference_frame.name)
|
|
451
534
|
|
|
452
535
|
@staticmethod
|
|
453
536
|
def __coords__(coordinates):
|
|
537
|
+
"""Formats the input list into 4xn np.array coordinates.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
coordinates (Point | np.ndarray | list):
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Coordinates formatted as a 1x4 np.ndarray.
|
|
544
|
+
|
|
545
|
+
Raises:
|
|
546
|
+
ValueError: If the input is not a list, numpy.ndarray, or Point.
|
|
454
547
|
"""
|
|
455
|
-
|
|
456
|
-
Static --> can be called 'from outside', without passing a Points object
|
|
457
|
-
"""
|
|
548
|
+
|
|
458
549
|
if isinstance(coordinates, Point):
|
|
459
550
|
return coordinates.coordinates
|
|
460
551
|
elif isinstance(coordinates, np.ndarray):
|
|
461
552
|
if coordinates.shape[0] not in [3, 4]:
|
|
462
553
|
raise ValueError("Input coordinates array must be 3 x n or 4 x n")
|
|
463
|
-
|
|
464
554
|
if coordinates.shape[0] == 3:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
coordinates =
|
|
555
|
+
new_coordinates = np.ones([4, coordinates.shape[1]])
|
|
556
|
+
new_coordinates[:3, :] = coordinates
|
|
557
|
+
coordinates = new_coordinates
|
|
468
558
|
return coordinates
|
|
469
559
|
else:
|
|
470
560
|
raise ValueError("input must be a list, numpy.ndarray or Point")
|
|
471
561
|
|
|
472
|
-
def
|
|
562
|
+
def set_name(self, name: str | None = None):
|
|
563
|
+
"""Sets the name of the point.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
name (str | None): Name to use for the point. If None, a random name will be generated.
|
|
567
|
+
"""
|
|
568
|
+
|
|
473
569
|
if name is None:
|
|
474
570
|
self.name = (
|
|
475
571
|
"P"
|
|
@@ -479,9 +575,13 @@ class Points:
|
|
|
479
575
|
else:
|
|
480
576
|
self.name = name
|
|
481
577
|
|
|
482
|
-
def
|
|
483
|
-
|
|
484
|
-
|
|
578
|
+
def set_coordinates(self, coordinates: numpy.ndarray | list) -> None:
|
|
579
|
+
"""Sets the coordinates of the point.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
coordinates (np.ndarray | list): Coordinates to set.
|
|
583
|
+
"""
|
|
584
|
+
|
|
485
585
|
coordinates = Points.__coords__(coordinates)
|
|
486
586
|
self.coordinates = coordinates
|
|
487
587
|
|
|
@@ -489,219 +589,245 @@ class Points:
|
|
|
489
589
|
self.y = self.coordinates[1, :]
|
|
490
590
|
self.z = self.coordinates[2, :]
|
|
491
591
|
|
|
492
|
-
|
|
592
|
+
def get_coordinates(self, reference_frame: ReferenceFrame | None = None) -> numpy.ndarray:
|
|
593
|
+
"""Returns the coordinates of the point.
|
|
493
594
|
|
|
494
|
-
|
|
495
|
-
|
|
595
|
+
Args:
|
|
596
|
+
reference_frame (ReferenceFrame | None): Reference frame in which the point coordinates are returned.
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
Coordinates of the point in the given reference frame.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
if reference_frame is None:
|
|
496
603
|
return self.coordinates
|
|
497
604
|
else:
|
|
498
|
-
return self.
|
|
605
|
+
return self.express_in(reference_frame)
|
|
499
606
|
|
|
500
|
-
def
|
|
501
|
-
"""
|
|
502
|
-
|
|
607
|
+
def express_in(self, target_frame: ReferenceFrame) -> np.ndarray:
|
|
608
|
+
"""Expresses the coordinates in another reference frame.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
target_frame (ReferenceFrame): Target reference frame.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Coordinates in the target reference frame.
|
|
503
615
|
"""
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
targetFrame == self.ref
|
|
507
|
-
We're after the definition of self
|
|
508
|
-
"""
|
|
616
|
+
|
|
617
|
+
if target_frame == self.reference_frame:
|
|
509
618
|
result = self.coordinates
|
|
510
619
|
else:
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
We know the coordinates in self.ref
|
|
515
|
-
#We need to apply the transformation from targetFrame to self.ref
|
|
516
|
-
self.ref --> self (self.transformation)
|
|
517
|
-
"""
|
|
518
|
-
# transform = targetFrame.getTransformationFrom(self.ref)
|
|
519
|
-
transform = self.ref.getPassiveTransformationTo(targetFrame)
|
|
620
|
+
# Apply coordinate transformation of self.coordinates from self.reference_frame to target_frame
|
|
621
|
+
transform = self.reference_frame.get_passive_transformation_to(target_frame)
|
|
520
622
|
if self.debug:
|
|
521
623
|
print("transform \n{0}".format(transform))
|
|
522
624
|
result = np.dot(transform, self.coordinates)
|
|
523
625
|
return result
|
|
524
626
|
|
|
525
|
-
def
|
|
526
|
-
"""
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
self.setCoordinates(newCoordinates)
|
|
533
|
-
self.ref = targetFrame
|
|
534
|
-
return
|
|
627
|
+
def change_reference_frame(self, target_frame: ReferenceFrame):
|
|
628
|
+
"""Re-defines `self` as attached to another reference frame.
|
|
629
|
+
|
|
630
|
+
This is done by:
|
|
631
|
+
- Calculating the coordinates of `self` in the target reference frame,
|
|
632
|
+
- Updating the definition of `self` in the new reference frame (with the newly calculated coordinates and the
|
|
633
|
+
target reference frame).
|
|
535
634
|
|
|
536
|
-
|
|
635
|
+
Args:
|
|
636
|
+
target_frame (ReferenceFrame): Target reference frame.
|
|
537
637
|
"""
|
|
538
|
-
|
|
638
|
+
|
|
639
|
+
new_coordinates = self.express_in(target_frame)
|
|
640
|
+
self.set_coordinates(new_coordinates)
|
|
641
|
+
|
|
642
|
+
self.reference_frame = target_frame
|
|
643
|
+
|
|
644
|
+
def get_num_points(self) -> int:
|
|
645
|
+
"""Returns the number of points in the set of points.
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
Number of points in the set of points.
|
|
539
649
|
"""
|
|
540
|
-
return Point(self.coordinates[:, index], ref=self.ref, name=name)
|
|
541
650
|
|
|
542
|
-
|
|
651
|
+
return self.coordinates.shape[1]
|
|
652
|
+
|
|
653
|
+
def get_point(self, index: int, name: str | None = None) -> Point:
|
|
654
|
+
"""Returns the point with the given index and assigns the given name to it.
|
|
543
655
|
|
|
544
|
-
|
|
656
|
+
Args:
|
|
657
|
+
index (int): Index of the point to return.
|
|
658
|
+
name (str): Name of the point.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
Point with the given index.
|
|
545
662
|
"""
|
|
546
|
-
bestFittingPlane(self,fitPlane="xy", usesvd=False,verbose=True)
|
|
547
663
|
|
|
548
|
-
|
|
549
|
-
|
|
664
|
+
return Point(self.coordinates[:, index], reference_frame=self.reference_frame, name=name)
|
|
665
|
+
|
|
666
|
+
get = get_point
|
|
667
|
+
|
|
668
|
+
def best_fitting_plane(self, plane: str = "xy", use_svd: bool = False, verbose: bool = True):
|
|
669
|
+
"""Returns the reference frame with the given plane as best fitting to all points in this collection of points.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
plane (str): Plane to fit. Must be in ["xy", "yz", "zx"].
|
|
673
|
+
use_svd (bool): Indicates whether to use SVD-base solution (Singular Value Decomposition) for
|
|
674
|
+
rigid/similarity transformations. If False and ndims = 3, the quaternion-based solution is
|
|
675
|
+
used.
|
|
676
|
+
verbose (bool): Indicates whether to print verbose output.
|
|
550
677
|
|
|
551
|
-
|
|
552
|
-
|
|
678
|
+
Returns:
|
|
679
|
+
Reference frame with the given plane as best fitting to all points in this collection of points.
|
|
553
680
|
"""
|
|
681
|
+
|
|
554
682
|
# Import necessary due to a circular dependency between Point and ReferenceFrame
|
|
555
|
-
from egse.coordinates.
|
|
683
|
+
from egse.coordinates.reference_frame import ReferenceFrame
|
|
556
684
|
|
|
557
685
|
debug = True
|
|
558
686
|
|
|
559
|
-
a, b, c = self.
|
|
560
|
-
# print (f"a {a}, b {b}, c {c}")
|
|
561
|
-
# print()
|
|
687
|
+
a, b, c = self.fit_plane(plane=plane, verbose=verbose)
|
|
562
688
|
|
|
563
|
-
|
|
564
|
-
# print(f"
|
|
565
|
-
# print()
|
|
689
|
+
unit_axes = Points.from_plane_parameters(a, b, c, reference_frame=self.reference_frame, plane=plane)
|
|
690
|
+
# print(f"Unit axes coordinates \n{np.round(unit_axes.coordinates,3)}")
|
|
566
691
|
|
|
567
|
-
#
|
|
692
|
+
# unit_axes contain 3 unit axes and an origin
|
|
568
693
|
# => the unit vectors do NOT belong to the target plane
|
|
569
694
|
# => they must be translated before
|
|
570
|
-
|
|
695
|
+
unit_coordinates = unit_axes.coordinates
|
|
571
696
|
for ax in range(3):
|
|
572
|
-
|
|
697
|
+
unit_coordinates[:3, ax] += unit_coordinates[:3, 3]
|
|
573
698
|
|
|
574
|
-
|
|
699
|
+
new_axes = Points(unit_coordinates, reference_frame=self.reference_frame)
|
|
575
700
|
|
|
576
|
-
# print(f"
|
|
701
|
+
# print(f"new_axes {np.round(new_axes.coordinates,3)}")
|
|
577
702
|
|
|
578
|
-
|
|
703
|
+
self_axes = Points(np.identity(4), reference_frame=self.reference_frame)
|
|
579
704
|
|
|
580
705
|
transform = t3add.affine_matrix_from_points(
|
|
581
|
-
|
|
706
|
+
self_axes.coordinates[:3, :], new_axes.coordinates[:3, :], shear=False, scale=False, use_svd=use_svd
|
|
582
707
|
)
|
|
583
708
|
|
|
584
709
|
if debug:
|
|
585
|
-
transform2 = t3add.
|
|
586
|
-
selfaxes.coordinates[:3, :], newaxes.coordinates[:3, :], verbose=verbose
|
|
587
|
-
)
|
|
710
|
+
transform2 = t3add.rigid_transform_3d(self_axes.coordinates[:3, :], new_axes.coordinates[:3, :])
|
|
588
711
|
|
|
589
712
|
if verbose:
|
|
590
|
-
print()
|
|
591
713
|
print(f"Transform \n{np.round(transform, 3)}")
|
|
592
714
|
if debug:
|
|
593
|
-
print()
|
|
594
715
|
print(f"Transform2 \n{np.round(transform2, 3)}")
|
|
595
|
-
print()
|
|
596
716
|
print(f"Both methods consistent ? {np.allclose(transform, transform2)}")
|
|
597
717
|
|
|
598
|
-
return ReferenceFrame(transformation=transform,
|
|
718
|
+
return ReferenceFrame(transformation=transform, reference_frame=self.reference_frame)
|
|
599
719
|
|
|
600
|
-
def
|
|
601
|
-
"""
|
|
602
|
-
fitPlane(self,fitPlane="xy",verbose=True)
|
|
720
|
+
def fit_plane(self, plane: str = "xy", verbose: bool = True) -> tuple[float, float, float]:
|
|
721
|
+
"""Fits the plane best fitting the points.
|
|
603
722
|
|
|
604
|
-
|
|
723
|
+
Depending on the `plane` parameter, the plane is fitted in the xy, yz, or zx plane:
|
|
724
|
+
- "xy": z = ax + by + c
|
|
725
|
+
- "yz": x = ay + bz + c
|
|
726
|
+
- "zx": y = az + bx + c
|
|
605
727
|
|
|
606
|
-
:
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
'zx' : y = az + bx + c
|
|
728
|
+
Args:
|
|
729
|
+
plane (str): Plane to fit. Must be in ["xy", "yz", "zx"].
|
|
730
|
+
verbose (bool): If True, print the results on screen.
|
|
610
731
|
|
|
611
|
-
:
|
|
732
|
+
Returns:
|
|
733
|
+
Best fitting parameters (a, b, c), corresponding to the fitted plane.
|
|
612
734
|
"""
|
|
735
|
+
|
|
613
736
|
xyz = [self.x, self.y, self.z]
|
|
614
737
|
|
|
615
|
-
|
|
738
|
+
num_points = len(xyz[0])
|
|
616
739
|
|
|
617
|
-
|
|
618
|
-
startingIndex = {"xy": 0, "yz": 1, "zx": 2}[fitPlane]
|
|
740
|
+
starting_index = {"xy": 0, "yz": 1, "zx": 2}[plane]
|
|
619
741
|
|
|
620
|
-
#
|
|
621
|
-
|
|
742
|
+
# Coefficients matrix
|
|
743
|
+
coefficients_matrix = np.vstack([xyz[starting_index], xyz[(starting_index + 1) % 3], np.ones(num_points)]).T
|
|
622
744
|
|
|
623
745
|
# Solve linear equations
|
|
624
|
-
a, b, c = np.linalg.lstsq(
|
|
746
|
+
a, b, c = np.linalg.lstsq(coefficients_matrix, xyz[(starting_index + 2) % 3], rcond=None)[0]
|
|
625
747
|
|
|
626
748
|
# Print results on screen
|
|
627
749
|
if verbose:
|
|
628
750
|
hprint = {"xy": "z = ax + by + c", "yz": "x = ay + bz + c", "zx": "y = az + bx + c"}
|
|
629
|
-
print(f"{hprint[
|
|
751
|
+
print(f"{hprint[plane]} : \n a = {a:7.3e} \n b = {b:7.3e} \n c = {c:7.3e}")
|
|
630
752
|
|
|
631
753
|
return a, b, c
|
|
632
754
|
|
|
633
755
|
@classmethod
|
|
634
|
-
def
|
|
635
|
-
|
|
756
|
+
def from_plane_parameters(
|
|
757
|
+
cls, a: float, b: float, c: float, reference_frame: ReferenceFrame, plane: str = "xy", verbose: bool = False
|
|
758
|
+
):
|
|
759
|
+
"""Returns the unit axes and the origin of the given reference frame.
|
|
636
760
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
761
|
+
Args:
|
|
762
|
+
a (float): Coefficient `a` in the equation of the target plane (z = ax + by + c)
|
|
763
|
+
b (float): Coefficient `b` in the equation of the target plane (z = ax = by + c)
|
|
764
|
+
c (float): Coefficient `c` in the equation of the target plane (z = ax + by + c)
|
|
765
|
+
reference_frame (ReferenceFrame): Reference frame.
|
|
766
|
+
plane (str): Plane to fit. Must be in ["xy", "yz", "zx"].
|
|
767
|
+
verbose (bool): If True, print the results on screen.
|
|
644
768
|
|
|
645
|
-
:
|
|
646
|
-
|
|
647
|
-
- the origin
|
|
648
|
-
in this order (origin last)
|
|
769
|
+
Returns:
|
|
770
|
+
Points object describing the unit axes and the origin of the reference frame defined by the input parameters.
|
|
649
771
|
"""
|
|
772
|
+
|
|
650
773
|
if plane != "xy":
|
|
651
774
|
print(f"WARNING: plane = {plane} NOT IMPLEMENTED")
|
|
652
|
-
# p0 : on the Z-axis
|
|
653
|
-
# x = y = 0
|
|
654
|
-
p0 = np.array([0, 0, c])
|
|
655
775
|
|
|
656
|
-
|
|
776
|
+
origin_coords = np.array([0, 0, c]) # Origin coordinates (x = y = 0, z = c)
|
|
777
|
+
|
|
778
|
+
# Intersection between
|
|
779
|
+
# - Target plane (z = ax + by + c)
|
|
780
|
+
# - Plane // to xy passing through z=c
|
|
781
|
+
|
|
657
782
|
if np.abs(b) > 1.0e-5:
|
|
658
|
-
# z = c, x = 1
|
|
659
|
-
pxy = np.array([1, -a / float(b), c])
|
|
783
|
+
pxy = np.array([1, -a / float(b), c]) # z = c, x = 1
|
|
660
784
|
else:
|
|
661
|
-
# z = c, y = 1
|
|
662
|
-
pxy = np.array([-b / float(a), 1, c])
|
|
785
|
+
pxy = np.array([-b / float(a), 1, c]) # z = c, y = 1
|
|
663
786
|
|
|
664
|
-
#
|
|
665
|
-
#
|
|
666
|
-
|
|
787
|
+
# Intersection between:
|
|
788
|
+
# - Target plane (z = ax + by + c)
|
|
789
|
+
# - yz-plane (x = 0)
|
|
667
790
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
#
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
791
|
+
pyz = np.array([0, 1, b + c]) # x = 0, y = 1
|
|
792
|
+
|
|
793
|
+
# # pzx : intersection of target plane and Z-X plane
|
|
794
|
+
# if np.abs(a)>1.e-3:
|
|
795
|
+
# # y = 0, z = 1
|
|
796
|
+
# pzx = np.array([(1-c)/float(a),0,1])
|
|
797
|
+
# else:
|
|
798
|
+
# # y = 0, x = 1
|
|
799
|
+
# pzx = np.array([1,0,a+c])
|
|
800
|
+
|
|
801
|
+
# Unit vector from [0, 0, 0] along the intersection between the target plane and the plane // to xy passing
|
|
802
|
+
# through z=c
|
|
803
|
+
|
|
804
|
+
x_unit_coords = (pxy - origin_coords) / np.linalg.norm(pxy - origin_coords) # Normalise
|
|
805
|
+
|
|
806
|
+
# Unit vector from [0, 0, 0] along the intersection between the target plane and the yz-plane
|
|
807
|
+
# In the target plane but not perpendicular to x_unit_coords (its norm doesn't matter)
|
|
808
|
+
|
|
809
|
+
y_temp = pyz - origin_coords # /np.linalg.norm(pyz-p0)
|
|
810
|
+
|
|
811
|
+
# x_unit_coords and y_temp are both in the plane
|
|
812
|
+
# -> z_unit_coords is perpendicular to both
|
|
813
|
+
|
|
814
|
+
z_unit_coords = np.cross(x_unit_coords, y_temp)
|
|
815
|
+
z_unit_coords /= np.linalg.norm(z_unit_coords) # Normalise
|
|
816
|
+
|
|
817
|
+
# y_unit_coords completes the right-handed reference frame
|
|
818
|
+
|
|
819
|
+
y_unit_coords = np.cross(z_unit_coords, x_unit_coords)
|
|
820
|
+
y_unit_coords /= np.linalg.norm(y_unit_coords) # Normalise
|
|
821
|
+
|
|
822
|
+
x_unit_point = Point(x_unit_coords, reference_frame=reference_frame)
|
|
823
|
+
y_unit_point = Point(y_unit_coords, reference_frame=reference_frame)
|
|
824
|
+
z_unit_point = Point(z_unit_coords, reference_frame=reference_frame)
|
|
825
|
+
origin = Point(origin_coords, reference_frame=reference_frame)
|
|
699
826
|
|
|
700
827
|
if verbose:
|
|
701
|
-
print(f"
|
|
702
|
-
print(f"
|
|
703
|
-
print(f"
|
|
704
|
-
print(f"
|
|
705
|
-
print()
|
|
828
|
+
print(f"x_unit_coords {x_unit_coords}")
|
|
829
|
+
print(f"y_unit_coords {y_unit_coords}")
|
|
830
|
+
print(f"z_unit_coords {z_unit_coords}")
|
|
831
|
+
print(f"origin_coords {origin_coords}")
|
|
706
832
|
|
|
707
|
-
return cls([
|
|
833
|
+
return cls([x_unit_point, y_unit_point, z_unit_point, origin], reference_frame=reference_frame)
|