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.
- CTPv/Camera/Intrinsics.py +205 -0
- CTPv/Camera/__init__.py +0 -0
- CTPv/Plucker/Line.py +526 -0
- CTPv/Plucker/__init__.py +0 -0
- CTPv/Transformation/TransformationMatrix.py +381 -0
- CTPv/Transformation/__init__.py +0 -0
- CTPv/__init__.py +0 -0
- ctpv-0.1.0.dist-info/METADATA +474 -0
- ctpv-0.1.0.dist-info/RECORD +12 -0
- ctpv-0.1.0.dist-info/WHEEL +5 -0
- ctpv-0.1.0.dist-info/licenses/LICENSE +7 -0
- ctpv-0.1.0.dist-info/top_level.txt +1 -0
@@ -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
|