/* * Copyright 2018 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/imu_based_pose_extrapolator.h" #include #include "absl/memory/memory.h" #include "cartographer/mapping/internal/3d/imu_integration.h" #include "cartographer/mapping/internal/3d/rotation_parameterization.h" #include "cartographer/mapping/internal/eigen_quaterniond_from_two_vectors.h" #include "cartographer/mapping/internal/optimization/ceres_pose.h" #include "cartographer/mapping/internal/optimization/cost_functions/acceleration_cost_function_3d.h" #include "cartographer/mapping/internal/optimization/cost_functions/rotation_cost_function_3d.h" #include "cartographer/mapping/internal/optimization/cost_functions/spa_cost_function_3d.h" #include "cartographer/mapping/pose_graph_interface.h" #include "cartographer/transform/transform.h" #include "glog/logging.h" namespace cartographer { namespace mapping { using ::cartographer::transform::TimestampedTransform; ImuBasedPoseExtrapolator::ImuBasedPoseExtrapolator( const proto::ImuBasedPoseExtrapolatorOptions& options) : options_(options), solver_options_( common::CreateCeresSolverOptions(options_.solver_options())) {} ImuBasedPoseExtrapolator::~ImuBasedPoseExtrapolator() { LOG(INFO) << "Number of iterations for pose extrapolation:"; LOG(INFO) << num_iterations_hist_.ToString(10); } std::unique_ptr ImuBasedPoseExtrapolator::InitializeWithImu( const proto::ImuBasedPoseExtrapolatorOptions& options, const std::vector& imu_data, const std::vector& initial_poses) { CHECK(!imu_data.empty()); LOG(INFO) << options.DebugString(); auto extrapolator = absl::make_unique(options); std::copy(imu_data.begin(), imu_data.end(), std::back_inserter(extrapolator->imu_data_)); if (!initial_poses.empty()) { for (const auto& pose : initial_poses) { if (pose.time > imu_data.front().time) { extrapolator->AddPose(pose.time, pose.transform); } } } else { extrapolator->AddPose( imu_data.back().time, transform::Rigid3d::Rotation(FromTwoVectors( imu_data.back().linear_acceleration, Eigen::Vector3d::UnitZ()))); } return extrapolator; } common::Time ImuBasedPoseExtrapolator::GetLastPoseTime() const { if (timed_pose_queue_.empty()) { return common::Time::min(); } return timed_pose_queue_.back().time; } common::Time ImuBasedPoseExtrapolator::GetLastExtrapolatedTime() const { return last_extrapolated_time_; } void ImuBasedPoseExtrapolator::AddPose(const common::Time time, const transform::Rigid3d& pose) { timed_pose_queue_.push_back(TimestampedTransform{time, pose}); while (timed_pose_queue_.size() > 3 && timed_pose_queue_[1].time <= time - common::FromSeconds(options_.pose_queue_duration())) { if (!previous_solution_.empty()) { CHECK_EQ(timed_pose_queue_.front().time, previous_solution_.front().time); previous_solution_.pop_front(); } timed_pose_queue_.pop_front(); } TrimImuData(); } void ImuBasedPoseExtrapolator::AddImuData(const sensor::ImuData& imu_data) { CHECK(timed_pose_queue_.empty() || imu_data.time >= timed_pose_queue_.back().time); imu_data_.push_back(imu_data); TrimImuData(); } void ImuBasedPoseExtrapolator::AddOdometryData( const sensor::OdometryData& odometry_data) { CHECK(timed_pose_queue_.empty() || odometry_data.time >= timed_pose_queue_.back().time); odometry_data_.push_back(odometry_data); TrimOdometryData(); } ImuBasedPoseExtrapolator::ExtrapolationResult ImuBasedPoseExtrapolator::ExtrapolatePosesWithGravity( const std::vector& times) { const auto& time = times.back(); const auto& newest_timed_pose = timed_pose_queue_.back(); CHECK_GE(time, newest_timed_pose.time); CHECK_GE(times.size(), 1); last_extrapolated_time_ = time; if (timed_pose_queue_.size() < 3 || common::ToSeconds(time - newest_timed_pose.time) < 1e-6) { return ExtrapolationResult{ std::vector( times.size() - 1, timed_pose_queue_.back().transform.cast()), timed_pose_queue_.back().transform, Eigen::Vector3d::Zero(), timed_pose_queue_.back().transform.rotation()}; } ceres::Problem::Options problem_options; ceres::Problem problem(problem_options); // Track gravity alignment over time and use this as a frame here so that // we can estimate the gravity alignment of the current pose. optimization::CeresPose gravity_from_local( gravity_from_local_, nullptr, absl::make_unique(), &problem); // Use deque so addresses stay constant during problem formulation. std::deque nodes; std::vector node_times; for (size_t i = 0; i < timed_pose_queue_.size(); i++) { const bool is_last = (i == (timed_pose_queue_.size() - 1)); const auto& timed_pose = timed_pose_queue_[i]; node_times.push_back(timed_pose.time); transform::Rigid3d gravity_from_node; // Use the last scan match result (timed_pose_queue_.back()) for // initialization here instead of the last result from the optimization. // This keeps poses from slowly drifting apart due to lack of feedback // from the scan matching here. if (!previous_solution_.empty() && !is_last) { CHECK_GT(previous_solution_.size(), i); CHECK_EQ(timed_pose.time, previous_solution_[i].time); gravity_from_node = previous_solution_[i].transform; } else { gravity_from_node = gravity_from_local_ * timed_pose.transform; } if (is_last) { nodes.emplace_back(gravity_from_node, nullptr, absl::make_unique>(), &problem); problem.SetParameterBlockConstant(nodes.back().translation()); } else { nodes.emplace_back(gravity_from_node, nullptr, absl::make_unique(), &problem); } } double gravity_constant = 9.8; bool fix_gravity = false; if (options_.gravity_constant() > 0) { fix_gravity = true; gravity_constant = options_.gravity_constant(); } auto imu_it_prev_prev = imu_data_.begin(); while (imu_it_prev_prev != std::prev(imu_data_.end()) && std::next(imu_it_prev_prev)->time <= timed_pose_queue_.back().time) { ++imu_it_prev_prev; } const TimestampedTransform prev_gravity_from_tracking = TimestampedTransform{node_times.back(), nodes.back().ToRigid()}; const TimestampedTransform prev_prev_gravity_from_tracking = TimestampedTransform{node_times.at(node_times.size() - 2), nodes.at(nodes.size() - 2).ToRigid()}; const transform::Rigid3d initial_estimate = ExtrapolatePoseWithImu( prev_gravity_from_tracking.transform, prev_gravity_from_tracking.time, prev_prev_gravity_from_tracking.transform, prev_prev_gravity_from_tracking.time, gravity_constant * Eigen::Vector3d::UnitZ(), time, imu_data_, &imu_it_prev_prev) .pose; nodes.emplace_back(initial_estimate, nullptr, absl::make_unique(), &problem); node_times.push_back(time); // Add cost functions for node constraints. for (size_t i = 0; i < timed_pose_queue_.size(); i++) { const auto& timed_pose = timed_pose_queue_[i]; problem.AddResidualBlock( optimization::SpaCostFunction3D::CreateAutoDiffCostFunction( PoseGraphInterface::Constraint::Pose{ timed_pose.transform, options_.pose_translation_weight(), options_.pose_rotation_weight()}), nullptr /* loss function */, gravity_from_local.rotation(), gravity_from_local.translation(), nodes.at(i).rotation(), nodes.at(i).translation()); } CHECK(!imu_data_.empty()); CHECK_LE(imu_data_.front().time, timed_pose_queue_.front().time); std::array imu_calibration{{1., 0., 0., 0.}}; problem.AddParameterBlock(imu_calibration.data(), 4, new ceres::QuaternionParameterization()); problem.SetParameterBlockConstant(imu_calibration.data()); auto imu_it = imu_data_.begin(); CHECK(imu_data_.size() == 1 || std::next(imu_it)->time > timed_pose_queue_.front().time); transform::Rigid3d last_node_odometry; common::Time last_node_odometry_time; for (size_t i = 1; i < nodes.size(); i++) { const common::Time first_time = node_times[i - 1]; const common::Time second_time = node_times[i]; auto imu_it2 = imu_it; const IntegrateImuResult result = IntegrateImu(imu_data_, first_time, second_time, &imu_it); if ((i + 1) < nodes.size()) { const common::Time third_time = node_times[i + 1]; const common::Duration first_duration = second_time - first_time; const common::Duration second_duration = third_time - second_time; const common::Time first_center = first_time + first_duration / 2; const common::Time second_center = second_time + second_duration / 2; const IntegrateImuResult result_to_first_center = IntegrateImu(imu_data_, first_time, first_center, &imu_it2); const IntegrateImuResult result_center_to_center = IntegrateImu(imu_data_, first_center, second_center, &imu_it2); // 'delta_velocity' is the change in velocity from the point in time // halfway between the first and second poses to halfway between // second and third pose. It is computed from IMU data and still // contains a delta due to gravity. The orientation of this vector is // in the IMU frame at the second pose. const Eigen::Vector3d delta_velocity = (result.delta_rotation.inverse() * result_to_first_center.delta_rotation) * result_center_to_center.delta_velocity; problem.AddResidualBlock( AccelerationCostFunction3D::CreateAutoDiffCostFunction( options_.imu_acceleration_weight(), delta_velocity, common::ToSeconds(first_duration), common::ToSeconds(second_duration)), nullptr /* loss function */, nodes.at(i).rotation(), nodes.at(i - 1).translation(), nodes.at(i).translation(), nodes.at(i + 1).translation(), &gravity_constant, imu_calibration.data()); // TODO(danielsievers): Fix gravity in CostFunction. if (fix_gravity) { problem.SetParameterBlockConstant(&gravity_constant); } else { // Force gravity constant to be positive. problem.SetParameterLowerBound(&gravity_constant, 0, 0.0); } } problem.AddResidualBlock( RotationCostFunction3D::CreateAutoDiffCostFunction( options_.imu_rotation_weight(), result.delta_rotation), nullptr /* loss function */, nodes.at(i - 1).rotation(), nodes.at(i).rotation(), imu_calibration.data()); // Add a relative pose constraint based on the odometry (if available). if (HasOdometryDataForTime(first_time) && HasOdometryDataForTime(second_time)) { // Here keep track of last node odometry to avoid double computation. // Do this if first loop, or if there were some missing odometry nodes // then recalculate. if (i == 1 || last_node_odometry_time != first_time) { last_node_odometry = InterpolateOdometry(first_time); last_node_odometry_time = first_time; } const transform::Rigid3d current_node_odometry = InterpolateOdometry(second_time); const transform::Rigid3d relative_odometry = CalculateOdometryBetweenNodes(last_node_odometry, current_node_odometry); problem.AddResidualBlock( optimization::SpaCostFunction3D::CreateAutoDiffCostFunction( PoseGraphInterface::Constraint::Pose{ relative_odometry, options_.odometry_translation_weight(), options_.odometry_rotation_weight()}), nullptr /* loss function */, nodes.at(i - 1).rotation(), nodes.at(i - 1).translation(), nodes.at(i).rotation(), nodes.at(i).translation()); // Use the current node odometry in the next iteration last_node_odometry = current_node_odometry; last_node_odometry_time = second_time; } } // Solve. ceres::Solver::Summary summary; ceres::Solve(solver_options_, &problem, &summary); LOG_IF_EVERY_N(INFO, !fix_gravity, 20) << "Gravity was: " << gravity_constant; const auto gravity_estimate = nodes.back().ToRigid().rotation(); const auto& last_pose = timed_pose_queue_.back(); const auto extrapolated_pose = TimestampedTransform{ time, last_pose.transform * nodes.at(nodes.size() - 2).ToRigid().inverse() * nodes.back().ToRigid()}; num_iterations_hist_.Add(summary.iterations.size()); gravity_from_local_ = gravity_from_local.ToRigid(); previous_solution_.clear(); for (size_t i = 0; i < nodes.size(); ++i) { previous_solution_.push_back( TimestampedTransform{node_times.at(i), nodes.at(i).ToRigid()}); } const Eigen::Vector3d current_velocity = (extrapolated_pose.transform.translation() - last_pose.transform.translation()) / common::ToSeconds(time - last_pose.time); return ExtrapolationResult{ InterpolatePoses(last_pose, extrapolated_pose, times.begin(), std::prev(times.end())), extrapolated_pose.transform, current_velocity, gravity_estimate}; } std::vector ImuBasedPoseExtrapolator::InterpolatePoses( const TimestampedTransform& start, const TimestampedTransform& end, const std::vector::const_iterator times_begin, const std::vector::const_iterator times_end) { std::vector poses; poses.reserve(std::distance(times_begin, times_end)); const float duration_scale = 1. / common::ToSeconds(end.time - start.time); const Eigen::Quaternionf start_rotation = Eigen::Quaternionf(start.transform.rotation()); const Eigen::Quaternionf end_rotation = Eigen::Quaternionf(end.transform.rotation()); const Eigen::Vector3f start_translation = start.transform.translation().cast(); const Eigen::Vector3f end_translation = end.transform.translation().cast(); for (auto it = times_begin; it != times_end; ++it) { const float factor = common::ToSeconds(*it - start.time) * duration_scale; const Eigen::Vector3f origin = start_translation + (end_translation - start_translation) * factor; const Eigen::Quaternionf rotation = start_rotation.slerp(factor, end_rotation); poses.emplace_back(origin, rotation); } return poses; } transform::Rigid3d ImuBasedPoseExtrapolator::ExtrapolatePose( const common::Time time) { return ExtrapolatePosesWithGravity(std::vector{time}) .current_pose; } Eigen::Quaterniond ImuBasedPoseExtrapolator::EstimateGravityOrientation( const common::Time time) { return ExtrapolatePosesWithGravity(std::vector{time}) .gravity_from_tracking; } template void ImuBasedPoseExtrapolator::TrimDequeData(std::deque* data) { while (data->size() > 1 && !timed_pose_queue_.empty() && data->at(1).time <= timed_pose_queue_.front().time) { data->pop_front(); } } void ImuBasedPoseExtrapolator::TrimImuData() { TrimDequeData(&imu_data_); } void ImuBasedPoseExtrapolator::TrimOdometryData() { TrimDequeData(&odometry_data_); } // Odometry methods bool ImuBasedPoseExtrapolator::HasOdometryData() const { return odometry_data_.size() >= 2; } bool ImuBasedPoseExtrapolator::HasOdometryDataForTime( const common::Time& time) const { return HasOdometryData() && odometry_data_.front().time < time && time < odometry_data_.back().time; } transform::Rigid3d ImuBasedPoseExtrapolator::InterpolateOdometry( const common::Time& time) const { // Only interpolate if time is within odometry data range. CHECK(HasOdometryDataForTime(time)) << "Odometry data range does not include time " << time; std::deque::const_iterator data = std::upper_bound( odometry_data_.begin(), odometry_data_.end(), time, [](const common::Time& time, const sensor::OdometryData& odometry_data) { return time < odometry_data.time; }); const TimestampedTransform first{std::prev(data)->time, std::prev(data)->pose}; const TimestampedTransform second{data->time, data->pose}; return Interpolate(first, second, time).transform; } transform::Rigid3d ImuBasedPoseExtrapolator::CalculateOdometryBetweenNodes( const transform::Rigid3d& first_node_odometry, const transform::Rigid3d& second_node_odometry) const { return first_node_odometry.inverse() * second_node_odometry; } } // namespace mapping } // namespace cartographer