Skip to content

Commit

Permalink
registration_template example
Browse files Browse the repository at this point in the history
  • Loading branch information
koide3 committed Mar 31, 2024
1 parent ec883c4 commit 76b0b93
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 24 deletions.
24 changes: 6 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ reg.align(*aligned);
It is also possible to directly feed `pcl::PointCloud` to `small_gicp::Registration`. Because all preprocessed data are exposed in this way, you can easily re-use them to obtain the best efficiency.

```cpp
#include <small_gicp/pcl/pcl_registration.hpp>
#include <small_gicp/pcl/pcl_point.hpp>
#include <small_gicp/pcl/pcl_point_traits.hpp>

pcl::PointCloud<pcl::PointXYZ>::Ptr raw_target = ...;
pcl::PointCloud<pcl::PointXYZ>::Ptr raw_source = ...;
Expand Down Expand Up @@ -167,7 +168,7 @@ auto result = registration.align(*target, *source, *target_tree, Eigen::Isometry
</details>
### Using `small_gicp::Registration` template (`registration.hpp`)
### Using `Registration` template ([03_registration_template.cpp](https://github.com/koide3/small_gicp/blob/master/src/example/03_registration_template.cpp))
If you want to fine-control and customize the registration process, use `small_gicp::Registration` template that allows modifying the inner algorithms and parameters.
<details><summary>Expand</summary>
Expand All @@ -193,8 +194,8 @@ auto target = std::make_shared<PointCloud>(target_points);
auto source = std::make_shared<PointCloud>(source_points);
// Downsampling
target = voxelgrid_downsampling_omp(*target, downsampling_resolution, num_threads);
source = voxelgrid_downsampling_omp(*source, downsampling_resolution, num_threads);
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<KdTreeOMP<PointCloud>>(target, num_threads);
Expand All @@ -218,20 +219,7 @@ size_t num_inliers = result.num_inliers; // Number of inlier source points
Eigen::Matrix<double, 6, 6> H = result.H; // Final Hessian matrix (6x6)
```

Custom registration example:

```cpp
using PerPointFactor = PointToPlaneICPFactor; // Point-to-plane ICP
using GeneralFactor = RestrictDoFFactor; // DoF restriction
using Reduction = ParallelReductionTBB; // TBB-based parallel reduction
using CorrespondenceRejector = DistanceRejector; // Distance-based correspondence rejection
using Optimizer = LevenbergMarquardtOptimizer; // Levenberg marquardt optimizer

Registration<PerPointFactor, Reduction, GeneralFactor, CorrespondenceRejector, Optimizer> registration;
registration.general_factor.set_translation_mask(Eigen::Vector3d(1.0, 1.0, 0.0)); // XY-translation only
registration.general_factor.set_ratation_mask(Eigen::Vector3d(0.0, 0.0, 1.0)); // Z-rotation only
registration.optimizer.init_lambda = 1e-3; // Initial damping scale
```
See [03_registration_template.cpp](https://github.com/koide3/small_gicp/blob/master/src/example/03_registration_template.cpp) for more detailed customization example.

</details>

Expand Down
2 changes: 1 addition & 1 deletion include/small_gicp/factors/gicp_factor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct GICPFactor {

size_t k_index;
double k_sq_dist;
if (!traits::nearest_neighbor_search(target_tree, transed_source_pt, &k_index, &k_sq_dist) || rejector(T, k_index, source_index, k_sq_dist)) {
if (!traits::nearest_neighbor_search(target_tree, transed_source_pt, &k_index, &k_sq_dist) || rejector(target, source, T, k_index, source_index, k_sq_dist)) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion include/small_gicp/factors/icp_factor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct ICPFactor {

size_t k_index;
double k_sq_dist;
if (!traits::nearest_neighbor_search(target_tree, transed_source_pt, &k_index, &k_sq_dist) || rejector(T, k_index, source_index, k_sq_dist)) {
if (!traits::nearest_neighbor_search(target_tree, transed_source_pt, &k_index, &k_sq_dist) || rejector(target, source, T, k_index, source_index, k_sq_dist)) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion include/small_gicp/factors/plane_icp_factor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct PointToPlaneICPFactor {

size_t k_index;
double k_sq_dist;
if (!traits::nearest_neighbor_search(target_tree, transed_source_pt, &k_index, &k_sq_dist) || rejector(T, k_index, source_index, k_sq_dist)) {
if (!traits::nearest_neighbor_search(target_tree, transed_source_pt, &k_index, &k_sq_dist) || rejector(target, source, T, k_index, source_index, k_sq_dist)) {
return false;
}

Expand Down
11 changes: 8 additions & 3 deletions include/small_gicp/registration/rejector.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ namespace small_gicp {

/// @brief Null correspondence rejector. This class accepts all input correspondences.
struct NullRejector {
bool operator()(const Eigen::Isometry3d& T, size_t target_index, size_t source_index, double sq_dist) const { return false; }
template <typename TargetPointCloud, typename SourcePointCloud>
bool operator()(const TargetPointCloud& target, const SourcePointCloud& source, const Eigen::Isometry3d& T, size_t target_index, size_t source_index, double sq_dist) const {
return false;
}
};

/// @brief Rejecting correspondences with large distances.
struct DistanceRejector {
DistanceRejector() : max_dist_sq(1.0) {}
DistanceRejector(double max_dist) : max_dist_sq(max_dist * max_dist) {}

bool operator()(const Eigen::Isometry3d& T, size_t target_index, size_t source_index, double sq_dist) const { return sq_dist > max_dist_sq; }
template <typename TargetPointCloud, typename SourcePointCloud>
bool operator()(const TargetPointCloud& target, const SourcePointCloud& source, const Eigen::Isometry3d& T, size_t target_index, size_t source_index, double sq_dist) const {
return sq_dist > max_dist_sq;
}

double max_dist_sq;
};
Expand Down
267 changes: 267 additions & 0 deletions src/example/03_registration_template.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/// @brief Basic point cloud registration example with small_gicp::align()
#include <iostream>
#include <small_gicp/benchmark/read_points.hpp>

#include <small_gicp/ann/kdtree_omp.hpp>
#include <small_gicp/points/point_cloud.hpp>
#include <small_gicp/factors/gicp_factor.hpp>
#include <small_gicp/factors/plane_icp_factor.hpp>
#include <small_gicp/util/downsampling_omp.hpp>
#include <small_gicp/util/normal_estimation_omp.hpp>
#include <small_gicp/registration/reduction_omp.hpp>
#include <small_gicp/registration/registration.hpp>

using namespace small_gicp;

/// @brief Basic registration example using small_gicp::Registration.
void example1(const std::vector<Eigen::Vector4f>& target_points, const std::vector<Eigen::Vector4f>& 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<PointCloud>(target_points);
auto source = std::make_shared<PointCloud>(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<KdTreeOMP<PointCloud>>(target, num_threads);
auto source_tree = std::make_shared<KdTreeOMP<PointCloud>>(source, 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<GICPFactor, ParallelReductionOMP> 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<double, 3> point; // Point coorindates
std::array<double, 3> normal; // Point normal
std::array<double, 36> features; // Point features
};

/// @brief My point cloud class.
using MyPointCloud = std::vector<MyPoint>;

// Define traits for MyPointCloud so that it can be fed to small_gicp algorithms.
namespace small_gicp {
namespace traits {
template <>
struct Traits<MyPointCloud> {
// *** Setters ***
// 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 getter is required.
// 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 covariance estimation)

// Resize the point cloud.
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<Eigen::Vector3d>(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<Eigen::Vector3d>(points[i].normal.data()) = n.head<3>(); }
// To feed this class to estimate_covariances(), the following setter is required.
// static void set_cov(MyPointCloud& points, size_t i, const Eigen::Matrix4d& cov);
};
} // 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<const Eigen::Matrix<double, 36, 1>> target_features(target[target_index].features.data());
Eigen::Map<const Eigen::Matrix<double, 36, 1>> 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 affect the entire registration process.
struct MyGeneralFactor {
MyGeneralFactor() : lambda(1e8) {}

/// @brief Update linearized system consisting of linearized per-point factors.
/// @note This method is called just before the linearized system is solved.
/// By modifying the linearized system (H, b, e), you can add constraints to the optimization.
/// @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 precision matrix.
/// @param b [in/out] Linearized information vector.
/// @param e [in/out] Error at the linearization point.
template <typename TargetPointCloud, typename SourcePointCloud, typename TargetTree>
void update_linearized_system(
const TargetPointCloud& target,
const SourcePointCloud& source,
const TargetTree& target_tree,
const Eigen::Isometry3d& T,
Eigen::Matrix<double, 6, 6>* H,
Eigen::Matrix<double, 6, 1>* b,
double* e) const {
// Optimization DoF mask [rx, ry, rz, tx, ty, tz] (1.0 = inactive, 0.0 = active)
Eigen::Matrix<double, 6, 1> 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 should update the error here to be consistent.
/// @param target Target point cloud
/// @param source Source point cloud
/// @param T Evaluation point
/// @param e [in/out] Error at the evaluation point.
template <typename TargetPointCloud, typename SourcePointCloud>
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<Eigen::Vector4f>& target_points, const std::vector<Eigen::Vector4f>& 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<MyPointCloud> target = std::make_shared<MyPointCloud>();
target->resize(target_points.size());
for (size_t i = 0; i < target_points.size(); i++) {
Eigen::Map<Eigen::Vector3d>(target->at(i).point.data()) = target_points[i].head<3>().cast<double>();
}

std::shared_ptr<MyPointCloud> source = std::make_shared<MyPointCloud>();
source->resize(source_points.size());
for (size_t i = 0; i < source_points.size(); i++) {
Eigen::Map<Eigen::Vector3d>(source->at(i).point.data()) = source_points[i].head<3>().cast<double>();
}

// 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<KdTreeOMP<MyPointCloud>>(target, num_threads);
auto source_tree = std::make_shared<KdTreeOMP<MyPointCloud>>(source, num_threads);

// Estimate point normals
estimate_normals_omp(*target, *target_tree, num_neighbors, num_threads);
estimate_normals_omp(*source, *source_tree, 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<PerPointFactor, Reduction, GeneralFactor, CorrespondenceRejector, Optimizer> 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
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;
}

int main(int argc, char** argv) {
std::vector<Eigen::Vector4f> target_points = read_ply("data/target.ply");
std::vector<Eigen::Vector4f> 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;
}

0 comments on commit 76b0b93

Please sign in to comment.