1 // This file is part of OpenMVG, an Open Multiple View Geometry C++ library.
2
3 // Copyright (c) 2015 Pierre MOULON.
4
5 // This Source Code Form is subject to the terms of the Mozilla Public
6 // License, v. 2.0. If a copy of the MPL was not distributed with this
7 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
9 //-----------------
10 // Test summary:
11 //-----------------
12 // - Create a SfM_Data scene from a synthetic dataset
13 // - since random noise have been added on 2d data point (initial residual is not small)
14 // - Check that residual is small once the generic Bundle Adjustment framework have been called.
15 // --
16 // - Perform the test for all the plausible intrinsic camera models
17 //-----------------
18
19 #include "openMVG/cameras/Camera_Common.hpp"
20 #include "openMVG/multiview/test_data_sets.hpp"
21 #include "openMVG/sfm/sfm.hpp"
22
23 #include "testing/testing.h"
24
25 #include <cmath>
26 #include <cstdio>
27 #include <iostream>
28 #include <random>
29
30 using namespace openMVG;
31 using namespace openMVG::cameras;
32 using namespace openMVG::geometry;
33 using namespace openMVG::sfm;
34
35 double RMSE(const SfM_Data & sfm_data);
36
37 SfM_Data getInputScene
38 (
39 const NViewDataSet & d,
40 const nViewDatasetConfigurator & config,
41 EINTRINSIC eintrinsic,
42 const bool b_use_gcp = false,
43 const bool b_use_pose_prior = false,
44 const bool b_use_noise_on_image_observations = true
45 );
46
TEST(BUNDLE_ADJUSTMENT,EffectiveMinimization_Pinhole)47 TEST(BUNDLE_ADJUSTMENT, EffectiveMinimization_Pinhole) {
48
49 const int nviews = 3;
50 const int npoints = 6;
51 const nViewDatasetConfigurator config;
52 const NViewDataSet d = NRealisticCamerasRing(nviews, npoints, config);
53
54 // Translate the input dataset to a SfM_Data scene
55 SfM_Data sfm_data = getInputScene(d, config, PINHOLE_CAMERA);
56
57 const double dResidual_before = RMSE(sfm_data);
58
59 // Call the BA interface and let it refine (Structure and Camera parameters [Intrinsics|Motion])
60 const bool bVerbose = true;
61 const bool bMultithread = false;
62 std::shared_ptr<Bundle_Adjustment> ba_object =
63 std::make_shared<Bundle_Adjustment_Ceres>(
64 Bundle_Adjustment_Ceres::BA_Ceres_options(bVerbose, bMultithread));
65 EXPECT_TRUE( ba_object->Adjust(sfm_data,
66 Optimize_Options(
67 Intrinsic_Parameter_Type::ADJUST_ALL,
68 Extrinsic_Parameter_Type::ADJUST_ALL,
69 Structure_Parameter_Type::ADJUST_ALL)) );
70
71 const double dResidual_after = RMSE(sfm_data);
72 EXPECT_TRUE( dResidual_before > dResidual_after);
73 }
74
TEST(BUNDLE_ADJUSTMENT,EffectiveMinimization_Pinhole_Radial_K1)75 TEST(BUNDLE_ADJUSTMENT, EffectiveMinimization_Pinhole_Radial_K1) {
76
77 const int nviews = 3;
78 const int npoints = 6;
79 const nViewDatasetConfigurator config;
80 const NViewDataSet d = NRealisticCamerasRing(nviews, npoints, config);
81
82 // Translate the input dataset to a SfM_Data scene
83 SfM_Data sfm_data = getInputScene(d, config, PINHOLE_CAMERA_RADIAL1);
84
85 const double dResidual_before = RMSE(sfm_data);
86
87 // Call the BA interface and let it refine (Structure and Camera parameters [Intrinsics|Motion])
88 const bool bVerbose = true;
89 const bool bMultithread = false;
90 std::shared_ptr<Bundle_Adjustment> ba_object =
91 std::make_shared<Bundle_Adjustment_Ceres>(
92 Bundle_Adjustment_Ceres::BA_Ceres_options(bVerbose, bMultithread));
93 EXPECT_TRUE( ba_object->Adjust(sfm_data,
94 Optimize_Options(
95 Intrinsic_Parameter_Type::ADJUST_ALL,
96 Extrinsic_Parameter_Type::ADJUST_ALL,
97 Structure_Parameter_Type::ADJUST_ALL)) );
98
99 const double dResidual_after = RMSE(sfm_data);
100 EXPECT_TRUE( dResidual_before > dResidual_after);
101 }
102
TEST(BUNDLE_ADJUSTMENT,EffectiveMinimization_Pinhole_Radial_K3)103 TEST(BUNDLE_ADJUSTMENT, EffectiveMinimization_Pinhole_Radial_K3) {
104
105 const int nviews = 3;
106 const int npoints = 6;
107 const nViewDatasetConfigurator config;
108 const NViewDataSet d = NRealisticCamerasRing(nviews, npoints, config);
109
110 // Translate the input dataset to a SfM_Data scene
111 SfM_Data sfm_data = getInputScene(d, config, PINHOLE_CAMERA_RADIAL3);
112
113 const double dResidual_before = RMSE(sfm_data);
114
115 // Call the BA interface and let it refine (Structure and Camera parameters [Intrinsics|Motion])
116 const bool bVerbose = true;
117 const bool bMultithread = false;
118 std::shared_ptr<Bundle_Adjustment> ba_object =
119 std::make_shared<Bundle_Adjustment_Ceres>(
120 Bundle_Adjustment_Ceres::BA_Ceres_options(bVerbose, bMultithread));
121 EXPECT_TRUE( ba_object->Adjust(sfm_data,
122 Optimize_Options(
123 Intrinsic_Parameter_Type::ADJUST_ALL,
124 Extrinsic_Parameter_Type::ADJUST_ALL,
125 Structure_Parameter_Type::ADJUST_ALL)) );
126
127 const double dResidual_after = RMSE(sfm_data);
128 EXPECT_TRUE( dResidual_before > dResidual_after);
129 }
130
TEST(BUNDLE_ADJUSTMENT,EffectiveMinimization_Pinhole_Intrinsic_Brown_T2)131 TEST(BUNDLE_ADJUSTMENT, EffectiveMinimization_Pinhole_Intrinsic_Brown_T2) {
132
133 const int nviews = 3;
134 const int npoints = 6;
135 const nViewDatasetConfigurator config;
136 const NViewDataSet d = NRealisticCamerasRing(nviews, npoints, config);
137
138 // Translate the input dataset to a SfM_Data scene
139 SfM_Data sfm_data = getInputScene(d, config, PINHOLE_CAMERA_BROWN);
140
141 const double dResidual_before = RMSE(sfm_data);
142
143 // Call the BA interface and let it refine (Structure and Camera parameters [Intrinsics|Motion])
144 const bool bVerbose = true;
145 const bool bMultithread = false;
146 std::shared_ptr<Bundle_Adjustment> ba_object =
147 std::make_shared<Bundle_Adjustment_Ceres>(
148 Bundle_Adjustment_Ceres::BA_Ceres_options(bVerbose, bMultithread));
149 EXPECT_TRUE( ba_object->Adjust(sfm_data,
150 Optimize_Options(
151 Intrinsic_Parameter_Type::ADJUST_ALL,
152 Extrinsic_Parameter_Type::ADJUST_ALL,
153 Structure_Parameter_Type::ADJUST_ALL)) );
154
155 const double dResidual_after = RMSE(sfm_data);
156 EXPECT_TRUE( dResidual_before > dResidual_after);
157 }
158
TEST(BUNDLE_ADJUSTMENT,EffectiveMinimization_Pinhole_Intrinsic_Fisheye)159 TEST(BUNDLE_ADJUSTMENT, EffectiveMinimization_Pinhole_Intrinsic_Fisheye) {
160
161 const int nviews = 3;
162 const int npoints = 6;
163 const nViewDatasetConfigurator config;
164 const NViewDataSet d = NRealisticCamerasRing(nviews, npoints, config);
165
166 // Translate the input dataset to a SfM_Data scene
167 SfM_Data sfm_data = getInputScene(d, config, PINHOLE_CAMERA_FISHEYE);
168
169 const double dResidual_before = RMSE(sfm_data);
170
171 // Call the BA interface and let it refine (Structure and Camera parameters [Intrinsics|Motion])
172 const bool bVerbose = true;
173 const bool bMultithread = false;
174 std::shared_ptr<Bundle_Adjustment> ba_object =
175 std::make_shared<Bundle_Adjustment_Ceres>(
176 Bundle_Adjustment_Ceres::BA_Ceres_options(bVerbose, bMultithread));
177 EXPECT_TRUE( ba_object->Adjust(sfm_data,
178 Optimize_Options(
179 Intrinsic_Parameter_Type::ADJUST_ALL,
180 Extrinsic_Parameter_Type::ADJUST_ALL,
181 Structure_Parameter_Type::ADJUST_ALL)) );
182
183 const double dResidual_after = RMSE(sfm_data);
184 EXPECT_TRUE( dResidual_before > dResidual_after);
185 }
186
187 //-- Test with GCP - Camera position once BA done must be the same as the GT
TEST(BUNDLE_ADJUSTMENT,EffectiveMinimization_Pinhole_GCP)188 TEST(BUNDLE_ADJUSTMENT, EffectiveMinimization_Pinhole_GCP) {
189
190 const int nviews = 3;
191 const int npoints = 6;
192 const nViewDatasetConfigurator config;
193 const NViewDataSet d = NRealisticCamerasRing(nviews, npoints, config);
194
195 // Translate the input dataset to a SfM_Data scene
196 SfM_Data sfm_data = getInputScene(d, config, PINHOLE_CAMERA, true);
197
198 const double dResidual_before = RMSE(sfm_data);
199
200 // Call the BA interface and let it refine (Structure and Camera parameters [Intrinsics|Motion])
201 const bool bVerbose = true;
202 const bool bMultithread = false;
203 std::shared_ptr<Bundle_Adjustment> ba_object =
204 std::make_shared<Bundle_Adjustment_Ceres>(
205 Bundle_Adjustment_Ceres::BA_Ceres_options(bVerbose, bMultithread));
206 EXPECT_TRUE( ba_object->Adjust(sfm_data,
207 Optimize_Options(
208 Intrinsic_Parameter_Type::NONE,
209 Extrinsic_Parameter_Type::ADJUST_ALL,
210 Structure_Parameter_Type::ADJUST_ALL,
211 //-> Use GCP to fit the SfM scene to the GT coordinates system (Scale, Rotation, Translation)
212 Control_Point_Parameter(20.0, true))) );
213
214 const double dResidual_after = RMSE(sfm_data);
215 EXPECT_TRUE( dResidual_before > dResidual_after);
216
217 //-- Check camera pose are to the right place (since GCP was used, the camera coordinates must be the same)
218 for (const auto & view_it : sfm_data.GetViews())
219 {
220 const View * view = view_it.second.get();
221 const Pose3 pose = sfm_data.GetPoseOrDie(view);
222 const double position_residual = (d._C[view->id_pose] - pose.center()).norm();
223 EXPECT_NEAR(0.0, position_residual, 1e-4);
224 }
225 }
226
227 //-- Test with PosePriors - Camera position once BA done must be the same as the GT
TEST(BUNDLE_ADJUSTMENT,EffectiveMinimization_Pinhole_PosePriors)228 TEST(BUNDLE_ADJUSTMENT, EffectiveMinimization_Pinhole_PosePriors) {
229
230 const int nviews = 12;
231 const int npoints = 6;
232 const nViewDatasetConfigurator config;
233 const NViewDataSet d = NRealisticCamerasRing(nviews, npoints, config);
234
235 // Translate the input dataset to a SfM_Data scene
236 const bool b_use_GCP = false;
237 const bool b_use_POSE_PRIOR = true;
238 const bool b_use_noise_on_image_observations = false;
239 SfM_Data sfm_data = getInputScene(d, config, PINHOLE_CAMERA,
240 b_use_GCP, b_use_POSE_PRIOR, b_use_noise_on_image_observations);
241
242 const double dResidual_before = RMSE(sfm_data);
243
244 // First run a BA without pose prior
245 // - check that RMSE is tiny and that the scene is not at the right place (the pose prior positions)
246
247 {
248 const bool b_use_POSE_PRIOR_in_BA = false;
249
250 const bool bVerbose = true;
251 const bool bMultithread = false;
252 std::shared_ptr<Bundle_Adjustment> ba_object =
253 std::make_shared<Bundle_Adjustment_Ceres>(
254 Bundle_Adjustment_Ceres::BA_Ceres_options(bVerbose, bMultithread));
255 EXPECT_TRUE( ba_object->Adjust(sfm_data,
256 Optimize_Options(
257 Intrinsic_Parameter_Type::ADJUST_ALL,
258 Extrinsic_Parameter_Type::ADJUST_ALL,
259 Structure_Parameter_Type::ADJUST_ALL,
260 Control_Point_Parameter(0.0, false),
261 b_use_POSE_PRIOR_in_BA)) );
262
263 const double dResidual_after = RMSE(sfm_data);
264 EXPECT_TRUE( dResidual_before > dResidual_after);
265
266 // Compute distance between SfM poses center and GPS Priors
267 Mat residuals(3, sfm_data.GetViews().size());
268 for (const auto & view_it : sfm_data.GetViews())
269 {
270 const ViewPriors * view = dynamic_cast<ViewPriors*>(view_it.second.get());
271 residuals.col(view->id_pose) = (sfm_data.GetPoseOrDie(view).center() - view->pose_center_).transpose();
272 }
273 // Check that the scene is not at the position of the POSE PRIORS center.
274 EXPECT_FALSE( (residuals.colwise().norm().sum() < 1e-8) );
275 }
276
277 // Then activate BA with pose prior & check that the scene is at the right place
278 {
279 const bool b_use_POSE_PRIOR_in_BA = true;
280
281 const bool bVerbose = true;
282 const bool bMultithread = false;
283 std::shared_ptr<Bundle_Adjustment> ba_object =
284 std::make_shared<Bundle_Adjustment_Ceres>(
285 Bundle_Adjustment_Ceres::BA_Ceres_options(bVerbose, bMultithread));
286 EXPECT_TRUE( ba_object->Adjust(sfm_data,
287 Optimize_Options(
288 Intrinsic_Parameter_Type::ADJUST_ALL,
289 Extrinsic_Parameter_Type::ADJUST_ALL,
290 Structure_Parameter_Type::ADJUST_ALL,
291 Control_Point_Parameter(0.0, false),
292 b_use_POSE_PRIOR_in_BA)) );
293
294 const double dResidual_after = RMSE(sfm_data);
295 EXPECT_TRUE( dResidual_before > dResidual_after);
296
297 // Compute distance between SfM poses center and GPS Priors
298 for (const auto & view_it : sfm_data.GetViews())
299 {
300 const ViewPriors * view = dynamic_cast<ViewPriors*>(view_it.second.get());
301 const Pose3 pose = sfm_data.GetPoseOrDie(view);
302 const double position_residual = (d._C[view->id_pose] - pose.center()).norm();
303 EXPECT_NEAR(0.0, position_residual, 1e-8);
304 }
305 }
306 }
307
308
309 /// Compute the Root Mean Square Error of the residuals
RMSE(const SfM_Data & sfm_data)310 double RMSE(const SfM_Data & sfm_data)
311 {
312 // Compute residuals for each observation
313 std::vector<double> vec;
314 for (const auto& landmark_it : sfm_data.GetLandmarks())
315 {
316 const Observations & obs = landmark_it.second.obs;
317 for (const auto& obs_it : obs)
318 {
319 const View * view = sfm_data.GetViews().find(obs_it.first)->second.get();
320 const Pose3 pose = sfm_data.GetPoseOrDie(view);
321 const std::shared_ptr<IntrinsicBase> intrinsic = sfm_data.GetIntrinsics().find(view->id_intrinsic)->second;
322 const Vec2 residual = intrinsic->residual(pose(landmark_it.second.X), obs_it.second.x);
323 vec.push_back( residual(0) );
324 vec.push_back( residual(1) );
325 }
326 }
327 const Eigen::Map<Eigen::RowVectorXd> residuals(&vec[0], vec.size());
328 const double RMSE = std::sqrt(residuals.squaredNorm() / vec.size());
329 return RMSE;
330 }
331
332 // Translation a synthetic scene into a valid SfM_Data scene.
333 // => A synthetic scene is used:
334 // some random noise is added on observed structure data points
335 // a tiny rotation to ground truth is added to the true rotation (in order to test BA effectiveness)
getInputScene(const NViewDataSet & d,const nViewDatasetConfigurator & config,EINTRINSIC eintrinsic,const bool b_use_gcp,const bool b_use_pose_prior,const bool b_use_noise_on_image_observations)336 SfM_Data getInputScene
337 (
338 const NViewDataSet & d,
339 const nViewDatasetConfigurator & config,
340 EINTRINSIC eintrinsic,
341 const bool b_use_gcp,
342 const bool b_use_pose_prior,
343 const bool b_use_noise_on_image_observations
344 )
345 {
346 // Translate the input dataset to a SfM_Data scene
347 SfM_Data sfm_data;
348
349 // 1. Views
350 // 2. Poses
351 // 3. Intrinsic data (shared, so only one camera intrinsic is defined)
352 // 4. Landmarks
353 // 5. GCP (optional)
354
355 const int nviews = d._C.size();
356 const int npoints = d._X.cols();
357
358 // 1. Views
359 for (int i = 0; i < nviews; ++i)
360 {
361 const IndexT id_view = i, id_pose = i, id_intrinsic = 0; //(shared intrinsics)
362
363 if (!b_use_pose_prior)
364 {
365 sfm_data.views[i] = std::make_shared<View>("", id_view, id_intrinsic, id_pose, config._cx *2, config._cy *2);
366 }
367 else // b_use_pose_prior == true
368 {
369 sfm_data.views[i] = std::make_shared<ViewPriors>("", id_view, id_intrinsic, id_pose, config._cx *2, config._cy *2);
370 ViewPriors * view = dynamic_cast<ViewPriors*>(sfm_data.views[i].get());
371 view->b_use_pose_center_ = true;
372 view->pose_center_ = d._C[i];
373 }
374 }
375
376 // Add a rotation to the GT (in order to make BA do some work)
377 const Mat3 rot = RotationAroundX(D2R(6));
378
379 // 2. Poses
380 for (int i = 0; i < nviews; ++i)
381 {
382 const Pose3 pose(rot * d._R[i], d._C[i]);
383 sfm_data.poses[i] = pose;
384 }
385
386 // 3. Intrinsic data (shared, so only one camera intrinsic is defined)
387 {
388 const unsigned int w = config._cx *2;
389 const unsigned int h = config._cy *2;
390 switch (eintrinsic)
391 {
392 case PINHOLE_CAMERA:
393 sfm_data.intrinsics[0] = std::make_shared<Pinhole_Intrinsic>
394 (w, h, config._fx, config._cx, config._cy);
395 break;
396 case PINHOLE_CAMERA_RADIAL1:
397 sfm_data.intrinsics[0] = std::make_shared<Pinhole_Intrinsic_Radial_K1>
398 (w, h, config._fx, config._cx, config._cy, 0.0);
399 break;
400 case PINHOLE_CAMERA_RADIAL3:
401 sfm_data.intrinsics[0] = std::make_shared<Pinhole_Intrinsic_Radial_K3>
402 (w, h, config._fx, config._cx, config._cy, 0., 0., 0.);
403 break;
404 case PINHOLE_CAMERA_BROWN:
405 sfm_data.intrinsics[0] = std::make_shared<Pinhole_Intrinsic_Brown_T2>
406 (w, h, config._fx, config._cx, config._cy, 0., 0., 0., 0., 0.);
407 break;
408 case PINHOLE_CAMERA_FISHEYE:
409 sfm_data.intrinsics[0] = std::make_shared<Pinhole_Intrinsic_Fisheye>
410 (w, h, config._fx, config._cx, config._cy, 0., 0., 0., 0.);
411 break;
412 default:
413 std::cout << "Not yet supported" << std::endl;
414 }
415 }
416
417 // 4. Landmarks
418 // Collect image observation of the landmarks X in each frame.
419 // => add some random noise to each (x,y) observation
420 std::default_random_engine random_generator;
421 std::normal_distribution<double> distribution(0, 0.1);
422 for (int i = 0; i < npoints; ++i)
423 {
424 // Create a landmark for each 3D points
425 Landmark landmark;
426 landmark.X = d._X.col(i);
427 for (int j = 0; j < nviews; ++j)
428 {
429 Vec2 pt = d._x[j].col(i);
430 if (b_use_noise_on_image_observations)
431 {
432 // Add some noise to image observations
433 pt(0) += distribution(random_generator);
434 pt(1) += distribution(random_generator);
435 }
436
437 landmark.obs[j] = Observation(pt, i);
438 }
439 sfm_data.structure[i] = landmark;
440 }
441
442 // 5. GCP
443 if (b_use_gcp)
444 {
445 if (npoints >= 4) // Use 4 GCP for this test
446 {
447 for (int i = 0; i < 4; ++i) // Select the 4 first point as GCP
448 {
449 // Collect observations of the landmarks X in each frame.
450 Landmark landmark;
451 landmark.X = d._X.col(i);
452 for (int j = 0; j < nviews; ++j)
453 {
454 landmark.obs[j] = Observation(d._x[j].col(i), i);
455 }
456 sfm_data.control_points[i] = landmark;
457 }
458 }
459 else
460 {
461 std::cerr << "Insufficient point count" << std::endl;
462 }
463 }
464 return sfm_data;
465 }
466
467 /* ************************************************************************* */
main()468 int main() { TestResult tr; return TestRegistry::runAllTests(tr);}
469 /* ************************************************************************* */
470