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