Skip to content

Commit cf6e6fa

Browse files
eclipse0922sewon.jeonCopilot
authored
Implements the Symmetric Iterative Closest Point (ICP) algorithm for point cloud registration. (#7276)
This change introduces a new method for aligning point clouds that considers the symmetry between source and target, leading to potentially more accurate and robust registration results, especially when dealing with noisy or incomplete data. Includes C++ and Python implementations with corresponding tests. Adds CUDA and SYCL implementation for the Symmetric ICP registration algorithm, enabling it to run on GPUs. --------- Co-authored-by: sewon.jeon <sewon.jeon@connecteve.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent aab533f commit cf6e6fa

29 files changed

Lines changed: 1803 additions & 21 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## Main
2+
- Add symmetric ICP registration to the legacy and Tensor pipelines (PR #7276).
23
- Replace OpenMP with oneAPI TBB for all CPU parallelism; Open3D no longer depends on OpenMP. This removes the `libomp` / `libgomp` runtime dependency and the thread oversubscription and crashes caused by loading multiple OpenMP runtimes in one process (e.g. alongside PyTorch in Python). The `WITH_OPENMP` CMake option is removed, oneTBB >= 2021.4.0 is required, and `OMP_NUM_THREADS` is replaced by `open3d.utility.set_max_threads()` (C++: `utility::SetMaxThreads()` or a `tbb::task_arena`). `utility::OMPProgressBar` is removed in favor of the thread-safe `utility::ProgressBar`; `utility::GetThreadNum()` and `utility::InParallel()` are removed (PR #6626) (issues #6196, #6544, #6750)
34
- Add point cloud smoothing algorithms: Moving Least Squares (MLS), Laplacian, Taubin, and bilateral smoothing. These methods provide flexible noise reduction for point clouds with different preservation characteristics (PR #7419).
45
- Add vcpkg support for easier dependency management (PR #7386)

cpp/open3d/Open3D.h.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
#include "open3d/pipelines/registration/GeneralizedICP.h"
6565
#include "open3d/pipelines/registration/GlobalOptimization.h"
6666
#include "open3d/pipelines/registration/Registration.h"
67+
#include "open3d/pipelines/registration/SymmetricICP.h"
6768
#include "open3d/pipelines/registration/TransformationEstimation.h"
6869
#include "open3d/t/geometry/Geometry.h"
6970
#include "open3d/t/geometry/Image.h"

cpp/open3d/pipelines/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ target_sources(pipelines PRIVATE
2323
registration/FastGlobalRegistration.cpp
2424
registration/Feature.cpp
2525
registration/GeneralizedICP.cpp
26+
registration/SymmetricICP.cpp
2627
registration/GlobalOptimization.cpp
2728
registration/PoseGraph.cpp
2829
registration/Registration.cpp
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// ----------------------------------------------------------------------------
2+
// - Open3D: www.open3d.org -
3+
// ----------------------------------------------------------------------------
4+
// Copyright (c) 2018-2024 www.open3d.org
5+
// SPDX-License-Identifier: MIT
6+
// ----------------------------------------------------------------------------
7+
8+
#include "open3d/pipelines/registration/SymmetricICP.h"
9+
10+
#include <tbb/blocked_range.h>
11+
#include <tbb/parallel_reduce.h>
12+
13+
#include <cmath>
14+
#include <cstddef>
15+
16+
#include "open3d/geometry/PointCloud.h"
17+
#include "open3d/pipelines/registration/SymmetricICPImpl.h"
18+
#include "open3d/utility/Eigen.h"
19+
#include "open3d/utility/Logging.h"
20+
21+
namespace open3d {
22+
namespace pipelines {
23+
namespace registration {
24+
namespace {
25+
26+
void ValidateSymmetricICPNormals(const geometry::PointCloud &source,
27+
const geometry::PointCloud &target) {
28+
if (!source.HasNormals() || !target.HasNormals()) {
29+
utility::LogError(
30+
"SymmetricICP requires both source and target to have "
31+
"normals.");
32+
}
33+
}
34+
35+
void ValidateSymmetricICPCorrespondences(const geometry::PointCloud &source,
36+
const geometry::PointCloud &target,
37+
const CorrespondenceSet &corres) {
38+
for (const Eigen::Vector2i &correspondence : corres) {
39+
if (correspondence[0] < 0 || correspondence[1] < 0 ||
40+
static_cast<std::size_t>(correspondence[0]) >=
41+
source.points_.size() ||
42+
static_cast<std::size_t>(correspondence[1]) >=
43+
target.points_.size()) {
44+
utility::LogError(
45+
"SymmetricICP correspondence ({}, {}) is out of range for "
46+
"source size {} and target size {}.",
47+
correspondence[0], correspondence[1], source.points_.size(),
48+
target.points_.size());
49+
}
50+
}
51+
}
52+
53+
Eigen::Vector3d GetSymmetricNormal(const Eigen::Vector3d &source_normal,
54+
const Eigen::Vector3d &target_normal) {
55+
if (source_normal.dot(target_normal) < 0.0) {
56+
return target_normal - source_normal;
57+
}
58+
return target_normal + source_normal;
59+
}
60+
61+
struct CorrespondenceSums {
62+
Eigen::Vector3d source = Eigen::Vector3d::Zero();
63+
Eigen::Vector3d target = Eigen::Vector3d::Zero();
64+
};
65+
66+
struct NormalEquations {
67+
Eigen::Matrix6d JTJ = Eigen::Matrix6d::Zero();
68+
Eigen::Vector6d JTr = Eigen::Vector6d::Zero();
69+
};
70+
71+
} // namespace
72+
73+
double TransformationEstimationSymmetric::ComputeRMSE(
74+
const geometry::PointCloud &source,
75+
const geometry::PointCloud &target,
76+
const CorrespondenceSet &corres) const {
77+
ValidateSymmetricICPNormals(source, target);
78+
ValidateSymmetricICPCorrespondences(source, target, corres);
79+
if (corres.empty()) {
80+
return 0.0;
81+
}
82+
83+
double err = 0.0;
84+
for (const auto &c : corres) {
85+
const Eigen::Vector3d &source_point = source.points_[c[0]];
86+
const Eigen::Vector3d &target_point = target.points_[c[1]];
87+
const Eigen::Vector3d normal = GetSymmetricNormal(
88+
source.normals_[c[0]], target.normals_[c[1]]);
89+
const double residual = (source_point - target_point).dot(normal);
90+
err += residual * residual;
91+
}
92+
return std::sqrt(err / static_cast<double>(corres.size()));
93+
}
94+
95+
Eigen::Matrix4d TransformationEstimationSymmetric::ComputeTransformation(
96+
const geometry::PointCloud &source,
97+
const geometry::PointCloud &target,
98+
const CorrespondenceSet &corres) const {
99+
ValidateSymmetricICPNormals(source, target);
100+
ValidateSymmetricICPCorrespondences(source, target, corres);
101+
if (corres.empty()) {
102+
return Eigen::Matrix4d::Identity();
103+
}
104+
105+
const CorrespondenceSums sums = tbb::parallel_reduce(
106+
tbb::blocked_range<std::size_t>(0, corres.size()),
107+
CorrespondenceSums(),
108+
[&](const tbb::blocked_range<std::size_t> &range,
109+
CorrespondenceSums local) {
110+
for (std::size_t i = range.begin(); i != range.end(); ++i) {
111+
local.source += source.points_[corres[i][0]];
112+
local.target += target.points_[corres[i][1]];
113+
}
114+
return local;
115+
},
116+
[](CorrespondenceSums lhs, const CorrespondenceSums &rhs) {
117+
lhs.source += rhs.source;
118+
lhs.target += rhs.target;
119+
return lhs;
120+
});
121+
const double inverse_count = 1.0 / static_cast<double>(corres.size());
122+
const Eigen::Vector3d source_mean = sums.source * inverse_count;
123+
const Eigen::Vector3d target_mean = sums.target * inverse_count;
124+
125+
// Centering the correspondences decouples the symmetric rotation and
126+
// translation system while the robust weight uses the raw residual.
127+
const NormalEquations equations = tbb::parallel_reduce(
128+
tbb::blocked_range<std::size_t>(0, corres.size()),
129+
NormalEquations(),
130+
[&](const tbb::blocked_range<std::size_t> &range,
131+
NormalEquations local) {
132+
for (std::size_t i = range.begin(); i != range.end(); ++i) {
133+
const Eigen::Vector3d &source_point =
134+
source.points_[corres[i][0]];
135+
const Eigen::Vector3d &target_point =
136+
target.points_[corres[i][1]];
137+
const Eigen::Vector3d normal =
138+
GetSymmetricNormal(source.normals_[corres[i][0]],
139+
target.normals_[corres[i][1]]);
140+
const Eigen::Vector3d source_centered =
141+
source_point - source_mean;
142+
const Eigen::Vector3d target_centered =
143+
target_point - target_mean;
144+
const double raw_residual =
145+
(source_point - target_point).dot(normal);
146+
const double residual =
147+
(source_centered - target_centered).dot(normal);
148+
149+
Eigen::Vector6d jacobian;
150+
jacobian.head<3>() =
151+
(source_centered + target_centered).cross(normal);
152+
jacobian.tail<3>() = normal;
153+
154+
const double weight = kernel_->Weight(raw_residual);
155+
local.JTJ.noalias() +=
156+
weight * jacobian * jacobian.transpose();
157+
local.JTr.noalias() += weight * jacobian * residual;
158+
}
159+
return local;
160+
},
161+
[](NormalEquations lhs, const NormalEquations &rhs) {
162+
lhs.JTJ += rhs.JTJ;
163+
lhs.JTr += rhs.JTr;
164+
return lhs;
165+
});
166+
167+
bool is_success = false;
168+
Eigen::Vector6d pose;
169+
std::tie(is_success, pose) =
170+
utility::SolveLinearSystemPSD(equations.JTJ, -equations.JTr);
171+
return is_success ? TransformSymmetricPoseToMatrix4d(pose, source_mean,
172+
target_mean)
173+
: Eigen::Matrix4d::Identity();
174+
}
175+
176+
std::tuple<std::shared_ptr<const geometry::PointCloud>,
177+
std::shared_ptr<const geometry::PointCloud>>
178+
TransformationEstimationSymmetric::InitializePointCloudsForTransformation(
179+
const geometry::PointCloud &source,
180+
const geometry::PointCloud &target,
181+
double max_correspondence_distance) const {
182+
ValidateSymmetricICPNormals(source, target);
183+
std::shared_ptr<const geometry::PointCloud> source_initialized_c(
184+
&source, [](const geometry::PointCloud *) {});
185+
std::shared_ptr<const geometry::PointCloud> target_initialized_c(
186+
&target, [](const geometry::PointCloud *) {});
187+
if (!source_initialized_c || !target_initialized_c) {
188+
utility::LogError(
189+
"Internal error: InitializePointCloudsFor"
190+
"Transformation returns nullptr.");
191+
}
192+
return std::make_tuple(source_initialized_c, target_initialized_c);
193+
}
194+
195+
RegistrationResult RegistrationSymmetricICP(
196+
const geometry::PointCloud &source,
197+
const geometry::PointCloud &target,
198+
double max_correspondence_distance,
199+
const Eigen::Matrix4d &init,
200+
const TransformationEstimationSymmetric &estimation,
201+
const ICPConvergenceCriteria &criteria) {
202+
return RegistrationICP(source, target, max_correspondence_distance, init,
203+
estimation, criteria);
204+
}
205+
206+
} // namespace registration
207+
} // namespace pipelines
208+
} // namespace open3d
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// ----------------------------------------------------------------------------
2+
// - Open3D: www.open3d.org -
3+
// ----------------------------------------------------------------------------
4+
// Copyright (c) 2018-2024 www.open3d.org
5+
// SPDX-License-Identifier: MIT
6+
// ----------------------------------------------------------------------------
7+
8+
#pragma once
9+
10+
#include "open3d/pipelines/registration/Registration.h"
11+
#include "open3d/pipelines/registration/RobustKernel.h"
12+
#include "open3d/pipelines/registration/TransformationEstimation.h"
13+
14+
namespace open3d {
15+
16+
namespace geometry {
17+
class PointCloud;
18+
}
19+
20+
namespace pipelines {
21+
namespace registration {
22+
23+
class RegistrationResult;
24+
25+
/// \brief Estimates a source-to-target transformation with symmetric ICP.
26+
///
27+
/// For each correspondence, \f$p\f$ is the source point and \f$q\f$ is the
28+
/// target point in the current aligned frame, with corresponding normals
29+
/// \f$n_p\f$ and \f$n_q\f$. After aligning the normal directions, the
30+
/// objective uses the single residual \f$(p-q)^T(n_p+n_q)\f$.
31+
class TransformationEstimationSymmetric : public TransformationEstimation {
32+
public:
33+
~TransformationEstimationSymmetric() override = default;
34+
35+
TransformationEstimationType GetTransformationEstimationType()
36+
const override {
37+
return type_;
38+
};
39+
/// \brief Constructs a symmetric transformation estimator.
40+
/// \param kernel Robust kernel applied to the symmetric residual.
41+
explicit TransformationEstimationSymmetric(
42+
std::shared_ptr<RobustKernel> kernel = std::make_shared<L2Loss>())
43+
: kernel_(std::move(kernel)) {}
44+
45+
/// \brief Computes the symmetric point-to-plane RMSE.
46+
/// \param source Source point cloud in the current aligned frame.
47+
/// \param target Target point cloud in the current aligned frame.
48+
/// \param corres Source-to-target correspondence indices.
49+
/// \return The symmetric point-to-plane RMSE.
50+
/// \throw std::runtime_error If either point cloud lacks normals or a
51+
/// correspondence index is out of range.
52+
double ComputeRMSE(const geometry::PointCloud &source,
53+
const geometry::PointCloud &target,
54+
const CorrespondenceSet &corres) const override;
55+
56+
/// \brief Estimates a source-to-target transformation update.
57+
/// \param source Source point cloud in the current aligned frame.
58+
/// \param target Target point cloud in the current aligned frame.
59+
/// \param corres Source-to-target correspondence indices.
60+
/// \return The source-to-target transformation update.
61+
/// \throw std::runtime_error If either point cloud lacks normals or a
62+
/// correspondence index is out of range.
63+
Eigen::Matrix4d ComputeTransformation(
64+
const geometry::PointCloud &source,
65+
const geometry::PointCloud &target,
66+
const CorrespondenceSet &corres) const override;
67+
68+
/// \brief Validates and initializes point clouds for symmetric ICP.
69+
/// \param source Source point cloud.
70+
/// \param target Target point cloud.
71+
/// \param max_correspondence_distance Maximum correspondence distance.
72+
/// \return The initialized source and target point clouds.
73+
/// \throw std::runtime_error If either point cloud lacks normals.
74+
std::tuple<std::shared_ptr<const geometry::PointCloud>,
75+
std::shared_ptr<const geometry::PointCloud>>
76+
InitializePointCloudsForTransformation(
77+
const geometry::PointCloud &source,
78+
const geometry::PointCloud &target,
79+
double max_correspondence_distance) const override;
80+
81+
/// shared_ptr to an Abstract RobustKernel that could mutate at runtime.
82+
std::shared_ptr<RobustKernel> kernel_ = std::make_shared<L2Loss>();
83+
84+
private:
85+
const TransformationEstimationType type_ =
86+
TransformationEstimationType::SymmetricICP;
87+
};
88+
89+
/// \brief Registers source to target with symmetric point-to-plane ICP.
90+
///
91+
/// For each correspondence in the current aligned frame, \f$p\f$ and
92+
/// \f$n_p\f$ denote the source point and normal, while \f$q\f$ and \f$n_q\f$
93+
/// denote the target point and normal. After aligning normal directions, the
94+
/// objective uses the single residual \f$(p-q)^T(n_p+n_q)\f$.
95+
/// \param source Source point cloud with normals.
96+
/// \param target Target point cloud with normals.
97+
/// \param max_correspondence_distance Maximum correspondence distance.
98+
/// \param init Initial source-to-target transformation.
99+
/// \param estimation Symmetric transformation estimator.
100+
/// \param criteria ICP convergence criteria.
101+
/// \return The registration result with a source-to-target transformation.
102+
/// \throw std::runtime_error If either point cloud lacks normals.
103+
RegistrationResult RegistrationSymmetricICP(
104+
const geometry::PointCloud &source,
105+
const geometry::PointCloud &target,
106+
double max_correspondence_distance,
107+
const Eigen::Matrix4d &init = Eigen::Matrix4d::Identity(),
108+
const TransformationEstimationSymmetric &estimation =
109+
TransformationEstimationSymmetric(),
110+
const ICPConvergenceCriteria &criteria = ICPConvergenceCriteria());
111+
112+
} // namespace registration
113+
} // namespace pipelines
114+
} // namespace open3d
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// ----------------------------------------------------------------------------
2+
// - Open3D: www.open3d.org -
3+
// ----------------------------------------------------------------------------
4+
// Copyright (c) 2018-2024 www.open3d.org
5+
// SPDX-License-Identifier: MIT
6+
// ----------------------------------------------------------------------------
7+
8+
#pragma once
9+
10+
#include <Eigen/Geometry>
11+
#include <cmath>
12+
13+
#include "open3d/utility/Eigen.h"
14+
15+
namespace open3d {
16+
namespace pipelines {
17+
namespace registration {
18+
19+
inline Eigen::Matrix4d TransformSymmetricPoseToMatrix4d(
20+
const Eigen::Vector6d &pose,
21+
const Eigen::Vector3d &source_mean,
22+
const Eigen::Vector3d &target_mean) {
23+
const Eigen::Vector3d g = pose.head<3>();
24+
const double g_norm = g.norm();
25+
const double theta = std::atan(g_norm);
26+
27+
Eigen::Matrix3d half_rotation = Eigen::Matrix3d::Identity();
28+
if (g_norm > 0.0) {
29+
half_rotation = Eigen::AngleAxisd(theta, g / g_norm).toRotationMatrix();
30+
}
31+
32+
// Symmetric ICP solves for a half-angle pose about correspondence means.
33+
// Equation 11 converts it to the full rigid transformation.
34+
const Eigen::Matrix3d rotation = half_rotation * half_rotation;
35+
const Eigen::Vector3d translation =
36+
target_mean + half_rotation * (pose.tail<3>() * std::cos(theta)) -
37+
rotation * source_mean;
38+
39+
Eigen::Matrix4d transformation = Eigen::Matrix4d::Identity();
40+
transformation.block<3, 3>(0, 0) = rotation;
41+
transformation.block<3, 1>(0, 3) = translation;
42+
return transformation;
43+
}
44+
45+
} // namespace registration
46+
} // namespace pipelines
47+
} // namespace open3d

0 commit comments

Comments
 (0)