2 Compare several methods for optimizing the view-graph.
3 We measure the distance from the ground truth in terms of the norm of
4 local coordinates (geodesic distance) on the F-manifold.
5 We also plot the final error of the optimization.
7 Author: Frank Dellaert (with heavy assist from ChatGPT)
13 import matplotlib.pyplot
as plt
23 LevenbergMarquardtOptimizer,
24 LevenbergMarquardtParams,
27 SimpleFundamentalMatrix,
32 K = gtsam.symbol_shorthand.K
35 methods = [
"SimpleF",
"Fundamental",
"Essential+Ks",
"Calibrated"]
41 if sym.chr() == ord(
"k"):
42 return f
"k{sym.index()}"
45 return f
"({edge.i()},{edge.j()})"
49 """simulate geometry (points and poses)"""
51 cal =
Cal3f(50.0, 50.0, 50.0)
54 points = SFMdata.createPoints()
57 extra_points = rng.uniform(-10, 10, (num_random_points, 3))
61 poses = SFMdata.posesOnCircle(num_cameras, 30)
63 return points, poses, cal
67 """Simulate measurements from each camera pose"""
68 measurements = [[
None for _
in points]
for _
in poses]
69 for i, pose
in enumerate(poses):
70 camera = PinholeCameraCal3f(pose, cal)
71 for j, point
in enumerate(points):
72 projection = camera.project(point)
73 noise = rng.normal(0, noise_std, size=2)
74 measurements[i][j] = projection + noise
81 E1 = EssentialMatrix.FromPose3(poses[0].
between(poses[1]))
82 E2 = EssentialMatrix.FromPose3(poses[0].
between(poses[2]))
85 if method ==
"Fundamental":
87 elif method ==
"SimpleF":
89 c = cal.principalPoint()
93 elif method ==
"Essential+Ks" or method ==
"Calibrated":
96 raise ValueError(f
"Unknown method {method}")
100 """build the factor graph"""
103 if method ==
"Fundamental":
104 FactorClass = gtsam.TransferFactorFundamentalMatrix
105 elif method ==
"SimpleF":
106 FactorClass = gtsam.TransferFactorSimpleFundamentalMatrix
107 elif method ==
"Essential+Ks":
108 FactorClass = gtsam.EssentialTransferFactorKCal3f
110 for i
in range(num_cameras):
112 graph.addPriorCal3f(
K(i), cal, model)
113 elif method ==
"Calibrated":
114 FactorClass = gtsam.EssentialTransferFactorCal3f
117 raise ValueError(f
"Unknown method {method}")
121 for a
in range(num_cameras):
122 b = (a + 1) % num_cameras
123 c = (a + 2) % num_cameras
131 for j
in range(
len(measurements[0])):
132 tuples1.append((z[a][j], z[b][j], z[c][j]))
133 tuples2.append((z[a][j], z[c][j], z[b][j]))
134 tuples3.append((z[c][j], z[b][j], z[a][j]))
137 if method
in [
"Calibrated"]:
138 graph.add(FactorClass(
EdgeKey(a, c),
EdgeKey(b, c), tuples1, cal))
139 graph.add(FactorClass(
EdgeKey(a, b),
EdgeKey(b, c), tuples2, cal))
140 graph.add(FactorClass(
EdgeKey(a, c),
EdgeKey(a, b), tuples3, cal))
150 """get initial estimate for method"""
151 initialEstimate =
Values()
154 if method
in [
"Fundamental",
"SimpleF"]:
155 F1, F2 = ground_truth
156 for a
in range(num_cameras):
157 b = (a + 1) % num_cameras
158 c = (a + 2) % num_cameras
159 initialEstimate.insert(
EdgeKey(a, b).
key(), F1)
160 initialEstimate.insert(
EdgeKey(a, c).
key(), F2)
161 total_dimension += F1.dim() + F2.dim()
162 elif method
in [
"Essential+Ks",
"Calibrated"]:
163 E1, E2 = ground_truth
164 for a
in range(num_cameras):
165 b = (a + 1) % num_cameras
166 c = (a + 2) % num_cameras
167 initialEstimate.insert(
EdgeKey(a, b).
key(), E1)
168 initialEstimate.insert(
EdgeKey(a, c).
key(), E2)
169 total_dimension += E1.dim() + E2.dim()
171 raise ValueError(f
"Unknown method {method}")
173 if method ==
"Essential+Ks":
175 for i
in range(num_cameras):
176 initialEstimate.insert(
K(i), cal)
177 total_dimension += cal.dim()
179 print(f
"Total dimension of the problem: {total_dimension}")
180 return initialEstimate
184 """optimize the graph"""
186 params.setlambdaInitial(1e10)
187 params.setlambdaUpperBound(1e10)
189 params.setVerbosityLM(
"SUMMARY")
191 result = optimizer.optimize()
192 iterations = optimizer.iterations()
193 return result, iterations
197 """Compute geodesic distances from ground truth"""
200 F1, F2 = ground_truth[
"Fundamental"]
202 for a
in range(num_cameras):
203 b = (a + 1) % num_cameras
204 c = (a + 2) % num_cameras
208 if method
in [
"Essential+Ks",
"Calibrated"]:
209 E_est_ab = result.atEssentialMatrix(key_ab)
210 E_est_ac = result.atEssentialMatrix(key_ac)
213 if method ==
"Fundamental":
214 F_est_ab = result.atFundamentalMatrix(key_ab)
215 F_est_ac = result.atFundamentalMatrix(key_ac)
216 elif method ==
"SimpleF":
217 SF_est_ab = result.atSimpleFundamentalMatrix(key_ab).
matrix()
218 SF_est_ac = result.atSimpleFundamentalMatrix(key_ac).
matrix()
221 elif method ==
"Essential+Ks":
223 cal_a = result.atCal3f(
K(a))
224 cal_b = result.atCal3f(
K(b))
225 cal_c = result.atCal3f(
K(c))
230 elif method ==
"Calibrated":
235 raise ValueError(f
"Unknown method {method}")
238 dist_ab = np.linalg.norm(F1.localCoordinates(F_est_ab))
239 dist_ac = np.linalg.norm(F2.localCoordinates(F_est_ac))
240 distances.append(dist_ab)
241 distances.append(dist_ac)
248 methods =
list(results.keys())
249 final_errors = [results[method][
"final_error"]
for method
in methods]
250 distances = [results[method][
"distances"]
for method
in methods]
251 iterations = [results[method][
"iterations"]
for method
in methods]
253 fig, ax1 = plt.subplots()
256 ax1.set_xlabel(
"Method")
257 ax1.set_ylabel(
"Median Error (log scale)", color=color)
258 ax1.set_yscale(
"log")
259 ax1.bar(methods, final_errors, color=color, alpha=0.6)
260 ax1.tick_params(axis=
"y", labelcolor=color)
264 ax2.set_ylabel(
"Median Geodesic Distance", color=color)
265 ax2.plot(methods, distances, color=color, marker=
"o", linestyle=
"-")
266 ax2.tick_params(axis=
"y", labelcolor=color)
269 for i, method
in enumerate(methods):
271 f
"{iterations[i]:.1f}",
273 textcoords=
"offset points",
279 plt.title(
"Comparison of Methods (Labels show avg iterations)")
287 parser = argparse.ArgumentParser(description=
"Compare Fundamental and Essential Matrix Methods")
288 parser.add_argument(
"--num_cameras", type=int, default=4, help=
"Number of cameras (default: 4)")
289 parser.add_argument(
"--num_extra_points", type=int, default=12, help=
"Number of extra random points (default: 12)")
290 parser.add_argument(
"--num_trials", type=int, default=5, help=
"Number of trials (default: 5)")
291 parser.add_argument(
"--seed", type=int, default=42, help=
"Random seed (default: 42)")
292 parser.add_argument(
"--noise_std", type=float, default=0.5, help=
"Standard deviation of noise (default: 0.5)")
293 args = parser.parse_args()
296 rng = np.random.default_rng(seed=args.seed)
299 results = {method: {
"distances": [],
"final_error": [],
"iterations": []}
for method
in methods}
302 points, poses, cal =
simulate_geometry(args.num_cameras, rng, args.num_extra_points)
308 initial_estimate: dict[Values] = {
309 method:
get_initial_estimate(method, args.num_cameras, ground_truth[method], cal)
for method
in methods
311 simple_f_result: Values =
Values()
313 for trial
in range(args.num_trials):
314 print(f
"\nTrial {trial + 1}/{args.num_trials}")
317 measurements =
simulate_data(points, poses, cal, rng, args.noise_std)
319 for method
in methods:
320 print(f
"\nRunning method: {method}")
326 if method ==
"Fundamental":
327 initial_estimate[method] = simple_f_result
330 result, iterations =
optimize(graph, initial_estimate[method], method)
333 if method ==
"SimpleF":
334 simple_f_result =
Values()
335 for a
in range(args.num_cameras):
336 b = (a + 1) % args.num_cameras
337 c = (a + 2) % args.num_cameras
340 F1 = result.atSimpleFundamentalMatrix(key_ab).
matrix()
341 F2 = result.atSimpleFundamentalMatrix(key_ac).
matrix()
346 distances =
compute_distances(method, result, ground_truth, args.num_cameras, cal)
349 final_error = graph.error(result)
352 results[method][
"distances"].extend(distances)
353 results[method][
"final_error"].append(final_error)
354 results[method][
"iterations"].append(iterations)
356 print(f
"Method: {method}")
357 print(f
"Final Error: {final_error:.3f}")
358 print(f
"Mean Geodesic Distance: {np.mean(distances):.3f}")
359 print(f
"Number of Iterations: {iterations}\n")
362 for method
in methods:
363 results[method][
"final_error"] = np.median(results[method][
"final_error"])
364 results[method][
"distances"] = np.median(results[method][
"distances"])
365 results[method][
"iterations"] = np.median(results[method][
"iterations"])
371 if __name__ ==
"__main__":