py4dgeo 0.7.0__cp310-cp310-macosx_14_0_arm64.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.cpython-310-darwin.so +0 -0
- py4dgeo/.dylibs/libomp.dylib +0 -0
- py4dgeo/UpdateableZipFile.py +81 -0
- py4dgeo/__init__.py +32 -0
- py4dgeo/cloudcompare.py +32 -0
- py4dgeo/epoch.py +814 -0
- py4dgeo/fallback.py +159 -0
- py4dgeo/logger.py +77 -0
- py4dgeo/m3c2.py +244 -0
- py4dgeo/m3c2ep.py +855 -0
- py4dgeo/pbm3c2.py +3870 -0
- py4dgeo/py4dgeo_python.cpp +487 -0
- py4dgeo/registration.py +474 -0
- py4dgeo/segmentation.py +1280 -0
- py4dgeo/util.py +263 -0
- py4dgeo-0.7.0.dist-info/METADATA +200 -0
- py4dgeo-0.7.0.dist-info/RECORD +21 -0
- py4dgeo-0.7.0.dist-info/WHEEL +5 -0
- py4dgeo-0.7.0.dist-info/entry_points.txt +3 -0
- py4dgeo-0.7.0.dist-info/licenses/COPYING.md +17 -0
- py4dgeo-0.7.0.dist-info/licenses/LICENSE.md +5 -0
py4dgeo/pbm3c2.py
ADDED
|
@@ -0,0 +1,3870 @@
|
|
|
1
|
+
import colorsys
|
|
2
|
+
import random
|
|
3
|
+
import typing
|
|
4
|
+
import pprint
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin
|
|
10
|
+
from sklearn.utils.validation import check_X_y, check_array, check_is_fitted
|
|
11
|
+
from sklearn.utils.multiclass import unique_labels
|
|
12
|
+
|
|
13
|
+
from sklearn.pipeline import Pipeline
|
|
14
|
+
from sklearn.compose import make_column_selector
|
|
15
|
+
|
|
16
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
17
|
+
from sklearn import set_config
|
|
18
|
+
|
|
19
|
+
set_config(display="diagram")
|
|
20
|
+
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
|
|
23
|
+
from py4dgeo import Epoch
|
|
24
|
+
from py4dgeo.util import Py4DGeoError, find_file
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from vedo import *
|
|
28
|
+
|
|
29
|
+
interactive_available = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
interactive_available = False
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Viewer",
|
|
35
|
+
"BaseTransformer",
|
|
36
|
+
"PerPointComputation",
|
|
37
|
+
"Segmentation",
|
|
38
|
+
"ExtractSegments",
|
|
39
|
+
"BuilderExtended_y",
|
|
40
|
+
"BuilderExtended_y_Visually",
|
|
41
|
+
"ClassifierWrapper",
|
|
42
|
+
"PBM3C2",
|
|
43
|
+
"build_input_scenario2_without_normals",
|
|
44
|
+
"build_input_scenario2_with_normals",
|
|
45
|
+
"PBM3C2WithSegments",
|
|
46
|
+
"set_interactive_backend",
|
|
47
|
+
"generate_random_extended_y",
|
|
48
|
+
"LLSV_PCA_COLUMNS",
|
|
49
|
+
"SEGMENTED_POINT_CLOUD_COLUMNS",
|
|
50
|
+
"SEGMENT_COLUMNS",
|
|
51
|
+
"generate_extended_y_from_prior_knowledge",
|
|
52
|
+
"generate_possible_region_pairs",
|
|
53
|
+
"DEFAULT_NO_SEGMENT",
|
|
54
|
+
"DEFAULT_STD_DEVIATION_OF_NO_CORE_POINT",
|
|
55
|
+
"add_no_corresponding_seg",
|
|
56
|
+
"config_epoch0_as_segments",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger("py4dgeo")
|
|
60
|
+
|
|
61
|
+
pp = pprint.PrettyPrinter(depth=4)
|
|
62
|
+
|
|
63
|
+
config_epoch0_as_segments = {
|
|
64
|
+
"get_pipeline_options": True,
|
|
65
|
+
"epoch0_Transform_PerPointComputation__skip": True,
|
|
66
|
+
"epoch0_Transform_Segmentation__skip": True,
|
|
67
|
+
"epoch0_Transform_Second_Segmentation__skip": True,
|
|
68
|
+
"epoch0_Transform_ExtractSegments__skip": True,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LLSV_PCA_COLUMNS:
|
|
73
|
+
X_COLUMN = 0
|
|
74
|
+
Y_COLUMN = 1
|
|
75
|
+
Z_COLUMN = 2
|
|
76
|
+
EPOCH_ID_COLUMN = 3
|
|
77
|
+
EIGENVALUE0_COLUMN = 4
|
|
78
|
+
EIGENVALUE1_COLUMN = 5
|
|
79
|
+
EIGENVALUE2_COLUMN = 6
|
|
80
|
+
EIGENVECTOR_0_X_COLUMN = 7
|
|
81
|
+
EIGENVECTOR_0_Y_COLUMN = 8
|
|
82
|
+
EIGENVECTOR_0_Z_COLUMN = 9
|
|
83
|
+
EIGENVECTOR_1_X_COLUMN = 10
|
|
84
|
+
EIGENVECTOR_1_Y_COLUMN = 11
|
|
85
|
+
EIGENVECTOR_1_Z_COLUMN = 12
|
|
86
|
+
EIGENVECTOR_2_X_COLUMN = 13
|
|
87
|
+
EIGENVECTOR_2_Y_COLUMN = 14
|
|
88
|
+
EIGENVECTOR_2_Z_COLUMN = 15
|
|
89
|
+
LLSV_COLUMN = 16
|
|
90
|
+
NUMBER_OF_COLUMNS = 17
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SEGMENTED_POINT_CLOUD_COLUMNS:
|
|
94
|
+
X_COLUMN = 0
|
|
95
|
+
Y_COLUMN = 1
|
|
96
|
+
Z_COLUMN = 2
|
|
97
|
+
EPOCH_ID_COLUMN = 3
|
|
98
|
+
EIGENVALUE0_COLUMN = 4
|
|
99
|
+
EIGENVALUE1_COLUMN = 5
|
|
100
|
+
EIGENVALUE2_COLUMN = 6
|
|
101
|
+
EIGENVECTOR_0_X_COLUMN = 7
|
|
102
|
+
EIGENVECTOR_0_Y_COLUMN = 8
|
|
103
|
+
EIGENVECTOR_0_Z_COLUMN = 9
|
|
104
|
+
EIGENVECTOR_1_X_COLUMN = 10
|
|
105
|
+
EIGENVECTOR_1_Y_COLUMN = 11
|
|
106
|
+
EIGENVECTOR_1_Z_COLUMN = 12
|
|
107
|
+
EIGENVECTOR_2_X_COLUMN = 13
|
|
108
|
+
EIGENVECTOR_2_Y_COLUMN = 14
|
|
109
|
+
EIGENVECTOR_2_Z_COLUMN = 15
|
|
110
|
+
LLSV_COLUMN = 16
|
|
111
|
+
SEGMENT_ID_COLUMN = 17
|
|
112
|
+
STANDARD_DEVIATION_COLUMN = 18
|
|
113
|
+
NUMBER_OF_COLUMNS = 19
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class SEGMENT_COLUMNS:
|
|
117
|
+
X_COLUMN = 0
|
|
118
|
+
Y_COLUMN = 1
|
|
119
|
+
Z_COLUMN = 2
|
|
120
|
+
EPOCH_ID_COLUMN = 3
|
|
121
|
+
EIGENVALUE0_COLUMN = 4
|
|
122
|
+
EIGENVALUE1_COLUMN = 5
|
|
123
|
+
EIGENVALUE2_COLUMN = 6
|
|
124
|
+
EIGENVECTOR_0_X_COLUMN = 7
|
|
125
|
+
EIGENVECTOR_0_Y_COLUMN = 8
|
|
126
|
+
EIGENVECTOR_0_Z_COLUMN = 9
|
|
127
|
+
EIGENVECTOR_1_X_COLUMN = 10
|
|
128
|
+
EIGENVECTOR_1_Y_COLUMN = 11
|
|
129
|
+
EIGENVECTOR_1_Z_COLUMN = 12
|
|
130
|
+
EIGENVECTOR_2_X_COLUMN = 13
|
|
131
|
+
EIGENVECTOR_2_Y_COLUMN = 14
|
|
132
|
+
EIGENVECTOR_2_Z_COLUMN = 15
|
|
133
|
+
LLSV_COLUMN = 16
|
|
134
|
+
SEGMENT_ID_COLUMN = 17
|
|
135
|
+
STANDARD_DEVIATION_COLUMN = 18
|
|
136
|
+
NR_POINTS_PER_SEG_COLUMN = 19
|
|
137
|
+
NUMBER_OF_COLUMNS = 20
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# default value, for the points that are part of NO segment ( e.g. -1 )
|
|
141
|
+
DEFAULT_NO_SEGMENT = -1
|
|
142
|
+
|
|
143
|
+
# default standard deviation value for points that are not "core points"
|
|
144
|
+
DEFAULT_STD_DEVIATION_OF_NO_CORE_POINT = -1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def set_interactive_backend(backend="vtk"):
|
|
148
|
+
"""Set the interactive backend for selection of correspondent segments.
|
|
149
|
+
|
|
150
|
+
All backends that can be used with the vedo library can be given here.
|
|
151
|
+
E.g. the following backends are available: vtk, ipyvtk, k3d, 2d, ipygany, panel, itk
|
|
152
|
+
"""
|
|
153
|
+
if interactive_available:
|
|
154
|
+
from vedo import settings
|
|
155
|
+
|
|
156
|
+
settings.default_backend = backend
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _extract_from_additional_dimensions(
|
|
160
|
+
epoch: Epoch,
|
|
161
|
+
column_names: typing.List[str],
|
|
162
|
+
required_number_of_columns: typing.List[int] = [],
|
|
163
|
+
):
|
|
164
|
+
"""
|
|
165
|
+
Build a numpy array using 'column_names' which are part of the 'additional_dimensions' field.
|
|
166
|
+
The result will maintain the same order or the columns found in 'column_names'.
|
|
167
|
+
|
|
168
|
+
:param epoch:
|
|
169
|
+
Epoch class.
|
|
170
|
+
:param column_names:
|
|
171
|
+
list[ str ]
|
|
172
|
+
:param required_number_of_columns:
|
|
173
|
+
list[ int ]
|
|
174
|
+
default [] , any number of parameter found gets accepted.
|
|
175
|
+
The number of columns required to consider the output valid.
|
|
176
|
+
:return
|
|
177
|
+
numpy array
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
result = np.empty(shape=(epoch.cloud.shape[0], 0), dtype=float)
|
|
181
|
+
|
|
182
|
+
for column in column_names:
|
|
183
|
+
if column in epoch.additional_dimensions.dtype.names:
|
|
184
|
+
result = np.concatenate(
|
|
185
|
+
(result, epoch.additional_dimensions[column]), axis=1
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
logger.debug(
|
|
189
|
+
f"Column '{column}' not found during _extract_from_additional_dimensions()"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert (
|
|
193
|
+
required_number_of_columns == []
|
|
194
|
+
or result.shape[1] in required_number_of_columns
|
|
195
|
+
), "The number of column found is not a valid one."
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def angle_difference_compute(normal1, normal2):
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
:param normal1:
|
|
204
|
+
unit vector
|
|
205
|
+
:param normal2:
|
|
206
|
+
unit vector
|
|
207
|
+
:return:
|
|
208
|
+
numpy array of angles in degrees.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
# normal1, normal2 have to be unit vectors ( and that is the case as a result of the SVD process )
|
|
212
|
+
return np.arccos(np.clip(np.dot(normal1, normal2), -1.0, 1.0)) * 180.0 / np.pi
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def geodesic_distance(v1, v2):
|
|
216
|
+
"""
|
|
217
|
+
Compute the shortest angular distance between 2 unit vectors.
|
|
218
|
+
|
|
219
|
+
:param v1:
|
|
220
|
+
unit vector
|
|
221
|
+
:param v2:
|
|
222
|
+
unit vector
|
|
223
|
+
:return:
|
|
224
|
+
numpy array of angles in degrees.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
return min(
|
|
228
|
+
np.arccos(np.clip(np.dot(v1, v2), -1.0, 1.0)) * 180.0 / np.pi,
|
|
229
|
+
np.arccos(np.clip(np.dot(v1, -v2), -1.0, 1.0)) * 180.0 / np.pi,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Viewer:
|
|
234
|
+
def __init__(self):
|
|
235
|
+
self.sets = []
|
|
236
|
+
self.plt = Plotter(axes=3)
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def HSVToRGB(h, s, v):
|
|
240
|
+
"""
|
|
241
|
+
Convert from HSV ( Hue Saturation Value ) color to RGB ( Red Blue Green )
|
|
242
|
+
|
|
243
|
+
:param h:
|
|
244
|
+
float [0, 1]
|
|
245
|
+
:param s:
|
|
246
|
+
float [0, 1]
|
|
247
|
+
:param v:
|
|
248
|
+
float [0, 1]
|
|
249
|
+
:return:
|
|
250
|
+
tuple [ float, float, float ]
|
|
251
|
+
"""
|
|
252
|
+
return colorsys.hsv_to_rgb(h, s, v)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def get_distinct_colors(n):
|
|
256
|
+
"""
|
|
257
|
+
Return a python list of 'n' distinct colors.
|
|
258
|
+
|
|
259
|
+
:param n:
|
|
260
|
+
number of colors.
|
|
261
|
+
:return:
|
|
262
|
+
python list of tuple [ float, float, float ]
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
huePartition = 1.0 / (n + 1)
|
|
266
|
+
return [
|
|
267
|
+
Viewer.HSVToRGB(huePartition * value, 1.0, 1.0) for value in range(0, n)
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def read_np_ndarray_from_xyz(input_file_name: str) -> np.ndarray:
|
|
272
|
+
"""
|
|
273
|
+
The reconstructed np.ndarray.
|
|
274
|
+
:param input_file_name:
|
|
275
|
+
The output file name.
|
|
276
|
+
:return:
|
|
277
|
+
np.ndarray
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
# Resolve the given path
|
|
281
|
+
filename = find_file(input_file_name)
|
|
282
|
+
|
|
283
|
+
# Read it
|
|
284
|
+
try:
|
|
285
|
+
logger.info(f"Reading np.ndarray from file '{filename}'")
|
|
286
|
+
np_ndarray = np.genfromtxt(filename, delimiter=",")
|
|
287
|
+
except ValueError:
|
|
288
|
+
raise Py4DGeoError("Malformed file: " + str(filename))
|
|
289
|
+
|
|
290
|
+
return np_ndarray
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def segmented_point_cloud_visualizer(
|
|
294
|
+
X: np.ndarray, columns=SEGMENTED_POINT_CLOUD_COLUMNS
|
|
295
|
+
):
|
|
296
|
+
"""
|
|
297
|
+
Visualize a segmented point cloud. ( the resulting point cloud after the segmentation process )
|
|
298
|
+
|
|
299
|
+
:param X:
|
|
300
|
+
numpy array (n_points, 19) with the following column structure:
|
|
301
|
+
[
|
|
302
|
+
x, y, z ( 3 columns ),
|
|
303
|
+
EpochID ( 1 column ),
|
|
304
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
305
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
306
|
+
Lowest local surface variation ( 1 column ),
|
|
307
|
+
Segment_ID ( 1 column ),
|
|
308
|
+
Standard deviation ( 1 column )
|
|
309
|
+
]
|
|
310
|
+
:return:
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
X_Y_Z_Columns = [columns.X_COLUMN, columns.Y_COLUMN, columns.Z_COLUMN]
|
|
314
|
+
|
|
315
|
+
viewer = Viewer()
|
|
316
|
+
|
|
317
|
+
nr_segments = int(X[:, columns.SEGMENT_ID_COLUMN].max())
|
|
318
|
+
colors = Viewer.get_distinct_colors(nr_segments + 1)
|
|
319
|
+
|
|
320
|
+
for i in range(0, nr_segments + 1):
|
|
321
|
+
mask = X[:, columns.SEGMENT_ID_COLUMN] == float(i)
|
|
322
|
+
# x,y,z
|
|
323
|
+
set_cloud = X[mask, :][:, X_Y_Z_Columns]
|
|
324
|
+
|
|
325
|
+
viewer.sets = viewer.sets + [Points(set_cloud, colors[i], alpha=1, r=10)]
|
|
326
|
+
|
|
327
|
+
viewer.plt.show(viewer.sets).close()
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def segments_visualizer(X: np.ndarray, columns=SEGMENT_COLUMNS):
|
|
331
|
+
"""
|
|
332
|
+
Segments visualizer.
|
|
333
|
+
|
|
334
|
+
:param X:
|
|
335
|
+
Each row is a segment, numpy array (1, 20) with the following column structure:
|
|
336
|
+
[
|
|
337
|
+
x, y, z ( 3 columns ), -> segment, core point
|
|
338
|
+
EpochID ( 1 column ),
|
|
339
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
340
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
341
|
+
Lowest local surface variation ( 1 column ),
|
|
342
|
+
Segment_ID ( 1 column ),
|
|
343
|
+
Standard deviation ( 1 column ),
|
|
344
|
+
Number of points found in Segment_ID segment ( 1 column )
|
|
345
|
+
]
|
|
346
|
+
:param columns:
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
viewer = Viewer()
|
|
350
|
+
|
|
351
|
+
nr_segments = X.shape[0]
|
|
352
|
+
colors = [(1, 0, 0), (0, 1, 0)]
|
|
353
|
+
|
|
354
|
+
viewer.plt.add_callback("KeyPress", viewer.toggle_transparency)
|
|
355
|
+
|
|
356
|
+
for i in range(0, nr_segments):
|
|
357
|
+
if X[i, columns.EPOCH_ID_COLUMN] == 0:
|
|
358
|
+
color = colors[0]
|
|
359
|
+
else:
|
|
360
|
+
color = colors[1]
|
|
361
|
+
|
|
362
|
+
ellipsoid = Ellipsoid(
|
|
363
|
+
pos=(X[i, 0], X[i, 1], X[i, 2]),
|
|
364
|
+
axis1=[
|
|
365
|
+
X[i, columns.EIGENVECTOR_0_X_COLUMN]
|
|
366
|
+
* X[i, columns.EIGENVALUE0_COLUMN]
|
|
367
|
+
* 0.5,
|
|
368
|
+
X[i, columns.EIGENVECTOR_0_Y_COLUMN]
|
|
369
|
+
* X[i, columns.EIGENVALUE0_COLUMN]
|
|
370
|
+
* 0.5,
|
|
371
|
+
X[i, columns.EIGENVECTOR_0_Z_COLUMN]
|
|
372
|
+
* X[i, columns.EIGENVALUE0_COLUMN]
|
|
373
|
+
* 0.3,
|
|
374
|
+
],
|
|
375
|
+
axis2=[
|
|
376
|
+
X[i, columns.EIGENVECTOR_1_X_COLUMN]
|
|
377
|
+
* X[i, columns.EIGENVALUE1_COLUMN]
|
|
378
|
+
* 0.5,
|
|
379
|
+
X[i, columns.EIGENVECTOR_1_Y_COLUMN]
|
|
380
|
+
* X[i, columns.EIGENVALUE1_COLUMN]
|
|
381
|
+
* 0.5,
|
|
382
|
+
X[i, columns.EIGENVECTOR_1_Z_COLUMN]
|
|
383
|
+
* X[i, columns.EIGENVALUE1_COLUMN]
|
|
384
|
+
* 0.5,
|
|
385
|
+
],
|
|
386
|
+
axis3=[
|
|
387
|
+
X[i, columns.EIGENVECTOR_2_X_COLUMN] * 0.1,
|
|
388
|
+
X[i, columns.EIGENVECTOR_2_Y_COLUMN] * 0.1,
|
|
389
|
+
X[i, columns.EIGENVECTOR_2_Z_COLUMN] * 0.1,
|
|
390
|
+
],
|
|
391
|
+
res=24,
|
|
392
|
+
c=color,
|
|
393
|
+
alpha=1,
|
|
394
|
+
)
|
|
395
|
+
# ellipsoid.caption(txt=str(i), size=(0.1, 0.05))
|
|
396
|
+
ellipsoid.id = X[i, columns.SEGMENT_ID_COLUMN]
|
|
397
|
+
ellipsoid.epoch = X[i, columns.EPOCH_ID_COLUMN]
|
|
398
|
+
ellipsoid.isOn = True
|
|
399
|
+
|
|
400
|
+
viewer.sets = viewer.sets + [ellipsoid]
|
|
401
|
+
|
|
402
|
+
viewer.plt.show(
|
|
403
|
+
viewer.sets,
|
|
404
|
+
Text2D(
|
|
405
|
+
"'z' - toggle transparency on/off "
|
|
406
|
+
"'g' - toggle on/off red ellipsoids "
|
|
407
|
+
"'d' - toggle on/off green ellipsoids",
|
|
408
|
+
pos="top-left",
|
|
409
|
+
bg="k",
|
|
410
|
+
s=0.7,
|
|
411
|
+
),
|
|
412
|
+
).close()
|
|
413
|
+
|
|
414
|
+
def toggle_transparency(self, evt):
|
|
415
|
+
if evt.keyPressed == "z":
|
|
416
|
+
logger.info("transparency toggle")
|
|
417
|
+
for segment in self.sets:
|
|
418
|
+
if segment.alpha() < 1.0:
|
|
419
|
+
segment.alpha(1)
|
|
420
|
+
else:
|
|
421
|
+
segment.alpha(0.5)
|
|
422
|
+
self.plt.render()
|
|
423
|
+
|
|
424
|
+
if evt.keyPressed == "g":
|
|
425
|
+
logger.info("toggle red")
|
|
426
|
+
for segment in self.sets:
|
|
427
|
+
if segment.epoch == 0:
|
|
428
|
+
if segment.isOn:
|
|
429
|
+
segment.off()
|
|
430
|
+
else:
|
|
431
|
+
segment.on()
|
|
432
|
+
segment.isOn = not segment.isOn
|
|
433
|
+
self.plt.render()
|
|
434
|
+
|
|
435
|
+
if evt.keyPressed == "d":
|
|
436
|
+
logger.info("toggle green")
|
|
437
|
+
for segment in self.sets:
|
|
438
|
+
if segment.epoch == 1:
|
|
439
|
+
if segment.isOn:
|
|
440
|
+
segment.off()
|
|
441
|
+
else:
|
|
442
|
+
segment.on()
|
|
443
|
+
segment.isOn = not segment.isOn
|
|
444
|
+
self.plt.render()
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def generate_random_extended_y(
|
|
448
|
+
X,
|
|
449
|
+
extended_y_file_name="locally_generated_extended_y.csv",
|
|
450
|
+
ratio=1 / 3,
|
|
451
|
+
low=0,
|
|
452
|
+
high=1,
|
|
453
|
+
columns=SEGMENT_COLUMNS,
|
|
454
|
+
):
|
|
455
|
+
"""
|
|
456
|
+
Generate a subset (1/3 from the total possible pairs) of random tuples of segments ID
|
|
457
|
+
(where each ID gets to be extracted from a different epoch) which are randomly labeled
|
|
458
|
+
between low and high)
|
|
459
|
+
|
|
460
|
+
:param X:
|
|
461
|
+
(n_segments, 20)
|
|
462
|
+
Each row contains a segment.
|
|
463
|
+
:param extended_y_file_name:
|
|
464
|
+
Name of the file where the serialized result is saved.
|
|
465
|
+
:param ratio:
|
|
466
|
+
The size of the pairs' subset size.
|
|
467
|
+
:param low:
|
|
468
|
+
Default minimum random value used as label
|
|
469
|
+
:param high:
|
|
470
|
+
Default maximum random value used as label
|
|
471
|
+
:param columns:
|
|
472
|
+
Column mapping used by each segment.
|
|
473
|
+
:return:
|
|
474
|
+
numpy (m_pairs, 3)
|
|
475
|
+
Where each row contains tuples of set0 segment id, set1 segment id, rand 0/1.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
mask_epoch0 = X[:, columns.EPOCH_ID_COLUMN] == 0
|
|
479
|
+
mask_epoch1 = X[:, columns.EPOCH_ID_COLUMN] == 1
|
|
480
|
+
|
|
481
|
+
epoch0_set = X[mask_epoch0, :] # all
|
|
482
|
+
epoch1_set = X[mask_epoch1, :] # all
|
|
483
|
+
|
|
484
|
+
ratio = np.clip(ratio, 0, 1)
|
|
485
|
+
nr_pairs = round(min(epoch0_set.shape[0], epoch1_set.shape[0]) * ratio)
|
|
486
|
+
|
|
487
|
+
indx0_seg_id = random.sample(range(epoch0_set.shape[0]), nr_pairs)
|
|
488
|
+
indx1_seg_id = random.sample(range(epoch1_set.shape[0]), nr_pairs)
|
|
489
|
+
|
|
490
|
+
set0_seg_id = epoch0_set[indx0_seg_id, columns.SEGMENT_ID_COLUMN]
|
|
491
|
+
set1_seg_id = epoch1_set[indx1_seg_id, columns.SEGMENT_ID_COLUMN]
|
|
492
|
+
|
|
493
|
+
rand_y = list(np.random.randint(low, high + 1, nr_pairs))
|
|
494
|
+
|
|
495
|
+
np.savetxt(
|
|
496
|
+
extended_y_file_name,
|
|
497
|
+
np.array([set0_seg_id, set1_seg_id, rand_y]).T,
|
|
498
|
+
delimiter=",",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
return np.array([set0_seg_id, set1_seg_id, rand_y]).T
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def generate_possible_region_pairs(
|
|
505
|
+
segments: np.ndarray, seg_id0_seg_id1_label: np.ndarray
|
|
506
|
+
):
|
|
507
|
+
"""
|
|
508
|
+
:param segments:
|
|
509
|
+
numpy array of shape (n_segments, segment_size)
|
|
510
|
+
:param seg_id0_seg_id1_label:
|
|
511
|
+
extended_y, numpy array (n_pairs, 3)
|
|
512
|
+
where each row contains: (id_segment_epoch0, id_segment_epoch1, label=0/1)
|
|
513
|
+
:return:
|
|
514
|
+
numpy array of shape (m_pairs, 7) where each row contain:
|
|
515
|
+
pairs_of_points[i, :3] -> a proposed position of a segment from epoch 0
|
|
516
|
+
pairs_pf_points[i, 3:] -> a proposed position of a segment from epoch 1
|
|
517
|
+
label: 0/1
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
segment_pairs = seg_id0_seg_id1_label
|
|
521
|
+
|
|
522
|
+
out = np.empty(shape=(0, 7))
|
|
523
|
+
for row in range(segment_pairs.shape[0]):
|
|
524
|
+
id_epoch0 = int(segment_pairs[row, 0])
|
|
525
|
+
id_epoch1 = int(segment_pairs[row, 1])
|
|
526
|
+
label = int(segment_pairs[row, 2])
|
|
527
|
+
points = np.hstack(
|
|
528
|
+
(
|
|
529
|
+
segments[id_epoch0, 0:3] + np.random.normal(0, 1),
|
|
530
|
+
segments[id_epoch1, 3:6] + np.random.normal(0, 1),
|
|
531
|
+
label,
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
out = np.vstack((out, points))
|
|
535
|
+
|
|
536
|
+
return out
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def generate_extended_y_from_prior_knowledge(
|
|
540
|
+
segments: np.ndarray,
|
|
541
|
+
pairs_of_points: np.ndarray,
|
|
542
|
+
threshold_max_distance: float,
|
|
543
|
+
columns=SEGMENT_COLUMNS,
|
|
544
|
+
) -> np.ndarray:
|
|
545
|
+
"""
|
|
546
|
+
:param segments:
|
|
547
|
+
numpy array of shape (n_segments, segment_size)
|
|
548
|
+
:param pairs_of_points:
|
|
549
|
+
numpy array of shape (m_pairs, 7) where each row contain:
|
|
550
|
+
pair_of_points[i, :3] -> a proposed position of a segment from epoch 0
|
|
551
|
+
pair_pf_points[i, 3:] -> a proposed position of a segment from epoch 1
|
|
552
|
+
label: 0/1
|
|
553
|
+
:param threshold_max_distance:
|
|
554
|
+
the radios accepted threshold for possible segments
|
|
555
|
+
:param columns:
|
|
556
|
+
Column mapping used by each segment.
|
|
557
|
+
:return:
|
|
558
|
+
extended_y, numpy array (n_pairs, 3)
|
|
559
|
+
where each row contains: (id_segment_epoch0, id_segment_epoch1, label=0/1)
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
extended_y = np.empty(shape=(0, 3), dtype=float)
|
|
563
|
+
|
|
564
|
+
# split points(segments) between epoch0 and epoch1
|
|
565
|
+
epoch0_mask = segments[:, columns.EPOCH_ID_COLUMN] == 0
|
|
566
|
+
epoch1_mask = segments[:, columns.EPOCH_ID_COLUMN] == 1
|
|
567
|
+
|
|
568
|
+
X_Y_Z_Columns = [columns.X_COLUMN, columns.Y_COLUMN, columns.Z_COLUMN]
|
|
569
|
+
|
|
570
|
+
epoch0_set = segments[epoch0_mask][X_Y_Z_Columns]
|
|
571
|
+
epoch1_set = segments[epoch1_mask][X_Y_Z_Columns]
|
|
572
|
+
|
|
573
|
+
# generate kd-tree for each of the 2 sets
|
|
574
|
+
epoch0 = Epoch(epoch0_set.T)
|
|
575
|
+
epoch0.build_kdtree()
|
|
576
|
+
epoch1 = Epoch(epoch1_set.T)
|
|
577
|
+
epoch1.build_kdtree()
|
|
578
|
+
|
|
579
|
+
# search for the near segments and build the 'extended y'
|
|
580
|
+
for row in pairs_of_points:
|
|
581
|
+
seg_epoch0, seg_epoch1, label = np.split(ary=row, indices_or_sections=[3, 6])
|
|
582
|
+
label = label[0]
|
|
583
|
+
|
|
584
|
+
candidates_seg_epoch0 = epoch0.kdtree.radius_search(
|
|
585
|
+
seg_epoch0, threshold_max_distance
|
|
586
|
+
)
|
|
587
|
+
candidates_seg_epoch1 = epoch1.kdtree.radius_search(
|
|
588
|
+
seg_epoch1, threshold_max_distance
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
if len(candidates_seg_epoch0) > 0 and len(candidates_seg_epoch1) > 0:
|
|
592
|
+
indx_min_epoch0 = candidates_seg_epoch0[
|
|
593
|
+
np.linalg.norm(
|
|
594
|
+
epoch0.cloud[candidates_seg_epoch0] - seg_epoch0
|
|
595
|
+
).argmin()
|
|
596
|
+
]
|
|
597
|
+
indx_min_epoch1 = candidates_seg_epoch1[
|
|
598
|
+
np.linalg.norm(
|
|
599
|
+
epoch1.cloud[candidates_seg_epoch0] - seg_epoch1
|
|
600
|
+
).argmin()
|
|
601
|
+
]
|
|
602
|
+
|
|
603
|
+
extended_y = np.vstack(
|
|
604
|
+
(
|
|
605
|
+
extended_y,
|
|
606
|
+
# index segment epoch0 , index segment epoch1, label=0/1
|
|
607
|
+
np.array([indx_min_epoch0, indx_min_epoch1, label]),
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
return extended_y
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def add_no_corresponding_seg(
|
|
615
|
+
segments: np.ndarray,
|
|
616
|
+
extended_y: np.ndarray = None,
|
|
617
|
+
threshold_max_distance: float = 3,
|
|
618
|
+
algorithm="closest",
|
|
619
|
+
extended_y_file_name: str = "extended_y.csv",
|
|
620
|
+
columns=SEGMENT_COLUMNS,
|
|
621
|
+
):
|
|
622
|
+
"""
|
|
623
|
+
:param segments:
|
|
624
|
+
numpy array of shape (n_segments, segment_size)
|
|
625
|
+
:param extended_y:
|
|
626
|
+
numpy array (n_pairs, 3)
|
|
627
|
+
where each row contains: (id_segment_epoch0, id_segment_epoch1, label=0/1)
|
|
628
|
+
:param threshold_max_distance:
|
|
629
|
+
the radios accepted threshold for possible segments
|
|
630
|
+
:param algorithm:
|
|
631
|
+
closest - select the closest segment, not used as the corresponding segment
|
|
632
|
+
random - select a random segment, from proximity (threshold_max_distance),
|
|
633
|
+
not used already as a corresponding segment
|
|
634
|
+
:param columns:
|
|
635
|
+
Column mapping used by each segment.
|
|
636
|
+
:param extended_y_file_name:
|
|
637
|
+
In case 'extended_y' is None, this file is used as input fallback.
|
|
638
|
+
:return:
|
|
639
|
+
extended_y, numpy array (n_pairs, 3)
|
|
640
|
+
where each row contains: (id_segment_epoch0, id_segment_epoch1, label=0/1)
|
|
641
|
+
"""
|
|
642
|
+
|
|
643
|
+
assert (
|
|
644
|
+
algorithm == "closest" or algorithm == "random"
|
|
645
|
+
), "'selection' parameter can be 'closest/random'"
|
|
646
|
+
|
|
647
|
+
if extended_y is None:
|
|
648
|
+
# Resolve the given path
|
|
649
|
+
filename = find_file(extended_y_file_name)
|
|
650
|
+
# Read it
|
|
651
|
+
try:
|
|
652
|
+
logger.info(
|
|
653
|
+
f"Reading tuples of (segment epoch0, segment epoch1, label) from file '{filename}'"
|
|
654
|
+
)
|
|
655
|
+
extended_y = np.genfromtxt(filename, delimiter=",")
|
|
656
|
+
except ValueError:
|
|
657
|
+
raise Py4DGeoError("Malformed file: " + str(filename))
|
|
658
|
+
|
|
659
|
+
# construct the corresponding pairs from the set of all pairs
|
|
660
|
+
extended_y_with_label_1 = extended_y[extended_y[:, 2] == 1]
|
|
661
|
+
|
|
662
|
+
new_extended_y = np.empty(shape=(0, 3), dtype=float)
|
|
663
|
+
|
|
664
|
+
# split points(segments) between epoch0 and epoch1
|
|
665
|
+
epoch0_mask = segments[:, columns.EPOCH_ID_COLUMN] == 0
|
|
666
|
+
epoch1_mask = segments[:, columns.EPOCH_ID_COLUMN] == 1
|
|
667
|
+
|
|
668
|
+
# compute index of each segment as part of 'segments'
|
|
669
|
+
epoch0_index = np.asarray(epoch0_mask).nonzero()[0]
|
|
670
|
+
epoch1_index = np.asarray(epoch1_mask).nonzero()[0]
|
|
671
|
+
|
|
672
|
+
X_Y_Z_Columns = [columns.X_COLUMN, columns.Y_COLUMN, columns.Z_COLUMN]
|
|
673
|
+
|
|
674
|
+
epoch0_set = segments[epoch0_mask][X_Y_Z_Columns]
|
|
675
|
+
epoch1_set = segments[epoch1_mask][X_Y_Z_Columns]
|
|
676
|
+
|
|
677
|
+
epoch0_set = epoch0_set.T
|
|
678
|
+
# generate kd-tree
|
|
679
|
+
epoch1 = Epoch(epoch1_set.T)
|
|
680
|
+
epoch1.build_kdtree()
|
|
681
|
+
|
|
682
|
+
# search for the near segments and build the 'extended y'
|
|
683
|
+
# for row in pairs_of_points:
|
|
684
|
+
for index, row in enumerate(epoch0_set):
|
|
685
|
+
index_seg_epoch0 = epoch0_index[index]
|
|
686
|
+
|
|
687
|
+
candidates_seg_epoch1 = epoch1.kdtree.radius_search(row, threshold_max_distance)
|
|
688
|
+
|
|
689
|
+
indexes_seg_epoch1 = epoch1_index[candidates_seg_epoch1]
|
|
690
|
+
|
|
691
|
+
if len(indexes_seg_epoch1) == 0:
|
|
692
|
+
continue
|
|
693
|
+
|
|
694
|
+
if algorithm == "closest":
|
|
695
|
+
index = (
|
|
696
|
+
(extended_y[:, 0] == index_seg_epoch0)
|
|
697
|
+
& (extended_y[:, 1] == indexes_seg_epoch1[0])
|
|
698
|
+
& (extended_y[:, 2] == 1)
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
if not index.any():
|
|
702
|
+
new_extended_y = np.vstack(
|
|
703
|
+
(new_extended_y, (index_seg_epoch0, indexes_seg_epoch1[0], 0))
|
|
704
|
+
)
|
|
705
|
+
else:
|
|
706
|
+
if len(candidates_seg_epoch1) > 1:
|
|
707
|
+
new_extended_y = np.vstack(
|
|
708
|
+
(new_extended_y, (index_seg_epoch0, indexes_seg_epoch1[1], 0))
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
if algorithm == "random":
|
|
712
|
+
while True:
|
|
713
|
+
rand_point = np.random.randint(low=0, high=len(indexes_seg_epoch1))
|
|
714
|
+
index = (
|
|
715
|
+
(extended_y[:, 0] == index_seg_epoch0)
|
|
716
|
+
& (extended_y[:, 1] == indexes_seg_epoch1[rand_point])
|
|
717
|
+
& (extended_y[:, 2] == 1)
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
if not index.any():
|
|
721
|
+
new_extended_y = np.vstack(
|
|
722
|
+
(
|
|
723
|
+
new_extended_y,
|
|
724
|
+
(index_seg_epoch0, indexes_seg_epoch1[rand_point], 0),
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
break
|
|
728
|
+
|
|
729
|
+
return np.vstack((extended_y, new_extended_y))
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
class BaseTransformer(TransformerMixin, BaseEstimator, ABC):
|
|
733
|
+
def __init__(self, skip=False, output_file_name=None, columns=None):
|
|
734
|
+
"""
|
|
735
|
+
:param skip:
|
|
736
|
+
Whether the current transform is applied or not.
|
|
737
|
+
:param output_file_name:
|
|
738
|
+
File where the result of the 'Transform()' method, a numpy array, is dumped.
|
|
739
|
+
"""
|
|
740
|
+
|
|
741
|
+
self.skip = skip
|
|
742
|
+
self.output_file_name = output_file_name
|
|
743
|
+
self.columns = columns
|
|
744
|
+
super(BaseTransformer, self).__init__()
|
|
745
|
+
|
|
746
|
+
@abstractmethod
|
|
747
|
+
def _fit(self, X, y=None):
|
|
748
|
+
"""
|
|
749
|
+
X : {array-like, sparse matrix}, shape (n_samples, n_features)
|
|
750
|
+
The training input samples.
|
|
751
|
+
y : None
|
|
752
|
+
There is no need of a target in a transformer, yet the pipeline API
|
|
753
|
+
requires this parameter.
|
|
754
|
+
Returns
|
|
755
|
+
-------
|
|
756
|
+
self : object
|
|
757
|
+
Returns self.
|
|
758
|
+
"""
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
@abstractmethod
|
|
762
|
+
def _transform(self, X):
|
|
763
|
+
"""
|
|
764
|
+
X : {array-like, sparse-matrix}, shape (n_samples, n_features)
|
|
765
|
+
The input samples.
|
|
766
|
+
Returns
|
|
767
|
+
-------
|
|
768
|
+
X_transformed : array, shape (n_samples, n_features)
|
|
769
|
+
"""
|
|
770
|
+
pass
|
|
771
|
+
|
|
772
|
+
def fit(self, X, y=None):
|
|
773
|
+
if self.skip:
|
|
774
|
+
return self
|
|
775
|
+
|
|
776
|
+
X = check_array(X, accept_sparse=True)
|
|
777
|
+
self.n_features_ = X.shape[1]
|
|
778
|
+
|
|
779
|
+
# Return the transformer
|
|
780
|
+
logger.info("Transformer Fit")
|
|
781
|
+
|
|
782
|
+
return self._fit(X, y)
|
|
783
|
+
|
|
784
|
+
def transform(self, X):
|
|
785
|
+
"""
|
|
786
|
+
param: X
|
|
787
|
+
numpy array
|
|
788
|
+
"""
|
|
789
|
+
|
|
790
|
+
if self.skip:
|
|
791
|
+
if self.output_file_name != None:
|
|
792
|
+
logger.debug(
|
|
793
|
+
f"The output file, {self.output_file_name} "
|
|
794
|
+
f"was set but the transformation process is skipped! (no output)"
|
|
795
|
+
)
|
|
796
|
+
return X
|
|
797
|
+
|
|
798
|
+
# Check if fit had been called
|
|
799
|
+
check_is_fitted(self, "n_features_")
|
|
800
|
+
|
|
801
|
+
# Input validation
|
|
802
|
+
X = check_array(X, accept_sparse=True)
|
|
803
|
+
|
|
804
|
+
# Check that the input is of the same shape as the one passed
|
|
805
|
+
# during fit.
|
|
806
|
+
if X.shape[1] != self.n_features_:
|
|
807
|
+
raise Py4DGeoError(
|
|
808
|
+
"Shape of input is different from what was seen in `fit`"
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
logger.info("Transformer Transform")
|
|
812
|
+
|
|
813
|
+
out = self._transform(X)
|
|
814
|
+
|
|
815
|
+
if self.output_file_name != None:
|
|
816
|
+
np.savetxt(self.output_file_name, out, delimiter=",")
|
|
817
|
+
logger.info(f"Saving Transform output in file: {self.output_file_name}")
|
|
818
|
+
|
|
819
|
+
return out
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
class PerPointComputation(BaseTransformer):
|
|
823
|
+
def __init__(
|
|
824
|
+
self, skip=False, radius=10, output_file_name=None, columns=LLSV_PCA_COLUMNS
|
|
825
|
+
):
|
|
826
|
+
"""
|
|
827
|
+
|
|
828
|
+
:param skip:
|
|
829
|
+
Whether the current transform is applied or not.
|
|
830
|
+
:param radius:
|
|
831
|
+
The radius used to extract the neighbour points using KD-tree.
|
|
832
|
+
:param output_file_name:
|
|
833
|
+
File where the result of the 'Transform()' method, a numpy array, is dumped.
|
|
834
|
+
"""
|
|
835
|
+
|
|
836
|
+
super(PerPointComputation, self).__init__(
|
|
837
|
+
skip=skip, output_file_name=output_file_name, columns=columns
|
|
838
|
+
)
|
|
839
|
+
self.radius = radius
|
|
840
|
+
|
|
841
|
+
def _llsv_and_pca(self, x, X):
|
|
842
|
+
"""
|
|
843
|
+
Compute PCA (implicitly, the normal vector as well) and lowest local surface variation
|
|
844
|
+
for point "x" using the set "X" as input.
|
|
845
|
+
|
|
846
|
+
:param x:
|
|
847
|
+
a reference to a row, part of the returned structure, of the following form:
|
|
848
|
+
[
|
|
849
|
+
x, y, z,
|
|
850
|
+
EpochID,
|
|
851
|
+
Eigenvalues( 3 columns ),
|
|
852
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
853
|
+
Lowest local surface variation ( 1 column )
|
|
854
|
+
]
|
|
855
|
+
:param X:
|
|
856
|
+
Subset of the point cloud of numpy array (m_samples, 3), that is found around 'x'(x,y,z) inside a 'radius'.
|
|
857
|
+
:return:
|
|
858
|
+
return a populated x with
|
|
859
|
+
(Eigenvalues, Eigenvectors, Lowest local surface variation)
|
|
860
|
+
"""
|
|
861
|
+
|
|
862
|
+
size = X.shape[0]
|
|
863
|
+
|
|
864
|
+
# compute mean
|
|
865
|
+
X_avg = np.mean(X, axis=0)
|
|
866
|
+
B = X - np.tile(X_avg, (size, 1))
|
|
867
|
+
|
|
868
|
+
# Find principal components (SVD)
|
|
869
|
+
U, S, VT = np.linalg.svd(B.T / np.sqrt(size), full_matrices=0)
|
|
870
|
+
|
|
871
|
+
x[-13:] = np.hstack(
|
|
872
|
+
(
|
|
873
|
+
S.reshape(1, -1),
|
|
874
|
+
U[:, 0].reshape(1, -1),
|
|
875
|
+
U[:, 1].reshape(1, -1),
|
|
876
|
+
U[:, 2].reshape(1, -1),
|
|
877
|
+
(S[-1] / np.sum(S)).reshape(1, -1),
|
|
878
|
+
)
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
return x
|
|
882
|
+
|
|
883
|
+
def _fit(self, X, y=None):
|
|
884
|
+
"""
|
|
885
|
+
|
|
886
|
+
:param X:
|
|
887
|
+
:param y:
|
|
888
|
+
:return:
|
|
889
|
+
"""
|
|
890
|
+
|
|
891
|
+
return self
|
|
892
|
+
|
|
893
|
+
def _transform(self, X):
|
|
894
|
+
"""
|
|
895
|
+
Extending X matrix by adding eigenvalues, eigenvectors, and lowest local surface variation columns.
|
|
896
|
+
|
|
897
|
+
:param X:
|
|
898
|
+
A numpy array with x, y, z, EpochID columns.
|
|
899
|
+
:return:
|
|
900
|
+
numpy matrix extended containing the following columns:
|
|
901
|
+
x, y, z ( 3 columns ),
|
|
902
|
+
EpochID ( 1 column ),
|
|
903
|
+
Eigenvalues( 3 columns ),
|
|
904
|
+
Eigenvectors( 3 columns ) X 3 [ in descending normal order ],
|
|
905
|
+
Lowest local surface variation( 1 column )
|
|
906
|
+
"""
|
|
907
|
+
|
|
908
|
+
X_Y_Z_Columns = [
|
|
909
|
+
self.columns.X_COLUMN,
|
|
910
|
+
self.columns.Y_COLUMN,
|
|
911
|
+
self.columns.Z_COLUMN,
|
|
912
|
+
]
|
|
913
|
+
|
|
914
|
+
mask_epoch0 = X[:, self.columns.EPOCH_ID_COLUMN] == 0
|
|
915
|
+
mask_epoch1 = X[:, self.columns.EPOCH_ID_COLUMN] == 1
|
|
916
|
+
|
|
917
|
+
epoch0_set = X[mask_epoch0, :-1]
|
|
918
|
+
epoch1_set = X[mask_epoch1, :-1]
|
|
919
|
+
|
|
920
|
+
# currently, Epoch class doesn't accept the empty point set, as a constructor parameter.
|
|
921
|
+
_epoch = [
|
|
922
|
+
Epoch(epoch_set)
|
|
923
|
+
for epoch_set in [epoch0_set, epoch1_set]
|
|
924
|
+
if epoch_set.shape[0] > 0
|
|
925
|
+
]
|
|
926
|
+
|
|
927
|
+
for current_epoch in _epoch:
|
|
928
|
+
current_epoch.build_kdtree()
|
|
929
|
+
|
|
930
|
+
# add extra columns
|
|
931
|
+
# Eigenvalues( 3 columns ) |
|
|
932
|
+
# Eigenvectors( 3 columns ) X 3 [ in descending normal order ] |
|
|
933
|
+
# Lowest local surface variation( 1 column )
|
|
934
|
+
# Total 13 new columns
|
|
935
|
+
new_columns = np.zeros((X.shape[0], 13))
|
|
936
|
+
X = np.hstack((X, new_columns))
|
|
937
|
+
|
|
938
|
+
# this process can be parallelized!
|
|
939
|
+
return np.apply_along_axis(
|
|
940
|
+
lambda x: self._llsv_and_pca(
|
|
941
|
+
x,
|
|
942
|
+
_epoch[int(x[self.columns.EPOCH_ID_COLUMN])].cloud[
|
|
943
|
+
_epoch[int(x[self.columns.EPOCH_ID_COLUMN])].kdtree.radius_search(
|
|
944
|
+
x[X_Y_Z_Columns], self.radius
|
|
945
|
+
)
|
|
946
|
+
],
|
|
947
|
+
),
|
|
948
|
+
1,
|
|
949
|
+
X,
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
class Segmentation(BaseTransformer):
|
|
954
|
+
def __init__(
|
|
955
|
+
self,
|
|
956
|
+
skip=False,
|
|
957
|
+
radius=2,
|
|
958
|
+
angle_diff_threshold=1,
|
|
959
|
+
distance_3D_threshold=1.5,
|
|
960
|
+
distance_orthogonal_threshold=1.5,
|
|
961
|
+
llsv_threshold=1,
|
|
962
|
+
roughness_threshold=5,
|
|
963
|
+
max_nr_points_neighborhood=100,
|
|
964
|
+
min_nr_points_per_segment=5,
|
|
965
|
+
with_previously_computed_segments=False,
|
|
966
|
+
output_file_name=None,
|
|
967
|
+
columns=SEGMENTED_POINT_CLOUD_COLUMNS,
|
|
968
|
+
):
|
|
969
|
+
"""
|
|
970
|
+
|
|
971
|
+
:param skip:
|
|
972
|
+
Whether the current transform is applied or not.
|
|
973
|
+
:param radius:
|
|
974
|
+
The radius used to extract the neighbour points using KD-tree during segmentation process.
|
|
975
|
+
:param angle_diff_threshold:
|
|
976
|
+
Angular deviation threshold for a point candidate’s local normal vector to the normal vector
|
|
977
|
+
of the initial seed point.
|
|
978
|
+
:param distance_3D_threshold:
|
|
979
|
+
Norm 2 distance threshold of the point candidate to the current set of points,
|
|
980
|
+
during the segmentation process.
|
|
981
|
+
:param distance_orthogonal_threshold:
|
|
982
|
+
Orthogonal distance threshold of the point candidate to the current plane segment
|
|
983
|
+
used during the segmentation process.
|
|
984
|
+
:param llsv_threshold:
|
|
985
|
+
The threshold on local surface variation.
|
|
986
|
+
:param roughness_threshold:
|
|
987
|
+
Threshold on local roughness.
|
|
988
|
+
:param max_nr_points_neighborhood:
|
|
989
|
+
The maximum number of points in the neighborhood of the point the candidate used
|
|
990
|
+
for checking during the segmentation process.
|
|
991
|
+
:param min_nr_points_per_segment:
|
|
992
|
+
The minimum number of points required to consider a segment as valid.
|
|
993
|
+
:param with_previously_computed_segments:
|
|
994
|
+
Used for differentiating between the first and the second segmentation.
|
|
995
|
+
( must be refactored!!! )
|
|
996
|
+
param output_file_name:
|
|
997
|
+
File where the result of the 'Transform()' method, a numpy array, is dumped.
|
|
998
|
+
"""
|
|
999
|
+
|
|
1000
|
+
super(Segmentation, self).__init__(
|
|
1001
|
+
skip=skip, output_file_name=output_file_name, columns=columns
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
self.radius = radius
|
|
1005
|
+
self.angle_diff_threshold = angle_diff_threshold
|
|
1006
|
+
self.distance_3D_threshold = distance_3D_threshold
|
|
1007
|
+
self.distance_orthogonal_threshold = distance_orthogonal_threshold
|
|
1008
|
+
self.llsv_threshold = llsv_threshold
|
|
1009
|
+
self.roughness_threshold = roughness_threshold
|
|
1010
|
+
self.max_nr_points_neighborhood = max_nr_points_neighborhood
|
|
1011
|
+
self.min_nr_points_per_segment = min_nr_points_per_segment
|
|
1012
|
+
self.with_previously_computed_segments = with_previously_computed_segments
|
|
1013
|
+
|
|
1014
|
+
def angle_difference_check(self, normal1, normal2):
|
|
1015
|
+
"""
|
|
1016
|
+
Check whether the angle between 2 normalized vectors is less than
|
|
1017
|
+
the used segmentation threshold "angle_diff_threshold" (in degrees)
|
|
1018
|
+
|
|
1019
|
+
:param normal1:
|
|
1020
|
+
normalized vector
|
|
1021
|
+
:param normal2:
|
|
1022
|
+
normalized vector
|
|
1023
|
+
:return:
|
|
1024
|
+
True/False
|
|
1025
|
+
"""
|
|
1026
|
+
|
|
1027
|
+
# normal1, normal2 have to be unit vectors (and that is the case as a result of the SVD process)
|
|
1028
|
+
return angle_difference_compute(normal1, normal2) <= self.angle_diff_threshold
|
|
1029
|
+
|
|
1030
|
+
def distance_3D_set_check(
|
|
1031
|
+
self, point, segment_id, X, X_Y_Z_Columns, SEGMENT_ID_COLUMN
|
|
1032
|
+
):
|
|
1033
|
+
"""
|
|
1034
|
+
Check whether the distance between the candidate point and all the points, currently part of the segment
|
|
1035
|
+
is less than the 'distance_3D_threshold'.
|
|
1036
|
+
|
|
1037
|
+
:param point:
|
|
1038
|
+
Numpy array (3, 1) candidate point during segmentation process.
|
|
1039
|
+
:param segment_id:
|
|
1040
|
+
The segment ID for which the candidate point is considered.
|
|
1041
|
+
:param X:
|
|
1042
|
+
numpy array (n_samples, 19)
|
|
1043
|
+
:param X_Y_Z_Columns:
|
|
1044
|
+
python list containing the indexes of the X,Y,Z columns.
|
|
1045
|
+
:param SEGMENT_ID_COLUMN:
|
|
1046
|
+
The column index used as the segment id.
|
|
1047
|
+
:return:
|
|
1048
|
+
True/False
|
|
1049
|
+
"""
|
|
1050
|
+
|
|
1051
|
+
point_mask = X[:, SEGMENT_ID_COLUMN] == segment_id
|
|
1052
|
+
|
|
1053
|
+
# can be optimized by changing the norm
|
|
1054
|
+
return (
|
|
1055
|
+
np.min(
|
|
1056
|
+
np.linalg.norm(X[point_mask, :3] - point.reshape(1, 3), ord=2, axis=1)
|
|
1057
|
+
)
|
|
1058
|
+
<= self.distance_3D_threshold
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
def compute_distance_orthogonal(self, candidate_point, plane_point, plane_normal):
|
|
1062
|
+
"""
|
|
1063
|
+
Compute the orthogonal distance between the candidate point and the segment represented by its plane.
|
|
1064
|
+
|
|
1065
|
+
:param candidate_point:
|
|
1066
|
+
numpy array (3, 1) candidate point during segmentation process.
|
|
1067
|
+
:param plane_point:
|
|
1068
|
+
numpy array (3, 1) representing a point (most likely, a core point), part of the segment.
|
|
1069
|
+
:param plane_normal:
|
|
1070
|
+
numpy array (3, 1)
|
|
1071
|
+
:return:
|
|
1072
|
+
True/False
|
|
1073
|
+
"""
|
|
1074
|
+
|
|
1075
|
+
d = -plane_point.dot(plane_normal)
|
|
1076
|
+
distance = (plane_normal.dot(candidate_point) + d) / np.linalg.norm(
|
|
1077
|
+
plane_normal
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
return distance
|
|
1081
|
+
|
|
1082
|
+
def distance_orthogonal_check(self, candidate_point, plane_point, plane_normal):
|
|
1083
|
+
"""
|
|
1084
|
+
Check whether the orthogonal distance between the candidate point and the segment represented by its plane
|
|
1085
|
+
is less than the 'distance_orthogonal_threshold'.
|
|
1086
|
+
|
|
1087
|
+
:param candidate_point:
|
|
1088
|
+
numpy array (3, 1) candidate point during segmentation process.
|
|
1089
|
+
:param plane_point:
|
|
1090
|
+
numpy array (3, 1) representing a point (most likely, a core point), part of the segment.
|
|
1091
|
+
:param plane_normal:
|
|
1092
|
+
numpy array (3, 1)
|
|
1093
|
+
:return:
|
|
1094
|
+
True/False
|
|
1095
|
+
"""
|
|
1096
|
+
|
|
1097
|
+
distance = self.compute_distance_orthogonal(
|
|
1098
|
+
candidate_point, plane_point, plane_normal
|
|
1099
|
+
)
|
|
1100
|
+
return distance - self.distance_orthogonal_threshold <= 0
|
|
1101
|
+
|
|
1102
|
+
def lowest_local_surface_variance_check(self, llsv):
|
|
1103
|
+
"""
|
|
1104
|
+
Check lowest local surface variance threshold.
|
|
1105
|
+
|
|
1106
|
+
:param llsv:
|
|
1107
|
+
lowest local surface variance
|
|
1108
|
+
:return:
|
|
1109
|
+
True/False
|
|
1110
|
+
"""
|
|
1111
|
+
|
|
1112
|
+
return llsv <= self.llsv_threshold
|
|
1113
|
+
|
|
1114
|
+
def _fit(self, X, y=None):
|
|
1115
|
+
"""
|
|
1116
|
+
|
|
1117
|
+
:param X:
|
|
1118
|
+
:param y:
|
|
1119
|
+
:return:
|
|
1120
|
+
"""
|
|
1121
|
+
|
|
1122
|
+
return self
|
|
1123
|
+
pass
|
|
1124
|
+
|
|
1125
|
+
def _transform(self, X):
|
|
1126
|
+
"""
|
|
1127
|
+
It applies the segmentation process.
|
|
1128
|
+
|
|
1129
|
+
:param X:
|
|
1130
|
+
Rows of points from Epoch0 and Epoch1
|
|
1131
|
+
X = (
|
|
1132
|
+
X0, Rows of points from Epoch 0
|
|
1133
|
+
X1 Rows of points from Epoch 1 <- OPTIONAL
|
|
1134
|
+
)
|
|
1135
|
+
It is assumed that rows for Epoch0 and Epoch1 are not interleaved!
|
|
1136
|
+
|
|
1137
|
+
numpy array (n_points, 17) with the following column structure:
|
|
1138
|
+
[
|
|
1139
|
+
x, y, z ( 3 column ),
|
|
1140
|
+
EpochID ( 1 column ),
|
|
1141
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
1142
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
1143
|
+
Lowest local surface variation ( 1 column )
|
|
1144
|
+
]
|
|
1145
|
+
:return:
|
|
1146
|
+
numpy array (n_points, 19) with the following column structure:
|
|
1147
|
+
[
|
|
1148
|
+
x, y, z ( 3 columns ),
|
|
1149
|
+
EpochID ( 1 column ),
|
|
1150
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
1151
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
1152
|
+
Lowest local surface variation ( 1 column ),
|
|
1153
|
+
Segment_ID ( 1 column ),
|
|
1154
|
+
Standard deviation ( 1 column )
|
|
1155
|
+
]
|
|
1156
|
+
"""
|
|
1157
|
+
|
|
1158
|
+
X_Y_Z_Columns = [
|
|
1159
|
+
self.columns.X_COLUMN,
|
|
1160
|
+
self.columns.Y_COLUMN,
|
|
1161
|
+
self.columns.Z_COLUMN,
|
|
1162
|
+
]
|
|
1163
|
+
|
|
1164
|
+
Normal_Columns = [
|
|
1165
|
+
self.columns.EIGENVECTOR_2_X_COLUMN,
|
|
1166
|
+
self.columns.EIGENVECTOR_2_Y_COLUMN,
|
|
1167
|
+
self.columns.EIGENVECTOR_2_Z_COLUMN,
|
|
1168
|
+
]
|
|
1169
|
+
|
|
1170
|
+
# the new columns are added only if they weren't already been added previously
|
|
1171
|
+
if not self.with_previously_computed_segments:
|
|
1172
|
+
new_column_segment_id = np.full(
|
|
1173
|
+
(X.shape[0], 1), DEFAULT_NO_SEGMENT, dtype=float
|
|
1174
|
+
)
|
|
1175
|
+
X = np.hstack((X, new_column_segment_id))
|
|
1176
|
+
|
|
1177
|
+
new_column_std_deviation = np.full(
|
|
1178
|
+
(X.shape[0], 1), DEFAULT_STD_DEVIATION_OF_NO_CORE_POINT, dtype=float
|
|
1179
|
+
)
|
|
1180
|
+
X = np.hstack((X, new_column_std_deviation))
|
|
1181
|
+
|
|
1182
|
+
mask_epoch0 = X[:, self.columns.EPOCH_ID_COLUMN] == 0
|
|
1183
|
+
mask_epoch1 = X[:, self.columns.EPOCH_ID_COLUMN] == 1
|
|
1184
|
+
|
|
1185
|
+
assert (
|
|
1186
|
+
mask_epoch0.shape[0] > 0
|
|
1187
|
+
), "The input X must contain at least elements from Epoch 0, e.g. EPOCH_ID_COLUMN==0"
|
|
1188
|
+
|
|
1189
|
+
logger.debug(
|
|
1190
|
+
f"'X' contains {np.count_nonzero(mask_epoch0)} elements from Epoch 0 "
|
|
1191
|
+
f"and {np.count_nonzero(mask_epoch1)} from Epoch 1"
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
epoch0_set = X[mask_epoch0][:, X_Y_Z_Columns]
|
|
1195
|
+
epoch1_set = X[mask_epoch1][:, X_Y_Z_Columns]
|
|
1196
|
+
|
|
1197
|
+
_epoch = [
|
|
1198
|
+
Epoch(epoch_set)
|
|
1199
|
+
for epoch_set in [epoch0_set, epoch1_set]
|
|
1200
|
+
if epoch_set.shape[0] > 0
|
|
1201
|
+
]
|
|
1202
|
+
|
|
1203
|
+
for current_epoch in _epoch:
|
|
1204
|
+
current_epoch.build_kdtree()
|
|
1205
|
+
|
|
1206
|
+
# sort by the "Lowest local surface variation"
|
|
1207
|
+
sort_indx_epoch0 = X[mask_epoch0, self.columns.LLSV_COLUMN].argsort()
|
|
1208
|
+
sort_indx_epoch1 = X[mask_epoch1, self.columns.LLSV_COLUMN].argsort()
|
|
1209
|
+
sort_indx_epoch = [sort_indx_epoch0, sort_indx_epoch1]
|
|
1210
|
+
|
|
1211
|
+
# it is assumed that the rows for Epoch0 and rows for Epoch1 are not interleaved!
|
|
1212
|
+
offset_in_X = [0, sort_indx_epoch0.shape[0]]
|
|
1213
|
+
|
|
1214
|
+
# initialization required between multiple Segmentations
|
|
1215
|
+
seg_id = np.max(X[:, self.columns.SEGMENT_ID_COLUMN])
|
|
1216
|
+
|
|
1217
|
+
for epoch_id in range(len(_epoch)):
|
|
1218
|
+
for indx_row in sort_indx_epoch[epoch_id] + offset_in_X[epoch_id]:
|
|
1219
|
+
# no part of a segment yet
|
|
1220
|
+
if X[indx_row, self.columns.SEGMENT_ID_COLUMN] < 0:
|
|
1221
|
+
seg_id += 1
|
|
1222
|
+
X[indx_row, self.columns.SEGMENT_ID_COLUMN] = seg_id
|
|
1223
|
+
|
|
1224
|
+
cumulative_distance_for_std_deviation = 0
|
|
1225
|
+
nr_points_for_std_deviation = 0
|
|
1226
|
+
|
|
1227
|
+
indx_kd_tree_list = _epoch[epoch_id].kdtree.radius_search(
|
|
1228
|
+
X[indx_row, X_Y_Z_Columns], self.radius
|
|
1229
|
+
)[: self.max_nr_points_neighborhood]
|
|
1230
|
+
for indx_kd_tree in indx_kd_tree_list:
|
|
1231
|
+
if (
|
|
1232
|
+
X[
|
|
1233
|
+
indx_kd_tree + offset_in_X[epoch_id],
|
|
1234
|
+
self.columns.SEGMENT_ID_COLUMN,
|
|
1235
|
+
]
|
|
1236
|
+
< 0
|
|
1237
|
+
and self.angle_difference_check(
|
|
1238
|
+
X[indx_row, Normal_Columns],
|
|
1239
|
+
X[indx_kd_tree + offset_in_X[epoch_id], Normal_Columns],
|
|
1240
|
+
)
|
|
1241
|
+
and self.distance_3D_set_check(
|
|
1242
|
+
X[indx_kd_tree + offset_in_X[epoch_id], X_Y_Z_Columns],
|
|
1243
|
+
seg_id,
|
|
1244
|
+
X,
|
|
1245
|
+
X_Y_Z_Columns,
|
|
1246
|
+
self.columns.SEGMENT_ID_COLUMN,
|
|
1247
|
+
)
|
|
1248
|
+
and self.distance_orthogonal_check(
|
|
1249
|
+
X[indx_kd_tree + offset_in_X[epoch_id], X_Y_Z_Columns],
|
|
1250
|
+
X[indx_row, X_Y_Z_Columns],
|
|
1251
|
+
X[indx_row, Normal_Columns],
|
|
1252
|
+
)
|
|
1253
|
+
and self.lowest_local_surface_variance_check(
|
|
1254
|
+
X[
|
|
1255
|
+
indx_kd_tree + offset_in_X[epoch_id],
|
|
1256
|
+
self.columns.LLSV_COLUMN,
|
|
1257
|
+
]
|
|
1258
|
+
)
|
|
1259
|
+
):
|
|
1260
|
+
X[
|
|
1261
|
+
indx_kd_tree + offset_in_X[epoch_id],
|
|
1262
|
+
self.columns.SEGMENT_ID_COLUMN,
|
|
1263
|
+
] = seg_id
|
|
1264
|
+
cumulative_distance_for_std_deviation += (
|
|
1265
|
+
self.compute_distance_orthogonal(
|
|
1266
|
+
X[
|
|
1267
|
+
indx_kd_tree + offset_in_X[epoch_id],
|
|
1268
|
+
X_Y_Z_Columns,
|
|
1269
|
+
],
|
|
1270
|
+
X[indx_row, X_Y_Z_Columns],
|
|
1271
|
+
X[indx_row, Normal_Columns],
|
|
1272
|
+
)
|
|
1273
|
+
** 2
|
|
1274
|
+
)
|
|
1275
|
+
nr_points_for_std_deviation += 1
|
|
1276
|
+
|
|
1277
|
+
nr_points_segment = np.count_nonzero(
|
|
1278
|
+
X[:, self.columns.SEGMENT_ID_COLUMN] == seg_id
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
# not enough points or 'roughness_threshold' exceeded
|
|
1282
|
+
if (
|
|
1283
|
+
nr_points_segment < self.min_nr_points_per_segment
|
|
1284
|
+
or cumulative_distance_for_std_deviation
|
|
1285
|
+
/ nr_points_for_std_deviation
|
|
1286
|
+
>= self.roughness_threshold
|
|
1287
|
+
):
|
|
1288
|
+
mask_seg_id = X[:, self.columns.SEGMENT_ID_COLUMN] == seg_id
|
|
1289
|
+
X[mask_seg_id, self.columns.SEGMENT_ID_COLUMN] = (
|
|
1290
|
+
DEFAULT_NO_SEGMENT
|
|
1291
|
+
)
|
|
1292
|
+
# since we don't have a new segment
|
|
1293
|
+
seg_id -= 1
|
|
1294
|
+
else:
|
|
1295
|
+
X[indx_row, self.columns.STANDARD_DEVIATION_COLUMN] = (
|
|
1296
|
+
cumulative_distance_for_std_deviation
|
|
1297
|
+
/ nr_points_for_std_deviation
|
|
1298
|
+
)
|
|
1299
|
+
return X
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
class PostPointCloudSegmentation(BaseTransformer):
|
|
1303
|
+
def __init__(
|
|
1304
|
+
self,
|
|
1305
|
+
skip=False,
|
|
1306
|
+
compute_normal=True,
|
|
1307
|
+
output_file_name=None,
|
|
1308
|
+
columns=SEGMENTED_POINT_CLOUD_COLUMNS,
|
|
1309
|
+
):
|
|
1310
|
+
"""
|
|
1311
|
+
:param skip:
|
|
1312
|
+
Whether the current transform is applied or not.
|
|
1313
|
+
:param compute_normal:
|
|
1314
|
+
|
|
1315
|
+
:param output_file_name:
|
|
1316
|
+
File where the result of the 'Transform()' method, a numpy array, is dumped.
|
|
1317
|
+
:param columns:
|
|
1318
|
+
|
|
1319
|
+
"""
|
|
1320
|
+
|
|
1321
|
+
super().__init__(skip=skip, output_file_name=output_file_name, columns=columns)
|
|
1322
|
+
self.compute_normal = compute_normal
|
|
1323
|
+
|
|
1324
|
+
def compute_distance_orthogonal(self, candidate_point, plane_point, plane_normal):
|
|
1325
|
+
"""
|
|
1326
|
+
Compute the orthogonal distance between the candidate point and the segment represented by its plane.
|
|
1327
|
+
|
|
1328
|
+
:param candidate_point:
|
|
1329
|
+
numpy array (3, 1) candidate point during segmentation process.
|
|
1330
|
+
:param plane_point:
|
|
1331
|
+
numpy array (3, 1) representing a point (most likely, a core point), part of the segment.
|
|
1332
|
+
:param plane_normal:
|
|
1333
|
+
numpy array (3, 1)
|
|
1334
|
+
:return:
|
|
1335
|
+
True/False
|
|
1336
|
+
"""
|
|
1337
|
+
|
|
1338
|
+
d = -plane_point.dot(plane_normal.T)
|
|
1339
|
+
distance = (plane_normal.dot(candidate_point) + d) / np.linalg.norm(
|
|
1340
|
+
plane_normal
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
return distance
|
|
1344
|
+
|
|
1345
|
+
def pca_compute_normal_and_mean(self, X):
|
|
1346
|
+
"""
|
|
1347
|
+
Perform PCA.
|
|
1348
|
+
The order of the eigenvalues and eigenvectors is consistent.
|
|
1349
|
+
|
|
1350
|
+
:param X:
|
|
1351
|
+
numpy array of shape (n_points, 3) with [x, y, z] columns.
|
|
1352
|
+
:return:
|
|
1353
|
+
a tuple of:
|
|
1354
|
+
Eig. values as numpy array of shape (1, 3)
|
|
1355
|
+
Eig. vector0 as numpy array of shape (1, 3)
|
|
1356
|
+
Eig. vector1 as numpy array of shape (1, 3)
|
|
1357
|
+
Eig. vector2 (normal vector) as numpy array of shape (1, 3)
|
|
1358
|
+
Position of the normal vector as numpy array of shape (1, 3),
|
|
1359
|
+
approximated as the mean of the input points.
|
|
1360
|
+
"""
|
|
1361
|
+
|
|
1362
|
+
size = X.shape[0]
|
|
1363
|
+
|
|
1364
|
+
# compute mean
|
|
1365
|
+
X_avg = np.mean(X, axis=0)
|
|
1366
|
+
B = X - np.tile(X_avg, (size, 1))
|
|
1367
|
+
|
|
1368
|
+
# Find principal components (SVD)
|
|
1369
|
+
U, S, VT = np.linalg.svd(B.T / np.sqrt(size), full_matrices=0)
|
|
1370
|
+
|
|
1371
|
+
assert S[0] != 0, "eig. value should not be 0!"
|
|
1372
|
+
assert S[1] != 0, "eig. value should not be 0!"
|
|
1373
|
+
assert S[2] != 0, "eig. value should not be 0!"
|
|
1374
|
+
|
|
1375
|
+
# Eig. values,
|
|
1376
|
+
# Eig. Vector0, Eig. Vector1, Eig. Vector2( the norma vector),
|
|
1377
|
+
# 'position' of the normal vector
|
|
1378
|
+
return (
|
|
1379
|
+
S.reshape(1, -1),
|
|
1380
|
+
U[:, 0].reshape(1, -1),
|
|
1381
|
+
U[:, 1].reshape(1, -1),
|
|
1382
|
+
U[:, 2].reshape(1, -1),
|
|
1383
|
+
X_avg.reshape(1, -1),
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
def _fit(self, X, y=None):
|
|
1387
|
+
"""
|
|
1388
|
+
:param X:
|
|
1389
|
+
:param y:
|
|
1390
|
+
:return:
|
|
1391
|
+
"""
|
|
1392
|
+
|
|
1393
|
+
return self
|
|
1394
|
+
pass
|
|
1395
|
+
|
|
1396
|
+
def _transform(self, X):
|
|
1397
|
+
"""
|
|
1398
|
+
|
|
1399
|
+
:param X:
|
|
1400
|
+
:return:
|
|
1401
|
+
"""
|
|
1402
|
+
|
|
1403
|
+
X_Y_Z_Columns = [
|
|
1404
|
+
self.columns.X_COLUMN,
|
|
1405
|
+
self.columns.Y_COLUMN,
|
|
1406
|
+
self.columns.Z_COLUMN,
|
|
1407
|
+
]
|
|
1408
|
+
|
|
1409
|
+
Eigval = [
|
|
1410
|
+
self.columns.EIGENVALUE0_COLUMN,
|
|
1411
|
+
self.columns.EIGENVALUE1_COLUMN,
|
|
1412
|
+
self.columns.EIGENVALUE2_COLUMN,
|
|
1413
|
+
]
|
|
1414
|
+
|
|
1415
|
+
Eigvec0 = [
|
|
1416
|
+
self.columns.EIGENVECTOR_0_X_COLUMN,
|
|
1417
|
+
self.columns.EIGENVECTOR_0_Y_COLUMN,
|
|
1418
|
+
self.columns.EIGENVECTOR_0_Z_COLUMN,
|
|
1419
|
+
]
|
|
1420
|
+
|
|
1421
|
+
Eigvec1 = [
|
|
1422
|
+
self.columns.EIGENVECTOR_1_X_COLUMN,
|
|
1423
|
+
self.columns.EIGENVECTOR_1_Y_COLUMN,
|
|
1424
|
+
self.columns.EIGENVECTOR_1_Z_COLUMN,
|
|
1425
|
+
]
|
|
1426
|
+
|
|
1427
|
+
Normal_Columns = [
|
|
1428
|
+
self.columns.EIGENVECTOR_2_X_COLUMN,
|
|
1429
|
+
self.columns.EIGENVECTOR_2_Y_COLUMN,
|
|
1430
|
+
self.columns.EIGENVECTOR_2_Z_COLUMN,
|
|
1431
|
+
]
|
|
1432
|
+
|
|
1433
|
+
highest_segment_id_used = int(X[:, self.columns.SEGMENT_ID_COLUMN].max())
|
|
1434
|
+
|
|
1435
|
+
for i in range(0, highest_segment_id_used + 1):
|
|
1436
|
+
mask = X[:, self.columns.SEGMENT_ID_COLUMN] == float(i)
|
|
1437
|
+
# extract all points, that are part of the same segment
|
|
1438
|
+
set_cloud = X[mask, :][:, X_Y_Z_Columns]
|
|
1439
|
+
|
|
1440
|
+
eig_values, e0, e1, normal, position = self.pca_compute_normal_and_mean(
|
|
1441
|
+
set_cloud
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
indexes = np.where(mask == True)[0]
|
|
1445
|
+
|
|
1446
|
+
# compute the closest point from the current segment to calculated "position"
|
|
1447
|
+
indx_min_in_indexes = np.linalg.norm(
|
|
1448
|
+
x=set_cloud - position, axis=1
|
|
1449
|
+
).argmin()
|
|
1450
|
+
indx_min_in_X = indexes[indx_min_in_indexes]
|
|
1451
|
+
|
|
1452
|
+
X[indx_min_in_X, Eigval] = eig_values
|
|
1453
|
+
X[indx_min_in_X, Eigvec0] = e0
|
|
1454
|
+
X[indx_min_in_X, Eigvec1] = e1
|
|
1455
|
+
|
|
1456
|
+
if self.compute_normal:
|
|
1457
|
+
X[indx_min_in_X, Normal_Columns] = normal
|
|
1458
|
+
|
|
1459
|
+
cumulative_distance_for_std_deviation = 0
|
|
1460
|
+
nr_points_for_std_deviation = indexes.shape[0] - 1
|
|
1461
|
+
for indx in indexes:
|
|
1462
|
+
cumulative_distance_for_std_deviation += (
|
|
1463
|
+
self.compute_distance_orthogonal(
|
|
1464
|
+
X[indx, X_Y_Z_Columns],
|
|
1465
|
+
X[indx_min_in_X, X_Y_Z_Columns],
|
|
1466
|
+
normal.reshape(1, -1),
|
|
1467
|
+
)
|
|
1468
|
+
** 2
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
X[indx_min_in_X, self.columns.STANDARD_DEVIATION_COLUMN] = (
|
|
1472
|
+
cumulative_distance_for_std_deviation / nr_points_for_std_deviation
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
return X
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
class ExtractSegments(BaseTransformer):
|
|
1479
|
+
def __init__(self, skip=False, output_file_name=None, columns=SEGMENT_COLUMNS):
|
|
1480
|
+
"""
|
|
1481
|
+
|
|
1482
|
+
:param skip:
|
|
1483
|
+
Whether the current transform is applied or not.
|
|
1484
|
+
:param output_file_name:
|
|
1485
|
+
File where the result of the 'Transform()' method, a numpy array, is dumped.
|
|
1486
|
+
"""
|
|
1487
|
+
|
|
1488
|
+
super(ExtractSegments, self).__init__(
|
|
1489
|
+
skip=skip, output_file_name=output_file_name, columns=columns
|
|
1490
|
+
)
|
|
1491
|
+
|
|
1492
|
+
def _fit(self, X, y=None):
|
|
1493
|
+
"""
|
|
1494
|
+
|
|
1495
|
+
:param X:
|
|
1496
|
+
:param y:
|
|
1497
|
+
:return:
|
|
1498
|
+
"""
|
|
1499
|
+
|
|
1500
|
+
return self
|
|
1501
|
+
pass
|
|
1502
|
+
|
|
1503
|
+
def _transform(self, X):
|
|
1504
|
+
"""
|
|
1505
|
+
Transform the numpy array of 'point cloud' to numpy array of 'segments' and extend the structure by adding
|
|
1506
|
+
a new column containing the 'number of points found in Segment_ID'. During this process, only one point
|
|
1507
|
+
that is part of a segment ( the core point ) is maintained, while all the others are discarded.
|
|
1508
|
+
|
|
1509
|
+
:param X:
|
|
1510
|
+
numpy array (n_points, 19) with the following column structure:
|
|
1511
|
+
[
|
|
1512
|
+
x, y, z ( 3 columns ), -> point from cloud
|
|
1513
|
+
EpochID ( 1 column ),
|
|
1514
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
1515
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
1516
|
+
Lowest local surface variation ( 1 column ),
|
|
1517
|
+
Segment_ID ( 1 column ),
|
|
1518
|
+
Standard deviation ( 1 column )
|
|
1519
|
+
]
|
|
1520
|
+
:return:
|
|
1521
|
+
numpy array (n_segments, 20) with the following column structure:
|
|
1522
|
+
[
|
|
1523
|
+
x, y, z ( 3 columns ), -> segment, core point
|
|
1524
|
+
EpochID ( 1 column ),
|
|
1525
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
1526
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
1527
|
+
Lowest local surface variation ( 1 column ),
|
|
1528
|
+
Segment_ID ( 1 column ),
|
|
1529
|
+
Standard deviation ( 1 column ),
|
|
1530
|
+
Number of points found in Segment_ID segment ( 1 column )
|
|
1531
|
+
]
|
|
1532
|
+
"""
|
|
1533
|
+
|
|
1534
|
+
max_segment_id = int(X[:, self.columns.SEGMENT_ID_COLUMN].max())
|
|
1535
|
+
X_Segments = np.empty(
|
|
1536
|
+
(int(max_segment_id) + 1, self.columns.NUMBER_OF_COLUMNS), dtype=float
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
for i in range(0, max_segment_id + 1):
|
|
1540
|
+
mask = X[:, self.columns.SEGMENT_ID_COLUMN] == float(i)
|
|
1541
|
+
set_cloud = X[mask, :] # all
|
|
1542
|
+
nr_points = set_cloud.shape[0]
|
|
1543
|
+
|
|
1544
|
+
# arg_min = set_cloud[:, LLSV_COLUMN].argmin()
|
|
1545
|
+
# X_Segments[i, :-1] = set_cloud[arg_min, :]
|
|
1546
|
+
|
|
1547
|
+
# find the CoM point, e.g. the point which has STD != DEFAULT_STD_DEVIATION_OF_NO_CORE_POINT
|
|
1548
|
+
mask_std = (
|
|
1549
|
+
set_cloud[:, self.columns.STANDARD_DEVIATION_COLUMN]
|
|
1550
|
+
!= DEFAULT_STD_DEVIATION_OF_NO_CORE_POINT
|
|
1551
|
+
)
|
|
1552
|
+
set_cloud_std = set_cloud[mask_std, :]
|
|
1553
|
+
assert (
|
|
1554
|
+
set_cloud_std.shape[0] == 1
|
|
1555
|
+
), "Only one element within a segment should have the standard deviation computed!"
|
|
1556
|
+
X_Segments[i, :-1] = set_cloud_std[0, :]
|
|
1557
|
+
|
|
1558
|
+
X_Segments[i, self.columns.NR_POINTS_PER_SEG_COLUMN] = nr_points
|
|
1559
|
+
|
|
1560
|
+
return X_Segments
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
class BuilderExtended_y(ABC):
|
|
1564
|
+
def __init__(self, columns):
|
|
1565
|
+
super().__init__()
|
|
1566
|
+
self.columns = columns
|
|
1567
|
+
|
|
1568
|
+
@abstractmethod
|
|
1569
|
+
def generate_extended_y(self, X):
|
|
1570
|
+
"""
|
|
1571
|
+
Generates tuples of ( segment index epoch 0, segment index epoch 1, 0/1 label )
|
|
1572
|
+
|
|
1573
|
+
:param X:
|
|
1574
|
+
numpy array of shape (n_segments, segment_features_size) containing all the segments for both,
|
|
1575
|
+
epoch 0 and epoch 1. Each row is a segment.
|
|
1576
|
+
:return:
|
|
1577
|
+
numpy array with shape (n_segments, 3)
|
|
1578
|
+
"""
|
|
1579
|
+
pass
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
class BuilderExtended_y_Visually(BuilderExtended_y):
|
|
1583
|
+
def __init__(self, columns=SEGMENT_COLUMNS):
|
|
1584
|
+
super(BuilderExtended_y_Visually, self).__init__(columns=columns)
|
|
1585
|
+
|
|
1586
|
+
self.current_pair = [None] * 2
|
|
1587
|
+
self.constructed_extended_y = np.empty(shape=(0, 3))
|
|
1588
|
+
|
|
1589
|
+
def toggle_transparenct(self, evt):
|
|
1590
|
+
if evt.keyPressed == "z":
|
|
1591
|
+
# transparency toggle
|
|
1592
|
+
for segment in self.sets:
|
|
1593
|
+
if segment.alpha() < 1.0:
|
|
1594
|
+
segment.alpha(1)
|
|
1595
|
+
else:
|
|
1596
|
+
segment.alpha(0.5)
|
|
1597
|
+
self.plt.render()
|
|
1598
|
+
|
|
1599
|
+
if evt.keyPressed == "g":
|
|
1600
|
+
# toggle red
|
|
1601
|
+
for segment in self.sets:
|
|
1602
|
+
if segment.epoch == 0:
|
|
1603
|
+
if segment.isOn == True:
|
|
1604
|
+
segment.off()
|
|
1605
|
+
else:
|
|
1606
|
+
segment.on()
|
|
1607
|
+
segment.isOn = not segment.isOn
|
|
1608
|
+
self.plt.render()
|
|
1609
|
+
|
|
1610
|
+
if evt.keyPressed == "d":
|
|
1611
|
+
# toggle green
|
|
1612
|
+
for segment in self.sets:
|
|
1613
|
+
if segment.epoch == 1:
|
|
1614
|
+
if segment.isOn == True:
|
|
1615
|
+
segment.off()
|
|
1616
|
+
else:
|
|
1617
|
+
segment.on()
|
|
1618
|
+
segment.isOn = not segment.isOn
|
|
1619
|
+
self.plt.render()
|
|
1620
|
+
|
|
1621
|
+
def controller(self, evt):
|
|
1622
|
+
"""
|
|
1623
|
+
|
|
1624
|
+
:param evt:
|
|
1625
|
+
:return:
|
|
1626
|
+
"""
|
|
1627
|
+
if not evt.actor:
|
|
1628
|
+
# no hit, return
|
|
1629
|
+
return
|
|
1630
|
+
logger.debug("point coords =%s", str(evt.picked3d), exc_info=1)
|
|
1631
|
+
if evt.isPoints:
|
|
1632
|
+
logger.debug("evt.actor = s", str(evt.actor))
|
|
1633
|
+
self.current_pair[int(evt.actor.epoch)] = evt.actor.id
|
|
1634
|
+
|
|
1635
|
+
if self.current_pair[0] != None and self.current_pair[1] != None:
|
|
1636
|
+
# we have a pair
|
|
1637
|
+
self.add_pair_button.status(self.add_pair_button.states[1])
|
|
1638
|
+
else:
|
|
1639
|
+
# we don't
|
|
1640
|
+
self.add_pair_button.status(self.add_pair_button.states[0])
|
|
1641
|
+
|
|
1642
|
+
def event_add_pair_button(self):
|
|
1643
|
+
"""
|
|
1644
|
+
|
|
1645
|
+
:return:
|
|
1646
|
+
"""
|
|
1647
|
+
|
|
1648
|
+
if self.current_pair[0] != None and self.current_pair[1] != None:
|
|
1649
|
+
try:
|
|
1650
|
+
self.constructed_extended_y = np.vstack(
|
|
1651
|
+
(
|
|
1652
|
+
self.constructed_extended_y,
|
|
1653
|
+
np.array(
|
|
1654
|
+
[
|
|
1655
|
+
self.current_pair[0],
|
|
1656
|
+
self.current_pair[1],
|
|
1657
|
+
int(self.label.status()),
|
|
1658
|
+
]
|
|
1659
|
+
),
|
|
1660
|
+
)
|
|
1661
|
+
)
|
|
1662
|
+
|
|
1663
|
+
self.current_pair[0] = None
|
|
1664
|
+
self.current_pair[1] = None
|
|
1665
|
+
self.label.status(self.label.states[0]) # firs state "None"
|
|
1666
|
+
|
|
1667
|
+
self.add_pair_button.switch()
|
|
1668
|
+
except:
|
|
1669
|
+
logger.error("You must select 0 or 1 as label")
|
|
1670
|
+
|
|
1671
|
+
def segments_visualizer(self, X):
|
|
1672
|
+
"""
|
|
1673
|
+
|
|
1674
|
+
:param X:
|
|
1675
|
+
:return:
|
|
1676
|
+
"""
|
|
1677
|
+
|
|
1678
|
+
self.sets = []
|
|
1679
|
+
|
|
1680
|
+
nr_segments = X.shape[0]
|
|
1681
|
+
colors = [(1, 0, 0), (0, 1, 0)]
|
|
1682
|
+
self.plt = Plotter(axes=3)
|
|
1683
|
+
|
|
1684
|
+
self.plt.add_callback("EndInteraction", self.controller)
|
|
1685
|
+
self.plt.add_callback("KeyPress", self.toggle_transparenct)
|
|
1686
|
+
|
|
1687
|
+
for i in range(0, nr_segments):
|
|
1688
|
+
# mask = X[:, 17] == float(i)
|
|
1689
|
+
# set_cloud = X[mask, :3] # x,y,z
|
|
1690
|
+
|
|
1691
|
+
if X[i, self.columns.EPOCH_ID_COLUMN] == 0:
|
|
1692
|
+
color = colors[0]
|
|
1693
|
+
else:
|
|
1694
|
+
color = colors[1]
|
|
1695
|
+
|
|
1696
|
+
# self.sets = self.sets + [Points(set_cloud, colors[i], alpha=1, r=10)]
|
|
1697
|
+
|
|
1698
|
+
# self.sets = self.sets + [ Point( pos=(X[i, 0],X[i, 1],X[i, 2]), r=15, c=colors[i], alpha=1 ) ]
|
|
1699
|
+
ellipsoid = Ellipsoid(
|
|
1700
|
+
pos=(X[i, 0], X[i, 1], X[i, 2]),
|
|
1701
|
+
axis1=[
|
|
1702
|
+
X[i, self.columns.EIGENVECTOR_0_X_COLUMN]
|
|
1703
|
+
* X[i, self.columns.EIGENVALUE0_COLUMN]
|
|
1704
|
+
* 0.5,
|
|
1705
|
+
X[i, self.columns.EIGENVECTOR_0_Y_COLUMN]
|
|
1706
|
+
* X[i, self.columns.EIGENVALUE0_COLUMN]
|
|
1707
|
+
* 0.5,
|
|
1708
|
+
X[i, self.columns.EIGENVECTOR_0_Z_COLUMN]
|
|
1709
|
+
* X[i, self.columns.EIGENVALUE0_COLUMN]
|
|
1710
|
+
* 0.3,
|
|
1711
|
+
],
|
|
1712
|
+
axis2=[
|
|
1713
|
+
X[i, self.columns.EIGENVECTOR_1_X_COLUMN]
|
|
1714
|
+
* X[i, self.columns.EIGENVALUE1_COLUMN]
|
|
1715
|
+
* 0.5,
|
|
1716
|
+
X[i, self.columns.EIGENVECTOR_1_Y_COLUMN]
|
|
1717
|
+
* X[i, self.columns.EIGENVALUE1_COLUMN]
|
|
1718
|
+
* 0.5,
|
|
1719
|
+
X[i, self.columns.EIGENVECTOR_1_Z_COLUMN]
|
|
1720
|
+
* X[i, self.columns.EIGENVALUE1_COLUMN]
|
|
1721
|
+
* 0.5,
|
|
1722
|
+
],
|
|
1723
|
+
axis3=[
|
|
1724
|
+
X[i, self.columns.EIGENVECTOR_2_X_COLUMN] * 0.1,
|
|
1725
|
+
X[i, self.columns.EIGENVECTOR_2_Y_COLUMN] * 0.1,
|
|
1726
|
+
X[i, self.columns.EIGENVECTOR_2_Z_COLUMN] * 0.1,
|
|
1727
|
+
],
|
|
1728
|
+
res=24,
|
|
1729
|
+
c=color,
|
|
1730
|
+
alpha=1,
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
ellipsoid.id = X[i, self.columns.SEGMENT_ID_COLUMN]
|
|
1734
|
+
ellipsoid.epoch = X[i, self.columns.EPOCH_ID_COLUMN]
|
|
1735
|
+
ellipsoid.isOn = True
|
|
1736
|
+
self.sets = self.sets + [ellipsoid]
|
|
1737
|
+
|
|
1738
|
+
self.label = self.plt.add_button(
|
|
1739
|
+
lambda: self.label.switch(),
|
|
1740
|
+
states=["Label (0/1)", "0", "1"], # None
|
|
1741
|
+
c=["w", "w", "w"],
|
|
1742
|
+
bc=["bb", "lr", "lg"],
|
|
1743
|
+
pos=(0.90, 0.25),
|
|
1744
|
+
size=24,
|
|
1745
|
+
)
|
|
1746
|
+
|
|
1747
|
+
self.add_pair_button = self.plt.add_button(
|
|
1748
|
+
self.event_add_pair_button,
|
|
1749
|
+
states=["Select pair", "Add pair"],
|
|
1750
|
+
c=["w", "w"],
|
|
1751
|
+
bc=["lg", "lr"],
|
|
1752
|
+
pos=(0.90, 0.15),
|
|
1753
|
+
size=24,
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
self.plt.show(
|
|
1757
|
+
self.sets,
|
|
1758
|
+
Text2D(
|
|
1759
|
+
"Select multiple pairs of red-green Ellipsoids with their corresponding labels (0/1) "
|
|
1760
|
+
"and then press 'Select pair'\n"
|
|
1761
|
+
"'z' - toggle transparency on/off 'g' - toggle on/off red ellipsoids 'd' toggle on/off red ellipsoids",
|
|
1762
|
+
pos="top-left",
|
|
1763
|
+
bg="k",
|
|
1764
|
+
s=0.7,
|
|
1765
|
+
),
|
|
1766
|
+
).close()
|
|
1767
|
+
return self.constructed_extended_y
|
|
1768
|
+
|
|
1769
|
+
def generate_extended_y(self, X):
|
|
1770
|
+
"""
|
|
1771
|
+
:param X:
|
|
1772
|
+
:return:
|
|
1773
|
+
"""
|
|
1774
|
+
|
|
1775
|
+
return self.segments_visualizer(X)
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
class ClassifierWrapper(ClassifierMixin, BaseEstimator):
|
|
1779
|
+
def __init__(
|
|
1780
|
+
self,
|
|
1781
|
+
neighborhood_search_radius=3,
|
|
1782
|
+
threshold_probability_most_similar=0.8,
|
|
1783
|
+
diff_between_most_similar_2=0.1,
|
|
1784
|
+
classifier=RandomForestClassifier(),
|
|
1785
|
+
columns=SEGMENT_COLUMNS,
|
|
1786
|
+
):
|
|
1787
|
+
"""
|
|
1788
|
+
:param neighborhood_search_radius:
|
|
1789
|
+
Maximum accepted Euclidean distance for any candidate segments.
|
|
1790
|
+
:param threshold_probability_most_similar:
|
|
1791
|
+
Lower bound probability threshold for the most similar ( candidate ) plane.
|
|
1792
|
+
:param diff_between_most_similar_2:
|
|
1793
|
+
Lower bound threshold of difference between first 2, most similar planes.
|
|
1794
|
+
:param classifier:
|
|
1795
|
+
The classifier used, default is RandomForestClassifier. ( sk-learn )
|
|
1796
|
+
:param columns:
|
|
1797
|
+
Column mapping used by seg_epoch0 and seg_epoch0
|
|
1798
|
+
"""
|
|
1799
|
+
|
|
1800
|
+
super().__init__()
|
|
1801
|
+
|
|
1802
|
+
self.neighborhood_search_radius = neighborhood_search_radius
|
|
1803
|
+
self.threshold_probability_most_similar = threshold_probability_most_similar
|
|
1804
|
+
self.diff_between_most_similar_2 = diff_between_most_similar_2
|
|
1805
|
+
self.classifier = classifier
|
|
1806
|
+
self.columns = columns
|
|
1807
|
+
|
|
1808
|
+
def compute_similarity_between(
|
|
1809
|
+
self, seg_epoch0: np.ndarray, seg_epoch1: np.ndarray
|
|
1810
|
+
) -> np.ndarray:
|
|
1811
|
+
"""
|
|
1812
|
+
Similarity function between 2 segments.
|
|
1813
|
+
|
|
1814
|
+
:param seg_epoch0:
|
|
1815
|
+
segment from epoch0, numpy array (1, 20) with the following column structure:
|
|
1816
|
+
[
|
|
1817
|
+
x, y, z ( 3 columns ), -> segment, core point
|
|
1818
|
+
EpochID ( 1 column ),
|
|
1819
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
1820
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
1821
|
+
Lowest local surface variation ( 1 column ),
|
|
1822
|
+
Segment_ID ( 1 column ),
|
|
1823
|
+
Standard deviation ( 1 column ),
|
|
1824
|
+
Number of points found in Segment_ID segment ( 1 column )
|
|
1825
|
+
]
|
|
1826
|
+
:param seg_epoch1:
|
|
1827
|
+
segment from epoch1, same structure as 'seg_epoch0'
|
|
1828
|
+
:return:
|
|
1829
|
+
numpy array of shape (6,) containing:
|
|
1830
|
+
angle, -> angle between plane normal vectors
|
|
1831
|
+
points_density_diff, -> difference between points density between pairs of segments
|
|
1832
|
+
eigen_value_smallest_diff, -> difference in the quality of plane fit (smallest eigenvalue)
|
|
1833
|
+
eigen_value_largest_diff, -> difference in plane extension (largest eigenvalue)
|
|
1834
|
+
eigen_value_middle_diff, -> difference in orthogonal plane extension (middle eigenvalue)
|
|
1835
|
+
nr_points_diff, -> difference in number of points per plane
|
|
1836
|
+
"""
|
|
1837
|
+
|
|
1838
|
+
Normal_Columns = [
|
|
1839
|
+
self.columns.EIGENVECTOR_2_X_COLUMN,
|
|
1840
|
+
self.columns.EIGENVECTOR_2_Y_COLUMN,
|
|
1841
|
+
self.columns.EIGENVECTOR_2_Z_COLUMN,
|
|
1842
|
+
]
|
|
1843
|
+
|
|
1844
|
+
angle = angle_difference_compute(
|
|
1845
|
+
seg_epoch0[Normal_Columns], seg_epoch1[Normal_Columns]
|
|
1846
|
+
)
|
|
1847
|
+
|
|
1848
|
+
points_density_seg_epoch0 = seg_epoch0[
|
|
1849
|
+
self.columns.NR_POINTS_PER_SEG_COLUMN
|
|
1850
|
+
] / (
|
|
1851
|
+
seg_epoch0[self.columns.EIGENVALUE0_COLUMN]
|
|
1852
|
+
* seg_epoch0[self.columns.EIGENVALUE1_COLUMN]
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
points_density_seg_epoch1 = seg_epoch1[
|
|
1856
|
+
self.columns.NR_POINTS_PER_SEG_COLUMN
|
|
1857
|
+
] / (
|
|
1858
|
+
seg_epoch1[self.columns.EIGENVALUE0_COLUMN]
|
|
1859
|
+
* seg_epoch1[self.columns.EIGENVALUE1_COLUMN]
|
|
1860
|
+
)
|
|
1861
|
+
|
|
1862
|
+
points_density_diff = abs(points_density_seg_epoch0 - points_density_seg_epoch1)
|
|
1863
|
+
|
|
1864
|
+
eigen_value_smallest_diff = abs(
|
|
1865
|
+
seg_epoch0[self.columns.EIGENVALUE2_COLUMN]
|
|
1866
|
+
- seg_epoch1[self.columns.EIGENVALUE2_COLUMN]
|
|
1867
|
+
)
|
|
1868
|
+
eigen_value_largest_diff = abs(
|
|
1869
|
+
seg_epoch0[self.columns.EIGENVALUE0_COLUMN]
|
|
1870
|
+
- seg_epoch1[self.columns.EIGENVALUE0_COLUMN]
|
|
1871
|
+
)
|
|
1872
|
+
eigen_value_middle_diff = abs(
|
|
1873
|
+
seg_epoch0[self.columns.EIGENVALUE1_COLUMN]
|
|
1874
|
+
- seg_epoch1[self.columns.EIGENVALUE1_COLUMN]
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
nr_points_diff = abs(
|
|
1878
|
+
seg_epoch0[self.columns.NR_POINTS_PER_SEG_COLUMN]
|
|
1879
|
+
- seg_epoch1[self.columns.NR_POINTS_PER_SEG_COLUMN]
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
return np.array(
|
|
1883
|
+
[
|
|
1884
|
+
angle,
|
|
1885
|
+
points_density_diff,
|
|
1886
|
+
eigen_value_smallest_diff,
|
|
1887
|
+
eigen_value_largest_diff,
|
|
1888
|
+
eigen_value_middle_diff,
|
|
1889
|
+
nr_points_diff,
|
|
1890
|
+
]
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
def _build_X_similarity(self, y_row, X):
|
|
1894
|
+
"""
|
|
1895
|
+
|
|
1896
|
+
:param y_row:
|
|
1897
|
+
numpy array of ( segment epoch0 id, segment epoch1 id, label(0/1) )
|
|
1898
|
+
:param X:
|
|
1899
|
+
numpy array of shape (n_segments, segment_features_size) containing all the segments for both,
|
|
1900
|
+
epoch 0 and epoch 1. Each row is a segment.
|
|
1901
|
+
:return:
|
|
1902
|
+
numpy array containing the similarity value between 2 segments.
|
|
1903
|
+
"""
|
|
1904
|
+
|
|
1905
|
+
seg_epoch0 = X[int(y_row[0]), :]
|
|
1906
|
+
seg_epoch1 = X[int(y_row[1]), :]
|
|
1907
|
+
|
|
1908
|
+
return self.compute_similarity_between(seg_epoch0, seg_epoch1)
|
|
1909
|
+
|
|
1910
|
+
def fit(self, X, y):
|
|
1911
|
+
"""
|
|
1912
|
+
This method takes care of the learning process by training the chosen 'classifier', using labeled data.
|
|
1913
|
+
|
|
1914
|
+
:param X:
|
|
1915
|
+
numpy array (n_segments, segment_size)
|
|
1916
|
+
:param y:
|
|
1917
|
+
numpy array of shape (m_extended_y, 3) where 'extended y' has the following structure:
|
|
1918
|
+
( tuples of index segment from epoch0, index segment from epoch1, label(0/1) )
|
|
1919
|
+
"""
|
|
1920
|
+
|
|
1921
|
+
X_similarity = np.apply_along_axis(
|
|
1922
|
+
lambda y_row: self._build_X_similarity(y_row, X), 1, y
|
|
1923
|
+
)
|
|
1924
|
+
|
|
1925
|
+
# Check that X and y have correct shape
|
|
1926
|
+
# X_similarity, y = check_X_y(X_similarity, y, multi_output=True)
|
|
1927
|
+
|
|
1928
|
+
# Store the classes seen during fit
|
|
1929
|
+
# self.classes_ = unique_labels(y[:, 2])
|
|
1930
|
+
|
|
1931
|
+
# self.X_ = X_similarity
|
|
1932
|
+
# self.y_ = y[:, 2]
|
|
1933
|
+
|
|
1934
|
+
logger.info(f"Fit ClassifierWrapper")
|
|
1935
|
+
|
|
1936
|
+
# Return the classifier
|
|
1937
|
+
return self.classifier.fit(X_similarity, y[:, 2])
|
|
1938
|
+
|
|
1939
|
+
def predict(self, X):
|
|
1940
|
+
"""
|
|
1941
|
+
For a set of segments from epoch 0 and epoch 1 it computes which one corresponds.
|
|
1942
|
+
|
|
1943
|
+
:param X:
|
|
1944
|
+
numpy array (n_segments, 20) with the following column structure:
|
|
1945
|
+
[
|
|
1946
|
+
x, y, z ( 3 columns ), -> segment, core point
|
|
1947
|
+
EpochID ( 1 column ),
|
|
1948
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
1949
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
1950
|
+
Lowest local surface variation ( 1 column ),
|
|
1951
|
+
Segment_ID ( 1 column ),
|
|
1952
|
+
Standard deviation ( 1 column ),
|
|
1953
|
+
Number of points found in Segment_ID segment ( 1 column )
|
|
1954
|
+
]
|
|
1955
|
+
|
|
1956
|
+
:return:
|
|
1957
|
+
numpy array where each row contains a pair of segments:
|
|
1958
|
+
[segment epoch 0, segment epoch 1]
|
|
1959
|
+
"""
|
|
1960
|
+
|
|
1961
|
+
# Check if fit had been called
|
|
1962
|
+
# check_is_fitted(self, ["X_", "y_"])
|
|
1963
|
+
|
|
1964
|
+
# Input validation
|
|
1965
|
+
# X = check_array(X)
|
|
1966
|
+
|
|
1967
|
+
mask_epoch0 = X[:, self.columns.EPOCH_ID_COLUMN] == 0
|
|
1968
|
+
mask_epoch1 = X[:, self.columns.EPOCH_ID_COLUMN] == 1
|
|
1969
|
+
|
|
1970
|
+
epoch0_set = X[mask_epoch0, :] # all
|
|
1971
|
+
epoch1_set = X[mask_epoch1, :] # all
|
|
1972
|
+
|
|
1973
|
+
self.epoch1_segments = Epoch(
|
|
1974
|
+
epoch1_set[
|
|
1975
|
+
:, [self.columns.X_COLUMN, self.columns.Y_COLUMN, self.columns.Z_COLUMN]
|
|
1976
|
+
]
|
|
1977
|
+
)
|
|
1978
|
+
self.epoch1_segments.build_kdtree()
|
|
1979
|
+
|
|
1980
|
+
list_segments_pair = np.empty((0, epoch0_set.shape[1] + epoch1_set.shape[1]))
|
|
1981
|
+
|
|
1982
|
+
# this operation can be parallelized
|
|
1983
|
+
for epoch0_set_row in epoch0_set:
|
|
1984
|
+
list_candidates = self.epoch1_segments.kdtree.radius_search(
|
|
1985
|
+
epoch0_set_row, self.neighborhood_search_radius
|
|
1986
|
+
)
|
|
1987
|
+
|
|
1988
|
+
list_classified = np.array(
|
|
1989
|
+
[
|
|
1990
|
+
self.classifier.predict_proba(
|
|
1991
|
+
self.compute_similarity_between(
|
|
1992
|
+
epoch0_set_row, epoch1_set[candidate, :]
|
|
1993
|
+
).reshape(1, -1)
|
|
1994
|
+
)[0][1]
|
|
1995
|
+
for candidate in list_candidates
|
|
1996
|
+
]
|
|
1997
|
+
)
|
|
1998
|
+
|
|
1999
|
+
if len(list_classified) < 2:
|
|
2000
|
+
continue
|
|
2001
|
+
|
|
2002
|
+
most_similar = list_classified.argsort()[-2:]
|
|
2003
|
+
|
|
2004
|
+
if (
|
|
2005
|
+
most_similar[1] >= self.threshold_probability_most_similar
|
|
2006
|
+
and abs(most_similar[1] - most_similar[0])
|
|
2007
|
+
>= self.diff_between_most_similar_2
|
|
2008
|
+
):
|
|
2009
|
+
list_segments_pair = np.vstack(
|
|
2010
|
+
(
|
|
2011
|
+
list_segments_pair,
|
|
2012
|
+
np.hstack(
|
|
2013
|
+
(epoch0_set_row, epoch1_set[most_similar[-1], :])
|
|
2014
|
+
).reshape(1, -1),
|
|
2015
|
+
)
|
|
2016
|
+
)
|
|
2017
|
+
|
|
2018
|
+
return list_segments_pair
|
|
2019
|
+
|
|
2020
|
+
|
|
2021
|
+
class PBM3C2:
|
|
2022
|
+
def __init__(
|
|
2023
|
+
self,
|
|
2024
|
+
per_point_computation=PerPointComputation(),
|
|
2025
|
+
segmentation=Segmentation(),
|
|
2026
|
+
second_segmentation=Segmentation(),
|
|
2027
|
+
extract_segments=ExtractSegments(),
|
|
2028
|
+
classifier=ClassifierWrapper(),
|
|
2029
|
+
):
|
|
2030
|
+
|
|
2031
|
+
logger.warning(
|
|
2032
|
+
f"This method is in experimental stage and undergoing active development."
|
|
2033
|
+
)
|
|
2034
|
+
|
|
2035
|
+
"""
|
|
2036
|
+
:param per_point_computation:
|
|
2037
|
+
lowest local surface variation and PCA computation. (computes the normal vector as well)
|
|
2038
|
+
:param segmentation:
|
|
2039
|
+
The object used for the first segmentation.
|
|
2040
|
+
:param second_segmentation:
|
|
2041
|
+
The object used for the second segmentation.
|
|
2042
|
+
:param extract_segments:
|
|
2043
|
+
The object used for building the segments.
|
|
2044
|
+
:param classifier:
|
|
2045
|
+
An instance of ClassifierWrapper class. The default wrapped classifier used is sk-learn RandomForest.
|
|
2046
|
+
"""
|
|
2047
|
+
|
|
2048
|
+
self._per_point_computation = per_point_computation
|
|
2049
|
+
self._segmentation = segmentation
|
|
2050
|
+
self._second_segmentation = second_segmentation
|
|
2051
|
+
self._extract_segments = extract_segments
|
|
2052
|
+
self._classifier = classifier
|
|
2053
|
+
|
|
2054
|
+
self._second_segmentation.set_params(with_previously_computed_segments=True)
|
|
2055
|
+
|
|
2056
|
+
def _reconstruct_input_with_normals(self, epoch, epoch_id, columns):
|
|
2057
|
+
"""
|
|
2058
|
+
It is an adapter from [x, y, z, N_x, N_y, N_z, Segment_ID] column structure of input 'epoch'
|
|
2059
|
+
to an output equivalent with the following pipeline computation:
|
|
2060
|
+
("Transform LLSV_and_PCA"), ("Transform Segmentation"), ("Transform Second Segmentation")
|
|
2061
|
+
|
|
2062
|
+
Note: When comparing distance results between this notebook and the base algorithm notebook, you might notice,
|
|
2063
|
+
that results do not necessarily agree even if the given segmentation information is exactly
|
|
2064
|
+
the same as the one computed in the base algorithm.
|
|
2065
|
+
This is due to the reconstruction process in this algorithm being forced to select the segment position
|
|
2066
|
+
(exported as the core point) from the segment points instead of reconstructing the correct position
|
|
2067
|
+
from the base algorithm.
|
|
2068
|
+
|
|
2069
|
+
:param epoch:
|
|
2070
|
+
Epoch object where each row has the following format: [x, y, z, N_x, N_y, N_z, Segment_ID]
|
|
2071
|
+
:param epoch_id:
|
|
2072
|
+
is 0 or 1 and represents one of the epochs used as part of distance computation.
|
|
2073
|
+
:param columns:
|
|
2074
|
+
|
|
2075
|
+
:return:
|
|
2076
|
+
numpy array of shape (n_points, 19) with the following column structure:
|
|
2077
|
+
[
|
|
2078
|
+
x,y,z, -> Center of the Mass
|
|
2079
|
+
EPOCH_ID_COLUMN, ->0/1
|
|
2080
|
+
Eigenvalue 1, Eigenvalue 2, Eigenvalue 3,
|
|
2081
|
+
Eigenvector0 (x,y,z),
|
|
2082
|
+
Eigenvector1 (x,y,z),
|
|
2083
|
+
Eigenvector2 (x,y,z), -> Normal vector
|
|
2084
|
+
LLSV_COLUMN, -> lowest local surface variation
|
|
2085
|
+
SEGMENT_ID_COLUMN,
|
|
2086
|
+
STANDARD_DEVIATION_COLUMN
|
|
2087
|
+
]
|
|
2088
|
+
"""
|
|
2089
|
+
|
|
2090
|
+
# x, y, z, N_x, N_y, N_z, Segment_ID
|
|
2091
|
+
assert epoch.shape[1] == 3 + 3 + 1, "epoch size mismatch!"
|
|
2092
|
+
|
|
2093
|
+
return np.hstack(
|
|
2094
|
+
(
|
|
2095
|
+
epoch[:, :3], # x,y,z X 3
|
|
2096
|
+
np.full(
|
|
2097
|
+
(epoch.shape[0], 1), epoch_id, dtype=float
|
|
2098
|
+
), # EPOCH_ID_COLUMN X 1
|
|
2099
|
+
np.full((epoch.shape[0], 3), 0, dtype=float), # Eigenvalue X 3
|
|
2100
|
+
np.full(
|
|
2101
|
+
(epoch.shape[0], 6), 0, dtype=float
|
|
2102
|
+
), # Eigenvector0, Eigenvector1 X 6
|
|
2103
|
+
epoch[:, 3:6], # Eigenvector2 X 3
|
|
2104
|
+
np.full((epoch.shape[0], 1), 0, dtype=float).reshape(
|
|
2105
|
+
-1, 1
|
|
2106
|
+
), # LLSV_COLUMN
|
|
2107
|
+
epoch[:, -1].reshape(-1, 1), # SEGMENT_ID_COLUMN
|
|
2108
|
+
np.full(
|
|
2109
|
+
(epoch.shape[0], 1),
|
|
2110
|
+
DEFAULT_STD_DEVIATION_OF_NO_CORE_POINT,
|
|
2111
|
+
dtype=float,
|
|
2112
|
+
).reshape(
|
|
2113
|
+
-1, 1
|
|
2114
|
+
), # STANDARD_DEVIATION_COLUMN
|
|
2115
|
+
)
|
|
2116
|
+
)
|
|
2117
|
+
|
|
2118
|
+
def _reconstruct_input_without_normals(self, epoch, epoch_id, columns):
|
|
2119
|
+
"""
|
|
2120
|
+
It is an adapter from [x, y, z, Segment_ID] column structure of input 'epoch'
|
|
2121
|
+
to an output equivalent with the following pipeline computation:
|
|
2122
|
+
("Transform LLSV_and_PCA"), ("Transform Segmentation"), ("Transform Second Segmentation")
|
|
2123
|
+
|
|
2124
|
+
Note: When comparing distance results between this notebook and the base algorithm notebook, you might notice,
|
|
2125
|
+
that results do not necessarily agree even if the given segmentation information is exactly
|
|
2126
|
+
the same as the one computed in the base algorithm.
|
|
2127
|
+
This is due to the reconstruction process in this algorithm being forced to select the segment position
|
|
2128
|
+
(exported as the core point) from the segment points instead of reconstructing the correct position
|
|
2129
|
+
from the base algorithm.
|
|
2130
|
+
|
|
2131
|
+
:param epoch:
|
|
2132
|
+
Epoch object where each row contains by: [x, y, z, Segment_ID]
|
|
2133
|
+
:param epoch_id:
|
|
2134
|
+
is 0 or 1 and represents one of the epochs used as part of distance computation.
|
|
2135
|
+
:param columns
|
|
2136
|
+
|
|
2137
|
+
:return:
|
|
2138
|
+
numpy array of shape (n_points, 19) with the following column structure:
|
|
2139
|
+
[
|
|
2140
|
+
x,y,z, -> Center of the Mass
|
|
2141
|
+
Epoch_ID, ->0/1
|
|
2142
|
+
Eigenvalue 1, Eigenvalue 2, Eigenvalue 3,
|
|
2143
|
+
Eigenvector0 (x,y,z),
|
|
2144
|
+
Eigenvector1 (x,y,z),
|
|
2145
|
+
Eigenvector2 (x,y,z), -> Normal vector
|
|
2146
|
+
LLSV, -> lowest local surface variation
|
|
2147
|
+
Segment_ID,
|
|
2148
|
+
Standard deviation
|
|
2149
|
+
]
|
|
2150
|
+
"""
|
|
2151
|
+
|
|
2152
|
+
# [x, y, z, Segment_ID] or [x, y, z, N_x, N_y, N_z, Segment_ID]
|
|
2153
|
+
assert (
|
|
2154
|
+
epoch.shape[1] == 3 + 1 or epoch.shape[1] == 3 + 3 + 1
|
|
2155
|
+
), "epoch size mismatch!"
|
|
2156
|
+
|
|
2157
|
+
return np.hstack(
|
|
2158
|
+
(
|
|
2159
|
+
epoch[:, :3], # x,y,z X 3
|
|
2160
|
+
np.full(
|
|
2161
|
+
(epoch.shape[0], 1), epoch_id, dtype=float
|
|
2162
|
+
), # EPOCH_ID_COLUMN X 1
|
|
2163
|
+
np.full((epoch.shape[0], 3), 0, dtype=float), # Eigenvalue X 3
|
|
2164
|
+
np.full(
|
|
2165
|
+
(epoch.shape[0], 6), 0, dtype=float
|
|
2166
|
+
), # Eigenvector0, Eigenvector1 X 6
|
|
2167
|
+
np.full((epoch.shape[0], 3), 0, dtype=float), # Eigenvector2 X 3
|
|
2168
|
+
np.full((epoch.shape[0], 1), 0, dtype=float).reshape(
|
|
2169
|
+
-1, 1
|
|
2170
|
+
), # LLSV_COLUMN
|
|
2171
|
+
epoch[:, -1].reshape(-1, 1), # SEGMENT_ID_COLUMN
|
|
2172
|
+
np.full(
|
|
2173
|
+
(epoch.shape[0], 1),
|
|
2174
|
+
DEFAULT_STD_DEVIATION_OF_NO_CORE_POINT,
|
|
2175
|
+
dtype=float,
|
|
2176
|
+
).reshape(
|
|
2177
|
+
-1, 1
|
|
2178
|
+
), # STANDARD_DEVIATION_COLUMN
|
|
2179
|
+
)
|
|
2180
|
+
)
|
|
2181
|
+
|
|
2182
|
+
@staticmethod
|
|
2183
|
+
def _print_default_parameters(kwargs, pipeline_param_dict):
|
|
2184
|
+
"""
|
|
2185
|
+
:param kwargs:
|
|
2186
|
+
:param pipeline_param_dict
|
|
2187
|
+
"""
|
|
2188
|
+
|
|
2189
|
+
# print the default parameters
|
|
2190
|
+
if ("get_pipeline_options", True) in kwargs.items():
|
|
2191
|
+
logger.info(
|
|
2192
|
+
f"----\n "
|
|
2193
|
+
f"The default parameters are:\n "
|
|
2194
|
+
f"{pp.pformat(pipeline_param_dict)} \n"
|
|
2195
|
+
f"----\n"
|
|
2196
|
+
)
|
|
2197
|
+
del kwargs["get_pipeline_options"]
|
|
2198
|
+
|
|
2199
|
+
@staticmethod
|
|
2200
|
+
def _overwrite_pipeline_parameters(
|
|
2201
|
+
kwargs, pipeline, message="The pipeline parameters after overwriting are:"
|
|
2202
|
+
):
|
|
2203
|
+
"""
|
|
2204
|
+
:param kwargs:
|
|
2205
|
+
:param pipeline:
|
|
2206
|
+
|
|
2207
|
+
:return: unused_kwargs
|
|
2208
|
+
The unused parameters / not found as part of 'pipeline'
|
|
2209
|
+
"""
|
|
2210
|
+
|
|
2211
|
+
unused_kwargs = {}
|
|
2212
|
+
|
|
2213
|
+
# if we have parameters
|
|
2214
|
+
if len(kwargs.items()) > 0:
|
|
2215
|
+
pipeline_params = pipeline.get_params()
|
|
2216
|
+
# overwrite the default parameters
|
|
2217
|
+
for key, value in kwargs.items():
|
|
2218
|
+
if key in pipeline_params.keys():
|
|
2219
|
+
pipeline.set_params(**{key: value})
|
|
2220
|
+
logger.debug(f"The pipeline parameter '{key}' is now '{value}'")
|
|
2221
|
+
else:
|
|
2222
|
+
unused_kwargs[key] = value
|
|
2223
|
+
logger.info(
|
|
2224
|
+
f"----\n "
|
|
2225
|
+
f"{message} \n"
|
|
2226
|
+
f"{pp.pformat(pipeline.get_params())} \n"
|
|
2227
|
+
f"----\n"
|
|
2228
|
+
)
|
|
2229
|
+
else:
|
|
2230
|
+
logger.info("No pipeline parameter is overwritten")
|
|
2231
|
+
|
|
2232
|
+
return unused_kwargs
|
|
2233
|
+
|
|
2234
|
+
def generate_extended_labels_interactively(
|
|
2235
|
+
self,
|
|
2236
|
+
epoch0: typing.Union[Epoch, None] = None,
|
|
2237
|
+
epoch1: typing.Union[Epoch, None] = None,
|
|
2238
|
+
builder_extended_y: BuilderExtended_y_Visually = BuilderExtended_y_Visually(),
|
|
2239
|
+
**kwargs,
|
|
2240
|
+
) -> typing.Union[typing.Tuple[np.ndarray, np.ndarray], None]:
|
|
2241
|
+
"""
|
|
2242
|
+
Given 2 Epochs, it builds a pair of (segments and 'extended y').
|
|
2243
|
+
|
|
2244
|
+
:param epoch0:
|
|
2245
|
+
Epoch object.
|
|
2246
|
+
:param epoch1:
|
|
2247
|
+
Epoch object.
|
|
2248
|
+
:param builder_extended_y:
|
|
2249
|
+
The object is used for generating 'extended y', visually.
|
|
2250
|
+
:param kwargs:
|
|
2251
|
+
|
|
2252
|
+
Used for customize the default pipeline parameters.
|
|
2253
|
+
|
|
2254
|
+
Getting the default parameters:
|
|
2255
|
+
e.g. "get_pipeline_options"
|
|
2256
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
2257
|
+
|
|
2258
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
2259
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
2260
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
2261
|
+
|
|
2262
|
+
this process is stateless
|
|
2263
|
+
|
|
2264
|
+
:return:
|
|
2265
|
+
tuple [Segments, 'extended y'] | None
|
|
2266
|
+
|
|
2267
|
+
where:
|
|
2268
|
+
|
|
2269
|
+
'Segments' has the following column structure:
|
|
2270
|
+
X_COLUMN, Y_COLUMN, Z_COLUMN, -> Center of Gravity
|
|
2271
|
+
EPOCH_ID_COLUMN, -> 0/1
|
|
2272
|
+
EIGENVALUE0_COLUMN, EIGENVALUE1_COLUMN, EIGENVALUE2_COLUMN,
|
|
2273
|
+
EIGENVECTOR_0_X_COLUMN, EIGENVECTOR_0_Y_COLUMN, EIGENVECTOR_0_Z_COLUMN,
|
|
2274
|
+
EIGENVECTOR_1_X_COLUMN, EIGENVECTOR_1_Y_COLUMN, EIGENVECTOR_1_Z_COLUMN,
|
|
2275
|
+
EIGENVECTOR_2_X_COLUMN, EIGENVECTOR_2_Y_COLUMN, EIGENVECTOR_2_Z_COLUMN, -> Normal vector
|
|
2276
|
+
LLSV_COLUMN, -> lowest local surface variation
|
|
2277
|
+
SEGMENT_ID_COLUMN,
|
|
2278
|
+
STANDARD_DEVIATION_COLUMN,
|
|
2279
|
+
NR_POINTS_PER_SEG_COLUMN,
|
|
2280
|
+
|
|
2281
|
+
'extended y' has the following structure: (tuples of index segment from epoch0, index segment from epoch1,
|
|
2282
|
+
label(0/1)) used for learning.
|
|
2283
|
+
"""
|
|
2284
|
+
|
|
2285
|
+
if not interactive_available:
|
|
2286
|
+
logger.error("Interactive session not available in this environment.")
|
|
2287
|
+
return
|
|
2288
|
+
|
|
2289
|
+
labeling_pipeline = Pipeline(
|
|
2290
|
+
[
|
|
2291
|
+
("Transform_PerPointComputation", self._per_point_computation),
|
|
2292
|
+
("Transform_Segmentation", self._segmentation),
|
|
2293
|
+
("Transform_Second_Segmentation", self._second_segmentation),
|
|
2294
|
+
("Transform_ExtractSegments", self._extract_segments),
|
|
2295
|
+
]
|
|
2296
|
+
)
|
|
2297
|
+
|
|
2298
|
+
# print the default parameters
|
|
2299
|
+
PBM3C2._print_default_parameters(
|
|
2300
|
+
kwargs=kwargs, pipeline_param_dict=labeling_pipeline.get_params()
|
|
2301
|
+
)
|
|
2302
|
+
|
|
2303
|
+
# no computation
|
|
2304
|
+
if epoch0 is None or epoch1 is None:
|
|
2305
|
+
# logger.info("epoch0 and epoch1 are required, no parameter changes applied")
|
|
2306
|
+
return
|
|
2307
|
+
|
|
2308
|
+
# save the default pipeline options
|
|
2309
|
+
default_options = labeling_pipeline.get_params()
|
|
2310
|
+
|
|
2311
|
+
# overwrite the default parameters
|
|
2312
|
+
unused_kwargs = PBM3C2._overwrite_pipeline_parameters(
|
|
2313
|
+
kwargs=kwargs, pipeline=labeling_pipeline
|
|
2314
|
+
)
|
|
2315
|
+
if len(unused_kwargs) > 0:
|
|
2316
|
+
logger.warning(
|
|
2317
|
+
f"The parameters '{unused_kwargs.keys()}' are not part of the pipeline parameters: \n "
|
|
2318
|
+
f"{pp.pformat(labeling_pipeline.get_params())}"
|
|
2319
|
+
)
|
|
2320
|
+
|
|
2321
|
+
# apply the pipeline
|
|
2322
|
+
X0 = np.hstack((epoch0.cloud[:, :], np.zeros((epoch0.cloud.shape[0], 1))))
|
|
2323
|
+
X1 = np.hstack((epoch1.cloud[:, :], np.ones((epoch1.cloud.shape[0], 1))))
|
|
2324
|
+
X = np.vstack((X0, X1))
|
|
2325
|
+
labeling_pipeline.fit(X)
|
|
2326
|
+
segments = labeling_pipeline.transform(X)
|
|
2327
|
+
|
|
2328
|
+
# restore the default pipeline options
|
|
2329
|
+
unused_kwargs = PBM3C2._overwrite_pipeline_parameters(
|
|
2330
|
+
kwargs=default_options,
|
|
2331
|
+
pipeline=labeling_pipeline,
|
|
2332
|
+
message="The pipeline parameters after restoration are: ",
|
|
2333
|
+
)
|
|
2334
|
+
assert (
|
|
2335
|
+
len(unused_kwargs) == 0
|
|
2336
|
+
), "All default options should be found when default parameter restoration is done"
|
|
2337
|
+
|
|
2338
|
+
return segments, builder_extended_y.generate_extended_y(segments)
|
|
2339
|
+
|
|
2340
|
+
def export_segmented_point_cloud_and_segments(
|
|
2341
|
+
self,
|
|
2342
|
+
epoch0: Epoch = None,
|
|
2343
|
+
epoch1: Epoch = None,
|
|
2344
|
+
x_y_z_id_epoch0_file_name: typing.Union[str, None] = "x_y_z_id_epoch0.xyz",
|
|
2345
|
+
x_y_z_id_epoch1_file_name: typing.Union[str, None] = "x_y_z_id_epoch1.xyz",
|
|
2346
|
+
extracted_segments_file_name: typing.Union[
|
|
2347
|
+
str, None
|
|
2348
|
+
] = "extracted_segments.seg",
|
|
2349
|
+
concatenate_name="",
|
|
2350
|
+
**kwargs,
|
|
2351
|
+
) -> typing.Union[
|
|
2352
|
+
typing.Tuple[np.ndarray, typing.Union[np.ndarray, None], np.ndarray], None
|
|
2353
|
+
]:
|
|
2354
|
+
"""
|
|
2355
|
+
For each epoch, it returns the segmentation of the point cloud as a numpy array (n_points, 4)
|
|
2356
|
+
and it also serializes them using the provided file names.
|
|
2357
|
+
where each row has the following structure: x, y, z, segment_id
|
|
2358
|
+
|
|
2359
|
+
It also generates a numpy array of segments of the form:
|
|
2360
|
+
X_COLUMN, Y_COLUMN, Z_COLUMN, -> Center of Gravity
|
|
2361
|
+
EPOCH_ID_COLUMN, -> 0/1
|
|
2362
|
+
EIGENVALUE0_COLUMN, EIGENVALUE1_COLUMN, EIGENVALUE2_COLUMN,
|
|
2363
|
+
EIGENVECTOR_0_X_COLUMN, EIGENVECTOR_0_Y_COLUMN, EIGENVECTOR_0_Z_COLUMN,
|
|
2364
|
+
EIGENVECTOR_1_X_COLUMN, EIGENVECTOR_1_Y_COLUMN, EIGENVECTOR_1_Z_COLUMN,
|
|
2365
|
+
EIGENVECTOR_2_X_COLUMN, EIGENVECTOR_2_Y_COLUMN, EIGENVECTOR_2_Z_COLUMN, -> Normal vector
|
|
2366
|
+
LLSV_COLUMN, -> lowest local surface variation
|
|
2367
|
+
SEGMENT_ID_COLUMN,
|
|
2368
|
+
STANDARD_DEVIATION_COLUMN,
|
|
2369
|
+
NR_POINTS_PER_SEG_COLUMN,
|
|
2370
|
+
|
|
2371
|
+
:param epoch0:
|
|
2372
|
+
Epoch object | None
|
|
2373
|
+
:param epoch1:
|
|
2374
|
+
Epoch object | None
|
|
2375
|
+
:param x_y_z_id_epoch0_file_name:
|
|
2376
|
+
The output file name for epoch0, point cloud segmentation, saved as a numpy array (n_points, 4)
|
|
2377
|
+
(x,y,z, segment_id)
|
|
2378
|
+
| None
|
|
2379
|
+
:param x_y_z_id_epoch1_file_name:
|
|
2380
|
+
The output file name for epoch1, point cloud segmentation, saved as a numpy array (n_points, 4)
|
|
2381
|
+
(x,y,z, segment_id)
|
|
2382
|
+
| None
|
|
2383
|
+
:param extracted_segments_file_name:
|
|
2384
|
+
The output file name for the file containing the segments, saved as a numpy array containing
|
|
2385
|
+
the column structure introduced above.
|
|
2386
|
+
| None
|
|
2387
|
+
:param concatenate_name:
|
|
2388
|
+
String that is utilized to uniquely identify the same transformer between multiple pipelines.
|
|
2389
|
+
:param kwargs:
|
|
2390
|
+
|
|
2391
|
+
Used for customize the default pipeline parameters.
|
|
2392
|
+
|
|
2393
|
+
Getting the default parameters:
|
|
2394
|
+
e.g. "get_pipeline_options"
|
|
2395
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
2396
|
+
|
|
2397
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
2398
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
2399
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
2400
|
+
|
|
2401
|
+
this process is stateless
|
|
2402
|
+
|
|
2403
|
+
:return:
|
|
2404
|
+
tuple [ x_y_z_id_epoch0, x_y_z_id_epoch1 | None, extracted_segments ] | None
|
|
2405
|
+
"""
|
|
2406
|
+
|
|
2407
|
+
pipe_segmentation = Pipeline(
|
|
2408
|
+
[
|
|
2409
|
+
(
|
|
2410
|
+
concatenate_name + "_Transform_PerPointComputation",
|
|
2411
|
+
self._per_point_computation,
|
|
2412
|
+
),
|
|
2413
|
+
(concatenate_name + "_Transform_Segmentation", self._segmentation),
|
|
2414
|
+
(
|
|
2415
|
+
concatenate_name + "_Transform_Second_Segmentation",
|
|
2416
|
+
self._second_segmentation,
|
|
2417
|
+
),
|
|
2418
|
+
]
|
|
2419
|
+
)
|
|
2420
|
+
|
|
2421
|
+
pipe_extract_segments = Pipeline(
|
|
2422
|
+
[
|
|
2423
|
+
(
|
|
2424
|
+
concatenate_name + "_Transform_ExtractSegments",
|
|
2425
|
+
self._extract_segments,
|
|
2426
|
+
),
|
|
2427
|
+
]
|
|
2428
|
+
)
|
|
2429
|
+
|
|
2430
|
+
# print the default parameters
|
|
2431
|
+
PBM3C2._print_default_parameters(
|
|
2432
|
+
kwargs=kwargs,
|
|
2433
|
+
pipeline_param_dict={
|
|
2434
|
+
**pipe_segmentation.get_params(),
|
|
2435
|
+
**pipe_extract_segments.get_params(),
|
|
2436
|
+
},
|
|
2437
|
+
)
|
|
2438
|
+
|
|
2439
|
+
# no computation
|
|
2440
|
+
if epoch0 is None: # or epoch1 is None:
|
|
2441
|
+
# logger.info("epoch0 is required, no parameter changes applied")
|
|
2442
|
+
return
|
|
2443
|
+
|
|
2444
|
+
# save the default pipeline options
|
|
2445
|
+
default_options = {
|
|
2446
|
+
**pipe_segmentation.get_params(),
|
|
2447
|
+
**pipe_extract_segments.get_params(),
|
|
2448
|
+
}
|
|
2449
|
+
del default_options["memory"]
|
|
2450
|
+
del default_options["steps"]
|
|
2451
|
+
del default_options["verbose"]
|
|
2452
|
+
|
|
2453
|
+
# overwrite the default parameters
|
|
2454
|
+
unused_kwargs = PBM3C2._overwrite_pipeline_parameters(
|
|
2455
|
+
kwargs=kwargs, pipeline=pipe_segmentation
|
|
2456
|
+
)
|
|
2457
|
+
unused_kwargs = PBM3C2._overwrite_pipeline_parameters(
|
|
2458
|
+
kwargs=unused_kwargs, pipeline=pipe_extract_segments
|
|
2459
|
+
)
|
|
2460
|
+
if len(unused_kwargs) > 0:
|
|
2461
|
+
logger.warning(
|
|
2462
|
+
f"The parameters '{unused_kwargs.keys()}' are not part of the pipeline parameters: \n "
|
|
2463
|
+
f"{pp.pformat({**pipe_segmentation.get_params(), **pipe_extract_segments.get_params()})}"
|
|
2464
|
+
)
|
|
2465
|
+
|
|
2466
|
+
# apply the pipeline
|
|
2467
|
+
|
|
2468
|
+
if isinstance(epoch0, Epoch):
|
|
2469
|
+
X0 = np.hstack((epoch0.cloud[:, :], np.zeros((epoch0.cloud.shape[0], 1))))
|
|
2470
|
+
else:
|
|
2471
|
+
X0 = epoch0
|
|
2472
|
+
|
|
2473
|
+
if isinstance(epoch1, Epoch):
|
|
2474
|
+
X1 = np.hstack((epoch1.cloud[:, :], np.ones((epoch1.cloud.shape[0], 1))))
|
|
2475
|
+
X = np.vstack((X0, X1))
|
|
2476
|
+
else:
|
|
2477
|
+
X = X0
|
|
2478
|
+
|
|
2479
|
+
pipe_segmentation.fit(X)
|
|
2480
|
+
# 'out' contains the segmentation of the point cloud.
|
|
2481
|
+
out = pipe_segmentation.transform(X)
|
|
2482
|
+
|
|
2483
|
+
pipe_extract_segments.fit(out)
|
|
2484
|
+
# 'extracted_segments' contains the new set of segments
|
|
2485
|
+
extracted_segments = pipe_extract_segments.transform(out)
|
|
2486
|
+
|
|
2487
|
+
# restore the default pipeline options
|
|
2488
|
+
unused_default_options = PBM3C2._overwrite_pipeline_parameters(
|
|
2489
|
+
kwargs=default_options,
|
|
2490
|
+
pipeline=pipe_segmentation,
|
|
2491
|
+
message="The pipeline parameters after restoration are: ",
|
|
2492
|
+
)
|
|
2493
|
+
unused_default_options = PBM3C2._overwrite_pipeline_parameters(
|
|
2494
|
+
kwargs=unused_default_options,
|
|
2495
|
+
pipeline=pipe_extract_segments,
|
|
2496
|
+
message="The pipeline parameters after restoration are: ",
|
|
2497
|
+
)
|
|
2498
|
+
assert (
|
|
2499
|
+
len(unused_default_options) == 0
|
|
2500
|
+
), "All default options should be found when default parameter restoration is done"
|
|
2501
|
+
|
|
2502
|
+
columns = self._second_segmentation.columns
|
|
2503
|
+
|
|
2504
|
+
Extract_Columns = [
|
|
2505
|
+
columns.X_COLUMN,
|
|
2506
|
+
columns.Y_COLUMN,
|
|
2507
|
+
columns.Z_COLUMN,
|
|
2508
|
+
columns.SEGMENT_ID_COLUMN,
|
|
2509
|
+
]
|
|
2510
|
+
|
|
2511
|
+
mask_epoch0 = out[:, columns.EPOCH_ID_COLUMN] == 0
|
|
2512
|
+
mask_epoch1 = out[:, columns.EPOCH_ID_COLUMN] == 1
|
|
2513
|
+
|
|
2514
|
+
out_epoch0 = out[mask_epoch0, :]
|
|
2515
|
+
out_epoch1 = out[mask_epoch1, :]
|
|
2516
|
+
|
|
2517
|
+
x_y_z_id_epoch0 = out_epoch0[:, Extract_Columns] # x,y,z, Seg_ID
|
|
2518
|
+
x_y_z_id_epoch1 = out_epoch1[:, Extract_Columns] # x,y,z, Seg_ID
|
|
2519
|
+
|
|
2520
|
+
if x_y_z_id_epoch0_file_name != None:
|
|
2521
|
+
logger.debug(f"Save 'x_y_z_id_epoch0' in file: {x_y_z_id_epoch0_file_name}")
|
|
2522
|
+
np.savetxt(x_y_z_id_epoch0_file_name, x_y_z_id_epoch0, delimiter=",")
|
|
2523
|
+
else:
|
|
2524
|
+
logger.debug(f"'x_y_z_id_epoch0' is not saved")
|
|
2525
|
+
|
|
2526
|
+
if x_y_z_id_epoch1_file_name != None:
|
|
2527
|
+
logger.debug(f"Save 'x_y_z_id_epoch1' in file: {x_y_z_id_epoch1_file_name}")
|
|
2528
|
+
np.savetxt(x_y_z_id_epoch1_file_name, x_y_z_id_epoch1, delimiter=",")
|
|
2529
|
+
else:
|
|
2530
|
+
logger.debug(f"'x_y_z_id_epoch1' is not saved")
|
|
2531
|
+
|
|
2532
|
+
if extracted_segments_file_name != None:
|
|
2533
|
+
logger.debug(
|
|
2534
|
+
f"Save 'extracted_segments' in file: {extracted_segments_file_name}"
|
|
2535
|
+
)
|
|
2536
|
+
np.savetxt(extracted_segments_file_name, extracted_segments, delimiter=",")
|
|
2537
|
+
else:
|
|
2538
|
+
logger.debug(f"'extracted_segments' is not saved")
|
|
2539
|
+
|
|
2540
|
+
return x_y_z_id_epoch0, x_y_z_id_epoch1, extracted_segments
|
|
2541
|
+
|
|
2542
|
+
def training(
|
|
2543
|
+
self,
|
|
2544
|
+
segments: np.ndarray = None,
|
|
2545
|
+
extended_y: np.ndarray = None,
|
|
2546
|
+
extracted_segments_file_name: str = "extracted_segments.seg",
|
|
2547
|
+
extended_y_file_name: str = "extended_y.csv",
|
|
2548
|
+
) -> None:
|
|
2549
|
+
"""
|
|
2550
|
+
It applies the training algorithm for the input pairs of Segments 'segments'
|
|
2551
|
+
and extended labels 'extended_y'.
|
|
2552
|
+
|
|
2553
|
+
:param segments:
|
|
2554
|
+
'Segments' numpy array of shape (n_segments, segment_size)
|
|
2555
|
+
|
|
2556
|
+
It has the following column structure:
|
|
2557
|
+
X_COLUMN, Y_COLUMN, Z_COLUMN, -> Center of Gravity
|
|
2558
|
+
EPOCH_ID_COLUMN, -> 0/1
|
|
2559
|
+
EIGENVALUE0_COLUMN, EIGENVALUE1_COLUMN, EIGENVALUE2_COLUMN,
|
|
2560
|
+
EIGENVECTOR_0_X_COLUMN, EIGENVECTOR_0_Y_COLUMN, EIGENVECTOR_0_Z_COLUMN,
|
|
2561
|
+
EIGENVECTOR_1_X_COLUMN, EIGENVECTOR_1_Y_COLUMN, EIGENVECTOR_1_Z_COLUMN,
|
|
2562
|
+
EIGENVECTOR_2_X_COLUMN, EIGENVECTOR_2_Y_COLUMN, EIGENVECTOR_2_Z_COLUMN, -> Normal vector
|
|
2563
|
+
LLSV_COLUMN, -> lowest local surface variation
|
|
2564
|
+
SEGMENT_ID_COLUMN,
|
|
2565
|
+
STANDARD_DEVIATION_COLUMN,
|
|
2566
|
+
NR_POINTS_PER_SEG_COLUMN,
|
|
2567
|
+
|
|
2568
|
+
:param extended_y:
|
|
2569
|
+
numpy array of shape (m_labels, 3)
|
|
2570
|
+
has the following structure: (tuples of index segment from epoch0, index segment from epoch1,
|
|
2571
|
+
label(0/1))
|
|
2572
|
+
:param extracted_segments_file_name:
|
|
2573
|
+
In case 'X' is None segments are loaded using 'extracted_segments_file_name'.
|
|
2574
|
+
:param extended_y_file_name:
|
|
2575
|
+
In case 'extended_y' is None, this file is used as input fallback.
|
|
2576
|
+
"""
|
|
2577
|
+
|
|
2578
|
+
if segments is None:
|
|
2579
|
+
# Resolve the given path
|
|
2580
|
+
filename = find_file(extracted_segments_file_name)
|
|
2581
|
+
# Read it
|
|
2582
|
+
try:
|
|
2583
|
+
logger.info(f"Reading segments from file '{filename}'")
|
|
2584
|
+
segments = np.genfromtxt(filename, delimiter=",")
|
|
2585
|
+
except ValueError:
|
|
2586
|
+
raise Py4DGeoError("Malformed file: " + str(filename))
|
|
2587
|
+
|
|
2588
|
+
if extended_y is None:
|
|
2589
|
+
# Resolve the given path
|
|
2590
|
+
filename = find_file(extended_y_file_name)
|
|
2591
|
+
# Read it
|
|
2592
|
+
try:
|
|
2593
|
+
logger.info(
|
|
2594
|
+
f"Reading tuples of (segment epoch0, segment epoch1, label) from file '{filename}'"
|
|
2595
|
+
)
|
|
2596
|
+
extended_y = np.genfromtxt(filename, delimiter=",")
|
|
2597
|
+
except ValueError:
|
|
2598
|
+
raise Py4DGeoError("Malformed file: " + str(filename))
|
|
2599
|
+
|
|
2600
|
+
training_pipeline = Pipeline(
|
|
2601
|
+
[
|
|
2602
|
+
("Classifier", self._classifier),
|
|
2603
|
+
]
|
|
2604
|
+
)
|
|
2605
|
+
|
|
2606
|
+
# apply the pipeline
|
|
2607
|
+
training_pipeline.fit(segments, extended_y)
|
|
2608
|
+
|
|
2609
|
+
def predict(
|
|
2610
|
+
self,
|
|
2611
|
+
epoch0: Epoch = None,
|
|
2612
|
+
epoch1: Epoch = None,
|
|
2613
|
+
epoch_additional_dimensions_lookup: typing.Dict[str, str] = None,
|
|
2614
|
+
**kwargs,
|
|
2615
|
+
) -> typing.Union[np.ndarray, None]:
|
|
2616
|
+
"""
|
|
2617
|
+
After extracting the segments from epoch0 and epoch1, it returns a numpy array of corresponding
|
|
2618
|
+
pairs of segments between epoch 0 and epoch 1.
|
|
2619
|
+
|
|
2620
|
+
:param epoch0:
|
|
2621
|
+
Epoch object.
|
|
2622
|
+
:param epoch1:
|
|
2623
|
+
Epoch object.
|
|
2624
|
+
:param epoch_additional_dimensions_lookup:
|
|
2625
|
+
A dictionary that maps between the names of the columns used internally
|
|
2626
|
+
and the names of the columns used by both epoch0 and epoch1.
|
|
2627
|
+
|
|
2628
|
+
No additional column is used.
|
|
2629
|
+
:param kwargs:
|
|
2630
|
+
|
|
2631
|
+
Used for customize the default pipeline parameters.
|
|
2632
|
+
|
|
2633
|
+
Getting the default parameters:
|
|
2634
|
+
e.g. "get_pipeline_options"
|
|
2635
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
2636
|
+
|
|
2637
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
2638
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
2639
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
2640
|
+
|
|
2641
|
+
this process is stateless
|
|
2642
|
+
|
|
2643
|
+
:return:
|
|
2644
|
+
A numpy array ( n_pairs, segment_features_size*2 ) where each row contains a pair of segments.
|
|
2645
|
+
| None
|
|
2646
|
+
"""
|
|
2647
|
+
|
|
2648
|
+
pipe_classifier = Pipeline(
|
|
2649
|
+
[
|
|
2650
|
+
("Classifier", self._classifier),
|
|
2651
|
+
]
|
|
2652
|
+
)
|
|
2653
|
+
|
|
2654
|
+
# arguments used, as part of the epoch0's pipeline.
|
|
2655
|
+
kwargs_epoch0 = {
|
|
2656
|
+
key: val
|
|
2657
|
+
for key, val in kwargs.items()
|
|
2658
|
+
if key.startswith("epoch0") or key == "get_pipeline_options"
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
# epoch0, segments computation
|
|
2662
|
+
out_for_epoch0 = self.export_segmented_point_cloud_and_segments(
|
|
2663
|
+
epoch0=epoch0,
|
|
2664
|
+
epoch1=None,
|
|
2665
|
+
x_y_z_id_epoch0_file_name=None,
|
|
2666
|
+
x_y_z_id_epoch1_file_name=None,
|
|
2667
|
+
extracted_segments_file_name=None,
|
|
2668
|
+
concatenate_name="epoch0",
|
|
2669
|
+
**kwargs_epoch0,
|
|
2670
|
+
)
|
|
2671
|
+
|
|
2672
|
+
# arguments used, as part of the epoch1's pipeline.
|
|
2673
|
+
kwargs_epoch1 = {
|
|
2674
|
+
key: val
|
|
2675
|
+
for key, val in kwargs.items()
|
|
2676
|
+
if key.startswith("epoch1") or key == "get_pipeline_options"
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
# epoch1, segments computation
|
|
2680
|
+
out_for_epoch1 = self.export_segmented_point_cloud_and_segments(
|
|
2681
|
+
epoch0=epoch1,
|
|
2682
|
+
epoch1=None,
|
|
2683
|
+
x_y_z_id_epoch0_file_name=None,
|
|
2684
|
+
x_y_z_id_epoch1_file_name=None,
|
|
2685
|
+
extracted_segments_file_name=None,
|
|
2686
|
+
concatenate_name="epoch1",
|
|
2687
|
+
**kwargs_epoch1,
|
|
2688
|
+
)
|
|
2689
|
+
|
|
2690
|
+
# arguments used, as part of the classifier 'pipeline'
|
|
2691
|
+
kwargs_classifier = {
|
|
2692
|
+
key: val
|
|
2693
|
+
for key, val in kwargs.items()
|
|
2694
|
+
if not key.startswith("epoch0") and not key.startswith("epoch1")
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
# print the default parameters for 'pipe_classifier'
|
|
2698
|
+
PBM3C2._print_default_parameters(
|
|
2699
|
+
kwargs=kwargs_classifier, pipeline_param_dict=pipe_classifier.get_params()
|
|
2700
|
+
)
|
|
2701
|
+
|
|
2702
|
+
# no computation
|
|
2703
|
+
if epoch0 is None or epoch1 is None:
|
|
2704
|
+
# logger.info("epoch0 and epoch1 are required, no parameter changes applied")
|
|
2705
|
+
return
|
|
2706
|
+
|
|
2707
|
+
# save the default pipe_classifier options
|
|
2708
|
+
default_options = pipe_classifier.get_params()
|
|
2709
|
+
|
|
2710
|
+
# overwrite the default parameters
|
|
2711
|
+
unused_kwargs = PBM3C2._overwrite_pipeline_parameters(
|
|
2712
|
+
kwargs=kwargs_classifier, pipeline=pipe_classifier
|
|
2713
|
+
)
|
|
2714
|
+
if len(unused_kwargs) > 0:
|
|
2715
|
+
logger.error(
|
|
2716
|
+
f"The parameters: '{unused_kwargs.keys()}' are not part of the pipeline parameters: \n "
|
|
2717
|
+
f"{pp.pformat(pipe_classifier.get_params())}"
|
|
2718
|
+
)
|
|
2719
|
+
|
|
2720
|
+
_0, _1, epoch0_segments = out_for_epoch0
|
|
2721
|
+
_0, _1, epoch1_segments = out_for_epoch1
|
|
2722
|
+
|
|
2723
|
+
columns = self._extract_segments.columns
|
|
2724
|
+
|
|
2725
|
+
# compute segment id offset
|
|
2726
|
+
max_seg_id_epoch0 = np.max(epoch0_segments[:, columns.SEGMENT_ID_COLUMN])
|
|
2727
|
+
# apply offset to segment from epoch1
|
|
2728
|
+
epoch1_segments[:, columns.SEGMENT_ID_COLUMN] += max_seg_id_epoch0 + 1
|
|
2729
|
+
|
|
2730
|
+
# enforce epoch ID as '0' for 'epoch0_segments'
|
|
2731
|
+
epoch0_segments[:, columns.EPOCH_ID_COLUMN] = 0
|
|
2732
|
+
|
|
2733
|
+
# change the epoch ID from 0 (the default one used during the previous computation for both epochs)
|
|
2734
|
+
# to 1 for 'epoch1_segments'
|
|
2735
|
+
epoch1_segments[:, columns.EPOCH_ID_COLUMN] = 1
|
|
2736
|
+
|
|
2737
|
+
extracted_segments = np.vstack((epoch0_segments, epoch1_segments))
|
|
2738
|
+
|
|
2739
|
+
out = pipe_classifier.predict(extracted_segments)
|
|
2740
|
+
|
|
2741
|
+
# restore the default pipeline options
|
|
2742
|
+
unused_default_options = PBM3C2._overwrite_pipeline_parameters(
|
|
2743
|
+
kwargs=default_options,
|
|
2744
|
+
pipeline=pipe_classifier,
|
|
2745
|
+
message="The pipeline parameters after restoration are: ",
|
|
2746
|
+
)
|
|
2747
|
+
assert (
|
|
2748
|
+
len(unused_default_options) == 0
|
|
2749
|
+
), "All default options should be found when default parameter restoration is done"
|
|
2750
|
+
|
|
2751
|
+
return out
|
|
2752
|
+
|
|
2753
|
+
def _compute_distances(
|
|
2754
|
+
self,
|
|
2755
|
+
epoch0_info: typing.Union[Epoch, np.ndarray] = None,
|
|
2756
|
+
epoch1: Epoch = None,
|
|
2757
|
+
alignment_error: float = 1.1,
|
|
2758
|
+
epoch_additional_dimensions_lookup: typing.Dict[str, str] = None,
|
|
2759
|
+
**kwargs,
|
|
2760
|
+
) -> typing.Union[typing.Tuple[np.ndarray, np.ndarray], None]:
|
|
2761
|
+
"""
|
|
2762
|
+
Compute the distance between 2 epochs. It also adds the following properties at the end of the computation:
|
|
2763
|
+
distances, corepoints (corepoints of epoch0), epochs (epoch0, epoch1), uncertainties
|
|
2764
|
+
|
|
2765
|
+
:param epoch0_info:
|
|
2766
|
+
Epoch object.
|
|
2767
|
+
| np.ndarray
|
|
2768
|
+
:param epoch1:
|
|
2769
|
+
Epoch object.
|
|
2770
|
+
:param alignment_error:
|
|
2771
|
+
alignment error reg between point clouds.
|
|
2772
|
+
:param epoch_additional_dimensions_lookup:
|
|
2773
|
+
A dictionary that maps between the names of the columns used internally
|
|
2774
|
+
and the names of the columns used by both epoch0 and epoch1.
|
|
2775
|
+
:param kwargs:
|
|
2776
|
+
Used for customize the default pipeline parameters.
|
|
2777
|
+
|
|
2778
|
+
Getting the default parameters:
|
|
2779
|
+
e.g. "get_pipeline_options"
|
|
2780
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
2781
|
+
|
|
2782
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
2783
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
2784
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
2785
|
+
|
|
2786
|
+
this process is stateless
|
|
2787
|
+
|
|
2788
|
+
:return:
|
|
2789
|
+
tuple [distances, uncertainties]
|
|
2790
|
+
'distances' is np.array (nr_similar_pairs, 1)
|
|
2791
|
+
'uncertainties' is np.array (nr_similar_pairs,1) and it has the following structure:
|
|
2792
|
+
dtype=np.dtype(
|
|
2793
|
+
[
|
|
2794
|
+
("lodetection", "<f8"),
|
|
2795
|
+
("spread1", "<f8"),
|
|
2796
|
+
("num_samples1", "<i8"),
|
|
2797
|
+
("spread2", "<f8"),
|
|
2798
|
+
("num_samples2", "<i8"),
|
|
2799
|
+
]
|
|
2800
|
+
)
|
|
2801
|
+
| None
|
|
2802
|
+
"""
|
|
2803
|
+
|
|
2804
|
+
logger.info(f"PBM3C2._compute_distances(...)")
|
|
2805
|
+
|
|
2806
|
+
# A numpy array where each row contains a pair of segments.
|
|
2807
|
+
segments_pair = self.predict(
|
|
2808
|
+
epoch0_info,
|
|
2809
|
+
epoch1,
|
|
2810
|
+
epoch_additional_dimensions_lookup=epoch_additional_dimensions_lookup,
|
|
2811
|
+
**kwargs,
|
|
2812
|
+
)
|
|
2813
|
+
|
|
2814
|
+
columns = self._extract_segments.columns
|
|
2815
|
+
|
|
2816
|
+
# no computation
|
|
2817
|
+
if epoch0_info is None or epoch1 is None:
|
|
2818
|
+
# logger.info("epoch0 and epoch1 are required, no parameter changes applied")
|
|
2819
|
+
return
|
|
2820
|
+
|
|
2821
|
+
size_segment = int(segments_pair.shape[1] / 2)
|
|
2822
|
+
nr_pairs = segments_pair.shape[0]
|
|
2823
|
+
|
|
2824
|
+
epoch0_segments = segments_pair[:, :size_segment]
|
|
2825
|
+
epoch1_segments = segments_pair[:, size_segment:]
|
|
2826
|
+
|
|
2827
|
+
# seg_id_epoch0, X_Column0, Y_Column0, Z_Column0, seg_id_epoch1, X_Column1, Y_Column1, Z_Column1, distance, uncertaintie
|
|
2828
|
+
output = np.empty((0, 10), dtype=float)
|
|
2829
|
+
|
|
2830
|
+
for indx in range(nr_pairs):
|
|
2831
|
+
segment_epoch0 = epoch0_segments[indx]
|
|
2832
|
+
segment_epoch1 = epoch1_segments[indx]
|
|
2833
|
+
|
|
2834
|
+
t0_CoG = segment_epoch0[
|
|
2835
|
+
[columns.X_COLUMN, columns.Y_COLUMN, columns.Z_COLUMN]
|
|
2836
|
+
]
|
|
2837
|
+
t1_CoG = segment_epoch1[
|
|
2838
|
+
[columns.X_COLUMN, columns.Y_COLUMN, columns.Z_COLUMN]
|
|
2839
|
+
]
|
|
2840
|
+
|
|
2841
|
+
Normal_Columns = [
|
|
2842
|
+
columns.EIGENVECTOR_2_X_COLUMN,
|
|
2843
|
+
columns.EIGENVECTOR_2_Y_COLUMN,
|
|
2844
|
+
columns.EIGENVECTOR_2_Z_COLUMN,
|
|
2845
|
+
]
|
|
2846
|
+
normal_vector_t0 = segment_epoch0[Normal_Columns]
|
|
2847
|
+
|
|
2848
|
+
M3C2_dist = normal_vector_t0.dot(t0_CoG - t1_CoG)
|
|
2849
|
+
|
|
2850
|
+
std_dev_normalized_squared_t0 = segment_epoch0[
|
|
2851
|
+
columns.STANDARD_DEVIATION_COLUMN
|
|
2852
|
+
]
|
|
2853
|
+
std_dev_normalized_squared_t1 = segment_epoch1[
|
|
2854
|
+
columns.STANDARD_DEVIATION_COLUMN
|
|
2855
|
+
]
|
|
2856
|
+
|
|
2857
|
+
LoDetection = 1.96 * (
|
|
2858
|
+
np.sqrt(std_dev_normalized_squared_t0 + std_dev_normalized_squared_t1)
|
|
2859
|
+
+ alignment_error
|
|
2860
|
+
)
|
|
2861
|
+
|
|
2862
|
+
# seg_id_epoch0, X_Column0, Y_Column0, Z_Column0, seg_id_epoch1, X_Column1, Y_Column1, Z_Column1, distance, uncertaintie
|
|
2863
|
+
args = (
|
|
2864
|
+
np.array([segment_epoch0[columns.SEGMENT_ID_COLUMN]]),
|
|
2865
|
+
t0_CoG,
|
|
2866
|
+
np.array([segment_epoch1[columns.SEGMENT_ID_COLUMN]]),
|
|
2867
|
+
t1_CoG,
|
|
2868
|
+
np.array([M3C2_dist]),
|
|
2869
|
+
np.array([LoDetection]),
|
|
2870
|
+
)
|
|
2871
|
+
row = np.concatenate(args)
|
|
2872
|
+
output = np.vstack((output, row))
|
|
2873
|
+
|
|
2874
|
+
# We don't return this anymore. It corresponds to the output structure of the original implementation.
|
|
2875
|
+
# return output
|
|
2876
|
+
|
|
2877
|
+
# distance vector
|
|
2878
|
+
self.distances = output[:, -2]
|
|
2879
|
+
|
|
2880
|
+
# corepoints to epoch0 ('initial' one)
|
|
2881
|
+
self.corepoints = Epoch(output[:, [1, 2, 3]])
|
|
2882
|
+
|
|
2883
|
+
# epochs
|
|
2884
|
+
self.epochs = (epoch0_info, epoch1)
|
|
2885
|
+
|
|
2886
|
+
self.uncertainties = np.empty(
|
|
2887
|
+
(output.shape[0], 1),
|
|
2888
|
+
dtype=np.dtype(
|
|
2889
|
+
[
|
|
2890
|
+
("lodetection", "<f8"),
|
|
2891
|
+
("spread1", "<f8"),
|
|
2892
|
+
("num_samples1", "<i8"),
|
|
2893
|
+
("spread2", "<f8"),
|
|
2894
|
+
("num_samples2", "<i8"),
|
|
2895
|
+
]
|
|
2896
|
+
),
|
|
2897
|
+
)
|
|
2898
|
+
|
|
2899
|
+
self.uncertainties["lodetection"] = output[:, -1].reshape(-1, 1)
|
|
2900
|
+
|
|
2901
|
+
self.uncertainties["spread1"] = np.sqrt(
|
|
2902
|
+
np.multiply(
|
|
2903
|
+
epoch0_segments[:, columns.STANDARD_DEVIATION_COLUMN],
|
|
2904
|
+
epoch0_segments[:, columns.NR_POINTS_PER_SEG_COLUMN],
|
|
2905
|
+
)
|
|
2906
|
+
).reshape(-1, 1)
|
|
2907
|
+
self.uncertainties["spread2"] = np.sqrt(
|
|
2908
|
+
np.multiply(
|
|
2909
|
+
epoch1_segments[:, columns.STANDARD_DEVIATION_COLUMN],
|
|
2910
|
+
epoch1_segments[:, columns.NR_POINTS_PER_SEG_COLUMN],
|
|
2911
|
+
)
|
|
2912
|
+
).reshape(-1, 1)
|
|
2913
|
+
|
|
2914
|
+
self.uncertainties["num_samples1"] = (
|
|
2915
|
+
epoch0_segments[:, columns.NR_POINTS_PER_SEG_COLUMN]
|
|
2916
|
+
.astype(int)
|
|
2917
|
+
.reshape(-1, 1)
|
|
2918
|
+
)
|
|
2919
|
+
self.uncertainties["num_samples2"] = (
|
|
2920
|
+
epoch1_segments[:, columns.NR_POINTS_PER_SEG_COLUMN]
|
|
2921
|
+
.astype(int)
|
|
2922
|
+
.reshape(-1, 1)
|
|
2923
|
+
)
|
|
2924
|
+
|
|
2925
|
+
return (self.distances, self.uncertainties)
|
|
2926
|
+
|
|
2927
|
+
def compute_distances(
|
|
2928
|
+
self,
|
|
2929
|
+
epoch0: typing.Union[Epoch, np.ndarray] = None,
|
|
2930
|
+
epoch1: Epoch = None,
|
|
2931
|
+
alignment_error: float = 1.1,
|
|
2932
|
+
**kwargs,
|
|
2933
|
+
) -> typing.Union[typing.Tuple[np.ndarray, np.ndarray], None]:
|
|
2934
|
+
"""
|
|
2935
|
+
Compute the distance between 2 epochs. It also adds the following properties at the end of the computation:
|
|
2936
|
+
distances, corepoints (corepoints of epoch0), epochs (epoch0, epoch1), uncertainties
|
|
2937
|
+
|
|
2938
|
+
:param epoch0:
|
|
2939
|
+
Epoch object.
|
|
2940
|
+
| np.ndarray
|
|
2941
|
+
:param epoch1:
|
|
2942
|
+
Epoch object.
|
|
2943
|
+
:param alignment_error:
|
|
2944
|
+
alignment error reg between point clouds.
|
|
2945
|
+
:param kwargs:
|
|
2946
|
+
Used for customize the default pipeline parameters.
|
|
2947
|
+
|
|
2948
|
+
Getting the default parameters:
|
|
2949
|
+
e.g. "get_pipeline_options"
|
|
2950
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
2951
|
+
|
|
2952
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
2953
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
2954
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
2955
|
+
|
|
2956
|
+
this process is stateless
|
|
2957
|
+
|
|
2958
|
+
:return:
|
|
2959
|
+
tuple [distances, uncertainties]
|
|
2960
|
+
'distances' is np.array (nr_similar_pairs, 1)
|
|
2961
|
+
'uncertainties' is np.array (nr_similar_pairs,1) and it has the following structure:
|
|
2962
|
+
dtype=np.dtype(
|
|
2963
|
+
[
|
|
2964
|
+
("lodetection", "<f8"),
|
|
2965
|
+
("spread1", "<f8"),
|
|
2966
|
+
("num_samples1", "<i8"),
|
|
2967
|
+
("spread2", "<f8"),
|
|
2968
|
+
("num_samples2", "<i8"),
|
|
2969
|
+
]
|
|
2970
|
+
)
|
|
2971
|
+
| None
|
|
2972
|
+
"""
|
|
2973
|
+
|
|
2974
|
+
logger.info(f"PBM3C2.compute_distances(...)")
|
|
2975
|
+
|
|
2976
|
+
return self._compute_distances(
|
|
2977
|
+
epoch0_info=epoch0,
|
|
2978
|
+
epoch1=epoch1,
|
|
2979
|
+
alignment_error=alignment_error,
|
|
2980
|
+
epoch_additional_dimensions_lookup=None,
|
|
2981
|
+
**kwargs,
|
|
2982
|
+
)
|
|
2983
|
+
|
|
2984
|
+
|
|
2985
|
+
def build_input_scenario2_with_normals(
|
|
2986
|
+
epoch0, epoch1, columns=SEGMENTED_POINT_CLOUD_COLUMNS
|
|
2987
|
+
):
|
|
2988
|
+
"""
|
|
2989
|
+
Build a segmented point cloud with computed normals for each point.
|
|
2990
|
+
|
|
2991
|
+
:param epoch0:
|
|
2992
|
+
x,y,z point cloud
|
|
2993
|
+
:param epoch1:
|
|
2994
|
+
x,y,z point cloud
|
|
2995
|
+
:param columns:
|
|
2996
|
+
|
|
2997
|
+
:return:
|
|
2998
|
+
tuple [
|
|
2999
|
+
numpy array (n_point_samples, 7) new_epoch0
|
|
3000
|
+
numpy array (m_point_samples, 7) new_epoch1
|
|
3001
|
+
]
|
|
3002
|
+
both containing: x,y,z, N_x,N_y,N_z, Segment_ID as columns.
|
|
3003
|
+
"""
|
|
3004
|
+
|
|
3005
|
+
Normal_Columns = [
|
|
3006
|
+
columns.EIGENVECTOR_2_X_COLUMN,
|
|
3007
|
+
columns.EIGENVECTOR_2_Y_COLUMN,
|
|
3008
|
+
columns.EIGENVECTOR_2_Z_COLUMN,
|
|
3009
|
+
]
|
|
3010
|
+
|
|
3011
|
+
x_y_z_Columns = [columns.X_COLUMN, columns.Y_COLUMN, columns.Z_COLUMN]
|
|
3012
|
+
|
|
3013
|
+
X0 = np.hstack((epoch0.cloud[:, :], np.zeros((epoch0.cloud.shape[0], 1))))
|
|
3014
|
+
X1 = np.hstack((epoch1.cloud[:, :], np.ones((epoch1.cloud.shape[0], 1))))
|
|
3015
|
+
|
|
3016
|
+
X = np.vstack((X0, X1))
|
|
3017
|
+
|
|
3018
|
+
transform_pipeline = Pipeline(
|
|
3019
|
+
[
|
|
3020
|
+
("Transform_PerPointComputation", PerPointComputation()),
|
|
3021
|
+
("Transform_Segmentation", Segmentation()),
|
|
3022
|
+
]
|
|
3023
|
+
)
|
|
3024
|
+
|
|
3025
|
+
transform_pipeline.fit(X)
|
|
3026
|
+
out = transform_pipeline.transform(X)
|
|
3027
|
+
|
|
3028
|
+
mask_epoch0 = out[:, columns.EPOCH_ID_COLUMN] == 0 # epoch0
|
|
3029
|
+
mask_epoch1 = out[:, columns.EPOCH_ID_COLUMN] == 1 # epoch1
|
|
3030
|
+
|
|
3031
|
+
new_epoch0 = out[mask_epoch0, :] # extract epoch0
|
|
3032
|
+
new_epoch1 = out[mask_epoch1, :] # extract epoch1
|
|
3033
|
+
|
|
3034
|
+
new_epoch0 = new_epoch0[
|
|
3035
|
+
:, x_y_z_Columns + Normal_Columns + [columns.SEGMENT_ID_COLUMN]
|
|
3036
|
+
]
|
|
3037
|
+
new_epoch1 = new_epoch1[
|
|
3038
|
+
:, x_y_z_Columns + Normal_Columns + [columns.SEGMENT_ID_COLUMN]
|
|
3039
|
+
]
|
|
3040
|
+
|
|
3041
|
+
# x,y,z, N_x,N_y,N_z, Segment_ID
|
|
3042
|
+
return new_epoch0, new_epoch1
|
|
3043
|
+
pass
|
|
3044
|
+
|
|
3045
|
+
|
|
3046
|
+
def build_input_scenario2_without_normals(
|
|
3047
|
+
epoch0, epoch1, columns=SEGMENTED_POINT_CLOUD_COLUMNS
|
|
3048
|
+
):
|
|
3049
|
+
"""
|
|
3050
|
+
Build a segmented point cloud.
|
|
3051
|
+
|
|
3052
|
+
:param epoch0:
|
|
3053
|
+
x,y,z point cloud
|
|
3054
|
+
:param epoch1:
|
|
3055
|
+
x,y,z point cloud
|
|
3056
|
+
:param columns:
|
|
3057
|
+
|
|
3058
|
+
:return:
|
|
3059
|
+
tuple [
|
|
3060
|
+
numpy array (n_point_samples, 4) new_epoch0
|
|
3061
|
+
numpy array (m_point_samples, 4) new_epoch1
|
|
3062
|
+
]
|
|
3063
|
+
both containing: x,y,z,Segment_ID as columns.
|
|
3064
|
+
"""
|
|
3065
|
+
|
|
3066
|
+
x_y_z_Columns = [columns.X_COLUMN, columns.Y_COLUMN, columns.Z_COLUMN]
|
|
3067
|
+
|
|
3068
|
+
Normal_Columns = [
|
|
3069
|
+
columns.EIGENVECTOR_2_X_COLUMN,
|
|
3070
|
+
columns.EIGENVECTOR_2_Y_COLUMN,
|
|
3071
|
+
columns.EIGENVECTOR_2_Z_COLUMN,
|
|
3072
|
+
]
|
|
3073
|
+
|
|
3074
|
+
X0 = np.hstack((epoch0.cloud[:, :], np.zeros((epoch0.cloud.shape[0], 1))))
|
|
3075
|
+
X1 = np.hstack((epoch1.cloud[:, :], np.ones((epoch1.cloud.shape[0], 1))))
|
|
3076
|
+
|
|
3077
|
+
X = np.vstack((X0, X1))
|
|
3078
|
+
|
|
3079
|
+
transform_pipeline = Pipeline(
|
|
3080
|
+
[
|
|
3081
|
+
("Transform_PerPointComputation", PerPointComputation()),
|
|
3082
|
+
("Transform_Segmentation", Segmentation()),
|
|
3083
|
+
]
|
|
3084
|
+
)
|
|
3085
|
+
|
|
3086
|
+
transform_pipeline.fit(X)
|
|
3087
|
+
out = transform_pipeline.transform(X)
|
|
3088
|
+
|
|
3089
|
+
mask_epoch0 = out[:, columns.EPOCH_ID_COLUMN] == 0 # epoch0
|
|
3090
|
+
mask_epoch1 = out[:, columns.EPOCH_ID_COLUMN] == 1 # epoch1
|
|
3091
|
+
|
|
3092
|
+
new_epoch0 = out[mask_epoch0, :] # extract epoch0
|
|
3093
|
+
new_epoch1 = out[mask_epoch1, :] # extract epoch1
|
|
3094
|
+
|
|
3095
|
+
new_epoch0 = new_epoch0[:, x_y_z_Columns + [columns.SEGMENT_ID_COLUMN]]
|
|
3096
|
+
new_epoch1 = new_epoch1[:, x_y_z_Columns + [columns.SEGMENT_ID_COLUMN]]
|
|
3097
|
+
|
|
3098
|
+
# x,y,z, Segment_ID
|
|
3099
|
+
return new_epoch0, new_epoch1
|
|
3100
|
+
pass
|
|
3101
|
+
|
|
3102
|
+
|
|
3103
|
+
class PBM3C2WithSegments(PBM3C2):
|
|
3104
|
+
def __init__(
|
|
3105
|
+
self,
|
|
3106
|
+
per_point_computation=PerPointComputation(),
|
|
3107
|
+
segmentation=Segmentation(),
|
|
3108
|
+
second_segmentation=Segmentation(),
|
|
3109
|
+
extract_segments=ExtractSegments(),
|
|
3110
|
+
post_segmentation=PostPointCloudSegmentation(compute_normal=True),
|
|
3111
|
+
classifier=ClassifierWrapper(),
|
|
3112
|
+
):
|
|
3113
|
+
"""
|
|
3114
|
+
:param per_point_computation:
|
|
3115
|
+
lowest local surface variation and PCA computation. (computes the normal vector as well)
|
|
3116
|
+
:param segmentation:
|
|
3117
|
+
The object used for the first segmentation.
|
|
3118
|
+
:param second_segmentation:
|
|
3119
|
+
The object used for the second segmentation.
|
|
3120
|
+
:param extract_segments:
|
|
3121
|
+
The object used for building the segments.
|
|
3122
|
+
:param post_segmentation:
|
|
3123
|
+
A transform object used to 'reconstruct' the result that is achieved using the "PB_P3C2 class"
|
|
3124
|
+
pipeline at the end of the point cloud segmentation.
|
|
3125
|
+
|
|
3126
|
+
The 'input' of this adaptor is formed by 2 epochs that contain as 'additional_dimensions'
|
|
3127
|
+
a segment_id column and optionally, precomputed normals as another 3 columns.
|
|
3128
|
+
|
|
3129
|
+
The 'output' of this adaptor is:
|
|
3130
|
+
numpy array (n_point_samples, 19) with the following column structure:
|
|
3131
|
+
[
|
|
3132
|
+
x, y, z ( 3 columns ),
|
|
3133
|
+
EpochID ( 1 column ),
|
|
3134
|
+
Eigenvalues( 3 columns ), -> that correspond to the next 3 Eigenvectors
|
|
3135
|
+
Eigenvectors( 3 columns ) X 3 -> in descending order using vector norm 2,
|
|
3136
|
+
Lowest local surface variation ( 1 column ),
|
|
3137
|
+
Segment_ID ( 1 column ),
|
|
3138
|
+
Standard deviation ( 1 column )
|
|
3139
|
+
]
|
|
3140
|
+
:param classifier:
|
|
3141
|
+
An instance of ClassifierWrapper class. The default wrapped classifier used is sk-learn RandomForest.
|
|
3142
|
+
"""
|
|
3143
|
+
|
|
3144
|
+
super().__init__(
|
|
3145
|
+
per_point_computation=per_point_computation,
|
|
3146
|
+
segmentation=segmentation,
|
|
3147
|
+
second_segmentation=second_segmentation,
|
|
3148
|
+
extract_segments=extract_segments,
|
|
3149
|
+
classifier=classifier,
|
|
3150
|
+
)
|
|
3151
|
+
|
|
3152
|
+
self._post_segmentation = post_segmentation
|
|
3153
|
+
|
|
3154
|
+
def generate_extended_labels_interactively(
|
|
3155
|
+
self,
|
|
3156
|
+
epoch0: Epoch = None,
|
|
3157
|
+
epoch1: Epoch = None,
|
|
3158
|
+
builder_extended_y: BuilderExtended_y_Visually = BuilderExtended_y_Visually(),
|
|
3159
|
+
epoch_additional_dimensions_lookup: typing.Dict[str, str] = dict(
|
|
3160
|
+
segment_id="segment_id", N_x="N_x", N_y="N_y", N_z="N_z"
|
|
3161
|
+
),
|
|
3162
|
+
**kwargs,
|
|
3163
|
+
) -> typing.Union[typing.Tuple[np.ndarray, np.ndarray], None]:
|
|
3164
|
+
"""
|
|
3165
|
+
Given 2 Epochs, it builds a pair of (segments and 'extended y').
|
|
3166
|
+
|
|
3167
|
+
:param epoch0:
|
|
3168
|
+
Epoch object,
|
|
3169
|
+
contains as 'additional_dimensions' a segment_id column (mandatory)
|
|
3170
|
+
and optionally, precomputed normals as another 3 columns.
|
|
3171
|
+
:param epoch1:
|
|
3172
|
+
Epoch object,
|
|
3173
|
+
contains as 'additional_dimensions' a segment_id column (mandatory)
|
|
3174
|
+
and optionally, precomputed normals as another 3 columns.
|
|
3175
|
+
:param builder_extended_y:
|
|
3176
|
+
The object is used for generating 'extended y', visually.
|
|
3177
|
+
:param epoch_additional_dimensions_lookup:
|
|
3178
|
+
A dictionary that maps between the names of the columns used internally to identify:
|
|
3179
|
+
segment id of the points: "segment_id" -> Mandatory part of the epochs
|
|
3180
|
+
Normal x-axes vector: "N_x" -> Optionally part of the epochs
|
|
3181
|
+
Normal y-axes vector: "N_y" -> Optionally part of the epochs
|
|
3182
|
+
Normal z-axes vector: "N_z" -> Optionally part of the epochs
|
|
3183
|
+
and the names of the columns used by both epoch0 and epoch1.
|
|
3184
|
+
:param kwargs:
|
|
3185
|
+
|
|
3186
|
+
Used for customize the default pipeline parameters.
|
|
3187
|
+
|
|
3188
|
+
Getting the default parameters:
|
|
3189
|
+
e.g. "get_pipeline_options"
|
|
3190
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
3191
|
+
|
|
3192
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
3193
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
3194
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
3195
|
+
|
|
3196
|
+
this process is stateless
|
|
3197
|
+
|
|
3198
|
+
:return:
|
|
3199
|
+
tuple [Segments, 'extended y'] | None
|
|
3200
|
+
|
|
3201
|
+
where:
|
|
3202
|
+
|
|
3203
|
+
'Segments' has the following column structure:
|
|
3204
|
+
X_COLUMN, Y_COLUMN, Z_COLUMN, -> Center of Gravity
|
|
3205
|
+
EPOCH_ID_COLUMN, -> 0/1
|
|
3206
|
+
EIGENVALUE0_COLUMN, EIGENVALUE1_COLUMN, EIGENVALUE2_COLUMN,
|
|
3207
|
+
EIGENVECTOR_0_X_COLUMN, EIGENVECTOR_0_Y_COLUMN, EIGENVECTOR_0_Z_COLUMN,
|
|
3208
|
+
EIGENVECTOR_1_X_COLUMN, EIGENVECTOR_1_Y_COLUMN, EIGENVECTOR_1_Z_COLUMN,
|
|
3209
|
+
EIGENVECTOR_2_X_COLUMN, EIGENVECTOR_2_Y_COLUMN, EIGENVECTOR_2_Z_COLUMN, -> Normal vector
|
|
3210
|
+
LLSV_COLUMN, -> lowest local surface variation
|
|
3211
|
+
SEGMENT_ID_COLUMN,
|
|
3212
|
+
STANDARD_DEVIATION_COLUMN,
|
|
3213
|
+
NR_POINTS_PER_SEG_COLUMN,
|
|
3214
|
+
|
|
3215
|
+
'extended y' has the following structure: (tuples of index segment from epoch0, index segment from epoch1,
|
|
3216
|
+
label(0/1)) used for learning.
|
|
3217
|
+
"""
|
|
3218
|
+
|
|
3219
|
+
if not interactive_available:
|
|
3220
|
+
logger.error("Interactive session not available in this environment.")
|
|
3221
|
+
return
|
|
3222
|
+
|
|
3223
|
+
transform_pipeline = Pipeline(
|
|
3224
|
+
[
|
|
3225
|
+
("Transform_Post_Segmentation", self._post_segmentation),
|
|
3226
|
+
("Transform_ExtractSegments", self._extract_segments),
|
|
3227
|
+
]
|
|
3228
|
+
)
|
|
3229
|
+
|
|
3230
|
+
# print the default parameters
|
|
3231
|
+
PBM3C2._print_default_parameters(
|
|
3232
|
+
kwargs=kwargs, pipeline_param_dict=transform_pipeline.get_params()
|
|
3233
|
+
)
|
|
3234
|
+
|
|
3235
|
+
# no computation
|
|
3236
|
+
if epoch0 is None or epoch1 is None:
|
|
3237
|
+
# logger.info("epoch0 and epoch1 are required, no parameter changes applied")
|
|
3238
|
+
return
|
|
3239
|
+
|
|
3240
|
+
# save the default pipeline options
|
|
3241
|
+
default_options = transform_pipeline.get_params()
|
|
3242
|
+
|
|
3243
|
+
# overwrite the default parameters
|
|
3244
|
+
PBM3C2._overwrite_pipeline_parameters(
|
|
3245
|
+
kwargs=kwargs, pipeline=transform_pipeline
|
|
3246
|
+
)
|
|
3247
|
+
|
|
3248
|
+
# extract columns
|
|
3249
|
+
|
|
3250
|
+
epoch0_normals = _extract_from_additional_dimensions(
|
|
3251
|
+
epoch=epoch0,
|
|
3252
|
+
column_names=[
|
|
3253
|
+
epoch_additional_dimensions_lookup["N_x"],
|
|
3254
|
+
epoch_additional_dimensions_lookup["N_y"],
|
|
3255
|
+
epoch_additional_dimensions_lookup["N_z"],
|
|
3256
|
+
],
|
|
3257
|
+
required_number_of_columns=[0, 3],
|
|
3258
|
+
)
|
|
3259
|
+
|
|
3260
|
+
epoch0_segment_id = _extract_from_additional_dimensions(
|
|
3261
|
+
epoch=epoch0,
|
|
3262
|
+
column_names=[
|
|
3263
|
+
epoch_additional_dimensions_lookup["segment_id"],
|
|
3264
|
+
],
|
|
3265
|
+
required_number_of_columns=[1],
|
|
3266
|
+
)
|
|
3267
|
+
|
|
3268
|
+
epoch0 = np.concatenate(
|
|
3269
|
+
(epoch0.cloud, epoch0_normals, epoch0_segment_id),
|
|
3270
|
+
axis=1,
|
|
3271
|
+
)
|
|
3272
|
+
|
|
3273
|
+
epoch1_normals = _extract_from_additional_dimensions(
|
|
3274
|
+
epoch=epoch1,
|
|
3275
|
+
column_names=[
|
|
3276
|
+
epoch_additional_dimensions_lookup["N_x"],
|
|
3277
|
+
epoch_additional_dimensions_lookup["N_y"],
|
|
3278
|
+
epoch_additional_dimensions_lookup["N_z"],
|
|
3279
|
+
],
|
|
3280
|
+
required_number_of_columns=[0, 3],
|
|
3281
|
+
)
|
|
3282
|
+
|
|
3283
|
+
epoch1_segment_id = _extract_from_additional_dimensions(
|
|
3284
|
+
epoch=epoch1,
|
|
3285
|
+
column_names=[
|
|
3286
|
+
epoch_additional_dimensions_lookup["segment_id"],
|
|
3287
|
+
],
|
|
3288
|
+
required_number_of_columns=[1],
|
|
3289
|
+
)
|
|
3290
|
+
|
|
3291
|
+
epoch1 = np.concatenate(
|
|
3292
|
+
(epoch1.cloud, epoch1_normals, epoch1_segment_id),
|
|
3293
|
+
axis=1,
|
|
3294
|
+
)
|
|
3295
|
+
|
|
3296
|
+
X = None
|
|
3297
|
+
for epoch_id, current_epoch in enumerate([epoch0, epoch1]):
|
|
3298
|
+
if current_epoch.shape[1] == 4:
|
|
3299
|
+
# [x, y, z, segment_id] columns
|
|
3300
|
+
assert self._post_segmentation.compute_normal, (
|
|
3301
|
+
"The reconstruction process doesn't have, as input, the Normal vector columns, hence, "
|
|
3302
|
+
"the normal vector computation is mandatory."
|
|
3303
|
+
)
|
|
3304
|
+
|
|
3305
|
+
logger.info(
|
|
3306
|
+
f"Reconstruct post segmentation output using [x, y, z, segment_id] "
|
|
3307
|
+
f"columns from epoch{epoch_id} "
|
|
3308
|
+
)
|
|
3309
|
+
|
|
3310
|
+
if X is not None:
|
|
3311
|
+
X = np.vstack(
|
|
3312
|
+
(
|
|
3313
|
+
X,
|
|
3314
|
+
self._reconstruct_input_without_normals(
|
|
3315
|
+
epoch=current_epoch,
|
|
3316
|
+
epoch_id=epoch_id,
|
|
3317
|
+
columns=self._post_segmentation.columns,
|
|
3318
|
+
),
|
|
3319
|
+
)
|
|
3320
|
+
)
|
|
3321
|
+
else:
|
|
3322
|
+
X = self._reconstruct_input_without_normals(
|
|
3323
|
+
epoch=current_epoch,
|
|
3324
|
+
epoch_id=epoch_id,
|
|
3325
|
+
columns=self._post_segmentation.columns,
|
|
3326
|
+
)
|
|
3327
|
+
else:
|
|
3328
|
+
# [x, y, z, N_x, N_y, N_z, segment_id] columns
|
|
3329
|
+
logger.info(
|
|
3330
|
+
f"Reconstruct post segmentation output using [x, y, z, N_x, N_y, N_z, segment_id] "
|
|
3331
|
+
f"columns from epoch{epoch_id}"
|
|
3332
|
+
)
|
|
3333
|
+
|
|
3334
|
+
if X is not None:
|
|
3335
|
+
X = np.vstack(
|
|
3336
|
+
(
|
|
3337
|
+
X,
|
|
3338
|
+
self._reconstruct_input_with_normals(
|
|
3339
|
+
epoch=current_epoch,
|
|
3340
|
+
epoch_id=epoch_id,
|
|
3341
|
+
columns=self._post_segmentation.columns,
|
|
3342
|
+
),
|
|
3343
|
+
)
|
|
3344
|
+
)
|
|
3345
|
+
else:
|
|
3346
|
+
X = self._reconstruct_input_with_normals(
|
|
3347
|
+
epoch=current_epoch,
|
|
3348
|
+
epoch_id=epoch_id,
|
|
3349
|
+
columns=self._post_segmentation.columns,
|
|
3350
|
+
)
|
|
3351
|
+
|
|
3352
|
+
# apply the pipeline
|
|
3353
|
+
|
|
3354
|
+
transform_pipeline.fit(X)
|
|
3355
|
+
segments = transform_pipeline.transform(X)
|
|
3356
|
+
|
|
3357
|
+
# restore the default pipeline options
|
|
3358
|
+
unused_kwargs = PBM3C2._overwrite_pipeline_parameters(
|
|
3359
|
+
kwargs=default_options,
|
|
3360
|
+
pipeline=transform_pipeline,
|
|
3361
|
+
message="The pipeline parameters after restoration are: ",
|
|
3362
|
+
)
|
|
3363
|
+
assert (
|
|
3364
|
+
len(unused_kwargs) == 0
|
|
3365
|
+
), "All default options should be found when default parameter restoration is done"
|
|
3366
|
+
|
|
3367
|
+
return segments, builder_extended_y.generate_extended_y(segments)
|
|
3368
|
+
|
|
3369
|
+
def reconstruct_post_segmentation_output(
|
|
3370
|
+
self,
|
|
3371
|
+
epoch0: Epoch = None,
|
|
3372
|
+
epoch1: Epoch = None,
|
|
3373
|
+
extracted_segments_file_name: typing.Union[
|
|
3374
|
+
str, None
|
|
3375
|
+
] = "extracted_segments.seg",
|
|
3376
|
+
epoch_additional_dimensions_lookup: typing.Dict[str, str] = dict(
|
|
3377
|
+
segment_id="segment_id", N_x="N_x", N_y="N_y", N_z="N_z"
|
|
3378
|
+
),
|
|
3379
|
+
concatenate_name: str = "",
|
|
3380
|
+
**kwargs,
|
|
3381
|
+
) -> typing.Union[
|
|
3382
|
+
typing.Tuple[np.ndarray, typing.Union[np.ndarray, None], np.ndarray], None
|
|
3383
|
+
]:
|
|
3384
|
+
"""
|
|
3385
|
+
'reconstruct' the result that is achieved using the "PB_P3C2 class" pipeline, by applying
|
|
3386
|
+
("Transform LLSV_and_PCA"), ("Transform Segmentation"),
|
|
3387
|
+
("Transform Second Segmentation") ("Transform ExtractSegments")
|
|
3388
|
+
using, as input, segmented point clouds.
|
|
3389
|
+
|
|
3390
|
+
:param epoch0:
|
|
3391
|
+
Epoch object,
|
|
3392
|
+
contains as 'additional_dimensions' a segment_id column (mandatory)
|
|
3393
|
+
and optionally, precomputed normals as another 3 columns.
|
|
3394
|
+
:param epoch1:
|
|
3395
|
+
Epoch object,
|
|
3396
|
+
contains as 'additional_dimensions' a segment_id column (mandatory)
|
|
3397
|
+
and optionally, precomputed normals as another 3 columns.
|
|
3398
|
+
:param extracted_segments_file_name:
|
|
3399
|
+
out file
|
|
3400
|
+
The file has the following structure:
|
|
3401
|
+
numpy array with shape (n_segments_samples, 20) where the column structure is as following:
|
|
3402
|
+
[
|
|
3403
|
+
X_COLUMN, Y_COLUMN, Z_COLUMN, -> Center of Gravity
|
|
3404
|
+
EPOCH_ID_COLUMN, -> 0/1
|
|
3405
|
+
EIGENVALUE0_COLUMN, EIGENVALUE1_COLUMN, EIGENVALUE2_COLUMN,
|
|
3406
|
+
EIGENVECTOR_0_X_COLUMN, EIGENVECTOR_0_Y_COLUMN, EIGENVECTOR_0_Z_COLUMN,
|
|
3407
|
+
EIGENVECTOR_1_X_COLUMN, EIGENVECTOR_1_Y_COLUMN, EIGENVECTOR_1_Z_COLUMN,
|
|
3408
|
+
EIGENVECTOR_2_X_COLUMN, EIGENVECTOR_2_Y_COLUMN, EIGENVECTOR_2_Z_COLUMN, -> Normal vector
|
|
3409
|
+
LLSV_COLUMN, -> lowest local surface variation
|
|
3410
|
+
SEGMENT_ID_COLUMN,
|
|
3411
|
+
STANDARD_DEVIATION_COLUMN,
|
|
3412
|
+
NR_POINTS_PER_SEG_COLUMN,
|
|
3413
|
+
]
|
|
3414
|
+
:param epoch_additional_dimensions_lookup:
|
|
3415
|
+
A dictionary that maps between the names of the columns used internally to identify:
|
|
3416
|
+
segment id of the points: "segment_id" -> Mandatory part of the epochs
|
|
3417
|
+
Normal x-axes vector: "N_x" -> Optionally part of the epochs
|
|
3418
|
+
Normal y-axes vector: "N_y" -> Optionally part of the epochs
|
|
3419
|
+
Normal z-axes vector: "N_z" -> Optionally part of the epochs
|
|
3420
|
+
and the names of the columns used by both epoch0 and epoch1.
|
|
3421
|
+
:param concatenate_name:
|
|
3422
|
+
String that is utilized to uniquely identify the same transformer between multiple pipelines.
|
|
3423
|
+
:param kwargs:
|
|
3424
|
+
Used for customize the default pipeline parameters.
|
|
3425
|
+
Getting the default parameters:
|
|
3426
|
+
e.g. "get_pipeline_options"
|
|
3427
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
3428
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
3429
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
3430
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
3431
|
+
|
|
3432
|
+
this process is stateless
|
|
3433
|
+
:return:
|
|
3434
|
+
tuple
|
|
3435
|
+
[
|
|
3436
|
+
numpy array with shape (n, 4|7) corresponding to epoch0 and
|
|
3437
|
+
containing [x,y,z,segment_id] | [x,y,z,N_x,N_y,N_z,segment_id],
|
|
3438
|
+
numpy array with shape (m, 4|7) corresponding to epoch1 and
|
|
3439
|
+
containing [x,y,z,segment_id] | [x,y,z,N_x,N_y,N_z,segment_id] | None,
|
|
3440
|
+
numpy array with shape (p, 20) corresponding to extracted_segments
|
|
3441
|
+
]
|
|
3442
|
+
| None
|
|
3443
|
+
"""
|
|
3444
|
+
|
|
3445
|
+
transform_pipeline = Pipeline(
|
|
3446
|
+
[
|
|
3447
|
+
(
|
|
3448
|
+
concatenate_name + "_Transform_Post Segmentation",
|
|
3449
|
+
self._post_segmentation,
|
|
3450
|
+
),
|
|
3451
|
+
(
|
|
3452
|
+
concatenate_name + "_Transform_ExtractSegments",
|
|
3453
|
+
self._extract_segments,
|
|
3454
|
+
),
|
|
3455
|
+
]
|
|
3456
|
+
)
|
|
3457
|
+
|
|
3458
|
+
# print the default parameters
|
|
3459
|
+
PBM3C2._print_default_parameters(
|
|
3460
|
+
kwargs=kwargs, pipeline_param_dict=transform_pipeline.get_params()
|
|
3461
|
+
)
|
|
3462
|
+
|
|
3463
|
+
# no computation
|
|
3464
|
+
if epoch0 is None: # or epoch1 is None:
|
|
3465
|
+
# logger.info("epoch0 is required, no parameter changes applied")
|
|
3466
|
+
return
|
|
3467
|
+
|
|
3468
|
+
# save the default pipeline options
|
|
3469
|
+
default_options = transform_pipeline.get_params()
|
|
3470
|
+
|
|
3471
|
+
# overwrite the default parameters
|
|
3472
|
+
PBM3C2._overwrite_pipeline_parameters(
|
|
3473
|
+
kwargs=kwargs, pipeline=transform_pipeline
|
|
3474
|
+
)
|
|
3475
|
+
|
|
3476
|
+
# extract columns
|
|
3477
|
+
|
|
3478
|
+
epoch0_normals = _extract_from_additional_dimensions(
|
|
3479
|
+
epoch=epoch0,
|
|
3480
|
+
column_names=[
|
|
3481
|
+
epoch_additional_dimensions_lookup["N_x"],
|
|
3482
|
+
epoch_additional_dimensions_lookup["N_y"],
|
|
3483
|
+
epoch_additional_dimensions_lookup["N_z"],
|
|
3484
|
+
],
|
|
3485
|
+
required_number_of_columns=[0, 3],
|
|
3486
|
+
)
|
|
3487
|
+
|
|
3488
|
+
epoch0_segment_id = _extract_from_additional_dimensions(
|
|
3489
|
+
epoch=epoch0,
|
|
3490
|
+
column_names=[
|
|
3491
|
+
epoch_additional_dimensions_lookup["segment_id"],
|
|
3492
|
+
],
|
|
3493
|
+
required_number_of_columns=[1],
|
|
3494
|
+
)
|
|
3495
|
+
|
|
3496
|
+
epoch0 = np.concatenate(
|
|
3497
|
+
(epoch0.cloud, epoch0_normals, epoch0_segment_id),
|
|
3498
|
+
axis=1,
|
|
3499
|
+
)
|
|
3500
|
+
|
|
3501
|
+
if epoch1 != None:
|
|
3502
|
+
epoch1_normals = _extract_from_additional_dimensions(
|
|
3503
|
+
epoch=epoch1,
|
|
3504
|
+
column_names=[
|
|
3505
|
+
epoch_additional_dimensions_lookup["N_x"],
|
|
3506
|
+
epoch_additional_dimensions_lookup["N_y"],
|
|
3507
|
+
epoch_additional_dimensions_lookup["N_z"],
|
|
3508
|
+
],
|
|
3509
|
+
required_number_of_columns=[0, 3],
|
|
3510
|
+
)
|
|
3511
|
+
|
|
3512
|
+
epoch1_segment_id = _extract_from_additional_dimensions(
|
|
3513
|
+
epoch=epoch1,
|
|
3514
|
+
column_names=[
|
|
3515
|
+
epoch_additional_dimensions_lookup["segment_id"],
|
|
3516
|
+
],
|
|
3517
|
+
required_number_of_columns=[1],
|
|
3518
|
+
)
|
|
3519
|
+
|
|
3520
|
+
epoch1 = np.concatenate(
|
|
3521
|
+
(epoch1.cloud, epoch1_normals, epoch1_segment_id),
|
|
3522
|
+
axis=1,
|
|
3523
|
+
)
|
|
3524
|
+
|
|
3525
|
+
epochs_list = [epoch0]
|
|
3526
|
+
if isinstance(epoch1, np.ndarray):
|
|
3527
|
+
epochs_list.append(epoch1)
|
|
3528
|
+
|
|
3529
|
+
X = None
|
|
3530
|
+
for epoch_id, current_epoch in enumerate(epochs_list):
|
|
3531
|
+
if current_epoch.shape[1] == 4:
|
|
3532
|
+
# [x, y, z, segment_id] columns
|
|
3533
|
+
assert self._post_segmentation.compute_normal, (
|
|
3534
|
+
"The reconstruction process doesn't have, as input, the Normal vector columns, hence, "
|
|
3535
|
+
"the normal vector computation is mandatory."
|
|
3536
|
+
)
|
|
3537
|
+
|
|
3538
|
+
logger.info(
|
|
3539
|
+
f"Reconstruct post segmentation output using [x, y, z, segment_id] "
|
|
3540
|
+
f"columns from epoch{epoch_id} "
|
|
3541
|
+
)
|
|
3542
|
+
|
|
3543
|
+
if X is not None:
|
|
3544
|
+
X = np.vstack(
|
|
3545
|
+
(
|
|
3546
|
+
X,
|
|
3547
|
+
self._reconstruct_input_without_normals(
|
|
3548
|
+
epoch=current_epoch,
|
|
3549
|
+
epoch_id=epoch_id,
|
|
3550
|
+
columns=self._post_segmentation.columns,
|
|
3551
|
+
),
|
|
3552
|
+
)
|
|
3553
|
+
)
|
|
3554
|
+
else:
|
|
3555
|
+
X = self._reconstruct_input_without_normals(
|
|
3556
|
+
epoch=current_epoch,
|
|
3557
|
+
epoch_id=epoch_id,
|
|
3558
|
+
columns=self._post_segmentation.columns,
|
|
3559
|
+
)
|
|
3560
|
+
else:
|
|
3561
|
+
# [x, y, z, N_x, N_y, N_z, segment_id] columns
|
|
3562
|
+
logger.info(
|
|
3563
|
+
f"Reconstruct post segmentation output using [x, y, z, N_x, N_y, N_z, segment_id] "
|
|
3564
|
+
f"columns from epoch{epoch_id}"
|
|
3565
|
+
)
|
|
3566
|
+
|
|
3567
|
+
if X is not None:
|
|
3568
|
+
X = np.vstack(
|
|
3569
|
+
(
|
|
3570
|
+
X,
|
|
3571
|
+
self._reconstruct_input_with_normals(
|
|
3572
|
+
epoch=current_epoch,
|
|
3573
|
+
epoch_id=epoch_id,
|
|
3574
|
+
columns=self._post_segmentation.columns,
|
|
3575
|
+
),
|
|
3576
|
+
)
|
|
3577
|
+
)
|
|
3578
|
+
else:
|
|
3579
|
+
X = self._reconstruct_input_with_normals(
|
|
3580
|
+
epoch=current_epoch,
|
|
3581
|
+
epoch_id=epoch_id,
|
|
3582
|
+
columns=self._post_segmentation.columns,
|
|
3583
|
+
)
|
|
3584
|
+
|
|
3585
|
+
# apply the pipeline
|
|
3586
|
+
|
|
3587
|
+
transform_pipeline.fit(X)
|
|
3588
|
+
extracted_segments = transform_pipeline.transform(X)
|
|
3589
|
+
|
|
3590
|
+
if extracted_segments_file_name is not None:
|
|
3591
|
+
logger.info(f"'Segments' saved in file: {extracted_segments_file_name}")
|
|
3592
|
+
np.savetxt(extracted_segments_file_name, extracted_segments, delimiter=",")
|
|
3593
|
+
else:
|
|
3594
|
+
logger.debug(f"No file name set as output for 'segments'")
|
|
3595
|
+
|
|
3596
|
+
# restore the default pipeline options
|
|
3597
|
+
unused_kwargs = PBM3C2._overwrite_pipeline_parameters(
|
|
3598
|
+
kwargs=default_options,
|
|
3599
|
+
pipeline=transform_pipeline,
|
|
3600
|
+
message="The pipeline parameters after restoration are: ",
|
|
3601
|
+
)
|
|
3602
|
+
assert (
|
|
3603
|
+
len(unused_kwargs) == 0
|
|
3604
|
+
), "All default options should be found when default parameter restoration is done"
|
|
3605
|
+
|
|
3606
|
+
return epoch0, epoch1, extracted_segments
|
|
3607
|
+
|
|
3608
|
+
def predict(
|
|
3609
|
+
self,
|
|
3610
|
+
epoch0: Epoch = None,
|
|
3611
|
+
epoch1: Epoch = None,
|
|
3612
|
+
epoch_additional_dimensions_lookup: typing.Dict[str, str] = dict(
|
|
3613
|
+
segment_id="segment_id", N_x="N_x", N_y="N_y", N_z="N_z"
|
|
3614
|
+
),
|
|
3615
|
+
**kwargs,
|
|
3616
|
+
) -> typing.Union[np.ndarray, None]:
|
|
3617
|
+
"""
|
|
3618
|
+
After the reconstruction of the result that is achieved using the "PB_P3C2 class" pipeline, by applying
|
|
3619
|
+
("Transform LLSV_and_PCA"), ("Transform Segmentation"), ("Transform Second Segmentation"), ("Transform ExtractSegments")
|
|
3620
|
+
applied to the segmented point cloud of epoch0 and epoch1, it returns a numpy array of corresponding
|
|
3621
|
+
pairs of segments between epoch 0 and epoch 1.
|
|
3622
|
+
|
|
3623
|
+
:param epoch0:
|
|
3624
|
+
Epoch object,
|
|
3625
|
+
contains as 'additional_dimensions' a segment_id column ( mandatory )
|
|
3626
|
+
and optionally, precomputed normals as another 3 columns.
|
|
3627
|
+
( the structure must be consistent with the structure of epoch1 parameter )
|
|
3628
|
+
:param epoch1:
|
|
3629
|
+
Epoch object.
|
|
3630
|
+
contains as 'additional_dimensions' a segment_id column ( mandatory )
|
|
3631
|
+
and optionally, precomputed normals as another 3 columns.
|
|
3632
|
+
( the structure must be consistent with the structure of epoch0 parameter )
|
|
3633
|
+
:param epoch_additional_dimensions_lookup:
|
|
3634
|
+
A dictionary that maps between the names of the columns used internally to identify:
|
|
3635
|
+
segment id of the points: "segment_id" -> Mandatory part of the epochs
|
|
3636
|
+
Normal x-axes vector: "N_x" -> Optionally part of the epochs
|
|
3637
|
+
Normal y-axes vector: "N_y" -> Optionally part of the epochs
|
|
3638
|
+
Normal z-axes vector: "N_z" -> Optionally part of the epochs
|
|
3639
|
+
and the names of the columns used by both epoch0 and epoch1.
|
|
3640
|
+
:param kwargs:
|
|
3641
|
+
Used for customize the default pipeline parameters.
|
|
3642
|
+
|
|
3643
|
+
Getting the default parameters:
|
|
3644
|
+
e.g. "get_pipeline_options"
|
|
3645
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
3646
|
+
|
|
3647
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
3648
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
3649
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
3650
|
+
|
|
3651
|
+
this process is stateless
|
|
3652
|
+
:return:
|
|
3653
|
+
A numpy array of shape ( n_pairs, segment_size*2 ) where each row contains a pair of segments.
|
|
3654
|
+
"""
|
|
3655
|
+
|
|
3656
|
+
predicting_pipeline = Pipeline(
|
|
3657
|
+
[
|
|
3658
|
+
("Transform_Post_Segmentation", self._post_segmentation),
|
|
3659
|
+
("Transform_ExtractSegments", self._extract_segments),
|
|
3660
|
+
("Classifier", self._classifier),
|
|
3661
|
+
]
|
|
3662
|
+
)
|
|
3663
|
+
|
|
3664
|
+
# print the default parameters
|
|
3665
|
+
PBM3C2._print_default_parameters(
|
|
3666
|
+
kwargs=kwargs, pipeline_param_dict=predicting_pipeline.get_params()
|
|
3667
|
+
)
|
|
3668
|
+
|
|
3669
|
+
# no computation
|
|
3670
|
+
if epoch0 is None or epoch1 is None:
|
|
3671
|
+
# logger.info("epoch0 and epoch1 are required, no parameter changes applied")
|
|
3672
|
+
return
|
|
3673
|
+
|
|
3674
|
+
# save the default pipeline options
|
|
3675
|
+
default_options = predicting_pipeline.get_params()
|
|
3676
|
+
|
|
3677
|
+
# overwrite the default parameters
|
|
3678
|
+
PBM3C2._overwrite_pipeline_parameters(
|
|
3679
|
+
kwargs=kwargs, pipeline=predicting_pipeline
|
|
3680
|
+
)
|
|
3681
|
+
|
|
3682
|
+
# extract columns
|
|
3683
|
+
|
|
3684
|
+
epoch0_normals = _extract_from_additional_dimensions(
|
|
3685
|
+
epoch=epoch0,
|
|
3686
|
+
column_names=[
|
|
3687
|
+
epoch_additional_dimensions_lookup["N_x"],
|
|
3688
|
+
epoch_additional_dimensions_lookup["N_y"],
|
|
3689
|
+
epoch_additional_dimensions_lookup["N_z"],
|
|
3690
|
+
],
|
|
3691
|
+
required_number_of_columns=[0, 3],
|
|
3692
|
+
)
|
|
3693
|
+
|
|
3694
|
+
epoch0_segment_id = _extract_from_additional_dimensions(
|
|
3695
|
+
epoch=epoch0,
|
|
3696
|
+
column_names=[
|
|
3697
|
+
epoch_additional_dimensions_lookup["segment_id"],
|
|
3698
|
+
],
|
|
3699
|
+
required_number_of_columns=[1],
|
|
3700
|
+
)
|
|
3701
|
+
|
|
3702
|
+
epoch0 = np.concatenate(
|
|
3703
|
+
(epoch0.cloud, epoch0_normals, epoch0_segment_id),
|
|
3704
|
+
axis=1,
|
|
3705
|
+
)
|
|
3706
|
+
|
|
3707
|
+
epoch1_normals = _extract_from_additional_dimensions(
|
|
3708
|
+
epoch=epoch1,
|
|
3709
|
+
column_names=[
|
|
3710
|
+
epoch_additional_dimensions_lookup["N_x"],
|
|
3711
|
+
epoch_additional_dimensions_lookup["N_y"],
|
|
3712
|
+
epoch_additional_dimensions_lookup["N_z"],
|
|
3713
|
+
],
|
|
3714
|
+
required_number_of_columns=[0, 3],
|
|
3715
|
+
)
|
|
3716
|
+
|
|
3717
|
+
epoch1_segment_id = _extract_from_additional_dimensions(
|
|
3718
|
+
epoch=epoch1,
|
|
3719
|
+
column_names=[
|
|
3720
|
+
epoch_additional_dimensions_lookup["segment_id"],
|
|
3721
|
+
],
|
|
3722
|
+
required_number_of_columns=[1],
|
|
3723
|
+
)
|
|
3724
|
+
|
|
3725
|
+
epoch1 = np.concatenate(
|
|
3726
|
+
(epoch1.cloud, epoch1_normals, epoch1_segment_id),
|
|
3727
|
+
axis=1,
|
|
3728
|
+
)
|
|
3729
|
+
|
|
3730
|
+
X = None
|
|
3731
|
+
for epoch_id, current_epoch in enumerate([epoch0, epoch1]):
|
|
3732
|
+
if current_epoch.shape[1] == 4:
|
|
3733
|
+
# [x, y, z, segment_id] columns
|
|
3734
|
+
assert self._post_segmentation.compute_normal, (
|
|
3735
|
+
"The reconstruction process doesn't have, as input, the Normal vector columns, hence, "
|
|
3736
|
+
"the normal vector computation is mandatory."
|
|
3737
|
+
)
|
|
3738
|
+
|
|
3739
|
+
logger.info(
|
|
3740
|
+
f"Reconstruct post segmentation output using [x, y, z, segment_id] "
|
|
3741
|
+
f"columns from epoch{epoch_id} "
|
|
3742
|
+
)
|
|
3743
|
+
|
|
3744
|
+
if X is not None:
|
|
3745
|
+
X = np.vstack(
|
|
3746
|
+
(
|
|
3747
|
+
X,
|
|
3748
|
+
self._reconstruct_input_without_normals(
|
|
3749
|
+
epoch=current_epoch,
|
|
3750
|
+
epoch_id=epoch_id,
|
|
3751
|
+
columns=self._post_segmentation.columns,
|
|
3752
|
+
),
|
|
3753
|
+
)
|
|
3754
|
+
)
|
|
3755
|
+
else:
|
|
3756
|
+
X = self._reconstruct_input_without_normals(
|
|
3757
|
+
epoch=current_epoch,
|
|
3758
|
+
epoch_id=epoch_id,
|
|
3759
|
+
columns=self._post_segmentation.columns,
|
|
3760
|
+
)
|
|
3761
|
+
else:
|
|
3762
|
+
# [x, y, z, N_x, N_y, N_z, segment_id] columns
|
|
3763
|
+
logger.info(
|
|
3764
|
+
f"Reconstruct post segmentation output using [x, y, z, N_x, N_y, N_z, segment_id] "
|
|
3765
|
+
f"columns from epoch{epoch_id}"
|
|
3766
|
+
)
|
|
3767
|
+
|
|
3768
|
+
if X is not None:
|
|
3769
|
+
X = np.vstack(
|
|
3770
|
+
(
|
|
3771
|
+
X,
|
|
3772
|
+
self._reconstruct_input_with_normals(
|
|
3773
|
+
epoch=current_epoch,
|
|
3774
|
+
epoch_id=epoch_id,
|
|
3775
|
+
columns=self._post_segmentation.columns,
|
|
3776
|
+
),
|
|
3777
|
+
)
|
|
3778
|
+
)
|
|
3779
|
+
else:
|
|
3780
|
+
X = self._reconstruct_input_with_normals(
|
|
3781
|
+
epoch=current_epoch,
|
|
3782
|
+
epoch_id=epoch_id,
|
|
3783
|
+
columns=self._post_segmentation.columns,
|
|
3784
|
+
)
|
|
3785
|
+
|
|
3786
|
+
# apply the pipeline
|
|
3787
|
+
|
|
3788
|
+
out = predicting_pipeline.predict(X)
|
|
3789
|
+
|
|
3790
|
+
# restore the default pipeline options
|
|
3791
|
+
unused_kwargs = PBM3C2._overwrite_pipeline_parameters(
|
|
3792
|
+
kwargs=default_options,
|
|
3793
|
+
pipeline=predicting_pipeline,
|
|
3794
|
+
message="The pipeline parameters after restoration are: ",
|
|
3795
|
+
)
|
|
3796
|
+
assert (
|
|
3797
|
+
len(unused_kwargs) == 0
|
|
3798
|
+
), "All default options should be found when default parameter restoration is done"
|
|
3799
|
+
|
|
3800
|
+
return out
|
|
3801
|
+
|
|
3802
|
+
def compute_distances(
|
|
3803
|
+
self,
|
|
3804
|
+
epoch0: Epoch = None,
|
|
3805
|
+
epoch1: Epoch = None,
|
|
3806
|
+
alignment_error: float = 1.1,
|
|
3807
|
+
epoch_additional_dimensions_lookup: typing.Dict[str, str] = dict(
|
|
3808
|
+
segment_id="segment_id", N_x="N_x", N_y="N_y", N_z="N_z"
|
|
3809
|
+
),
|
|
3810
|
+
**kwargs,
|
|
3811
|
+
) -> typing.Union[typing.Tuple[np.ndarray, np.ndarray], None]:
|
|
3812
|
+
"""
|
|
3813
|
+
Compute the distance between 2 epochs. It also adds the following properties at the end of the computation:
|
|
3814
|
+
distances, corepoints (corepoints of epoch0), epochs (epoch0, epoch1), uncertainties
|
|
3815
|
+
|
|
3816
|
+
:param epoch0:
|
|
3817
|
+
Epoch object,
|
|
3818
|
+
contains as 'additional_dimensions' a segment_id column (mandatory)
|
|
3819
|
+
and optionally, precomputed normals as another 3 columns.
|
|
3820
|
+
:param epoch1:
|
|
3821
|
+
Epoch object,
|
|
3822
|
+
contains as 'additional_dimensions' a segment_id column (mandatory)
|
|
3823
|
+
and optionally, precomputed normals as another 3 columns.
|
|
3824
|
+
:param alignment_error:
|
|
3825
|
+
alignment error reg between point clouds.
|
|
3826
|
+
:param epoch_additional_dimensions_lookup:
|
|
3827
|
+
A dictionary that maps between the names of the columns used internally to identify:
|
|
3828
|
+
segment id of the points: "segment_id" -> Mandatory part of the epochs
|
|
3829
|
+
Normal x-axes vector: "N_x" -> Optionally part of the epochs
|
|
3830
|
+
Normal y-axes vector: "N_y" -> Optionally part of the epochs
|
|
3831
|
+
Normal z-axes vector: "N_z" -> Optionally part of the epochs
|
|
3832
|
+
and the names of the columns used by both epoch0 and epoch1.
|
|
3833
|
+
:param kwargs:
|
|
3834
|
+
Used for customize the default pipeline parameters.
|
|
3835
|
+
|
|
3836
|
+
Getting the default parameters:
|
|
3837
|
+
e.g. "get_pipeline_options"
|
|
3838
|
+
In case this parameter is True, the method will print the pipeline options as kwargs.
|
|
3839
|
+
|
|
3840
|
+
e.g. "output_file_name" (of a specific step in the pipeline) default value is "None".
|
|
3841
|
+
In case of setting it, the result of computation at that step is dump as xyz file.
|
|
3842
|
+
e.g. "distance_3D_threshold" (part of Segmentation Transform)
|
|
3843
|
+
|
|
3844
|
+
this process is stateless
|
|
3845
|
+
|
|
3846
|
+
:return:
|
|
3847
|
+
tuple [distances, uncertainties]
|
|
3848
|
+
'distances' is np.array (nr_similar_pairs, 1)
|
|
3849
|
+
'uncertainties' is np array (nr_similar_pairs, 1) and it has the following structure:
|
|
3850
|
+
dtype=np.dtype(
|
|
3851
|
+
[
|
|
3852
|
+
("lodetection", "<f8"),
|
|
3853
|
+
("spread1", "<f8"),
|
|
3854
|
+
("num_samples1", "<i8"),
|
|
3855
|
+
("spread2", "<f8"),
|
|
3856
|
+
("num_samples2", "<i8"),
|
|
3857
|
+
]
|
|
3858
|
+
)
|
|
3859
|
+
| None
|
|
3860
|
+
"""
|
|
3861
|
+
|
|
3862
|
+
logger.info(f"PBM3C2WithSegments.compute_distances(...)")
|
|
3863
|
+
|
|
3864
|
+
return super()._compute_distances(
|
|
3865
|
+
epoch0_info=epoch0,
|
|
3866
|
+
epoch1=epoch1,
|
|
3867
|
+
alignment_error=alignment_error,
|
|
3868
|
+
epoch_additional_dimensions_lookup=epoch_additional_dimensions_lookup,
|
|
3869
|
+
**kwargs,
|
|
3870
|
+
)
|