Open KotaYonezawa opened 2 years ago
I find it difficult to image how this problem would occur in practice. It sounds like it could theoretically happen, but I am not sure which inputs specifically would lead to it. Can you provide an example (clouds and guessed transform) where NDT fails to give the correct solution due to the gimbal lock? Did you experience this already?
Thanks for your comment! > mvieth-san. Yes, I faced this issue on my project. To explain it, I created simple example based on following data. (based on my previous post) https://github.com/PointCloudLibrary/pcl/issues/5056#issuecomment-988864151
Red is target cloud, and green is source cloud. These point clouds are created from same mesh object, but for source data, shift 1m to X and Y direction, and rotate 10 degree around Z axis, and after that, rotated 90 degrees around Y axis. And I set -90 degrees rotation around Y axis for init guess. (Note that it's gimbal lock condition for eular angles used in NDT algo.)
I wrote simple code based on NDT tutorial.
// Loading first scan of room.
pcl::PointCloud<pcl::PointXYZ>::Ptr target_cloud (new pcl::PointCloud<pcl::PointXYZ>);
if (pcl::io::loadPCDFile<pcl::PointXYZ> ("bunny1.pcd", *target_cloud) == -1)
{
PCL_ERROR ("Couldn't read file bunny1.pcd \n");
return (-1);
}
std::cout << "Loaded " << target_cloud->size () << " data points from bunny1.pcd" << std::endl;
// Loading second scan of room from new perspective.
pcl::PointCloud<pcl::PointXYZ>::Ptr input_cloud (new pcl::PointCloud<pcl::PointXYZ>);
if (pcl::io::loadPCDFile<pcl::PointXYZ> ("bunny2.pcd", *input_cloud) == -1)
{
PCL_ERROR ("Couldn't read file bunny2.pcd \n");
return (-1);
}
std::cout << "Loaded " << input_cloud->size () << " data points from bunny2.pcd" << std::endl;
// Filtering input scan to roughly 10% of original size to increase speed of registration.
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud (new pcl::PointCloud<pcl::PointXYZ>);
pcl::ApproximateVoxelGrid<pcl::PointXYZ> approximate_voxel_filter;
approximate_voxel_filter.setLeafSize (0.2, 0.2, 0.2);
approximate_voxel_filter.setInputCloud (input_cloud);
approximate_voxel_filter.filter (*filtered_cloud);
std::cout << "Filtered cloud contains " << filtered_cloud->size ()
<< " data points from bunny2.pcd" << std::endl;
// Initializing Normal Distributions Transform (NDT).
pcl::NormalDistributionsTransform<pcl::PointXYZ, pcl::PointXYZ> ndt;
// Setting scale dependent NDT parameters
// Setting minimum transformation difference for termination condition.
ndt.setTransformationEpsilon (0.000001);
// Setting maximum step size for More-Thuente line search.
ndt.setStepSize (1.0);
//Setting Resolution of NDT grid structure (VoxelGridCovariance).
ndt.setResolution (1.0);
// Setting max number of registration iterations.
ndt.setMaximumIterations (35);
// Outlier ratio.
ndt.setOulierRatio (0.55);
// Setting point cloud to be aligned.
ndt.setInputSource (filtered_cloud);
// Setting point cloud to be aligned to.
ndt.setInputTarget (target_cloud);
// Set initial alignment estimate found using robot odometry.
Eigen::AngleAxisf init_rotation (-90 * M_PI / 180.0, Eigen::Vector3f::UnitY ());
Eigen::Translation3f init_translation (0, 0, 0);
Eigen::Matrix4f init_guess = (init_translation * init_rotation).matrix ();
// Calculating required rigid transform to align the input cloud to the target cloud.
pcl::PointCloud<pcl::PointXYZ>::Ptr output_cloud (new pcl::PointCloud<pcl::PointXYZ>);
ndt.align (*output_cloud, init_guess);
std::cout << "Normal Distributions Transform has converged:" << ndt.hasConverged ()
<< " score: " << ndt.getFitnessScore () << std::endl;
// Transforming unfiltered, input cloud using found transform.
pcl::transformPointCloud (*input_cloud, *output_cloud, ndt.getFinalTransformation ());
// Saving transformed input cloud.
pcl::io::savePCDFileASCII ("bunny2_transformed.pcd", *output_cloud);
Without my wrapper, NDT matching was not OK like as follows.
After adding my wrapper (overloading align() function), NDT matching was OK like as follows.
Point cloud data used in this test is as follows. bunny_pcddata.zip
I additionally checked 20, 40, 60, 80 degrees around Y axis cases. (Of course, init guess was also changed for each angles.)
// Set initial alignment estimate found using robot odometry.
Eigen::AngleAxisf init_rotation (-80 * M_PI / 180.0, Eigen::Vector3f::UnitY ());
...
The result was as follows. Only 90 degrees case (without modification) was failed.
rotation angles around Y axis | Matching result (without modification) | Matching result (with align() overload) |
---|---|---|
20 degrees | OK | OK |
40 degrees | OK | OK |
60 degrees | OK | OK |
80 degrees | OK | OK |
90 degrees | NG | OK |
Sorry for the delay, and thank you for your investigations. I am still looking into the problem, but I am not so familiar with NDT, so it is taking some time. I am trying to figure out where exactly inside ndt.hpp the problem with the gimbal lock happens, I suspect in computeAngleDerivatives
. Your suggestion with the align-wrapper could be a workaround, but, as you said, ideally we would find a solution so that a wrapper is not necessary. Unfortunately, we have to keep using Euler angles and can't switch to another rotation representation like Quaternions -- that would be too much work and would probably require another doctoral thesis :smile:
Describe the bug
Thanks a lot for great and useful library.
I found that NormalDistributionsTransform::align may not work correctly by gimbal lock. It's depend on user specified "guess" argument. (the initial gross estimation of the transformation)
Context
In NormalDistributionsTransform::computeTransformation() function in ndt.hpp, user specified guess is converted to eular angles.
So, if the user specified "guess" includes rotation near to gimbal lock condition, the NDT algorithm can work incorrectly by gimbal lock.
I checked Dr. M.Magnusson's paper, in 6.2.2.
In the following, 3D Euler angles will be used, in spite of the potential problems associated with this rotation representation. The advantages — no constraint required for the numerical optimisation procedure, and slightly less complicated derivatives — are assessed to outweigh the risk of gimbal lock, which would only occur at such large angles that the local registration procedure would most likely fail anyway.
I think this part means that eular angles should be used to specify rotation from guessed pose to converged pose. But in current implementation, eular angles specifies rotation from current input cloud pose to converged pose.
So, in current implementation, the user should take care about guess to avoid gimbal lock. But many users will not understand it, and may face unexpected behavior by this issue. Some of user will be very difficult to solve it, I supposed.
Now I implemented following "wrapper" function to avoid gimbal lock regardless of guess. (Overloading align function.)
But I think it's temporal, not final solution. If possible, it's better to fix in ndt.hpp algorithm internally... What do you think?
Expected behavior
I think the user should be able to specify "guess" argument without any restriction.
Your Environment (please complete the following information):
OS: Windows10 Compiler: Compiler: Visual Studio 2019 Build Tools PCL Version: HEAD
Possible Solution
As above, I implemented wrapper solution as temporal. But I think it's better to fix in algorithm internal implementation in ndt.hpp if possible.