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