CTPv 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.
@@ -0,0 +1,381 @@
1
+ import os
2
+ import numpy as np
3
+ import open3d as o3d
4
+ import matplotlib.pyplot as plt
5
+ from scipy.spatial.transform import Rotation as R
6
+ # Define the Arrow3D class
7
+ # from mpl_toolkits.mplot3d import Axes3D
8
+ # from mpl_toolkits.mplot3d.art3d import Poly3DCollection
9
+ # from matplotlib.patches import FancyArrowPatch
10
+ # from mpl_toolkits.mplot3d import proj3d
11
+
12
+ class TransformationMatrix:
13
+ """
14
+ A class to represent and manipulate a 4x4 transformation matrix.
15
+ This class provides methods to handle transformations including translation, rotation, and their combinations.
16
+ It also supports various ways to set and get the transformation properties such as Euler angles and quaternions.
17
+ """
18
+ def __init__(self):
19
+ """
20
+ Initialize the transformation matrix as a 4x4 identity matrix.
21
+ Additional attributes include info and units for metadata and unit specification.
22
+ """
23
+ self.H = np.eye(4)
24
+ self.info = ["default1", "default2"]
25
+ self.units = "mm"
26
+
27
+ @property
28
+ def T(self):
29
+ """Get the translation vector from the transformation matrix."""
30
+ return self.H[:3, 3]
31
+
32
+ @T.setter
33
+ def T(self, t):
34
+ """
35
+ Set the translation vector of the transformation matrix.
36
+
37
+ Parameters:
38
+ t : array-like
39
+ The translation vector with three elements.
40
+ """
41
+ t = np.asarray(t).flatten()
42
+ if t.shape[0] == 3:
43
+ self.H[:3, 3] = t
44
+ else:
45
+ raise ValueError("Translation vector must have 3 elements.")
46
+
47
+ @property
48
+ def R(self):
49
+ """Get the rotation matrix from the transformation matrix."""
50
+ return self.H[:3, :3]
51
+
52
+ @R.setter
53
+ def R(self, r):
54
+ """
55
+ Set the rotation matrix of the transformation matrix.
56
+
57
+ Parameters:
58
+ r : array-like
59
+ The 3x3 rotation matrix.
60
+ """
61
+ r = np.asarray(r)
62
+ if r.shape == (3, 3):
63
+ self.H[:3, :3] = r
64
+ else:
65
+ raise ValueError("Rotation matrix must be 3x3.")
66
+
67
+ @property
68
+ def angles(self):
69
+ """Get Euler angles in radians (XYZ convention) from the rotation matrix."""
70
+ return R.from_matrix(self.R).as_euler('xyz')
71
+
72
+ @angles.setter
73
+ def angles(self, angles):
74
+ """
75
+ Set the rotation matrix using Euler angles (in radians) with XYZ convention.
76
+
77
+ Parameters:
78
+ angles : array-like
79
+ The Euler angles in radians.
80
+ """
81
+ self.R = R.from_euler('xyz', angles).as_matrix()
82
+
83
+ @property
84
+ def angles_degree(self):
85
+ """Get Euler angles in degrees from the rotation matrix."""
86
+ return np.degrees(self.angles)
87
+
88
+ @angles_degree.setter
89
+ def angles_degree(self, angles):
90
+ """
91
+ Set the rotation matrix using Euler angles in degrees.
92
+
93
+ Parameters:
94
+ angles : array-like
95
+ The Euler angles in degrees.
96
+ """
97
+ self.angles = np.radians(angles)
98
+
99
+ @property
100
+ def quaternion(self):
101
+ """Get the quaternion representation of the rotation matrix."""
102
+ return R.from_matrix(self.R).as_quat()
103
+
104
+ @quaternion.setter
105
+ def quaternion(self, quat):
106
+ """
107
+ Set the rotation matrix using a quaternion.
108
+
109
+ Parameters:
110
+ quat : array-like
111
+ The quaternion representing the rotation.
112
+ """
113
+ self.R = R.from_quat(quat).as_matrix()
114
+
115
+ def transform(self, points):
116
+ """
117
+ Apply the transformation to a set of 3D points.
118
+
119
+ Parameters:
120
+ points : array-like
121
+ Nx3 array of points to transform.
122
+
123
+ Returns:
124
+ numpy.ndarray
125
+ Transformed Nx3 array of points.
126
+ """
127
+ points = np.asarray(points)
128
+ if points.ndim == 1 and points.shape[0] == 3:
129
+ points = points.reshape(1, 3)
130
+ elif points.ndim != 2 or points.shape[1] != 3:
131
+ raise ValueError("Input points should have shape (N, 3) or (3,)")
132
+
133
+ # Convert to homogeneous coordinates
134
+ homogeneous_points = np.hstack((points, np.ones((points.shape[0], 1))))
135
+
136
+ # Apply transformation
137
+ transformed_points = (self.H @ homogeneous_points.T).T
138
+
139
+ return transformed_points[:, :3] # Remove the homogeneous coordinate
140
+
141
+ def invert(self):
142
+ """
143
+ Invert the transformation matrix.
144
+ This method computes the inverse of the transformation matrix and updates the matrix and its info.
145
+ """
146
+ H_inv = np.linalg.inv(self.H)
147
+ self.info = self.info[::-1]
148
+ self.H = H_inv
149
+
150
+ def save_bundler_file(self, output_file, intrinsics = None):
151
+ """
152
+ Write a Bundler file (v0.3) for MeshLab texturing.
153
+
154
+ Parameters:
155
+ T : (4,4) np.ndarray
156
+ The provided transformation matrix (assumed camera-to-world).
157
+ K : (3,3) np.ndarray
158
+ The intrinsic matrix.
159
+ output_file : str
160
+ The filename for the output Bundler file.
161
+ """
162
+ # Compute the average focal length from the intrinsics.
163
+ # Bundler expects a single focal length (in pixels).
164
+ if intrinsics==None:
165
+ K = np.array([
166
+ [1.770689941406250000e+03, 0.000000000000000000e+00, 6.852999877929687500e+02],
167
+ [0.000000000000000000e+00, 1.765030029296875000e+03, 4.927000122070312500e+02],
168
+ [0.000000000000000000e+00, 0.000000000000000000e+00, 1.000000000000000000e+00]
169
+ ])
170
+ else:
171
+ K = intrinsics
172
+ f = (K[0, 0] + K[1, 1]) / 2.0
173
+
174
+ # If no radial distortion is provided, set them to zero.
175
+ k1, k2 = 0.0, 0.0
176
+
177
+ # Extract the rotation matrix (R) and translation vector (t) from T.
178
+
179
+ # For Bundler, we need the world-to-camera transformation.
180
+ # If T is camera-to-world, its inverse is:
181
+ # R_inv = R^T, and t_inv = -R^T * t
182
+
183
+ R_inv = self.H[:3, :3]
184
+ t_inv = self.H[:3, 3]
185
+ # Build the Bundler file content.
186
+ lines = []
187
+ lines.append("# Bundle file v0.3")
188
+ lines.append("1 0") # one camera, zero points
189
+ lines.append(f"{f:.8f} {k1:.8f} {k2:.8f}")
190
+
191
+ # Add the rotation matrix rows (world-to-camera rotation).
192
+ for row in R_inv:
193
+ lines.append(" ".join(f"{val:.8f}" for val in row))
194
+ # Add the translation vector.
195
+ lines.append(" ".join(f"{val:.8f}" for val in t_inv))
196
+
197
+ bundler_content = "\n".join(lines)
198
+
199
+ # Write the content to the specified output file.
200
+ with open(output_file, "w") as f_out:
201
+ f_out.write(bundler_content)
202
+
203
+ print(f"Bundler file written to {output_file}")
204
+
205
+ def load_bundler_file(self, filename):
206
+ """
207
+ Load the transformation matrix from a Bundler file, ignoring the first 3 lines.
208
+
209
+ Parameters:
210
+ filename : str
211
+ Path to the input Bundler file.
212
+ """
213
+ with open(filename, 'r') as f:
214
+ lines = f.readlines()[3:] # Ignore the first 3 lines
215
+
216
+ # Read the rotation matrix
217
+ R = []
218
+ for i in range(3):
219
+ R.append(list(map(float, lines.pop(0).split())))
220
+ self.R = np.array(R)
221
+
222
+ # Read the translation vector
223
+ T = list(map(float, lines.pop(0).split()))
224
+ self.T = np.array(T)
225
+
226
+ def plot(self, scale=1.0):
227
+ """
228
+ Plot the transformation as a coordinate frame using matplotlib.
229
+
230
+ Parameters:
231
+ scale : float, optional
232
+ The scale of the coordinate frame axes.
233
+ """
234
+ fig = plt.figure()
235
+ ax = fig.add_subplot(111, projection="3d")
236
+
237
+ # Define the origin and the end points of the axes in homogeneous coordinates
238
+ origin = np.array([0, 0, 0])
239
+ x_axis = np.array([scale, 0, 0])
240
+ y_axis = np.array([0, scale, 0])
241
+ z_axis = np.array([0, 0, scale])
242
+
243
+ # Apply the transformation
244
+ origin_transformed = self.transform(origin)
245
+ x_axis_transformed = self.transform(x_axis)
246
+ y_axis_transformed = self.transform(y_axis)
247
+ z_axis_transformed = self.transform(z_axis)
248
+
249
+ # Plot the transformed axes
250
+
251
+ ax = fig.add_subplot(111, projection="3d")
252
+ ax.plot([origin_transformed[0, 0]], [origin_transformed[0, 1]], [origin_transformed[0, 2]], 'o', markersize=10,
253
+ color='red', alpha=0.5)
254
+ # Plot the lines
255
+ ax.plot([origin_transformed[0,0], x_axis_transformed[0, 0]], [origin_transformed[0,1], x_axis_transformed[0, 1]], [origin_transformed[0,2], x_axis_transformed[0, 2]],
256
+ color='red',label='X-axis')
257
+ ax.plot([origin_transformed[0,0], y_axis_transformed [0, 0]], [origin_transformed[0,1], y_axis_transformed [0, 1]], [origin_transformed[0,2], y_axis_transformed [0, 2]],
258
+ color='green',label='Y-axis')
259
+ ax.plot([origin_transformed[0,0], z_axis_transformed[0, 0]], [origin_transformed[0,1], z_axis_transformed[0, 1]], [origin_transformed[0,2], z_axis_transformed[0, 2]],
260
+ color='blue',label='Z-axis')
261
+
262
+
263
+ ax.set_xlabel("X")
264
+ ax.set_ylabel("Y")
265
+ ax.set_zlabel("Z")
266
+ ax.legend()
267
+
268
+ # Set the aspect ratio to be equal
269
+ ax.set_box_aspect([1, 1, 1])
270
+
271
+ plt.show()
272
+
273
+ def plot_open3d(self, scale=1.0):
274
+ """
275
+ Plot the transformation as a coordinate frame using Open3D.
276
+
277
+ Parameters:
278
+ scale : float, optional
279
+ The scale of the coordinate frame axes.
280
+ """
281
+ # Create the transformation coordinate frame
282
+ mesh_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=scale, origin=self.T)
283
+ R = self.R
284
+ mesh_frame.rotate(R, center=self.T)
285
+
286
+ # Create the base coordinate frame at the origin
287
+ base_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=scale, origin=[0, 0, 0])
288
+
289
+ # Create a marker (e.g., a sphere) at the origin
290
+ marker = o3d.geometry.TriangleMesh.create_sphere(radius=0.1)
291
+ marker.translate([0, 0, 0])
292
+ marker.paint_uniform_color([1, 0, 0]) # Red color
293
+
294
+ # Visualize both frames and the marker
295
+ o3d.visualization.draw_geometries([mesh_frame, base_frame, marker])
296
+
297
+ def copy(self):
298
+ """
299
+ Return a copy of the current TransformationMatrix instance.
300
+
301
+ Returns:
302
+ TransformationMatrix
303
+ A new instance of TransformationMatrix with the same transformation matrix and metadata.
304
+ """
305
+ new_instance = TransformationMatrix()
306
+ new_instance.H = np.copy(self.H)
307
+ new_instance.info = self.info[:]
308
+ new_instance.units = self.units
309
+ return new_instance
310
+
311
+ def load_from_json(self, filename):
312
+ """
313
+ Load the transformation matrix and metadata from a JSON file.
314
+
315
+ Parameters:
316
+ filename : str
317
+ Path to the JSON file.
318
+ """
319
+ import json
320
+ with open(filename, 'r') as f:
321
+ data = json.load(f)
322
+
323
+ self.H = np.array(data['H'])
324
+ self.info = data['info']
325
+ self.units = data['units']
326
+
327
+ def save_to_json(self, filename):
328
+ """
329
+ Save the transformation matrix and metadata to a JSON file.
330
+
331
+ Parameters:
332
+ filename : str
333
+ Path to the output JSON file.
334
+ """
335
+ import json
336
+ #make sure the directory exists
337
+ directory = os.path.dirname(filename)
338
+ if directory:
339
+ os.makedirs(directory, exist_ok=True)
340
+
341
+ data = {
342
+ 'H': self.H.tolist(),
343
+ 'info': self.info,
344
+ 'units': self.units
345
+ }
346
+ with open(filename, 'w') as f:
347
+ json.dump(data, f, indent=4)
348
+ def __matmul__(self, other):
349
+ """
350
+ Overload the @ operator for transformation chaining.
351
+
352
+ Parameters:
353
+ other : TransformationMatrix
354
+ Another TransformationMatrix to multiply with.
355
+
356
+ Returns:
357
+ TransformationMatrix
358
+ The resulting TransformationMatrix from the multiplication.
359
+ """
360
+ if not isinstance(other, TransformationMatrix):
361
+ raise TypeError("Can only multiply with another TransformationMatrix")
362
+
363
+ result = TransformationMatrix()
364
+ result.H = self.H @ other.H
365
+
366
+ if not self.info or not other.info:
367
+ raise ValueError("Info lists should not be empty")
368
+
369
+ info = [self.info[0], other.info[-1]]
370
+ result.info = info
371
+ return result
372
+
373
+ def __repr__(self):
374
+ """
375
+ Return a string representation of the TransformationMatrix.
376
+
377
+ Returns:
378
+ str
379
+ A string representation of the transformation matrix.
380
+ """
381
+ return f"TransformationMatrix(\n{self.H})"
File without changes
CTPv/__init__.py ADDED
File without changes