Skip to content

Commit e547055

Browse files
committed
Address NDT review feedback
1 parent b61440e commit e547055

5 files changed

Lines changed: 58 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
- Fix macOS arm64 builds, add CI runner for macOS arm64 (PR #6695)
5252
- Fix KDTreeFlann possibly using a dangling pointer instead of internal storage and simplified its members (PR #6734)
5353
- Fix RANSAC early stop if no inliers in a specific iteration (PR #6789)
54-
- Add 3D Normal Distributions Transform registration with C++ and Python APIs.
54+
- Add 3D Normal Distributions Transform registration with C++ and Python APIs (PR #7517).
5555
- Fix segmentation fault (infinite recursion) of DetectPlanarPatches if multiple points have same coordinates (PR #6794)
5656
- `TriangleMesh`'s `+=` operator appends UVs regardless of the presence of existing features (PR #6728)
5757
- Fix build with fmt v10.2.0 (#6783)

cpp/open3d/pipelines/registration/NormalDistributionsTransform.cpp

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ struct VoxelGaussian {
6262

6363
using VoxelMap = std::unordered_map<VoxelKey, VoxelGaussian, VoxelKeyHash>;
6464

65+
struct VoxelAccumulator {
66+
int count = 0;
67+
Eigen::Vector3d mean = Eigen::Vector3d::Zero();
68+
Eigen::Matrix3d covariance_accumulator = Eigen::Matrix3d::Zero();
69+
70+
void AddPoint(const Eigen::Vector3d &point) {
71+
++count;
72+
const Eigen::Vector3d delta = point - mean;
73+
mean += delta / static_cast<double>(count);
74+
const Eigen::Vector3d delta_after_update = point - mean;
75+
covariance_accumulator += delta * delta_after_update.transpose();
76+
}
77+
};
78+
79+
using VoxelAccumulatorMap =
80+
std::unordered_map<VoxelKey, VoxelAccumulator, VoxelKeyHash>;
81+
6582
struct NDTLinearSystem {
6683
Eigen::Matrix6d JTJ = Eigen::Matrix6d::Zero();
6784
Eigen::Vector6d JTr = Eigen::Vector6d::Zero();
@@ -152,39 +169,24 @@ void ValidateNDTOption(const NormalDistributionsTransformOption &option) {
152169
VoxelMap BuildVoxelGaussians(const geometry::PointCloud &target,
153170
const NormalDistributionsTransformOption &option) {
154171
const double inv_voxel_size = 1.0 / option.voxel_size_;
155-
std::unordered_map<VoxelKey, std::vector<int>, VoxelKeyHash> voxel_indices;
156-
for (int i = 0; i < static_cast<int>(target.points_.size()); ++i) {
157-
voxel_indices[GetVoxelKey(target.points_[i], inv_voxel_size)].push_back(
158-
i);
172+
VoxelAccumulatorMap voxel_accumulators;
173+
for (const Eigen::Vector3d &point : target.points_) {
174+
voxel_accumulators[GetVoxelKey(point, inv_voxel_size)].AddPoint(point);
159175
}
160176

161177
VoxelMap voxel_map;
162-
for (const auto &item : voxel_indices) {
163-
const auto &indices = item.second;
164-
if (static_cast<int>(indices.size()) < option.min_points_per_voxel_) {
178+
for (const auto &item : voxel_accumulators) {
179+
const VoxelAccumulator &accumulator = item.second;
180+
if (accumulator.count < option.min_points_per_voxel_) {
165181
continue;
166182
}
167183

168184
VoxelGaussian gaussian;
169-
gaussian.count = static_cast<int>(indices.size());
170-
for (const int idx : indices) {
171-
gaussian.mean += target.points_[idx];
172-
}
173-
gaussian.mean /= static_cast<double>(indices.size());
174-
175-
Eigen::Matrix3d covariance = Eigen::Matrix3d::Zero();
176-
double representative_distance2 = std::numeric_limits<double>::max();
177-
for (const int idx : indices) {
178-
const Eigen::Vector3d centered =
179-
target.points_[idx] - gaussian.mean;
180-
covariance += centered * centered.transpose();
181-
const double distance2 = centered.squaredNorm();
182-
if (distance2 < representative_distance2) {
183-
representative_distance2 = distance2;
184-
gaussian.representative_index = idx;
185-
}
186-
}
187-
covariance /= static_cast<double>(indices.size() - 1);
185+
gaussian.count = accumulator.count;
186+
gaussian.mean = accumulator.mean;
187+
const Eigen::Matrix3d covariance =
188+
accumulator.covariance_accumulator /
189+
static_cast<double>(accumulator.count - 1);
188190

189191
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> solver(covariance);
190192
if (solver.info() != Eigen::Success) {
@@ -205,6 +207,22 @@ VoxelMap BuildVoxelGaussians(const geometry::PointCloud &target,
205207
solver.eigenvectors().transpose();
206208
voxel_map.emplace(item.first, gaussian);
207209
}
210+
211+
for (int i = 0; i < static_cast<int>(target.points_.size()); ++i) {
212+
const Eigen::Vector3d &point = target.points_[i];
213+
auto voxel_itr = voxel_map.find(GetVoxelKey(point, inv_voxel_size));
214+
if (voxel_itr == voxel_map.end()) {
215+
continue;
216+
}
217+
VoxelGaussian &gaussian = voxel_itr->second;
218+
const double distance2 = (point - gaussian.mean).squaredNorm();
219+
if (gaussian.representative_index < 0 ||
220+
distance2 < (target.points_[gaussian.representative_index] -
221+
gaussian.mean)
222+
.squaredNorm()) {
223+
gaussian.representative_index = i;
224+
}
225+
}
208226
return voxel_map;
209227
}
210228

cpp/open3d/pipelines/registration/NormalDistributionsTransform.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ class NormalDistributionsTransformOption {
7979
///
8080
/// This implementation builds a voxelized Gaussian model from the target point
8181
/// cloud and optimizes the rigid source-to-target transformation with
82-
/// Gauss-Newton iterations.
82+
/// Gauss-Newton iterations. The returned correspondence set uses the target
83+
/// point closest to each accepted voxel mean as its representative, and the
84+
/// inlier RMSE is computed from those representative point correspondences.
8385
///
8486
/// \param source The source point cloud.
8587
/// \param target The target point cloud.

cpp/pybind/pipelines/registration/registration.cpp

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -418,18 +418,8 @@ Sets :math:`c = 1` if ``with_scaling`` is ``False``.
418418
py::detail::bind_copy_functions<NormalDistributionsTransformOption>(
419419
ndt_option);
420420
ndt_option
421-
.def(py::init([](double voxel_size, int min_points_per_voxel,
422-
double covariance_regularization,
423-
double transformation_epsilon,
424-
double relative_objective, int max_iteration,
425-
double outlier_threshold,
426-
int neighbor_search_type) {
427-
return new NormalDistributionsTransformOption(
428-
voxel_size, min_points_per_voxel,
429-
covariance_regularization, transformation_epsilon,
430-
relative_objective, max_iteration,
431-
outlier_threshold, neighbor_search_type);
432-
}),
421+
.def(py::init<double, int, double, double, double, int, double,
422+
int>(),
433423
"voxel_size"_a = 1.0, "min_points_per_voxel"_a = 6,
434424
"covariance_regularization"_a = 1e-3,
435425
"transformation_epsilon"_a = 1e-6,
@@ -773,8 +763,6 @@ must hold true for all edges.)");
773763
"the "
774764
"source point's correspondence is itself."},
775765
{"option", "Registration option"},
776-
{"ndt_option",
777-
"Normal Distributions Transform registration option."},
778766
{"ransac_n",
779767
"Fit ransac with ``ransac_n`` correspondences"},
780768
{"source_feature", "Source point cloud feature."},
@@ -834,8 +822,11 @@ must hold true for all edges.)");
834822
"source"_a, "target"_a,
835823
"option"_a = NormalDistributionsTransformOption(),
836824
"init"_a = Eigen::Matrix4d::Identity());
825+
auto ndt_argument_docstrings = map_shared_argument_docstrings;
826+
ndt_argument_docstrings["option"] =
827+
"Normal Distributions Transform registration option.";
837828
docstring::FunctionDocInject(m_registration, "registration_ndt",
838-
map_shared_argument_docstrings);
829+
ndt_argument_docstrings);
839830

840831
m_registration.def(
841832
"registration_ransac_based_on_correspondence",

docs/tutorial/pipelines/ndt_registration.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ Open3D exposes NDT through
1515
collected in ``NormalDistributionsTransformOption``, including both voxel
1616
Gaussian model parameters and convergence criteria. Optimization stops when
1717
the pose update is small or the relative change in mean Mahalanobis objective
18-
falls below the configured threshold:
18+
falls below the configured threshold. In the returned ``RegistrationResult``,
19+
each accepted voxel is represented by the target point closest to its mean,
20+
and ``inlier_rmse`` is the Euclidean RMSE over those representative point
21+
correspondences:
1922

2023
.. literalinclude:: ../../../examples/python/pipelines/ndt_registration.py
2124
:language: python

0 commit comments

Comments
 (0)