1 // This file is part of OpenMVG, an Open Multiple View Geometry C++ library.
2 
3 // Copyright (c) 2018 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/pipelines/stellar/stellar_solver.hpp"
10 
11 #include "openMVG/cameras/cameras.hpp"
12 #include "openMVG/geometry/pose3.hpp"
13 #include "openMVG/matching/indMatch.hpp"
14 #include "openMVG/multiview/triangulation_nview.hpp"
15 
16 #include "openMVG/sfm/pipelines/sfm_features_provider.hpp"
17 #include "openMVG/sfm/pipelines/sfm_matches_provider.hpp"
18 #include "openMVG/sfm/pipelines/stellar/stellar_definitions.hpp"
19 #include "openMVG/sfm/sfm_data_BA_ceres.hpp"
20 #include "openMVG/sfm/sfm_data_filters.hpp"
21 #include "openMVG/sfm/sfm_data_io.hpp"
22 #include "openMVG/sfm/sfm_data_triangulation.hpp"
23 #include "openMVG/system/logger.hpp"
24 
25 #include "openMVG/tracks/tracks.hpp"
26 #include "openMVG/types.hpp"
27 
28 #include <algorithm>
29 
30 #include "ceres/ceres.h"
31 
32 namespace openMVG {
33 namespace sfm {
34 
35 /// Function that estimate the relative scale of a triplet of pose.
36 /// Since a triplet is made of two edges, we list the 3 view tracks
37 /// Then compute the median depth for each pair and save the depth ratio
EstimateTripletRelativeScale(const Pair_Set & pairs,const SfM_Data & sfm_data,const Features_Provider * features_provider,const Matches_Provider * matches_provider,const Hash_Map<Pair,geometry::Pose3> & relative_poses,Relative_Scale & relative_scale)38 bool EstimateTripletRelativeScale
39 (
40   const Pair_Set & pairs,  // 2 pair of Pose Ids
41   const SfM_Data & sfm_data,
42   const Features_Provider * features_provider,
43   const Matches_Provider * matches_provider,
44   const Hash_Map<Pair, geometry::Pose3> & relative_poses,
45   Relative_Scale & relative_scale
46 )
47 {
48   if (pairs.size() != 2)
49   {
50     return false;
51   }
52   // Assert that:
53   // - at least there is 3 poses => Required condition to compute a scaling factor
54   // - that we have valid relative pose for triplet edges.
55   Hash_Map<IndexT, IndexT> pose_count;
56   std::set<IndexT> set_pose;
57   for (const Pair & it : pairs)
58   {
59     if (relative_poses.count(it) == 0)
60       return false;
61 
62     ++pose_count[it.first];
63     ++pose_count[it.second];
64     set_pose.insert(it.first);
65     set_pose.insert(it.second);
66   }
67   if (pose_count.size() != 3)
68   {
69     return false;
70   }
71   // Identify the common pose node_id
72   const IndexT node_id = [&]
73   {
74     for (const auto id_count : pose_count)
75     {
76       if (id_count.second == 2)
77         return id_count.first;
78     }
79     return UndefinedIndexT;
80   }();
81 
82   if (node_id == UndefinedIndexT)
83     return false;
84 
85   //
86   // Compute tracks/landmarks visibility for the 3-uplet
87   //
88   openMVG::tracks::STLMAPTracks map_tracksCommon;
89   {
90     matching::PairWiseMatches matches;
91     //-- List all view that shared some content with the used poses
92     for (const auto & matches_it : matches_provider->pairWise_matches_)
93     {
94       const Pair & pair = matches_it.first;
95 
96       const View
97         * view_I = sfm_data.GetViews().find(pair.first)->second.get(),
98         * view_J = sfm_data.GetViews().find(pair.second)->second.get();
99 
100       if (set_pose.count(view_I->id_pose)
101           && set_pose.count(view_J->id_pose))
102       {
103         const matching::IndMatches & pair_matches = matches_it.second;
104         matches[pair] = pair_matches;
105       }
106     }
107 
108     // Computing tracks
109     openMVG::tracks::TracksBuilder tracksBuilder;
110     tracksBuilder.Build(matches);
111     tracksBuilder.Filter(3);
112 
113     // If there is insufficient 3-view track count,
114     // We reject this triplet, since the relative scale factor cannot be reliably computed.
115     if (tracksBuilder.NbTracks() < 15)
116     {
117       return false;
118     }
119 
120     tracksBuilder.ExportToSTL(map_tracksCommon);
121   }
122 
123   //
124   // Triangulate observations for each pose pair
125   // Store track depth per pose pair
126   // Compute relative the median depth scale ratio factor
127   //
128   std::map<Pair, std::vector<double>> depths;
129   for (const auto & tracks : map_tracksCommon)
130   {
131     const tracks::submapTrack & track = tracks.second;
132 
133     for (const Pair & cu_pair : pairs)
134     {
135       //
136       // Triangulate the observed tracks
137       // and store the depth per pair to compute the scale ratio between the pair
138       //
139 
140       // Check the track is supported at least by 3 poses
141       {
142         std::set<IndexT> poses_id;
143         for (const auto & track_it : track)
144         {
145           const IndexT view_idx = track_it.first;
146           const View * view = sfm_data.GetViews().find(view_idx)->second.get();
147           poses_id.insert(view->id_pose);
148         }
149         if (poses_id.size() < 3)
150           continue;
151       }
152 
153       std::map<IndexT, geometry::Pose3> poses;
154       std::vector<Vec3> bearing;
155       std::vector<Mat34> vec_poses;
156 
157       for (const auto & track_it : track)
158       {
159         const IndexT view_idx = track_it.first;
160         const View * view = sfm_data.GetViews().find(view_idx)->second.get();
161 
162         if (view->id_pose == cu_pair.first || view->id_pose == cu_pair.second)
163         {
164           const geometry::Pose3 pose = (view->id_pose == cu_pair.first) ?
165             geometry::Pose3(Mat3::Identity(), Vec3::Zero())
166             : relative_poses.at(cu_pair);
167 
168           const auto cam = sfm_data.GetIntrinsics().at(view->id_intrinsic).get();
169 
170           poses[view->id_pose] = pose;
171 
172           vec_poses.emplace_back(pose.asMatrix());
173           const size_t feat_idx = track_it.second;
174           const Vec2 feat_pos = features_provider->feats_per_view.at(view_idx)[feat_idx].coords().cast<double>();
175           bearing.emplace_back((*cam)(cam->get_ud_pixel(feat_pos)));
176         }
177       }
178       const Eigen::Map<const Mat3X> bearing_matrix(bearing[0].data(), 3, bearing.size());
179       Vec4 Xhomogeneous;
180       if (TriangulateNViewAlgebraic(bearing_matrix, vec_poses, &Xhomogeneous))
181       {
182         const Vec3 X = Xhomogeneous.hnormalized();
183 
184         const geometry::Pose3 pose = poses[node_id];
185         const double depth = (X - pose.center()).norm();
186         // Store the depth for this 2-uplet of edges
187         depths[cu_pair].push_back(depth);
188       }
189     }
190   }
191 
192   if (depths.size() != 2) // Else one of the view did not produce any depth data
193   {
194     return false;
195   }
196 
197   // Compute median depths
198   std::vector<float> median_depths;
199   median_depths.reserve(2);
200   for (auto & depth_it : depths)
201   {
202     if (depth_it.second.empty())
203       continue;
204 
205     std::nth_element(
206       depth_it.second.begin(),
207       depth_it.second.begin()+(depth_it.second.size()/2),
208       depth_it.second.end());
209     const double median = depth_it.second[depth_it.second.size()/2];
210     median_depths.push_back(median);
211     //std::cout << "(" << depth_it.first.first << "," << depth_it.first.second << ") : " << median << ", "
212     //  << depth_it.second.size() << " points." << std::endl;
213   }
214 
215   const double depth_ratio = median_depths[0] / median_depths[1];
216 
217   // Return the computed triplet depth ratio:
218   relative_scale = {*pairs.cbegin(), *std::next(pairs.cbegin(), 1), depth_ratio};
219 
220   const bool b_refine_triplet = false;
221   if (b_refine_triplet)
222   {
223     // Build a triplet that contained the computed depth ratio:
224     const Pair pair01 = relative_scale.pairs[0];
225     const Pair pair12 = relative_scale.pairs[1];
226 
227     Hash_Map<IndexT, geometry::Pose3> triplet_pose;
228 
229     triplet_pose[node_id] = geometry::Pose3(); // Identity
230     if (pair01.first == node_id)
231     {
232       geometry::Pose3 relative_pose = relative_poses.at(pair01);
233       relative_pose.center() /= depth_ratio;
234       triplet_pose[pair01.second] = relative_pose;
235     }
236     else
237     {
238       geometry::Pose3 relative_pose = relative_poses.at(pair01).inverse();
239       relative_pose.center() /= depth_ratio;
240       triplet_pose[pair01.first] = relative_pose;
241     }
242 
243     if (pair12.first == node_id)
244     {
245       geometry::Pose3 relative_pose = relative_poses.at(pair12);
246       triplet_pose[pair12.second] = relative_pose;
247     }
248     else
249     {
250       geometry::Pose3 relative_pose = relative_poses.at(pair12).inverse();
251       triplet_pose[pair12.first] = relative_pose;
252     }
253 
254     // Create a scene containing the triplet and save it to disk:
255     SfM_Data tiny_scene;
256     for (const auto &  triplet_pose_it : triplet_pose)
257     {
258       const IndexT pose_id = triplet_pose_it.first;
259       // Add view
260       // Add intrinsic
261       // Add poses
262       const View * view = sfm_data.GetViews().at(pose_id).get();
263       tiny_scene.views.insert(*sfm_data.GetViews().find(pose_id));
264       tiny_scene.intrinsics.insert(*sfm_data.GetIntrinsics().find(view->id_intrinsic));
265       tiny_scene.poses[pose_id] = triplet_pose[pose_id];
266     }
267 
268     // Add tracks
269     Landmarks & landmarks = tiny_scene.structure;
270     for (const auto & tracks : map_tracksCommon)
271     {
272       const tracks::submapTrack & track = tracks.second;
273       Observations obs;
274       for (const auto & track_it : track)
275       {
276         const IndexT I = track_it.first;
277         const size_t featIndexI = track_it.second;
278         const Vec2 x = features_provider->feats_per_view.at(I)[featIndexI].coords().cast<double>();
279 
280         obs[I] = std::move(Observation(x, featIndexI));
281       }
282       landmarks[tracks.first].obs = std::move(obs);
283     }
284 
285     // Triangulation
286     sfm::SfM_Data_Structure_Computation_Blind structure_estimator(false);
287     structure_estimator.triangulate(tiny_scene);
288 
289     // - refine only Structure and Rotations & translations (keep intrinsic constant)
290     Bundle_Adjustment_Ceres::BA_Ceres_options options(true, false);
291     options.linear_solver_type_ = ceres::DENSE_SCHUR;
292     Bundle_Adjustment_Ceres bundle_adjustment_obj(options);
293     const Optimize_Options ba_refine_paramerer_options
294     (
295       cameras::Intrinsic_Parameter_Type::NONE,
296       Extrinsic_Parameter_Type::ADJUST_ALL,
297       Structure_Parameter_Type::ADJUST_ALL
298     );
299     if (!bundle_adjustment_obj.Adjust(tiny_scene, ba_refine_paramerer_options))
300     {
301       return false;
302     }
303   } // end -- b_refine_triplet
304 
305   return true;
306 }
307 
Stellar_Solver(const StellarPod & stellar_pod,const Hash_Map<Pair,Pose3> & relative_poses,const SfM_Data & sfm_data,const Matches_Provider * matches_provider,const Features_Provider * features_provider,const bool use_all_matches,const bool use_threading)308 Stellar_Solver::Stellar_Solver
309 (
310   const StellarPod & stellar_pod,
311   const Hash_Map<Pair, Pose3> & relative_poses,
312   const SfM_Data & sfm_data,
313   const Matches_Provider * matches_provider,
314   const Features_Provider * features_provider,
315   const bool use_all_matches,
316   const bool use_threading
317 ):stellar_pod_(stellar_pod),
318   relative_poses_(relative_poses),
319   sfm_data_(sfm_data),
320   matches_provider_(matches_provider),
321   features_provider_(features_provider),
322   use_all_matches_(use_all_matches),
323   use_threading_(use_threading)
324 {
325 }
326 
Solve(Poses & poses)327 bool Stellar_Solver::Solve(Poses & poses)
328 {
329   // Check if the stellar pod is solvable
330   // - one common node defined in every pair
331   const std::set<IndexT> set_pose = [&] {
332     std::set<IndexT> pose_set;
333     for (const Pair & it : stellar_pod_)
334     {
335       pose_set.insert(it.first);
336       pose_set.insert(it.second);
337     }
338     return pose_set;
339   }();
340   const std::vector<IndexT> pose_count = [&]{
341     std::vector<IndexT> vec(set_pose.size(), 0);
342     for (const Pair & it : stellar_pod_)
343     {
344       ++vec[std::distance(set_pose.cbegin(), set_pose.find(it.first))];
345       ++vec[std::distance(set_pose.cbegin(), set_pose.find(it.second))];
346     }
347     return vec;
348   }();
349   if (pose_count.size() < 3)
350     return false;
351 
352   const auto iterator_node_id = max_element(pose_count.cbegin(), pose_count.cend());
353 
354   if (iterator_node_id == pose_count.cend())
355     return false;
356 
357   const IndexT central_node_id = *std::next(set_pose.cbegin(), std::distance(pose_count.cbegin(), iterator_node_id));
358 
359   const std::vector<Pair_Set> edge_two_uplets = ListEdge2Uplets();
360 
361   OPENMVG_LOG_INFO
362     << "Stellar pod details:\n"
363     << "#central pose id: " << central_node_id << "\n"
364     << "#pairs: " << stellar_pod_.size() << "\n"
365     << "#2-uplets: " << edge_two_uplets.size();
366 
367   std::vector<Relative_Scale> relative_scales;
368   if (!Solve2UpletsRelativeScales(edge_two_uplets, relative_scales))
369     return false;
370 
371   Hash_Map<IndexT, geometry::Pose3> stellar_poses;
372   if (!SolveStellarPoses(central_node_id, relative_scales, stellar_poses))
373     return false;
374 
375   SfM_Data sfm_data_stellar_pod;
376   if (!Optimize(sfm_data_stellar_pod, stellar_poses, relative_scales))
377     return false;
378   poses = sfm_data_stellar_pod.poses;
379 
380   /*std::ostringstream os;
381   os << "Stellar_" << central_node_id;
382   Save(sfm_data_stellar_pod,
383        stlplus::create_filespec("./",os.str(),".ply"),
384        ESfM_Data(ALL));
385        */
386   return poses.size() >= 2;
387 }
388 
ListEdge2Uplets()389 std::vector<Pair_Set> Stellar_Solver::ListEdge2Uplets()
390 {
391   // List the possible edge 2-uplets (triplet of poses)
392   std::vector<Pair_Set> two_uplets;
393   for (auto it0 = stellar_pod_.cbegin(); it0 != stellar_pod_.cend(); ++it0)
394   {
395     for (auto it1 = std::next(it0, 1); it1 != stellar_pod_.cend(); ++it1)
396     {
397       two_uplets.push_back({*it0, *it1});
398     }
399   }
400   return two_uplets;
401 }
402 
Solve2UpletsRelativeScales(const std::vector<Pair_Set> & edge_two_uplets,std::vector<Relative_Scale> & relative_scales)403 bool Stellar_Solver::Solve2UpletsRelativeScales
404 (
405   const std::vector<Pair_Set> & edge_two_uplets,
406   std::vector<Relative_Scale> & relative_scales
407 )
408 {
409   // List possible 2-uplet and solve their relative scales
410   // some 2-uplet cannot lead to a relative scale if they don't share a sufficient track amount
411   for (const auto edge_two_uplet : edge_two_uplets)
412   {
413     Relative_Scale relative_scale;
414     if (EstimateTripletRelativeScale(
415           edge_two_uplet,  // Pair of pose Ids that define a triplet of pose
416           sfm_data_,
417           features_provider_,
418           matches_provider_,
419           relative_poses_,
420           relative_scale))
421     {
422       relative_scales.emplace_back(relative_scale);
423     }
424   }
425   return !relative_scales.empty();
426 }
427 
SolveStellarPoses(const IndexT & central_node_id,const std::vector<Relative_Scale> & relative_scales,Hash_Map<IndexT,geometry::Pose3> & triplet_poses)428 bool Stellar_Solver::SolveStellarPoses
429 (
430   const IndexT & central_node_id,
431   const std::vector<Relative_Scale> & relative_scales,
432   Hash_Map<IndexT, geometry::Pose3> & triplet_poses
433 )
434 {
435   // Re-scale edges to a common scale
436   if (!Solve_stellar_translation_scales_averaging(
437     central_node_id,
438     relative_scales,
439     relative_poses_,
440     triplet_poses,
441     Stellar_Translation_Averaging_Solver_Type::SCALING_SOLVER_L2_FULL))
442   {
443     return false;
444   }
445   return true;
446 }
447 
Optimize(SfM_Data & stellar_pod_reconstruction,const Hash_Map<IndexT,geometry::Pose3> & triplet_poses,const std::vector<Relative_Scale> & relative_scales)448 bool Stellar_Solver::Optimize
449 (
450   SfM_Data & stellar_pod_reconstruction,
451   const Hash_Map<IndexT, geometry::Pose3> & triplet_poses,
452   const std::vector<Relative_Scale> & relative_scales
453 )
454 {
455   if (triplet_poses.size() < 3)
456   {
457     return false;
458   }
459 
460   stellar_pod_reconstruction.poses.clear();
461   stellar_pod_reconstruction.intrinsics.clear();
462   stellar_pod_reconstruction.structure.clear();
463   stellar_pod_reconstruction.views.clear();
464 
465   // 1. Fill the sfm_data scene with view, intrinsic, poses
466   // 2. Select valid matches pairs, build track and triangulate them
467   // 3. Refine the sfm_data scene
468 
469   // Add the poses
470   {
471     for (const auto & pose_it : triplet_poses)
472     {
473       stellar_pod_reconstruction.poses[pose_it.first] = pose_it.second;
474     }
475   }
476 
477   // Collect the pairs used by this stellar pod
478   const Pair_Set & used_pairs = Relative_Scale::Get_pairs(relative_scales);
479 
480   // Collect, matches, intrinsics and views data linked to the poses ids
481   openMVG::tracks::STLMAPTracks tracks;
482   {
483     matching::PairWiseMatches matches;
484     for (const auto & matches_it : matches_provider_->pairWise_matches_)
485     {
486       const Pair & pair = matches_it.first;
487 
488       const View * view_I = sfm_data_.GetViews().find(pair.first)->second.get();
489       const View * view_J = sfm_data_.GetViews().find(pair.second)->second.get();
490 
491       if (triplet_poses.count(view_I->id_pose)
492           && triplet_poses.count(view_J->id_pose))
493       {
494         // Collect matches
495         const Pair pose_pair(std::min(view_I->id_pose,view_J->id_pose),
496                              std::max(view_I->id_pose,view_J->id_pose));
497         const matching::IndMatches & pair_matches = matches_it.second;
498         if (!use_all_matches_)
499         {
500           if (used_pairs.count(pose_pair))
501             matches[pair] = pair_matches;
502           else
503             continue; // This pair is ignored
504         }
505         else
506         {
507           matches[pair] = pair_matches;
508         }
509 
510         // Add view information and related intrinsic data
511         const std::array<const View*, 2> view_ids = {view_I, view_J};
512         for (const View* view : view_ids)
513         {
514           if (stellar_pod_reconstruction.views.count(view->id_view) == 0)
515             stellar_pod_reconstruction.views.insert(*(sfm_data_.GetViews().find(view->id_view)));
516           if (stellar_pod_reconstruction.intrinsics.count(view->id_intrinsic) == 0)
517             stellar_pod_reconstruction.intrinsics.insert(*(sfm_data_.GetIntrinsics().find(view->id_intrinsic)));
518         }
519       }
520     }
521     // Computing tracks
522     {
523       openMVG::tracks::TracksBuilder tracksBuilder;
524       tracksBuilder.Build(matches);
525       tracksBuilder.Filter(2); // [2-n] view based matches
526       tracksBuilder.ExportToSTL(tracks);
527     }
528   }
529 
530   // Fill the sfm_data landmark observations and 3d initial position (triangulation):
531   {
532     // Add the track observations to the sfm_data
533     Landmarks & landmarks = stellar_pod_reconstruction.structure;
534     for (const auto & tracks_it : tracks)
535     {
536       const tracks::submapTrack & track = tracks_it.second;
537 
538       // Check if the track is observed by more than 2 differnt pose ids
539       {
540         std::set<IndexT> poses_id;
541         for (const auto & track_it : track)
542         {
543           const IndexT view_idx = track_it.first;
544           const View * view = sfm_data_.GetViews().find(view_idx)->second.get();
545           poses_id.insert(view->id_pose);
546           if (poses_id.size() > 2) break; // early exit
547         }
548         if (poses_id.size() < 2)
549           continue;
550       }
551       // Collect the views and features observing this landmark
552       landmarks[tracks_it.first].obs = [&]
553       {
554         Observations obs;
555         for (const auto & track_it : track)
556         {
557           const IndexT view_idx = track_it.first;
558           const IndexT feat_idx = track_it.second;
559           const Vec2 x = features_provider_->feats_per_view.at(view_idx)[feat_idx].coords().cast<double>();
560 
561           obs[view_idx] = {x, feat_idx};
562         }
563         return obs;
564       }();
565     }
566     // Triangulate
567     sfm::SfM_Data_Structure_Computation_Blind structure_estimator(false);
568     structure_estimator.triangulate(stellar_pod_reconstruction);
569   }
570 
571   // - refine only Structure and Rotations & translations (keep intrinsic constant)
572   Bundle_Adjustment_Ceres::BA_Ceres_options options(false, use_threading_);
573   options.linear_solver_type_ = ceres::DENSE_SCHUR;
574   Bundle_Adjustment_Ceres bundle_adjustment_obj(options);
575   const Optimize_Options ba_refine_parameter_options
576   (
577     cameras::Intrinsic_Parameter_Type::NONE,
578     Extrinsic_Parameter_Type::ADJUST_ALL,
579     Structure_Parameter_Type::ADJUST_ALL
580   );
581   if (!bundle_adjustment_obj.Adjust(stellar_pod_reconstruction, ba_refine_parameter_options))
582   {
583     return false;
584   }
585 
586   // In order to ensure that the scene is refined correctly and valid we perform
587   // a second run of optimization where the matches that most lead to invalid triangulation are removed.
588   {
589     const IndexT min_point_per_pose = 12; // 6 min -> Keep more since depending of the camera intrinsic DoF we can need more
590     const IndexT min_track_length = 3; // 2 min
591     const double depth_median_limit_factor =  5.2;    //5.2 * median ~= X84,
592     DepthCleaning(
593       stellar_pod_reconstruction,
594       depth_median_limit_factor,
595       min_point_per_pose,
596       min_track_length);
597 
598     // Remove outliers (max_angle, residual error)
599     const size_t pointcount_initial = stellar_pod_reconstruction.structure.size();
600     RemoveOutliers_PixelResidualError(stellar_pod_reconstruction, 4.0);
601     RemoveOutliers_AngleError(stellar_pod_reconstruction, 2.0);
602 
603     // Remove poses that does not cover a sufficient number of observations (some observations are removed too)
604     eraseUnstablePosesAndObservations(stellar_pod_reconstruction, min_point_per_pose, min_track_length);
605 
606     if (!bundle_adjustment_obj.Adjust(stellar_pod_reconstruction, ba_refine_parameter_options))
607     {
608       return false;
609     }
610   }
611 
612   return (stellar_pod_reconstruction.GetPoses().size() >= 3);
613 }
614 
615 } // namespace sfm
616 } // namespace openMVG
617