Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce [Timed]RangefinderPoint. #1357

Merged
merged 31 commits into from
Aug 10, 2018

Conversation

ojura
Copy link
Contributor

@ojura ojura commented Jul 31, 2018

@ojura
Copy link
Contributor Author

ojura commented Jul 31, 2018

So, this PR is about getting rid of direct uses of Vector3/4 throughout the Cartographer codebase and using a new class [Timed]RangefinderPoint instead. Also, I made sure not to interrupt the data flow of points - wherever we recalculate the position inside RangefinderPoint, the structure itself is preserved (using overloading of operator*). This enables libcartographer users to extend RangefinderPoint with their own stuff and have that additional metadata survive the entire local SLAM process (so we get the same RangefinderPoints inside a LocalSlamResult).

Sorry for the PR linediff size. Vector3/4s were sprinkled all over the codebase. It would be great if you could review it quickly so it doesn't rot away :)

@@ -30,14 +39,14 @@ message CompressedPointCloud {
message TimedPointCloudData {
int64 timestamp = 1;
transform.proto.Vector3f origin = 2;
repeated transform.proto.Vector4f point_data = 3;
repeated TimedRangefinderPoint point_data = 3;
Copy link
Contributor Author

@ojura ojura Jul 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok to break compatibility here?
If not, perhaps we could rename the third field to point_data_legacy and have the new TimedRangefinderPoint as the 4th field?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, please do that. Let's not break it for now. We will have plenty of opportunities to do so :)

std::transform(
proto.misses().begin(), proto.misses().end(), std::back_inserter(misses),
static_cast<Eigen::Vector3f (*)(const transform::proto::Vector3f&)>(
transform::ToEigen));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that this monstrosity disappearing is the highlight of this PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot stop being surprised that we have such code in the first place... :)

@@ -190,7 +190,8 @@ LocalTrajectoryBuilder3D::AddRangeData(
// 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);
hit_in_local.RestrictDistanceFromOrigin(origin_in_local,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We avoid breaking the data flow and discarding possible additional metadata here by
(1) computing hit_in_local using operator*(Rigid, RangefinderPoint) which is metadata-friendly
(2) avoiding creating a completely new RangefinderPoint by using a RestrictDistanceFromOrigin method, which is again metadata-friendly

@gaschler
Copy link
Contributor

gaschler commented Aug 2, 2018

@ojura Please consider presenting your design in the open house (today or in two weeks) to make sure implementation goes in the right direction.

@ojura
Copy link
Contributor Author

ojura commented Aug 3, 2018

To address some concerns from yesterday:
Performance:
30 runs of roslaunch cartographer_ros offline_backpack_2d.launch bag_filenames:=b2-2016-04-05-14-44-52.bag no_rviz:=true
with [Timed]RangefinderPoint: μ = 64.64 s, σ = 0.2
master: μ = 65.06 s, σ = 0.31

I believe this change does not increase complexity significantly and that it makes code more readable. It is made clear that a rangefinder point is being passed, and not just any Vector3f. Also, I think .time() is more readable than accessing the fourth element ([3]) of a Vector4f.

@ojura ojura force-pushed the introduce-rangefinderpoint branch from aeb815c to f2edc50 Compare August 7, 2018 18:47
@ojura
Copy link
Contributor Author

ojura commented Aug 7, 2018

@pifon2a @gaschler @cschuet friendly ping, PTAL?

Copy link
Contributor

@pifon2a pifon2a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like what you're doing here.

@@ -52,7 +53,7 @@ struct PointsBatch {
int trajectory_id;

// Geometry of the points in the map frame.
std::vector<Eigen::Vector3f> points;
cartographer::sensor::PointCloud points;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just sensor::PointCloud.

@@ -29,14 +30,14 @@ 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<RangefinderPoint> PointCloud;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
using PointCloud = std::... here and below

@@ -20,6 +20,15 @@ option java_outer_classname = "Sensor";

import "cartographer/transform/proto/transform.proto";

message RangefinderPoint {
transform.proto.Vector3f position = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I like 'position' name here. In ROS it was called just a point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. We're not bound by ROS conventions here, though. A RangefinderPoint having a point member sounds kind of odd to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I see position I always confuse it with pose which also has orientation. What about x_y_z or coordinates? Or leave it as position.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the robotics classes at my uni, we were taught the following definition of a robot pose: The combination of position and orientation is referred to as the pose of an object.

So if that's okay with you, I'd leave it as position :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is ok. :) I did not have robotics classes :)

}

message TimedRangefinderPoint {
RangefinderPoint rangefinder_point = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline the type: transform.proto.Vector3f point = 1; . It can happen that RangefinderPoint has some metadata that we do not really want in TimedRangefinderPoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, should the C++ implementation follow this as well? I.e. TimedRangefinderPoint contains a Vector3f?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

@@ -30,14 +39,14 @@ message CompressedPointCloud {
message TimedPointCloudData {
int64 timestamp = 1;
transform.proto.Vector3f origin = 2;
repeated transform.proto.Vector4f point_data = 3;
repeated TimedRangefinderPoint point_data = 3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, please do that. Let's not break it for now. We will have plenty of opportunities to do so :)

std::transform(
proto.misses().begin(), proto.misses().end(), std::back_inserter(misses),
static_cast<Eigen::Vector3f (*)(const transform::proto::Vector3f&)>(
transform::ToEigen));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot stop being surprised that we have such code in the first place... :)

@@ -32,7 +32,7 @@ struct TimedPointCloudData {

struct TimedPointCloudOriginData {
struct RangeMeasurement {
Eigen::Vector4f point_time;
sensor::TimedRangefinderPoint point_time;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omit sensor::.

namespace cartographer {
namespace sensor {

// Stores 3D position of a point observed by a rangefinder sensor.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Please, use composition here.
  2. All data members should be in private section of the class.
  3. Try to get rid of friend functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the friend operator*: we typically apply a multiplication with a Rigid3 pose estimate from the left. Unfortunately, a member operator* only allows this as the left operand. So I see two solutions: either an overloaded friend operator* like this, or a member function e.g. RangefinderPoint::ToLocal(Rigid3 pose_estimate) which would do a left multiplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, about composition: you mentioned you would inline the Vector3f in TimedRangefinderPoint proto. Does this mean that the C++ class should be the same?

Slicing a TimedRangefinderPoint into a RangefinderPoint then means a little bit of more work. Should I put it in a TimedRangefinderPoint::ToRangefinderPoint member?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make them structs. As in proto.

struct TimedRangefinderPoint {
  proto::RangefinderPoint ToProto() const;
  Eigen::Vector3f position_;
  float time_;
};

struct RangefinderPoint {
  proto::RangefinderPoint ToProto() const;
  Eigen::Vector3f position_;
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't _ only for private members? Also, I think structs shouldn't have methods, IIRC ToProto/FromProto are implemented as functions outside structs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In structs, you do not need _, sorry, i just copied. Structs should not have complicated logic, yes. But serialization/deserialization can be put there. Up to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, cool. I'm only missing one more detail: where to implement discarding time? Member TimedRangefinderPoint::ToRangefinderPoint or standalone function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need it? Isn't it just a call to constructor of RangefinderPoint(timed_point.position)?

@ojura
Copy link
Contributor Author

ojura commented Aug 8, 2018

Okay, the nits should be fixed. I updated the two new classes to be structs. FromProto([Range|TimedPointCloud]Data) should be able to handle old .pbstreams.

for (const auto& point_proto : proto.returns())
returns.push_back(FromProto(point_proto));
} else {
returns.reserve(proto.returns_legacy().size());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should take care of deserializing old .pbstreams

return lhs.position == rhs.position && lhs.time == rhs.time;
}

RangefinderPoint FromProto(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pifon2a WDYT about inlining all of these? Point processing functions seem like they could cause a bottleneck if not inlined.

namespace cartographer {
namespace sensor {

// Stores 3D position of a point observed by a rangefinder sensor.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need it? Isn't it just a call to constructor of RangefinderPoint(timed_point.position)?

const Eigen::Vector3f origin_in_local =
range_data_poses[i] *
synchronized_data.origins.at(synchronized_data.ranges[i].origin_index);
const Eigen::Vector3f hit_in_local = range_data_poses[i] * hit.head<3>();
const Eigen::Vector3f delta = hit_in_local - origin_in_local;
sensor::RangefinderPoint hit_in_local =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am not mistaken, you can write it like:
sensor::RangefinderPoint hit_in_local{range_data_poses[i] * hit.position}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with this and the comment below. The whole point of this PR is to enable adding metadata by changing only rangefinder_point.h. The rest of Cartographer should be written with an assumption that there is some hidden metadata in RangefinderPoint. This metadata should be preserved throughout the SLAM pipeline. By constructing a completely new point here, and not using operator*, we would discard this data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you provide an example of such metadata? And when it will be added to Cartographer.

Copy link
Contributor Author

@ojura ojura Aug 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see the comment below. My primary motivation for this is keeping the laser ID (i.e. which laser of the e.g. 16 lasers on the lidar has observed the point). Currently, I do not plan on upstreaming this, but I would like to be able to do this with minimal interventions to Cartographer, hence this PR. But if there's interest, sure, why not.

Another possible metadata that we might want to add is the sensor ID, to help resolve issues like #947.

float time;
};

template <class T>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since there is no metadata yet, I doubt that the operators*() are needed at all.

return proto;
}

inline RangefinderPoint ToRangefinderPoint(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToRangefinderPoint/ToTimedRangefinderPoint are also not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my use case, they absolutely are. If I decide to add an int LaserId to [Timed]RangefinderPoint in my fork, I want to be able to do this only by updating rangefinder_point.h. In this case, I would update ToRangefinderPoint to also copy over LaserId. I want to avoid having to update the rest of Cartographer codebase for this. This is why I insist on using operator* and these two functions.

if (proto.point_data().size() > 0) {
timed_point_cloud.reserve(proto.point_data().size());
for (const auto& timed_point_proto : proto.point_data())
timed_point_cloud.push_back(FromProto(timed_point_proto));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unfortunately, style guide requires {} here.

@ojura
Copy link
Contributor Author

ojura commented Aug 10, 2018

@pifon2a Have I managed to convince you about operator* and ToRangefinderPoint? Ready to merge?

@pifon2a
Copy link
Contributor

pifon2a commented Aug 10, 2018 via email

Copy link
Contributor

@pifon2a pifon2a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, looks fine.

@pifon2a pifon2a merged commit 73c3d47 into cartographer-project:master Aug 10, 2018
@ojura ojura deleted the introduce-rangefinderpoint branch August 10, 2018 21:30
akshay-prsd pushed a commit to BossaNova/cartographer that referenced this pull request Jul 18, 2019
schayapathy pushed a commit to schayapathy/cartographer that referenced this pull request Aug 15, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants