py4dgeo 0.7.0__cp311-cp311-win_amd64.whl → 1.0.0__cp311-cp311-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.
Binary file
py4dgeo/__init__.py CHANGED
@@ -1,3 +1,14 @@
1
+ """""" # start delvewheel patch
2
+ def _delvewheel_patch_1_12_0():
3
+ import os
4
+ if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, '.'))):
5
+ os.add_dll_directory(libs_dir)
6
+
7
+
8
+ _delvewheel_patch_1_12_0()
9
+ del _delvewheel_patch_1_12_0
10
+ # end delvewheel patch
11
+
1
12
  from py4dgeo.logger import set_py4dgeo_logfile
2
13
  from py4dgeo.cloudcompare import CloudCompareM3C2
3
14
  from py4dgeo.epoch import (
@@ -7,6 +18,7 @@ from py4dgeo.epoch import (
7
18
  save_epoch,
8
19
  load_epoch,
9
20
  )
21
+ from _py4dgeo import SearchTree
10
22
  from py4dgeo.m3c2 import M3C2, write_m3c2_results_to_las
11
23
  from py4dgeo.m3c2ep import M3C2EP
12
24
  from py4dgeo.registration import (
@@ -27,6 +39,9 @@ from py4dgeo.util import (
27
39
  set_memory_policy,
28
40
  get_num_threads,
29
41
  set_num_threads,
42
+ initialize_openmp_defaults,
30
43
  )
31
44
 
32
- from py4dgeo.pbm3c2 import *
45
+ initialize_openmp_defaults()
46
+
47
+ from py4dgeo.pbm3c2 import PBM3C2
py4dgeo/cloudcompare.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from py4dgeo.m3c2 import M3C2
2
2
 
3
-
4
3
  _cloudcompare_param_mapping = {
5
4
  "normalscale": "normal_radii",
6
5
  "registrationerror": "reg_error",
py4dgeo/epoch.py CHANGED
@@ -115,6 +115,16 @@ class Epoch(_py4dgeo.Epoch):
115
115
  "The KDTree of an Epoch cannot be changed after initialization."
116
116
  )
117
117
 
118
+ @property
119
+ def octree(self):
120
+ return self._octree
121
+
122
+ @octree.setter
123
+ def octree(self, octree):
124
+ raise Py4DGeoError(
125
+ "The Octree of an Epoch cannot be changed after initialization."
126
+ )
127
+
118
128
  @property
119
129
  def normals(self):
120
130
  # Maybe calculate normals
@@ -137,9 +147,7 @@ class Epoch(_py4dgeo.Epoch):
137
147
  A vector to determine orientation of the normals. It should point "up".
138
148
  """
139
149
 
140
- # Ensure that the KDTree is built
141
- if self.kdtree.leaf_parameter() == 0:
142
- self.build_kdtree()
150
+ self._validate_search_tree()
143
151
 
144
152
  # Reuse the multiscale code with a single radius in order to
145
153
  # avoid code duplication.
@@ -153,6 +161,18 @@ class Epoch(_py4dgeo.Epoch):
153
161
 
154
162
  return self.normals
155
163
 
164
+ def _validate_search_tree(self):
165
+ """ "Check if the default search tree is built"""
166
+
167
+ tree_type = self.get_default_radius_search_tree()
168
+
169
+ if tree_type == _py4dgeo.SearchTree.KDTreeSearch:
170
+ if self.kdtree.leaf_parameter() == 0:
171
+ self.build_kdtree()
172
+ else:
173
+ if self.octree.get_number_of_points() == 0:
174
+ self.build_octree()
175
+
156
176
  def normals_attachment(self, normals_array):
157
177
  """Attach normals to the epoch object
158
178
 
@@ -282,6 +302,12 @@ class Epoch(_py4dgeo.Epoch):
282
302
  logger.info(f"Building KDTree structure with leaf parameter {leaf_size}")
283
303
  self.kdtree.build_tree(leaf_size)
284
304
 
305
+ def build_octree(self):
306
+ """Build the search octree index"""
307
+ if self.octree.get_number_of_points() == 0:
308
+ logger.info(f"Building Octree structure")
309
+ self.octree.build_tree()
310
+
285
311
  def transform(
286
312
  self,
287
313
  transformation: typing.Optional[Transformation] = None,
@@ -343,6 +369,9 @@ class Epoch(_py4dgeo.Epoch):
343
369
  # Invalidate the KDTree
344
370
  self.kdtree.invalidate()
345
371
 
372
+ # Invalidate the Octree
373
+ self.octree.invalidate()
374
+
346
375
  if self._normals is None:
347
376
  self._normals = np.empty((1, 3)) # dummy array to avoid error in C++ code
348
377
  # Apply the actual transformation as efficient C++
@@ -453,6 +482,11 @@ class Epoch(_py4dgeo.Epoch):
453
482
  self.kdtree.save_index(kdtreefile)
454
483
  zf.write(kdtreefile, arcname="kdtree")
455
484
 
485
+ octreefile = os.path.join(tmp_dir, "octree")
486
+ with open(octreefile, "w") as f:
487
+ self.octree.save_index(octreefile)
488
+ zf.write(octreefile, arcname="octree")
489
+
456
490
  @staticmethod
457
491
  def load(filename):
458
492
  """Construct an Epoch instance by loading it from a file
@@ -499,6 +533,15 @@ class Epoch(_py4dgeo.Epoch):
499
533
  kdtreefile = zf.extract("kdtree", path=tmp_dir)
500
534
  epoch.kdtree.load_index(kdtreefile)
501
535
 
536
+ # Restore the Octree object if present
537
+ try:
538
+ octreefile = zf.extract("octree", path=tmp_dir)
539
+ epoch.octree.load_index(octreefile)
540
+ except KeyError:
541
+ logger.warning(
542
+ "No octree found in the archive. Skipping octree loading."
543
+ )
544
+
502
545
  # Read the transformation if it exists
503
546
  if version >= 3:
504
547
  trafofile = zf.extract("trafo.json", path=tmp_dir)
@@ -578,6 +621,7 @@ def read_from_xyz(
578
621
  xyz_columns=[0, 1, 2],
579
622
  normal_columns=[],
580
623
  additional_dimensions={},
624
+ additional_dimensions_dtypes={},
581
625
  **parse_opts,
582
626
  ):
583
627
  """Create an epoch from an xyz file
@@ -603,6 +647,11 @@ def read_from_xyz(
603
647
  They will be read from the file and are accessible under their names from the
604
648
  created Epoch objects.
605
649
  Additional column indexes start with 3.
650
+ :type additional_dimensions: dict
651
+ :param additional_dimensions_dtypes:
652
+ A dictionary, mapping column names to numpy dtypes which should be used
653
+ in parsing the data.
654
+ :type additional_dimensions_dtypes: dict
606
655
  :type parse_opts: dict
607
656
  """
608
657
 
@@ -633,32 +682,29 @@ def read_from_xyz(
633
682
 
634
683
  try:
635
684
  normals = np.genfromtxt(
636
- filename, dtype=np.float64, usecols=normal_columns, **parse_opts
685
+ filename,
686
+ dtype=np.float64,
687
+ usecols=normal_columns,
688
+ **parse_opts,
637
689
  )
638
690
  except ValueError:
639
691
  raise Py4DGeoError("Malformed XYZ file")
640
692
 
641
693
  # Potentially read additional_dimensions passed by the user
642
- additional_columns = np.empty(
643
- shape=(cloud.shape[0], 1),
644
- dtype=np.dtype([(name, "<f8") for name in additional_dimensions.values()]),
645
- )
646
-
647
- add_cols = list(sorted(additional_dimensions.keys()))
648
- try:
649
- parsed_additionals = np.genfromtxt(
650
- filename, dtype=np.float64, usecols=add_cols, **parse_opts
694
+ if additional_dimensions:
695
+ additional_columns = np.genfromtxt(
696
+ filename,
697
+ dtype=np.dtype(
698
+ [
699
+ (name, additional_dimensions_dtypes.get(name, np.float64))
700
+ for name in additional_dimensions.values()
701
+ ]
702
+ ),
703
+ usecols=additional_dimensions.keys(),
704
+ **parse_opts,
651
705
  )
652
- # Ensure that the parsed array is two-dimensional, even if only
653
- # one additional dimension was given (avoids an edge case)
654
- parsed_additionals = parsed_additionals.reshape(-1, 1)
655
- except ValueError:
656
- raise Py4DGeoError("Malformed XYZ file")
657
-
658
- for i, col in enumerate(add_cols):
659
- additional_columns[additional_dimensions[col]] = parsed_additionals[
660
- :, i
661
- ].reshape(-1, 1)
706
+ else:
707
+ additional_columns = np.empty(shape=(cloud.shape[0], 1), dtype=[])
662
708
 
663
709
  # Finalize the construction of the new epoch
664
710
  new_epoch = Epoch(cloud, normals=normals, additional_dimensions=additional_columns)
@@ -675,6 +721,7 @@ def read_from_xyz(
675
721
  xyz_columns=xyz_columns,
676
722
  normal_columns=normal_columns,
677
723
  additional_dimensions=additional_dimensions,
724
+ additional_dimensions_dtypes=additional_dimensions_dtypes,
678
725
  **parse_opts,
679
726
  )
680
727
  )
@@ -725,16 +772,20 @@ def read_from_las(*filenames, normal_columns=[], additional_dimensions={}):
725
772
  ]
726
773
  ).transpose()
727
774
 
728
- # set scan positions
729
775
  # build additional_dimensions dtype structure
730
776
  additional_columns = np.empty(
731
777
  shape=(cloud.shape[0], 1),
732
- dtype=np.dtype([(name, "<f8") for name in additional_dimensions.values()]),
778
+ dtype=np.dtype(
779
+ [
780
+ (column_name, lasfile.points[column_id].dtype)
781
+ for column_id, column_name in additional_dimensions.items()
782
+ ]
783
+ ),
733
784
  )
785
+
786
+ # and fill it with the data from the lasfile
734
787
  for column_id, column_name in additional_dimensions.items():
735
- additional_columns[column_name] = np.array(
736
- lasfile.points[column_id], dtype=np.int32
737
- ).reshape(-1, 1)
788
+ additional_columns[column_name] = lasfile.points[column_id].reshape(-1, 1)
738
789
 
739
790
  # Construct Epoch and go into recursion
740
791
  new_epoch = Epoch(
py4dgeo/fallback.py CHANGED
@@ -7,7 +7,7 @@ import _py4dgeo
7
7
 
8
8
 
9
9
  def radius_workingset_finder(params: _py4dgeo.WorkingSetFinderParameters) -> np.ndarray:
10
- indices = params.epoch._kdtree.radius_search(params.corepoint, params.radius)
10
+ indices = params.epoch._radius_search(params.corepoint, params.radius)
11
11
  return params.epoch._cloud[indices, :]
12
12
 
13
13
 
@@ -35,7 +35,7 @@ def cylinder_workingset_finder(
35
35
  params.corepoint[0, :]
36
36
  + (2 * i - N + 1) / N * max_cylinder_length * params.cylinder_axis[0, :]
37
37
  )
38
- indices = params.epoch._kdtree.radius_search(qp, r_cyl)
38
+ indices = params.epoch._radius_search(qp, r_cyl)
39
39
 
40
40
  # Gather the points from the point cloud
41
41
  superset = params.epoch._cloud[indices, :]
@@ -85,12 +85,12 @@ def mean_stddev_distance(
85
85
  np.sqrt(
86
86
  variance1 / params.workingset1.shape[0]
87
87
  + variance2 / params.workingset2.shape[0]
88
- )
88
+ ).item()
89
89
  + params.registration_error
90
90
  ),
91
- spread1=np.sqrt(variance1),
91
+ spread1=np.sqrt(variance1).item(),
92
92
  num_samples1=params.workingset1.shape[0],
93
- spread2=np.sqrt(variance2),
93
+ spread2=np.sqrt(variance2).item(),
94
94
  num_samples2=params.workingset2.shape[0],
95
95
  )
96
96
 
py4dgeo/logger.py CHANGED
@@ -8,8 +8,13 @@ def create_default_logger(filename=None):
8
8
  # Create the logger instance
9
9
  logger = logging.getLogger("py4dgeo")
10
10
 
11
- # Reset the handlers to avoid handler duplication
12
- logger.handlers.clear()
11
+ # Close and remove existing handlers to avoid duplication and leaks
12
+ for handler in logger.handlers[:]:
13
+ try:
14
+ handler.close()
15
+ except Exception:
16
+ pass
17
+ logger.removeHandler(handler)
13
18
 
14
19
  # Apply default for logfile name
15
20
  if filename is None:
py4dgeo/m3c2.py CHANGED
@@ -16,7 +16,6 @@ import laspy
16
16
 
17
17
  import _py4dgeo
18
18
 
19
-
20
19
  logger = logging.getLogger("py4dgeo")
21
20
 
22
21
 
@@ -72,7 +71,9 @@ class M3C2LikeAlgorithm(abc.ABC):
72
71
  """The normal direction(s) to use for this algorithm."""
73
72
  raise NotImplementedError
74
73
 
75
- def calculate_distances(self, epoch1, epoch2):
74
+ def calculate_distances(
75
+ self, epoch1, epoch2, searchtree: typing.Optional[str] = None
76
+ ):
76
77
  """Calculate the distances between two epochs"""
77
78
 
78
79
  if isinstance(self.cyl_radii, typing.Iterable):
@@ -80,7 +81,9 @@ class M3C2LikeAlgorithm(abc.ABC):
80
81
  "DEPRECATION: use cyl_radius instead of cyl_radii. In a future version, cyl_radii will be removed!"
81
82
  )
82
83
  if len(self.cyl_radii) != 1:
83
- Py4DGeoError("cyl_radii must be a list containing a single float!")
84
+ raise Py4DGeoError(
85
+ "cyl_radii must be a list containing a single float!"
86
+ )
84
87
  elif self.cyl_radius is None:
85
88
  self.cyl_radius = self.cyl_radii[0]
86
89
  self.cyl_radii = None
@@ -90,11 +93,9 @@ class M3C2LikeAlgorithm(abc.ABC):
90
93
  f"{self.name} requires exactly one cylinder radius to be given as a float."
91
94
  )
92
95
 
93
- # Ensure that the KDTree data structures have been built. This is no-op
94
- # if it has already been triggered before - e.g. by a user with a custom
95
- # leaf cutoff parameter.
96
- epoch1.build_kdtree()
97
- epoch2.build_kdtree()
96
+ # Ensure appropriate trees are built
97
+ epoch1._validate_search_tree()
98
+ epoch2._validate_search_tree()
98
99
 
99
100
  distances, uncertainties = _py4dgeo.compute_distances(
100
101
  self.corepoints,
@@ -174,8 +175,9 @@ class M3C2(M3C2LikeAlgorithm):
174
175
  if normals_epoch is None:
175
176
  normals_epoch = self.epochs[0]
176
177
  normals_epoch = as_epoch(normals_epoch)
177
- # Ensure that the KDTree data structures have been built.
178
- normals_epoch.build_kdtree()
178
+
179
+ # Ensure appropriate tree structures have been built
180
+ normals_epoch._validate_search_tree()
179
181
 
180
182
  # Trigger the precomputation
181
183
  self.corepoint_normals, self._directions_radii = (
py4dgeo/m3c2ep.py CHANGED
@@ -61,8 +61,9 @@ class M3C2EP(M3C2):
61
61
  f"{self.name} requires exactly one cylinder radius to be given"
62
62
  )
63
63
 
64
- epoch1.build_kdtree()
65
- epoch2.build_kdtree()
64
+ # Ensure appropriate trees are built
65
+ epoch1._validate_search_tree()
66
+ epoch2._validate_search_tree()
66
67
 
67
68
  p1_coords = epoch1.cloud
68
69
  p1_positions = epoch1.scanpos_id
@@ -802,12 +803,12 @@ def process_corepoint_list(
802
803
  normal = n[np.newaxis, :]
803
804
  M3C2_spread1[cp_idx] = np.sqrt(
804
805
  np.matmul(np.matmul(normal, p1_CoG_Cxx), normal.T)
805
- )
806
+ ).squeeze()
806
807
  M3C2_spread2[cp_idx] = np.sqrt(
807
808
  np.matmul(np.matmul(normal, p2_CoG_Cxx), normal.T)
808
- )
809
- M3C2_cov1[cp_idx] = p1_CoG_Cxx
810
- M3C2_cov2[cp_idx] = p2_CoG_Cxx
809
+ ).squeeze()
810
+ M3C2_cov1[cp_idx] = p1_CoG_Cxx.squeeze()
811
+ M3C2_cov2[cp_idx] = p2_CoG_Cxx.squeeze()
811
812
 
812
813
  pbarQueue.put((1, 0)) # point processed
813
814
 
@@ -837,13 +838,13 @@ def radius_search(epoch: Epoch, query: np.ndarray, radius: float):
837
838
  :type radius: float
838
839
  """
839
840
  if len(query.shape) == 1 and query.shape[0] == 3:
840
- return [epoch.kdtree.radius_search(query, radius)]
841
+ return [epoch._radius_search(query, radius)]
841
842
 
842
843
  if len(query.shape) == 2 and query.shape[1] == 3:
843
844
  neighbors = []
844
845
  for i in range(query.shape[0]):
845
846
  q = query[i]
846
- result = epoch.kdtree.radius_search(q, radius)
847
+ result = epoch._radius_search(q, radius)
847
848
  neighbors.append(result)
848
849
  return neighbors
849
850