-
Notifications
You must be signed in to change notification settings - Fork 392
/
Copy pathtest_sim3solver.py
408 lines (309 loc) · 14.5 KB
/
test_sim3solver.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
import sys
import numpy as np
import time
import rerun as rr
import cv2
from matplotlib import pyplot as plt
sys.path.append("./lib/")
import sim3solver
fx = 517.306408
fy = 516.469215
cx = 318.643040
cy = 255.313989
width = 640
height = 480
K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]])
def horn_absolute_orientation(P, Q):
"""
Implementation of Horn's method for absolute orientation using unit quaternions.
Args:
P (np.ndarray): A (n, 3) array of points in the source frame.
Q (np.ndarray): A (n, 3) array of corresponding points in the target frame.
Returns:
R (np.ndarray): The optimal rotation matrix (3x3).
t (np.ndarray): The optimal translation vector (3x1).
"""
assert P.shape == Q.shape, "Point sets must have the same shape."
# Step 1: Compute the centroids of P and Q
centroid_P = np.mean(P, axis=0)
centroid_Q = np.mean(Q, axis=0)
# Step 2: Center the points by subtracting the centroids
P_prime = P - centroid_P
Q_prime = Q - centroid_Q
# Step 3: Compute the cross-covariance matrix H
H = P_prime.T @ Q_prime
# Step 4: Compute the SVD of H
U, S, Vt = np.linalg.svd(H)
# Step 5: Compute the optimal rotation matrix R
R = Vt.T @ U.T
# Handle the case where the determinant of R is negative
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
# Step 6: Compute the translation vector t
t = centroid_Q - R @ centroid_P
return R, t
def rotation_matrix_from_yaw_pitch_roll(yaw_degs, pitch_degs, roll_degs):
# Convert angles from degrees to radians
yaw = np.radians(yaw_degs)
pitch = np.radians(pitch_degs)
roll = np.radians(roll_degs)
# Rotation matrix for Roll (X-axis rotation)
Rx = np.array([
[1, 0, 0],
[0, np.cos(roll), -np.sin(roll)],
[0, np.sin(roll), np.cos(roll)]
])
# Rotation matrix for Pitch (Y-axis rotation)
Ry = np.array([
[np.cos(pitch), 0, np.sin(pitch)],
[0, 1, 0],
[-np.sin(pitch), 0, np.cos(pitch)]
])
# Rotation matrix for Yaw (Z-axis rotation)
Rz = np.array([
[np.cos(yaw), -np.sin(yaw), 0],
[np.sin(yaw), np.cos(yaw), 0],
[0, 0, 1]
])
# Final rotation matrix: R = Rz * Ry * Rx
R = Rz @ Ry @ Rx
return R
# generate random point in camera image
# output = [Nx2] (N = number of points)
def generate_random_points_2d(width, height, num_points):
points_2d = np.random.uniform(low=[0, 0], high=[width, height], size=(num_points, 2)).astype(np.float32)
return points_2d
# back project 2d image points with given camera matrix by using a random depths in the range [minD, maxD]
# output = [Nx3] (N = number of points)
def backproject_points(K, points_2d, minD, maxD):
fx = K[0, 0]
fy = K[1, 1]
cx = K[0, 2]
cy = K[1, 2]
z = np.random.uniform(minD, maxD, size=points_2d.shape[0])
x = (points_2d[:, 0] - cx) * z / fx
y = (points_2d[:, 1] - cy) * z / fy
points_3d = np.vstack((x, y, z)).T
return points_3d.astype(np.float32)
def project_points(K, points_3d_c, width, height):
num_points = points_3d_c.shape[0]
points_2d = np.zeros((num_points,2), dtype=np.float32)
mask = np.zeros((num_points,1), dtype=np.float32)
fx = K[0, 0]
fy = K[1, 1]
cx = K[0, 2]
cy = K[1, 2]
for i in range(num_points):
point = points_3d_c[i,:]
x = point[0] / point[2] * fx + cx
y = point[1] / point[2] * fy + cy
points_2d[i, :] = np.array([x, y])
if x >= 0 and x < width and y >= 0 and y < height:
mask[i] = 1
return points_2d, mask
def check_solution(solver_input_data, scale12, Rc1c2, tc1c2):
# check solution
# transform points from world coordinates to camera coordinates
points_3d_c1 = (solver_input_data.Rcw1 @ np.array(solver_input_data.points_3d_w1).T + solver_input_data.tcw1.reshape(3,1)).T
points_3d_c2 = (solver_input_data.Rcw2 @ np.array(solver_input_data.points_3d_w2).T + solver_input_data.tcw2.reshape(3,1)).T
aligned_points_c1 = (scale12*Rc1c2 @ np.array(points_3d_c2).T + tc1c2.reshape(3,1)).T
average_alignment_error = np.mean(np.linalg.norm(aligned_points_c1 - points_3d_c1, axis=1))
print(f'[check_solution] Average alignment error: {average_alignment_error}')
return average_alignment_error
def check_solution2(solver_input_data2, scale12, Rc1c2, tc1c2):
# check solution
# transform points from world coordinates to camera coordinates
points_3d_c1 = solver_input_data2.points_3d_c1
points_3d_c2 = solver_input_data2.points_3d_c2
aligned_points_c1 = (scale12*Rc1c2 @ np.array(points_3d_c2).T + tc1c2.reshape(3,1)).T
average_alignment_error = np.mean(np.linalg.norm(aligned_points_c1 - points_3d_c1, axis=1))
print(f'[check_solution2] Average alignment error: {average_alignment_error}')
return average_alignment_error
def create_mock_input():
# Creating mock data for Sim3SolverInput
num_points = 100
# grount-truth rotation and translation from world a to world b (this represents the pose estimation drift in Sim(3))
gt_drift_R = rotation_matrix_from_yaw_pitch_roll(10.4,-5.6,3.9) # input in degrees
gt_drift_t = np.array([0.1,-0.6, 0.2], dtype=np.float32)
gt_drift_s = 0.9
solver_input_data = sim3solver.Sim3SolverInput()
solver_input_data.fix_scale = False
# keyframes data
solver_input_data.K1 = K # Camera matrix for KF1
solver_input_data.Rcw1 = np.eye(3, dtype=np.float32) # Rotation matrix for KF1
solver_input_data.tcw1 = np.array([0.0, 0.0, 0.0], dtype=np.float32) # Translation vector for KF1
Rwc1 = solver_input_data.Rcw1.T
twc1 = (-Rwc1 @ solver_input_data.tcw1.T).T
solver_input_data.K2 = K # Camera matrix for KF2 (first, it coincides with KF1)
solver_input_data.Rcw2 = rotation_matrix_from_yaw_pitch_roll(0,0,0) # Rotation matrix for KF2
solver_input_data.tcw2 = np.array([-0.1,0.2,-0.7], dtype=np.float32) # Translation vector for KF2
Rwc2 = solver_input_data.Rcw2.T
twc2 = (-Rwc2 @ solver_input_data.tcw2.T).T
Rc2c1 = solver_input_data.Rcw2 @ solver_input_data.Rcw1.T
tc2c1 = solver_input_data.tcw2 - (Rc2c1 @ solver_input_data.tcw1.T).T
Rc1c2 = Rc2c1.T
tc1c2 = -Rc1c2 @ tc2c1
print(f'Rc1c2: {Rc1c2}')
print(f'tc1c2: {tc1c2}')
# Set a seed for reproducibility
np.random.seed(42) # You can change the seed value to any integer
# generate random points in camera1 image and back project them with random depths
points_2d_c1 = generate_random_points_2d(width, height, num_points)
#print(f'points 2D shape: {points_2d_c1.shape}')
#print(f'points 2D: {points_2d_c1}')
points_3d_c1 = backproject_points(solver_input_data.K1, points_2d_c1, 1.0, 10.0)
# check which points are visible in camera 2
points_3d_c2 = (Rc2c1 @ points_3d_c1.T + tc2c1.reshape((3, 1))).T
points_2d_c2, mask = project_points(solver_input_data.K2, points_3d_c2, width, height)
# remove points that are not visible in camera 2
mask = mask.ravel()
points_2d_c1 = points_2d_c1[mask == 1,:]
points_3d_c1 = points_3d_c1[mask == 1,:]
points_2d_c2 = points_2d_c2[mask == 1,:]
points_3d_c2 = points_3d_c2[mask == 1,:]
points_3d_w1 = (Rwc1 @ points_3d_c1.T + twc1.reshape(3,1)).T
points_3d_w2 = points_3d_w1.copy()
print(f'visible 3D points shape: {points_3d_w1.shape}')
#print(f'points 3D: {points_3d_w1}')
# check projections
if False:
points_2d_c1_check, mask_c1 = project_points(solver_input_data.K1, (solver_input_data.Rcw1 @ points_3d_w1.T + solver_input_data.tcw1.reshape(3,1)).T, width, height)
average_projection_error_c1 = np.mean(np.linalg.norm(points_2d_c1 - points_2d_c1_check, axis=1))
print(f'[double-check] average projection error 1: {average_projection_error_c1}, mask = {np.sum(mask_c1)}')
points_2d_c2_check, mask_c2 = project_points(solver_input_data.K2, (solver_input_data.Rcw2 @ points_3d_w2.T + solver_input_data.tcw2.reshape(3,1)).T, width, height)
average_projection_error_c2 = np.mean(np.linalg.norm(points_2d_c2 - points_2d_c2_check, axis=1))
print(f'[duble-check] average projection error 2: {average_projection_error_c2}, mask = {np.sum(mask_c2)}')
#print(f'original Rc1c2: {Rc1c2}')
#print(f'original tc1c2: {tc1c2}')
# now we have perfectly matched points
# let's simulate a drift on camera 2
# Tc1c2_d = Tc1c2 * gt_drift_T
Rc1c2_d = Rc1c2 @ gt_drift_R
tc1c2_d = Rc1c2 @ gt_drift_t + tc1c2
points_3d_c2_d = gt_drift_s * points_3d_c2 # scaling is applied in camera frame
#print(f'drifted Rc1c2: {Rc1c2_d}')
#print(f'drifted tc1c2: {tc1c2_d}')
Rwc2_d = Rwc1 @ Rc1c2_d
twc2_d = Rwc1 @ tc1c2_d + twc1
Rcw2_d = Rwc2_d.T
tcw2_d = -Rcw2_d @ twc2_d
# camera 2 does not know anything about its drift
#solver_input_data.Rcw2 = Rcw2_d
#solver_input_data.tcw2 = tcw2_d
points_3d_w2_d = (Rwc2_d @ points_3d_c2_d.T + twc2_d.reshape(3,1)).T
#test = solver_input_data.Rcw1 @ Rcw2_d
# matched 3D points
solver_input_data.points_3d_w1 = points_3d_w1
solver_input_data.points_3d_w2 = points_3d_w2_d
print(f'3D points 1 shape: {points_3d_w1.shape}')
print(f'3D points 2 shape: {points_3d_w2_d.shape}')
# Mock sigma squared data
solver_input_data.sigmas2_1 = [1.0 for _ in range(num_points)]
solver_input_data.sigmas2_2 = [1.0 for _ in range(num_points)]
solver_input_data.fix_scale = False # Scale is not fixed
# Correction includes the inverse of the drift
inv_drift_R = gt_drift_R.T
inv_drift_t = -inv_drift_R @ gt_drift_t/gt_drift_s
Rcorr = Rc1c2 @ inv_drift_R
tcorr = Rc1c2 @ inv_drift_t + tc1c2
scorr = 1/gt_drift_s
print(f'correction R: {Rcorr}')
print(f'correction t: {tcorr}')
print(f'correction s: {scorr}')
check_solution(solver_input_data, scorr, Rcorr, tcorr)
return solver_input_data
def test_sim3solver(solver_input_data):
print('----------------------------------------------')
if False:
rr.init("sim3solver_test", spawn=True)
rr.log("points_3d_w1", rr.Points3D(solver_input_data.points_3d_w1, colors=[255,0,0]))
rr.log("points_3d_w2", rr.Points3D(solver_input_data.points_3d_w2, colors=[0,255,0]))
if False:
# test python solution by using points in world coordinates
R, t = horn_absolute_orientation(np.array(solver_input_data.points_3d_w1), np.array(solver_input_data.points_3d_w2))
print(f'python solution: R: {R}, t: {t}')
# Create Sim3Solver object with the input data
solver = sim3solver.Sim3Solver(solver_input_data)
# Set RANSAC parameters (using defaults here)
solver.set_ransac_parameters(0.99,20,300)
# Prepare variables for iterative solving
vbInliers = [False] * len(solver_input_data.points_3d_w1)
nInliers = 0
bConverged = False
# Test the first iteration (e.g., 10 iterations)
transformation, bNoMore, vbInliers, nInliers, bConverged = solver.iterate(5)
if False:
print("Estimated transformation after 10 iterations:")
print(transformation)
# Check if the solver returned inliers and transformation
print("Number of inliers:", nInliers)
print(f"bConverged: {bConverged}")
#print("Vector of inliers:", vbInliers)
# Test getting the best transformation
Tc1c2 = solver.get_estimated_transformation()
#print("Estimated transformation matrix Tc1c2:")
#print(Tc1c2)
# Test getting estimated rotation, translation, and scale
Rc1c2 = solver.get_estimated_rotation()
tc1c2 = solver.get_estimated_translation()
scale12 = solver.get_estimated_scale()
error3d = solver.compute_3d_registration_error()
print("Estimated rotation matrix Rc1c2:")
print(Rc1c2)
print("Estimated translation vector tc1c2:")
print(tc1c2)
print("Estimated scale scale12:")
print(scale12)
print('error3d: ', error3d)
# check solution
check_solution(solver_input_data, scale12, Rc1c2, tc1c2)
def test_sim3solver2(solver_input_data):
print('----------------------------------------------')
# Here we use a the second input data format
solver_input_data2 = sim3solver.Sim3SolverInput2()
solver_input_data2.points_3d_c1 = (solver_input_data.Rcw1 @ np.array(solver_input_data.points_3d_w1).T + solver_input_data.tcw1.reshape(3,1)).T
solver_input_data2.points_3d_c2 = (solver_input_data.Rcw2 @ np.array(solver_input_data.points_3d_w2).T + solver_input_data.tcw2.reshape(3,1)).T
solver_input_data2.sigmas2_1 = solver_input_data.sigmas2_1
solver_input_data2.sigmas2_2 = solver_input_data.sigmas2_2
solver_input_data2.fix_scale = solver_input_data.fix_scale
# Create Sim3Solver object with the input data
solver = sim3solver.Sim3Solver(solver_input_data2)
# Set RANSAC parameters (using defaults here)
solver.set_ransac_parameters(0.99,20,300)
# Prepare variables for iterative solving
vbInliers = [False] * len(solver_input_data2.points_3d_c1)
nInliers = 0
bConverged = False
# Test the first iteration (e.g., 10 iterations)
transformation, bNoMore, vbInliers, nInliers, bConverged = solver.iterate(5)
if False:
print("Estimated transformation after 10 iterations:")
print(transformation)
# Check if the solver returned inliers and transformation
print("Number of inliers:", nInliers)
print(f"bConverged: {bConverged}")
#print("Vector of inliers:", vbInliers)
# Test getting the best transformation
Tc1c2 = solver.get_estimated_transformation()
#print("Estimated transformation matrix Tc1c2:")
#print(Tc1c2)
# Test getting estimated rotation, translation, and scale
Rc1c2 = solver.get_estimated_rotation()
tc1c2 = solver.get_estimated_translation()
scale12 = solver.get_estimated_scale()
error3d = solver.compute_3d_registration_error()
print("Estimated rotation matrix Rc1c2:")
print(Rc1c2)
print("Estimated translation vector tc1c2:")
print(tc1c2)
print("Estimated scale scale12:")
print(scale12)
print('error3d: ', error3d)
# check solution
check_solution2(solver_input_data2, scale12, Rc1c2, tc1c2)
if __name__ == "__main__":
# Create a Sim3SolverInput object with mock data
solver_input_data = create_mock_input()
test_sim3solver(solver_input_data)
test_sim3solver2(solver_input_data)