-
Notifications
You must be signed in to change notification settings - Fork 49
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
WIP: Add Axis-Aligned Bounding Box option to Plane-Slicer Planner #123
base: master
Are you sure you want to change the base?
WIP: Add Axis-Aligned Bounding Box option to Plane-Slicer Planner #123
Conversation
f30d772
to
12cf52c
Compare
I would recommend not re-introducing the noether/tool_path_planner/src/surface_walk_raster_generator.cpp Lines 1242 to 1243 in f250219
Also, I would recommend keeping the naming the same for this parameter for both the surface walk and plane slice planner. I don't really care if it's called |
Actually maybe there are 2.5 valid means of determining the raster axis: Option 1Use the longest axis of the object-oriented bounding box (done) Option 2Use an axis defined in the mesh (i.e. global) coordinate system (done in the surface walk planner as of #121) noether/tool_path_planner/src/surface_walk_raster_generator.cpp Lines 1243 to 1249 in f250219
Option 2.5Use the longest axis of the axis-aligned bounding box (proposed in this PR)
|
As far as I am aware, this value never existed for this planner before. Nevertheless, it simplifies the solution to a number of issues we are experiencing on a project. I will attempt to describe below, but for now I will just say that I expect
I have adjusted the code to use the probably-much-more stable .isApprox() from this snippet.
Edit: I think this parameter is actually orthogonal to the parameter in the surface walk planner; this one is perpendicular to the direction of traversal of the resulting path, and instead indicates the direction from one path to the next. Perhaps it is better to keep a distinct name.
Unfortunately, this method is proving somewhat unstable on nearly-rectangular surfaces. A perfectly rectangular part gets a more or less sensible direction, but a slightly-no-longer-rectangular gets odd diagonals that are proving very difficult for our robot to plan. This is why we added the
This is very close to what we actually did.
This is done over 2 because of improved response in this particular planner. Since the planes do not follow the surface at all but only blindly propagate in a constant direction, this planner really only works on nearly-planar parts. Using the longest axis of the AABB allows the user to care a little less whether the part is vertical or horizontal (which, in our use case, could change without the opportunity to come back and re-do parameters). It also more closely mirrors the behavior of the OBB/local axes case. |
I have pushed an attempt to complete the JSON serialization of the new config parameters. I would appreciate a once-over to ensure correct behavior. I will push a test case shortly. |
True, but it did exist in the surface walk parameter and was removed in #121
I would argue that
Two are ambiguous cases that have no clear indication about which parameter takes priority. This is why I am advocating for the single parameter
I see. I think we should settle on a single parameter (either direction of the raster line or the direction of traversal) and use it the same way in both planners. I think it would be very confusing for two very similarly named parameters in two very similar planners to generate very different behaviors. IMO, a user should be able to substitute the plane slice planner for the surface walk planner (and vice-versa) with minimal parameter changes and receive essentially the same output (at least in terms of raster direction, etc.) |
I see now this is true. (I'll note that your PR removing the parameter occurred after I made this branch, so I wasn't aware it was gone.)
I will come back to this.
This is, in part, why I left substantial comments in the _config.msg file. I can copy them to the header.
I think this sentiment is noble and was dismayed to see that they did not actually share an interface. However, deeper investigation made me move away from the idea of a shared interface in this case. The plane slice planner is severely limited in ways the surface walker is not. On many meshes, a user switching from surface walker to plane slicer would never be able to get similar results, even with careful re-tuning of parameters. While the surface walker turns to keep its local cutting planes locally normal to the surface, the plane slicer uses perfectly parallel planes. On almost any curved surface, the distances between rasters generated by the plane slicer can vary wildly, whereas the surface walker will produce approximately equal-spaced paths. Ironically, the plane slicer can actually work on a set of surfaces the surface walker cannot: perfectly planar surfaces. But this distinction means that for any surface that is either not perfectly planar or has curvature along more than one direction, these two planners will never be able to put out the same results, regardless of any tuning. Again, I think it is noble to try to keep api similar and I do very much like interchangeable code. Unfortunately, apples cannot be oranges, and I think differences that meet varied requirements or highlight varied strengths is ... acceptable. |
If we're going to add this change, let's do our due diligence and actually implement this the same way for both planners, not just comment how they're different. I'm fine with either behavior (line direction or traversal direction); it just needs to be the same for both planners. And obviously we should document what that behavior is. The point I'm making is that, for a reasonably flat and smooth mesh, the surface walk and plane slice methods should produce roughly the same output given the same "shared" parameters. I understand that each planner has its limitations, but their outputs shouldn't be dramatically different for a relatively flat and smooth mesh. Sharing an interface might be a nice way to enforce this, but the first and easiest step is to make sure that the parameters that they share (i.e. parameters that have the same names) result in the same type of behavior |
5905069
to
8bf55c3
Compare
8bf55c3
to
812f52e
Compare
…ents_legnths <= 0
Generate extra rasters
float64 min_segment_size # The minimum segment size to allow when finding intersections; small segments will be discarded | ||
float64 min_hole_size # A path may pass over holes smaller than this, but must be broken when larger holes are encounterd. | ||
float64 tool_offset # How far off the surface the tool needs to be | ||
bool generate_extra_rasters # Whether an extra raster stroke should be added to the beginning and end of the raster pattern |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This perhaps could be made to be num_of_extra_rasters so that more than one could be added if needed. Zero would be the default case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good idea. thanks
@@ -66,6 +66,9 @@ class HalfedgeEdgeGenerator : public PathGenerator | |||
|
|||
/**@brief point distance parameter used in conjunction with the spacing method */ | |||
double point_dist = 0.01; | |||
|
|||
/** @brief maximum segment length */ | |||
double max_segment_length = 1.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this parameter used for in the Plane Slicer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in the halfedge edge generator header, which is used for generating edge paths. I assume based on the description that it is the max allows edge segment size. @drchrislewis Is this correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this controls the maximum length of an edge segment. There are alternate parameters and methods for subdividing edges that may result in better performance, but each had corner cases that were more complex, so I went with the simplest method to get us going. For example, since exterior edges always start and end at the same point, we could simply sub-divide by some number whenever there was a circuit. It would be really nice to find the best places to subdivide rather than do it evenly. Again these are complex. One could also classify edges and horizontal-left, hor-rt, ver-up and vert-down. and break them up into 4. Once again this is complex to implement, but may be necessary for some applications/customers.
raster_dir["x"] = raster_direction.x(); | ||
raster_dir["y"] = raster_direction.y(); | ||
raster_dir["z"] = raster_direction.z(); | ||
jv["raster_direction"] = raster_dir; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will work, yaml might've been a better choice as it can directly casts to std vectors and maps
// coordinate frame | ||
extents.push_back(Vector3d::UnitY() * (bounds[3] - bounds[2])); // Extent in y-direction of supplied mesh | ||
// coordinate frame | ||
extents.push_back(Vector3d::UnitZ() * (bounds[5] - bounds[4])); // Extent in z-direction of supplied mesh |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be good to call cwise.abs() to ensure that the values of extends only have positive values even though theoretically it should always be positive
Vector3d raster_dir = (rotation_offset * Vector3d::UnitY()).normalized(); | ||
|
||
// Calculate direction of raster strokes, rotated by the above-specified amount | ||
Vector3d raster_dir = (rotation_offset * config_.raster_direction).normalized(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what kind of results you would get if your raster_direction vector has more than one non-zero component or if it is (0, 0, 1)? Perhaps we should impose some restrictions on what values the raster_dir vector can take
@@ -681,7 +714,15 @@ boost::optional<ToolPaths> PlaneSlicerRasterGenerator::generate() | |||
} | |||
|
|||
// converting to poses msg now | |||
rasters = convertToPoses(rasters_data_vec); | |||
if (config_.generate_extra_rasters) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If generate_extra_rasters
was an integer you could run a subsequent for loop that generates the requested number of "extra rasters"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: make extra_rasters an unsigned integer, defaulted to zero, when non-zero, generate as many as requested
bool SurfaceWalkRasterGenerator::setConfiguration(const Config& config) | ||
{ | ||
config_ = config; | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newer compiler seems to flag these as errors
if (!test_raster_direction.isApprox(Eigen::Vector3d::Zero())) | ||
{ | ||
config.raster_direction = test_raster_direction; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a warning should be thrown when this validation fails
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we assume that we intend to remove raster_wrt_global_axes and use the fact that norm(raster_direction) == 0 implies use default alignment method, and that norm(raster_direction)>0 means to use it, then this syntax is ok.
{ | ||
auto p1 = t1.translation(); | ||
auto p2 = t2.translation(); | ||
return sqrt(pow(p1.x() - p2.x(), 2) + pow(p1.y() - p2.y(), 2) + pow(p1.z() - p2.z(), 2)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could do (p2 - p2).norm() I think and the name of the method should be something more indicative of the kind of distance being returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an artifact born from the use of the keyword auto. When a programmer is not sure what kind of thing they are working with, they might assume it will take some extra work to dig out the data they want. I think we should remove this function, and also replace auto from nearly every for loop we have in our libraries. It helps readability and maintainability. auto should only be used when it is clear from nearby code exactly what the type is.
tool_path_planner/src/utilities.cpp
Outdated
return sqrt(pow(p1.x() - p2.x(), 2) + pow(p1.y() - p2.y(), 2) + pow(p1.z() - p2.z(), 2)); | ||
} | ||
|
||
ToolPaths splitSegments(ToolPaths tool_paths, double max_segment_length) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What use case is there for this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is used by the half-edge-edge-path generator to split an edge segment into sub-segments because the robot is rarely able to traverse a full circuit without winding up.
@DavidMerzJr @marip8 This is a comment regarding the raster_wrt_global_axes, raster_direction, and raster_rotation. The interfaces should match for every rastering method. There are in fact three methods to use. 1. automatic, align raster strokes with the major axis of the geometry. 2. Same as 1, but add a rotation around the minor inertia axis of the geometry, and 3 align with some global axis. However, I believe the implementation of the axis aligned bounding box was not doing 1 correctly on the plane slicer which resulted in poor performance. See the heat_raster method for an example of how to compute the major, minor and middle axes correctly. It does this to cut the surface when no source cut is provided. When using the offline tool, it was very convenient to let the method run amok, and then adjust with the single rotate term. It would be more cumbersome to enter a raster axis. So there are really two methods, aligned with major axis, but rotated by an angle, and aligned with a global axis. If both an rotation angle and a global axis is provided, what should happen? Since global axis is likely to be seldom used, I suggest when it is non-zero, it overrides. |
Edge Path Splitting by Axes
…hanges and comments
… paths for sanding
…me distance rather than using just one
…s is still a WIP.
…nterleaving rasters
…face and vertex normals from mesh either as shape_msg or pcl::Polygonmesh. Modified plane slicer to use MLS for its normal estimation.
…n in half-edge code.
Smoothing and interleaving rebased
…o that it defaults to longest side of bounding box
Fixed default raster direction
Added ability to hop inlets and peninsulas
Functionality requested by @Levi-Armstrong. I am working on a test case to demonstrate the effectiveness of this change. I would appreciate some help puzzling through the JSON serialization code associated with the config structures. (Since I modified the config structure, I will need to update the JSON serialization to keep it fully functional.)