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.
- _py4dgeo.cp311-win_amd64.pyd +0 -0
- py4dgeo/__init__.py +16 -1
- py4dgeo/cloudcompare.py +0 -1
- py4dgeo/epoch.py +79 -28
- py4dgeo/fallback.py +5 -5
- py4dgeo/logger.py +7 -2
- py4dgeo/m3c2.py +12 -10
- py4dgeo/m3c2ep.py +9 -8
- py4dgeo/pbm3c2.py +569 -3745
- py4dgeo/py4dgeo_python.cpp +345 -18
- py4dgeo/segmentation.py +56 -11
- py4dgeo/util.py +131 -46
- 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-0.7.0.dist-info → py4dgeo-1.0.0.dist-info}/METADATA +28 -16
- py4dgeo-1.0.0.dist-info/RECORD +22 -0
- {py4dgeo-0.7.0.dist-info → py4dgeo-1.0.0.dist-info}/WHEEL +1 -1
- py4dgeo-0.7.0.dist-info/RECORD +0 -20
- py4dgeo-0.7.0.dist-info/licenses/COPYING.md +0 -17
- {py4dgeo-0.7.0.dist-info → py4dgeo-1.0.0.dist-info}/entry_points.txt +0 -0
- {py4dgeo-0.7.0.dist-info → py4dgeo-1.0.0.dist-info}/licenses/LICENSE.md +0 -0
_py4dgeo.cp311-win_amd64.pyd
CHANGED
|
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
|
-
|
|
45
|
+
initialize_openmp_defaults()
|
|
46
|
+
|
|
47
|
+
from py4dgeo.pbm3c2 import PBM3C2
|
py4dgeo/cloudcompare.py
CHANGED
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
|
-
|
|
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,
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
653
|
-
|
|
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(
|
|
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] =
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
12
|
-
logger.handlers
|
|
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(
|
|
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(
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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.
|
|
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.
|
|
847
|
+
result = epoch._radius_search(q, radius)
|
|
847
848
|
neighbors.append(result)
|
|
848
849
|
return neighbors
|
|
849
850
|
|