Skip to content

Add named frames to CollisionObjects (v2)#1439

Merged
rhaschke merged 7 commits intomoveit:masterfrom
felixvd:add-named-frames-to-collision-objects
Jun 1, 2019
Merged

Add named frames to CollisionObjects (v2)#1439
rhaschke merged 7 commits intomoveit:masterfrom
felixvd:add-named-frames-to-collision-objects

Conversation

@felixvd
Copy link
Copy Markdown
Contributor

@felixvd felixvd commented Apr 17, 2019

Description

This PR supercedes #1060 and implements #1041. It includes all the changes requested in the last thread and now targets the correct branch. It requires this PR to moveit_msgs.

In short, this PR adds subframes to CollisionObjects so that one can write "move the tip of object A to somewhere on object B". In the animation below, the cylinder tip is moved to each side of the box in this manner:

simplescreenrecorder-2019-04-17_10 08 11

I made a test package here, which you can run with rosrun moveit_tutorials subframes_tutorial. You can reproduce the animation above by entering the numbers 2, 3, 4, 5.

Small changelog:
I added a version of getFrameTransform to robot_state.cpp which combines the previous getFrameTransform and knowsFrameTransform, so that retrieval of a frame does not require two subsequent searches. I also added getFrameInfo, which returns the name of the robot link that a CollisionObject or named frame is attached to. This is used for changing the frames of constraints in validateConstraintFrames, because only links that are part of the link model can be used for motion planning.

I included the changes to the rest of the codebase where the new getFrameTransform would be used. I can take them out and start a separate PR for that, but they did not seem to cause issues.

Last questions:

  • (Changed) Is the static variable I added here and here used correctly? I was stumped on how to return a reference correctly in a case like this. I suspect this is not thread-safe. Would appreciate a check from @rhaschke and/or whoever feels confident.
  • (fixed by using a reference) I made getFrameTransform forward to getFrameInfo, discarding the robot link returned by getFrameInfo. Should the functions be kept separate? I am not sure if makes a big difference performance-wise. The difference is one string copy.
  • Naming 1: In Add named frames to CollisionObjects moveit_msgs#50, @henningkayser proposed subframe_ids and subframe_poses instead of frame_names and named_frame_poses. Is "subframe" misleading?
  • (Done) Naming 2: Another suggestion is to change the name of the GetConstraintValidity service. I am not opposed, it is only used internally anyway. validateConstraintFrames is the current version.

The Travis build will fail because moveit_msgs is affected. Before merging, I will remove the simple Rviz visualization of the named frames (the red dots in the animation). The visualization will have to be part of a PR addressing #1122 .

Would love to get this merged ASAP, because the GSoC project will affect this area of the code significantly. Thanks for reviewing!

@v4hn @davetcoleman

Checklist

  • Required by CI: Code is auto formatted using clang-format
  • Extended the tutorials / documentation, if necessary reference
  • Include a screenshot if changing a GUI
  • Document API changes relevant to the user in the moveit/MIGRATION.md notes
  • Created tests, which fail without this PR reference
  • Decide if this should be cherry-picked to other current ROS branches
  • While waiting for someone to review your request, please consider reviewing another open pull request to support the maintainers

Copy link
Copy Markdown
Contributor

@jonbinney jonbinney left a comment

Choose a reason for hiding this comment

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

I've done a partial review and found that I have opinions on some of the high level choices in the design.

  1. There's this constant tension in the design of the planning scene (and world). Adding more specific features makes certain use cases very convenient. But it also means more code that might only be useful to some people. Personally I'd rather keep all the subframe info in my own external data structure, and be able to make that as complex as desired, without putting that into the planning scene itself. But I don't feel strongly about that, and it sounds like others find this basic concept quite useful, so I'll move on.

  2. Mixing the child frames into the top level object namespace makes a few parts of the API slightly more convenient for the user, but I think it's too likely to cause problems. I think it would be a lot cleaner API (and less code) if the named frames only makes sense in the context of a specific object. Thoughts?

bool World::knowsTransform(const std::string& object_id) const
{
auto it = objects_.find(id);
// Check object names first
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like this code does what it's meant to, but i'm torn. It seems a bit sketchy to put both "objects" and "child objects" in the same object namespace. Based on the description of the PR, I was expecting these child frames to only make sense in the context of their parent objects. For example "grasp_pose" or "handle_pose" on a teapot aren't the name of an object at the top level. It feels like it would be cleaner to not treat these child frames "objects" at all; so it seems like a top level "knowsTransform" should only check for object transforms (and therefor wouldn't be too useful compared to hasObject().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See the mainline comment

throw std::runtime_error("No transform found for object_id");
}

std::string World::getObjectOwningFrame(const std::string& frame_name) const
Copy link
Copy Markdown
Contributor

@jonbinney jonbinney Apr 22, 2019

Choose a reason for hiding this comment

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

Same comment here - i'd prefer that named frames within an object only make sense in the context of an object. So it wouldn't make sense to call getObjectOwningFrame("handle_frame") because i might have objects "teapot1" and "teapot2" in the scene which both have a "handle_frame"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See the mainline comment about namespacing

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 22, 2019

Thanks for the second look!

2. Mixing the child frames into the top level object namespace makes a few parts of the API slightly more convenient for the user, but I think it's too likely to cause problems. I think it would be a lot cleaner API (and less code) if the named frames only makes sense in the context of a specific object. Thoughts?

@henningkayser proposed prefixing the named frames/subframes with the object name (which already needs to be unique), so they're namespaced. The named frame tip of screwdriver would be screwdriver_tip or screwdriver/tip in the scene. What do you think about that?

I don't know what you mean by "the named frames only make sense in the context of a specific object". Do you mean the user should get the frame transformation from the object to the object part, apply it to the pose they want to move the part to, and then plan with the new pose applied to the end effector? I do think that's too cumbersome to be useful.

@jonbinney
Copy link
Copy Markdown
Contributor

@felixvd yes, i was proposing requiring the user to manually lookup the object and child-frame transforms separately and chain them together. I was imagining something like:

transform = world.getTransform("object_foo") * world.getObject("object_foo").getTransform("child_frame_bar")

Which didn't seem so bad; but I can see that it would be even more convenient to have a single function call in the API as you suggest. Prefixing with the object name is interesting, but it doesn't feel very machine-friendly (it's certainly user-friendly). How about having a function like:

transform = world.getChildFrameTransform(object_name, chlid_frame_name)

And then within the world we can do the lookups and multiply the two transforms, and raise a runtime error if one of them doesn't exist?

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 23, 2019

I can add the function, but it would make things a lot easier to just write "move top of cup under spout" instead of multiplying transforms. Normal users don't handle raw transforms in ROS either, and I don't think they should be expected to. It's something that notoriously goes wrong and that I don't miss doing at all.

I'll implement an attempt at namespacing this week though.

@jonbinney
Copy link
Copy Markdown
Contributor

Sounds good - having the function that handles the namespacing for the user sounds like a good compromise.

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 24, 2019

I changed the naming from "named_frames" to "subframes". Subframes are always prefixed by their parent object's name, so the subframe "tip" of an object "cylinder" can be used as "cylinder_tip" in the planning scene. The namespacing is done in world.cpp and attached_body.h, in the respective getTransform function for collision::world::objects and AttachedBody objects.

In the current commit there is a bug that seems to cause a crash when the subframe name is shorter than the object's name (?), but I should be able to fix that soon. You can see it occurring for "box_top" in my test package, but not for the longer subframe names. Shortening them causes the error to occur for them, too.

@jonbinney
Copy link
Copy Markdown
Contributor

Thanks @felixvd ! It is going to be a few days before i have time to do my next review of this, but I will get to it as soon as I can.

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 25, 2019

Just documenting that this branch contains essentially the same commit that seems to be causing me trouble, except this one is fine. Should be done before the maintainer meeting tomorrow.

Copy link
Copy Markdown
Contributor

@rhaschke rhaschke left a comment

Choose a reason for hiding this comment

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

... it would be a lot cleaner API if the named frames are only used in the context of a specific object.

I agree with @jonbinney, that it feels strange to access those transforms in the same flat namespace as all other transforms. This makes it both, more costly (because you need to search all objects) and less transparent. I support Jon's proposal to use a function like
transform = world.getObjectSubFrame(object_name, chlid_frame_name)

* Use these to define points of interest on the object to plan with
* (e.g. screwdriver_tip, kettle_spout, mug_base).
* */
std::map<std::string, Eigen::Isometry3d> subframe_poses_;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You need to use an aligned allocator for the Eigen transforms!
What about directly using moveit::core::Transforms or at least moveit::core::FixedTransformsMap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't know what an aligned allocator is, but I can replace the std::map with moveit::core::FixedTransformsMap if that works?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I had to look this up too: this is the best resource I found, and this issue is some recent discussion on it.

👍 for using FixedTransformsMap

@rhaschke
Copy link
Copy Markdown
Contributor

Dear Felix, could you please clean up your commit history a little bit?
I suggest separating the core functional changes from variable renamings. All fixups should be squashed into corresponding commits. As of now, the PR is large and not very modular in its commits.

@rhaschke
Copy link
Copy Markdown
Contributor

Just documenting that this branch contains essentially the same commit that seems to be causing me trouble, except this one is fine.

@felixvd, what problem(s) are you referring to? Travis failures?

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 26, 2019

Thanks for the review!

Dear Felix, could you please clean up your commit history a little bit?
I suggest separating the core functional changes from variable renamings. All fixups should be squashed into corresponding commits. As of now, the PR is large and not very modular in its commits.

I usually do squash/reorder before pushing, but I am not sure what the right way to do it is when commits were already pushed. Separate everything into multiple new PRs and abandon this one? Force push the branch to a clean version of commits? I assume that would interfere with the PR and review interface here on Github?
Please advise on the preferred method.

it feels strange to access those transforms in the same flat namespace as all other transforms. This makes it both, more costly (because you need to search all objects) and less transparent. I support Jon's proposal to use a function like
transform = world.getObjectSubFrame(object_name, chlid_frame_name)

I worry that this is too complicated for the common user. I am hoping for this functionality to be readily accessible, and end users should not need to multiply transforms. The world object is already on a deeper level than the average user who uses only geometry_msg objects with the move_group or planning_scene_interface.

Just to confirm: Is it the search through all the subframes that you find problematic, or the access through the top-level namespace?

If it is the latter, I don't know that this design decision would be consistent. We already let users specify a collision object name as a target for planning, although that requires a search operation through the scene as well. If we do not value this convenience, why not ask users to do transform = world.getFixedTransform(object_id)? (That function does not exist at the moment, but it could)

If it is the former, I am not sure that performance would be affected significantly. The search through object subframes always happens last, so robot links, CollisionObjects and AttachedBody objects are retrieved as fast as before. It will also only make a difference in scenes with many objects and frames. It might be premature optimization to favor this rather than an easier interface.

This is the current API. I hope we can agree that this is very readable, even for beginners, and that readability is important:

geometry_msgs::PoseStamped ps;
tf2::Quaternion quaternion;
moveit::planning_interface::MoveGroupInterface group("panda_arm");

ps.header.frame_id = "box/hole";
quaternion.setRPY(0, (180.0 / 180.0 * M_PI), 0);
ps.pose.orientation = tf2::toMsg(quaternion);
ps.pose.position.z = 0.01;
group.setEndEffectorLink("cylinder/tip");
group.setPoseTarget(ps);
group.go();

The only alternative I could imagine (to avoid the top-level namespacing) is something like this:
(ignore the slightly incorrect syntax)

moveit::planning_interface::PlanningSceneInterface planning_scene_interface;

//ps.header.frame_id = "box/hole"
quaternion.setRPY(0, (180.0 / 180.0 * M_PI), 0);
ps.pose.orientation = tf2::toMsg(quaternion);
ps.pose.position.z = 0.01;
ps = planning_scene_interface.transformPoseHeaderFromSubframe(ps, object="box", subframe="hole");
// Returns a transformed ps such that ps.header.frame_id = "box"
group.setEndEffectorLink("cylinder/tip");
group.setPoseTarget(ps);
group.go();

But I find this less elegant, and likely much harder to find for new users.

@felixvd, what problem(s) are you referring to? Travis failures?

On 8f31daa, planning to the subframe box/top fails, but on e8c5172 it is fine. When I added the changes from 8f31daa in this branch while adding only comments, I could not reproduce the issue.

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 26, 2019

For the protocol, @rhaschke and I had a quick Skype call to discuss this PR and some ideas.

  • Robert raised the idea of creating a global transform map in the planning scene, rather than keeping two separate entities (robot state and collision::world), which now get even more complex with this PR. I agree that there is some acrobatics involved to make it work (like the validateConstraintFrames service), and that the separation into robot_state and collision::world is strange and likely not very performant. I did not dare to make any more drastic changes, but I think this kind of change makes a lot of sense. We agreed to make that discussion a separate issue and try to get this PR merge-ready first.
  • We are both in favor of the API I proposed, but with a slash as a prefix separator instead of an underscore, so the example from my last post would read:
ps.header.frame_id = "box/hole"
group.setEndEffectorLink("cylinder/tip");
  • The above actually already works, because the lookup ignores the separator (it just jumps one character to find the subframe name)
  • I will condense the commit history of this PR and force push a few prettier commits by next week.

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 26, 2019

As a detail that is open to comments, I wondered in the OP if the string copy in getFrameInfo here is wasteful, because getFrameTransform does not need the string. Robert proposed returning the pointer to the robot link instead of the name, so the overhead would only be an assignment and not a string copy. However, because the link model is const, a std::pair object would have to be created, and I am not sure how much overhead that is. The resulting function declaration would be:

std::pair<const Eigen::Isometry3d&, const LinkModel*> RobotState::getFrameInfo(const std::string& frame_id, bool& frame_found) const;

@rhaschke
Copy link
Copy Markdown
Contributor

However, because the link model is const, a std::pair object would have to be created.

Can't you just "return" (via arguments) a const pointer to the LinkModel, like so:
const Eigen::Isometry3d& getFrameInfo(const std::string &frame_id, LinkModel* const &link, bool &frame_found) const;
Note the order of const in the type specification.
https://stackoverflow.com/questions/11514688/passing-const-pointer-by-reference

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 26, 2019

Sounds great, just what I was looking for. I'll do that if it works.

@felixvd felixvd force-pushed the add-named-frames-to-collision-objects branch from 0dfc7ee to 18c0b19 Compare April 28, 2019 14:23
@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 28, 2019

As it turns out, with the order const LinkModel* &link everything compiles and works fine, but with the order LinkModel* const &link, the compiler would give this error:

/home/catkin_ws/src/moveit/moveit_core/robot_state/src/robot_state.cpp:1019:44: error: assignment of read-only reference ‘robot_link’
     robot_link = robot_model_->getRootLink();
                                            ^
/home/catkin_ws/src/moveit/moveit_core/robot_state/src/robot_state.cpp:1019:43: error: invalid conversion from ‘const moveit::core::LinkModel*’ to ‘moveit::core::LinkModel*’ [-fpermissive]
     robot_link = robot_model_->getRootLink();

Second, I fixed the confusing issue I had earlier where only std::cout messages changed the program outcome. I had accidentally left the return value undefined after catching an exception in hasSubframeTransform.

Finally, as requested by @rhaschke, I cleaned up the commit history into hopefully readable chunks. I recommend looking at each of those separately instead of all the changed files together. There are more detailed explanations in the commit messages, as well. Each commit probably does not compile on its own, they are just grouped semantically for easier reading. Thanks in advance!

Copy link
Copy Markdown
Contributor

@BryceStevenWilley BryceStevenWilley left a comment

Choose a reason for hiding this comment

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

First off, great feature, thanks for putting in the work for it.

General comments from reading the mainline thread:

  • I think I agree with @jonbinney in that "the named frames only make sense in the context of a specific object". It seems the discussion came down to the two APIs you mentioned in this comment. I don't see too much of a difference between the two at the moment, but I do find the latter make more sense when extending this idea. For example, if you have an object that you have multiple screw holes in, a call like
std::vector<std::string> frames = planning_scene_interface.getSubframes(ps, object="box");
for (frame : frames)
{
    ps = planning_scene_interface.transformPoseHeaderFromSubframe(ps, object="box", subframe=frame);
    move_group.setPoseTarget(ps);
    move_group.go();
}

flows better with the second API. Although it could still work with the first API, it'd just be a little more awkward, adding strings and what not. At the moment, with the existing functionality, I don't have a preference between the two APIs, and am okay with your proposed one.

  • Agree with using / instead of _ as separators, but the code should be strict about what's there, not just skipping any character that's present (what a weird bug for someone to run into: "drill/bit is the same as drillabit").

* Use these to define points of interest on the object to plan with
* (e.g. screwdriver_tip, kettle_spout, mug_base).
* */
std::map<std::string, Eigen::Isometry3d> subframe_poses_;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I had to look this up too: this is the best resource I found, and this issue is some recent discussion on it.

👍 for using FixedTransformsMap

@felixvd felixvd force-pushed the add-named-frames-to-collision-objects branch from 18c0b19 to a00ae85 Compare April 29, 2019 06:33
@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented Apr 29, 2019

I updated each of the commits as suggested by Bryce, and started changing from std::map to Transforms in the WIP commit, but it looks like it's not that easy. It seems that the ensureUnique function in world.cpp tries to copy the object, but Transforms is set to boost::noncopyable.

Compiler error for the WIP commit:

Errors     << moveit_core:make /home/catkin_ws/logs/moveit_core/build.make.049.log
/home/catkin_ws/src/moveit/moveit_core/collision_detection/src/world.cpp: In member function ‘void collision_detection::World::ensureUnique(collision_detection::World::ObjectPtr&)’:
/home/catkin_ws/src/moveit/moveit_core/collision_detection/src/world.cpp:131:30: error: use of deleted function ‘collision_detection::World::Object::Object(const collision_detection::World::Object&)’
     obj.reset(new Object(*obj));
                              ^
In file included from /home/catkin_ws/src/moveit/moveit_core/collision_detection/src/world.cpp:37:0:
/home/catkin_ws/src/moveit/moveit_core/collision_detection/include/moveit/collision_detection/world.h:82:10: note: ‘collision_detection::World::Object::Object(const collision_detection::World::Object&)’ is implicitly deleted because the default definition would be ill-formed:
   struct Object
          ^~~~~~
/home/catkin_ws/src/moveit/moveit_core/collision_detection/include/moveit/collision_detection/world.h:82:10: error: use of deleted function ‘moveit::core::Transforms::Transforms(const moveit::core::Transforms&)’
In file included from /home/catkin_ws/src/moveit/moveit_core/collision_detection/include/moveit/collision_detection/world.h:41:0,
                 from /home/catkin_ws/src/moveit/moveit_core/collision_detection/src/world.cpp:37:
/home/catkin_ws/src/moveit/moveit_core/transforms/include/moveit/transforms/transforms.h:61:7: note: ‘moveit::core::Transforms::Transforms(const moveit::core::Transforms&)’ is implicitly deleted because the default definition would be ill-formed:
 class Transforms : private boost::noncopyable
       ^~~~~~~~~~
/home/catkin_ws/src/moveit/moveit_core/transforms/include/moveit/transforms/transforms.h:61:7: error: use of deleted function ‘boost::noncopyable_::noncopyable::noncopyable(const boost::noncopyable_::noncopyable&)’
In file included from /usr/include/boost/noncopyable.hpp:15:0,
                 from /home/catkin_ws/src/moveit/moveit_core/transforms/include/moveit/transforms/transforms.h:43,
                 from /home/catkin_ws/src/moveit/moveit_core/collision_detection/include/moveit/collision_detection/world.h:41,
                 from /home/catkin_ws/src/moveit/moveit_core/collision_detection/src/world.cpp:37:
/usr/include/boost/core/noncopyable.hpp:34:7: note: declared here
       noncopyable( const noncopyable& ) = delete;
       ^~~~~~~~~~~

I wouldn't know how to fix this (I assume ensureUnique is there for a reason), but I know that the subframe transforms are only used when changing collision objects in the scene and once in validateConstraintFrames when the planning request is constructed. Since they are not used in collision checking and other high-iteration procedures, there might not be a performance gain to be had here, but I don't know much about memory issues.

@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented May 23, 2019

Looks good to me just reading the code, but can you explain the change in this commit? This seems to change behavior from the original version, and is also not in line with the behavior of getTransform in the same file.

@rhaschke
Copy link
Copy Markdown
Contributor

The original code considered object frames to be known only if there is a unique shape associated with it. You changed this semantic in the first place:
SceneTransforms::knowsObject
PlanningScene::knowsFrameTransform
PlanningScene::getFrameTransform

@felixvd felixvd force-pushed the add-named-frames-to-collision-objects branch from 502f2e1 to da7539f Compare May 23, 2019 15:22
@felixvd
Copy link
Copy Markdown
Contributor Author

felixvd commented May 23, 2019

True. I was thinking of the getTransform logic which just gave a warning. My bad.

I force-pushed your branch to let Travis run, but this line displays an error message when using subframes. I would keep using 'hasLinkModel'/'getLinkModel' because there are never that many robot links and creating the motion planning request is not a frequent operation, so this should not be costly.

Also do you want the fixup commits combined before merging?

@rhaschke
Copy link
Copy Markdown
Contributor

this line displays an error message when using subframes.

You are right. I wasn't aware of the verboseness of this method anymore...
Please revert to the hasLink/getLink pair.

felixvd and others added 6 commits May 23, 2019 14:07
The getFrameInfo function is needed later for validateConstraintFrames. The getFrameTransform functions with "frame_found" parameter allow skipping the redundant searches in the planning_scene that are also part of this commit.
In PlanningScene and collisions.cpp
@felixvd felixvd force-pushed the add-named-frames-to-collision-objects branch from da7539f to e5eb0d7 Compare May 29, 2019 14:24
Copy link
Copy Markdown
Contributor

@rhaschke rhaschke left a comment

Choose a reason for hiding this comment

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

I think, this is finally ready for merging. 🎉

@rhaschke
Copy link
Copy Markdown
Contributor

rhaschke commented Jun 1, 2019

@felixvd, can you file your tutorial examples as a PR, please?

@rhaschke
Copy link
Copy Markdown
Contributor

rhaschke commented Jun 2, 2019

@felixvd, I just noticed that I already filed the tutorial PR: moveit/moveit_tutorials#326
Please review there.

@davetcoleman
Copy link
Copy Markdown
Member

So glad this major new feature was merged in!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants