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 #include "openMVG/sfm/sfm_data_BA_ceres.hpp"
10 
11 #ifdef OPENMVG_USE_OPENMP
12 #include <omp.h>
13 #endif
14 
15 #include "ceres/problem.h"
16 #include "ceres/solver.h"
17 #include "openMVG/cameras/Camera_Common.hpp"
18 #include "openMVG/cameras/Camera_Intrinsics.hpp"
19 #include "openMVG/geometry/Similarity3.hpp"
20 #include "openMVG/geometry/Similarity3_Kernel.hpp"
21 //- Robust estimation - LMeds (since no threshold can be defined)
22 #include "openMVG/robust_estimation/robust_estimator_LMeds.hpp"
23 #include "openMVG/sfm/sfm_data_BA_ceres_camera_functor.hpp"
24 #include "openMVG/sfm/sfm_data_transform.hpp"
25 #include "openMVG/sfm/sfm_data.hpp"
26 #include "openMVG/system/logger.hpp"
27 #include "openMVG/types.hpp"
28 
29 #include <ceres/rotation.h>
30 #include <ceres/types.h>
31 
32 #include <iostream>
33 #include <limits>
34 
35 namespace openMVG {
36 namespace sfm {
37 
38 using namespace openMVG::cameras;
39 using namespace openMVG::geometry;
40 
41 // Ceres CostFunctor used for SfM pose center to GPS pose center minimization
42 struct PoseCenterConstraintCostFunction
43 {
44   Vec3 weight_;
45   Vec3 pose_center_constraint_;
46 
PoseCenterConstraintCostFunctionopenMVG::sfm::PoseCenterConstraintCostFunction47   PoseCenterConstraintCostFunction
48   (
49     const Vec3 & center,
50     const Vec3 & weight
51   ): weight_(weight), pose_center_constraint_(center)
52   {
53   }
54 
55   template <typename T> bool
operator ()openMVG::sfm::PoseCenterConstraintCostFunction56   operator()
57   (
58     const T* const cam_extrinsics, // R_t
59     T* residuals
60   )
61   const
62   {
63     using Vec3T = Eigen::Matrix<T,3,1>;
64     Eigen::Map<const Vec3T> cam_R(&cam_extrinsics[0]);
65     Eigen::Map<const Vec3T> cam_t(&cam_extrinsics[3]);
66     const Vec3T cam_R_transpose(-cam_R);
67 
68     Vec3T pose_center;
69     // Rotate the point according the camera rotation
70     ceres::AngleAxisRotatePoint(cam_R_transpose.data(), cam_t.data(), pose_center.data());
71     pose_center = pose_center * T(-1);
72 
73     Eigen::Map<Vec3T> residuals_eigen(residuals);
74     residuals_eigen = weight_.cast<T>().cwiseProduct(pose_center - pose_center_constraint_.cast<T>());
75 
76     return true;
77   }
78 };
79 
80 /// Create the appropriate cost functor according the provided input camera intrinsic model.
81 /// The residual can be weighetd if desired (default 0.0 means no weight).
IntrinsicsToCostFunction(IntrinsicBase * intrinsic,const Vec2 & observation,const double weight)82 ceres::CostFunction * IntrinsicsToCostFunction
83 (
84   IntrinsicBase * intrinsic,
85   const Vec2 & observation,
86   const double weight
87 )
88 {
89   switch (intrinsic->getType())
90   {
91     case PINHOLE_CAMERA:
92       return ResidualErrorFunctor_Pinhole_Intrinsic::Create(observation, weight);
93     case PINHOLE_CAMERA_RADIAL1:
94       return ResidualErrorFunctor_Pinhole_Intrinsic_Radial_K1::Create(observation, weight);
95     case PINHOLE_CAMERA_RADIAL3:
96       return ResidualErrorFunctor_Pinhole_Intrinsic_Radial_K3::Create(observation, weight);
97     case PINHOLE_CAMERA_BROWN:
98       return ResidualErrorFunctor_Pinhole_Intrinsic_Brown_T2::Create(observation, weight);
99     case PINHOLE_CAMERA_FISHEYE:
100       return ResidualErrorFunctor_Pinhole_Intrinsic_Fisheye::Create(observation, weight);
101     case CAMERA_SPHERICAL:
102       return ResidualErrorFunctor_Intrinsic_Spherical::Create(intrinsic, observation, weight);
103     default:
104       return {};
105   }
106 }
107 
BA_Ceres_options(const bool bVerbose,bool bmultithreaded)108 Bundle_Adjustment_Ceres::BA_Ceres_options::BA_Ceres_options
109 (
110   const bool bVerbose,
111   bool bmultithreaded
112 )
113 : bVerbose_(bVerbose),
114   nb_threads_(1),
115   parameter_tolerance_(1e-8), //~= numeric_limits<float>::epsilon()
116   bUse_loss_function_(true),
117   max_num_iterations_(500)
118 {
119   #ifdef OPENMVG_USE_OPENMP
120     nb_threads_ = omp_get_max_threads();
121   #endif // OPENMVG_USE_OPENMP
122   if (!bmultithreaded)
123     nb_threads_ = 1;
124 
125   bCeres_summary_ = false;
126 
127   // Default configuration use a DENSE representation
128   linear_solver_type_ = ceres::DENSE_SCHUR;
129   preconditioner_type_ = ceres::JACOBI;
130   // If Sparse linear solver are available
131   // Descending priority order by efficiency (SUITE_SPARSE > CX_SPARSE > EIGEN_SPARSE)
132   if (ceres::IsSparseLinearAlgebraLibraryTypeAvailable(ceres::SUITE_SPARSE))
133   {
134     sparse_linear_algebra_library_type_ = ceres::SUITE_SPARSE;
135     linear_solver_type_ = ceres::SPARSE_SCHUR;
136   }
137   else
138   {
139     if (ceres::IsSparseLinearAlgebraLibraryTypeAvailable(ceres::CX_SPARSE))
140     {
141       sparse_linear_algebra_library_type_ = ceres::CX_SPARSE;
142       linear_solver_type_ = ceres::SPARSE_SCHUR;
143     }
144     else
145     if (ceres::IsSparseLinearAlgebraLibraryTypeAvailable(ceres::EIGEN_SPARSE))
146     {
147       sparse_linear_algebra_library_type_ = ceres::EIGEN_SPARSE;
148       linear_solver_type_ = ceres::SPARSE_SCHUR;
149     }
150   }
151 }
152 
153 
Bundle_Adjustment_Ceres(const Bundle_Adjustment_Ceres::BA_Ceres_options & options)154 Bundle_Adjustment_Ceres::Bundle_Adjustment_Ceres
155 (
156   const Bundle_Adjustment_Ceres::BA_Ceres_options & options
157 )
158 : ceres_options_(options)
159 {}
160 
161 Bundle_Adjustment_Ceres::BA_Ceres_options &
ceres_options()162 Bundle_Adjustment_Ceres::ceres_options()
163 {
164   return ceres_options_;
165 }
166 
Adjust(SfM_Data & sfm_data,const Optimize_Options & options)167 bool Bundle_Adjustment_Ceres::Adjust
168 (
169   SfM_Data & sfm_data,     // the SfM scene to refine
170   const Optimize_Options & options
171 )
172 {
173   //----------
174   // Add camera parameters
175   // - intrinsics
176   // - poses [R|t]
177 
178   // Create residuals for each observation in the bundle adjustment problem. The
179   // parameters for cameras and points are added automatically.
180   //----------
181 
182 
183   double pose_center_robust_fitting_error = 0.0;
184   openMVG::geometry::Similarity3 sim_to_center;
185   bool b_usable_prior = false;
186   if (options.use_motion_priors_opt && sfm_data.GetViews().size() > 3)
187   {
188     // - Compute a robust X-Y affine transformation & apply it
189     // - This early transformation enhance the conditionning (solution closer to the Prior coordinate system)
190     {
191       // Collect corresponding camera centers
192       std::vector<Vec3> X_SfM, X_GPS;
193       for (const auto & view_it : sfm_data.GetViews())
194       {
195         const sfm::ViewPriors * prior = dynamic_cast<sfm::ViewPriors*>(view_it.second.get());
196         if (prior != nullptr && prior->b_use_pose_center_ && sfm_data.IsPoseAndIntrinsicDefined(prior))
197         {
198           X_SfM.push_back( sfm_data.GetPoses().at(prior->id_pose).center() );
199           X_GPS.push_back( prior->pose_center_ );
200         }
201       }
202       openMVG::geometry::Similarity3 sim;
203 
204       // Compute the registration:
205       if (X_GPS.size() > 3)
206       {
207         const Mat X_SfM_Mat = Eigen::Map<Mat>(X_SfM[0].data(),3, X_SfM.size());
208         const Mat X_GPS_Mat = Eigen::Map<Mat>(X_GPS[0].data(),3, X_GPS.size());
209         geometry::kernel::Similarity3_Kernel kernel(X_SfM_Mat, X_GPS_Mat);
210         const double lmeds_median = openMVG::robust::LeastMedianOfSquares(kernel, &sim);
211         if (lmeds_median != std::numeric_limits<double>::max())
212         {
213           b_usable_prior = true; // PRIOR can be used safely
214 
215           // Compute the median residual error once the registration is applied
216           for (Vec3 & pos : X_SfM) // Transform SfM poses for residual computation
217           {
218             pos = sim(pos);
219           }
220           Vec residual = (Eigen::Map<Mat3X>(X_SfM[0].data(), 3, X_SfM.size()) - Eigen::Map<Mat3X>(X_GPS[0].data(), 3, X_GPS.size())).colwise().norm();
221           std::sort(residual.data(), residual.data() + residual.size());
222           pose_center_robust_fitting_error = residual(residual.size()/2);
223 
224           // Apply the found transformation to the SfM Data Scene
225           openMVG::sfm::ApplySimilarity(sim, sfm_data);
226 
227           // Move entire scene to center for better numerical stability
228           Vec3 pose_centroid = Vec3::Zero();
229           for (const auto & pose_it : sfm_data.poses)
230           {
231             pose_centroid += (pose_it.second.center() / (double)sfm_data.poses.size());
232           }
233           sim_to_center = openMVG::geometry::Similarity3(openMVG::sfm::Pose3(Mat3::Identity(), pose_centroid), 1.0);
234           openMVG::sfm::ApplySimilarity(sim_to_center, sfm_data, true);
235         }
236       }
237       else
238       {
239         OPENMVG_LOG_WARNING << "Cannot used the motion prior, insufficient number of motion priors/poses";
240       }
241     }
242   }
243 
244   ceres::Problem problem;
245 
246   // Data wrapper for refinement:
247   Hash_Map<IndexT, std::vector<double>> map_intrinsics;
248   Hash_Map<IndexT, std::vector<double>> map_poses;
249 
250   // Setup Poses data & subparametrization
251   for (const auto & pose_it : sfm_data.poses)
252   {
253     const IndexT indexPose = pose_it.first;
254 
255     const Pose3 & pose = pose_it.second;
256     const Mat3 R = pose.rotation();
257     const Vec3 t = pose.translation();
258 
259     double angleAxis[3];
260     ceres::RotationMatrixToAngleAxis((const double*)R.data(), angleAxis);
261     // angleAxis + translation
262     map_poses[indexPose] = {angleAxis[0], angleAxis[1], angleAxis[2], t(0), t(1), t(2)};
263 
264     double * parameter_block = &map_poses.at(indexPose)[0];
265     problem.AddParameterBlock(parameter_block, 6);
266     if (options.extrinsics_opt == Extrinsic_Parameter_Type::NONE)
267     {
268       // set the whole parameter block as constant for best performance
269       problem.SetParameterBlockConstant(parameter_block);
270     }
271     else  // Subset parametrization
272     {
273       std::vector<int> vec_constant_extrinsic;
274       // If we adjust only the translation, we must set ROTATION as constant
275       if (options.extrinsics_opt == Extrinsic_Parameter_Type::ADJUST_TRANSLATION)
276       {
277         // Subset rotation parametrization
278         vec_constant_extrinsic.insert(vec_constant_extrinsic.end(), {0,1,2});
279       }
280       // If we adjust only the rotation, we must set TRANSLATION as constant
281       if (options.extrinsics_opt == Extrinsic_Parameter_Type::ADJUST_ROTATION)
282       {
283         // Subset translation parametrization
284         vec_constant_extrinsic.insert(vec_constant_extrinsic.end(), {3,4,5});
285       }
286       if (!vec_constant_extrinsic.empty())
287       {
288         ceres::SubsetParameterization *subset_parameterization =
289           new ceres::SubsetParameterization(6, vec_constant_extrinsic);
290         problem.SetParameterization(parameter_block, subset_parameterization);
291       }
292     }
293   }
294 
295   // Setup Intrinsics data & subparametrization
296   for (const auto & intrinsic_it : sfm_data.intrinsics)
297   {
298     const IndexT indexCam = intrinsic_it.first;
299 
300     if (isValid(intrinsic_it.second->getType()))
301     {
302       map_intrinsics[indexCam] = intrinsic_it.second->getParams();
303       if (!map_intrinsics.at(indexCam).empty())
304       {
305         double * parameter_block = &map_intrinsics.at(indexCam)[0];
306         problem.AddParameterBlock(parameter_block, map_intrinsics.at(indexCam).size());
307         if (options.intrinsics_opt == Intrinsic_Parameter_Type::NONE)
308         {
309           // set the whole parameter block as constant for best performance
310           problem.SetParameterBlockConstant(parameter_block);
311         }
312         else
313         {
314           const std::vector<int> vec_constant_intrinsic =
315             intrinsic_it.second->subsetParameterization(options.intrinsics_opt);
316           if (!vec_constant_intrinsic.empty())
317           {
318             ceres::SubsetParameterization *subset_parameterization =
319               new ceres::SubsetParameterization(
320                 map_intrinsics.at(indexCam).size(), vec_constant_intrinsic);
321             problem.SetParameterization(parameter_block, subset_parameterization);
322           }
323         }
324       }
325     }
326     else
327     {
328       OPENMVG_LOG_ERROR << "Unsupported camera type.";
329     }
330   }
331 
332   // Set a LossFunction to be less penalized by false measurements
333   //  - set it to nullptr if you don't want use a lossFunction.
334   ceres::LossFunction * p_LossFunction =
335     ceres_options_.bUse_loss_function_ ?
336       new ceres::HuberLoss(Square(4.0))
337       : nullptr;
338 
339   // For all visibility add reprojections errors:
340   for (auto & structure_landmark_it : sfm_data.structure)
341   {
342     const Observations & obs = structure_landmark_it.second.obs;
343 
344     for (const auto & obs_it : obs)
345     {
346       // Build the residual block corresponding to the track observation:
347       const View * view = sfm_data.views.at(obs_it.first).get();
348 
349       // Each Residual block takes a point and a camera as input and outputs a 2
350       // dimensional residual. Internally, the cost function stores the observed
351       // image location and compares the reprojection against the observation.
352       ceres::CostFunction* cost_function =
353         IntrinsicsToCostFunction(sfm_data.intrinsics.at(view->id_intrinsic).get(),
354                                  obs_it.second.x);
355 
356       if (cost_function)
357       {
358         if (!map_intrinsics.at(view->id_intrinsic).empty())
359         {
360           problem.AddResidualBlock(cost_function,
361             p_LossFunction,
362             &map_intrinsics.at(view->id_intrinsic)[0],
363             &map_poses.at(view->id_pose)[0],
364             structure_landmark_it.second.X.data());
365         }
366         else
367         {
368           problem.AddResidualBlock(cost_function,
369             p_LossFunction,
370             &map_poses.at(view->id_pose)[0],
371             structure_landmark_it.second.X.data());
372         }
373       }
374       else
375       {
376         OPENMVG_LOG_ERROR << "Cannot create a CostFunction for this camera model.";
377         return false;
378       }
379     }
380     if (options.structure_opt == Structure_Parameter_Type::NONE)
381       problem.SetParameterBlockConstant(structure_landmark_it.second.X.data());
382   }
383 
384   if (options.control_point_opt.bUse_control_points)
385   {
386     // Use Ground Control Point:
387     // - fixed 3D points with weighted observations
388     for (auto & gcp_landmark_it : sfm_data.control_points)
389     {
390       const Observations & obs = gcp_landmark_it.second.obs;
391 
392       for (const auto & obs_it : obs)
393       {
394         // Build the residual block corresponding to the track observation:
395         const View * view = sfm_data.views.at(obs_it.first).get();
396 
397         // Each Residual block takes a point and a camera as input and outputs a 2
398         // dimensional residual. Internally, the cost function stores the observed
399         // image location and compares the reprojection against the observation.
400         ceres::CostFunction* cost_function =
401           IntrinsicsToCostFunction(
402             sfm_data.intrinsics.at(view->id_intrinsic).get(),
403             obs_it.second.x,
404             options.control_point_opt.weight);
405 
406         if (cost_function)
407         {
408           if (!map_intrinsics.at(view->id_intrinsic).empty())
409           {
410             problem.AddResidualBlock(cost_function,
411                                      nullptr,
412                                      &map_intrinsics.at(view->id_intrinsic)[0],
413                                      &map_poses.at(view->id_pose)[0],
414                                      gcp_landmark_it.second.X.data());
415           }
416           else
417           {
418             problem.AddResidualBlock(cost_function,
419                                      nullptr,
420                                      &map_poses.at(view->id_pose)[0],
421                                      gcp_landmark_it.second.X.data());
422           }
423         }
424       }
425       if (obs.empty())
426       {
427         OPENMVG_LOG_ERROR
428           << "Cannot use this GCP id: " << gcp_landmark_it.first
429           << ". There is not linked image observation.";
430       }
431       else
432       {
433         // Set the 3D point as FIXED (it's a valid GCP)
434         problem.SetParameterBlockConstant(gcp_landmark_it.second.X.data());
435       }
436     }
437   }
438 
439   // Add Pose prior constraints if any
440   if (b_usable_prior)
441   {
442     for (const auto & view_it : sfm_data.GetViews())
443     {
444       const sfm::ViewPriors * prior = dynamic_cast<sfm::ViewPriors*>(view_it.second.get());
445       if (prior != nullptr && prior->b_use_pose_center_ && sfm_data.IsPoseAndIntrinsicDefined(prior))
446       {
447         // Add the cost functor (distance from Pose prior to the SfM_Data Pose center)
448         ceres::CostFunction * cost_function =
449           new ceres::AutoDiffCostFunction<PoseCenterConstraintCostFunction, 3, 6>(
450             new PoseCenterConstraintCostFunction(prior->pose_center_, prior->center_weight_));
451 
452         problem.AddResidualBlock(
453           cost_function,
454           new ceres::HuberLoss(
455             Square(pose_center_robust_fitting_error)),
456                    &map_poses.at(prior->id_view)[0]);
457       }
458     }
459   }
460 
461   // Configure a BA engine and run it
462   //  Make Ceres automatically detect the bundle structure.
463   ceres::Solver::Options ceres_config_options;
464   ceres_config_options.max_num_iterations = ceres_options_.max_num_iterations_;
465   ceres_config_options.preconditioner_type =
466     static_cast<ceres::PreconditionerType>(ceres_options_.preconditioner_type_);
467   ceres_config_options.linear_solver_type =
468     static_cast<ceres::LinearSolverType>(ceres_options_.linear_solver_type_);
469   ceres_config_options.sparse_linear_algebra_library_type =
470     static_cast<ceres::SparseLinearAlgebraLibraryType>(ceres_options_.sparse_linear_algebra_library_type_);
471   ceres_config_options.minimizer_progress_to_stdout = ceres_options_.bVerbose_;
472   ceres_config_options.logging_type = ceres::SILENT;
473   ceres_config_options.num_threads = ceres_options_.nb_threads_;
474 #if CERES_VERSION_MAJOR < 2
475   ceres_config_options.num_linear_solver_threads = ceres_options_.nb_threads_;
476 #endif
477   ceres_config_options.parameter_tolerance = ceres_options_.parameter_tolerance_;
478 
479   // Solve BA
480   ceres::Solver::Summary summary;
481   ceres::Solve(ceres_config_options, &problem, &summary);
482   if (ceres_options_.bCeres_summary_)
483     OPENMVG_LOG_INFO << summary.FullReport();
484 
485   // If no error, get back refined parameters
486   if (!summary.IsSolutionUsable())
487   {
488     OPENMVG_LOG_ERROR << "IsSolutionUsable is false. Bundle Adjustment failed.";
489     return false;
490   }
491   else // Solution is usable
492   {
493     if (ceres_options_.bVerbose_)
494     {
495       // Display statistics about the minimization
496       OPENMVG_LOG_INFO
497         << "\nBundle Adjustment statistics (approximated RMSE):\n"
498         << " #views: " << sfm_data.views.size() << "\n"
499         << " #poses: " << sfm_data.poses.size() << "\n"
500         << " #intrinsics: " << sfm_data.intrinsics.size() << "\n"
501         << " #tracks: " << sfm_data.structure.size() << "\n"
502         << " #residuals: " << summary.num_residuals << "\n"
503         << " Initial RMSE: " << std::sqrt( summary.initial_cost / summary.num_residuals) << "\n"
504         << " Final RMSE: " << std::sqrt( summary.final_cost / summary.num_residuals) << "\n"
505         << " Time (s): " << summary.total_time_in_seconds
506         << " \n--\n"
507         << " Used motion prior: " << static_cast<int>(b_usable_prior);
508     }
509 
510     // Update camera poses with refined data
511     if (options.extrinsics_opt != Extrinsic_Parameter_Type::NONE)
512     {
513       for (auto & pose_it : sfm_data.poses)
514       {
515         const IndexT indexPose = pose_it.first;
516 
517         Mat3 R_refined;
518         ceres::AngleAxisToRotationMatrix(&map_poses.at(indexPose)[0], R_refined.data());
519         Vec3 t_refined(map_poses.at(indexPose)[3], map_poses.at(indexPose)[4], map_poses.at(indexPose)[5]);
520         // Update the pose
521         Pose3 & pose = pose_it.second;
522         if (options.extrinsics_opt == Extrinsic_Parameter_Type::ADJUST_ROTATION)
523         {
524             // Update only rotation
525             pose.rotation() = R_refined;
526         }
527         else if (options.extrinsics_opt == Extrinsic_Parameter_Type::ADJUST_TRANSLATION)
528         {
529             // Update only translation
530             Vec3 C_refined = -R_refined.transpose() * t_refined;
531             pose.center() = C_refined;
532         }
533         else
534         {
535             // Update rotation + translation
536             pose = Pose3(R_refined, -R_refined.transpose() * t_refined);
537         }
538       }
539     }
540 
541     // Update camera intrinsics with refined data
542     if (options.intrinsics_opt != Intrinsic_Parameter_Type::NONE)
543     {
544       for (auto & intrinsic_it : sfm_data.intrinsics)
545       {
546         const IndexT indexCam = intrinsic_it.first;
547 
548         const std::vector<double> & vec_params = map_intrinsics.at(indexCam);
549         intrinsic_it.second->updateFromParams(vec_params);
550       }
551     }
552 
553     // Structure is already updated directly if needed (no data wrapping)
554 
555     if (b_usable_prior)
556     {
557       // set back to the original scene centroid
558       openMVG::sfm::ApplySimilarity(sim_to_center.inverse(), sfm_data, true);
559 
560       //--
561       // - Compute some fitting statistics
562       //--
563 
564       // Collect corresponding camera centers
565       std::vector<Vec3> X_SfM, X_GPS;
566       for (const auto & view_it : sfm_data.GetViews())
567       {
568         const sfm::ViewPriors * prior = dynamic_cast<sfm::ViewPriors*>(view_it.second.get());
569         if (prior != nullptr && prior->b_use_pose_center_ && sfm_data.IsPoseAndIntrinsicDefined(prior))
570         {
571           X_SfM.push_back( sfm_data.GetPoses().at(prior->id_pose).center() );
572           X_GPS.push_back( prior->pose_center_ );
573         }
574       }
575       // Compute the registration fitting error (once BA with Prior have been used):
576       if (X_GPS.size() > 3)
577       {
578         // Compute the median residual error
579         const Vec residual = (Eigen::Map<Mat3X>(X_SfM[0].data(), 3, X_SfM.size()) - Eigen::Map<Mat3X>(X_GPS[0].data(), 3, X_GPS.size())).colwise().norm();
580         std::ostringstream os;
581         os
582           << "Pose prior statistics (user units):\n"
583           << " - Starting median fitting error: " << pose_center_robust_fitting_error << "\n"
584           << " - Final fitting error:\n";
585         minMaxMeanMedian<Vec::Scalar>(residual.data(), residual.data() + residual.size(), os);
586         OPENMVG_LOG_INFO << os.str();
587       }
588     }
589     return true;
590   }
591 }
592 
593 } // namespace sfm
594 } // namespace openMVG
595