Add TimedPointCloud and TimedRangeData. (#601)

Adds a type TimedPointCloud that holds 4-dimensional vectors where measurement time of individual points are stored in the fourth entry.
Uses TimedRangeData to pass TimedPointCloud of incoming measurements to LocalTrajectoryBuilder.

Fixes #573.
master
gaschler 2017-10-24 11:47:35 +02:00 committed by GitHub
parent 638aee7c2a
commit d4db1e79a6
15 changed files with 151 additions and 54 deletions

View File

@ -46,10 +46,10 @@ class GlobalTrajectoryBuilder
void AddRangefinderData(const common::Time time, void AddRangefinderData(const common::Time time,
const Eigen::Vector3f& origin, const Eigen::Vector3f& origin,
const sensor::PointCloud& ranges) override { const sensor::TimedPointCloud& ranges) override {
std::unique_ptr<typename LocalTrajectoryBuilder::InsertionResult> std::unique_ptr<typename LocalTrajectoryBuilder::InsertionResult>
insertion_result = local_trajectory_builder_.AddRangeData( insertion_result = local_trajectory_builder_.AddRangeData(
time, sensor::RangeData{origin, ranges, {}}); time, sensor::TimedRangeData{origin, ranges, {}});
if (insertion_result == nullptr) { if (insertion_result == nullptr) {
return; return;
} }

View File

@ -53,7 +53,7 @@ class GlobalTrajectoryBuilderInterface {
virtual void AddRangefinderData(common::Time time, virtual void AddRangefinderData(common::Time time,
const Eigen::Vector3f& origin, const Eigen::Vector3f& origin,
const sensor::PointCloud& ranges) = 0; const sensor::TimedPointCloud& ranges) = 0;
virtual void AddSensorData(const sensor::ImuData& imu_data) = 0; virtual void AddSensorData(const sensor::ImuData& imu_data) = 0;
virtual void AddSensorData(const sensor::OdometryData& odometry_data) = 0; virtual void AddSensorData(const sensor::OdometryData& odometry_data) = 0;
virtual void AddSensorData( virtual void AddSensorData(

View File

@ -56,7 +56,7 @@ class TrajectoryBuilder {
void AddRangefinderData(const string& sensor_id, common::Time time, void AddRangefinderData(const string& sensor_id, common::Time time,
const Eigen::Vector3f& origin, const Eigen::Vector3f& origin,
const sensor::PointCloud& ranges) { const sensor::TimedPointCloud& ranges) {
AddSensorData(sensor_id, AddSensorData(sensor_id,
common::make_unique<sensor::DispatchableRangefinderData>( common::make_unique<sensor::DispatchableRangefinderData>(
time, origin, ranges)); time, origin, ranges));

View File

@ -75,7 +75,7 @@ void LocalTrajectoryBuilder::ScanMatch(
std::unique_ptr<LocalTrajectoryBuilder::InsertionResult> std::unique_ptr<LocalTrajectoryBuilder::InsertionResult>
LocalTrajectoryBuilder::AddRangeData(const common::Time time, LocalTrajectoryBuilder::AddRangeData(const common::Time time,
const sensor::RangeData& range_data) { const sensor::TimedRangeData& range_data) {
// Initialize extrapolator now if we do not ever use an IMU. // Initialize extrapolator now if we do not ever use an IMU.
if (!options_.use_imu_data()) { if (!options_.use_imu_data()) {
InitializeExtrapolator(time); InitializeExtrapolator(time);
@ -93,19 +93,22 @@ LocalTrajectoryBuilder::AddRangeData(const common::Time time,
sensor::RangeData{Eigen::Vector3f::Zero(), {}, {}}; sensor::RangeData{Eigen::Vector3f::Zero(), {}, {}};
} }
// TODO(gaschler): Take time delta of individual points into account.
const transform::Rigid3f tracking_delta = const transform::Rigid3f tracking_delta =
first_pose_estimate_.inverse() * first_pose_estimate_.inverse() *
extrapolator_->ExtrapolatePose(time).cast<float>(); extrapolator_->ExtrapolatePose(time).cast<float>();
const sensor::RangeData range_data_in_first_tracking = // TODO(gaschler): Try to move this common behavior with 3D into helper.
sensor::TransformRangeData(range_data, tracking_delta); const sensor::TimedRangeData range_data_in_first_tracking =
sensor::TransformTimedRangeData(range_data, tracking_delta);
// Drop any returns below the minimum range and convert returns beyond the // Drop any returns below the minimum range and convert returns beyond the
// maximum range into misses. // maximum range into misses.
for (const Eigen::Vector3f& hit : range_data_in_first_tracking.returns) { for (const Eigen::Vector4f& hit : range_data_in_first_tracking.returns) {
const Eigen::Vector3f delta = hit - range_data_in_first_tracking.origin; const Eigen::Vector3f delta =
hit.head<3>() - range_data_in_first_tracking.origin;
const float range = delta.norm(); const float range = delta.norm();
if (range >= options_.min_range()) { if (range >= options_.min_range()) {
if (range <= options_.max_range()) { if (range <= options_.max_range()) {
accumulated_range_data_.returns.push_back(hit); accumulated_range_data_.returns.push_back(hit.head<3>());
} else { } else {
accumulated_range_data_.misses.push_back( accumulated_range_data_.misses.push_back(
range_data_in_first_tracking.origin + range_data_in_first_tracking.origin +

View File

@ -38,6 +38,7 @@ namespace mapping_2d {
// Wires up the local SLAM stack (i.e. pose extrapolator, scan matching, etc.) // Wires up the local SLAM stack (i.e. pose extrapolator, scan matching, etc.)
// without loop closure. // without loop closure.
// TODO(gaschler): Add test for this class similar to the 3D test.
class LocalTrajectoryBuilder { class LocalTrajectoryBuilder {
public: public:
struct InsertionResult { struct InsertionResult {
@ -56,7 +57,7 @@ class LocalTrajectoryBuilder {
// Range data must be approximately horizontal for 2D SLAM. // Range data must be approximately horizontal for 2D SLAM.
std::unique_ptr<InsertionResult> AddRangeData( std::unique_ptr<InsertionResult> AddRangeData(
common::Time, const sensor::RangeData& range_data); common::Time, const sensor::TimedRangeData& range_data);
void AddImuData(const sensor::ImuData& imu_data); void AddImuData(const sensor::ImuData& imu_data);
void AddOdometerData(const sensor::OdometryData& odometry_data); void AddOdometerData(const sensor::OdometryData& odometry_data);

View File

@ -60,7 +60,7 @@ void LocalTrajectoryBuilder::AddImuData(const sensor::ImuData& imu_data) {
std::unique_ptr<LocalTrajectoryBuilder::InsertionResult> std::unique_ptr<LocalTrajectoryBuilder::InsertionResult>
LocalTrajectoryBuilder::AddRangeData(const common::Time time, LocalTrajectoryBuilder::AddRangeData(const common::Time time,
const sensor::RangeData& range_data) { const sensor::TimedRangeData& range_data) {
if (extrapolator_ == nullptr) { if (extrapolator_ == nullptr) {
// Until we've initialized the extrapolator with our first IMU message, we // Until we've initialized the extrapolator with our first IMU message, we
// cannot compute the orientation of the rangefinder. // cannot compute the orientation of the rangefinder.
@ -73,17 +73,19 @@ LocalTrajectoryBuilder::AddRangeData(const common::Time time,
sensor::RangeData{Eigen::Vector3f::Zero(), {}, {}}; sensor::RangeData{Eigen::Vector3f::Zero(), {}, {}};
} }
// TODO(gaschler): Take time delta of individual points into account.
const transform::Rigid3f tracking_delta = const transform::Rigid3f tracking_delta =
first_pose_estimate_.inverse() * first_pose_estimate_.inverse() *
extrapolator_->ExtrapolatePose(time).cast<float>(); extrapolator_->ExtrapolatePose(time).cast<float>();
const sensor::RangeData range_data_in_first_tracking = const sensor::TimedRangeData range_data_in_first_tracking =
sensor::TransformRangeData(range_data, tracking_delta); sensor::TransformTimedRangeData(range_data, tracking_delta);
for (const Eigen::Vector3f& hit : range_data_in_first_tracking.returns) { for (const Eigen::Vector4f& hit : range_data_in_first_tracking.returns) {
const Eigen::Vector3f delta = hit - range_data_in_first_tracking.origin; const Eigen::Vector3f delta =
hit.head<3>() - range_data_in_first_tracking.origin;
const float range = delta.norm(); const float range = delta.norm();
if (range >= options_.min_range()) { if (range >= options_.min_range()) {
if (range <= options_.max_range()) { if (range <= options_.max_range()) {
accumulated_range_data_.returns.push_back(hit); accumulated_range_data_.returns.push_back(hit.head<3>());
} else { } else {
// We insert a ray cropped to 'max_range' as a miss for hits beyond the // 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 // maximum range. This way the free space up to the maximum range will

View File

@ -54,7 +54,7 @@ class LocalTrajectoryBuilder {
void AddImuData(const sensor::ImuData& imu_data); void AddImuData(const sensor::ImuData& imu_data);
std::unique_ptr<InsertionResult> AddRangeData( std::unique_ptr<InsertionResult> AddRangeData(
common::Time time, const sensor::RangeData& range_data); common::Time time, const sensor::TimedRangeData& range_data);
void AddOdometerData(const sensor::OdometryData& odometry_data); void AddOdometerData(const sensor::OdometryData& odometry_data);
const mapping::PoseEstimate& pose_estimate() const; const mapping::PoseEstimate& pose_estimate() const;

View File

@ -170,36 +170,47 @@ class LocalTrajectoryBuilderTest : public ::testing::Test {
return first * (to - from) + from; return first * (to - from) + from;
} }
sensor::RangeData GenerateRangeData(const transform::Rigid3d& pose) { sensor::TimedRangeData GenerateRangeData(const transform::Rigid3d& pose) {
// 360 degree rays at 16 angles. // 360 degree rays at 16 angles.
sensor::PointCloud directions_in_rangefinder_frame; sensor::TimedPointCloud directions_in_rangefinder_frame;
for (int r = -8; r != 8; ++r) { for (int r = -8; r != 8; ++r) {
for (int s = -250; s != 250; ++s) { for (int s = -250; s != 250; ++s) {
directions_in_rangefinder_frame.push_back( Eigen::Vector4f first_point;
Eigen::AngleAxisf(M_PI * s / 250., Eigen::Vector3f::UnitZ()) * first_point << Eigen::AngleAxisf(M_PI * s / 250.,
Eigen::AngleAxisf(M_PI / 12. * r / 8., Eigen::Vector3f::UnitY()) * Eigen::Vector3f::UnitZ()) *
Eigen::Vector3f::UnitX()); Eigen::AngleAxisf(M_PI / 12. * r / 8.,
Eigen::Vector3f::UnitY()) *
Eigen::Vector3f::UnitX(),
0.;
directions_in_rangefinder_frame.push_back(first_point);
// Second orthogonal rangefinder. // Second orthogonal rangefinder.
directions_in_rangefinder_frame.push_back( Eigen::Vector4f second_point;
Eigen::AngleAxisf(M_PI / 2., Eigen::Vector3f::UnitX()) * second_point << Eigen::AngleAxisf(M_PI / 2., Eigen::Vector3f::UnitX()) *
Eigen::AngleAxisf(M_PI * s / 250., Eigen::Vector3f::UnitZ()) * Eigen::AngleAxisf(M_PI * s / 250.,
Eigen::AngleAxisf(M_PI / 12. * r / 8., Eigen::Vector3f::UnitY()) * Eigen::Vector3f::UnitZ()) *
Eigen::Vector3f::UnitX()); Eigen::AngleAxisf(M_PI / 12. * r / 8.,
Eigen::Vector3f::UnitY()) *
Eigen::Vector3f::UnitX(),
0.;
directions_in_rangefinder_frame.push_back(second_point);
} }
} }
// We simulate a 30 m edge length box around the origin, also containing // We simulate a 30 m edge length box around the origin, also containing
// 50 cm radius spheres. // 50 cm radius spheres.
sensor::PointCloud returns_in_world_frame; sensor::TimedPointCloud returns_in_world_frame;
for (const Eigen::Vector3f& direction_in_world_frame : for (const Eigen::Vector4f& direction_in_world_frame :
sensor::TransformPointCloud(directions_in_rangefinder_frame, sensor::TransformTimedPointCloud(directions_in_rangefinder_frame,
pose.cast<float>())) { pose.cast<float>())) {
const Eigen::Vector3f origin = const Eigen::Vector3f origin =
pose.cast<float>() * Eigen::Vector3f::Zero(); pose.cast<float>() * Eigen::Vector3f::Zero();
returns_in_world_frame.push_back(CollideWithBubbles( Eigen::Vector4f return_point;
origin, CollideWithBox(origin, direction_in_world_frame))); return_point << CollideWithBubbles(
origin, CollideWithBox(origin, direction_in_world_frame.head<3>())),
0.;
returns_in_world_frame.push_back(return_point);
} }
return {Eigen::Vector3f::Zero(), return {Eigen::Vector3f::Zero(),
sensor::TransformPointCloud(returns_in_world_frame, sensor::TransformTimedPointCloud(returns_in_world_frame,
pose.inverse().cast<float>()), pose.inverse().cast<float>()),
{}}; {}};
} }
@ -242,8 +253,8 @@ class LocalTrajectoryBuilderTest : public ::testing::Test {
const auto range_data = GenerateRangeData(node.pose); const auto range_data = GenerateRangeData(node.pose);
if (local_trajectory_builder_->AddRangeData( if (local_trajectory_builder_->AddRangeData(
node.time, node.time,
sensor::RangeData{range_data.origin, range_data.returns, {}}) != sensor::TimedRangeData{
nullptr) { range_data.origin, range_data.returns, {}}) != nullptr) {
const auto pose_estimate = local_trajectory_builder_->pose_estimate(); const auto pose_estimate = local_trajectory_builder_->pose_estimate();
EXPECT_THAT(pose_estimate.pose, transform::IsNearly(node.pose, 1e-1)); EXPECT_THAT(pose_estimate.pose, transform::IsNearly(node.pose, 1e-1));
++num_poses; ++num_poses;

View File

@ -29,7 +29,7 @@ namespace cartographer {
namespace sensor { namespace sensor {
// A compressed representation of a point cloud consisting of a collection of // A compressed representation of a point cloud consisting of a collection of
// points (Vector3f). // points (Vector3f) without time information.
// Internally, points are grouped by blocks. Each block encodes a bit of meta // Internally, points are grouped by blocks. Each block encodes a bit of meta
// data (number of points in block, coordinates of the block) and encodes each // data (number of points in block, coordinates of the block) and encodes each
// point with a fixed bit rate in relation to the block. // point with a fixed bit rate in relation to the block.

View File

@ -42,7 +42,7 @@ class DispatchableRangefinderData : public Data {
public: public:
DispatchableRangefinderData(const common::Time time, DispatchableRangefinderData(const common::Time time,
const Eigen::Vector3f& origin, const Eigen::Vector3f& origin,
const PointCloud& ranges) const TimedPointCloud& ranges)
: time_(time), origin_(origin), ranges_(ranges) {} : time_(time), origin_(origin), ranges_(ranges) {}
common::Time GetTime() const override { return time_; } common::Time GetTime() const override { return time_; }
@ -54,7 +54,7 @@ class DispatchableRangefinderData : public Data {
private: private:
const common::Time time_; const common::Time time_;
const Eigen::Vector3f origin_; const Eigen::Vector3f origin_;
const PointCloud ranges_; const TimedPointCloud ranges_;
}; };
template <typename DataType> template <typename DataType>

View File

@ -32,7 +32,20 @@ PointCloud TransformPointCloud(const PointCloud& point_cloud,
return result; return result;
} }
PointCloud Crop(const PointCloud& point_cloud, const float min_z, TimedPointCloud TransformTimedPointCloud(const TimedPointCloud& point_cloud,
const transform::Rigid3f& transform) {
TimedPointCloud result;
result.reserve(point_cloud.size());
for (const Eigen::Vector4f& point : point_cloud) {
Eigen::Vector4f result_point;
result_point.head<3>() = transform * point.head<3>();
result_point[3] = point[3];
result.emplace_back(result_point);
}
return result;
}
PointCloud CropPointCloud(const PointCloud& point_cloud, const float min_z,
const float max_z) { const float max_z) {
PointCloud cropped_point_cloud; PointCloud cropped_point_cloud;
for (const auto& point : point_cloud) { for (const auto& point : point_cloud) {
@ -43,5 +56,16 @@ PointCloud Crop(const PointCloud& point_cloud, const float min_z,
return cropped_point_cloud; return cropped_point_cloud;
} }
TimedPointCloud CropTimedPointCloud(const TimedPointCloud& point_cloud,
const float min_z, const float max_z) {
TimedPointCloud cropped_point_cloud;
for (const auto& point : point_cloud) {
if (min_z <= point.z() && point.z() <= max_z) {
cropped_point_cloud.push_back(point);
}
}
return cropped_point_cloud;
}
} // namespace sensor } // namespace sensor
} // namespace cartographer } // namespace cartographer

View File

@ -27,25 +27,38 @@
namespace cartographer { namespace cartographer {
namespace sensor { namespace sensor {
// Stores 3D positions of points.
// For 2D points, the third entry is 0.f.
typedef std::vector<Eigen::Vector3f> PointCloud; typedef std::vector<Eigen::Vector3f> PointCloud;
struct PointCloudWithIntensities { // Stores 3D positions of points with their measurement time in the fourth
PointCloud points; // entry. Time is in seconds, increasing and relative to the moment when
std::vector<float> intensities; // 'points[0]' was acquired. If timing is not available, all fourth entries
// are 0.f. For 2D points, the third entry is 0.f and the fourth entry is time.
typedef std::vector<Eigen::Vector4f> TimedPointCloud;
// For each item in 'points', contains the time delta of when it was acquired struct PointCloudWithIntensities {
// after points[0], i.e. the first entry is always 0.f. If timing TimedPointCloud points;
// information is not available all entries will be 0.f. std::vector<float> intensities;
std::vector<float> offset_seconds;
}; };
// Transforms 'point_cloud' according to 'transform'. // Transforms 'point_cloud' according to 'transform'.
PointCloud TransformPointCloud(const PointCloud& point_cloud, PointCloud TransformPointCloud(const PointCloud& point_cloud,
const transform::Rigid3f& transform); const transform::Rigid3f& transform);
// Transforms 'point_cloud' according to 'transform'.
TimedPointCloud TransformTimedPointCloud(const TimedPointCloud& point_cloud,
const transform::Rigid3f& transform);
// Returns a new point cloud without points that fall outside the region defined // Returns a new point cloud without points that fall outside the region defined
// by 'min_z' and 'max_z'. // by 'min_z' and 'max_z'.
PointCloud Crop(const PointCloud& point_cloud, float min_z, float max_z); PointCloud CropPointCloud(const PointCloud& point_cloud, float min_z,
float max_z);
// Returns a new point cloud without points that fall outside the region defined
// by 'min_z' and 'max_z'.
TimedPointCloud CropTimedPointCloud(const TimedPointCloud& point_cloud,
float min_z, float max_z);
} // namespace sensor } // namespace sensor
} // namespace cartographer } // namespace cartographer

View File

@ -37,6 +37,18 @@ TEST(PointCloudTest, TransformPointCloud) {
EXPECT_NEAR(3.5f, transformed_point_cloud[1].y(), 1e-6); EXPECT_NEAR(3.5f, transformed_point_cloud[1].y(), 1e-6);
} }
TEST(PointCloudTest, TransformTimedPointCloud) {
TimedPointCloud point_cloud;
point_cloud.emplace_back(0.5f, 0.5f, 1.f, 0.f);
point_cloud.emplace_back(3.5f, 0.5f, 42.f, 0.f);
const TimedPointCloud transformed_point_cloud = TransformTimedPointCloud(
point_cloud, transform::Embed3D(transform::Rigid2f::Rotation(M_PI_2)));
EXPECT_NEAR(-0.5f, transformed_point_cloud[0].x(), 1e-6);
EXPECT_NEAR(0.5f, transformed_point_cloud[0].y(), 1e-6);
EXPECT_NEAR(-0.5f, transformed_point_cloud[1].x(), 1e-6);
EXPECT_NEAR(3.5f, transformed_point_cloud[1].y(), 1e-6);
}
} // namespace } // namespace
} // namespace sensor } // namespace sensor
} // namespace cartographer } // namespace cartographer

View File

@ -31,10 +31,27 @@ RangeData TransformRangeData(const RangeData& range_data,
}; };
} }
TimedRangeData TransformTimedRangeData(const TimedRangeData& range_data,
const transform::Rigid3f& transform) {
return TimedRangeData{
transform * range_data.origin,
TransformTimedPointCloud(range_data.returns, transform),
TransformTimedPointCloud(range_data.misses, transform),
};
}
RangeData CropRangeData(const RangeData& range_data, const float min_z, RangeData CropRangeData(const RangeData& range_data, const float min_z,
const float max_z) { const float max_z) {
return RangeData{range_data.origin, Crop(range_data.returns, min_z, max_z), return RangeData{range_data.origin,
Crop(range_data.misses, min_z, max_z)}; CropPointCloud(range_data.returns, min_z, max_z),
CropPointCloud(range_data.misses, min_z, max_z)};
}
TimedRangeData CropTimedRangeData(const TimedRangeData& range_data,
const float min_z, const float max_z) {
return TimedRangeData{range_data.origin,
CropTimedPointCloud(range_data.returns, min_z, max_z),
CropTimedPointCloud(range_data.misses, min_z, max_z)};
} }
} // namespace sensor } // namespace sensor

View File

@ -35,12 +35,26 @@ struct RangeData {
PointCloud misses; PointCloud misses;
}; };
// Like 'RangeData', but with 'TimedPointClouds'.
struct TimedRangeData {
Eigen::Vector3f origin;
TimedPointCloud returns;
TimedPointCloud misses;
};
RangeData TransformRangeData(const RangeData& range_data, RangeData TransformRangeData(const RangeData& range_data,
const transform::Rigid3f& transform); const transform::Rigid3f& transform);
TimedRangeData TransformTimedRangeData(const TimedRangeData& range_data,
const transform::Rigid3f& transform);
// Crops 'range_data' according to the region defined by 'min_z' and 'max_z'. // Crops 'range_data' according to the region defined by 'min_z' and 'max_z'.
RangeData CropRangeData(const RangeData& range_data, float min_z, float max_z); RangeData CropRangeData(const RangeData& range_data, float min_z, float max_z);
// Crops 'range_data' according to the region defined by 'min_z' and 'max_z'.
TimedRangeData CropTimedRangeData(const TimedRangeData& range_data, float min_z,
float max_z);
} // namespace sensor } // namespace sensor
} // namespace cartographer } // namespace cartographer