/* * Copyright 2016 The Cartographer Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "cartographer/mapping/internal/2d/pose_graph_2d.h" #include #include #include #include "absl/memory/memory.h" #include "cartographer/common/internal/testing/lua_parameter_dictionary_test_helpers.h" #include "cartographer/common/thread_pool.h" #include "cartographer/common/time.h" #include "cartographer/mapping/2d/probability_grid_range_data_inserter_2d.h" #include "cartographer/mapping/2d/submap_2d.h" #include "cartographer/transform/rigid_transform.h" #include "cartographer/transform/rigid_transform_test_helpers.h" #include "cartographer/transform/transform.h" #include "gmock/gmock.h" namespace cartographer { namespace mapping { namespace { class PoseGraph2DTest : public ::testing::Test { protected: PoseGraph2DTest() : thread_pool_(1) { // Builds a wavy, irregularly circular point cloud that is unique // rotationally. This gives us good rotational texture and avoids any // possibility of the CeresScanMatcher2D preferring free space (> // kMinProbability) to unknown space (== kMinProbability). for (float t = 0.f; t < 2.f * M_PI; t += 0.005f) { const float r = (std::sin(20.f * t) + 2.f) * std::sin(t + 2.f); point_cloud_.push_back( {Eigen::Vector3f{r * std::sin(t), r * std::cos(t), 0.f}}); } { auto parameter_dictionary = common::MakeDictionary(R"text( return { num_range_data = 1, grid_options_2d = { grid_type = "PROBABILITY_GRID", resolution = 0.05, }, range_data_inserter = { range_data_inserter_type = "PROBABILITY_GRID_INSERTER_2D", probability_grid_range_data_inserter = { insert_free_space = true, hit_probability = 0.53, miss_probability = 0.495, }, tsdf_range_data_inserter = { truncation_distance = 0.3, maximum_weight = 10., update_free_space = false, normal_estimation_options = { num_normal_samples = 4, sample_radius = 0.5, }, project_sdf_distance_to_scan_normal = false, update_weight_range_exponent = 0, update_weight_angle_scan_normal_to_ray_kernel_bandwidth = 0, update_weight_distance_cell_to_hit_kernel_bandwidth = 0, }, }, })text"); active_submaps_ = absl::make_unique( mapping::CreateSubmapsOptions2D(parameter_dictionary.get())); } { auto parameter_dictionary = common::MakeDictionary(R"text( return { optimize_every_n_nodes = 1000, constraint_builder = { sampling_ratio = 1., max_constraint_distance = 6., min_score = 0.5, global_localization_min_score = 0.6, loop_closure_translation_weight = 1., loop_closure_rotation_weight = 1., log_matches = true, fast_correlative_scan_matcher = { linear_search_window = 3., angular_search_window = 0.1, branch_and_bound_depth = 3, }, ceres_scan_matcher = { occupied_space_weight = 20., translation_weight = 10., rotation_weight = 1., ceres_solver_options = { use_nonmonotonic_steps = true, max_num_iterations = 50, num_threads = 1, }, }, fast_correlative_scan_matcher_3d = { branch_and_bound_depth = 3, full_resolution_depth = 3, min_rotational_score = 0.1, min_low_resolution_score = 0.5, linear_xy_search_window = 4., linear_z_search_window = 4., angular_search_window = 0.1, }, ceres_scan_matcher_3d = { occupied_space_weight_0 = 20., translation_weight = 10., rotation_weight = 1., only_optimize_yaw = true, ceres_solver_options = { use_nonmonotonic_steps = true, max_num_iterations = 50, num_threads = 1, }, }, }, matcher_translation_weight = 1., matcher_rotation_weight = 1., optimization_problem = { acceleration_weight = 1., rotation_weight = 1e2, huber_scale = 1., local_slam_pose_translation_weight = 0., local_slam_pose_rotation_weight = 0., odometry_translation_weight = 0., odometry_rotation_weight = 0., fixed_frame_pose_translation_weight = 1e1, fixed_frame_pose_rotation_weight = 1e2, fixed_frame_pose_use_tolerant_loss = false, fixed_frame_pose_tolerant_loss_param_a = 1, fixed_frame_pose_tolerant_loss_param_b = 1, log_solver_summary = true, use_online_imu_extrinsics_in_3d = true, fix_z_in_3d = false, ceres_solver_options = { use_nonmonotonic_steps = false, max_num_iterations = 200, num_threads = 1, }, }, max_num_final_iterations = 200, global_sampling_ratio = 0.01, log_residual_histograms = true, global_constraint_search_after_n_seconds = 10.0, })text"); auto options = CreatePoseGraphOptions(parameter_dictionary.get()); pose_graph_ = absl::make_unique( options, absl::make_unique( options.optimization_problem_options()), &thread_pool_); } current_pose_ = transform::Rigid2d::Identity(); } void MoveRelativeWithNoise(const transform::Rigid2d& movement, const transform::Rigid2d& noise) { current_pose_ = current_pose_ * movement; const sensor::PointCloud new_point_cloud = sensor::TransformPointCloud( point_cloud_, transform::Embed3D(current_pose_.inverse().cast())); const sensor::RangeData range_data{ Eigen::Vector3f::Zero(), new_point_cloud, {}}; const transform::Rigid2d pose_estimate = noise * current_pose_; constexpr int kTrajectoryId = 0; active_submaps_->InsertRangeData(TransformRangeData( range_data, transform::Embed3D(pose_estimate.cast()))); std::vector> insertion_submaps; for (const auto& submap : active_submaps_->submaps()) { insertion_submaps.push_back(submap); } pose_graph_->AddNode( std::make_shared( TrajectoryNode::Data{common::FromUniversal(0), Eigen::Quaterniond::Identity(), range_data.returns, {}, {}, {}, transform::Embed3D(pose_estimate)}), kTrajectoryId, insertion_submaps); } void MoveRelative(const transform::Rigid2d& movement) { MoveRelativeWithNoise(movement, transform::Rigid2d::Identity()); } template std::vector ToVectorInt(const Range& range) { return std::vector(range.begin(), range.end()); } sensor::PointCloud point_cloud_; std::unique_ptr active_submaps_; common::ThreadPool thread_pool_; std::unique_ptr pose_graph_; transform::Rigid2d current_pose_; }; TEST_F(PoseGraph2DTest, EmptyMap) { pose_graph_->RunFinalOptimization(); const auto nodes = pose_graph_->GetTrajectoryNodes(); EXPECT_TRUE(nodes.empty()); } TEST_F(PoseGraph2DTest, NoMovement) { MoveRelative(transform::Rigid2d::Identity()); MoveRelative(transform::Rigid2d::Identity()); MoveRelative(transform::Rigid2d::Identity()); pose_graph_->RunFinalOptimization(); const auto nodes = pose_graph_->GetTrajectoryNodes(); ASSERT_THAT(ToVectorInt(nodes.trajectory_ids()), ::testing::ContainerEq(std::vector{0})); EXPECT_THAT(nodes.SizeOfTrajectoryOrZero(0), ::testing::Eq(3u)); EXPECT_THAT(nodes.at(NodeId{0, 0}).global_pose, transform::IsNearly(transform::Rigid3d::Identity(), 1e-2)); EXPECT_THAT(nodes.at(NodeId{0, 1}).global_pose, transform::IsNearly(transform::Rigid3d::Identity(), 1e-2)); EXPECT_THAT(nodes.at(NodeId{0, 2}).global_pose, transform::IsNearly(transform::Rigid3d::Identity(), 1e-2)); } TEST_F(PoseGraph2DTest, NoOverlappingNodes) { std::mt19937 rng(0); std::uniform_real_distribution distribution(-1., 1.); std::vector poses; for (int i = 0; i != 4; ++i) { MoveRelative(transform::Rigid2d({0.25 * distribution(rng), 5.}, 0.)); poses.emplace_back(current_pose_); } pose_graph_->RunFinalOptimization(); const auto nodes = pose_graph_->GetTrajectoryNodes(); ASSERT_THAT(ToVectorInt(nodes.trajectory_ids()), ::testing::ContainerEq(std::vector{0})); for (int i = 0; i != 4; ++i) { EXPECT_THAT( poses[i], IsNearly(transform::Project2D(nodes.at(NodeId{0, i}).global_pose), 1e-2)) << i; } } TEST_F(PoseGraph2DTest, ConsecutivelyOverlappingNodes) { std::mt19937 rng(0); std::uniform_real_distribution distribution(-1., 1.); std::vector poses; for (int i = 0; i != 5; ++i) { MoveRelative(transform::Rigid2d({0.25 * distribution(rng), 2.}, 0.)); poses.emplace_back(current_pose_); } pose_graph_->RunFinalOptimization(); const auto nodes = pose_graph_->GetTrajectoryNodes(); ASSERT_THAT(ToVectorInt(nodes.trajectory_ids()), ::testing::ContainerEq(std::vector{0})); for (int i = 0; i != 5; ++i) { EXPECT_THAT( poses[i], IsNearly(transform::Project2D(nodes.at(NodeId{0, i}).global_pose), 1e-2)) << i; } } TEST_F(PoseGraph2DTest, OverlappingNodes) { std::mt19937 rng(0); std::uniform_real_distribution distribution(-1., 1.); std::vector ground_truth; std::vector poses; for (int i = 0; i != 5; ++i) { const double noise_x = 0.1 * distribution(rng); const double noise_y = 0.1 * distribution(rng); const double noise_orientation = 0.1 * distribution(rng); transform::Rigid2d noise({noise_x, noise_y}, noise_orientation); MoveRelativeWithNoise( transform::Rigid2d({0.15 * distribution(rng), 0.4}, 0.), noise); ground_truth.emplace_back(current_pose_); poses.emplace_back(noise * current_pose_); } pose_graph_->RunFinalOptimization(); const auto nodes = pose_graph_->GetTrajectoryNodes(); ASSERT_THAT(ToVectorInt(nodes.trajectory_ids()), ::testing::ContainerEq(std::vector{0})); transform::Rigid2d true_movement = ground_truth.front().inverse() * ground_truth.back(); transform::Rigid2d movement_before = poses.front().inverse() * poses.back(); transform::Rigid2d error_before = movement_before.inverse() * true_movement; transform::Rigid3d optimized_movement = nodes.BeginOfTrajectory(0)->data.global_pose.inverse() * std::prev(nodes.EndOfTrajectory(0))->data.global_pose; transform::Rigid2d optimized_error = transform::Project2D(optimized_movement).inverse() * true_movement; EXPECT_THAT(std::abs(optimized_error.normalized_angle()), ::testing::Lt(std::abs(error_before.normalized_angle()))); EXPECT_THAT(optimized_error.translation().norm(), ::testing::Lt(error_before.translation().norm())); } } // namespace } // namespace mapping } // namespace cartographer