ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
contrib_subsample.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 
11 #include "pybind/docstring.h"
13 #include "pybind/pybind_utils.h"
14 
15 namespace cloudViewer {
16 namespace ml {
17 namespace contrib {
18 
19 const py::tuple SubsampleBatch(py::array points,
20  py::array batches,
23  float sampleDl,
24  const std::string& method,
25  int max_p,
26  int verbose) {
27  std::vector<PointXYZ> original_points;
28  std::vector<PointXYZ> subsampled_points;
29  std::vector<int> original_batches;
30  std::vector<int> subsampled_batches;
31  std::vector<float> original_features;
32  std::vector<float> subsampled_features;
33  std::vector<int> original_classes;
34  std::vector<int> subsampled_classes;
35 
36  // Fill original_points.
38  if (points_t.GetDtype() != core::Float32) {
39  utility::LogError("points must be np.float32.");
40  }
41  if (points_t.NumDims() != 2 || points_t.GetShape()[1] != 3) {
42  utility::LogError("points must have shape (N, 3), but got {}.",
43  points_t.GetShape().ToString());
44  }
45  int64_t num_points = points_t.NumElements() / 3;
46  original_points = std::vector<PointXYZ>(
47  reinterpret_cast<PointXYZ*>(points_t.GetDataPtr()),
48  reinterpret_cast<PointXYZ*>(points_t.GetDataPtr()) + num_points);
49 
50  // Fill original batches.
51  core::Tensor batches_t = core::PyArrayToTensor(batches, true).Contiguous();
52  if (batches_t.GetDtype() != core::Int32) {
53  utility::LogError("batches must be np.int32.");
54  }
55  if (batches_t.NumDims() != 1) {
56  utility::LogError("batches must have shape (NB,), but got {}.",
57  batches_t.GetShape().ToString());
58  }
59  int64_t num_batches = batches_t.GetShape()[0];
60  if (static_cast<int64_t>(batches_t.Sum({0}).Item<int32_t>()) !=
61  num_points) {
62  utility::LogError("batches got {} points, but points got {} points.",
63  batches_t.Sum({0}).Item<int32_t>(), num_points);
64  }
65  original_batches = batches_t.ToFlatVector<int32_t>();
66  if (verbose) {
67  utility::LogInfo("Got {} batches with a total of {} points as inputs.",
68  num_batches, num_points);
69  }
70 
71  // Fill original_features.
72  int64_t feature_dim = -1;
73  if (features.has_value()) {
74  core::Tensor features_t =
75  core::PyArrayToTensor(features.value(), true).Contiguous();
76  if (features_t.GetDtype() != core::Float32) {
77  utility::LogError("features must be np.float32.");
78  }
79  if (features_t.NumDims() != 2) {
80  utility::LogError("features must have shape (N, d), but got {}.",
81  features_t.GetShape().ToString());
82  }
83  if (features_t.GetShape()[0] != num_points) {
85  "features's shape {} is not compatible with "
86  "points's shape {}, their first dimension must "
87  "be equal.",
88  features_t.GetShape().ToString(),
89  points_t.GetShape().ToString());
90  }
91  feature_dim = features_t.GetShape()[1];
92  original_features = features_t.ToFlatVector<float>();
93  }
94 
95  // Fill original_classes.
96  if (classes.has_value()) {
97  core::Tensor classes_t =
98  core::PyArrayToTensor(classes.value(), true).Contiguous();
99  if (classes_t.GetDtype() != core::Int32) {
100  utility::LogError("classes must be np.int32.");
101  }
102  if (classes_t.NumDims() != 1) {
103  utility::LogError("classes must have shape (N,), but got {}.",
104  classes_t.GetShape().ToString());
105  }
106  if (classes_t.GetShape()[0] != num_points) {
108  "classes's shape {} is not compatible with "
109  "points's shape {}, their first dimension must "
110  "be equal.",
111  classes_t.GetShape().ToString(),
112  points_t.GetShape().ToString());
113  }
114  original_classes = classes_t.ToFlatVector<int32_t>();
115  }
116 
117  // Call function.
119  original_points, subsampled_points, original_features,
120  subsampled_features, original_classes, subsampled_classes,
121  original_batches, subsampled_batches, sampleDl, max_p);
122 
123  // Wrap result subsampled_points. Data will be copied.
124  int64_t num_subsampled_points =
125  static_cast<int64_t>(subsampled_points.size());
126  core::Tensor subsampled_points_t(
127  reinterpret_cast<float*>(subsampled_points.data()),
128  {num_subsampled_points, 3}, core::Float32);
129 
130  // Wrap result subsampled_batches. Data will be copied.
131  int64_t num_subsampled_batches =
132  static_cast<int64_t>(subsampled_batches.size());
133  core::Tensor subsampled_batches_t = core::Tensor(
134  subsampled_batches, {num_subsampled_batches}, core::Int32);
135  if (static_cast<int64_t>(subsampled_batches_t.Sum({0}).Item<int32_t>()) !=
136  num_subsampled_points) {
138  "subsampled_batches got {} points, but subsampled_points got "
139  "{} points.",
140  subsampled_batches_t.Sum({0}).Item<int32_t>(),
141  num_subsampled_points);
142  }
143  if (verbose) {
144  utility::LogInfo("Subsampled to {} batches with a total of {} points.",
145  num_subsampled_batches, num_subsampled_points);
146  }
147 
148  // Wrap result subsampled_features. Data will be copied.
149  core::Tensor subsampled_features_t;
150  if (features.has_value()) {
151  if (subsampled_features.size() % num_subsampled_points != 0) {
153  "Error: subsampled_points.size() {} is not a "
154  "multiple of num_subsampled_points {}.",
155  subsampled_points.size(), num_subsampled_points);
156  }
157  int64_t subsampled_feature_dim =
158  static_cast<int64_t>(subsampled_features.size()) /
159  num_subsampled_points;
160  if (feature_dim != subsampled_feature_dim) {
162  "Error: input feature dim {} does not match "
163  "the subsampled feature dim {}.",
164  feature_dim, subsampled_feature_dim);
165  }
166  subsampled_features_t = core::Tensor(
167  subsampled_features, {num_subsampled_points, feature_dim},
168  core::Float32);
169  }
170 
171  // Wrap result subsampled_classes. Data will be copied.
172  core::Tensor subsampled_classes_t;
173  if (classes.has_value()) {
174  if (static_cast<int64_t>(subsampled_classes.size()) !=
175  num_subsampled_points) {
177  "Error: subsampled_classes.size() {} != "
178  "num_subsampled_points {}.",
179  subsampled_classes.size(), num_subsampled_points);
180  }
181  subsampled_classes_t = core::Tensor(
182  subsampled_classes, {num_subsampled_points}, core::Int32);
183  }
184 
185  if (features.has_value() && classes.has_value()) {
186  return py::make_tuple(core::TensorToPyArray(subsampled_points_t),
187  core::TensorToPyArray(subsampled_batches_t),
188  core::TensorToPyArray(subsampled_features_t),
189  core::TensorToPyArray(subsampled_classes_t));
190  } else if (features.has_value()) {
191  return py::make_tuple(core::TensorToPyArray(subsampled_points_t),
192  core::TensorToPyArray(subsampled_batches_t),
193  core::TensorToPyArray(subsampled_features_t));
194  } else if (classes.has_value()) {
195  return py::make_tuple(core::TensorToPyArray(subsampled_points_t),
196  core::TensorToPyArray(subsampled_batches_t),
197  core::TensorToPyArray(subsampled_classes_t));
198  } else {
199  return py::make_tuple(core::TensorToPyArray(subsampled_points_t),
200  core::TensorToPyArray(subsampled_batches_t));
201  }
202 }
203 
204 const py::object Subsample(py::array points,
207  float sampleDl,
208  int verbose) {
209  std::vector<PointXYZ> original_points;
210  std::vector<PointXYZ> subsampled_points;
211  std::vector<float> original_features;
212  std::vector<float> subsampled_features;
213  std::vector<int> original_classes;
214  std::vector<int> subsampled_classes;
215 
216  // Fill original_points.
218  if (points_t.GetDtype() != core::Float32) {
219  utility::LogError("points must be np.float32.");
220  }
221  if (points_t.NumDims() != 2 || points_t.GetShape()[1] != 3) {
222  utility::LogError("points must have shape (N, 3), but got {}.",
223  points_t.GetShape().ToString());
224  }
225  int64_t num_points = points_t.NumElements() / 3;
226  original_points = std::vector<PointXYZ>(
227  reinterpret_cast<PointXYZ*>(points_t.GetDataPtr()),
228  reinterpret_cast<PointXYZ*>(points_t.GetDataPtr()) + num_points);
229  if (verbose) {
230  utility::LogInfo("Got {} points as inputs.", num_points);
231  }
232 
233  // Fill original_features.
234  int64_t feature_dim = -1;
235  if (features.has_value()) {
236  core::Tensor features_t =
237  core::PyArrayToTensor(features.value(), true).Contiguous();
238  if (features_t.GetDtype() != core::Float32) {
239  utility::LogError("features must be np.float32.");
240  }
241  if (features_t.NumDims() != 2) {
242  utility::LogError("features must have shape (N, d), but got {}.",
243  features_t.GetShape().ToString());
244  }
245  if (features_t.GetShape()[0] != num_points) {
247  "features's shape {} is not compatible with "
248  "points's shape {}, their first dimension must "
249  "be equal.",
250  features_t.GetShape().ToString(),
251  points_t.GetShape().ToString());
252  }
253  feature_dim = features_t.GetShape()[1];
254  original_features = features_t.ToFlatVector<float>();
255  }
256 
257  // Fill original_classes.
258  if (classes.has_value()) {
259  core::Tensor classes_t =
260  core::PyArrayToTensor(classes.value(), true).Contiguous();
261  if (classes_t.GetDtype() != core::Int32) {
262  utility::LogError("classes must be np.int32.");
263  }
264  if (classes_t.NumDims() != 1) {
265  utility::LogError("classes must have shape (N,), but got {}.",
266  classes_t.GetShape().ToString());
267  }
268  if (classes_t.GetShape()[0] != num_points) {
270  "classes's shape {} is not compatible with "
271  "points's shape {}, their first dimension must "
272  "be equal.",
273  classes_t.GetShape().ToString(),
274  points_t.GetShape().ToString());
275  }
276  original_classes = classes_t.ToFlatVector<int32_t>();
277  }
278 
279  // Call function.
280  grid_subsampling(original_points, subsampled_points, original_features,
281  subsampled_features, original_classes, subsampled_classes,
282  sampleDl, verbose);
283 
284  // Wrap result subsampled_points. Data will be copied.
285  int64_t num_subsampled_points =
286  static_cast<int64_t>(subsampled_points.size());
287  core::Tensor subsampled_points_t(
288  reinterpret_cast<float*>(subsampled_points.data()),
289  {num_subsampled_points, 3}, core::Float32);
290  if (verbose) {
291  utility::LogInfo("Subsampled to {} points.", num_subsampled_points);
292  }
293 
294  // Wrap result subsampled_features. Data will be copied.
295  core::Tensor subsampled_features_t;
296  if (features.has_value()) {
297  if (subsampled_features.size() % num_subsampled_points != 0) {
299  "Error: subsampled_points.size() {} is not a "
300  "multiple of num_subsampled_points {}.",
301  subsampled_points.size(), num_subsampled_points);
302  }
303  int64_t subsampled_feature_dim =
304  static_cast<int64_t>(subsampled_features.size()) /
305  num_subsampled_points;
306  if (feature_dim != subsampled_feature_dim) {
308  "Error: input feature dim {} does not match "
309  "the subsampled feature dim {}.",
310  feature_dim, subsampled_feature_dim);
311  }
312  subsampled_features_t = core::Tensor(
313  subsampled_features, {num_subsampled_points, feature_dim},
314  core::Float32);
315  }
316 
317  // Wrap result subsampled_classes. Data will be copied.
318  core::Tensor subsampled_classes_t;
319  if (classes.has_value()) {
320  if (static_cast<int64_t>(subsampled_classes.size()) !=
321  num_subsampled_points) {
323  "Error: subsampled_classes.size() {} != "
324  "num_subsampled_points {}.",
325  subsampled_classes.size(), num_subsampled_points);
326  }
327  subsampled_classes_t = core::Tensor(
328  subsampled_classes, {num_subsampled_points}, core::Int32);
329  }
330 
331  if (features.has_value() && classes.has_value()) {
332  return py::make_tuple(core::TensorToPyArray(subsampled_points_t),
333  core::TensorToPyArray(subsampled_features_t),
334  core::TensorToPyArray(subsampled_classes_t));
335  } else if (features.has_value()) {
336  return py::make_tuple(core::TensorToPyArray(subsampled_points_t),
337  core::TensorToPyArray(subsampled_features_t));
338  } else if (classes.has_value()) {
339  return py::make_tuple(core::TensorToPyArray(subsampled_points_t),
340  core::TensorToPyArray(subsampled_classes_t));
341  } else {
342  return core::TensorToPyArray(subsampled_points_t);
343  }
344 }
345 
346 void pybind_contrib_subsample(py::module& m_contrib) {
347  m_contrib.def("subsample", &Subsample, "points"_a,
348  "features"_a = py::none(), "classes"_a = py::none(),
349  "sampleDl"_a = 0.1, "verbose"_a = 0);
350 
351  m_contrib.def("subsample_batch", &SubsampleBatch, "points"_a, "batches"_a,
352  "features"_a = py::none(), "classes"_a = py::none(),
353  "sampleDl"_a = 0.1, "method"_a = "barycenters", "max_p"_a = 0,
354  "verbose"_a = 0);
355 }
356 
357 } // namespace contrib
358 } // namespace ml
359 } // namespace cloudViewer
int points
std::string ToString() const
Definition: SizeVector.cpp:132
Tensor Contiguous() const
Definition: Tensor.cpp:772
int64_t NumDims() const
Definition: Tensor.h:1172
Tensor Sum(const SizeVector &dims, bool keepdim=false) const
Definition: Tensor.cpp:1240
Dtype GetDtype() const
Definition: Tensor.h:1164
std::vector< T > ToFlatVector() const
Retrieve all values as an std::vector, for debugging and testing.
Definition: Tensor.h:1025
int64_t NumElements() const
Definition: Tensor.h:1170
SizeVector GetShape() const
Definition: Tensor.h:1127
constexpr bool has_value() const noexcept
Definition: Optional.h:440
constexpr T const & value() const &
Definition: Optional.h:465
#define LogInfo(...)
Definition: Logging.h:81
#define LogError(...)
Definition: Logging.h:60
py::array TensorToPyArray(const Tensor &tensor)
Convert Tensor class to py::array (Numpy array).
Tensor PyArrayToTensor(py::array array, bool inplace)
const Dtype Int32
Definition: Dtype.cpp:46
const Dtype Float32
Definition: Dtype.cpp:42
void pybind_contrib_subsample(py::module &m_contrib)
const py::object Subsample(py::array points, utility::optional< py::array > features, utility::optional< py::array > classes, float sampleDl, int verbose)
void batch_grid_subsampling(std::vector< PointXYZ > &original_points, std::vector< PointXYZ > &subsampled_points, std::vector< float > &original_features, std::vector< float > &subsampled_features, std::vector< int > &original_classes, std::vector< int > &subsampled_classes, std::vector< int > &original_batches, std::vector< int > &subsampled_batches, float sampleDl, int max_p)
void grid_subsampling(std::vector< PointXYZ > &original_points, std::vector< PointXYZ > &subsampled_points, std::vector< float > &original_features, std::vector< float > &subsampled_features, std::vector< int > &original_classes, std::vector< int > &subsampled_classes, float sampleDl, int verbose)
const py::tuple SubsampleBatch(py::array points, py::array batches, utility::optional< py::array > features, utility::optional< py::array > classes, float sampleDl, const std::string &method, int max_p, int verbose)
Generic file read and write utility for python interface.