/* * Software License Agreement (BSD License) * * Point Cloud Library (PCL) - www.pointclouds.org * Copyright (c) 2011-2012, Willow Garage, Inc. * Copyright (c) 2012-, Open Perception, Inc. * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the copyright holder(s) nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * $Id$ * */ #ifndef PCL_NDT_2D_IMPL_H_ #define PCL_NDT_2D_IMPL_H_ #include // for boost::noncopyable #include // for SelfAdjointEigenSolver, EigenSolver #include #include namespace pcl { namespace ndt2d { /** \brief Class to store vector value and first and second derivatives * (grad vector and hessian matrix), so they can be returned easily from * functions */ template struct ValueAndDerivatives { ValueAndDerivatives() : hessian(), grad(), value() {} Eigen::Matrix hessian; Eigen::Matrix grad; T value; static ValueAndDerivatives Zero() { ValueAndDerivatives r; r.hessian = Eigen::Matrix::Zero(); r.grad = Eigen::Matrix::Zero(); r.value = 0; return r; } ValueAndDerivatives& operator+=(ValueAndDerivatives const& r) { hessian += r.hessian; grad += r.grad; value += r.value; return *this; } }; /** \brief A normal distribution estimation class. * * First the indices of of the points from a point cloud that should be * modelled by the distribution are added with addIdx (...). * * Then estimateParams (...) uses the stored point indices to estimate the * parameters of a normal distribution, and discards the stored indices. * * Finally the distriubution, and its derivatives, may be evaluated at any * point using test (...). */ template class NormalDist { using PointCloud = pcl::PointCloud; public: NormalDist() = default; /** \brief Store a point index to use later for estimating distribution parameters. * \param[in] i Point index to store */ void addIdx(std::size_t i) { pt_indices_.push_back(i); } /** \brief Estimate the normal distribution parameters given the point indices * provided. Memory of point indices is cleared. \param[in] cloud Point cloud * corresponding to indices passed to addIdx. \param[in] min_covar_eigvalue_mult Set * the smallest eigenvalue to this times the largest. */ void estimateParams(const PointCloud& cloud, double min_covar_eigvalue_mult = 0.001) { Eigen::Vector2d sx = Eigen::Vector2d::Zero(); Eigen::Matrix2d sxx = Eigen::Matrix2d::Zero(); for (const auto& pt_index : pt_indices_) { Eigen::Vector2d p(cloud[pt_index].x, cloud[pt_index].y); sx += p; sxx += p * p.transpose(); } n_ = pt_indices_.size(); if (n_ >= min_n_) { mean_ = sx / static_cast(n_); // Using maximum likelihood estimation as in the original paper Eigen::Matrix2d covar = (sxx - 2 * (sx * mean_.transpose())) / static_cast(n_) + mean_ * mean_.transpose(); Eigen::SelfAdjointEigenSolver solver(covar); if (solver.eigenvalues()[0] < min_covar_eigvalue_mult * solver.eigenvalues()[1]) { PCL_DEBUG("[pcl::NormalDist::estimateParams] NDT normal fit: adjusting " "eigenvalue %f\n", solver.eigenvalues()[0]); Eigen::Matrix2d l = solver.eigenvalues().asDiagonal(); Eigen::Matrix2d q = solver.eigenvectors(); // set minimum smallest eigenvalue: l(0, 0) = l(1, 1) * min_covar_eigvalue_mult; covar = q * l * q.transpose(); } covar_inv_ = covar.inverse(); } pt_indices_.clear(); } /** \brief Return the 'score' (denormalised likelihood) and derivatives of score of * the point p given this distribution. \param[in] transformed_pt Location to * evaluate at. \param[in] cos_theta sin(theta) of the current rotation angle * of rigid transformation: to avoid repeated evaluation \param[in] sin_theta * cos(theta) of the current rotation angle of rigid transformation: to avoid repeated * evaluation estimateParams must have been called after at least three points were * provided, or this will return zero. * */ ValueAndDerivatives<3, double> test(const PointT& transformed_pt, const double& cos_theta, const double& sin_theta) const { if (n_ < min_n_) return ValueAndDerivatives<3, double>::Zero(); ValueAndDerivatives<3, double> r; const double x = transformed_pt.x; const double y = transformed_pt.y; const Eigen::Vector2d p_xy(transformed_pt.x, transformed_pt.y); const Eigen::Vector2d q = p_xy - mean_; const Eigen::RowVector2d qt_cvi(q.transpose() * covar_inv_); const double exp_qt_cvi_q = std::exp(-0.5 * static_cast(qt_cvi * q)); r.value = -exp_qt_cvi_q; Eigen::Matrix jacobian; jacobian << 1, 0, -(x * sin_theta + y * cos_theta), 0, 1, x * cos_theta - y * sin_theta; for (std::size_t i = 0; i < 3; i++) r.grad[i] = static_cast(qt_cvi * jacobian.col(i)) * exp_qt_cvi_q; // second derivative only for i == j == 2: const Eigen::Vector2d d2q_didj(y * sin_theta - x * cos_theta, -(x * sin_theta + y * cos_theta)); for (std::size_t i = 0; i < 3; i++) for (std::size_t j = 0; j < 3; j++) r.hessian(i, j) = -exp_qt_cvi_q * (static_cast(-qt_cvi * jacobian.col(i)) * static_cast(-qt_cvi * jacobian.col(j)) + (-qt_cvi * ((i == 2 && j == 2) ? d2q_didj : Eigen::Vector2d::Zero())) + (-jacobian.col(j).transpose() * covar_inv_ * jacobian.col(i))); return r; } protected: const std::size_t min_n_{3}; std::size_t n_{0}; std::vector pt_indices_; Eigen::Vector2d mean_; Eigen::Matrix2d covar_inv_; }; /** \brief Build a set of normal distributions modelling a 2D point cloud, * and provide the value and derivatives of the model at any point via the * test (...) function. */ template class NDTSingleGrid : public boost::noncopyable { using PointCloud = pcl::PointCloud; using PointCloudConstPtr = typename PointCloud::ConstPtr; using NormalDist = pcl::ndt2d::NormalDist; public: NDTSingleGrid(PointCloudConstPtr cloud, const Eigen::Vector2f& about, const Eigen::Vector2f& extent, const Eigen::Vector2f& step) : min_(about - extent) , max_(min_ + 2 * extent) , step_(step) , cells_((max_[0] - min_[0]) / step_[0], (max_[1] - min_[1]) / step_[1]) , normal_distributions_(cells_[0], cells_[1]) { // sort through all points, assigning them to distributions: std::size_t used_points = 0; for (std::size_t i = 0; i < cloud->size(); i++) if (NormalDist* n = normalDistForPoint(cloud->at(i))) { n->addIdx(i); used_points++; } PCL_DEBUG("[pcl::NDTSingleGrid] NDT single grid %dx%d using %d/%d points\n", cells_[0], cells_[1], used_points, cloud->size()); // then bake the distributions such that they approximate the // points (and throw away memory of the points) for (int x = 0; x < cells_[0]; x++) for (int y = 0; y < cells_[1]; y++) normal_distributions_.coeffRef(x, y).estimateParams(*cloud); } /** \brief Return the 'score' (denormalised likelihood) and derivatives of score of * the point p given this distribution. \param[in] transformed_pt Location to * evaluate at. \param[in] cos_theta sin(theta) of the current rotation angle * of rigid transformation: to avoid repeated evaluation \param[in] sin_theta * cos(theta) of the current rotation angle of rigid transformation: to avoid repeated * evaluation */ ValueAndDerivatives<3, double> test(const PointT& transformed_pt, const double& cos_theta, const double& sin_theta) const { const NormalDist* n = normalDistForPoint(transformed_pt); // index is in grid, return score from the normal distribution from // the correct part of the grid: if (n) return n->test(transformed_pt, cos_theta, sin_theta); return ValueAndDerivatives<3, double>::Zero(); } protected: /** \brief Return the normal distribution covering the location of point p * \param[in] p a point */ NormalDist* normalDistForPoint(PointT const& p) const { // this would be neater in 3d... Eigen::Vector2f idxf; for (std::size_t i = 0; i < 2; i++) idxf[i] = (p.getVector3fMap()[i] - min_[i]) / step_[i]; Eigen::Vector2i idxi = idxf.cast(); for (std::size_t i = 0; i < 2; i++) if (idxi[i] >= cells_[i] || idxi[i] < 0) return nullptr; // const cast to avoid duplicating this function in const and // non-const variants... return const_cast(&normal_distributions_.coeffRef(idxi[0], idxi[1])); } Eigen::Vector2f min_; Eigen::Vector2f max_; Eigen::Vector2f step_; Eigen::Vector2i cells_; Eigen::Matrix normal_distributions_; }; /** \brief Build a Normal Distributions Transform of a 2D point cloud. This * consists of the sum of four overlapping models of the original points * with normal distributions. * The value and derivatives of the model at any point can be evaluated * with the test (...) function. */ template class NDT2D : public boost::noncopyable { using PointCloud = pcl::PointCloud; using PointCloudConstPtr = typename PointCloud::ConstPtr; using SingleGrid = NDTSingleGrid; public: /** \brief * \param[in] cloud the input point cloud * \param[in] about Centre of the grid for normal distributions model * \param[in] extent Extent of grid for normal distributions model * \param[in] step Size of region that each normal distribution will model */ NDT2D(PointCloudConstPtr cloud, const Eigen::Vector2f& about, const Eigen::Vector2f& extent, const Eigen::Vector2f& step) { Eigen::Vector2f dx(step[0] / 2, 0); Eigen::Vector2f dy(0, step[1] / 2); single_grids_[0].reset(new SingleGrid(cloud, about, extent, step)); single_grids_[1].reset(new SingleGrid(cloud, about + dx, extent, step)); single_grids_[2].reset(new SingleGrid(cloud, about + dy, extent, step)); single_grids_[3].reset(new SingleGrid(cloud, about + dx + dy, extent, step)); } /** \brief Return the 'score' (denormalised likelihood) and derivatives of score of * the point p given this distribution. \param[in] transformed_pt Location to * evaluate at. \param[in] cos_theta sin(theta) of the current rotation angle * of rigid transformation: to avoid repeated evaluation \param[in] sin_theta * cos(theta) of the current rotation angle of rigid transformation: to avoid repeated * evaluation */ ValueAndDerivatives<3, double> test(const PointT& transformed_pt, const double& cos_theta, const double& sin_theta) const { ValueAndDerivatives<3, double> r = ValueAndDerivatives<3, double>::Zero(); for (const auto& single_grid : single_grids_) r += single_grid->test(transformed_pt, cos_theta, sin_theta); return r; } protected: std::shared_ptr single_grids_[4]; }; } // namespace ndt2d } // namespace pcl namespace Eigen { /* This NumTraits specialisation is necessary because NormalDist is used as * the element type of an Eigen Matrix. */ template struct NumTraits> { using Real = double; using Literal = double; static Real dummy_precision() { return 1.0; } enum { IsComplex = 0, IsInteger = 0, IsSigned = 0, RequireInitialization = 1, ReadCost = 1, AddCost = 1, MulCost = 1 }; }; } // namespace Eigen namespace pcl { template void NormalDistributionsTransform2D::computeTransformation( PointCloudSource& output, const Eigen::Matrix4f& guess) { PointCloudSource intm_cloud = output; nr_iterations_ = 0; converged_ = false; if (guess != Eigen::Matrix4f::Identity()) { transformation_ = guess; transformPointCloud(output, intm_cloud, transformation_); } // build Normal Distribution Transform of target cloud: ndt2d::NDT2D target_ndt(target_, grid_centre_, grid_extent_, grid_step_); // can't seem to use .block<> () member function on transformation_ // directly... gcc bug? Eigen::Matrix4f& transformation = transformation_; // work with x translation, y translation and z rotation: extending to 3D // would be some tricky maths, but not impossible. const Eigen::Matrix3f initial_rot(transformation.block<3, 3>(0, 0)); const Eigen::Vector3f rot_x(initial_rot * Eigen::Vector3f::UnitX()); const double z_rotation = std::atan2(rot_x[1], rot_x[0]); Eigen::Vector3d xytheta_transformation( transformation(0, 3), transformation(1, 3), z_rotation); while (!converged_) { const double cos_theta = std::cos(xytheta_transformation[2]); const double sin_theta = std::sin(xytheta_transformation[2]); previous_transformation_ = transformation; ndt2d::ValueAndDerivatives<3, double> score = ndt2d::ValueAndDerivatives<3, double>::Zero(); for (std::size_t i = 0; i < intm_cloud.size(); i++) score += target_ndt.test(intm_cloud[i], cos_theta, sin_theta); PCL_DEBUG("[pcl::NormalDistributionsTransform2D::computeTransformation] NDT score " "%f (x=%f,y=%f,r=%f)\n", float(score.value), xytheta_transformation[0], xytheta_transformation[1], xytheta_transformation[2]); if (score.value != 0) { // test for positive definiteness, and adjust to ensure it if necessary: Eigen::EigenSolver solver; solver.compute(score.hessian, false); double min_eigenvalue = 0; for (int i = 0; i < 3; i++) if (solver.eigenvalues()[i].real() < min_eigenvalue) min_eigenvalue = solver.eigenvalues()[i].real(); // ensure "safe" positive definiteness: this is a detail missing // from the original paper if (min_eigenvalue < 0) { double lambda = 1.1 * min_eigenvalue - 1; score.hessian += Eigen::Vector3d(-lambda, -lambda, -lambda).asDiagonal(); solver.compute(score.hessian, false); PCL_DEBUG("[pcl::NormalDistributionsTransform2D::computeTransformation] adjust " "hessian: %f: new eigenvalues:%f %f %f\n", float(lambda), solver.eigenvalues()[0].real(), solver.eigenvalues()[1].real(), solver.eigenvalues()[2].real()); } assert(solver.eigenvalues()[0].real() >= 0 && solver.eigenvalues()[1].real() >= 0 && solver.eigenvalues()[2].real() >= 0); Eigen::Vector3d delta_transformation(-score.hessian.inverse() * score.grad); Eigen::Vector3d new_transformation = xytheta_transformation + newton_lambda_.cwiseProduct(delta_transformation); xytheta_transformation = new_transformation; // update transformation matrix from x, y, theta: transformation.block<3, 3>(0, 0).matrix() = Eigen::Matrix3f(Eigen::AngleAxisf( static_cast(xytheta_transformation[2]), Eigen::Vector3f::UnitZ())); transformation.block<3, 1>(0, 3).matrix() = Eigen::Vector3f(static_cast(xytheta_transformation[0]), static_cast(xytheta_transformation[1]), 0.0f); // std::cout << "new transformation:\n" << transformation << std::endl; } else { PCL_ERROR("[pcl::NormalDistributionsTransform2D::computeTransformation] no " "overlap: try increasing the size or reducing the step of the grid\n"); break; } transformPointCloud(output, intm_cloud, transformation); nr_iterations_++; if (update_visualizer_) update_visualizer_(output, *indices_, *target_, *indices_); // std::cout << "eps=" << std::abs ((transformation - previous_transformation_).sum // ()) << std::endl; Eigen::Matrix4f transformation_delta = transformation.inverse() * previous_transformation_; double cos_angle = 0.5 * (transformation_delta.coeff(0, 0) + transformation_delta.coeff(1, 1) + transformation_delta.coeff(2, 2) - 1); double translation_sqr = transformation_delta.coeff(0, 3) * transformation_delta.coeff(0, 3) + transformation_delta.coeff(1, 3) * transformation_delta.coeff(1, 3) + transformation_delta.coeff(2, 3) * transformation_delta.coeff(2, 3); if (nr_iterations_ >= max_iterations_ || ((transformation_epsilon_ > 0 && translation_sqr <= transformation_epsilon_) && (transformation_rotation_epsilon_ > 0 && cos_angle >= transformation_rotation_epsilon_)) || ((transformation_epsilon_ <= 0) && (transformation_rotation_epsilon_ > 0 && cos_angle >= transformation_rotation_epsilon_)) || ((transformation_epsilon_ > 0 && translation_sqr <= transformation_epsilon_) && (transformation_rotation_epsilon_ <= 0))) { converged_ = true; } } final_transformation_ = transformation; output = intm_cloud; } } // namespace pcl #endif // PCL_NDT_2D_IMPL_H_