Add SphericalVoxelGrid Filter#2171
Conversation
This commit adds the SphericalVoxelGrid filter to the filters library, which allows point clouds to be downsampled using a leaf size specified in spherical coordinates.
|
Hi, thanks for the effort.
|
I imagine it would help other contributors if this were updated. Thanks for the quick comments. |
👍
Certainly, we should change it. Thanks for letting us know and sorry for extra effort. |
|
I have one more quick comment regarding phi/theta and setter/getter functions. Maybe it is more intuitive to name the functions like |
|
Discussed changes are implemented. I agree with leaf size functions and have renamed the external interface to use the more intuitive vertical/horizontal language. Phi/theta is only internal. Should I modify that tutorial here or would another PR be more appropriate? |
| /** \brief Get the radial size of the leaves | ||
| */ | ||
| inline float | ||
| getLeafSizeR () { return (leaf_size_r_); } |
There was a problem hiding this comment.
This method can be marked const (same for all the other getters).
|
|
||
| /** \brief Get the cartesian origin used to build the spherical grid. | ||
| */ | ||
| inline Eigen::Vector3f |
There was a problem hiding this comment.
Is there any pragmatic reason to have 3-vector in getOrigin but 4-vector in setOrigin?
There was a problem hiding this comment.
No. I believe this was mimicking some of the behavior in VoxelGrid. Is there a particular reason to prefer 4 vs 3 vectors (SIMD instructions or something)? If not, I'll switch setOrigin to take a 3-vector since it needs 3-dimensional coordinates.
There was a problem hiding this comment.
Taking into account how this is used in the class (single vector subtraction inside a fat loop), I would be very surprised to see any performance difference. I'd back switching to 3-vectors.
| * \param[out] limit_max the maximum allowed field value | ||
| */ | ||
| inline void | ||
| getFilterLimits (double &limit_min, double &limit_max) |
There was a problem hiding this comment.
Did you choose API for this function following some other similar existing in PCL? Then let's keep it this way for consistency, otherwise I'd propose to return std::pair<double, double>.
There was a problem hiding this comment.
Yes, this is the same filter field interface that VoxelGrid uses.
There was a problem hiding this comment.
OK, let's keep it this way then.
| * \param[out] limit_negative true if data \b outside the interval [min; max] is to be returned, false otherwise | ||
| */ | ||
| inline void | ||
| getFilterLimitsNegative (bool &limit_negative) { limit_negative = filter_limit_negative_; } |
There was a problem hiding this comment.
Preserving consistent interface with VoxelGrid
| * Software License Agreement (BSD License) | ||
| * | ||
| * Point Cloud Library (PCL) - www.pointclouds.org | ||
| * Copyright (c) 2009, Willow Garage, Inc. |
There was a problem hiding this comment.
I don't think we need Willow here. Your contribution is brand new code, even if you were "inspired" by the voxel grid implementation. As such, it should be copyrighted for Open Perception 2018.
| { | ||
| if (!input_->is_dense) | ||
| // Check if the point is invalid | ||
| if (!pcl_isfinite (input_->points[*it].x) || |
There was a problem hiding this comment.
Can be replaced with isFinite (input_->points[*it])
| // Get the filter value | ||
| const uint8_t* pt_data = reinterpret_cast<const uint8_t*> (&input_->points[*it]); | ||
| float filter_value = 0; | ||
| memcpy (&filter_value, pt_data + fields[filter_idx].offset, sizeof (float)); |
There was a problem hiding this comment.
Being picky, not every field is of type float. Intended use is with float fields though, so this is probably ok.
There was a problem hiding this comment.
How about I introduce a check to verify that the field is a float?
There was a problem hiding this comment.
Based on my previous comment, this assumption that the type is a single precision floating point might need to be revised.
There was a problem hiding this comment.
I don't think it makes sense to invoke memcopy to just copy a single float. Make filter_value a pointer and initialize it in the correct position.
|
|
||
| // Find the number of radial layers required given the farthest point and resolution | ||
| max_radius_ = (max_p - filter_origin_).norm (); | ||
| uint64_t rIdxNum = static_cast<uint64_t>(std::floor(max_radius_ / leaf_size_r_)) + 1; |
There was a problem hiding this comment.
Why did you choose 64 bit here? And wouldn't it be more consistent to cast the division operands to double then?
There was a problem hiding this comment.
The purpose of this calculation is to verify that the number of discrete voxels does not overflow an int32_t. The multiplication within the if statement below is performed with uint64_ts to ensure that the potential overflow is detected. In this case, I just performed the cast here instead of within the if statement. I will move the cast into the if statement and switch rIdxNum to int32_t to make things more consistent.
|
|
||
| PointT shiftedPoint = input_->points[*it]; | ||
|
|
||
| shiftedPoint.x -= filter_origin_[0]; |
There was a problem hiding this comment.
shiftedPoint.getVector4fMap () -= filter_origin_
| shiftedPoint.z -= filter_origin_[2]; | ||
|
|
||
| // Convert to spherical coordinates | ||
| float r = std::sqrt (shiftedPoint.x * shiftedPoint.x + |
There was a problem hiding this comment.
shiftedPoint.getVector3fMap ().norm ()
Please another PR. |
Make functions const, make origin function vectors consistent, delete getMaxDistance declarations, add point field type check
|
Thanks! From the interface and implementation perspective everything looks fine for me now. Only the formatting does not match the style guide, which requires spaces between function names and opening brackets. (Sorry for that :-)) I'll be away for several weeks starting this weekend. In the meantime I guess @SergioRAgostinho will be back, provide his comments, and then hopefully merge this. |
|
Fixed. Thanks for the quick review! Also, I need to add the point cloud that the user can download as part of the tutorial. Where do I put this pcd file so that the tutorial link works correctly? |
|
We typically put PCD files for tutorials into this repository: https://github.com/PointCloudLibrary/data/tree/master/tutorials. |
|
|
||
| project(voxel_grid) | ||
|
|
||
| find_package(PCL 1.8.1 REQUIRED) |
There was a problem hiding this comment.
This should be 1.8.1.99. Reason: 1.8.1 is already released and it does not include your filter. We indicate "the next release after x.x.x" by appending .99 suffix. When time comes to release the next version (whichever number it will have), we will search/replace all occurrences.
Agreed. Off to start reviewing this now 🛫 Sorry for the delay. |
SergioRAgostinho
left a comment
There was a problem hiding this comment.
Partial review, but already some work.
| #include <pcl/filters/spherical_voxel_grid.h> | ||
|
|
||
| int | ||
| main (int argc, char** argv) |
There was a problem hiding this comment.
You don't use argc or argv so don't define them.
| int | ||
| main (int argc, char** argv) | ||
| { | ||
| pcl::PointCloud<pcl::PointXYZI>::Ptr input(new pcl::PointCloud<pcl::PointXYZI>); |
| main (int argc, char** argv) | ||
| { | ||
| pcl::PointCloud<pcl::PointXYZI>::Ptr input(new pcl::PointCloud<pcl::PointXYZI>); | ||
| pcl::PointCloud<pcl::PointXYZI>::Ptr output(new pcl::PointCloud<pcl::PointXYZI>); |
There was a problem hiding this comment.
This doesn't need be a shared pointer. You're alway employing it like this *output.
Spacing.
|
|
||
| pcl::io::loadPCDFile<pcl::PointXYZI> ("spherical_voxel_grid_example.pcd", *input); | ||
|
|
||
| std::cout << "Size before downsample: " << input->points.size() << std::endl; |
There was a problem hiding this comment.
Please use input->size () instead.
Spacing.
| voxel.setLeafSize (0.1, 90, 300); | ||
| voxel.filter (*output); | ||
|
|
||
| std::cout << "Size after downsample: " << output->points.size() << std::endl; |
There was a problem hiding this comment.
Please use output->size () instead.
Spacing.
|
|
||
| /** \brief Get the cartesian origin used to build the spherical grid. | ||
| */ | ||
| inline Eigen::Vector3f |
There was a problem hiding this comment.
Return const Eigen::Vector3f& instead. That way no copy is generated if the user doesn't want it.
There was a problem hiding this comment.
But we are returning a result of head<3>() function call, this can not be a reference.
There was a problem hiding this comment.
Then what about changing the getters and setters to Eigen::Vector4f which is in reality what we store?
There was a problem hiding this comment.
What we store is an implementation detail, I don't think it should influence our getters/setters. So the question is rather what is more convenient for the user: non-homogeneous or homogeneous coordinates? And I have no answer to this :)
There was a problem hiding this comment.
All our point types are inherently homogenous and we do provide Eigen maps for both 3f and 4f types, from and to them, so I feel having the Vector4f version in the getter/setter places no inconvenience from the user perspective, allowing us to "potentially" save a copy.
(That was a long sentence!)
Anyway, It's not a big thing and since we don't seem to have a clear opinion here I leave it up to @sddavis14 to decide in light of the additional information we provided.
There was a problem hiding this comment.
Will come back to this issue and make a final decision when time permits. For now, I have fixed the other discussed problems.
| setFilterFieldName (const std::string &field_name) { filter_field_name_ = field_name; } | ||
|
|
||
| /** \brief Get the name of the field used for filtering. */ | ||
| inline std::string const |
There was a problem hiding this comment.
Return a const std::string& instead for the same reasons mentioned above.
| double filter_limit_min_; | ||
|
|
||
| /** \brief The maximum allowed filter value a point will be considered from. */ | ||
| double filter_limit_max_; |
There was a problem hiding this comment.
PCL coordinates are all stored in floats. I'm not sure if makes sense to have doubles representing the filters limits. @taketwo ?
| * \param[out] limit_negative true if data \b outside the interval [min; max] is to be returned, false otherwise | ||
| */ | ||
| inline void | ||
| getFilterLimitsNegative (bool &limit_negative) const { limit_negative = filter_limit_negative_; } |
There was a problem hiding this comment.
The method seem to be copy of the one below. I vote for keeping the one below.
| bool filter_limit_negative_; | ||
|
|
||
| /** Cartesian origin of the spherical coordinate system. */ | ||
| Eigen::Vector4f filter_origin_; |
There was a problem hiding this comment.
Since you store and eigen vectorizable type internally in your class you need to define the Eigen macro for overloading the new operator.
There was a problem hiding this comment.
That was already done in the base class Filter
There was a problem hiding this comment.
Are you sure that's enough? I never looked into the internals of the macro to understand what exactly it does.
There was a problem hiding this comment.
There was a problem hiding this comment.
Edit: Sorry for the bad copy paste.
-
I'm not particularly fond with the whole sorting mechanism to "pseudo cluster" indices. I believe you should use a vector/
mapof vectors of indices to cluster things. Sorting is aN*log(N)which in this case can be avoided. Also, since the downsampling is done based on averaging, once you build your "spherical voxel" grid you can literally do everything in a single pass, because you can compute an average in an online fashion. @PointCloudLibrary/admins opinions? -
We specify angular resolution in terms of divisions, but I feel that in a normal case I would want to specify the vertical and horizontal resolution in terms of angles. Working in divisions ensures that all voxels span the same vertical and horizontal angles uniformly, which is something we can't ensure if a user would suggest something like 100º for the vertical resolution since it's not a divisor of 180º. Therefore it would be cool to have the option of "suggesting" a resolution specifying angles and then we would "round" that suggestion ensuring we have uniform angular intervals.
-
I believe we should further exploit the possibility of having a common base class, which serves as a basis for all other VoxelGrid classes? These parameters are common to both
VoxelGridandSpherical:min_points_per_voxel_,filter_field_name_,filter_limit_min_,filter_limit_max_,filter_limit_negative_. The point is, there are bounding box/filter functionalities that should be common to some extent to all voxel grid filters and I feel those should be shared if possible. I'm unsure at this point if the best option if to create a new base class or refactor/virtualize some of theVoxelGridmethods. Request for comments @PointCloudLibrary/admins . -
Regarding the unit tests, please have a look at Test Fixture and if you want to have a look at a comprehensive example implemented into PCL the octree iterator test is the way to go.
-
I suggested a few refactoring actions in the
applyFiltermethod in hopes of improving its readability. I've personally been trying this rule of thumb of making sure no single function spans longer than a full screen for a while now and it has served my well. This assumes of course there are no major penalties for doing so. I don't believe there are any in the cases I suggested. Let's leave those changes to the very last since the code can still change considerably in the meantime. -
Nice to have: every time you're defining local variables, declare them
constwhenever they're actuallyconst.
|
|
||
| project(spherical_voxel_grid) | ||
|
|
||
| find_package(PCL 1.8.1.99 REQUIRED) |
There was a problem hiding this comment.
This is going for 1.9.0 so you can change it already.
There was a problem hiding this comment.
Even though we know this will be 1.9.0, such version does not exist yet, current master compiles into 1.8.1.99. If we already request 1.9.0, it will be impossible to build this tutorial.
| -------- | ||
|
|
||
| First, download the dataset `spherical_voxel_grid_example.pcd | ||
| <https://raw.github.com/PointCloudLibrary/data/master/tutorials/spherical_voxel_grid_example.pcd>`_ |
There was a problem hiding this comment.
I don't see this file in this PR.
There was a problem hiding this comment.
Note that the file is in the data repository. Still, it's missing, you are right.
There was a problem hiding this comment.
Meaning we're gonna need a PR there adding the file. @sddavis14 please then add a link to the PR here for traceability purposes.
|
|
||
| #define PCL_INSTANTIATE_SphericalVoxelGrid(T) template class PCL_EXPORTS pcl::SphericalVoxelGrid<T>; | ||
|
|
||
| #endif // PCL_FILTERS_SPHERICAL_VOXEL_GRID_IMPL_H_ No newline at end of file |
There was a problem hiding this comment.
New line at the end of the file.
| * itself well to reducing the noise and modifying the resolutions of point clouds | ||
| * generated by scanners which sample in a spherical manner. | ||
| * | ||
| * \author Spencer Davis, Radu B. Rusu, Bastian Steder |
There was a problem hiding this comment.
You can leave it. It was more a curiosity remark than an actual rectification comment.
| /** \brief Set to true if we want to return the data outside (\a filter_limit_min_;\a filter_limit_max_). Default: false. */ | ||
| bool filter_limit_negative_; | ||
|
|
||
| /** Cartesian origin of the spherical coordinate system. */ |
There was a problem hiding this comment.
I don't think it's necessary because we have JAVADOC_AUTOBRIEF=ON.
| EXPECT_NEAR(output_cloud->points[3].intensity, 15, 0.001); | ||
|
|
||
|
|
||
| /* Test origin movement */ |
There was a problem hiding this comment.
This needs to go to a different test.
| EXPECT_NEAR(output_cloud->points[3].intensity, 15, 0.001); | ||
|
|
||
|
|
||
| /* Test data filter */ |
There was a problem hiding this comment.
This needs to go to a different test.
| EXPECT_NEAR(output_cloud->points[3].intensity, 20, 0.001); | ||
|
|
||
|
|
||
| /* Test data filter negative */ |
There was a problem hiding this comment.
This needs to go to a different unit test.
| EXPECT_NEAR(output_cloud->points[3].g, 35, 0.001); | ||
| EXPECT_NEAR(output_cloud->points[3].b, 45, 0.001); | ||
|
|
||
| voxel.setInputCloud(input_cloudrgb); |
There was a problem hiding this comment.
This needs to go to a different unit test.
| EXPECT_NEAR(output_cloud->points[3].b, 45, 0.001); | ||
| EXPECT_NEAR(output_cloud->points[3].a, 55, 0.001); | ||
|
|
||
| voxel.setInputCloud(input_cloudrgba); |
There was a problem hiding this comment.
This needs to go to a different unit test.
Using PCL is a research-grade library. I don't think we can require our contributors to optimize the heck out of the code they submit. If performance of
The API can be extended in future. We can not (and should not) catch all use-cases and "what if"s upfront.
I agree this would be great. However, there was some reason @sddavis14 did not do this, right? |
|
A couple of thoughts based on everyone's comments: The original intent of this PR was to develop this functionality separately from the original My original assumption was that we would not want to modify the original VoxelGrid source. However, if we are open to refactoring things using a My question then becomes: How much do we want to modify the original We've identified several problems common to both the
Should we limit our changes to things that are internal to the functionality of the filter, or are we OK with dropping some of the extraneous functionality? Is it worth the performance uncertainty to switch to a |
That is true. The vector of vectors will still provide some warranties though: we have inexpensive indexing, linear time for traversal and there's no insertion per se, it can be initialized in one go. Even if we start N points being N somewhere in 10^6 ~ 9 orders I would assume the typical voxelization strategies aim to keep things around 10^4 ~ 5 output points which should be fairly tractable and better than the current N*log(N) sort.
Asking is "free of charge". If the user is willing to go through the trouble of investing his own time in doing so, that's something else. But I still believe that if we know that something can be done better in some sense, we should put those ideas on the table. But I understand your point: having an actual need for a performant implementation will always drive/motivate to go through those changes.
I'm very pro towards this and I cannot thank you enough for the help you're willing to provide.
I would deprecate voxel grid of these functionalities and hint the user to use the
That is indeed true but I still believe the vector of vectors to store the "clusters" might provide us some performance benefits.
Some additional insights (not fully sure about some):
So at first sight for all voxel filters, there's a stage on how to voxelize/partition space and then the actual filtering/processing stage. I believe we should highlight this in the class design. |
|
Agree with everything. Considering your insights 2 and 3, what if instead of |
|
Opened a new "discussion space" in #2211, to avoid going too far out of topic in this PR. In the meantime @sddavis14, just let us know if you feel like embarking on this journey. Your contribution is already valuable but if you feel like lending us an extra hand, your help is definitely welcome. |
|
@SergioRAgostinho Yes, I would like to be involved. This seems like a longer-term project. Are we planning to include this whole change in PCL release 1.9.0? If so, it seems like I should abandon the current implementation of As another note, I've done some very quick benchmarks on my machine to compare the Sort method Map Method Sort method Map Method As you can see, the map method is slower when there are a large number of voxels compared to the input point cloud size. As the number of voxels is decreased, the map method eventually becomes faster. The code is also significantly simpler with the map. I haven't had time to try this with a vector of vectors because that implementation is a little more complex. Do you all think the map method is worth investigating further? |
I would say that it's not a good idea to block 1.9.0 until this is done so I would move it to the next milestone. We went on a somewhat similar (and yet much lighter) effort with the octree module and that one started in
Thanks for that. Just as quick sanity check reminder don't forget to compile with optimizations enabled. Given those results I would say that the map of vectors is to be dropped. I believe most people voxelize to stay in the range between 10^4-6 points and from your numbers the map is falling short on the transition from 10^4 to 10^5 already. I'm really curious on the vector of vectors performance now. Edit: Also, assuming your computer can handle the stress, it might be worth to try on the limits I mentioned in my last comments Case 1Nr of Input Points: 10⁷ Case 2Nr of Input Points: 10⁹ Case 3Nr of Input Points: 10⁷ Case 4Nr of Input Points: 10⁹ |
Would you be able to do the comparison again using std::unordered_map instead of std::map? std::map is a sorted version that internally uses red-black trees. So insertion requires log(N), which you notice in your experiments with larger N. In comparison search, insertion and removal in std::unordered_map have average constant-time complexity. Is there anywhere I could have a look at the std::map code? |
|
Nice to have: Option to use a log-polar mapping instead of a equidistant polar mapping. |
|
@Duffycola unordered_map is definitely a better solution. However, it requires C++11. Not sure what the current integration timeline for C++11/14 support is @SergioRAgostinho. I've finally had some time to revisit this issue over the past few days and I've actually built a custom version that uses a hash table. Experimentally, this improves performance for most leaf size/point cloud size combinations. I will try compiling the library with C++11 and just using unordered_map to see how that does vs my (presumably naive, but no C++11) hash table implementation. I can post the In the linked issue discussion (below) @jefft255 mentions that support for smaller leaf sizes is important in some applications. I think solving this particular problem should be another priority of our changes. As discussed in the issue, the goal is to support any style of mapping. Any help with the class design to accomplish this effectively would definitely be appreciated. It probably makes sense to continue this discussion in the rewrite issue: #2211 |
Feel free to use C++11/14 features in your code. Although we don't have fixed dates, I'm positive that by the time your code is ready to merge, the library will require C++14. |
I see.. I think one could also use a std::vector assuming it's ok to compute the min/max dimensions of the cloud before allocating a fixed-size vector. I will pass some comments in the other thread... |
|
This pull request has been automatically marked as stale because it hasn't had Come back whenever you have time. We look forward to your contribution. |
This filter adds the functionality discussed in issue #2044
Included in this pull request is:
As suggested in the issue, I was able to reuse a significant amount of code from the standard VoxelGrid filter. As such, I maintained the old copyright notices and authors with the addition of my name.
It may help to reference the original VoxelGrid source while reviewing this PR.
Thanks.
Maintainer Edit: Closes #2044