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.
@@ -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