py4dgeo 1.0.0__cp314-cp314t-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _py4dgeo.cp314t-win_amd64.pyd +0 -0
- py4dgeo/UpdateableZipFile.py +81 -0
- py4dgeo/__init__.py +47 -0
- py4dgeo/cloudcompare.py +31 -0
- py4dgeo/epoch.py +865 -0
- py4dgeo/fallback.py +159 -0
- py4dgeo/logger.py +82 -0
- py4dgeo/m3c2.py +246 -0
- py4dgeo/m3c2ep.py +856 -0
- py4dgeo/pbm3c2.py +694 -0
- py4dgeo/py4dgeo_python.cpp +814 -0
- py4dgeo/registration.py +474 -0
- py4dgeo/segmentation.py +1325 -0
- py4dgeo/util.py +348 -0
- py4dgeo-1.0.0.data/platlib/libomp-a12116ba72d1d6820407cf30be23da04.dll +0 -0
- py4dgeo-1.0.0.data/platlib/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
- py4dgeo-1.0.0.dist-info/DELVEWHEEL +2 -0
- py4dgeo-1.0.0.dist-info/METADATA +212 -0
- py4dgeo-1.0.0.dist-info/RECORD +22 -0
- py4dgeo-1.0.0.dist-info/WHEEL +5 -0
- py4dgeo-1.0.0.dist-info/entry_points.txt +3 -0
- py4dgeo-1.0.0.dist-info/licenses/LICENSE.md +5 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
#include <pybind11/eigen.h>
|
|
2
|
+
#include <pybind11/functional.h>
|
|
3
|
+
#include <pybind11/numpy.h>
|
|
4
|
+
#include <pybind11/pybind11.h>
|
|
5
|
+
#include <pybind11/stl.h>
|
|
6
|
+
|
|
7
|
+
#ifdef PY4DGEO_WITH_OPENMP
|
|
8
|
+
#include <omp.h>
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
#include <py4dgeo/compute.hpp>
|
|
12
|
+
#include <py4dgeo/epoch.hpp>
|
|
13
|
+
#include <py4dgeo/kdtree.hpp>
|
|
14
|
+
#include <py4dgeo/octree.hpp>
|
|
15
|
+
#include <py4dgeo/py4dgeo.hpp>
|
|
16
|
+
#include <py4dgeo/pybind11_numpy_interop.hpp>
|
|
17
|
+
#include <py4dgeo/registration.hpp>
|
|
18
|
+
#include <py4dgeo/searchtree.hpp>
|
|
19
|
+
#include <py4dgeo/segmentation.hpp>
|
|
20
|
+
|
|
21
|
+
#include <algorithm>
|
|
22
|
+
#include <cstddef>
|
|
23
|
+
#include <fstream>
|
|
24
|
+
#include <ios>
|
|
25
|
+
#include <optional>
|
|
26
|
+
#include <sstream>
|
|
27
|
+
#include <stdexcept>
|
|
28
|
+
#include <string>
|
|
29
|
+
#include <tuple>
|
|
30
|
+
#include <utility>
|
|
31
|
+
#include <vector>
|
|
32
|
+
|
|
33
|
+
namespace py = pybind11;
|
|
34
|
+
|
|
35
|
+
namespace py4dgeo {
|
|
36
|
+
|
|
37
|
+
PYBIND11_MODULE(_py4dgeo, m)
|
|
38
|
+
{
|
|
39
|
+
m.doc() = "Python Bindings for py4dgeo";
|
|
40
|
+
|
|
41
|
+
// The enum class for our memory policy
|
|
42
|
+
py::enum_<MemoryPolicy>(m, "MemoryPolicy", py::arithmetic())
|
|
43
|
+
.value("STRICT", MemoryPolicy::STRICT)
|
|
44
|
+
.value("MINIMAL", MemoryPolicy::MINIMAL)
|
|
45
|
+
.value("COREPOINTS", MemoryPolicy::COREPOINTS)
|
|
46
|
+
.value("RELAXED", MemoryPolicy::RELAXED)
|
|
47
|
+
.export_values();
|
|
48
|
+
|
|
49
|
+
// The enum class for the type of search tree
|
|
50
|
+
py::enum_<SearchTree>(m, "SearchTree")
|
|
51
|
+
.value("KDTreeSearch", SearchTree::KDTree)
|
|
52
|
+
.value("OctreeSearch", SearchTree::Octree)
|
|
53
|
+
.export_values();
|
|
54
|
+
|
|
55
|
+
// Register a numpy structured type for uncertainty calculation. This allows
|
|
56
|
+
// us to allocate memory in C++ and expose it as a structured numpy array in
|
|
57
|
+
// Python. The given names will be usable in Python.
|
|
58
|
+
PYBIND11_NUMPY_DTYPE(DistanceUncertainty,
|
|
59
|
+
lodetection,
|
|
60
|
+
spread1,
|
|
61
|
+
num_samples1,
|
|
62
|
+
spread2,
|
|
63
|
+
num_samples2);
|
|
64
|
+
|
|
65
|
+
// Also expose the DistanceUncertainty data structure in Python, so that
|
|
66
|
+
// Python fallbacks can use it directly to define their result.
|
|
67
|
+
py::class_<DistanceUncertainty> unc(m, "DistanceUncertainty");
|
|
68
|
+
unc.def(py::init<double, double, IndexType, double, IndexType>(),
|
|
69
|
+
py::arg("lodetection") = 0.0,
|
|
70
|
+
py::arg("spread1") = 0.0,
|
|
71
|
+
py::arg("num_samples1") = 0,
|
|
72
|
+
py::arg("spread2") = 0.0,
|
|
73
|
+
py::arg("num_samples2") = 0);
|
|
74
|
+
|
|
75
|
+
// The epoch class
|
|
76
|
+
py::class_<Epoch> epoch(m, "Epoch");
|
|
77
|
+
|
|
78
|
+
// Initializing with a numpy array prevents the numpy array from being
|
|
79
|
+
// garbage collected as long as the Epoch object is alive
|
|
80
|
+
epoch.def(py::init<EigenPointCloudRef>(), py::keep_alive<1, 2>());
|
|
81
|
+
|
|
82
|
+
// We can directly access the point cloud, the kdtree and the octree
|
|
83
|
+
epoch.def_readwrite("_cloud", &Epoch::cloud);
|
|
84
|
+
epoch.def_readwrite("_kdtree", &Epoch::kdtree);
|
|
85
|
+
epoch.def_readwrite("_octree", &Epoch::octree);
|
|
86
|
+
|
|
87
|
+
epoch.def(
|
|
88
|
+
"_radius_search",
|
|
89
|
+
[](Epoch& self, py::array_t<double> qp, double radius) {
|
|
90
|
+
// Ensure appropriate search tree has been built
|
|
91
|
+
if (Epoch::get_default_radius_search_tree() == SearchTree::KDTree) {
|
|
92
|
+
if (self.kdtree.get_leaf_parameter() == 0) {
|
|
93
|
+
self.kdtree.build_tree(10);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
if (self.octree.get_number_of_points() == 0) {
|
|
97
|
+
self.octree.build_tree();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get a pointer for the query point
|
|
102
|
+
auto ptr = static_cast<const double*>(qp.request().ptr);
|
|
103
|
+
|
|
104
|
+
// Now perform the radius search
|
|
105
|
+
RadiusSearchResult result;
|
|
106
|
+
auto radius_search_func = get_radius_search_function(self, radius);
|
|
107
|
+
Eigen::Vector3d query_point(ptr[0], ptr[1], ptr[2]);
|
|
108
|
+
radius_search_func(query_point, result);
|
|
109
|
+
|
|
110
|
+
return as_pyarray(std::move(result));
|
|
111
|
+
},
|
|
112
|
+
py::arg("query_point"),
|
|
113
|
+
py::arg("radius"),
|
|
114
|
+
"Perform a radius search");
|
|
115
|
+
|
|
116
|
+
// Set and get default search trees
|
|
117
|
+
epoch.def_static(
|
|
118
|
+
"set_default_radius_search_tree",
|
|
119
|
+
[](const std::string& tree_name_input) {
|
|
120
|
+
std::string tree_name = tree_name_input;
|
|
121
|
+
std::transform(
|
|
122
|
+
tree_name.begin(), tree_name.end(), tree_name.begin(), ::tolower);
|
|
123
|
+
|
|
124
|
+
if (tree_name == "kdtree") {
|
|
125
|
+
Epoch::set_default_radius_search_tree(SearchTree::KDTree);
|
|
126
|
+
} else if (tree_name == "octree") {
|
|
127
|
+
Epoch::set_default_radius_search_tree(SearchTree::Octree);
|
|
128
|
+
} else {
|
|
129
|
+
throw std::invalid_argument("Unknown search tree type: " +
|
|
130
|
+
tree_name_input);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
py::arg("tree_name"),
|
|
134
|
+
"Set the default search tree for radius searches (\"kdtree\" or "
|
|
135
|
+
"\"octree\")");
|
|
136
|
+
|
|
137
|
+
epoch.def_static(
|
|
138
|
+
"set_default_nearest_neighbor_tree",
|
|
139
|
+
[](const std::string& tree_name_input) {
|
|
140
|
+
std::string tree_name = tree_name_input;
|
|
141
|
+
std::transform(
|
|
142
|
+
tree_name.begin(), tree_name.end(), tree_name.begin(), ::tolower);
|
|
143
|
+
|
|
144
|
+
if (tree_name == "kdtree") {
|
|
145
|
+
Epoch::set_default_nearest_neighbor_tree(SearchTree::KDTree);
|
|
146
|
+
} else if (tree_name == "octree") {
|
|
147
|
+
Epoch::set_default_nearest_neighbor_tree(SearchTree::Octree);
|
|
148
|
+
} else {
|
|
149
|
+
throw std::invalid_argument("Unknown search tree type: " +
|
|
150
|
+
tree_name_input);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
py::arg("tree_name"),
|
|
154
|
+
"Set the default search tree for nearest neighbor searches (\"kdtree\" or "
|
|
155
|
+
"\"octree\")");
|
|
156
|
+
|
|
157
|
+
epoch.def_static("get_default_radius_search_tree",
|
|
158
|
+
&Epoch::get_default_radius_search_tree);
|
|
159
|
+
|
|
160
|
+
epoch.def_static("get_default_nearest_neighbor_tree",
|
|
161
|
+
&Epoch::get_default_nearest_neighbor_tree);
|
|
162
|
+
|
|
163
|
+
// Pickling support for the Epoch class
|
|
164
|
+
epoch.def(py::pickle(
|
|
165
|
+
[](const Epoch& self) {
|
|
166
|
+
// Serialize into in-memory stream
|
|
167
|
+
std::stringstream buf;
|
|
168
|
+
self.to_stream(buf);
|
|
169
|
+
return py::bytes(buf.str());
|
|
170
|
+
},
|
|
171
|
+
[](const py::bytes& data) {
|
|
172
|
+
std::stringstream buf(data.cast<std::string>());
|
|
173
|
+
return Epoch::from_stream(buf);
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
// Expose the KDTree class
|
|
177
|
+
py::class_<KDTree> kdtree(m, "KDTree", py::buffer_protocol());
|
|
178
|
+
|
|
179
|
+
// Map __init__ to constructor
|
|
180
|
+
kdtree.def(py::init<>(&KDTree::create));
|
|
181
|
+
|
|
182
|
+
// Allow updating KDTree from a given file
|
|
183
|
+
kdtree.def("load_index", [](KDTree& self, std::string filename) {
|
|
184
|
+
std::ifstream stream(filename, std::ios::binary | std::ios::in);
|
|
185
|
+
self.loadIndex(stream);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Allow dumping KDTree to a file
|
|
189
|
+
kdtree.def("save_index", [](const KDTree& self, std::string filename) {
|
|
190
|
+
std::ofstream stream(filename, std::ios::binary | std::ios::out);
|
|
191
|
+
self.saveIndex(stream);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Allow building the KDTree structure
|
|
195
|
+
kdtree.def(
|
|
196
|
+
"build_tree", &KDTree::build_tree, "Trigger building the search k-d tree");
|
|
197
|
+
|
|
198
|
+
// Allow invalidating the KDTree structure
|
|
199
|
+
kdtree.def(
|
|
200
|
+
"invalidate", &KDTree::invalidate, "Invalidate the search k-d tree");
|
|
201
|
+
|
|
202
|
+
// Give access to the leaf parameter that the k-d tree has been built with
|
|
203
|
+
kdtree.def(
|
|
204
|
+
"leaf_parameter",
|
|
205
|
+
&KDTree::get_leaf_parameter,
|
|
206
|
+
"Retrieve the leaf parameter that the k-d tree has been built with.");
|
|
207
|
+
|
|
208
|
+
// Add all the radius search methods
|
|
209
|
+
kdtree.def(
|
|
210
|
+
"radius_search",
|
|
211
|
+
[](const KDTree& self, py::array_t<double> qp, double radius) {
|
|
212
|
+
// Get a pointer for the query point
|
|
213
|
+
auto ptr = static_cast<const double*>(qp.request().ptr);
|
|
214
|
+
|
|
215
|
+
RadiusSearchResult result;
|
|
216
|
+
self.radius_search(ptr, radius, result);
|
|
217
|
+
|
|
218
|
+
return as_pyarray(std::move(result));
|
|
219
|
+
},
|
|
220
|
+
"Search point in given radius!");
|
|
221
|
+
|
|
222
|
+
kdtree.def(
|
|
223
|
+
"nearest_neighbors",
|
|
224
|
+
[](const KDTree& self, EigenPointCloudConstRef cloud, int k) {
|
|
225
|
+
NearestNeighborsDistanceResult result;
|
|
226
|
+
self.nearest_neighbors_with_distances(cloud, result, k);
|
|
227
|
+
|
|
228
|
+
py::array_t<long int> indices_array(result.size());
|
|
229
|
+
py::array_t<double> distances_array(result.size());
|
|
230
|
+
|
|
231
|
+
auto indices_array_ptr = indices_array.mutable_data();
|
|
232
|
+
auto distances_array_ptr = distances_array.mutable_data();
|
|
233
|
+
|
|
234
|
+
for (std::size_t i = 0; i < result.size(); ++i) {
|
|
235
|
+
*indices_array_ptr++ = result[i].first[result[i].first.size() - 1];
|
|
236
|
+
*distances_array_ptr++ = result[i].second[result[i].second.size() - 1];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return std::make_pair(indices_array, distances_array);
|
|
240
|
+
},
|
|
241
|
+
"Find nearest neighbors for all points in a cloud!");
|
|
242
|
+
|
|
243
|
+
// Pickling support for the KDTree data structure
|
|
244
|
+
kdtree.def("__getstate__", [](const KDTree&) {
|
|
245
|
+
// If a user pickles KDTree itself, we end up redundantly storing
|
|
246
|
+
// the point cloud itself, because the KDTree is only usable with the
|
|
247
|
+
// cloud (scipy does exactly the same). We solve the problem by asking
|
|
248
|
+
// users to pickle Epoch instead, which is the much cleaner solution.
|
|
249
|
+
throw std::runtime_error{
|
|
250
|
+
"Please pickle Epoch instead of KDTree. Otherwise unpickled KDTree does "
|
|
251
|
+
"not know the point cloud."
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Expose the Octree class
|
|
256
|
+
py::class_<Octree> octree(m, "Octree", py::buffer_protocol());
|
|
257
|
+
|
|
258
|
+
// Map __init__ to constructor
|
|
259
|
+
octree.def(py::init<>(&Octree::create));
|
|
260
|
+
|
|
261
|
+
// Allow updating Octree from a given file
|
|
262
|
+
octree.def("load_index", [](Octree& self, std::string filename) {
|
|
263
|
+
std::ifstream stream(filename, std::ios::binary | std::ios::in);
|
|
264
|
+
self.loadIndex(stream);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Allow dumping Octree to a file
|
|
268
|
+
octree.def("save_index", [](const Octree& self, std::string filename) {
|
|
269
|
+
std::ofstream stream(filename, std::ios::binary | std::ios::out);
|
|
270
|
+
self.saveIndex(stream);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Allow building the Octree structure
|
|
274
|
+
octree.def("build_tree",
|
|
275
|
+
&Octree::build_tree,
|
|
276
|
+
py::arg("force_cubic") = false,
|
|
277
|
+
py::arg("min_corner") = std::nullopt,
|
|
278
|
+
py::arg("max_corner") = std::nullopt,
|
|
279
|
+
"Trigger building the search octree");
|
|
280
|
+
|
|
281
|
+
// Allow invalidating the Octree structure
|
|
282
|
+
octree.def("invalidate", &Octree::invalidate, "Invalidate the search octree");
|
|
283
|
+
|
|
284
|
+
// Allow extraction of number of points
|
|
285
|
+
octree.def("get_number_of_points",
|
|
286
|
+
&Octree::get_number_of_points,
|
|
287
|
+
"Return the number of points in the associated cloud");
|
|
288
|
+
|
|
289
|
+
// Allow extraction of maximum octree depth
|
|
290
|
+
octree.def("get_max_depth",
|
|
291
|
+
&Octree::get_max_depth,
|
|
292
|
+
"Return the maximum octree depth level");
|
|
293
|
+
|
|
294
|
+
// Allow extraction of bounding box size
|
|
295
|
+
octree.def("get_box_size",
|
|
296
|
+
&Octree::get_box_size,
|
|
297
|
+
"Return the side length of the bounding box");
|
|
298
|
+
|
|
299
|
+
// Allow extraction of min point
|
|
300
|
+
octree.def("get_min_point",
|
|
301
|
+
&Octree::get_min_point,
|
|
302
|
+
"Return the minimum point of the bounding box");
|
|
303
|
+
|
|
304
|
+
// Allow extraction of max point
|
|
305
|
+
octree.def(
|
|
306
|
+
"get_max_point", &Octree::get_max_point, "Return 8-bit dilated integer");
|
|
307
|
+
|
|
308
|
+
// Allow extraction of cell sizes
|
|
309
|
+
octree.def("get_cell_size",
|
|
310
|
+
&Octree::get_cell_size,
|
|
311
|
+
"Return the size of cells at a level of depth");
|
|
312
|
+
|
|
313
|
+
// Allow extraction of number of cells
|
|
314
|
+
octree.def("get_number_of_cells",
|
|
315
|
+
&Octree::get_number_of_cells,
|
|
316
|
+
"Return the number of cells at a level of depth");
|
|
317
|
+
|
|
318
|
+
// Allow extraction of number of cells per axis
|
|
319
|
+
octree.def("get_number_of_cells_per_axis",
|
|
320
|
+
&Octree::get_number_of_cells_per_axis,
|
|
321
|
+
"Return the number of cells per axis at a level of depth");
|
|
322
|
+
|
|
323
|
+
// Allow extraction of number of occupied cells per level
|
|
324
|
+
octree.def("get_number_of_occupied_cells",
|
|
325
|
+
&Octree::get_number_of_occupied_cells,
|
|
326
|
+
"Return the number of occupied cells per level of depth");
|
|
327
|
+
|
|
328
|
+
// Allow extraction of maximum amount of points
|
|
329
|
+
octree.def("get_max_cell_population",
|
|
330
|
+
&Octree::get_max_cell_population,
|
|
331
|
+
"Return the maximum number of points per cell per level of depth");
|
|
332
|
+
|
|
333
|
+
// Allow extraction of average amount of points
|
|
334
|
+
octree.def("get_average_cell_population",
|
|
335
|
+
&Octree::get_average_cell_population,
|
|
336
|
+
"Return the average number of points per cell per level of depth");
|
|
337
|
+
|
|
338
|
+
// Allow extraction of std of amount of points
|
|
339
|
+
octree.def("get_std_cell_population",
|
|
340
|
+
&Octree::get_std_cell_population,
|
|
341
|
+
"Return the standard deviation of number of points per cell per "
|
|
342
|
+
"level of depth");
|
|
343
|
+
|
|
344
|
+
// Allow extraction of coordinates of all points
|
|
345
|
+
octree.def(
|
|
346
|
+
"get_coordinate",
|
|
347
|
+
[](const Octree& self, Octree::SpatialKey truncated_key) {
|
|
348
|
+
Octree::OctreeCoordinate coord = self.get_coordinates(truncated_key);
|
|
349
|
+
|
|
350
|
+
return coord;
|
|
351
|
+
},
|
|
352
|
+
"Retrieve the octree coordinate corresponding to a cell key.",
|
|
353
|
+
py::arg("truncated_key"));
|
|
354
|
+
|
|
355
|
+
octree.def(
|
|
356
|
+
"get_coordinates",
|
|
357
|
+
[](const Octree& self, const Octree::KeyContainer& truncated_keys) {
|
|
358
|
+
return self.get_coordinates(truncated_keys);
|
|
359
|
+
},
|
|
360
|
+
"Retrieve the coordinates of given cell keys in the octree.",
|
|
361
|
+
py::arg("truncated_keys"));
|
|
362
|
+
|
|
363
|
+
octree.def(
|
|
364
|
+
"get_coordinates",
|
|
365
|
+
[](const Octree& self, std::optional<unsigned int> level) {
|
|
366
|
+
unsigned int lvl = level.value_or(self.get_max_depth());
|
|
367
|
+
|
|
368
|
+
return self.get_coordinates_at_level(lvl);
|
|
369
|
+
},
|
|
370
|
+
"Retrieve the coordinates at level of all points in the octree.",
|
|
371
|
+
py::arg("level") = std::nullopt);
|
|
372
|
+
|
|
373
|
+
// Allow extraction of spatial keys
|
|
374
|
+
octree.def("get_spatial_keys",
|
|
375
|
+
&Octree::get_spatial_keys,
|
|
376
|
+
"Return the computed spatial keys");
|
|
377
|
+
|
|
378
|
+
// Allow extraction of point indices
|
|
379
|
+
octree.def("get_point_indices",
|
|
380
|
+
&Octree::get_point_indices,
|
|
381
|
+
"Return the sorted point indices");
|
|
382
|
+
|
|
383
|
+
// Allow extraction from points in cell
|
|
384
|
+
octree.def(
|
|
385
|
+
"get_point_indices_from_cells",
|
|
386
|
+
[](const Octree& self, Octree::SpatialKey key, unsigned int level) {
|
|
387
|
+
RadiusSearchResult result;
|
|
388
|
+
self.get_point_indices_from_cells(key, level, result);
|
|
389
|
+
|
|
390
|
+
return as_pyarray(std::move(result));
|
|
391
|
+
},
|
|
392
|
+
"Retrieve point indices and spatial keys for a given cell",
|
|
393
|
+
py::arg("key"),
|
|
394
|
+
py::arg("level"));
|
|
395
|
+
|
|
396
|
+
octree.def(
|
|
397
|
+
"get_point_indices_from_cells",
|
|
398
|
+
[](const Octree& self,
|
|
399
|
+
const Octree::KeyContainer& keys,
|
|
400
|
+
unsigned int level) {
|
|
401
|
+
RadiusSearchResult result;
|
|
402
|
+
result.reserve(keys.size() * self.get_max_cell_population(level));
|
|
403
|
+
|
|
404
|
+
self.get_point_indices_from_cells(keys, level, result);
|
|
405
|
+
|
|
406
|
+
return as_pyarray(std::move(result));
|
|
407
|
+
},
|
|
408
|
+
"Retrieve point indices and spatial keys for given cells",
|
|
409
|
+
py::arg("keys"),
|
|
410
|
+
py::arg("level"));
|
|
411
|
+
|
|
412
|
+
// Allow extraction from cell population
|
|
413
|
+
octree.def(
|
|
414
|
+
"get_cell_population",
|
|
415
|
+
[](const Octree& self, Octree::SpatialKey key, unsigned int level) {
|
|
416
|
+
return self.get_cell_population(key, level);
|
|
417
|
+
},
|
|
418
|
+
"Retrieve point count for a given cell",
|
|
419
|
+
py::arg("key"),
|
|
420
|
+
py::arg("level"));
|
|
421
|
+
|
|
422
|
+
octree.def(
|
|
423
|
+
"get_cell_population",
|
|
424
|
+
[](const Octree& self,
|
|
425
|
+
const Octree::KeyContainer& keys,
|
|
426
|
+
unsigned int level) {
|
|
427
|
+
std::vector<std::size_t> populations =
|
|
428
|
+
self.get_cell_population(keys, level);
|
|
429
|
+
return as_pyarray(std::move(populations));
|
|
430
|
+
},
|
|
431
|
+
"Retrieve point counts for given cells",
|
|
432
|
+
py::arg("keys"),
|
|
433
|
+
py::arg("level"));
|
|
434
|
+
|
|
435
|
+
// Allow extraction of unique occupied cells at a given level
|
|
436
|
+
octree.def(
|
|
437
|
+
"get_unique_cells",
|
|
438
|
+
[](const Octree& self, unsigned int level) {
|
|
439
|
+
Octree::KeyContainer unique_keys = self.get_unique_cells(level);
|
|
440
|
+
return as_pyarray(std::move(unique_keys));
|
|
441
|
+
},
|
|
442
|
+
"Retrieve unique occupied cells at a given level",
|
|
443
|
+
py::arg("level"));
|
|
444
|
+
|
|
445
|
+
// Allow computation of level of depth at which a radius search will be most
|
|
446
|
+
// efficient
|
|
447
|
+
octree.def("find_appropriate_level_for_radius_search",
|
|
448
|
+
&Octree::find_appropriate_level_for_radius_search,
|
|
449
|
+
"Return the level of depth at which a radius search will be most "
|
|
450
|
+
"efficient");
|
|
451
|
+
|
|
452
|
+
// Allow radius search with optional depth level specification
|
|
453
|
+
octree.def(
|
|
454
|
+
"radius_search",
|
|
455
|
+
[](const Octree& self,
|
|
456
|
+
Eigen::Ref<const Eigen::Vector3d> query_point,
|
|
457
|
+
double radius,
|
|
458
|
+
std::optional<unsigned int> level) {
|
|
459
|
+
unsigned int lvl =
|
|
460
|
+
level.value_or(self.find_appropriate_level_for_radius_search(radius));
|
|
461
|
+
|
|
462
|
+
RadiusSearchResult result;
|
|
463
|
+
self.radius_search(query_point, radius, lvl, result);
|
|
464
|
+
|
|
465
|
+
return as_pyarray(std::move(result));
|
|
466
|
+
},
|
|
467
|
+
"Search point in given radius!",
|
|
468
|
+
py::arg("query_point"),
|
|
469
|
+
py::arg("radius"),
|
|
470
|
+
py::arg("level") = std::nullopt);
|
|
471
|
+
|
|
472
|
+
// Pickling support for the Octree data structure
|
|
473
|
+
octree.def("__getstate__", [](const Octree&) {
|
|
474
|
+
// If a user pickles Octree itself, we end up redundantly storing
|
|
475
|
+
// the point cloud itself, because the Octree is only usable with the
|
|
476
|
+
// cloud (scipy does exactly the same). We solve the problem by asking
|
|
477
|
+
// users to pickle Epoch instead, which is the much cleaner solution.
|
|
478
|
+
throw std::runtime_error{
|
|
479
|
+
"Please pickle Epoch instead of Octree. Otherwise unpickled Octree does "
|
|
480
|
+
"not know the point cloud."
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Segment point cloud into a supervoxels
|
|
485
|
+
m.def("segment_pc_in_supervoxels",
|
|
486
|
+
[](Epoch& epoch,
|
|
487
|
+
const KDTree& kdtree,
|
|
488
|
+
EigenNormalSetConstRef normals,
|
|
489
|
+
double resolution,
|
|
490
|
+
int k,
|
|
491
|
+
int minSVPvalue) {
|
|
492
|
+
std::vector<Supervoxel> supervoxels =
|
|
493
|
+
segment_pc(epoch, kdtree, normals, resolution, k, minSVPvalue);
|
|
494
|
+
|
|
495
|
+
py::list np_arrays_cloud;
|
|
496
|
+
py::list np_arrays_centroid;
|
|
497
|
+
py::list np_arrays_boundary_points;
|
|
498
|
+
py::list np_arrays_normals;
|
|
499
|
+
|
|
500
|
+
for (const auto& sv : supervoxels) {
|
|
501
|
+
// Convert Eigen::MatrixXd to a NumPy array
|
|
502
|
+
auto np_array_cloud = py::array_t<double>(
|
|
503
|
+
sv.cloud.rows() * sv.cloud.cols(), sv.cloud.data());
|
|
504
|
+
auto np_array_normals = py::array_t<double>(
|
|
505
|
+
sv.normals.rows() * sv.normals.cols(), sv.normals.data());
|
|
506
|
+
auto np_array_centroid =
|
|
507
|
+
py::array_t<double>(sv.centroid.size(), sv.centroid.data());
|
|
508
|
+
auto np_array_boundary_points = py::array_t<double>(
|
|
509
|
+
sv.boundary_points.rows() * sv.boundary_points.cols(),
|
|
510
|
+
sv.boundary_points.data());
|
|
511
|
+
|
|
512
|
+
// Reshape the arrays to their original shape
|
|
513
|
+
np_array_cloud.resize({ sv.cloud.rows(), sv.cloud.cols() });
|
|
514
|
+
np_array_normals.resize({ sv.normals.rows(), sv.normals.cols() });
|
|
515
|
+
np_array_centroid.resize({ sv.centroid.size() });
|
|
516
|
+
np_array_boundary_points.resize(
|
|
517
|
+
{ sv.boundary_points.rows(), sv.boundary_points.cols() });
|
|
518
|
+
|
|
519
|
+
np_arrays_cloud.append(np_array_cloud);
|
|
520
|
+
np_arrays_normals.append(np_array_normals);
|
|
521
|
+
np_arrays_centroid.append(np_array_centroid);
|
|
522
|
+
np_arrays_boundary_points.append(np_array_boundary_points);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return std::make_tuple(np_arrays_cloud,
|
|
526
|
+
np_arrays_normals,
|
|
527
|
+
np_arrays_centroid,
|
|
528
|
+
np_arrays_boundary_points);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Perform a transformation of a point cloud using Gauss-Newton method
|
|
532
|
+
m.def("fit_transform_GN",
|
|
533
|
+
[](EigenPointCloudConstRef cloud1,
|
|
534
|
+
EigenPointCloudConstRef cloud2,
|
|
535
|
+
EigenNormalSetConstRef normals) {
|
|
536
|
+
Eigen::Matrix4d transformation =
|
|
537
|
+
fit_transform_GN(cloud1, cloud2, normals);
|
|
538
|
+
return transformation;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// The main distance computation function that is the main entry point of M3C2
|
|
542
|
+
m.def(
|
|
543
|
+
"compute_distances",
|
|
544
|
+
[](EigenPointCloudConstRef corepoints,
|
|
545
|
+
double scale,
|
|
546
|
+
const Epoch& epoch1,
|
|
547
|
+
const Epoch& epoch2,
|
|
548
|
+
EigenNormalSetConstRef directions,
|
|
549
|
+
double max_distance,
|
|
550
|
+
double registration_error,
|
|
551
|
+
const WorkingSetFinderCallback& workingsetfinder,
|
|
552
|
+
const DistanceUncertaintyCalculationCallback& distancecalculator) {
|
|
553
|
+
// Allocate memory for the return types
|
|
554
|
+
DistanceVector distances;
|
|
555
|
+
UncertaintyVector uncertainties;
|
|
556
|
+
|
|
557
|
+
{
|
|
558
|
+
// compute_distances may spawn multiple threads that may call Python
|
|
559
|
+
// functions (which requires them to acquire the GIL), so we need to
|
|
560
|
+
// first release the GIL on the main thread before calling
|
|
561
|
+
// compute_distances
|
|
562
|
+
py::gil_scoped_release release_gil;
|
|
563
|
+
compute_distances(corepoints,
|
|
564
|
+
scale,
|
|
565
|
+
epoch1,
|
|
566
|
+
epoch2,
|
|
567
|
+
directions,
|
|
568
|
+
max_distance,
|
|
569
|
+
registration_error,
|
|
570
|
+
distances,
|
|
571
|
+
uncertainties,
|
|
572
|
+
workingsetfinder,
|
|
573
|
+
distancecalculator);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return std::make_tuple(as_pyarray(std::move(distances)),
|
|
577
|
+
as_pyarray(std::move(uncertainties)));
|
|
578
|
+
},
|
|
579
|
+
"The main M3C2 distance calculation algorithm");
|
|
580
|
+
|
|
581
|
+
// Multiscale direction computation
|
|
582
|
+
m.def(
|
|
583
|
+
"compute_multiscale_directions",
|
|
584
|
+
[](const Epoch& epoch,
|
|
585
|
+
EigenPointCloudConstRef corepoints,
|
|
586
|
+
const std::vector<double>& normal_radii,
|
|
587
|
+
EigenNormalSetConstRef orientation) {
|
|
588
|
+
EigenNormalSet result(corepoints.rows(), 3);
|
|
589
|
+
std::vector<double> used_radii;
|
|
590
|
+
|
|
591
|
+
compute_multiscale_directions(
|
|
592
|
+
epoch, corepoints, normal_radii, orientation, result, used_radii);
|
|
593
|
+
|
|
594
|
+
return std::make_tuple(std::move(result),
|
|
595
|
+
as_pyarray(std::move(used_radii)));
|
|
596
|
+
},
|
|
597
|
+
py::arg("epoch"),
|
|
598
|
+
py::arg("corepoints"),
|
|
599
|
+
py::arg("normal_radii"),
|
|
600
|
+
py::arg("orientation"),
|
|
601
|
+
"Compute M3C2 multiscale directions");
|
|
602
|
+
|
|
603
|
+
// Corresponence distances computation
|
|
604
|
+
m.def("compute_correspondence_distances",
|
|
605
|
+
&compute_correspondence_distances,
|
|
606
|
+
"Compute correspondence distances");
|
|
607
|
+
|
|
608
|
+
// Callback parameter structs
|
|
609
|
+
py::class_<WorkingSetFinderParameters> ws_params(
|
|
610
|
+
m, "WorkingSetFinderParameters");
|
|
611
|
+
ws_params.def_property_readonly(
|
|
612
|
+
"epoch", [](const WorkingSetFinderParameters& self) { return self.epoch; });
|
|
613
|
+
ws_params.def_property_readonly(
|
|
614
|
+
"radius",
|
|
615
|
+
[](const WorkingSetFinderParameters& self) { return self.radius; });
|
|
616
|
+
ws_params.def_property_readonly(
|
|
617
|
+
"corepoint",
|
|
618
|
+
[](const WorkingSetFinderParameters& self) { return self.corepoint; });
|
|
619
|
+
ws_params.def_property_readonly(
|
|
620
|
+
"cylinder_axis",
|
|
621
|
+
[](const WorkingSetFinderParameters& self) { return self.cylinder_axis; });
|
|
622
|
+
ws_params.def_property_readonly(
|
|
623
|
+
"max_distance",
|
|
624
|
+
[](const WorkingSetFinderParameters& self) { return self.max_distance; });
|
|
625
|
+
|
|
626
|
+
py::class_<DistanceUncertaintyCalculationParameters> d_params(
|
|
627
|
+
m, "DistanceUncertaintyCalculationParameters");
|
|
628
|
+
d_params.def_property_readonly(
|
|
629
|
+
"workingset1", [](const DistanceUncertaintyCalculationParameters& self) {
|
|
630
|
+
return self.workingset1;
|
|
631
|
+
});
|
|
632
|
+
d_params.def_property_readonly(
|
|
633
|
+
"workingset2", [](const DistanceUncertaintyCalculationParameters& self) {
|
|
634
|
+
return self.workingset2;
|
|
635
|
+
});
|
|
636
|
+
d_params.def_property_readonly(
|
|
637
|
+
"corepoint", [](const DistanceUncertaintyCalculationParameters& self) {
|
|
638
|
+
return self.corepoint;
|
|
639
|
+
});
|
|
640
|
+
d_params.def_property_readonly(
|
|
641
|
+
"normal", [](const DistanceUncertaintyCalculationParameters& self) {
|
|
642
|
+
return self.normal;
|
|
643
|
+
});
|
|
644
|
+
d_params.def_property_readonly(
|
|
645
|
+
"registration_error",
|
|
646
|
+
[](const DistanceUncertaintyCalculationParameters& self) {
|
|
647
|
+
return self.registration_error;
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// The ObjectByChange class is used as the return type for spatiotemporal
|
|
651
|
+
// segmentations
|
|
652
|
+
py::class_<ObjectByChange> obc(m, "ObjectByChange");
|
|
653
|
+
obc.def_property_readonly(
|
|
654
|
+
"indices_distances",
|
|
655
|
+
[](const ObjectByChange& self) { return self.indices_distances; });
|
|
656
|
+
obc.def_property_readonly(
|
|
657
|
+
"start_epoch", [](const ObjectByChange& self) { return self.start_epoch; });
|
|
658
|
+
obc.def_property_readonly(
|
|
659
|
+
"end_epoch", [](const ObjectByChange& self) { return self.end_epoch; });
|
|
660
|
+
obc.def_property_readonly(
|
|
661
|
+
"threshold", [](const ObjectByChange& self) { return self.threshold; });
|
|
662
|
+
obc.def(py::pickle(
|
|
663
|
+
[](const ObjectByChange& self) {
|
|
664
|
+
// Serialize into in-memory stream
|
|
665
|
+
std::stringstream buf;
|
|
666
|
+
|
|
667
|
+
// Write indices
|
|
668
|
+
std::size_t size = self.indices_distances.size();
|
|
669
|
+
buf.write(reinterpret_cast<const char*>(&size), sizeof(std::size_t));
|
|
670
|
+
for (auto p : self.indices_distances)
|
|
671
|
+
buf.write(reinterpret_cast<const char*>(&p),
|
|
672
|
+
sizeof(std::pair<IndexType, double>));
|
|
673
|
+
|
|
674
|
+
// Write other data
|
|
675
|
+
buf.write(reinterpret_cast<const char*>(&self.start_epoch),
|
|
676
|
+
sizeof(IndexType));
|
|
677
|
+
buf.write(reinterpret_cast<const char*>(&self.end_epoch),
|
|
678
|
+
sizeof(IndexType));
|
|
679
|
+
buf.write(reinterpret_cast<const char*>(&self.threshold), sizeof(double));
|
|
680
|
+
return py::bytes(buf.str());
|
|
681
|
+
},
|
|
682
|
+
[](const py::bytes& data) {
|
|
683
|
+
std::stringstream buf(data.cast<std::string>());
|
|
684
|
+
ObjectByChange obj;
|
|
685
|
+
|
|
686
|
+
std::size_t size;
|
|
687
|
+
buf.read(reinterpret_cast<char*>(&size), sizeof(std::size_t));
|
|
688
|
+
std::pair<IndexType, double> buffer;
|
|
689
|
+
for (std::size_t i = 0; i < size; ++i) {
|
|
690
|
+
buf.read(reinterpret_cast<char*>(&buffer),
|
|
691
|
+
sizeof(std::pair<IndexType, double>));
|
|
692
|
+
obj.indices_distances.insert(buffer);
|
|
693
|
+
}
|
|
694
|
+
buf.read(reinterpret_cast<char*>(&obj.start_epoch), sizeof(IndexType));
|
|
695
|
+
buf.read(reinterpret_cast<char*>(&obj.end_epoch), sizeof(IndexType));
|
|
696
|
+
buf.read(reinterpret_cast<char*>(&obj.threshold), sizeof(double));
|
|
697
|
+
return obj;
|
|
698
|
+
}));
|
|
699
|
+
|
|
700
|
+
py::class_<RegionGrowingSeed> rgs(m, "RegionGrowingSeed");
|
|
701
|
+
rgs.def(py::init<IndexType, IndexType, IndexType>(),
|
|
702
|
+
py::arg("index"),
|
|
703
|
+
py::arg("start_epoch"),
|
|
704
|
+
py::arg("end_epoch"));
|
|
705
|
+
rgs.def_property_readonly(
|
|
706
|
+
"index", [](const RegionGrowingSeed& self) { return self.index; });
|
|
707
|
+
rgs.def_property_readonly("start_epoch", [](const RegionGrowingSeed& self) {
|
|
708
|
+
return self.start_epoch;
|
|
709
|
+
});
|
|
710
|
+
rgs.def_property_readonly(
|
|
711
|
+
"end_epoch", [](const RegionGrowingSeed& self) { return self.end_epoch; });
|
|
712
|
+
rgs.def(py::pickle(
|
|
713
|
+
[](const RegionGrowingSeed& self) {
|
|
714
|
+
// Serialize into in-memory stream
|
|
715
|
+
std::stringstream buf;
|
|
716
|
+
buf.write(reinterpret_cast<const char*>(&self.index), sizeof(IndexType));
|
|
717
|
+
buf.write(reinterpret_cast<const char*>(&self.start_epoch),
|
|
718
|
+
sizeof(IndexType));
|
|
719
|
+
buf.write(reinterpret_cast<const char*>(&self.end_epoch),
|
|
720
|
+
sizeof(IndexType));
|
|
721
|
+
return py::bytes(buf.str());
|
|
722
|
+
},
|
|
723
|
+
[](const py::bytes& data) {
|
|
724
|
+
std::stringstream buf(data.cast<std::string>());
|
|
725
|
+
IndexType index, start_epoch, end_epoch;
|
|
726
|
+
buf.read(reinterpret_cast<char*>(&index), sizeof(IndexType));
|
|
727
|
+
buf.read(reinterpret_cast<char*>(&start_epoch), sizeof(IndexType));
|
|
728
|
+
buf.read(reinterpret_cast<char*>(&end_epoch), sizeof(IndexType));
|
|
729
|
+
return RegionGrowingSeed{ index, start_epoch, end_epoch };
|
|
730
|
+
}));
|
|
731
|
+
|
|
732
|
+
py::class_<RegionGrowingAlgorithmData> rgwd(m, "RegionGrowingAlgorithmData");
|
|
733
|
+
rgwd.def(py::init<EigenSpatiotemporalArrayConstRef,
|
|
734
|
+
const Epoch&,
|
|
735
|
+
double,
|
|
736
|
+
RegionGrowingSeed,
|
|
737
|
+
std::vector<double>,
|
|
738
|
+
std::size_t,
|
|
739
|
+
std::size_t>(),
|
|
740
|
+
py::arg("data"),
|
|
741
|
+
py::arg("epoch"),
|
|
742
|
+
py::arg("radius"),
|
|
743
|
+
py::arg("seed"),
|
|
744
|
+
py::arg("thresholds"),
|
|
745
|
+
py::arg("min_segments"),
|
|
746
|
+
py::arg("max_segments"));
|
|
747
|
+
|
|
748
|
+
py::class_<TimeseriesDistanceFunctionData> tdfd(
|
|
749
|
+
m, "TimeseriesDistanceFunctionData");
|
|
750
|
+
tdfd.def(py::init<EigenTimeSeriesConstRef, EigenTimeSeriesConstRef>(),
|
|
751
|
+
py::arg("ts1"),
|
|
752
|
+
py::arg("ts2"));
|
|
753
|
+
tdfd.def_property_readonly(
|
|
754
|
+
"ts1", [](const TimeseriesDistanceFunctionData& self) { return self.ts1; });
|
|
755
|
+
tdfd.def_property_readonly(
|
|
756
|
+
"ts2", [](const TimeseriesDistanceFunctionData& self) { return self.ts2; });
|
|
757
|
+
tdfd.def_property_readonly(
|
|
758
|
+
"norm1",
|
|
759
|
+
[](const TimeseriesDistanceFunctionData& self) { return self.norm1; });
|
|
760
|
+
tdfd.def_property_readonly(
|
|
761
|
+
"norm2",
|
|
762
|
+
[](const TimeseriesDistanceFunctionData& self) { return self.norm2; });
|
|
763
|
+
|
|
764
|
+
py::class_<ChangePointDetectionData> cpdd(m, "ChangePointDetectionData");
|
|
765
|
+
cpdd.def(
|
|
766
|
+
py::
|
|
767
|
+
init<EigenTimeSeriesConstRef, IndexType, IndexType, IndexType, double>(),
|
|
768
|
+
py::arg("ts"),
|
|
769
|
+
py::arg("window_size"),
|
|
770
|
+
py::arg("min_size"),
|
|
771
|
+
py::arg("jump"),
|
|
772
|
+
py::arg("penalty"));
|
|
773
|
+
|
|
774
|
+
m.def("transform_pointcloud_inplace",
|
|
775
|
+
[](EigenPointCloudRef cloud,
|
|
776
|
+
const py::array_t<double>& t,
|
|
777
|
+
EigenPointCloudConstRef rp,
|
|
778
|
+
EigenNormalSetRef normals) {
|
|
779
|
+
Transformation trafo;
|
|
780
|
+
|
|
781
|
+
auto r = t.unchecked<2>();
|
|
782
|
+
for (IndexType i = 0; i < 4; ++i)
|
|
783
|
+
for (IndexType j = 0; j < 4; ++j)
|
|
784
|
+
trafo(i, j) = r(i, j);
|
|
785
|
+
|
|
786
|
+
transform_pointcloud_inplace(cloud, trafo, rp, normals);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// The main algorithms for the spatiotemporal segmentations
|
|
790
|
+
m.def("region_growing",
|
|
791
|
+
[](const RegionGrowingAlgorithmData& data,
|
|
792
|
+
const TimeseriesDistanceFunction& distance_function) {
|
|
793
|
+
// The region_growing function may call Python callback functions
|
|
794
|
+
py::gil_scoped_release release_gil;
|
|
795
|
+
return region_growing(data, distance_function);
|
|
796
|
+
});
|
|
797
|
+
m.def("change_point_detection", &change_point_detection);
|
|
798
|
+
|
|
799
|
+
// Callback implementations
|
|
800
|
+
m.def("radius_workingset_finder", &radius_workingset_finder);
|
|
801
|
+
m.def("cylinder_workingset_finder", &cylinder_workingset_finder);
|
|
802
|
+
m.def("mean_stddev_distance", &mean_stddev_distance);
|
|
803
|
+
m.def("median_iqr_distance", &median_iqr_distance);
|
|
804
|
+
m.def("dtw_distance", &dtw_distance);
|
|
805
|
+
m.def("normalized_dtw_distance", &normalized_dtw_distance);
|
|
806
|
+
|
|
807
|
+
// Expose OpenMP threading control
|
|
808
|
+
#ifdef PY4DGEO_WITH_OPENMP
|
|
809
|
+
m.def("omp_set_num_threads", &omp_set_num_threads);
|
|
810
|
+
m.def("omp_get_max_threads", &omp_get_max_threads);
|
|
811
|
+
#endif
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
} // namespace py4dgeo
|