ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
Feature.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
9 
10 #include <Parallel.h>
11 
16 
17 namespace cloudViewer {
18 namespace t {
19 
20 namespace pipelines {
21 namespace registration {
22 
24  const geometry::PointCloud &input,
25  const utility::optional<int> max_nn,
26  const utility::optional<double> radius,
27  const utility::optional<core::Tensor> &indices) {
29  {core::Float64, core::Float32});
30  if (max_nn.has_value() && max_nn.value() <= 3) {
31  utility::LogError("max_nn must be greater than 3.");
32  }
33  if (radius.has_value() && radius.value() <= 0) {
34  utility::LogError("radius must be greater than 0.");
35  }
36  if (!input.HasPointNormals()) {
37  utility::LogError("The input point cloud has no normal.");
38  }
39 
40  const int64_t num_points = input.GetPointPositions().GetLength();
41  const core::Dtype dtype = input.GetPointPositions().GetDtype();
42  const core::Device device = input.GetPointPositions().GetDevice();
43 
45  core::Int32);
46  bool tree_set = false;
47 
48  const bool filter_fpfh = indices.has_value();
49  // If we are computing a subset of the FPFH feature,
50  // cache some information to speed up the computation
51  // if the ratio of the indices to the total number of points is high.
52  const double cache_info_indices_ratio_thresh = 0.01;
53  bool cache_fpfh_info = true;
54 
55  core::Tensor mask_fpfh_points;
56  core::Tensor indices_fpfh_points;
57  core::Tensor map_point_idx_to_required_point_idx;
58  core::Tensor map_required_point_idx_to_point_idx;
59  core::Tensor save_p_indices, save_p_distance2, save_p_counts;
60  core::Tensor mask_spfh_points;
61 
62  // If we are computing a subset of the FPFH feature, we need to find
63  // the subset of points (neighbors) required to compute the FPFH features.
64  if (filter_fpfh) {
65  if (indices.value().GetLength() == 0) {
66  return core::Tensor::Zeros({0, 33}, dtype, device);
67  }
68  mask_fpfh_points =
69  core::Tensor::Zeros({num_points}, core::Bool, device);
70  mask_fpfh_points.IndexSet({indices.value()},
71  core::Tensor::Ones({1}, core::Bool, device));
72  const core::Tensor query_point_positions =
73  input.GetPointPositions().IndexGet({mask_fpfh_points});
74  core::Tensor p_indices, p_distance2, p_counts;
75  if (radius.has_value() && max_nn.has_value()) {
76  tree_set = tree.HybridIndex(radius.value());
77  if (!tree_set) {
78  utility::LogError("Building HybridIndex failed.");
79  }
80  std::tie(p_indices, p_distance2, p_counts) = tree.HybridSearch(
81  query_point_positions, radius.value(), max_nn.value());
82  } else if (!radius.has_value() && max_nn.has_value()) {
83  tree_set = tree.KnnIndex();
84  if (!tree_set) {
85  utility::LogError("Building KnnIndex failed.");
86  }
87  std::tie(p_indices, p_distance2) =
88  tree.KnnSearch(query_point_positions, max_nn.value());
89 
90  // Make counts full with min(max_nn, num_points).
91  const int fill_value =
92  max_nn.value() > num_points ? num_points : max_nn.value();
93  p_counts = core::Tensor::Full({query_point_positions.GetLength()},
94  fill_value, core::Int32, device);
95  } else if (radius.has_value() && !max_nn.has_value()) {
96  tree_set = tree.FixedRadiusIndex(radius.value());
97  if (!tree_set) {
98  utility::LogError("Building RadiusIndex failed.");
99  }
100  std::tie(p_indices, p_distance2, p_counts) = tree.FixedRadiusSearch(
101  query_point_positions, radius.value());
102  } else {
103  utility::LogError("Both max_nn and radius are none.");
104  }
105 
106  core::Tensor mask_required_points =
107  core::Tensor::Zeros({num_points}, core::Bool, device);
108  mask_required_points.IndexSet(
109  {p_indices.To(core::Int64).View({-1})},
110  core::Tensor::Ones({1}, core::Bool, device));
111  map_required_point_idx_to_point_idx =
112  mask_required_points.NonZero().GetItem(
114  indices_fpfh_points =
115  mask_fpfh_points.NonZero().GetItem({core::TensorKey::Index(0)});
116 
117  const bool is_radius_search = p_indices.GetShape().size() == 1;
118 
119  // Cache the info if the ratio of the indices to the total number of
120  // points is high and we are not doing a radius search. Radius search
121  // requires a different pipeline since tensor output p_counts is a
122  // prefix sum.
123  cache_fpfh_info = !is_radius_search &&
124  (indices_fpfh_points.GetLength() >=
125  cache_info_indices_ratio_thresh * num_points);
126 
127  if (cache_fpfh_info) {
128  map_point_idx_to_required_point_idx =
129  core::Tensor::Full({num_points}, -1, core::Int32, device);
130  map_point_idx_to_required_point_idx.IndexSet(
131  {map_required_point_idx_to_point_idx},
133  0, map_required_point_idx_to_point_idx.GetLength(),
134  1, core::Int32, device));
135 
136  core::SizeVector save_p_indices_shape = p_indices.GetShape();
137  save_p_indices_shape[0] =
138  map_required_point_idx_to_point_idx.GetLength();
139  save_p_indices = core::Tensor::Zeros(save_p_indices_shape,
140  core::Int32, device);
141  save_p_distance2 = core::Tensor::Zeros(save_p_indices.GetShape(),
142  dtype, device);
143  save_p_counts = core::Tensor::Zeros(
144  {map_required_point_idx_to_point_idx.GetLength() +
145  (is_radius_search ? 1 : 0)},
146  core::Int32, device);
147 
148  core::Tensor map_fpfh_point_idx_to_required_point_idx =
149  map_point_idx_to_required_point_idx
150  .IndexGet({indices_fpfh_points})
151  .To(core::Int64);
152 
153  save_p_indices.IndexSet({map_fpfh_point_idx_to_required_point_idx},
154  p_indices);
155  save_p_distance2.IndexSet(
156  {map_fpfh_point_idx_to_required_point_idx}, p_distance2);
157  save_p_counts.IndexSet({map_fpfh_point_idx_to_required_point_idx},
158  p_counts);
159 
160  // If we are filtering FPFH features, we have already computed some
161  // info about the FPFH points' neighbors. Now we just need to
162  // compute the info for the remaining required points, so skip the
163  // computation for the already computed info.
164  mask_spfh_points =
165  core::Tensor::Zeros({num_points}, core::Bool, device);
166  mask_spfh_points.IndexSet(
167  {map_required_point_idx_to_point_idx},
168  core::Tensor::Ones({1}, core::Bool, device));
169  mask_spfh_points.IndexSet(
170  {indices_fpfh_points},
171  core::Tensor::Zeros({1}, core::Bool, device));
172  } else {
173  mask_spfh_points = mask_required_points;
174  }
175  }
176 
177  const core::Tensor query_point_positions =
178  filter_fpfh ? input.GetPointPositions().IndexGet({mask_spfh_points})
179  : input.GetPointPositions();
180 
181  // Compute nearest neighbors and squared distances.
182  core::Tensor p_indices, p_distance2, p_counts;
183 
184  if (radius.has_value() && max_nn.has_value()) {
185  if (!tree_set) {
186  tree_set = tree.HybridIndex(radius.value());
187  if (!tree_set) {
188  utility::LogError("Building HybridIndex failed.");
189  }
190  }
191  std::tie(p_indices, p_distance2, p_counts) = tree.HybridSearch(
192  query_point_positions, radius.value(), max_nn.value());
194  "Use HybridSearch [max_nn: {} | radius {}] for computing FPFH "
195  "feature.",
196  max_nn.value(), radius.value());
197  } else if (!radius.has_value() && max_nn.has_value()) {
198  if (!tree_set) {
199  tree_set = tree.KnnIndex();
200  if (!tree_set) {
201  utility::LogError("Building KnnIndex failed.");
202  }
203  }
204 
205  // tree.KnnSearch complains if the query point cloud is empty.
206  if (query_point_positions.GetLength() > 0) {
207  std::tie(p_indices, p_distance2) =
208  tree.KnnSearch(query_point_positions, max_nn.value());
209 
210  const int fill_value =
211  max_nn.value() > num_points ? num_points : max_nn.value();
212 
213  p_counts = core::Tensor::Full({query_point_positions.GetLength()},
214  fill_value, core::Int32, device);
215  } else {
216  p_indices = core::Tensor::Zeros({0, max_nn.value()}, core::Int32,
217  device);
218  p_distance2 =
219  core::Tensor::Zeros({0, max_nn.value()}, dtype, device);
220  p_counts = core::Tensor::Zeros({0}, core::Int32, device);
221  }
222 
224  "Use KNNSearch [max_nn: {}] for computing FPFH feature.",
225  max_nn.value());
226  } else if (radius.has_value() && !max_nn.has_value()) {
227  if (!tree_set) {
228  tree_set = tree.FixedRadiusIndex(radius.value());
229  if (!tree_set) {
230  utility::LogError("Building RadiusIndex failed.");
231  }
232  }
233  std::tie(p_indices, p_distance2, p_counts) =
234  tree.FixedRadiusSearch(query_point_positions, radius.value());
236  "Use RadiusSearch [radius: {}] for computing FPFH feature.",
237  radius.value());
238  } else {
239  utility::LogError("Both max_nn and radius are none.");
240  }
241 
242  core::Tensor fpfh;
243  if (filter_fpfh) {
244  const int64_t size = indices_fpfh_points.GetLength();
245  fpfh = core::Tensor::Zeros({size, 33}, dtype, device);
246  core::Tensor final_p_indices, final_p_distance2, final_p_counts;
247  if (cache_fpfh_info) {
248  core::Tensor map_spfh_idx_to_required_point_idx =
249  map_point_idx_to_required_point_idx
250  .IndexGet({mask_spfh_points})
251  .To(core::Int64);
252  save_p_indices.IndexSet({map_spfh_idx_to_required_point_idx},
253  p_indices);
254  save_p_distance2.IndexSet({map_spfh_idx_to_required_point_idx},
255  p_distance2);
256  save_p_counts.IndexSet({map_spfh_idx_to_required_point_idx},
257  p_counts);
258  final_p_indices = save_p_indices;
259  final_p_distance2 = save_p_distance2;
260  final_p_counts = save_p_counts;
261  } else {
262  final_p_indices = p_indices;
263  final_p_distance2 = p_distance2;
264  final_p_counts = p_counts;
265  }
267  input.GetPointPositions(), input.GetPointNormals(),
268  final_p_indices, final_p_distance2, final_p_counts, fpfh,
269  mask_fpfh_points, map_required_point_idx_to_point_idx);
270  } else {
271  const int64_t size = input.GetPointPositions().GetLength();
272  fpfh = core::Tensor::Zeros({size, 33}, dtype, device);
274  input.GetPointPositions(), input.GetPointNormals(), p_indices,
275  p_distance2, p_counts, fpfh);
276  }
277  return fpfh;
278 }
279 
281  const core::Tensor &target_features,
282  bool mutual_filter,
283  float mutual_consistent_ratio) {
284  const int num_searches = mutual_filter ? 2 : 1;
285 
286  std::array<core::Tensor, 2> features{source_features, target_features};
287  std::vector<core::Tensor> corres(num_searches);
288 
289  const int kMaxThreads = utility::EstimateMaxThreads();
290  const int kOuterThreads = std::min(kMaxThreads, num_searches);
291  (void)kOuterThreads; // Avoids compiler warning if OpenMP is disabled
292 
293  // corres[0]: corres_ij, corres[1]: corres_ji
294 #pragma omp parallel for num_threads(kOuterThreads)
295  for (int i = 0; i < num_searches; ++i) {
296  core::nns::NearestNeighborSearch nns(features[1 - i],
298  nns.KnnIndex();
299  auto result = nns.KnnSearch(features[i], 1);
300 
301  corres[i] = result.first.View({-1});
302  }
303 
304  auto corres_ij = corres[0];
305  core::Tensor arange_source =
306  core::Tensor::Arange(0, source_features.GetLength(), 1,
307  corres_ij.GetDtype(), corres_ij.GetDevice());
308 
309  // Change view for the appending axis
310  core::Tensor result_ij =
311  arange_source.View({-1, 1}).Append(corres_ij.View({-1, 1}), 1);
312 
313  if (!mutual_filter) {
314  return result_ij;
315  }
316 
317  auto corres_ji = corres[1];
318  // Mutually consistent
319  core::Tensor corres_ii = corres_ji.IndexGet({corres_ij});
320  core::Tensor identical = corres_ii.Eq(arange_source);
321  core::Tensor result_mutual = corres_ij.IndexGet({identical});
322  if (result_mutual.GetLength() >
323  mutual_consistent_ratio * arange_source.GetLength()) {
324  utility::LogDebug("{:d} correspondences remain after mutual filter",
325  result_mutual.GetLength());
326  return arange_source.IndexGet({identical})
327  .View({-1, 1})
328  .Append(result_mutual.View({-1, 1}), 1);
329  }
330  // fall back to full correspondences
332  "Too few correspondences ({:d}) after mutual filter, fall back to "
333  "original correspondences.",
334  result_mutual.GetLength());
335  return result_ij;
336 }
337 
338 } // namespace registration
339 } // namespace pipelines
340 } // namespace t
341 } // namespace cloudViewer
int size
#define AssertTensorDtypes(tensor,...)
Definition: TensorCheck.h:33
core::Tensor result
Definition: VtkUtils.cpp:76
static const Dtype Int64
Definition: Dtype.h:29
static TensorKey Index(int64_t index)
Definition: TensorKey.cpp:134
Tensor NonZero() const
Definition: Tensor.cpp:1755
void IndexSet(const std::vector< Tensor > &index_tensors, const Tensor &src_tensor)
Advanced indexing getter.
Definition: Tensor.cpp:936
Tensor Eq(const Tensor &value) const
Element-wise equals-to of tensors, returning a new boolean tensor.
Definition: Tensor.cpp:1682
static Tensor Arange(const Scalar start, const Scalar stop, const Scalar step=1, const Dtype dtype=core::Int64, const Device &device=core::Device("CPU:0"))
Create a 1D tensor with evenly spaced values in the given interval.
Definition: Tensor.cpp:436
int64_t GetLength() const
Definition: Tensor.h:1125
Dtype GetDtype() const
Definition: Tensor.h:1164
Tensor GetItem(const TensorKey &tk) const
Definition: Tensor.cpp:473
Tensor IndexGet(const std::vector< Tensor > &index_tensors) const
Advanced indexing getter. This will always allocate a new Tensor.
Definition: Tensor.cpp:905
static Tensor Zeros(const SizeVector &shape, Dtype dtype, const Device &device=Device("CPU:0"))
Create a tensor fill with zeros.
Definition: Tensor.cpp:406
Tensor View(const SizeVector &dst_shape) const
Definition: Tensor.cpp:721
Device GetDevice() const override
Definition: Tensor.cpp:1435
static Tensor Ones(const SizeVector &shape, Dtype dtype, const Device &device=Device("CPU:0"))
Create a tensor fill with ones.
Definition: Tensor.cpp:412
SizeVector GetShape() const
Definition: Tensor.h:1127
Tensor To(Dtype dtype, bool copy=false) const
Definition: Tensor.cpp:739
static Tensor Full(const SizeVector &shape, T fill_value, Dtype dtype, const Device &device=Device("CPU:0"))
Create a tensor fill with specified value.
Definition: Tensor.h:253
A Class for nearest neighbor search.
std::tuple< Tensor, Tensor, Tensor > FixedRadiusSearch(const Tensor &query_points, double radius, bool sort=true)
bool HybridIndex(utility::optional< double > radius={})
bool FixedRadiusIndex(utility::optional< double > radius={})
std::tuple< Tensor, Tensor, Tensor > HybridSearch(const Tensor &query_points, const double radius, const int max_knn) const
std::pair< Tensor, Tensor > KnnSearch(const Tensor &query_points, int knn)
A point cloud contains a list of 3D points.
Definition: PointCloud.h:82
core::Tensor & GetPointNormals()
Get the value of the "normals" attribute. Convenience function.
Definition: PointCloud.h:130
core::Tensor & GetPointPositions()
Get the value of the "positions" attribute. Convenience function.
Definition: PointCloud.h:124
constexpr bool has_value() const noexcept
Definition: Optional.h:440
constexpr T const & value() const &
Definition: Optional.h:465
#define LogWarning(...)
Definition: Logging.h:72
#define LogError(...)
Definition: Logging.h:60
#define LogDebug(...)
Definition: Logging.h:90
int min(int a, int b)
Definition: cutil_math.h:53
const Dtype Bool
Definition: Dtype.cpp:52
const Dtype Int64
Definition: Dtype.cpp:47
Tensor Append(const Tensor &self, const Tensor &other, const utility::optional< int64_t > &axis)
Appends the two tensors, along the given axis into a new tensor. Both the tensors must have same data...
const Dtype Int32
Definition: Dtype.cpp:46
void To(const core::Tensor &src, core::Tensor &dst, double scale, double offset)
Definition: Image.cpp:17
void ComputeFPFHFeature(const core::Tensor &points, const core::Tensor &normals, const core::Tensor &indices, const core::Tensor &distance2, const core::Tensor &counts, core::Tensor &fpfhs, const utility::optional< core::Tensor > &mask, const utility::optional< core::Tensor > &map_info_idx_to_point_idx)
Definition: Feature.cpp:18
core::Tensor ComputeFPFHFeature(const geometry::PointCloud &input, const utility::optional< int > max_nn, const utility::optional< double > radius, const utility::optional< core::Tensor > &indices)
Definition: Feature.cpp:23
core::Tensor CorrespondencesFromFeatures(const core::Tensor &source_features, const core::Tensor &target_features, bool mutual_filter, float mutual_consistent_ratio)
Function to find correspondences via 1-nearest neighbor feature matching. Target is used to construct...
Definition: Feature.cpp:280
int EstimateMaxThreads()
Estimate the maximum number of threads to be used in a parallel region.
Definition: Parallel.cpp:31
Generic file read and write utility for python interface.