Random voxel filtering. (#1753)

This leads to slightly slower performance but better quality.
Also replaces the VoxelFilter class by free standing functions.

Signed-off-by: Wolfgang Hess <whess@lyft.com>
master
Wolfgang Hess 2020-09-30 18:32:39 +02:00 committed by GitHub
parent 1ad6398bce
commit c84da8ec0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 137 deletions

View File

@ -58,8 +58,8 @@ LocalTrajectoryBuilder2D::TransformToGravityAlignedFrameAndFilter(
options_.min_z(), options_.max_z()); options_.min_z(), options_.max_z());
return sensor::RangeData{ return sensor::RangeData{
cropped.origin, cropped.origin,
sensor::VoxelFilter(options_.voxel_filter_size()).Filter(cropped.returns), sensor::VoxelFilter(cropped.returns, options_.voxel_filter_size()),
sensor::VoxelFilter(options_.voxel_filter_size()).Filter(cropped.misses)}; sensor::VoxelFilter(cropped.misses, options_.voxel_filter_size())};
} }
std::unique_ptr<transform::Rigid2d> LocalTrajectoryBuilder2D::ScanMatch( std::unique_ptr<transform::Rigid2d> LocalTrajectoryBuilder2D::ScanMatch(
@ -226,8 +226,8 @@ LocalTrajectoryBuilder2D::AddAccumulatedRangeData(
non_gravity_aligned_pose_prediction * gravity_alignment.inverse()); non_gravity_aligned_pose_prediction * gravity_alignment.inverse());
const sensor::PointCloud& filtered_gravity_aligned_point_cloud = const sensor::PointCloud& filtered_gravity_aligned_point_cloud =
sensor::AdaptiveVoxelFilter(options_.adaptive_voxel_filter_options()) sensor::AdaptiveVoxelFilter(gravity_aligned_range_data.returns,
.Filter(gravity_aligned_range_data.returns); options_.adaptive_voxel_filter_options());
if (filtered_gravity_aligned_point_cloud.empty()) { if (filtered_gravity_aligned_point_cloud.empty()) {
return nullptr; return nullptr;
} }

View File

@ -154,9 +154,8 @@ LocalTrajectoryBuilder3D::AddRangeData(
accumulated_point_cloud_origin_data_.clear(); accumulated_point_cloud_origin_data_.clear();
} }
synchronized_data.ranges = synchronized_data.ranges = sensor::VoxelFilter(
sensor::VoxelFilter(0.5f * options_.voxel_filter_size()) synchronized_data.ranges, 0.5f * options_.voxel_filter_size());
.Filter(synchronized_data.ranges);
accumulated_point_cloud_origin_data_.emplace_back( accumulated_point_cloud_origin_data_.emplace_back(
std::move(synchronized_data)); std::move(synchronized_data));
++num_accumulated_; ++num_accumulated_;
@ -237,10 +236,10 @@ LocalTrajectoryBuilder3D::AddRangeData(
const auto voxel_filter_start = std::chrono::steady_clock::now(); const auto voxel_filter_start = std::chrono::steady_clock::now();
const sensor::RangeData filtered_range_data = { const sensor::RangeData filtered_range_data = {
extrapolation_result.current_pose.translation().cast<float>(), extrapolation_result.current_pose.translation().cast<float>(),
sensor::VoxelFilter(options_.voxel_filter_size()) sensor::VoxelFilter(accumulated_range_data.returns,
.Filter(accumulated_range_data.returns), options_.voxel_filter_size()),
sensor::VoxelFilter(options_.voxel_filter_size()) sensor::VoxelFilter(accumulated_range_data.misses,
.Filter(accumulated_range_data.misses)}; options_.voxel_filter_size())};
const auto voxel_filter_stop = std::chrono::steady_clock::now(); const auto voxel_filter_stop = std::chrono::steady_clock::now();
const auto voxel_filter_duration = voxel_filter_stop - voxel_filter_start; const auto voxel_filter_duration = voxel_filter_stop - voxel_filter_start;
@ -274,19 +273,18 @@ LocalTrajectoryBuilder3D::AddAccumulatedRangeData(
const auto scan_matcher_start = std::chrono::steady_clock::now(); 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 = const sensor::PointCloud high_resolution_point_cloud_in_tracking =
adaptive_voxel_filter.Filter(filtered_range_data_in_tracking.returns); sensor::AdaptiveVoxelFilter(
filtered_range_data_in_tracking.returns,
options_.high_resolution_adaptive_voxel_filter_options());
if (high_resolution_point_cloud_in_tracking.empty()) { if (high_resolution_point_cloud_in_tracking.empty()) {
LOG(WARNING) << "Dropped empty high resolution point cloud data."; LOG(WARNING) << "Dropped empty high resolution point cloud data.";
return nullptr; 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 = const sensor::PointCloud low_resolution_point_cloud_in_tracking =
low_resolution_adaptive_voxel_filter.Filter( sensor::AdaptiveVoxelFilter(
filtered_range_data_in_tracking.returns); filtered_range_data_in_tracking.returns,
options_.low_resolution_adaptive_voxel_filter_options());
if (low_resolution_point_cloud_in_tracking.empty()) { if (low_resolution_point_cloud_in_tracking.empty()) {
LOG(WARNING) << "Dropped empty low resolution point cloud data."; LOG(WARNING) << "Dropped empty low resolution point cloud data.";
return nullptr; return nullptr;

View File

@ -17,7 +17,10 @@
#include "cartographer/sensor/internal/voxel_filter.h" #include "cartographer/sensor/internal/voxel_filter.h"
#include <cmath> #include <cmath>
#include <random>
#include <utility>
#include "absl/container/flat_hash_map.h"
#include "cartographer/common/math.h" #include "cartographer/common/math.h"
namespace cartographer { namespace cartographer {
@ -43,7 +46,7 @@ PointCloud AdaptivelyVoxelFiltered(
// 'point_cloud' is already sparse enough. // 'point_cloud' is already sparse enough.
return point_cloud; return point_cloud;
} }
PointCloud result = VoxelFilter(options.max_length()).Filter(point_cloud); PointCloud result = VoxelFilter(point_cloud, options.max_length());
if (result.size() >= options.min_num_points()) { if (result.size() >= options.min_num_points()) {
// Filtering with 'max_length' resulted in a sufficiently dense point cloud. // Filtering with 'max_length' resulted in a sufficiently dense point cloud.
return result; return result;
@ -54,15 +57,14 @@ PointCloud AdaptivelyVoxelFiltered(
for (float high_length = options.max_length(); for (float high_length = options.max_length();
high_length > 1e-2f * options.max_length(); high_length /= 2.f) { high_length > 1e-2f * options.max_length(); high_length /= 2.f) {
float low_length = high_length / 2.f; float low_length = high_length / 2.f;
result = VoxelFilter(low_length).Filter(point_cloud); result = VoxelFilter(point_cloud, low_length);
if (result.size() >= options.min_num_points()) { if (result.size() >= options.min_num_points()) {
// Binary search to find the right amount of filtering. 'low_length' gave // Binary search to find the right amount of filtering. 'low_length' gave
// a sufficiently dense 'result', 'high_length' did not. We stop when the // a sufficiently dense 'result', 'high_length' did not. We stop when the
// edge length is at most 10% off. // edge length is at most 10% off.
while ((high_length - low_length) / low_length > 1e-1f) { while ((high_length - low_length) / low_length > 1e-1f) {
const float mid_length = (low_length + high_length) / 2.f; const float mid_length = (low_length + high_length) / 2.f;
const PointCloud candidate = const PointCloud candidate = VoxelFilter(point_cloud, mid_length);
VoxelFilter(mid_length).Filter(point_cloud);
if (candidate.size() >= options.min_num_points()) { if (candidate.size() >= options.min_num_points()) {
low_length = mid_length; low_length = mid_length;
result = candidate; result = candidate;
@ -76,58 +78,77 @@ PointCloud AdaptivelyVoxelFiltered(
return result; return result;
} }
using VoxelKeyType = uint64_t;
VoxelKeyType GetVoxelCellIndex(const Eigen::Vector3f& point,
const float resolution) {
const Eigen::Array3f index = point.array() / resolution;
const uint64_t x = common::RoundToInt(index.x());
const uint64_t y = common::RoundToInt(index.y());
const uint64_t z = common::RoundToInt(index.z());
return (x << 42) + (y << 21) + z;
}
template <class T, class PointFunction>
std::vector<T> RandomizedVoxelFilter(const std::vector<T>& point_cloud,
const float resolution,
PointFunction&& point_function) {
// According to https://en.wikipedia.org/wiki/Reservoir_sampling
std::minstd_rand0 generator;
absl::flat_hash_map<VoxelKeyType, std::pair<int, int>>
voxel_count_and_point_index;
for (size_t i = 0; i < point_cloud.size(); i++) {
auto& voxel = voxel_count_and_point_index[GetVoxelCellIndex(
point_function(point_cloud[i]), resolution)];
voxel.first++;
if (voxel.first == 1) {
voxel.second = i;
} else {
std::uniform_int_distribution<> distribution(1, voxel.first);
if (distribution(generator) == voxel.first) {
voxel.second = i;
}
}
}
std::vector<bool> points_used(point_cloud.size(), false);
for (const auto& voxel_and_index : voxel_count_and_point_index) {
points_used[voxel_and_index.second.second] = true;
}
std::vector<T> results;
for (size_t i = 0; i < point_cloud.size(); i++) {
if (points_used[i]) {
results.push_back(point_cloud[i]);
}
}
return results;
}
} // namespace } // namespace
PointCloud VoxelFilter::Filter(const PointCloud& point_cloud) { PointCloud VoxelFilter(const PointCloud& point_cloud, const float resolution) {
PointCloud results; return RandomizedVoxelFilter(
for (const RangefinderPoint& point : point_cloud) { point_cloud, resolution,
auto it_inserted = [](const RangefinderPoint& point) { return point.position; });
voxel_set_.insert(IndexToKey(GetCellIndex(point.position)));
if (it_inserted.second) {
results.push_back(point);
}
}
return results;
} }
TimedPointCloud VoxelFilter::Filter(const TimedPointCloud& timed_point_cloud) { TimedPointCloud VoxelFilter(const TimedPointCloud& timed_point_cloud,
TimedPointCloud results; const float resolution) {
for (const TimedRangefinderPoint& point : timed_point_cloud) { return RandomizedVoxelFilter(
auto it_inserted = timed_point_cloud, resolution,
voxel_set_.insert(IndexToKey(GetCellIndex(point.position))); [](const TimedRangefinderPoint& point) { return point.position; });
if (it_inserted.second) {
results.push_back(point);
}
}
return results;
} }
std::vector<TimedPointCloudOriginData::RangeMeasurement> VoxelFilter::Filter( std::vector<sensor::TimedPointCloudOriginData::RangeMeasurement> VoxelFilter(
const std::vector<TimedPointCloudOriginData::RangeMeasurement>& const std::vector<sensor::TimedPointCloudOriginData::RangeMeasurement>&
range_measurements) { range_measurements,
std::vector<TimedPointCloudOriginData::RangeMeasurement> results; const float resolution) {
for (const auto& range_measurement : range_measurements) { return RandomizedVoxelFilter(
auto it_inserted = voxel_set_.insert( range_measurements, resolution,
IndexToKey(GetCellIndex(range_measurement.point_time.position))); [](const sensor::TimedPointCloudOriginData::RangeMeasurement&
if (it_inserted.second) { range_measurement) {
results.push_back(range_measurement); return range_measurement.point_time.position;
} });
}
return results;
}
VoxelFilter::KeyType VoxelFilter::IndexToKey(const Eigen::Array3i& index) {
KeyType k_0(static_cast<uint32>(index[0]));
KeyType k_1(static_cast<uint32>(index[1]));
KeyType k_2(static_cast<uint32>(index[2]));
return (k_0 << 2 * 32) | (k_1 << 1 * 32) | k_2;
}
Eigen::Array3i VoxelFilter::GetCellIndex(const Eigen::Vector3f& point) const {
Eigen::Array3f index = point.array() / resolution_;
return Eigen::Array3i(common::RoundToInt(index.x()),
common::RoundToInt(index.y()),
common::RoundToInt(index.z()));
} }
proto::AdaptiveVoxelFilterOptions CreateAdaptiveVoxelFilterOptions( proto::AdaptiveVoxelFilterOptions CreateAdaptiveVoxelFilterOptions(
@ -140,13 +161,11 @@ proto::AdaptiveVoxelFilterOptions CreateAdaptiveVoxelFilterOptions(
return options; return options;
} }
AdaptiveVoxelFilter::AdaptiveVoxelFilter( PointCloud AdaptiveVoxelFilter(
const proto::AdaptiveVoxelFilterOptions& options) const PointCloud& point_cloud,
: options_(options) {} const proto::AdaptiveVoxelFilterOptions& options) {
PointCloud AdaptiveVoxelFilter::Filter(const PointCloud& point_cloud) const {
return AdaptivelyVoxelFiltered( return AdaptivelyVoxelFiltered(
options_, FilterByMaxRange(point_cloud, options_.max_range())); options, FilterByMaxRange(point_cloud, options.max_range()));
} }
} // namespace sensor } // namespace sensor

View File

@ -19,7 +19,6 @@
#include <bitset> #include <bitset>
#include "absl/container/flat_hash_set.h"
#include "cartographer/common/lua_parameter_dictionary.h" #include "cartographer/common/lua_parameter_dictionary.h"
#include "cartographer/sensor/point_cloud.h" #include "cartographer/sensor/point_cloud.h"
#include "cartographer/sensor/proto/adaptive_voxel_filter_options.pb.h" #include "cartographer/sensor/proto/adaptive_voxel_filter_options.pb.h"
@ -28,55 +27,20 @@
namespace cartographer { namespace cartographer {
namespace sensor { namespace sensor {
// Voxel filter for point clouds. For each voxel, the assembled point cloud PointCloud VoxelFilter(const PointCloud& point_cloud, const float resolution);
// contains the first point that fell into it from any of the inserted point TimedPointCloud VoxelFilter(const TimedPointCloud& timed_point_cloud,
// clouds. const float resolution);
class VoxelFilter { std::vector<sensor::TimedPointCloudOriginData::RangeMeasurement> VoxelFilter(
public: const std::vector<sensor::TimedPointCloudOriginData::RangeMeasurement>&
// 'size' is the length of a voxel edge. range_measurements,
explicit VoxelFilter(float size) : resolution_(size) {} const float resolution);
VoxelFilter(const VoxelFilter&) = delete;
VoxelFilter& operator=(const VoxelFilter&) = delete;
// Returns a voxel filtered copy of 'point_cloud'.
PointCloud Filter(const PointCloud& point_cloud);
// Same for TimedPointCloud.
TimedPointCloud Filter(const TimedPointCloud& timed_point_cloud);
// Same for RangeMeasurement.
std::vector<TimedPointCloudOriginData::RangeMeasurement> Filter(
const std::vector<TimedPointCloudOriginData::RangeMeasurement>&
range_measurements);
private:
using KeyType = std::bitset<3 * 32>;
static KeyType IndexToKey(const Eigen::Array3i& index);
Eigen::Array3i GetCellIndex(const Eigen::Vector3f& point) const;
float resolution_;
absl::flat_hash_set<KeyType> voxel_set_;
};
proto::AdaptiveVoxelFilterOptions CreateAdaptiveVoxelFilterOptions( proto::AdaptiveVoxelFilterOptions CreateAdaptiveVoxelFilterOptions(
common::LuaParameterDictionary* const parameter_dictionary); common::LuaParameterDictionary* const parameter_dictionary);
class AdaptiveVoxelFilter { PointCloud AdaptiveVoxelFilter(
public: const PointCloud& point_cloud,
explicit AdaptiveVoxelFilter( const proto::AdaptiveVoxelFilterOptions& options);
const proto::AdaptiveVoxelFilterOptions& options);
AdaptiveVoxelFilter(const AdaptiveVoxelFilter&) = delete;
AdaptiveVoxelFilter& operator=(const AdaptiveVoxelFilter&) = delete;
PointCloud Filter(const PointCloud& point_cloud) const;
private:
const proto::AdaptiveVoxelFilterOptions options_;
};
} // namespace sensor } // namespace sensor
} // namespace cartographer } // namespace cartographer

View File

@ -24,33 +24,38 @@ namespace cartographer {
namespace sensor { namespace sensor {
namespace { namespace {
using ::testing::ContainerEq; using ::testing::Contains;
TEST(VoxelFilterTest, ReturnsTheFirstPointInEachVoxel) { TEST(VoxelFilterTest, ReturnsOnePointInEachVoxel) {
PointCloud point_cloud = {{Eigen::Vector3f{0.f, 0.f, 0.f}}, const PointCloud point_cloud({{{0.f, 0.f, 0.f}},
{Eigen::Vector3f{0.1f, -0.1f, 0.1f}}, {{0.1f, -0.1f, 0.1f}},
{Eigen::Vector3f{0.3f, -0.1f, 0.f}}, {{0.3f, -0.1f, 0.f}},
{Eigen::Vector3f{0.f, 0.f, 0.1f}}}; {{0.f, 0.f, 0.1f}}});
EXPECT_THAT(VoxelFilter(0.3f).Filter(point_cloud), const PointCloud result = VoxelFilter(point_cloud, 0.3f);
ContainerEq(PointCloud{point_cloud[0], point_cloud[2]})); ASSERT_EQ(result.size(), 2);
EXPECT_THAT(point_cloud, Contains(result[0]));
EXPECT_THAT(point_cloud, Contains(result[1]));
EXPECT_THAT(result, Contains(point_cloud[2]));
} }
TEST(VoxelFilterTest, HandlesLargeCoordinates) { TEST(VoxelFilterTest, HandlesLargeCoordinates) {
PointCloud point_cloud = {{Eigen::Vector3f{100000.f, 0.f, 0.f}}, const PointCloud point_cloud({{{100000.f, 0.f, 0.f}},
{Eigen::Vector3f{100000.001f, -0.0001f, 0.0001f}}, {{100000.001f, -0.0001f, 0.0001f}},
{Eigen::Vector3f{100000.003f, -0.0001f, 0.f}}, {{100000.003f, -0.0001f, 0.f}},
{Eigen::Vector3f{-200000.f, 0.f, 0.f}}}; {{-200000.f, 0.f, 0.f}}});
EXPECT_THAT(VoxelFilter(0.01f).Filter(point_cloud), const PointCloud result = VoxelFilter(point_cloud, 0.01f);
ContainerEq(PointCloud{point_cloud[0], point_cloud[3]})); EXPECT_EQ(result.size(), 2);
EXPECT_THAT(result, Contains(point_cloud[3]));
} }
TEST(VoxelFilterTest, IgnoresTime) { TEST(VoxelFilterTest, IgnoresTime) {
TimedPointCloud timed_point_cloud; TimedPointCloud timed_point_cloud;
for (int i = 0; i < 100; ++i) { for (int i = 0; i < 100; ++i) {
timed_point_cloud.push_back({Eigen::Vector3f{-100.f, 0.3f, 0.4f}, 1.f * i}); timed_point_cloud.push_back({{-100.f, 0.3f, 0.4f}, 1.f * i});
} }
EXPECT_THAT(VoxelFilter(0.3f).Filter(timed_point_cloud), const TimedPointCloud result = VoxelFilter(timed_point_cloud, 0.3f);
ContainerEq(TimedPointCloud{timed_point_cloud[0]})); ASSERT_EQ(result.size(), 1);
EXPECT_THAT(timed_point_cloud, Contains(result[0]));
} }
} // namespace } // namespace