ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
ecvFeature.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 
8 #include "ecvFeature.h"
9 
10 #include <Logging.h>
11 #include <Parallel.h>
12 
13 #include "ecvKDTreeFlann.h"
14 #include "ecvPointCloud.h"
15 
16 // EIGEN
17 #include <Eigen/Dense>
18 
19 namespace cloudViewer {
20 namespace utility {
21 
22 static Eigen::Vector4d ComputePairFeatures(const Eigen::Vector3d &p1,
23  const Eigen::Vector3d &n1,
24  const Eigen::Vector3d &p2,
25  const Eigen::Vector3d &n2) {
26  Eigen::Vector4d result;
27  Eigen::Vector3d dp2p1 = p2 - p1;
28  result(3) = dp2p1.norm();
29  if (result(3) == 0.0) {
30  return Eigen::Vector4d::Zero();
31  }
32  auto n1_copy = n1;
33  auto n2_copy = n2;
34  double angle1 = n1_copy.dot(dp2p1) / result(3);
35  double angle2 = n2_copy.dot(dp2p1) / result(3);
36  if (acos(fabs(angle1)) > acos(fabs(angle2))) {
37  n1_copy = n2;
38  n2_copy = n1;
39  dp2p1 *= -1.0;
40  result(2) = -angle2;
41  } else {
42  result(2) = angle1;
43  }
44  auto v = dp2p1.cross(n1_copy);
45  double v_norm = v.norm();
46  if (v_norm == 0.0) {
47  return Eigen::Vector4d::Zero();
48  }
49  v /= v_norm;
50  auto w = n1_copy.cross(v);
51  result(1) = v.dot(n2_copy);
52  result(0) = atan2(w.dot(n2_copy), n1_copy.dot(n2_copy));
53  return result;
54 }
55 
56 static std::shared_ptr<Feature> ComputeSPFHFeature(
57  const ccPointCloud &input,
58  const geometry::KDTreeFlann &kdtree,
59  const geometry::KDTreeSearchParam &search_param,
60  const utility::optional<std::vector<size_t>> &indices =
62  const bool filter_spfh = indices.has_value();
63  const auto spfh_indices = indices.value_or(std::vector<size_t>());
64 
65  const size_t n_spfh = filter_spfh ? spfh_indices.size() : input.size();
66  auto feature = std::make_shared<Feature>();
67  feature->Resize(33, (int)n_spfh);
68 
69 #pragma omp parallel for schedule(static) \
70  num_threads(utility::EstimateMaxThreads())
71  for (int i = 0; i < (int)n_spfh; i++) {
72  const int point_idx = filter_spfh ? spfh_indices[i] : i;
73  const auto &point = input.getEigenPoint(point_idx);
74  const auto &normal = input.getEigenNormal(point_idx);
75  std::vector<int> indices;
76  std::vector<double> distance2;
77  if (kdtree.Search(point, search_param, indices, distance2) > 1) {
78  // only compute SPFH feature when a point has neighbors
79  double hist_incr = 100.0 / (double)(indices.size() - 1);
80  for (size_t k = 1; k < indices.size(); k++) {
81  // skip the point itself, compute histogram
82  auto pf = ComputePairFeatures(point, normal,
83  input.getEigenPoint(indices[k]),
84  input.getEigenNormal(indices[k]));
85  int h_index = (int)(floor(11 * (pf(0) + M_PI) / (2.0 * M_PI)));
86  if (h_index < 0) h_index = 0;
87  if (h_index >= 11) h_index = 10;
88  feature->data_(h_index, i) += hist_incr;
89  h_index = (int)(floor(11 * (pf(1) + 1.0) * 0.5));
90  if (h_index < 0) h_index = 0;
91  if (h_index >= 11) h_index = 10;
92  feature->data_(h_index + 11, i) += hist_incr;
93  h_index = (int)(floor(11 * (pf(2) + 1.0) * 0.5));
94  if (h_index < 0) h_index = 0;
95  if (h_index >= 11) h_index = 10;
96  feature->data_(h_index + 22, i) += hist_incr;
97  }
98  }
99  }
100  return feature;
101 }
102 
103 std::shared_ptr<Feature> Feature::SelectByIndex(
104  const std::vector<size_t> &indices, bool invert /* = false */) const {
105  auto output = std::make_shared<Feature>();
106 
107  std::vector<bool> mask = std::vector<bool>(data_.cols(), invert);
108  size_t n_output_features = 0;
109  for (size_t i : indices) {
110  if (i < mask.size()) {
111  if (mask[i] == invert) {
112  mask[i] = !invert;
113  n_output_features++;
114  }
115  } else {
117  "[SelectByIndex] contains index {} that is "
118  "not within the bounds",
119  (int)i);
120  }
121  }
122  if (invert) {
123  n_output_features = data_.cols() - n_output_features;
124  }
125 
126  output->Resize(data_.rows(), n_output_features);
127 
128  for (size_t i = 0, current_col_feature = 0;
129  i < static_cast<size_t>(data_.cols()); i++) {
130  if (mask[i]) {
131  output->data_.col(current_col_feature++) = data_.col(i);
132  }
133  }
134 
136  "[SelectByIndex] Feature group down sampled from {:d} features to "
137  "{:d} features.",
138  (int)data_.cols(), (int)output->data_.cols());
139 
140  return output;
141 }
142 
143 std::shared_ptr<Feature> ComputeFPFHFeature(
144  const ccPointCloud &input,
146  &search_param /* = geometry::KDTreeSearchParamKNN()*/,
147  const utility::optional<std::vector<size_t>>
148  &indices /* = utility::nullopt*/) {
149  if (!input.hasNormals()) {
151  "[ComputeFPFHFeature] Failed because input point cloud has no "
152  "normal.");
153  }
154  const bool filter_fpfh = indices.has_value();
155  std::vector<int> fpfh_indices;
156  if (filter_fpfh) {
157  std::vector<bool> mask_fpfh(input.size(), false);
158  for (auto idx : indices.value()) {
159  if (idx < mask_fpfh.size()) {
160  if (!mask_fpfh[idx]) {
161  mask_fpfh[idx] = true;
162  }
163  } else {
165  "[ComputeFPFHFeature] contains index {} that is "
166  "not within the bounds",
167  idx);
168  }
169  }
170  fpfh_indices.reserve(indices.value().size());
171  for (size_t i = 0; i < mask_fpfh.size(); i++) {
172  if (mask_fpfh[i]) {
173  fpfh_indices.push_back(i);
174  }
175  }
176  }
177 
178  const size_t n_fpfh = filter_fpfh ? fpfh_indices.size() : input.size();
179 
180  geometry::KDTreeFlann kdtree(input);
181 
182  std::vector<size_t> spfh_indices;
183  std::vector<int> map_point_idx_to_spfh_idx;
184  std::vector<std::vector<int>> map_fpfh_idx_to_indices;
185  std::vector<std::vector<double>> map_fpfh_idx_to_distance2;
186  if (filter_fpfh) {
187  // compute neighbors of the selected points
188  // using vector<uint8_t> as a boolean mask for the parallel loop
189  // since vector<bool> is not thread safe in writing.
190  std::vector<uint8_t> mask_spfh(input.size(), 0);
191  map_fpfh_idx_to_indices = std::vector<std::vector<int>>(n_fpfh);
192  map_fpfh_idx_to_distance2 = std::vector<std::vector<double>>(n_fpfh);
193 #pragma omp parallel for schedule(static) \
194  num_threads(utility::EstimateMaxThreads())
195  for (int i = 0; i < (int)n_fpfh; i++) {
196  const auto &point = input.getEigenPoint(fpfh_indices[i]);
197  std::vector<int> p_indices;
198  std::vector<double> p_distance2;
199  kdtree.Search(point, search_param, p_indices, p_distance2);
200  for (size_t k = 0; k < p_indices.size(); k++) {
201  if (!mask_spfh[p_indices[k]]) {
202  mask_spfh[p_indices[k]] = 1;
203  }
204  }
205  map_fpfh_idx_to_indices[i] = std::move(p_indices);
206  map_fpfh_idx_to_distance2[i] = std::move(p_distance2);
207  }
208  size_t spfh_indices_reserve_factor;
209  switch (search_param.GetSearchType()) {
211  spfh_indices_reserve_factor =
212  ((const geometry::KDTreeSearchParamKNN &)search_param)
213  .knn_;
214  break;
216  spfh_indices_reserve_factor =
218  search_param)
219  .max_nn_;
220  break;
221  default:
222  spfh_indices_reserve_factor = 30;
223  }
224  spfh_indices.reserve(spfh_indices_reserve_factor * fpfh_indices.size());
225  map_point_idx_to_spfh_idx = std::vector<int>(input.size(), -1);
226  for (size_t i = 0; i < mask_spfh.size(); i++) {
227  if (mask_spfh[i]) {
228  map_point_idx_to_spfh_idx[i] = spfh_indices.size();
229  spfh_indices.push_back(i);
230  }
231  }
232  }
233 
234  auto feature = std::make_shared<Feature>();
235  feature->Resize(33, (int)n_fpfh);
236 
237  auto spfh = filter_fpfh ? ComputeSPFHFeature(input, kdtree, search_param,
238  spfh_indices)
239  : ComputeSPFHFeature(input, kdtree, search_param);
240  if (spfh == nullptr) {
241  utility::LogError("Internal error: SPFH feature is nullptr.");
242  }
243 #pragma omp parallel for schedule(static) \
244  num_threads(utility::EstimateMaxThreads())
245  for (int i = 0; i < (int)n_fpfh; i++) {
246  int i_spfh;
247  std::vector<int> p_indices;
248  std::vector<double> p_distance2;
249  if (filter_fpfh) {
250  i_spfh = map_point_idx_to_spfh_idx[fpfh_indices[i]];
251  p_indices = std::move(map_fpfh_idx_to_indices[i]);
252  p_distance2 = std::move(map_fpfh_idx_to_distance2[i]);
253  } else {
254  i_spfh = i;
255  kdtree.Search(input.getEigenPoint(i), search_param, p_indices,
256  p_distance2);
257  }
258  if (p_indices.size() > 1) {
259  double sum[3] = {0.0, 0.0, 0.0};
260  for (size_t k = 1; k < p_indices.size(); k++) {
261  // skip the point itself
262  double dist = p_distance2[k];
263  if (dist == 0.0) continue;
264  int p_index_k =
265  filter_fpfh ? map_point_idx_to_spfh_idx[p_indices[k]]
266  : p_indices[k];
267  for (int j = 0; j < 33; j++) {
268  double val = spfh->data_(j, p_index_k) / dist;
269  sum[j / 11] += val;
270  feature->data_(j, i) += val;
271  }
272  }
273  for (int j = 0; j < 3; j++)
274  if (sum[j] != 0.0) sum[j] = 100.0 / sum[j];
275  for (int j = 0; j < 33; j++) {
276  feature->data_(j, i) *= sum[j / 11];
277  // The commented line is the fpfh function in the paper.
278  // But according to PCL implementation, it is skipped.
279  // Our initial test shows that the full fpfh function in the
280  // paper seems to be better than PCL implementation. Further
281  // test required.
282  feature->data_(j, i) += spfh->data_(j, i_spfh);
283  }
284  }
285  }
286 
288  "[ComputeFPFHFeature] Computed {:d} features from "
289  "input point cloud with {:d} points.",
290  (int)feature->data_.cols(), (int)input.size());
291 
292  return feature;
293 }
294 
296  const Feature &target_features,
297  bool mutual_filter,
298  float mutual_consistent_ratio) {
299  const int num_searches = mutual_filter ? 2 : 1;
300 
301  // Access by reference, since Eigen Matrix could be copied
302  std::array<std::reference_wrapper<const Feature>, 2> features{
303  std::reference_wrapper<const Feature>(source_features),
304  std::reference_wrapper<const Feature>(target_features)};
305  std::array<int, 2> num_pts{int(source_features.data_.cols()),
306  int(target_features.data_.cols())};
307  std::vector<CorrespondenceSet> corres(num_searches);
308 
309  const int kMaxThreads = utility::EstimateMaxThreads();
310  const int kOuterThreads = std::min(kMaxThreads, num_searches);
311  const int kInnerThreads = std::max(kMaxThreads / num_searches, 1);
312  (void)kOuterThreads; // Avoids compiler warning if OpenMP is disabled
313  (void)kInnerThreads;
314 #pragma omp parallel for num_threads(kOuterThreads)
315  for (int k = 0; k < num_searches; ++k) {
316  geometry::KDTreeFlann kdtree(features[1 - k]);
317 
318  int num_pts_k = num_pts[k];
319  corres[k] = CorrespondenceSet(num_pts_k);
320 #pragma omp parallel for num_threads(kInnerThreads)
321  for (int i = 0; i < num_pts_k; i++) {
322  std::vector<int> corres_tmp(1);
323  std::vector<double> dist_tmp(1);
324 
325  kdtree.SearchKNN(Eigen::VectorXd(features[k].get().data_.col(i)), 1,
326  corres_tmp, dist_tmp);
327  int j = corres_tmp[0];
328  corres[k][i] = Eigen::Vector2i(i, j);
329  }
330  }
331 
332  // corres[0]: corres_ij, corres[1]: corres_ji
333  if (!mutual_filter) return corres[0];
334 
335  // should not use parallel for due to emplace back
336  CorrespondenceSet corres_mutual;
337  int num_src_pts = num_pts[0];
338  for (int i = 0; i < num_src_pts; ++i) {
339  int j = corres[0][i](1);
340  if (corres[1][j](1) == i) {
341  corres_mutual.emplace_back(i, j);
342  }
343  }
344 
345  // Empirically mutual correspondence set should not be too small
346  if (int(corres_mutual.size()) >=
347  int(mutual_consistent_ratio * num_src_pts)) {
348  utility::LogDebug("{:d} correspondences remain after mutual filter",
349  corres_mutual.size());
350  return corres_mutual;
351  }
353  "Too few correspondences ({:d}) after mutual filter, fall back to "
354  "original correspondences.",
355  corres_mutual.size());
356  return corres[0];
357 }
358 
359 } // namespace utility
360 } // namespace cloudViewer
constexpr double M_PI
Pi.
Definition: CVConst.h:19
double normal[3]
core::Tensor result
Definition: VtkUtils.cpp:76
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
Eigen::Vector3d getEigenNormal(size_t index) const
bool hasNormals() const override
Returns whether normals are enabled or not.
unsigned size() const override
Definition: PointCloudTpl.h:38
Eigen::Vector3d getEigenPoint(size_t index) const
KDTree with FLANN for nearest neighbor search.
int SearchKNN(const T &query, int knn, std::vector< int > &indices, std::vector< double > &distance2) const
int Search(const T &query, const KDTreeSearchParam &param, std::vector< int > &indices, std::vector< double > &distance2) const
KDTree search parameters for hybrid KNN and radius search.
KDTree search parameters for pure KNN search.
Base class for KDTree search parameters.
SearchType GetSearchType() const
Get the search type (KNN, Radius, Hybrid) for the search parameter.
Class to store featrues for registration.
Definition: ecvFeature.h:29
std::shared_ptr< Feature > SelectByIndex(const std::vector< size_t > &indices, bool invert=false) const
Selects features from input Feature group, with indices in indices, and returns a new Feature group w...
Definition: ecvFeature.cpp:103
Eigen::MatrixXd data_
Data buffer storing features.
Definition: ecvFeature.h:54
#define LogWarning(...)
Definition: Logging.h:72
#define LogError(...)
Definition: Logging.h:60
#define LogDebug(...)
Definition: Logging.h:90
static std::shared_ptr< Feature > ComputeSPFHFeature(const ccPointCloud &input, const geometry::KDTreeFlann &kdtree, const geometry::KDTreeSearchParam &search_param, const utility::optional< std::vector< size_t >> &indices=utility::nullopt)
Definition: ecvFeature.cpp:56
static Eigen::Vector4d ComputePairFeatures(const Eigen::Vector3d &p1, const Eigen::Vector3d &n1, const Eigen::Vector3d &p2, const Eigen::Vector3d &n2)
Definition: ecvFeature.cpp:22
int EstimateMaxThreads()
Estimate the maximum number of threads to be used in a parallel region.
Definition: Parallel.cpp:31
std::vector< Eigen::Vector2i > CorrespondenceSet
Definition: ecvFeature.h:24
MiniVec< float, N > floor(const MiniVec< float, N > &a)
Definition: MiniVec.h:75
constexpr nullopt_t nullopt
Definition: Optional.h:136
std::shared_ptr< Feature > ComputeFPFHFeature(const ccPointCloud &input, const geometry::KDTreeSearchParam &search_param=geometry::KDTreeSearchParamKNN(), const utility::optional< std::vector< size_t >> &indices=utility::nullopt)
Definition: ecvFeature.cpp:143
CorrespondenceSet CorrespondencesFromFeatures(const Feature &source_features, const Feature &target_features, bool mutual_filter=false, float mutual_consistency_ratio=0.1)
Function to find correspondences via 1-nearest neighbor feature matching. Target is used to construct...
Definition: ecvFeature.cpp:295
Generic file read and write utility for python interface.
Eigen::Matrix< Index, 2, 1 > Vector2i
Definition: knncpp.h:29