py4dgeo 0.7.0__cp313-cp313-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.
@@ -0,0 +1,487 @@
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/py4dgeo.hpp"
15
+ #include "py4dgeo/pybind11_numpy_interop.hpp"
16
+ #include "py4dgeo/registration.hpp"
17
+ #include "py4dgeo/segmentation.hpp"
18
+
19
+ #include <fstream>
20
+ #include <sstream>
21
+ #include <string>
22
+ #include <tuple>
23
+
24
+ namespace py = pybind11;
25
+
26
+ namespace py4dgeo {
27
+
28
+ PYBIND11_MODULE(_py4dgeo, m)
29
+ {
30
+ m.doc() = "Python Bindings for py4dgeo";
31
+
32
+ // The enum class for our memory policy
33
+ py::enum_<MemoryPolicy>(m, "MemoryPolicy", py::arithmetic())
34
+ .value("STRICT", MemoryPolicy::STRICT)
35
+ .value("MINIMAL", MemoryPolicy::MINIMAL)
36
+ .value("COREPOINTS", MemoryPolicy::COREPOINTS)
37
+ .value("RELAXED", MemoryPolicy::RELAXED)
38
+ .export_values();
39
+
40
+ // Register a numpy structured type for uncertainty calculation. This allows
41
+ // us to allocate memory in C++ and expose it as a structured numpy array in
42
+ // Python. The given names will be usable in Python.
43
+ PYBIND11_NUMPY_DTYPE(DistanceUncertainty,
44
+ lodetection,
45
+ spread1,
46
+ num_samples1,
47
+ spread2,
48
+ num_samples2);
49
+
50
+ // Also expose the DistanceUncertainty data structure in Python, so that
51
+ // Python fallbacks can use it directly to define their result.
52
+ py::class_<DistanceUncertainty> unc(m, "DistanceUncertainty");
53
+ unc.def(py::init<double, double, IndexType, double, IndexType>(),
54
+ py::arg("lodetection") = 0.0,
55
+ py::arg("spread1") = 0.0,
56
+ py::arg("num_samples1") = 0,
57
+ py::arg("spread2") = 0.0,
58
+ py::arg("num_samples2") = 0);
59
+
60
+ // The epoch class
61
+ py::class_<Epoch> epoch(m, "Epoch");
62
+
63
+ // Initializing with a numpy array prevents the numpy array from being
64
+ // garbage collected as long as the Epoch object is alive
65
+ epoch.def(py::init<EigenPointCloudRef>(), py::keep_alive<1, 2>());
66
+
67
+ // We can directly access the point cloud and the kdtree
68
+ epoch.def_readwrite("_cloud", &Epoch::cloud);
69
+ epoch.def_readwrite("_kdtree", &Epoch::kdtree);
70
+
71
+ // Pickling support for the Epoch class
72
+ epoch.def(py::pickle(
73
+ [](const Epoch& self) {
74
+ // Serialize into in-memory stream
75
+ std::stringstream buf;
76
+ self.to_stream(buf);
77
+ return py::bytes(buf.str());
78
+ },
79
+ [](const py::bytes& data) {
80
+ std::stringstream buf(data.cast<std::string>());
81
+ return Epoch::from_stream(buf);
82
+ }));
83
+
84
+ // Expose the KDTree class
85
+ py::class_<KDTree> kdtree(m, "KDTree", py::buffer_protocol());
86
+
87
+ // Map __init__ to constructor
88
+ kdtree.def(py::init<>(&KDTree::create));
89
+
90
+ // Allow updating KDTree from a given file
91
+ kdtree.def("load_index", [](KDTree& self, std::string filename) {
92
+ std::ifstream stream(filename, std::ios::binary | std::ios::in);
93
+ self.loadIndex(stream);
94
+ });
95
+
96
+ // Allow dumping KDTree to a file
97
+ kdtree.def("save_index", [](const KDTree& self, std::string filename) {
98
+ std::ofstream stream(filename, std::ios::binary | std::ios::out);
99
+ self.saveIndex(stream);
100
+ });
101
+
102
+ // Allow building the KDTree structure
103
+ kdtree.def(
104
+ "build_tree", &KDTree::build_tree, "Trigger building the search tree");
105
+
106
+ // Allow invalidating the KDTree structure
107
+ kdtree.def("invalidate", &KDTree::invalidate, "Invalidate the search tree");
108
+
109
+ // Give access to the leaf parameter that the tree has been built with
110
+ kdtree.def("leaf_parameter",
111
+ &KDTree::get_leaf_parameter,
112
+ "Retrieve the leaf parameter that the tree has been built with.");
113
+
114
+ // Add all the radius search methods
115
+ kdtree.def(
116
+ "radius_search",
117
+ [](const KDTree& self, py::array_t<double> qp, double radius) {
118
+ // Get a pointer for the query point
119
+ auto ptr = static_cast<const double*>(qp.request().ptr);
120
+
121
+ KDTree::RadiusSearchResult result;
122
+ self.radius_search(ptr, radius, result);
123
+
124
+ return as_pyarray(std::move(result));
125
+ },
126
+ "Search point in given radius!");
127
+
128
+ kdtree.def(
129
+ "nearest_neighbors",
130
+ [](const KDTree& self, EigenPointCloudConstRef cloud, int k) {
131
+ KDTree::NearestNeighborsDistanceResult result;
132
+ self.nearest_neighbors_with_distances(cloud, result, k);
133
+
134
+ py::array_t<long int> indices_array(result.size());
135
+ py::array_t<double> distances_array(result.size());
136
+
137
+ auto indices_array_ptr = indices_array.mutable_data();
138
+ auto distances_array_ptr = distances_array.mutable_data();
139
+
140
+ for (size_t i = 0; i < result.size(); ++i) {
141
+ *indices_array_ptr++ = result[i].first[result[i].first.size() - 1];
142
+ *distances_array_ptr++ = result[i].second[result[i].second.size() - 1];
143
+ }
144
+
145
+ return std::make_pair(indices_array, distances_array);
146
+ },
147
+ "Find nearest neighbors for all points in a cloud!");
148
+
149
+ // Pickling support for the KDTree data structure
150
+ kdtree.def("__getstate__", [](const KDTree&) {
151
+ // If a user pickles KDTree itself, we end up redundantly storing
152
+ // the point cloud itself, because the KDTree is only usable with the
153
+ // cloud (scipy does exactly the same). We solve the problem by asking
154
+ // users to pickle Epoch instead, which is the much cleaner solution.
155
+ throw std::runtime_error{
156
+ "Please pickle Epoch instead of KDTree. Otherwise unpickled KDTree does "
157
+ "not know the point cloud."
158
+ };
159
+ });
160
+
161
+ // Segment point cloud into a supervoxels
162
+ m.def("segment_pc_in_supervoxels",
163
+ [](Epoch& epoch,
164
+ const KDTree& kdtree,
165
+ EigenNormalSetConstRef normals,
166
+ double resolution,
167
+ int k,
168
+ int minSVPvalue) {
169
+ std::vector<Supervoxel> supervoxels =
170
+ segment_pc(epoch, kdtree, normals, resolution, k, minSVPvalue);
171
+
172
+ py::list np_arrays_cloud;
173
+ py::list np_arrays_centroid;
174
+ py::list np_arrays_boundary_points;
175
+ py::list np_arrays_normals;
176
+
177
+ for (const auto& sv : supervoxels) {
178
+ // Convert Eigen::MatrixXd to a NumPy array
179
+ auto np_array_cloud = py::array_t<double>(
180
+ sv.cloud.rows() * sv.cloud.cols(), sv.cloud.data());
181
+ auto np_array_normals = py::array_t<double>(
182
+ sv.normals.rows() * sv.normals.cols(), sv.normals.data());
183
+ auto np_array_centroid =
184
+ py::array_t<double>(sv.centroid.size(), sv.centroid.data());
185
+ auto np_array_boundary_points = py::array_t<double>(
186
+ sv.boundary_points.rows() * sv.boundary_points.cols(),
187
+ sv.boundary_points.data());
188
+
189
+ // Reshape the arrays to their original shape
190
+ np_array_cloud.resize({ sv.cloud.rows(), sv.cloud.cols() });
191
+ np_array_normals.resize({ sv.normals.rows(), sv.normals.cols() });
192
+ np_array_centroid.resize({ sv.centroid.size() });
193
+ np_array_boundary_points.resize(
194
+ { sv.boundary_points.rows(), sv.boundary_points.cols() });
195
+
196
+ np_arrays_cloud.append(np_array_cloud);
197
+ np_arrays_normals.append(np_array_normals);
198
+ np_arrays_centroid.append(np_array_centroid);
199
+ np_arrays_boundary_points.append(np_array_boundary_points);
200
+ }
201
+
202
+ return std::make_tuple(np_arrays_cloud,
203
+ np_arrays_normals,
204
+ np_arrays_centroid,
205
+ np_arrays_boundary_points);
206
+ });
207
+
208
+ // Perform a transformation of a point cloud using Gauss-Newton method
209
+ m.def("fit_transform_GN",
210
+ [](EigenPointCloudConstRef cloud1,
211
+ EigenPointCloudConstRef cloud2,
212
+ EigenNormalSetConstRef normals) {
213
+ Eigen::Matrix4d transformation =
214
+ fit_transform_GN(cloud1, cloud2, normals);
215
+ return transformation;
216
+ });
217
+
218
+ // The main distance computation function that is the main entry point of M3C2
219
+ m.def(
220
+ "compute_distances",
221
+ [](EigenPointCloudConstRef corepoints,
222
+ double scale,
223
+ const Epoch& epoch1,
224
+ const Epoch& epoch2,
225
+ EigenNormalSetConstRef directions,
226
+ double max_distance,
227
+ double registration_error,
228
+ const WorkingSetFinderCallback& workingsetfinder,
229
+ const DistanceUncertaintyCalculationCallback& distancecalculator) {
230
+ // Allocate memory for the return types
231
+ DistanceVector distances;
232
+ UncertaintyVector uncertainties;
233
+
234
+ {
235
+ // compute_distances may spawn multiple threads that may call Python
236
+ // functions (which requires them to acquire the GIL), so we need to
237
+ // first release the GIL on the main thread before calling
238
+ // compute_distances
239
+ py::gil_scoped_release release_gil;
240
+ compute_distances(corepoints,
241
+ scale,
242
+ epoch1,
243
+ epoch2,
244
+ directions,
245
+ max_distance,
246
+ registration_error,
247
+ distances,
248
+ uncertainties,
249
+ workingsetfinder,
250
+ distancecalculator);
251
+ }
252
+
253
+ return std::make_tuple(as_pyarray(std::move(distances)),
254
+ as_pyarray(std::move(uncertainties)));
255
+ },
256
+ "The main M3C2 distance calculation algorithm");
257
+
258
+ // Multiscale direction computation
259
+ m.def(
260
+ "compute_multiscale_directions",
261
+ [](const Epoch& epoch,
262
+ EigenPointCloudConstRef corepoints,
263
+ const std::vector<double>& normal_radii,
264
+ EigenNormalSetConstRef orientation) {
265
+ EigenNormalSet result(corepoints.rows(), 3);
266
+ std::vector<double> used_radii;
267
+
268
+ compute_multiscale_directions(
269
+ epoch, corepoints, normal_radii, orientation, result, used_radii);
270
+
271
+ return std::make_tuple(std::move(result),
272
+ as_pyarray(std::move(used_radii)));
273
+ },
274
+ "Compute M3C2 multiscale directions");
275
+
276
+ // Corresponence distances computation
277
+ m.def("compute_correspondence_distances",
278
+ &compute_correspondence_distances,
279
+ "Compute correspondence distances");
280
+
281
+ // Callback parameter structs
282
+ py::class_<WorkingSetFinderParameters> ws_params(
283
+ m, "WorkingSetFinderParameters");
284
+ ws_params.def_property_readonly(
285
+ "epoch", [](const WorkingSetFinderParameters& self) { return self.epoch; });
286
+ ws_params.def_property_readonly(
287
+ "radius",
288
+ [](const WorkingSetFinderParameters& self) { return self.radius; });
289
+ ws_params.def_property_readonly(
290
+ "corepoint",
291
+ [](const WorkingSetFinderParameters& self) { return self.corepoint; });
292
+ ws_params.def_property_readonly(
293
+ "cylinder_axis",
294
+ [](const WorkingSetFinderParameters& self) { return self.cylinder_axis; });
295
+ ws_params.def_property_readonly(
296
+ "max_distance",
297
+ [](const WorkingSetFinderParameters& self) { return self.max_distance; });
298
+
299
+ py::class_<DistanceUncertaintyCalculationParameters> d_params(
300
+ m, "DistanceUncertaintyCalculationParameters");
301
+ d_params.def_property_readonly(
302
+ "workingset1", [](const DistanceUncertaintyCalculationParameters& self) {
303
+ return self.workingset1;
304
+ });
305
+ d_params.def_property_readonly(
306
+ "workingset2", [](const DistanceUncertaintyCalculationParameters& self) {
307
+ return self.workingset2;
308
+ });
309
+ d_params.def_property_readonly(
310
+ "corepoint", [](const DistanceUncertaintyCalculationParameters& self) {
311
+ return self.corepoint;
312
+ });
313
+ d_params.def_property_readonly(
314
+ "normal", [](const DistanceUncertaintyCalculationParameters& self) {
315
+ return self.normal;
316
+ });
317
+ d_params.def_property_readonly(
318
+ "registration_error",
319
+ [](const DistanceUncertaintyCalculationParameters& self) {
320
+ return self.registration_error;
321
+ });
322
+
323
+ // The ObjectByChange class is used as the return type for spatiotemporal
324
+ // segmentations
325
+ py::class_<ObjectByChange> obc(m, "ObjectByChange");
326
+ obc.def_property_readonly(
327
+ "indices_distances",
328
+ [](const ObjectByChange& self) { return self.indices_distances; });
329
+ obc.def_property_readonly(
330
+ "start_epoch", [](const ObjectByChange& self) { return self.start_epoch; });
331
+ obc.def_property_readonly(
332
+ "end_epoch", [](const ObjectByChange& self) { return self.end_epoch; });
333
+ obc.def_property_readonly(
334
+ "threshold", [](const ObjectByChange& self) { return self.threshold; });
335
+ obc.def(py::pickle(
336
+ [](const ObjectByChange& self) {
337
+ // Serialize into in-memory stream
338
+ std::stringstream buf;
339
+
340
+ // Write indices
341
+ std::size_t size = self.indices_distances.size();
342
+ buf.write(reinterpret_cast<const char*>(&size), sizeof(std::size_t));
343
+ for (auto p : self.indices_distances)
344
+ buf.write(reinterpret_cast<const char*>(&p),
345
+ sizeof(std::pair<IndexType, double>));
346
+
347
+ // Write other data
348
+ buf.write(reinterpret_cast<const char*>(&self.start_epoch),
349
+ sizeof(IndexType));
350
+ buf.write(reinterpret_cast<const char*>(&self.end_epoch),
351
+ sizeof(IndexType));
352
+ buf.write(reinterpret_cast<const char*>(&self.threshold), sizeof(double));
353
+ return py::bytes(buf.str());
354
+ },
355
+ [](const py::bytes& data) {
356
+ std::stringstream buf(data.cast<std::string>());
357
+ ObjectByChange obj;
358
+
359
+ std::size_t size;
360
+ buf.read(reinterpret_cast<char*>(&size), sizeof(std::size_t));
361
+ std::pair<IndexType, double> buffer;
362
+ for (std::size_t i = 0; i < size; ++i) {
363
+ buf.read(reinterpret_cast<char*>(&buffer),
364
+ sizeof(std::pair<IndexType, double>));
365
+ obj.indices_distances.insert(buffer);
366
+ }
367
+ buf.read(reinterpret_cast<char*>(&obj.start_epoch), sizeof(IndexType));
368
+ buf.read(reinterpret_cast<char*>(&obj.end_epoch), sizeof(IndexType));
369
+ buf.read(reinterpret_cast<char*>(&obj.threshold), sizeof(double));
370
+ return obj;
371
+ }));
372
+
373
+ py::class_<RegionGrowingSeed> rgs(m, "RegionGrowingSeed");
374
+ rgs.def(py::init<IndexType, IndexType, IndexType>(),
375
+ py::arg("index"),
376
+ py::arg("start_epoch"),
377
+ py::arg("end_epoch"));
378
+ rgs.def_property_readonly(
379
+ "index", [](const RegionGrowingSeed& self) { return self.index; });
380
+ rgs.def_property_readonly("start_epoch", [](const RegionGrowingSeed& self) {
381
+ return self.start_epoch;
382
+ });
383
+ rgs.def_property_readonly(
384
+ "end_epoch", [](const RegionGrowingSeed& self) { return self.end_epoch; });
385
+ rgs.def(py::pickle(
386
+ [](const RegionGrowingSeed& self) {
387
+ // Serialize into in-memory stream
388
+ std::stringstream buf;
389
+ buf.write(reinterpret_cast<const char*>(&self.index), sizeof(IndexType));
390
+ buf.write(reinterpret_cast<const char*>(&self.start_epoch),
391
+ sizeof(IndexType));
392
+ buf.write(reinterpret_cast<const char*>(&self.end_epoch),
393
+ sizeof(IndexType));
394
+ return py::bytes(buf.str());
395
+ },
396
+ [](const py::bytes& data) {
397
+ std::stringstream buf(data.cast<std::string>());
398
+ IndexType index, start_epoch, end_epoch;
399
+ buf.read(reinterpret_cast<char*>(&index), sizeof(IndexType));
400
+ buf.read(reinterpret_cast<char*>(&start_epoch), sizeof(IndexType));
401
+ buf.read(reinterpret_cast<char*>(&end_epoch), sizeof(IndexType));
402
+ return RegionGrowingSeed{ index, start_epoch, end_epoch };
403
+ }));
404
+
405
+ py::class_<RegionGrowingAlgorithmData> rgwd(m, "RegionGrowingAlgorithmData");
406
+ rgwd.def(py::init<EigenSpatiotemporalArrayConstRef,
407
+ const Epoch&,
408
+ double,
409
+ RegionGrowingSeed,
410
+ std::vector<double>,
411
+ std::size_t,
412
+ std::size_t>(),
413
+ py::arg("data"),
414
+ py::arg("epoch"),
415
+ py::arg("radius"),
416
+ py::arg("seed"),
417
+ py::arg("thresholds"),
418
+ py::arg("min_segments"),
419
+ py::arg("max_segments"));
420
+
421
+ py::class_<TimeseriesDistanceFunctionData> tdfd(
422
+ m, "TimeseriesDistanceFunctionData");
423
+ tdfd.def(py::init<EigenTimeSeriesConstRef, EigenTimeSeriesConstRef>(),
424
+ py::arg("ts1"),
425
+ py::arg("ts2"));
426
+ tdfd.def_property_readonly(
427
+ "ts1", [](const TimeseriesDistanceFunctionData& self) { return self.ts1; });
428
+ tdfd.def_property_readonly(
429
+ "ts2", [](const TimeseriesDistanceFunctionData& self) { return self.ts2; });
430
+ tdfd.def_property_readonly(
431
+ "norm1",
432
+ [](const TimeseriesDistanceFunctionData& self) { return self.norm1; });
433
+ tdfd.def_property_readonly(
434
+ "norm2",
435
+ [](const TimeseriesDistanceFunctionData& self) { return self.norm2; });
436
+
437
+ py::class_<ChangePointDetectionData> cpdd(m, "ChangePointDetectionData");
438
+ cpdd.def(
439
+ py::
440
+ init<EigenTimeSeriesConstRef, IndexType, IndexType, IndexType, double>(),
441
+ py::arg("ts"),
442
+ py::arg("window_size"),
443
+ py::arg("min_size"),
444
+ py::arg("jump"),
445
+ py::arg("penalty"));
446
+
447
+ m.def("transform_pointcloud_inplace",
448
+ [](EigenPointCloudRef cloud,
449
+ const py::array_t<double>& t,
450
+ EigenPointCloudConstRef rp,
451
+ EigenNormalSetRef normals) {
452
+ Transformation trafo;
453
+
454
+ auto r = t.unchecked<2>();
455
+ for (IndexType i = 0; i < 4; ++i)
456
+ for (IndexType j = 0; j < 4; ++j)
457
+ trafo(i, j) = r(i, j);
458
+
459
+ transform_pointcloud_inplace(cloud, trafo, rp, normals);
460
+ });
461
+
462
+ // The main algorithms for the spatiotemporal segmentations
463
+ m.def("region_growing",
464
+ [](const RegionGrowingAlgorithmData& data,
465
+ const TimeseriesDistanceFunction& distance_function) {
466
+ // The region_growing function may call Python callback functions
467
+ py::gil_scoped_release release_gil;
468
+ return region_growing(data, distance_function);
469
+ });
470
+ m.def("change_point_detection", &change_point_detection);
471
+
472
+ // Callback implementations
473
+ m.def("radius_workingset_finder", &radius_workingset_finder);
474
+ m.def("cylinder_workingset_finder", &cylinder_workingset_finder);
475
+ m.def("mean_stddev_distance", &mean_stddev_distance);
476
+ m.def("median_iqr_distance", &median_iqr_distance);
477
+ m.def("dtw_distance", &dtw_distance);
478
+ m.def("normalized_dtw_distance", &normalized_dtw_distance);
479
+
480
+ // Expose OpenMP threading control
481
+ #ifdef PY4DGEO_WITH_OPENMP
482
+ m.def("omp_set_num_threads", &omp_set_num_threads);
483
+ m.def("omp_get_max_threads", &omp_get_max_threads);
484
+ #endif
485
+ }
486
+
487
+ } // namespace py4dgeo