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