// SPDX-FileCopyrightText: Copyright 2024 Kenji Koide // SPDX-License-Identifier: MIT /// @brief Basic point cloud registration example with small_gicp::align() #include #include #include #include #include #include #include #include #include #include #include using namespace small_gicp; /// @brief Basic registration example using small_gicp::Registration. void example1(const std::vector& target_points, const std::vector& source_points) { int num_threads = 4; // Number of threads to be used double downsampling_resolution = 0.25; // Downsampling resolution int num_neighbors = 10; // Number of neighbor points used for normal and covariance estimation double max_correspondence_distance = 1.0; // Maximum correspondence distance between points (e.g., triming threshold) // Convert to small_gicp::PointCloud auto target = std::make_shared(target_points); auto source = std::make_shared(source_points); // Downsampling target = voxelgrid_sampling_omp(*target, downsampling_resolution, num_threads); source = voxelgrid_sampling_omp(*source, downsampling_resolution, num_threads); // Create KdTree auto target_tree = std::make_shared>(target, KdTreeBuilderOMP(num_threads)); auto source_tree = std::make_shared>(source, KdTreeBuilderOMP(num_threads)); // Estimate point covariances estimate_covariances_omp(*target, *target_tree, num_neighbors, num_threads); estimate_covariances_omp(*source, *source_tree, num_neighbors, num_threads); // GICP + OMP-based parallel reduction Registration registration; registration.reduction.num_threads = num_threads; registration.rejector.max_dist_sq = max_correspondence_distance * max_correspondence_distance; // Align point clouds Eigen::Isometry3d init_T_target_source = Eigen::Isometry3d::Identity(); auto result = registration.align(*target, *source, *target_tree, init_T_target_source); std::cout << "--- T_target_source ---" << std::endl << result.T_target_source.matrix() << std::endl; std::cout << "converged:" << result.converged << std::endl; std::cout << "error:" << result.error << std::endl; std::cout << "iterations:" << result.iterations << std::endl; std::cout << "num_inliers:" << result.num_inliers << std::endl; std::cout << "--- H ---" << std::endl << result.H << std::endl; std::cout << "--- b ---" << std::endl << result.b.transpose() << std::endl; } /** Custom registration example **/ /// @brief Point structure with mean, normal, and features. struct MyPoint { std::array point; // Point coorindates std::array normal; // Point normal std::array features; // Point features }; /// @brief My point cloud class. using MyPointCloud = std::vector; // Define traits for MyPointCloud so that it can be fed to small_gicp algorithms. namespace small_gicp { namespace traits { template <> struct Traits { // *** Getters *** // The following getters are required for feeding this class to registration algorithms. // Number of points in the point cloud. static size_t size(const MyPointCloud& points) { return points.size(); } // Check if the point cloud has points. static bool has_points(const MyPointCloud& points) { return !points.empty(); } // Check if the point cloud has normals. static bool has_normals(const MyPointCloud& points) { return !points.empty(); } // Get i-th point. The last element should be 1.0. static Eigen::Vector4d point(const MyPointCloud& points, size_t i) { const auto& p = points[i].point; return Eigen::Vector4d(p[0], p[1], p[2], 1.0); } // Get i-th normal. The last element should be 0.0. static Eigen::Vector4d normal(const MyPointCloud& points, size_t i) { const auto& n = points[i].normal; return Eigen::Vector4d(n[0], n[1], n[2], 0.0); } // To use GICP, the following covariance getters are required additionally. // static bool has_covs(const MyPointCloud& points) { return !points.empty(); } // static const Eigen::Matrix4d cov(const MyPointCloud& points, size_t i); // *** Setters *** // The following methods are required for feeding this class to preprocessing algorithms. // (e.g., downsampling and normal estimation) // Resize the point cloud. This must retain the values of existing points. static void resize(MyPointCloud& points, size_t n) { points.resize(n); } // Set i-th point. pt = [x, y, z, 1.0]. static void set_point(MyPointCloud& points, size_t i, const Eigen::Vector4d& pt) { Eigen::Map(points[i].point.data()) = pt.head<3>(); } // Set i-th normal. n = [nx, ny, nz, 0.0]. static void set_normal(MyPointCloud& points, size_t i, const Eigen::Vector4d& n) { Eigen::Map(points[i].normal.data()) = n.head<3>(); } // To feed this class to estimate_covariances(), the following setter is required additionally. // static void set_cov(MyPointCloud& points, size_t i, const Eigen::Matrix4d& cov); }; } // namespace traits } // namespace small_gicp /// @brief Custom nearest neighbor search with brute force search. (Do not use this in practical applications) struct MyNearestNeighborSearch { public: MyNearestNeighborSearch(const std::shared_ptr& points) : points(points) {} size_t knn_search(const Eigen::Vector4d& pt, int k, size_t* k_indices, double* k_sq_dists) const { // Priority queue to hold top-k neighbors const auto comp = [](const std::pair& lhs, const std::pair& rhs) { return lhs.second < rhs.second; }; std::priority_queue, std::vector>, decltype(comp)> queue(comp); // Push pairs of (index, squared distance) to the queue for (size_t i = 0; i < points->size(); i++) { const double sq_dist = (Eigen::Map(points->at(i).point.data()) - pt.head<3>()).squaredNorm(); queue.push({i, sq_dist}); if (queue.size() > k) { queue.pop(); } } // Pop results const size_t n = queue.size(); while (!queue.empty()) { k_indices[queue.size() - 1] = queue.top().first; k_sq_dists[queue.size() - 1] = queue.top().second; queue.pop(); } return n; } std::shared_ptr points; }; // Define traits for MyNearestNeighborSearch so that it can be fed to small_gicp algorithms. namespace small_gicp { namespace traits { template <> struct Traits { /// @brief Find k-nearest neighbors. /// @note This generic knn search is used for preprocessing (e.g., normal estimation). /// @param search Nearest neighbor search /// @param point Query point [x, y, z, 1.0] /// @param k Number of neighbors /// @param k_indices [out] Indices of the k-nearest neighbors /// @param k_sq_dists [out] Squared distances of the k-nearest neighbors /// @return Number of neighbors found static size_t knn_search(const MyNearestNeighborSearch& search, const Eigen::Vector4d& point, size_t k, size_t* k_indices, double* k_sq_dists) { return search.knn_search(point, k, k_indices, k_sq_dists); } /// @brief Find the nearest neighbor. This is a special case of knn_search with k=1 and is used in point cloud registration. /// You can define this to optimize the search speed for k=1. Otherwise, nearest_neighbor_search() automatically falls back to knn_search() with k=1. /// It is also valid to define only nearest_neighbor_search() and do not define knn_search() if you only feed your class to registration but not to preprocessing. /// @param search Nearest neighbor search /// @param point Query point [x, y, z, 1.0] /// @param k_indices [out] Indices of the k-nearest neighbors /// @param k_sq_dists [out] Squared distances of the k-nearest neighbors /// @return 1 if the nearest neighbor is found, 0 otherwise // static size_t nearest_neighbor_search(const MyNearestNeighborSearch& search, const Eigen::Vector4d& point, size_t* k_index, double* k_sq_dist); }; } // namespace traits } // namespace small_gicp /// @brief Custom correspondence rejector. struct MyCorrespondenceRejector { MyCorrespondenceRejector() : max_correpondence_dist_sq(1.0), min_feature_cos_dist(0.9) {} /// @brief Check if the correspondence should be rejected. /// @param T Current estimate of T_target_source /// @param target_index Target point index /// @param source_index Source point index /// @param sq_dist Squared distance between the points /// @return True if the correspondence should be rejected bool operator()(const MyPointCloud& target, const MyPointCloud& source, const Eigen::Isometry3d& T, size_t target_index, size_t source_index, double sq_dist) const { // Reject correspondences with large distances if (sq_dist > max_correpondence_dist_sq) { return true; } // You can define your own rejection criteria here (e.g., based on features) Eigen::Map> target_features(target[target_index].features.data()); Eigen::Map> source_features(target[target_index].features.data()); if (target_features.dot(source_features) < min_feature_cos_dist) { return true; } return false; } double max_correpondence_dist_sq; // Maximum correspondence distance double min_feature_cos_dist; // Maximum feature distance }; /// @brief Custom general factor that can control the registration process. struct MyGeneralFactor { MyGeneralFactor() : lambda(1e8) {} /// @brief Update linearized system. /// @note This method is called just before the linearized system is solved. /// By modifying the linearized system (H, b, e), you can inject arbitrary constraints. /// @param target Target point cloud /// @param source Source point cloud /// @param target_tree Nearest neighbor search for the target point cloud /// @param T Linearization point /// @param H [in/out] Linearized information matrix. /// @param b [in/out] Linearized information vector. /// @param e [in/out] Error at the linearization point. template void update_linearized_system( const TargetPointCloud& target, const SourcePointCloud& source, const TargetTree& target_tree, const Eigen::Isometry3d& T, Eigen::Matrix* H, Eigen::Matrix* b, double* e) const { // Optimization DoF mask [rx, ry, rz, tx, ty, tz] (1.0 = inactive, 0.0 = active) Eigen::Matrix dof_mask; dof_mask << 1.0, 1.0, 0.0, 0.0, 0.0, 0.0; // Fix roll and pitch rotation by adding a large penalty (soft contraint) (*H) += dof_mask.asDiagonal() * lambda; } /// @brief Update error consisting of per-point factors. /// @note This method is called just after the linearized system is solved in LM to check if the objective function is decreased. /// If you modified the error in update_linearized_system(), you need to update the error here for consistency. /// @param target Target point cloud /// @param source Source point cloud /// @param T Evaluation point /// @param e [in/out] Error at the evaluation point. template void update_error(const TargetPointCloud& target, const SourcePointCloud& source, const Eigen::Isometry3d& T, double* e) const { // No update is required for the error. } double lambda; ///< Regularization parameter (Increasing this makes the constraint stronger) }; /// @brief Example to perform preprocessing and registration separately. void example2(const std::vector& target_points, const std::vector& source_points) { int num_threads = 4; // Number of threads to be used double downsampling_resolution = 0.25; // Downsampling resolution int num_neighbors = 10; // Number of neighbor points used for normal and covariance estimation double max_correspondence_distance = 1.0; // Maximum correspondence distance between points (e.g., triming threshold) // Convert to MyPointCloud std::shared_ptr target = std::make_shared(); target->resize(target_points.size()); for (size_t i = 0; i < target_points.size(); i++) { Eigen::Map(target->at(i).point.data()) = target_points[i].head<3>().cast(); } std::shared_ptr source = std::make_shared(); source->resize(source_points.size()); for (size_t i = 0; i < source_points.size(); i++) { Eigen::Map(source->at(i).point.data()) = source_points[i].head<3>().cast(); } // Downsampling works directly on MyPointCloud target = voxelgrid_sampling_omp(*target, downsampling_resolution, num_threads); source = voxelgrid_sampling_omp(*source, downsampling_resolution, num_threads); // Create nearest neighbor search auto target_search = std::make_shared(target); auto source_search = std::make_shared(target); // Estimate point normals // You can use your custom nearest neighbor search here! estimate_normals_omp(*target, *target_search, num_neighbors, num_threads); estimate_normals_omp(*source, *source_search, num_neighbors, num_threads); // Compute point features (e.g., FPFH, SHOT, etc.) for (size_t i = 0; i < target->size(); i++) { target->at(i).features.fill(1.0); } for (size_t i = 0; i < source->size(); i++) { source->at(i).features.fill(1.0); } // Point-to-plane ICP + OMP-based parallel reduction using PerPointFactor = PointToPlaneICPFactor; // Use point-to-plane ICP factor. Target must have normals. using Reduction = ParallelReductionOMP; // Use OMP-based parallel reduction using GeneralFactor = MyGeneralFactor; // Use custom general factor using CorrespondenceRejector = MyCorrespondenceRejector; // Use custom correspondence rejector using Optimizer = LevenbergMarquardtOptimizer; // Use Levenberg-Marquardt optimizer Registration registration; registration.reduction.num_threads = num_threads; registration.rejector.max_correpondence_dist_sq = max_correspondence_distance * max_correspondence_distance; registration.general_factor.lambda = 1e8; // Align point clouds // Again, you can use your custom nearest neighbor search here! Eigen::Isometry3d init_T_target_source = Eigen::Isometry3d::Identity(); auto result = registration.align(*target, *source, *target_search, init_T_target_source); std::cout << "--- T_target_source ---" << std::endl << result.T_target_source.matrix() << std::endl; std::cout << "converged:" << result.converged << std::endl; std::cout << "error:" << result.error << std::endl; std::cout << "iterations:" << result.iterations << std::endl; std::cout << "num_inliers:" << result.num_inliers << std::endl; std::cout << "--- H ---" << std::endl << result.H << std::endl; std::cout << "--- b ---" << std::endl << result.b.transpose() << std::endl; } int main(int argc, char** argv) { std::vector target_points = read_ply("data/target.ply"); std::vector source_points = read_ply("data/source.ply"); if (target_points.empty() || source_points.empty()) { std::cerr << "error: failed to read points from data/(target|source).ply" << std::endl; return 1; } example1(target_points, source_points); example2(target_points, source_points); return 0; }