409 lines
18 KiB
C++
409 lines
18 KiB
C++
/*
|
|
* 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/3d/local_trajectory_builder_3d.h"
|
|
|
|
#include <memory>
|
|
|
|
#include "cartographer/common/make_unique.h"
|
|
#include "cartographer/common/time.h"
|
|
#include "cartographer/mapping/internal/3d/scan_matching/rotational_scan_matcher.h"
|
|
#include "cartographer/mapping/proto/3d/local_trajectory_builder_options_3d.pb.h"
|
|
#include "cartographer/mapping/proto/3d/submaps_options_3d.pb.h"
|
|
#include "cartographer/mapping/proto/scan_matching//ceres_scan_matcher_options_3d.pb.h"
|
|
#include "cartographer/mapping/proto/scan_matching//real_time_correlative_scan_matcher_options.pb.h"
|
|
#include "glog/logging.h"
|
|
|
|
namespace cartographer {
|
|
namespace mapping {
|
|
|
|
static auto* kLocalSlamLatencyMetric = metrics::Gauge::Null();
|
|
static auto* kLocalSlamVoxelFilterFraction = metrics::Gauge::Null();
|
|
static auto* kLocalSlamScanMatcherFraction = metrics::Gauge::Null();
|
|
static auto* kLocalSlamInsertIntoSubmapFraction = metrics::Gauge::Null();
|
|
static auto* kRealTimeCorrelativeScanMatcherScoreMetric =
|
|
metrics::Histogram::Null();
|
|
static auto* kCeresScanMatcherCostMetric = metrics::Histogram::Null();
|
|
static auto* kScanMatcherResidualDistanceMetric = metrics::Histogram::Null();
|
|
static auto* kScanMatcherResidualAngleMetric = metrics::Histogram::Null();
|
|
|
|
LocalTrajectoryBuilder3D::LocalTrajectoryBuilder3D(
|
|
const mapping::proto::LocalTrajectoryBuilderOptions3D& options,
|
|
const std::vector<std::string>& expected_range_sensor_ids)
|
|
: options_(options),
|
|
active_submaps_(options.submaps_options()),
|
|
motion_filter_(options.motion_filter_options()),
|
|
real_time_correlative_scan_matcher_(
|
|
common::make_unique<scan_matching::RealTimeCorrelativeScanMatcher3D>(
|
|
options_.real_time_correlative_scan_matcher_options())),
|
|
ceres_scan_matcher_(
|
|
common::make_unique<scan_matching::CeresScanMatcher3D>(
|
|
options_.ceres_scan_matcher_options())),
|
|
accumulated_range_data_{Eigen::Vector3f::Zero(), {}, {}},
|
|
range_data_collator_(expected_range_sensor_ids) {}
|
|
|
|
LocalTrajectoryBuilder3D::~LocalTrajectoryBuilder3D() {}
|
|
|
|
std::unique_ptr<transform::Rigid3d> LocalTrajectoryBuilder3D::ScanMatch(
|
|
const transform::Rigid3d& pose_prediction,
|
|
const sensor::PointCloud& low_resolution_point_cloud_in_tracking,
|
|
const sensor::PointCloud& high_resolution_point_cloud_in_tracking) {
|
|
std::shared_ptr<const mapping::Submap3D> matching_submap =
|
|
active_submaps_.submaps().front();
|
|
transform::Rigid3d initial_ceres_pose =
|
|
matching_submap->local_pose().inverse() * pose_prediction;
|
|
if (options_.use_online_correlative_scan_matching()) {
|
|
// We take a copy since we use 'initial_ceres_pose' as an output argument.
|
|
const transform::Rigid3d initial_pose = initial_ceres_pose;
|
|
const double score = real_time_correlative_scan_matcher_->Match(
|
|
initial_pose, high_resolution_point_cloud_in_tracking,
|
|
matching_submap->high_resolution_hybrid_grid(), &initial_ceres_pose);
|
|
kRealTimeCorrelativeScanMatcherScoreMetric->Observe(score);
|
|
}
|
|
|
|
transform::Rigid3d pose_observation_in_submap;
|
|
ceres::Solver::Summary summary;
|
|
ceres_scan_matcher_->Match(
|
|
(matching_submap->local_pose().inverse() * pose_prediction).translation(),
|
|
initial_ceres_pose,
|
|
{{&high_resolution_point_cloud_in_tracking,
|
|
&matching_submap->high_resolution_hybrid_grid()},
|
|
{&low_resolution_point_cloud_in_tracking,
|
|
&matching_submap->low_resolution_hybrid_grid()}},
|
|
&pose_observation_in_submap, &summary);
|
|
kCeresScanMatcherCostMetric->Observe(summary.final_cost);
|
|
const double residual_distance = (pose_observation_in_submap.translation() -
|
|
initial_ceres_pose.translation())
|
|
.norm();
|
|
kScanMatcherResidualDistanceMetric->Observe(residual_distance);
|
|
const double residual_angle =
|
|
pose_observation_in_submap.rotation().angularDistance(
|
|
initial_ceres_pose.rotation());
|
|
kScanMatcherResidualAngleMetric->Observe(residual_angle);
|
|
return common::make_unique<transform::Rigid3d>(matching_submap->local_pose() *
|
|
pose_observation_in_submap);
|
|
}
|
|
|
|
void LocalTrajectoryBuilder3D::AddImuData(const sensor::ImuData& imu_data) {
|
|
if (extrapolator_ != nullptr) {
|
|
extrapolator_->AddImuData(imu_data);
|
|
return;
|
|
}
|
|
// We derive velocities from poses which are at least 1 ms apart for numerical
|
|
// stability. Usually poses known to the extrapolator will be further apart
|
|
// in time and thus the last two are used.
|
|
constexpr double kExtrapolationEstimationTimeSec = 0.001;
|
|
extrapolator_ = mapping::PoseExtrapolator::InitializeWithImu(
|
|
::cartographer::common::FromSeconds(kExtrapolationEstimationTimeSec),
|
|
options_.imu_gravity_time_constant(), imu_data);
|
|
}
|
|
|
|
std::unique_ptr<LocalTrajectoryBuilder3D::MatchingResult>
|
|
LocalTrajectoryBuilder3D::AddRangeData(
|
|
const std::string& sensor_id,
|
|
const sensor::TimedPointCloudData& unsynchronized_data) {
|
|
auto synchronized_data =
|
|
range_data_collator_.AddRangeData(sensor_id, unsynchronized_data);
|
|
if (synchronized_data.ranges.empty()) {
|
|
LOG(INFO) << "Range data collator filling buffer.";
|
|
return nullptr;
|
|
}
|
|
|
|
const common::Time& time = synchronized_data.time;
|
|
if (extrapolator_ == nullptr) {
|
|
// Until we've initialized the extrapolator with our first IMU message, we
|
|
// cannot compute the orientation of the rangefinder.
|
|
LOG(INFO) << "IMU not yet initialized.";
|
|
return nullptr;
|
|
}
|
|
|
|
CHECK(!synchronized_data.ranges.empty());
|
|
CHECK_LE(synchronized_data.ranges.back().point_time[3], 0.f);
|
|
const common::Time time_first_point =
|
|
time +
|
|
common::FromSeconds(synchronized_data.ranges.front().point_time[3]);
|
|
if (time_first_point < extrapolator_->GetLastPoseTime()) {
|
|
LOG(INFO) << "Extrapolator is still initializing.";
|
|
return nullptr;
|
|
}
|
|
|
|
if (num_accumulated_ == 0) {
|
|
accumulation_started_ = std::chrono::steady_clock::now();
|
|
}
|
|
|
|
std::vector<sensor::TimedPointCloudOriginData::RangeMeasurement> hits =
|
|
sensor::VoxelFilter(0.5f * options_.voxel_filter_size())
|
|
.Filter(synchronized_data.ranges);
|
|
|
|
std::vector<transform::Rigid3f> hits_poses;
|
|
hits_poses.reserve(hits.size());
|
|
bool warned = false;
|
|
|
|
for (const auto& hit : hits) {
|
|
common::Time time_point = time + common::FromSeconds(hit.point_time[3]);
|
|
if (time_point < extrapolator_->GetLastExtrapolatedTime()) {
|
|
if (!warned) {
|
|
LOG(ERROR)
|
|
<< "Timestamp of individual range data point jumps backwards from "
|
|
<< extrapolator_->GetLastExtrapolatedTime() << " to " << time_point;
|
|
warned = true;
|
|
}
|
|
time_point = extrapolator_->GetLastExtrapolatedTime();
|
|
}
|
|
hits_poses.push_back(
|
|
extrapolator_->ExtrapolatePose(time_point).cast<float>());
|
|
}
|
|
|
|
if (num_accumulated_ == 0) {
|
|
// 'accumulated_range_data_.origin' is not used.
|
|
accumulated_range_data_ = sensor::RangeData{{}, {}, {}};
|
|
}
|
|
|
|
for (size_t i = 0; i < hits.size(); ++i) {
|
|
const Eigen::Vector3f hit_in_local =
|
|
hits_poses[i] * hits[i].point_time.head<3>();
|
|
const Eigen::Vector3f origin_in_local =
|
|
hits_poses[i] * synchronized_data.origins.at(hits[i].origin_index);
|
|
const Eigen::Vector3f delta = hit_in_local - origin_in_local;
|
|
const float range = delta.norm();
|
|
if (range >= options_.min_range()) {
|
|
if (range <= options_.max_range()) {
|
|
accumulated_range_data_.returns.push_back(hit_in_local);
|
|
} else {
|
|
// We insert a ray cropped to 'max_range' as a miss for hits beyond the
|
|
// maximum range. This way the free space up to the maximum range will
|
|
// be updated.
|
|
accumulated_range_data_.misses.push_back(
|
|
origin_in_local + options_.max_range() / range * delta);
|
|
}
|
|
}
|
|
}
|
|
++num_accumulated_;
|
|
|
|
if (num_accumulated_ >= options_.num_accumulated_range_data()) {
|
|
const common::Time current_sensor_time = synchronized_data.time;
|
|
common::optional<common::Duration> sensor_duration;
|
|
if (last_sensor_time_.has_value()) {
|
|
sensor_duration = current_sensor_time - last_sensor_time_.value();
|
|
}
|
|
last_sensor_time_ = current_sensor_time;
|
|
num_accumulated_ = 0;
|
|
|
|
transform::Rigid3f current_pose =
|
|
extrapolator_->ExtrapolatePose(time).cast<float>();
|
|
|
|
const auto voxel_filter_start = std::chrono::steady_clock::now();
|
|
const sensor::RangeData filtered_range_data = {
|
|
current_pose.translation(),
|
|
sensor::VoxelFilter(options_.voxel_filter_size())
|
|
.Filter(accumulated_range_data_.returns),
|
|
sensor::VoxelFilter(options_.voxel_filter_size())
|
|
.Filter(accumulated_range_data_.misses)};
|
|
const auto voxel_filter_stop = std::chrono::steady_clock::now();
|
|
const auto voxel_filter_duration = voxel_filter_stop - voxel_filter_start;
|
|
|
|
if (sensor_duration.has_value()) {
|
|
const double voxel_filter_fraction =
|
|
common::ToSeconds(voxel_filter_duration) /
|
|
common::ToSeconds(sensor_duration.value());
|
|
kLocalSlamVoxelFilterFraction->Set(voxel_filter_fraction);
|
|
}
|
|
|
|
return AddAccumulatedRangeData(
|
|
time,
|
|
sensor::TransformRangeData(filtered_range_data, current_pose.inverse()),
|
|
sensor_duration);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::unique_ptr<LocalTrajectoryBuilder3D::MatchingResult>
|
|
LocalTrajectoryBuilder3D::AddAccumulatedRangeData(
|
|
const common::Time time,
|
|
const sensor::RangeData& filtered_range_data_in_tracking,
|
|
const common::optional<common::Duration>& sensor_duration) {
|
|
if (filtered_range_data_in_tracking.returns.empty()) {
|
|
LOG(WARNING) << "Dropped empty range data.";
|
|
return nullptr;
|
|
}
|
|
const transform::Rigid3d pose_prediction =
|
|
extrapolator_->ExtrapolatePose(time);
|
|
|
|
const auto scan_matcher_start = std::chrono::steady_clock::now();
|
|
|
|
sensor::AdaptiveVoxelFilter adaptive_voxel_filter(
|
|
options_.high_resolution_adaptive_voxel_filter_options());
|
|
const sensor::PointCloud high_resolution_point_cloud_in_tracking =
|
|
adaptive_voxel_filter.Filter(filtered_range_data_in_tracking.returns);
|
|
if (high_resolution_point_cloud_in_tracking.empty()) {
|
|
LOG(WARNING) << "Dropped empty high resolution point cloud data.";
|
|
return nullptr;
|
|
}
|
|
|
|
sensor::AdaptiveVoxelFilter low_resolution_adaptive_voxel_filter(
|
|
options_.low_resolution_adaptive_voxel_filter_options());
|
|
const sensor::PointCloud low_resolution_point_cloud_in_tracking =
|
|
low_resolution_adaptive_voxel_filter.Filter(
|
|
filtered_range_data_in_tracking.returns);
|
|
if (low_resolution_point_cloud_in_tracking.empty()) {
|
|
LOG(WARNING) << "Dropped empty low resolution point cloud data.";
|
|
return nullptr;
|
|
}
|
|
|
|
std::unique_ptr<transform::Rigid3d> pose_estimate =
|
|
ScanMatch(pose_prediction, low_resolution_point_cloud_in_tracking,
|
|
high_resolution_point_cloud_in_tracking);
|
|
if (pose_estimate == nullptr) {
|
|
LOG(WARNING) << "Scan matching failed.";
|
|
return nullptr;
|
|
}
|
|
extrapolator_->AddPose(time, *pose_estimate);
|
|
|
|
const auto scan_matcher_stop = std::chrono::steady_clock::now();
|
|
const auto scan_matcher_duration = scan_matcher_stop - scan_matcher_start;
|
|
if (sensor_duration.has_value()) {
|
|
const double scan_matcher_fraction =
|
|
common::ToSeconds(scan_matcher_duration) /
|
|
common::ToSeconds(sensor_duration.value());
|
|
kLocalSlamScanMatcherFraction->Set(scan_matcher_fraction);
|
|
}
|
|
|
|
const Eigen::Quaterniond gravity_alignment =
|
|
extrapolator_->EstimateGravityOrientation(time);
|
|
sensor::RangeData filtered_range_data_in_local = sensor::TransformRangeData(
|
|
filtered_range_data_in_tracking, pose_estimate->cast<float>());
|
|
|
|
const auto insert_into_submap_start = std::chrono::steady_clock::now();
|
|
std::unique_ptr<InsertionResult> insertion_result = InsertIntoSubmap(
|
|
time, filtered_range_data_in_local, filtered_range_data_in_tracking,
|
|
high_resolution_point_cloud_in_tracking,
|
|
low_resolution_point_cloud_in_tracking, *pose_estimate,
|
|
gravity_alignment);
|
|
const auto insert_into_submap_stop = std::chrono::steady_clock::now();
|
|
|
|
const auto insert_into_submap_duration =
|
|
insert_into_submap_stop - insert_into_submap_start;
|
|
if (sensor_duration.has_value()) {
|
|
const double insert_into_submap_fraction =
|
|
common::ToSeconds(insert_into_submap_duration) /
|
|
common::ToSeconds(sensor_duration.value());
|
|
kLocalSlamInsertIntoSubmapFraction->Set(insert_into_submap_fraction);
|
|
}
|
|
const auto accumulation_duration =
|
|
std::chrono::steady_clock::now() - accumulation_started_;
|
|
kLocalSlamLatencyMetric->Set(common::ToSeconds(accumulation_duration));
|
|
return common::make_unique<MatchingResult>(MatchingResult{
|
|
time, *pose_estimate, std::move(filtered_range_data_in_local),
|
|
std::move(insertion_result)});
|
|
}
|
|
|
|
void LocalTrajectoryBuilder3D::AddOdometryData(
|
|
const sensor::OdometryData& odometry_data) {
|
|
if (extrapolator_ == nullptr) {
|
|
// Until we've initialized the extrapolator we cannot add odometry data.
|
|
LOG(INFO) << "Extrapolator not yet initialized.";
|
|
return;
|
|
}
|
|
extrapolator_->AddOdometryData(odometry_data);
|
|
}
|
|
|
|
std::unique_ptr<LocalTrajectoryBuilder3D::InsertionResult>
|
|
LocalTrajectoryBuilder3D::InsertIntoSubmap(
|
|
const common::Time time,
|
|
const sensor::RangeData& filtered_range_data_in_local,
|
|
const sensor::RangeData& filtered_range_data_in_tracking,
|
|
const sensor::PointCloud& high_resolution_point_cloud_in_tracking,
|
|
const sensor::PointCloud& low_resolution_point_cloud_in_tracking,
|
|
const transform::Rigid3d& pose_estimate,
|
|
const Eigen::Quaterniond& gravity_alignment) {
|
|
if (motion_filter_.IsSimilar(time, pose_estimate)) {
|
|
return nullptr;
|
|
}
|
|
// Querying the active submaps must be done here before calling
|
|
// InsertRangeData() since the queried values are valid for next insertion.
|
|
std::vector<std::shared_ptr<const mapping::Submap3D>> insertion_submaps;
|
|
for (const std::shared_ptr<mapping::Submap3D>& submap :
|
|
active_submaps_.submaps()) {
|
|
insertion_submaps.push_back(submap);
|
|
}
|
|
active_submaps_.InsertRangeData(filtered_range_data_in_local,
|
|
gravity_alignment);
|
|
const Eigen::VectorXf rotational_scan_matcher_histogram =
|
|
scan_matching::RotationalScanMatcher::ComputeHistogram(
|
|
sensor::TransformPointCloud(
|
|
filtered_range_data_in_tracking.returns,
|
|
transform::Rigid3f::Rotation(gravity_alignment.cast<float>())),
|
|
options_.rotational_histogram_size());
|
|
return common::make_unique<InsertionResult>(
|
|
InsertionResult{std::make_shared<const mapping::TrajectoryNode::Data>(
|
|
mapping::TrajectoryNode::Data{
|
|
time,
|
|
gravity_alignment,
|
|
{}, // 'filtered_point_cloud' is only used in 2D.
|
|
high_resolution_point_cloud_in_tracking,
|
|
low_resolution_point_cloud_in_tracking,
|
|
rotational_scan_matcher_histogram,
|
|
pose_estimate}),
|
|
std::move(insertion_submaps)});
|
|
}
|
|
|
|
void LocalTrajectoryBuilder3D::RegisterMetrics(
|
|
metrics::FamilyFactory* family_factory) {
|
|
auto* latency = family_factory->NewGaugeFamily(
|
|
"mapping_3d_local_trajectory_builder_latency",
|
|
"Duration from first incoming point cloud in accumulation to local slam "
|
|
"result");
|
|
|
|
auto* voxel_filter_fraction = family_factory->NewGaugeFamily(
|
|
"mapping_3d_local_trajectory_builder_voxel_filter_fraction",
|
|
"Fraction of total sensor time taken up by voxel filter.");
|
|
kLocalSlamVoxelFilterFraction = voxel_filter_fraction->Add({});
|
|
|
|
auto* scan_matcher_fraction = family_factory->NewGaugeFamily(
|
|
"mapping_3d_local_trajectory_builder_scan_matcher_fraction",
|
|
"Fraction of total sensor time taken up by scan matcher.");
|
|
kLocalSlamScanMatcherFraction = scan_matcher_fraction->Add({});
|
|
|
|
auto* insert_into_submap_fraction = family_factory->NewGaugeFamily(
|
|
"mapping_3d_local_trajectory_builder"
|
|
"insert_into_submap_fraction",
|
|
"Fraction of total sensor time taken up by inserting into submap.");
|
|
kLocalSlamInsertIntoSubmapFraction = insert_into_submap_fraction->Add({});
|
|
|
|
kLocalSlamLatencyMetric = latency->Add({});
|
|
auto score_boundaries = metrics::Histogram::FixedWidth(0.05, 20);
|
|
auto* scores = family_factory->NewHistogramFamily(
|
|
"mapping_3d_local_trajectory_builder_scores", "Local scan matcher scores",
|
|
score_boundaries);
|
|
kRealTimeCorrelativeScanMatcherScoreMetric =
|
|
scores->Add({{"scan_matcher", "real_time_correlative"}});
|
|
auto cost_boundaries = metrics::Histogram::ScaledPowersOf(2, 0.01, 100);
|
|
auto* costs = family_factory->NewHistogramFamily(
|
|
"mapping_3d_local_trajectory_builder_costs", "Local scan matcher costs",
|
|
cost_boundaries);
|
|
kCeresScanMatcherCostMetric = costs->Add({{"scan_matcher", "ceres"}});
|
|
auto distance_boundaries = metrics::Histogram::ScaledPowersOf(2, 0.01, 10);
|
|
auto* residuals = family_factory->NewHistogramFamily(
|
|
"mapping_3d_local_trajectory_builder_residuals",
|
|
"Local scan matcher residuals", distance_boundaries);
|
|
kScanMatcherResidualDistanceMetric =
|
|
residuals->Add({{"component", "distance"}});
|
|
kScanMatcherResidualAngleMetric = residuals->Add({{"component", "angle"}});
|
|
}
|
|
|
|
} // namespace mapping
|
|
} // namespace cartographer
|