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

Add new mls projection method. Deprecated MovingLeastSquares::setPolynomialFit(). #1960

Merged

Conversation

Levi-Armstrong
Copy link
Contributor

This PR adds a new projection method which projects the point orthogonal to the mls surface. This produces a better reconstruction in areas of high curvature when using a polynomial fit. There are several free function added that could be used by other mls techniques and meshing algorithms.

Note: This is builds on PR #1952 which has not been merged but I wanted to get this submitted to start discussions on the new features added.

@Levi-Armstrong
Copy link
Contributor Author

Any preferences on the structure (free functions)?

@taketwo
Copy link
Member

taketwo commented Aug 11, 2017

Hi, I'm away all next week, so no inputs from my side till next next week.

@UnaNancyOwen
Copy link
Member

UnaNancyOwen commented Aug 12, 2017

I'm sorry, it is a click mistake. :'(

@SergioRAgostinho
Copy link
Member

Sorry for the delay guys. I'll be making a push on things until the end of the week.

Copy link
Member

@SergioRAgostinho SergioRAgostinho left a comment

Choose a reason for hiding this comment

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

Hey @Levi-Armstrong. I'm sorry to ask for this, but I think I'll need some help to understand all changes you did. I can see you added a projection method, but you also moved a lot of code around, and it's not being easy for me to understand all changes you did.

Can you provide me with a bullet list with more details? I'm especially concerned with the refactoring process you did.

I'm also unsure about the free functions and constants but I first I need to get a better feel for the PR first.

Apologies once again.


namespace pcl
{

const double MLS_PROJECTION_CONVERGENCE_TOLERANCE = 1e-8;
const double MLS_MINIMUM_PRINCIPLE_CURVATURE = 1e-5;
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure about having this here...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where would you like them? I could also do away with them and put the values in there respected places.

Copy link
Member

Choose a reason for hiding this comment

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

I'd say so. They both are implementation details and each is used only in a single place. Just move these const definitions into their respective functions, nobody will miss them in the main header.

Eigen::Vector3d mean; /**< \brief The mean point of all the neighbors. */
Eigen::Vector3d plane_normal; /**< \brief The normal of the local plane of the query point. */
Eigen::Vector3d u_axis; /**< \brief The axis corresponding to the u-coordinates of the local plane of the query point. */
Eigen::Vector3d v_axis; /**< \brief The axis corresponding to the v-coordinates of the local plane of the query point. */
Eigen::VectorXd c_vec; /**< \brief The polynomial coefficients Example: z = c_vec[0] + c_vec[1]*v + c_vec[2]*v^2 + c_vec[3]*u + c_vec[4]*u*v + c_vec[5]*u^2 */
int num_neighbors; /**< \brief The number of neighbors used to create the mls surface. */
float curvature; /**< \brief The curvature at the query point. */
int order; /**< \brief The order of the polynomial used if polynomial_fit = true */
bool polynomial_fit; /**< \brief If True, a polynomial surface was fitted to the neighbors as the mls surface */
Copy link
Member

Choose a reason for hiding this comment

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

I think would use a negative order value to indicate no polynomial_fit was done.

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 like this. I can make it so zero indicates no polynomial fit.

Copy link
Member

Choose a reason for hiding this comment

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

Then consider this variable to be unsigned and probably you don't need 32bits to represent the order of your polynomial.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with the unsigned recommendation, it expresses the intent about the expected valid range and semantics better. However, I don't think that using a smaller int will add anything to the picture and make the code more expressive. From the storage viewpoint it will probably also make no difference due to alignment.

That being said, I wonder if this variable is needed at all. Isn't it the same as c_vec.length()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only issue I see with using c_vec.length() is you can get a length of zero if the fit failed.

Copy link
Member

Choose a reason for hiding this comment

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

Ok

@Levi-Armstrong
Copy link
Contributor Author

Hey @Levi-Armstrong. I'm sorry to ask for this, but I think I'll need some help to understand all changes you did. I can see you added a projection method, but you also moved a lot of code around, and it's not being easy for me to understand all changes you did.

Can you provide me with a bullet list with more details? I'm especially concerned with the refactoring process you did.

Sorry, I was on travel all last week. As for the free function, I went back and forth on this on whether to make them free function or member function. The main advantage is if they are free functions I can easily write tests for these individual function, but I am not opposed to moving them to member function if this is preferred.

In short most of the refactoring was to reduce code duplication and make it easier to read given the addition of different projection methods.

  • Original there was some duplication of code with in the existing implementation for example add data to the point cloud here. So to prevent from duplicating code I created this helper function.
  • Additional items like calculating the partial derivatives was written inline, but with the addition of a new projection method it was moved to an independent function since it will be used several places to prevent duplicating code.
  • Also moved the currently projections method (Simple Projection) to its own function given the addition of a new projection method for readability.
  • Also moved the code for computing the mls surface to its own code for readability and given that there are various techniques for computing this via custom weighting functions it would make it simpler in the future if such techniques wanted to made available.

@@ -474,7 +626,7 @@ namespace pcl
}

/** \brief Smooth a given point and its neighborghood using Moving Least Squares.
* \param[in] index the inex of the query point in the input cloud
* \param[in] index the index of the query point in the input cloud
* \param[in] nn_indices the set of nearest neighbors indices for pt
* \param[in] nn_sqr_dists the set of nearest neighbors squared distances for pt
Copy link
Member

Choose a reason for hiding this comment

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

This param is gone

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 will remove it.

{
}

void pcl::getMLSCoordinates (const Eigen::Vector3d &pt, const pcl::MLSResult &mls_result, double &u, double &v, double &w)
Copy link
Member

Choose a reason for hiding this comment

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

Return type should be on a separate line

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 will make the change.

Copy link
Member

Choose a reason for hiding this comment

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

The comments applies to a number of methods below.

P_weight_Pt.llt ().solveInPlace (c_vec);
}

if (cache_mls_results_)
Copy link
Member

Choose a reason for hiding this comment

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

Do I understand right that caching became unconditional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is conditional. Since the the vector gets resized I just fill out the object when it is passed to the function instead of constructing a new object within the computeMLSPointNormal method.

Location 1

Location 2

Copy link
Member

Choose a reason for hiding this comment

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

Ah, right. Thanks for clarification.

@taketwo taketwo added needs: code review Specify why not closed/merged yet needs: author reply Specify why not closed/merged yet labels Aug 30, 2017
@SergioRAgostinho
Copy link
Member

Thanks for the explanation. I will try to make the best out of it 😅

Sorry, I was on travel all last week. As for the free function, I went back and forth on this on whether to make them free function or member function. The main advantage is if they are free functions I can easily write tests for these individual function, but I am not opposed to moving them to member function if this is preferred.

In general, you should not bend method visibility in order to be able write unit tests. Writing the tests comes after you've decided what makes sense to be public in your API.

In short most of the refactoring was to reduce code duplication and make it easier to read given the addition of different projection methods.

  • Original there was some duplication of code with in the existing implementation for example add data to the point cloud here. So to prevent from duplicating code I created this helper function.
  • Additional items like calculating the partial derivatives was written inline, but with the addition of a new projection method it was moved to an independent function since it will be used several places to prevent duplicating code.
  • Also moved the currently projections method (Simple Projection) to its own function given the addition of a new projection method for readability.
  • Also moved the code for computing the mls surface to its own code for readability and given that there are various techniques for computing this via custom weighting functions it would make it simpler in the future if such techniques wanted to made available.

For future PRs, please split logic changes like the ones you described into multiple commits. It makes it easier to review.

Copy link
Member

@SergioRAgostinho SergioRAgostinho left a comment

Choose a reason for hiding this comment

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

I'm still lacking insight into this, but overall I feel this code is messy. I also don't like that the default precision is double for almost everything.

I aknowledge that my comments are more directed towards the original implementation and not necessarily this PR :/

Eigen::Vector3d mean; /**< \brief The mean point of all the neighbors. */
Eigen::Vector3d plane_normal; /**< \brief The normal of the local plane of the query point. */
Eigen::Vector3d u_axis; /**< \brief The axis corresponding to the u-coordinates of the local plane of the query point. */
Eigen::Vector3d v_axis; /**< \brief The axis corresponding to the v-coordinates of the local plane of the query point. */
Eigen::VectorXd c_vec; /**< \brief The polynomial coefficients Example: z = c_vec[0] + c_vec[1]*v + c_vec[2]*v^2 + c_vec[3]*u + c_vec[4]*u*v + c_vec[5]*u^2 */
int num_neighbors; /**< \brief The number of neighbors used to create the mls surface. */
float curvature; /**< \brief The curvature at the query point. */
int order; /**< \brief The order of the polynomial used if polynomial_fit = true */
bool polynomial_fit; /**< \brief If True, a polynomial surface was fitted to the neighbors as the mls surface */
Copy link
Member

Choose a reason for hiding this comment

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

Then consider this variable to be unsigned and probably you don't need 32bits to represent the order of your polynomial.

double z_v; /**< @brief The partial derivative dz/dv. */
double z_uu; /**< @brief The partial derivative d^2z/du^2. */
double z_vv; /**< @brief The partial derivative d^2z/dv^2. */
double z_uv; /**< @brief The partial derivative d^2z/dudv. */
Copy link
Member

Choose a reason for hiding this comment

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

This comment is applicable for the rest. You use double precision by default. Is this really required?

* \param[out] v The v-coordinate of the point in local MLS frame.
* \param[out] w The w-coordinate of the point in local MLS frame.
*/
void getMLSCoordinates (const Eigen::Vector3d &pt, const pcl::MLSResult &mls_result, double &u, double &v, double &w);
Copy link
Member

Choose a reason for hiding this comment

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

This and a number of methods which take a reference to a const MLSResult feel like they should be methods of MLSResult.

* \param[out] u The u-coordinate of the point in local MLS frame.
* \param[out] v The v-coordinate of the point in local MLS frame.
*/
void getMLSCoordinates (const Eigen::Vector3d &pt, const pcl::MLSResult &mls_result, double &u, double &v);
Copy link
Member

Choose a reason for hiding this comment

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

Same as above

* \param[in] mls_result The MLS results data
* \return The polynomial value at the provide uv coordinates.
*/
double getPolynomialValue (const double u, const double v, const pcl::MLSResult &mls_result);
Copy link
Member

Choose a reason for hiding this comment

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

Suggest move to method.

double F2u = d.z_uv * gw + d.z_v * d.z_u - d.z_uv * w;
double F2v = 1 + d.z_vv * gw + d.z_v * d.z_v - d.z_vv * w;

Eigen::MatrixXd J (2, 2);
Copy link
Member

Choose a reason for hiding this comment

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

This matrix has known size at compile time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes.

Copy link
Member

Choose a reason for hiding this comment

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

I think the idea behind Sergio's message was that J can be changed to a fixed-sized Eigen type, which will (maybe) make the following matrix inverse computation faster by using a specialized 2x2 implementation (compared to generic size implementation).

Actually, I would expect Eigen to be smart enough to perform runtime dispatching to specialized implementations, but we'd need to dig into the sources to find this out. So the easiest way to ensure we are as efficient as possible is to just switch to Eigen::Matrix2d.

Copy link
Member

Choose a reason for hiding this comment

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

I think the idea behind Sergio's message was that J can be changed to a fixed-sized Eigen type, which will (maybe) make the following matrix inverse computation faster by using a specialized 2x2 implementation (compared to generic size implementation).

It was this exactly. Sorry for being unclear.

J(1, 1) = F2v;

Eigen::Vector2d err (e1, e2);
Eigen::MatrixXd update = J.inverse () * err;
Copy link
Member

Choose a reason for hiding this comment

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

And this one as well it's 2x1

Eigen::Vector4d model_coefficients;
pcl::eigen33 (covariance_matrix, eigen_value, eigen_vector);
model_coefficients.head<3> ().matrix () = eigen_vector;
model_coefficients[3] = 0;
Copy link
Member

Choose a reason for hiding this comment

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

Use the constructor instead

mls_result.curvature = static_cast<float> (covariance_matrix.trace ());
// Compute the curvature surface change
if (mls_result.curvature != 0)
mls_result.curvature = fabsf (float (eigen_value / double (mls_result.curvature)));
Copy link
Member

Choose a reason for hiding this comment

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

Why is the curvature casted to double?

Copy link
Member

Choose a reason for hiding this comment

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

Also, please always use std::abs et al., we had problems with MSVC if I remember correctly.

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 believe that is how it was in the original implementation.

Copy link
Member

@taketwo taketwo Sep 12, 2017

Choose a reason for hiding this comment

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

Nevertheless. PCL is written C++ and we should always prefer C++ library functions over their C-style counterparts. Besides, as I wrote, we occasionally get bug reports about MSVC choking at some of these functions (e.g. #848, #1548).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, my comment was in reference to the cast to double.

for (size_t ni = 0; ni < mls_result.num_neighbors; ++ni)
{
de_meaned[ni][0] = cloud.points[nn_indices[ni]].x - mls_result.mean[0];
de_meaned[ni][1] = cloud.points[nn_indices[ni]].y - mls_result.mean[1];
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure vectorization is properly exploited here. You should use the vector maps for this operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I don't follow. Can you provide more detail?

@Levi-Armstrong
Copy link
Contributor Author

I will work on the requested changes today.

@Levi-Armstrong
Copy link
Contributor Author

I'm still lacking insight into this, but overall I feel this code is messy. I also don't like that the default precision is double for almost everything.

I aknowledge that my comments are more directed towards the original implementation and not necessarily this PR :/

I am happy to change things from the original implementation, just let me know what it is if it has not already been mentioned above.

@Levi-Armstrong
Copy link
Contributor Author

For the mls class should the setPolynomialFit be removed and just use the order for determining if a polynomial fit should be used like the request in the MLSResults structure? The reason I ask is with the change to MLSResults I had to add logic to the setPolynomialFit and SetPolynomialOrder to insure order get set properly since the order is the only parameter used within the MLSResults to determine if a polynomial fit was used.

@Levi-Armstrong
Copy link
Contributor Author

@SergioRAgostinho and @taketwo I have made most of the requested changes except a few which I had questions about.

@Levi-Armstrong Levi-Armstrong force-pushed the addNewMLSProjection branch 2 times, most recently from 0fa5ea3 to 07a5533 Compare September 14, 2017 12:22
@Levi-Armstrong
Copy link
Contributor Author

@taketwo and @SergioRAgostinho I removed the mls variable polynomial_fit_ and fix style guide related issues. Should I fix item internal to pcl that use the setPolynomialFit (MLS Test and Command Line Tool)?

@taketwo
Copy link
Member

taketwo commented Sep 19, 2017

Should I fix item internal to pcl that use the setPolynomialFit (MLS Test and Command Line Tool)?

You mean to remove usage of depreciated functions within the library? Definitely!

@Levi-Armstrong
Copy link
Contributor Author

OK, The use of setPolynomialFit has been removed.

@SergioRAgostinho
Copy link
Member

SergioRAgostinho commented Sep 19, 2017 via email

Copy link
Member

@taketwo taketwo left a comment

Choose a reason for hiding this comment

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

LGTM

@taketwo taketwo removed needs: code review Specify why not closed/merged yet needs: author reply Specify why not closed/merged yet labels Sep 20, 2017
@taketwo taketwo added this to the pcl-1.9.0 milestone Sep 20, 2017
Copy link
Member

@SergioRAgostinho SergioRAgostinho left a comment

Choose a reason for hiding this comment

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

I'm just waiting for the tools build job to complete (since it pertains this PR) and then I'll proceed with the merge.

@SergioRAgostinho SergioRAgostinho merged commit 079cf24 into PointCloudLibrary:master Sep 21, 2017
@SergioRAgostinho
Copy link
Member

FYI, our tools job is exceeding the build time. It needs some intervention.

@taketwo
Copy link
Member

taketwo commented Aug 26, 2018

This qualifies for new feature, doesn't it?

@SergioRAgostinho
Copy link
Member

Probably. I'll add the label.

@SergioRAgostinho SergioRAgostinho added the changelog: new feature Meta-information for changelog generation label Aug 26, 2018
taketwo added a commit to taketwo/pcl that referenced this pull request Sep 9, 2018
PR PointCloudLibrary#1960 deprecated `setPolynomialFit()` but did not update the
openni_mls_smoothing app.
taketwo added a commit to taketwo/pcl that referenced this pull request Sep 9, 2018
PR PointCloudLibrary#1960 deprecated `setPolynomialFit()` but did not update the
openni_mls_smoothing app.
@SergioRAgostinho SergioRAgostinho added the changelog: deprecation Meta-information for changelog generation label Sep 25, 2018
@SergioRAgostinho SergioRAgostinho changed the title Add new mls projection method Add new mls projection method. Deprecated MovingLeastSquares::setPolynomialFit(). Sep 25, 2018
@keineahnung2345
Copy link
Contributor

@Levi-Armstrong Hi, I am reading the code of projectPointOrthogonalToPolynomialSurface and there is a reference link in the comment. From the code, it looks like an iterative algorithm, and also Jacobian matrix is used, but I cannot find those things in the reference link. Could you please explain how the function projectPointOrthogonalToPolynomialSurface and the reference link are related? Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog: API break Meta-information for changelog generation changelog: deprecation Meta-information for changelog generation changelog: new feature Meta-information for changelog generation module: surface module: tools
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants