|
| 1 | +#include "glomap/controllers/global_mapper.h" |
| 2 | +#include "glomap/controllers/rotation_averager.h" |
| 3 | +#include "glomap/estimators/gravity_refinement.h" |
| 4 | +#include "glomap/io/colmap_io.h" |
| 5 | +#include "glomap/math/rigid3d.h" |
| 6 | +#include "glomap/types.h" |
| 7 | + |
| 8 | +#include <colmap/estimators/alignment.h> |
| 9 | +#include <colmap/scene/synthetic.h> |
| 10 | +#include <colmap/util/testing.h> |
| 11 | + |
| 12 | +#include <gtest/gtest.h> |
| 13 | + |
| 14 | +namespace glomap { |
| 15 | +namespace { |
| 16 | + |
| 17 | +void CreateRandomRotation(const double stddev, Eigen::Quaterniond& q) { |
| 18 | + std::random_device rd{}; |
| 19 | + std::mt19937 gen{rd()}; |
| 20 | + |
| 21 | + // Construct a random axis |
| 22 | + double theta = double(rand()) / RAND_MAX * 2 * M_PI; |
| 23 | + double phi = double(rand()) / RAND_MAX * M_PI; |
| 24 | + Eigen::Vector3d axis(std::cos(theta) * std::sin(phi), |
| 25 | + std::sin(theta) * std::sin(phi), |
| 26 | + std::cos(phi)); |
| 27 | + |
| 28 | + // Construct a random angle |
| 29 | + std::normal_distribution<double> d{0, stddev}; |
| 30 | + double angle = d(gen); |
| 31 | + q = Eigen::AngleAxisd(angle, axis); |
| 32 | +} |
| 33 | + |
| 34 | +void PrepareGravity(const colmap::Reconstruction& gt, |
| 35 | + std::unordered_map<image_t, Image>& images, |
| 36 | + double stddev_gravity = 0.0, |
| 37 | + double outlier_ratio = 0.0) { |
| 38 | + for (auto& image_id : gt.RegImageIds()) { |
| 39 | + Eigen::Vector3d gravity = |
| 40 | + gt.Image(image_id).CamFromWorld().rotation * Eigen::Vector3d(0, 1, 0); |
| 41 | + |
| 42 | + if (stddev_gravity > 0.0) { |
| 43 | + Eigen::Quaterniond q; |
| 44 | + CreateRandomRotation(DegToRad(stddev_gravity), q); |
| 45 | + gravity = q * gravity; |
| 46 | + } |
| 47 | + |
| 48 | + if (outlier_ratio > 0.0 && double(rand()) / RAND_MAX < outlier_ratio) { |
| 49 | + Eigen::Quaterniond q; |
| 50 | + CreateRandomRotation(1., q); |
| 51 | + gravity = Rigid3dToAngleAxis(Rigid3d(q, Eigen::Vector3d::Zero())).normalized(); |
| 52 | + } |
| 53 | + images[image_id].gravity_info.SetGravity(gravity); |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | + |
| 58 | +GlobalMapperOptions CreateMapperTestOptions() { |
| 59 | + GlobalMapperOptions options; |
| 60 | + options.skip_view_graph_calibration = false; |
| 61 | + options.skip_relative_pose_estimation = false; |
| 62 | + options.skip_rotation_averaging = true; |
| 63 | + options.skip_track_establishment = true; |
| 64 | + options.skip_global_positioning = true; |
| 65 | + options.skip_bundle_adjustment = true; |
| 66 | + options.skip_retriangulation = true; |
| 67 | + |
| 68 | + return options; |
| 69 | +} |
| 70 | + |
| 71 | +RotationAveragerOptions CreateRATestOptions(bool use_gravity = false) { |
| 72 | + RotationAveragerOptions options; |
| 73 | + options.skip_initialization = true; |
| 74 | + options.use_gravity = use_gravity; |
| 75 | + return options; |
| 76 | +} |
| 77 | + |
| 78 | +void ExpectEqualRotations( |
| 79 | + const colmap::Reconstruction& gt, |
| 80 | + const colmap::Reconstruction& computed, |
| 81 | + const double max_rotation_error_deg) { |
| 82 | + |
| 83 | + const std::set<image_t> reg_image_ids_set = gt.RegImageIds(); |
| 84 | + std::vector<image_t> reg_image_ids(reg_image_ids_set.begin(), |
| 85 | + reg_image_ids_set.end()); |
| 86 | + for (size_t i = 0; i < reg_image_ids.size(); i++) { |
| 87 | + const image_t image_id1 = reg_image_ids[i]; |
| 88 | + for (size_t j = 0; j < reg_image_ids.size(); j++) { |
| 89 | + if (i == j) continue; |
| 90 | + const image_t image_id2 = reg_image_ids[j]; |
| 91 | + |
| 92 | + const Rigid3d cam2_from_cam1 = |
| 93 | + computed.Image(image_id2).CamFromWorld() * |
| 94 | + colmap::Inverse(computed.Image(image_id1).CamFromWorld()); |
| 95 | + |
| 96 | + const Rigid3d cam2_from_cam1_gt = |
| 97 | + gt.Image(image_id2).CamFromWorld() * |
| 98 | + colmap::Inverse(gt.Image(image_id1).CamFromWorld()); |
| 99 | + |
| 100 | + double rotation_error_deg = CalcAngle(cam2_from_cam1_gt, cam2_from_cam1); |
| 101 | + EXPECT_LT(rotation_error_deg, max_rotation_error_deg); |
| 102 | + } |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +void ExpectEqualGravity( |
| 107 | + const colmap::Reconstruction& gt, |
| 108 | + const std::unordered_map<image_t, Image>& images_computed, |
| 109 | + const double max_gravity_error_deg) { |
| 110 | + for (const auto& image_id : gt.RegImageIds()) { |
| 111 | + const Eigen::Vector3d gravity_gt = |
| 112 | + gt.Image(image_id).CamFromWorld().rotation * Eigen::Vector3d(0, 1, 0); |
| 113 | + const Eigen::Vector3d gravity_computed = |
| 114 | + images_computed.at(image_id).gravity_info.GetGravity(); |
| 115 | + |
| 116 | + double gravity_error_deg = CalcAngle(gravity_gt, gravity_computed); |
| 117 | + EXPECT_LT(gravity_error_deg, max_gravity_error_deg); |
| 118 | + |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +TEST(RotationEstimator, WithoutNoise) { |
| 123 | + const std::string database_path = colmap::CreateTestDir() + "/database.db"; |
| 124 | + |
| 125 | + // FLAGS_v = 2; |
| 126 | + colmap::Database database(database_path); |
| 127 | + colmap::Reconstruction gt_reconstruction; |
| 128 | + colmap::SyntheticDatasetOptions synthetic_dataset_options; |
| 129 | + synthetic_dataset_options.num_cameras = 2; |
| 130 | + synthetic_dataset_options.num_images = 9; |
| 131 | + synthetic_dataset_options.num_points3D = 50; |
| 132 | + synthetic_dataset_options.point2D_stddev = 0; |
| 133 | + colmap::SynthesizeDataset( |
| 134 | + synthetic_dataset_options, >_reconstruction, &database); |
| 135 | + |
| 136 | + ViewGraph view_graph; |
| 137 | + std::unordered_map<camera_t, Camera> cameras; |
| 138 | + std::unordered_map<image_t, Image> images; |
| 139 | + std::unordered_map<track_t, Track> tracks; |
| 140 | + |
| 141 | + ConvertDatabaseToGlomap(database, view_graph, cameras, images); |
| 142 | + |
| 143 | + // PrepareRelativeRotations(view_graph, images); |
| 144 | + PrepareGravity(gt_reconstruction, images); |
| 145 | + |
| 146 | + GlobalMapper global_mapper(CreateMapperTestOptions()); |
| 147 | + global_mapper.Solve(database, view_graph, cameras, images, tracks); |
| 148 | + |
| 149 | + // Version with Gravity |
| 150 | + for (bool use_gravity : {true, false}) { |
| 151 | + SolveRotationAveraging( |
| 152 | + view_graph, images, CreateRATestOptions(use_gravity)); |
| 153 | + |
| 154 | + colmap::Reconstruction reconstruction; |
| 155 | + ConvertGlomapToColmap(cameras, images, tracks, reconstruction); |
| 156 | + ExpectEqualRotations( |
| 157 | + gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/1e-2); |
| 158 | + } |
| 159 | +} |
| 160 | + |
| 161 | +TEST(RotationEstimator, WithNoiseAndOutliers) { |
| 162 | + const std::string database_path = colmap::CreateTestDir() + "/database.db"; |
| 163 | + |
| 164 | + // FLAGS_v = 1; |
| 165 | + colmap::Database database(database_path); |
| 166 | + colmap::Reconstruction gt_reconstruction; |
| 167 | + colmap::SyntheticDatasetOptions synthetic_dataset_options; |
| 168 | + synthetic_dataset_options.num_cameras = 2; |
| 169 | + synthetic_dataset_options.num_images = 7; |
| 170 | + synthetic_dataset_options.num_points3D = 100; |
| 171 | + // synthetic_dataset_options.point2D_stddev = 0.5; |
| 172 | + synthetic_dataset_options.point2D_stddev = 1; |
| 173 | + synthetic_dataset_options.inlier_match_ratio = 0.6; |
| 174 | + colmap::SynthesizeDataset( |
| 175 | + synthetic_dataset_options, >_reconstruction, &database); |
| 176 | + |
| 177 | + ViewGraph view_graph; |
| 178 | + std::unordered_map<camera_t, Camera> cameras; |
| 179 | + std::unordered_map<image_t, Image> images; |
| 180 | + std::unordered_map<track_t, Track> tracks; |
| 181 | + |
| 182 | + ConvertDatabaseToGlomap(database, view_graph, cameras, images); |
| 183 | + |
| 184 | + PrepareGravity(gt_reconstruction, images, /*stddev_gravity=*/3e-1); |
| 185 | + |
| 186 | + GlobalMapper global_mapper(CreateMapperTestOptions()); |
| 187 | + global_mapper.Solve(database, view_graph, cameras, images, tracks); |
| 188 | + |
| 189 | + for (bool use_gravity : {true, false}) { |
| 190 | + SolveRotationAveraging( |
| 191 | + view_graph, images, CreateRATestOptions(use_gravity)); |
| 192 | + |
| 193 | + colmap::Reconstruction reconstruction; |
| 194 | + ConvertGlomapToColmap(cameras, images, tracks, reconstruction); |
| 195 | + if (use_gravity) |
| 196 | + ExpectEqualRotations( |
| 197 | + gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/1.); |
| 198 | + else |
| 199 | + ExpectEqualRotations( |
| 200 | + gt_reconstruction, reconstruction, /*max_rotation_error_deg=*/2.); |
| 201 | + } |
| 202 | + |
| 203 | +} |
| 204 | + |
| 205 | +TEST(RotationEstimator, RefineGravity) { |
| 206 | + const std::string database_path = colmap::CreateTestDir() + "/database.db"; |
| 207 | + |
| 208 | + // FLAGS_v = 2; |
| 209 | + colmap::Database database(database_path); |
| 210 | + colmap::Reconstruction gt_reconstruction; |
| 211 | + colmap::SyntheticDatasetOptions synthetic_dataset_options; |
| 212 | + synthetic_dataset_options.num_cameras = 2; |
| 213 | + synthetic_dataset_options.num_images = 100; |
| 214 | + synthetic_dataset_options.num_points3D = 50; |
| 215 | + synthetic_dataset_options.point2D_stddev = 0; |
| 216 | + colmap::SynthesizeDataset( |
| 217 | + synthetic_dataset_options, >_reconstruction, &database); |
| 218 | + |
| 219 | + ViewGraph view_graph; |
| 220 | + std::unordered_map<camera_t, Camera> cameras; |
| 221 | + std::unordered_map<image_t, Image> images; |
| 222 | + std::unordered_map<track_t, Track> tracks; |
| 223 | + |
| 224 | + ConvertDatabaseToGlomap(database, view_graph, cameras, images); |
| 225 | + |
| 226 | + // PrepareRelativeRotations(view_graph, images); |
| 227 | + PrepareGravity(gt_reconstruction, images, /*stddev_gravity=*/0., /*outlier_ratio=*/0.3); |
| 228 | + |
| 229 | + GlobalMapper global_mapper(CreateMapperTestOptions()); |
| 230 | + global_mapper.Solve(database, view_graph, cameras, images, tracks); |
| 231 | + |
| 232 | + |
| 233 | + GravityRefinerOptions opt_grav_refine; |
| 234 | + GravityRefiner grav_refiner(opt_grav_refine); |
| 235 | + grav_refiner.RefineGravity(view_graph, images); |
| 236 | + |
| 237 | + // Check whether the gravity does not have error after refinement |
| 238 | + ExpectEqualGravity(gt_reconstruction, images, /*max_gravity_error_deg=*/1e-2); |
| 239 | + |
| 240 | + |
| 241 | +} |
| 242 | + |
| 243 | +} // namespace |
| 244 | +} // namespace glomap |
0 commit comments