hazy-frames 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hazy/__init__.py +29 -0
- hazy/constants.py +11 -0
- hazy/frame.py +494 -0
- hazy/primitives.py +834 -0
- hazy/utils.py +60 -0
- hazy_frames-0.1.0.dist-info/METADATA +253 -0
- hazy_frames-0.1.0.dist-info/RECORD +9 -0
- hazy_frames-0.1.0.dist-info/WHEEL +4 -0
- hazy_frames-0.1.0.dist-info/licenses/LICENSE +21 -0
hazy/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Coordinate system module for frame-aware geometric transformations.
|
|
2
|
+
|
|
3
|
+
This module provides hierarchical reference frames and geometric primitives (Point, Vector)
|
|
4
|
+
that carry frame information and support coordinate transformations.
|
|
5
|
+
|
|
6
|
+
Key classes:
|
|
7
|
+
- Frame: Hierarchical reference frame with transformation tracking
|
|
8
|
+
- Point: Geometric point with frame awareness (w=1)
|
|
9
|
+
- Vector: Geometric vector with frame awareness (w=0)
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from hazy import Frame, Point, Vector
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Create frames
|
|
15
|
+
>>> parent = Frame(name="parent")
|
|
16
|
+
>>> child = Frame(parent=parent, name="child")
|
|
17
|
+
>>> child.translate(x=5).rotate_euler(z=90, degrees=True)
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Create points in child frame
|
|
20
|
+
>>> p_local = Point(1, 0, 0, frame=child)
|
|
21
|
+
>>> p_parent = p_local.to_frame(parent)
|
|
22
|
+
>>> print(p_parent) # Point at (5, 1, 0) in parent frame
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from hazy.frame import Frame
|
|
27
|
+
from hazy.primitives import Point, Vector
|
|
28
|
+
|
|
29
|
+
__all__ = ["Frame", "Point", "Vector"]
|
hazy/constants.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.spatial.transform import Rotation
|
|
5
|
+
|
|
6
|
+
VSMALL: float = 1e-8
|
|
7
|
+
VVSMALL: float = 1e-12
|
|
8
|
+
|
|
9
|
+
IDENTITY_SCALE = np.ones(3, dtype=float)
|
|
10
|
+
IDENTITY_TRANSLATION = np.zeros(3, dtype=float)
|
|
11
|
+
IDENTITY_ROTATION = Rotation.identity()
|
hazy/frame.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Hierarchical reference frames with transformation tracking and caching.
|
|
2
|
+
|
|
3
|
+
This module provides a Frame class for managing coordinate system transformations
|
|
4
|
+
using homogeneous transformation matrices. Frames can be hierarchically organized
|
|
5
|
+
with parent-child relationships, and transformations are cached for performance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
from functools import reduce, wraps
|
|
12
|
+
from operator import add, mul
|
|
13
|
+
from typing import TYPE_CHECKING, Literal, Self, overload
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from scipy.spatial.transform import Rotation
|
|
17
|
+
|
|
18
|
+
from hazy.constants import IDENTITY_ROTATION, IDENTITY_SCALE, IDENTITY_TRANSLATION
|
|
19
|
+
from hazy.primitives import Point, Vector
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from numpy.typing import ArrayLike, NDArray
|
|
23
|
+
|
|
24
|
+
# from laser_cross_calibration.coordinate_system.primitives import Point, Vector
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def invalidate_transform_cache(method):
|
|
28
|
+
"""Decorator to invalidate cached transforms when frame is modified.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
method: Method that modifies the frame
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Wrapped method that clears caches before execution
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
RuntimeError: If frame is frozen
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@wraps(method)
|
|
41
|
+
def wrapper(self: Frame, *args, **kwargs):
|
|
42
|
+
if self._is_frozen:
|
|
43
|
+
raise RuntimeError("Can not modify frozen frame.")
|
|
44
|
+
self._cached_transform = None
|
|
45
|
+
self._cached_transform_global = None
|
|
46
|
+
return method(self, *args, **kwargs)
|
|
47
|
+
|
|
48
|
+
return wrapper
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Frame:
|
|
52
|
+
"""Hierarchical reference frame with transformation tracking.
|
|
53
|
+
|
|
54
|
+
Frames support accumulation of transformations (translation, rotation, scaling)
|
|
55
|
+
and provide cached transformation matrices for efficient repeated calculations.
|
|
56
|
+
|
|
57
|
+
Transformations are applied in S→R→T order (Scale, Rotation, Translation)
|
|
58
|
+
when converting from local to parent coordinates.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
parent: Parent frame in hierarchy (None for root frames)
|
|
62
|
+
name: Human-readable frame identifier
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
parent: Frame | None = None,
|
|
68
|
+
name: str | None = None,
|
|
69
|
+
):
|
|
70
|
+
"""Initialize a new reference frame.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
parent: Parent frame in hierarchy (None for root frames)
|
|
74
|
+
name: Frame identifier (auto-generated if not provided)
|
|
75
|
+
"""
|
|
76
|
+
self._parent: Frame | None = parent
|
|
77
|
+
self._name = name or f"Frame-{id(self)}"
|
|
78
|
+
|
|
79
|
+
self._rotations: list[Rotation] = [IDENTITY_ROTATION]
|
|
80
|
+
self._translations: list[NDArray[np.floating]] = [IDENTITY_TRANSLATION]
|
|
81
|
+
self._scalings: list[NDArray[np.floating]] = [IDENTITY_SCALE]
|
|
82
|
+
|
|
83
|
+
self._cached_transform: NDArray[np.floating] | None = None
|
|
84
|
+
self._cached_transform_global: NDArray[np.floating] | None = None
|
|
85
|
+
|
|
86
|
+
self._is_frozen = False
|
|
87
|
+
|
|
88
|
+
def __deepcopy__(self, memo):
|
|
89
|
+
"""Create a deep copy of the frame and its hierarchy."""
|
|
90
|
+
cls = self.__class__
|
|
91
|
+
result = cls.__new__(cls)
|
|
92
|
+
memo[id(self)] = result
|
|
93
|
+
|
|
94
|
+
for k, v in self.__dict__.items():
|
|
95
|
+
setattr(result, k, copy.deepcopy(v, memo))
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def parent(self) -> Frame | None:
|
|
101
|
+
return self._parent
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def name(self) -> str:
|
|
105
|
+
return self._name
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def root(self) -> Frame:
|
|
109
|
+
"""Get the root frame of this hierarchy.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Root frame (self if no parent, otherwise traverses up to root)
|
|
113
|
+
"""
|
|
114
|
+
current = self
|
|
115
|
+
while current.parent is not None:
|
|
116
|
+
current = current.parent
|
|
117
|
+
return current
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def combined_rotation(self) -> Rotation:
|
|
121
|
+
"""Combined rotation from all accumulated rotations."""
|
|
122
|
+
return reduce(mul, self._rotations)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def combined_scale(self) -> NDArray[np.floating]:
|
|
126
|
+
"""Combined scaling matrix from all accumulated scalings."""
|
|
127
|
+
return np.diag(np.append(reduce(mul, self._scalings), 1))
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def combined_translation(self) -> NDArray[np.floating]:
|
|
131
|
+
"""Combined translation vector from all accumulated translations."""
|
|
132
|
+
return reduce(add, self._translations)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def transform_to_parent(self) -> NDArray[np.floating]:
|
|
136
|
+
"""4x4 homogeneous transformation matrix from local to parent frame.
|
|
137
|
+
|
|
138
|
+
Transformations are applied in S→R→T order (Scale, Rotation, Translation).
|
|
139
|
+
Results are cached for performance.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
4x4 transformation matrix (copy to prevent modification)
|
|
143
|
+
"""
|
|
144
|
+
if self._cached_transform is not None:
|
|
145
|
+
return self._cached_transform.copy()
|
|
146
|
+
|
|
147
|
+
transform = np.eye(4)
|
|
148
|
+
|
|
149
|
+
transform = self.combined_scale @ transform
|
|
150
|
+
transform[:3, :3] = self.combined_rotation.as_matrix() @ transform[:3, :3]
|
|
151
|
+
transform[:3, 3] += self.combined_translation
|
|
152
|
+
|
|
153
|
+
self._cached_transform = transform.copy()
|
|
154
|
+
|
|
155
|
+
return transform.copy()
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def transform_from_parent(self) -> NDArray[np.floating]:
|
|
159
|
+
"""4x4 homogeneous transformation matrix from parent to local frame.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Inverse of transform_to_parent
|
|
163
|
+
"""
|
|
164
|
+
return np.linalg.inv(self.transform_to_parent)
|
|
165
|
+
|
|
166
|
+
def freeze(self) -> Self:
|
|
167
|
+
"""Freeze frame to prevent further modifications.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Self for method chaining
|
|
171
|
+
"""
|
|
172
|
+
self._is_frozen = True
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def unfreeze(self) -> Self:
|
|
176
|
+
"""Unfreeze frame to allow modifications.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Self for method chaining
|
|
180
|
+
"""
|
|
181
|
+
self._is_frozen = False
|
|
182
|
+
return self
|
|
183
|
+
|
|
184
|
+
@invalidate_transform_cache
|
|
185
|
+
def rotate_euler(
|
|
186
|
+
self,
|
|
187
|
+
*,
|
|
188
|
+
x: float = 0.0,
|
|
189
|
+
y: float = 0.0,
|
|
190
|
+
z: float = 0.0,
|
|
191
|
+
seq: Literal["xyz", "xzy", "yzx", "yxz", "zxy", "zyx"] = "xyz",
|
|
192
|
+
degrees: bool = False,
|
|
193
|
+
) -> Self:
|
|
194
|
+
"""Add Euler angle rotation to frame.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
x: Rotation around x-axis
|
|
198
|
+
y: Rotation around y-axis
|
|
199
|
+
z: Rotation around z-axis
|
|
200
|
+
seq: Rotation sequence (default: xyz)
|
|
201
|
+
degrees: If True, angles are in degrees, otherwise radians
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Self for method chaining
|
|
205
|
+
"""
|
|
206
|
+
R = Rotation.from_euler(seq=seq, angles=(x, y, z), degrees=degrees)
|
|
207
|
+
self._rotations.append(R)
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
@invalidate_transform_cache
|
|
211
|
+
def rotate(self, rotation) -> Self:
|
|
212
|
+
"""Add rotation matrix to frame.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
rotation: 3x3 rotation matrix
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Self for method chaining
|
|
219
|
+
"""
|
|
220
|
+
R = Rotation.from_matrix(rotation)
|
|
221
|
+
self._rotations.append(R)
|
|
222
|
+
return self
|
|
223
|
+
|
|
224
|
+
@invalidate_transform_cache
|
|
225
|
+
def translate(self, *, x=0.0, y=0.0, z=0.0) -> Self:
|
|
226
|
+
"""Add translation to frame.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
x: Translation along x-axis
|
|
230
|
+
y: Translation along y-axis
|
|
231
|
+
z: Translation along z-axis
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Self for method chaining
|
|
235
|
+
"""
|
|
236
|
+
translation = np.array([x, y, z], dtype=float)
|
|
237
|
+
self._translations.append(translation)
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
@invalidate_transform_cache
|
|
241
|
+
def scale(self, scale: float | tuple[float, float, float] | ArrayLike) -> Self:
|
|
242
|
+
"""Add scaling to frame.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
scale: Uniform scale factor or (sx, sy, sz) tuple
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Self for method chaining
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValueError: If tuple doesn't have exactly 3 elements
|
|
252
|
+
"""
|
|
253
|
+
if isinstance(scale, float | int):
|
|
254
|
+
scaling = np.ones(3, dtype=float) * scale
|
|
255
|
+
else:
|
|
256
|
+
scaling = np.asarray(scale, dtype=float).flatten()
|
|
257
|
+
if scaling.shape != (3,):
|
|
258
|
+
raise ValueError()
|
|
259
|
+
|
|
260
|
+
self._scalings.append(scaling)
|
|
261
|
+
return self
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def transform_to_global(self) -> NDArray[np.floating]:
|
|
265
|
+
"""4x4 transformation matrix from this frame to global frame.
|
|
266
|
+
|
|
267
|
+
Recursively composes transformations through parent hierarchy.
|
|
268
|
+
Results are cached for performance.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
4x4 transformation matrix
|
|
272
|
+
"""
|
|
273
|
+
if self._cached_transform_global is not None:
|
|
274
|
+
return self._cached_transform_global.copy()
|
|
275
|
+
|
|
276
|
+
if self.parent is None:
|
|
277
|
+
self._cached_transform_global = np.eye(4, dtype=float)
|
|
278
|
+
else:
|
|
279
|
+
self._cached_transform_global = (
|
|
280
|
+
self.parent.transform_to_global @ self.transform_to_parent
|
|
281
|
+
)
|
|
282
|
+
return self._cached_transform_global
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def transform_from_global(self) -> NDArray[np.floating]:
|
|
286
|
+
"""4x4 transformation matrix from global frame to this frame.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Inverse of transform_to_global
|
|
290
|
+
"""
|
|
291
|
+
return np.linalg.inv(self.transform_to_global)
|
|
292
|
+
|
|
293
|
+
def transform_to(self, target: Frame) -> NDArray[np.floating]:
|
|
294
|
+
"""Compute transformation matrix from this frame to target frame.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
target: Target reference frame
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
4x4 transformation matrix from self to target
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
RuntimeError: If frames belong to different hierarchies (different roots)
|
|
304
|
+
"""
|
|
305
|
+
if self == target:
|
|
306
|
+
return np.eye(4)
|
|
307
|
+
|
|
308
|
+
if self.root is not target.root:
|
|
309
|
+
raise RuntimeError(
|
|
310
|
+
f"Cannot transform between frames from different hierarchies. "
|
|
311
|
+
f"Frame '{self.name}' has root '{self.root.name}', "
|
|
312
|
+
f"but frame '{target.name}' has root '{target.root.name}'."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return target.transform_from_global @ self.transform_to_global
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def x_axis(self) -> Vector:
|
|
319
|
+
return Vector(x=1.0, y=0.0, z=0.0, frame=self)
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def x_axis_global(self) -> Vector:
|
|
323
|
+
return self.x_axis.to_frame(target_frame=self.root)
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def y_axis(self) -> Vector:
|
|
327
|
+
return Vector(x=0.0, y=1.0, z=0.0, frame=self)
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def y_axis_global(self) -> Vector:
|
|
331
|
+
return self.y_axis.to_frame(target_frame=self.root)
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def z_axis(self) -> Vector:
|
|
335
|
+
return Vector(x=0.0, y=0.0, z=1.0, frame=self)
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def z_axis_global(self) -> Vector:
|
|
339
|
+
return self.z_axis.to_frame(target_frame=self.root)
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def origin(self) -> Point:
|
|
343
|
+
return Point(x=0.0, y=0.0, z=0.0, frame=self)
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def origin_global(self):
|
|
347
|
+
return self.origin.to_frame(target_frame=self.root)
|
|
348
|
+
|
|
349
|
+
@overload
|
|
350
|
+
def vector(self, x: float, y: float, z: float) -> Vector: ...
|
|
351
|
+
|
|
352
|
+
@overload
|
|
353
|
+
def vector(self, x: ArrayLike) -> Vector: ...
|
|
354
|
+
|
|
355
|
+
def vector(
|
|
356
|
+
self, x: float | ArrayLike, y: float | None = None, z: float | None = None
|
|
357
|
+
) -> Vector:
|
|
358
|
+
"""Create vector in this frame.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
x: X-coordinate or array-like [x, y, z]
|
|
362
|
+
y: Y-coordinate (required if x is scalar)
|
|
363
|
+
z: Z-coordinate (required if x is scalar)
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Vector in this frame
|
|
367
|
+
|
|
368
|
+
Examples:
|
|
369
|
+
>>> frame.vector(1.0, 2.0, 3.0)
|
|
370
|
+
>>> frame.vector([1, 2, 3])
|
|
371
|
+
>>> frame.vector(np.array([1, 2, 3]))
|
|
372
|
+
"""
|
|
373
|
+
if y is None and z is None:
|
|
374
|
+
return Vector.from_array(x, frame=self)
|
|
375
|
+
elif y is not None and z is not None:
|
|
376
|
+
return Vector(x=x, y=y, z=z, frame=self)
|
|
377
|
+
else:
|
|
378
|
+
raise ValueError("Provide either (x, y, z) or single array-like")
|
|
379
|
+
|
|
380
|
+
@overload
|
|
381
|
+
def point(self, x: float, y: float, z: float) -> Point: ...
|
|
382
|
+
|
|
383
|
+
@overload
|
|
384
|
+
def point(self, x: ArrayLike) -> Point: ...
|
|
385
|
+
|
|
386
|
+
def point(
|
|
387
|
+
self, x: float | ArrayLike, y: float | None = None, z: float | None = None
|
|
388
|
+
) -> Point:
|
|
389
|
+
"""Create point in this frame.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
x: X-coordinate or array-like [x, y, z]
|
|
393
|
+
y: Y-coordinate (required if x is scalar)
|
|
394
|
+
z: Z-coordinate (required if x is scalar)
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Point in this frame
|
|
398
|
+
|
|
399
|
+
Examples:
|
|
400
|
+
>>> frame.point(1.0, 2.0, 3.0)
|
|
401
|
+
>>> frame.point([1, 2, 3])
|
|
402
|
+
>>> frame.point(np.array([1, 2, 3]))
|
|
403
|
+
"""
|
|
404
|
+
if y is None and z is None:
|
|
405
|
+
return Point.from_array(x, frame=self)
|
|
406
|
+
elif y is not None and z is not None:
|
|
407
|
+
return Point(x=x, y=y, z=z, frame=self)
|
|
408
|
+
else:
|
|
409
|
+
raise ValueError("Provide either (x, y, z) or single array-like")
|
|
410
|
+
|
|
411
|
+
def batch_transform_points_global(
|
|
412
|
+
self, points: NDArray[np.floating]
|
|
413
|
+
) -> NDArray[np.floating]:
|
|
414
|
+
"""Batch transform an array of points from this coordinate system to global.
|
|
415
|
+
Homogenous coordinate (w=1) will be added automatically.
|
|
416
|
+
|
|
417
|
+
Args
|
|
418
|
+
points: array of 3d points, will be reshaped to (N, 3)
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
points transformed to global space
|
|
422
|
+
"""
|
|
423
|
+
points = np.asarray(points)
|
|
424
|
+
original_shape = points.shape
|
|
425
|
+
points_homogenous = np.hstack(
|
|
426
|
+
[points.reshape(-1, 3), np.ones((points.size // 3, 1))]
|
|
427
|
+
)
|
|
428
|
+
transformed = points_homogenous @ self.transform_to_global.T
|
|
429
|
+
return transformed[:, :3].reshape(original_shape)
|
|
430
|
+
|
|
431
|
+
def batch_transform_vectors_global(
|
|
432
|
+
self, vectors: NDArray[np.floating]
|
|
433
|
+
) -> NDArray[np.floating]:
|
|
434
|
+
"""Batch transform an array of vectors from this coordinate system to global.
|
|
435
|
+
Homogenous coordinate (w=0) will be added automatically.
|
|
436
|
+
|
|
437
|
+
Args
|
|
438
|
+
vectors: array of 3d vectors, will be reshaped to (N, 3)
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
vectors transformed to global space
|
|
442
|
+
"""
|
|
443
|
+
vectors = np.asarray(vectors)
|
|
444
|
+
original_shape = vectors.shape
|
|
445
|
+
vectors_homogenous = np.hstack(
|
|
446
|
+
[vectors.reshape(-1, 3), np.ones((vectors.size // 3, 1))]
|
|
447
|
+
)
|
|
448
|
+
transformed = vectors_homogenous @ self.transform_to_global.T
|
|
449
|
+
return transformed[:, :3].reshape(original_shape)
|
|
450
|
+
|
|
451
|
+
def __repr__(self) -> str:
|
|
452
|
+
parent_name = self.parent.name if self.parent else "None"
|
|
453
|
+
# Subtract 1 because we always have identity elements
|
|
454
|
+
n_rot = len(self._rotations) - 1
|
|
455
|
+
n_trans = len(self._translations) - 1
|
|
456
|
+
n_scale = len(self._scalings) - 1
|
|
457
|
+
transforms = f"{n_rot}R+{n_trans}T+{n_scale}S"
|
|
458
|
+
frozen = " [FROZEN]" if self._is_frozen else ""
|
|
459
|
+
return (
|
|
460
|
+
f"Frame('{self.name}', "
|
|
461
|
+
f"parent='{parent_name}', "
|
|
462
|
+
f"transforms={transforms}{frozen})"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
@classmethod
|
|
466
|
+
def make_root(cls, name: str | None = None) -> Frame:
|
|
467
|
+
"""Create a root frame (frame without parent).
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
name: Optional name for the root frame
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
New root frame
|
|
474
|
+
|
|
475
|
+
Example:
|
|
476
|
+
>>> root = Frame.make_root(name="world")
|
|
477
|
+
>>> robot = root.make_child(name="robot")
|
|
478
|
+
"""
|
|
479
|
+
return cls(parent=None, name=name)
|
|
480
|
+
|
|
481
|
+
def make_child(self, name: str | None = None) -> Frame:
|
|
482
|
+
"""Creates a frame with this frame as its parent.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
name: Optional name for the child frame
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
New child frame
|
|
489
|
+
|
|
490
|
+
Example:
|
|
491
|
+
>>> root = Frame.make_root(name="world")
|
|
492
|
+
>>> child = root.make_child(name="child")
|
|
493
|
+
"""
|
|
494
|
+
return Frame(parent=self, name=name)
|