pye57 0.4.18__cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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.

Potentially problematic release.


This version of pye57 might be problematic. Click here for more details.

pye57/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from pye57 import libe57
2
+ from pye57.scan_header import ScanHeader
3
+ from pye57.e57 import E57
pye57/__version__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.4.18"
pye57/e57.py ADDED
@@ -0,0 +1,438 @@
1
+ import uuid
2
+ import os
3
+ from typing import Dict
4
+ from enum import Enum
5
+
6
+ import numpy as np
7
+ from pyquaternion import Quaternion
8
+
9
+ from pye57.__version__ import __version__
10
+ from pye57 import libe57
11
+ from pye57 import ScanHeader
12
+ from pye57.utils import convert_spherical_to_cartesian
13
+
14
+ try:
15
+ from exceptions import WindowsError
16
+ except ImportError:
17
+ class WindowsError(OSError):
18
+ pass
19
+
20
+
21
+ SUPPORTED_CARTESIAN_POINT_FIELDS = {
22
+ "cartesianX": "d",
23
+ "cartesianY": "d",
24
+ "cartesianZ": "d",
25
+ }
26
+
27
+ SUPPORTED_SPHERICAL_POINT_FIELDS = {
28
+ "sphericalRange": "d",
29
+ "sphericalAzimuth": "d",
30
+ "sphericalElevation": "d",
31
+ }
32
+
33
+ class COORDINATE_SYSTEMS(Enum):
34
+ CARTESIAN = SUPPORTED_CARTESIAN_POINT_FIELDS
35
+ SPHERICAL = SUPPORTED_SPHERICAL_POINT_FIELDS
36
+
37
+ SUPPORTED_POINT_FIELDS = {
38
+ **SUPPORTED_CARTESIAN_POINT_FIELDS,
39
+ **SUPPORTED_SPHERICAL_POINT_FIELDS,
40
+ "intensity": "f",
41
+ "colorRed": "B",
42
+ "colorGreen": "B",
43
+ "colorBlue": "B",
44
+ "rowIndex": "H",
45
+ "columnIndex": "H",
46
+ "cartesianInvalidState": "b",
47
+ "sphericalInvalidState": "b",
48
+ }
49
+
50
+
51
+ class E57:
52
+ def __init__(self, path, mode="r"):
53
+ if mode not in "rw":
54
+ raise ValueError("Only 'r' and 'w' modes are supported")
55
+ self.path = path
56
+ try:
57
+ self.image_file = libe57.ImageFile(path, mode)
58
+ if mode == "w":
59
+ self.write_default_header()
60
+ except Exception as e:
61
+ try:
62
+ self.image_file.close()
63
+ os.remove(path)
64
+ except (AttributeError, WindowsError, PermissionError):
65
+ pass
66
+ raise e
67
+
68
+ def __del__(self):
69
+ self.close()
70
+
71
+ def __enter__(self):
72
+ return self
73
+
74
+ def __exit__(self, exc_type, exc_val, exc_tb):
75
+ self.close()
76
+
77
+ def close(self):
78
+ if hasattr(self, "image_file"):
79
+ self.image_file.close()
80
+
81
+ @property
82
+ def root(self):
83
+ return self.image_file.root()
84
+
85
+ @property
86
+ def data3d(self):
87
+ return self.root["data3D"]
88
+
89
+ @property
90
+ def scan_count(self):
91
+ return len(self.data3d)
92
+
93
+ def get_header(self, index):
94
+ return ScanHeader(self.data3d[index])
95
+
96
+ def write_default_header(self):
97
+ imf = self.image_file
98
+ imf.extensionsAdd("", libe57.E57_V1_0_URI)
99
+ self.root.set("formatName", libe57.StringNode(imf, "ASTM E57 3D Imaging Data File"))
100
+ self.root.set("guid", libe57.StringNode(imf, "{%s}" % uuid.uuid4()))
101
+ self.root.set("versionMajor", libe57.IntegerNode(imf, libe57.E57_FORMAT_MAJOR))
102
+ self.root.set("versionMinor", libe57.IntegerNode(imf, libe57.E57_FORMAT_MINOR))
103
+ self.root.set("e57LibraryVersion", libe57.StringNode(imf, libe57.E57_LIBRARY_ID))
104
+ self.root.set("coordinateMetadata", libe57.StringNode(imf, ""))
105
+ creation_date_time = libe57.StructureNode(imf)
106
+ creation_date_time.set("dateTimeValue", libe57.FloatNode(imf, 0.0))
107
+ creation_date_time.set("isAtomicClockReferenced", libe57.IntegerNode(imf, 0))
108
+ self.root.set("creationDateTime", creation_date_time)
109
+ self.root.set("data3D", libe57.VectorNode(imf, True))
110
+ self.root.set("images2D", libe57.VectorNode(imf, True))
111
+
112
+ def make_buffer(self, field_name, capacity, do_conversion=True, do_scaling=True):
113
+ # now this exception should never get hit through read_scan or read_scan_raw
114
+ # for read_scan, the headers are constructed, so they should all be supported
115
+ # for read_scan_raw, it now filters out the unsupported headers
116
+ # however, if make_buffer or make_buffers gets called, an unsupported field name could be passed in directly
117
+ # if we don't want users calling them, maybe we could make them private, and this would be an assertion
118
+ if field_name not in SUPPORTED_POINT_FIELDS:
119
+ raise ValueError("Unsupported point field: %s" % field_name)
120
+
121
+ np_array = np.empty(capacity, SUPPORTED_POINT_FIELDS[field_name])
122
+ buffer = libe57.SourceDestBuffer(self.image_file,
123
+ field_name,
124
+ np_array,
125
+ capacity,
126
+ do_conversion,
127
+ do_scaling)
128
+ return np_array, buffer
129
+
130
+ def make_buffers(self, field_names, capacity, do_conversion=True, do_scaling=True):
131
+ data = {}
132
+ buffers = libe57.VectorSourceDestBuffer()
133
+ for field in field_names:
134
+ d, b = self.make_buffer(field, capacity, do_conversion=do_conversion, do_scaling=do_scaling)
135
+ data[field] = d
136
+ buffers.append(b)
137
+ return data, buffers
138
+
139
+ def read_scan_raw(self, index, ignore_unsupported_fields=False) -> Dict:
140
+ header = self.get_header(index)
141
+ supported_point_fields = []
142
+ unsupported_point_fields = []
143
+ for field in header.point_fields:
144
+ if field in SUPPORTED_POINT_FIELDS:
145
+ supported_point_fields.append(field)
146
+ else:
147
+ unsupported_point_fields.append(field)
148
+ if unsupported_point_fields != [] and not ignore_unsupported_fields:
149
+ raise ValueError("Unsupported point fields: %s.\n"
150
+ "Consider using 'ignore_unsupported_fields' to skip them." % unsupported_point_fields)
151
+ # could it call make_buffers instead, it looks like the code's identical
152
+ data = {}
153
+ buffers = libe57.VectorSourceDestBuffer()
154
+ for field in supported_point_fields:
155
+ np_array, buffer = self.make_buffer(field, header.point_count)
156
+ data[field] = np_array
157
+ buffers.append(buffer)
158
+
159
+ header.points.reader(buffers).read()
160
+
161
+ return data
162
+
163
+ def scan_position(self, index):
164
+ pt = np.array([[0, 0, 0]])
165
+ header = self.get_header(index)
166
+ return self.to_global(pt, header.rotation, header.translation)
167
+
168
+ @staticmethod
169
+ def to_global(points, rotation, translation):
170
+ rotation_matrix = Quaternion(rotation).rotation_matrix
171
+ return (np.dot(rotation_matrix, points.T) + translation.reshape(3, 1)).T
172
+
173
+ def read_scan(self,
174
+ index,
175
+ *,
176
+ intensity=False,
177
+ colors=False,
178
+ row_column=False,
179
+ transform=True,
180
+ ignore_missing_fields=False) -> Dict:
181
+ header = self.get_header(index)
182
+ n_points = header.point_count
183
+
184
+ coordinate_system = header.get_coordinate_system(COORDINATE_SYSTEMS)
185
+ if coordinate_system is COORDINATE_SYSTEMS.CARTESIAN:
186
+ validState = "cartesianInvalidState"
187
+ fields = list(SUPPORTED_CARTESIAN_POINT_FIELDS.keys())
188
+ elif coordinate_system is COORDINATE_SYSTEMS.SPHERICAL:
189
+ validState = "sphericalInvalidState"
190
+ fields = list(SUPPORTED_SPHERICAL_POINT_FIELDS.keys())
191
+ if intensity:
192
+ fields.append("intensity")
193
+ if colors:
194
+ fields.append("colorRed")
195
+ fields.append("colorGreen")
196
+ fields.append("colorBlue")
197
+ if row_column:
198
+ fields.append("rowIndex")
199
+ fields.append("columnIndex")
200
+ fields.append(validState)
201
+
202
+ for field in fields[:]:
203
+ if field not in header.point_fields:
204
+ if ignore_missing_fields:
205
+ fields.remove(field)
206
+ else:
207
+ raise ValueError("Requested to read a field (%s) with is absent from the e57 file. "
208
+ "Consider using 'ignore_missing_fields' to skip it." % field)
209
+
210
+ data, buffers = self.make_buffers(fields, n_points)
211
+ header.points.reader(buffers).read()
212
+
213
+ if validState in data:
214
+ valid = ~data[validState].astype("?")
215
+
216
+ for field in data:
217
+ data[field] = data[field][valid]
218
+
219
+ del data[validState]
220
+
221
+ if transform:
222
+ if coordinate_system is COORDINATE_SYSTEMS.CARTESIAN:
223
+ xyz = np.array([data["cartesianX"], data["cartesianY"], data["cartesianZ"]]).T
224
+ elif coordinate_system is COORDINATE_SYSTEMS.SPHERICAL:
225
+ rae = np.array([data["sphericalRange"], data["sphericalAzimuth"], data["sphericalElevation"]]).T
226
+ # rae to xyz
227
+ xyz = convert_spherical_to_cartesian(rae)
228
+ # translation to global coordinates
229
+ if header.has_pose():
230
+ xyz = self.to_global(xyz, header.rotation, header.translation)
231
+ data["cartesianX"] = xyz[:, 0]
232
+ data["cartesianY"] = xyz[:, 1]
233
+ data["cartesianZ"] = xyz[:, 2]
234
+ return data
235
+
236
+ def write_scan_raw(self, data: Dict, *, name=None, rotation=None, translation=None, scan_header=None):
237
+ for field in data.keys():
238
+ if field not in SUPPORTED_POINT_FIELDS:
239
+ raise ValueError("Unsupported point field: %s" % field)
240
+
241
+ if rotation is None:
242
+ rotation = getattr(scan_header, "rotation", np.array([1, 0, 0, 0]))
243
+
244
+ if translation is None:
245
+ translation = getattr(scan_header, "translation", np.array([0, 0, 0]))
246
+
247
+ if name is None:
248
+ name = getattr(scan_header, "name", "Scan %s" % len(self.data3d))
249
+
250
+ temperature = getattr(scan_header, "temperature", 0)
251
+ relativeHumidity = getattr(scan_header, "relativeHumidity", 0)
252
+ atmosphericPressure = getattr(scan_header, "atmosphericPressure", 0)
253
+
254
+ scan_node = libe57.StructureNode(self.image_file)
255
+ scan_node.set("guid", libe57.StringNode(self.image_file, "{%s}" % uuid.uuid4()))
256
+ scan_node.set("name", libe57.StringNode(self.image_file, name))
257
+ scan_node.set("temperature", libe57.FloatNode(self.image_file, temperature))
258
+ scan_node.set("relativeHumidity", libe57.FloatNode(self.image_file, relativeHumidity))
259
+ scan_node.set("atmosphericPressure", libe57.FloatNode(self.image_file, atmosphericPressure))
260
+ scan_node.set("description", libe57.StringNode(self.image_file, "pye57 v%s" % __version__))
261
+
262
+ n_points = data["cartesianX"].shape[0]
263
+
264
+ ibox = libe57.StructureNode(self.image_file)
265
+ if "rowIndex" in data and "columnIndex" in data:
266
+ min_row = np.min(data["rowIndex"])
267
+ max_row = np.max(data["rowIndex"])
268
+ min_col = np.min(data["columnIndex"])
269
+ max_col = np.max(data["columnIndex"])
270
+ ibox.set("rowMinimum", libe57.IntegerNode(self.image_file, min_row))
271
+ ibox.set("rowMaximum", libe57.IntegerNode(self.image_file, max_row))
272
+ ibox.set("columnMinimum", libe57.IntegerNode(self.image_file, min_col))
273
+ ibox.set("columnMaximum", libe57.IntegerNode(self.image_file, max_col))
274
+ else:
275
+ ibox.set("rowMinimum", libe57.IntegerNode(self.image_file, 0))
276
+ ibox.set("rowMaximum", libe57.IntegerNode(self.image_file, n_points - 1))
277
+ ibox.set("columnMinimum", libe57.IntegerNode(self.image_file, 0))
278
+ ibox.set("columnMaximum", libe57.IntegerNode(self.image_file, 0))
279
+ ibox.set("returnMinimum", libe57.IntegerNode(self.image_file, 0))
280
+ ibox.set("returnMaximum", libe57.IntegerNode(self.image_file, 0))
281
+ scan_node.set("indexBounds", ibox)
282
+
283
+ if "intensity" in data:
284
+ int_min = getattr(scan_header, "intensityMinimum", np.min(data["intensity"]))
285
+ int_max = getattr(scan_header, "intensityMaximum", np.max(data["intensity"]))
286
+ intbox = libe57.StructureNode(self.image_file)
287
+ intbox.set("intensityMinimum", libe57.FloatNode(self.image_file, int_min))
288
+ intbox.set("intensityMaximum", libe57.FloatNode(self.image_file, int_max))
289
+ scan_node.set("intensityLimits", intbox)
290
+
291
+ color = all(c in data for c in ["colorRed", "colorGreen", "colorBlue"])
292
+ if color:
293
+ colorbox = libe57.StructureNode(self.image_file)
294
+ colorbox.set("colorRedMinimum", libe57.IntegerNode(self.image_file, 0))
295
+ colorbox.set("colorRedMaximum", libe57.IntegerNode(self.image_file, 255))
296
+ colorbox.set("colorGreenMinimum", libe57.IntegerNode(self.image_file, 0))
297
+ colorbox.set("colorGreenMaximum", libe57.IntegerNode(self.image_file, 255))
298
+ colorbox.set("colorBlueMinimum", libe57.IntegerNode(self.image_file, 0))
299
+ colorbox.set("colorBlueMaximum", libe57.IntegerNode(self.image_file, 255))
300
+ scan_node.set("colorLimits", colorbox)
301
+
302
+ bbox_node = libe57.StructureNode(self.image_file)
303
+ x, y, z = data["cartesianX"], data["cartesianY"], data["cartesianZ"]
304
+ valid = None
305
+ if "cartesianInvalidState" in data:
306
+ valid = ~data["cartesianInvalidState"].astype("?")
307
+ x, y, z = x[valid], y[valid], z[valid]
308
+ bb_min = np.array([x.min(), y.min(), z.min()])
309
+ bb_max = np.array([x.max(), y.max(), z.max()])
310
+ del valid, x, y, z
311
+
312
+ if scan_header is not None:
313
+ bb_min_scaled = np.array([scan_header.xMinimum, scan_header.yMinimum, scan_header.zMinimum])
314
+ bb_max_scaled = np.array([scan_header.xMaximum, scan_header.yMaximum, scan_header.zMaximum])
315
+ else:
316
+ bb_min_scaled = self.to_global(bb_min.reshape(-1, 3), rotation, translation)[0]
317
+ bb_max_scaled = self.to_global(bb_max.reshape(-1, 3), rotation, translation)[0]
318
+
319
+ bbox_node.set("xMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[0]))
320
+ bbox_node.set("xMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[0]))
321
+ bbox_node.set("yMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[1]))
322
+ bbox_node.set("yMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[1]))
323
+ bbox_node.set("zMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[2]))
324
+ bbox_node.set("zMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[2]))
325
+ scan_node.set("cartesianBounds", bbox_node)
326
+
327
+ if rotation is not None and translation is not None:
328
+ pose_node = libe57.StructureNode(self.image_file)
329
+ scan_node.set("pose", pose_node)
330
+ rotation_node = libe57.StructureNode(self.image_file)
331
+ rotation_node.set("w", libe57.FloatNode(self.image_file, rotation[0]))
332
+ rotation_node.set("x", libe57.FloatNode(self.image_file, rotation[1]))
333
+ rotation_node.set("y", libe57.FloatNode(self.image_file, rotation[2]))
334
+ rotation_node.set("z", libe57.FloatNode(self.image_file, rotation[3]))
335
+ pose_node.set("rotation", rotation_node)
336
+ translation_node = libe57.StructureNode(self.image_file)
337
+ translation_node.set("x", libe57.FloatNode(self.image_file, translation[0]))
338
+ translation_node.set("y", libe57.FloatNode(self.image_file, translation[1]))
339
+ translation_node.set("z", libe57.FloatNode(self.image_file, translation[2]))
340
+ pose_node.set("translation", translation_node)
341
+
342
+ start_datetime = getattr(scan_header, "acquisitionStart_dateTimeValue", 0)
343
+ start_atomic = getattr(scan_header, "acquisitionStart_isAtomicClockReferenced", False)
344
+ end_datetime = getattr(scan_header, "acquisitionEnd_dateTimeValue", 0)
345
+ end_atomic = getattr(scan_header, "acquisitionEnd_isAtomicClockReferenced", False)
346
+ acquisition_start = libe57.StructureNode(self.image_file)
347
+ scan_node.set("acquisitionStart", acquisition_start)
348
+ acquisition_start.set("dateTimeValue", libe57.FloatNode(self.image_file, start_datetime))
349
+ acquisition_start.set("isAtomicClockReferenced", libe57.IntegerNode(self.image_file, start_atomic))
350
+ acquisition_end = libe57.StructureNode(self.image_file)
351
+ scan_node.set("acquisitionEnd", acquisition_end)
352
+ acquisition_end.set("dateTimeValue", libe57.FloatNode(self.image_file, end_datetime))
353
+ acquisition_end.set("isAtomicClockReferenced", libe57.IntegerNode(self.image_file, end_atomic))
354
+
355
+ # todo: pointGroupingSchemes
356
+
357
+ points_prototype = libe57.StructureNode(self.image_file)
358
+
359
+ is_scaled = False
360
+ precision = libe57.E57_DOUBLE if is_scaled else libe57.E57_SINGLE
361
+
362
+ center = (bb_max + bb_min) / 2
363
+
364
+ chunk_size = 5000000
365
+
366
+ x_node = libe57.FloatNode(self.image_file, center[0], precision, bb_min[0], bb_max[0])
367
+ y_node = libe57.FloatNode(self.image_file, center[1], precision, bb_min[1], bb_max[1])
368
+ z_node = libe57.FloatNode(self.image_file, center[2], precision, bb_min[2], bb_max[2])
369
+ points_prototype.set("cartesianX", x_node)
370
+ points_prototype.set("cartesianY", y_node)
371
+ points_prototype.set("cartesianZ", z_node)
372
+
373
+ field_names = ["cartesianX", "cartesianY", "cartesianZ"]
374
+
375
+ if "intensity" in data:
376
+ intensity_min = np.min(data["intensity"])
377
+ intensity_max = np.max(data["intensity"])
378
+ intensity_node = libe57.FloatNode(self.image_file, intensity_min, precision, intensity_min, intensity_max)
379
+ points_prototype.set("intensity", intensity_node)
380
+ field_names.append("intensity")
381
+
382
+ if all(color in data for color in ["colorRed", "colorGreen", "colorBlue"]):
383
+ points_prototype.set("colorRed", libe57.IntegerNode(self.image_file, 0, 0, 255))
384
+ points_prototype.set("colorGreen", libe57.IntegerNode(self.image_file, 0, 0, 255))
385
+ points_prototype.set("colorBlue", libe57.IntegerNode(self.image_file, 0, 0, 255))
386
+ field_names.append("colorRed")
387
+ field_names.append("colorGreen")
388
+ field_names.append("colorBlue")
389
+
390
+ if "rowIndex" in data and "columnIndex" in data:
391
+ min_row = np.min(data["rowIndex"])
392
+ max_row = np.max(data["rowIndex"])
393
+ min_col = np.min(data["columnIndex"])
394
+ max_col = np.max(data["columnIndex"])
395
+ points_prototype.set("rowIndex", libe57.IntegerNode(self.image_file, min_row, min_row, max_row))
396
+ field_names.append("rowIndex")
397
+ points_prototype.set("columnIndex", libe57.IntegerNode(self.image_file, min_col, min_col, max_col))
398
+ field_names.append("columnIndex")
399
+
400
+ if "cartesianInvalidState" in data:
401
+ min_state = np.min(data["cartesianInvalidState"])
402
+ max_state = np.max(data["cartesianInvalidState"])
403
+ points_prototype.set("cartesianInvalidState", libe57.IntegerNode(self.image_file, 0, min_state, max_state))
404
+ field_names.append("cartesianInvalidState")
405
+
406
+ # other fields
407
+ # // "sphericalRange"
408
+ # // "sphericalAzimuth"
409
+ # // "sphericalElevation"
410
+ # // "timeStamp"
411
+ # // "sphericalInvalidState"
412
+ # // "isColorInvalid"
413
+ # // "isIntensityInvalid"
414
+ # // "isTimeStampInvalid"
415
+
416
+ arrays, buffers = self.make_buffers(field_names, chunk_size)
417
+
418
+ codecs = libe57.VectorNode(self.image_file, True)
419
+ points = libe57.CompressedVectorNode(self.image_file, points_prototype, codecs)
420
+ scan_node.set("points", points)
421
+
422
+ self.data3d.append(scan_node)
423
+
424
+ writer = points.writer(buffers)
425
+
426
+ current_index = 0
427
+ while current_index != n_points:
428
+ current_chunk = min(n_points - current_index, chunk_size)
429
+
430
+ for type_ in SUPPORTED_POINT_FIELDS:
431
+ if type_ in arrays:
432
+ arrays[type_][:current_chunk] = data[type_][current_index:current_index + current_chunk]
433
+
434
+ writer.write(current_chunk)
435
+
436
+ current_index += current_chunk
437
+
438
+ writer.close()
pye57/exception.py ADDED
@@ -0,0 +1,4 @@
1
+
2
+
3
+ class PyE57Exception(BaseException):
4
+ pass
pye57/scan_header.py ADDED
@@ -0,0 +1,227 @@
1
+ import numpy as np
2
+ from pyquaternion import Quaternion
3
+
4
+ from pye57 import libe57
5
+ from pye57.utils import get_fields, get_node
6
+
7
+ class ScanHeader:
8
+ """Provides summary statistics for an individual lidar scan in an E57 file.
9
+
10
+ Including the number of points, bounds and pose of the scan.
11
+ """
12
+ def __init__(self, scan_node):
13
+ self.node = scan_node
14
+ points = self.node["points"]
15
+ self.point_fields = get_fields(libe57.StructureNode(points.prototype()))
16
+ self.scan_fields = get_fields(self.node)
17
+
18
+ @classmethod
19
+ def from_data3d(cls, data3d):
20
+ return [cls(scan) for scan in data3d]
21
+
22
+ def has_pose(self):
23
+ return self.node.isDefined("pose")
24
+
25
+ @property
26
+ def point_count(self):
27
+ return self.points.childCount()
28
+
29
+ @property
30
+ def rotation_matrix(self) -> np.array:
31
+ try:
32
+ rotation = self.node["pose"]["rotation"]
33
+ q = Quaternion([e.value() for e in rotation])
34
+ except libe57.E57Exception:
35
+ q = Quaternion()
36
+ return q.rotation_matrix
37
+
38
+ @property
39
+ def rotation(self) -> np.array:
40
+ try:
41
+ rotation = self.node["pose"]["rotation"]
42
+ q = Quaternion([e.value() for e in rotation])
43
+ except libe57.E57Exception:
44
+ q = Quaternion()
45
+ return q.elements
46
+
47
+ @property
48
+ def translation(self):
49
+ try:
50
+ translation_values = [e.value() for e in self.node["pose"]["translation"]]
51
+ except libe57.E57Exception:
52
+ translation_values = [0] * 3
53
+ return np.array(translation_values)
54
+
55
+ def pretty_print(self, node=None, indent=""):
56
+ if node is None:
57
+ node = self.node
58
+ lines = []
59
+ for field in get_fields(node):
60
+ child_node = node[field]
61
+ value = ""
62
+ if hasattr(child_node, "value"):
63
+ value = ": %s" % child_node.value()
64
+ lines.append(indent + str(child_node) + value)
65
+ if isinstance(child_node, libe57.StructureNode):
66
+ lines += self.pretty_print(child_node, indent + " ")
67
+ return lines
68
+
69
+ def __getitem__(self, item):
70
+ return self.node[item]
71
+
72
+ def get_coordinate_system(self, COORDINATE_SYSTEMS):
73
+ if all(x in self.point_fields for x in COORDINATE_SYSTEMS.CARTESIAN.value):
74
+ coordinate_system = COORDINATE_SYSTEMS.CARTESIAN
75
+ elif all(x in self.point_fields for x in COORDINATE_SYSTEMS.SPHERICAL.value):
76
+ coordinate_system = COORDINATE_SYSTEMS.SPHERICAL
77
+ else:
78
+ raise Exception(f"Scans coordinate system not supported, unsupported point field {self.point_fields}")
79
+ return coordinate_system
80
+
81
+ @property
82
+ def guid(self):
83
+ return self["guid"].value()
84
+
85
+ @property
86
+ def temperature(self):
87
+ return self["temperature"].value()
88
+
89
+ @property
90
+ def relativeHumidity(self):
91
+ return self["relativeHumidity"].value()
92
+
93
+ @property
94
+ def atmosphericPressure(self):
95
+ return self["atmosphericPressure"].value()
96
+
97
+ @property
98
+ def indexBounds(self):
99
+ return self["indexBounds"]
100
+
101
+ @property
102
+ def rowMinimum(self):
103
+ return self.indexBounds["rowMinimum"].value()
104
+
105
+ @property
106
+ def rowMaximum(self):
107
+ return self.indexBounds["rowMaximum"].value()
108
+
109
+ @property
110
+ def columnMinimum(self):
111
+ return self.indexBounds["columnMinimum"].value()
112
+
113
+ @property
114
+ def columnMaximum(self):
115
+ return self.indexBounds["columnMaximum"].value()
116
+
117
+ @property
118
+ def returnMinimum(self):
119
+ return self.indexBounds["returnMinimum"].value()
120
+
121
+ @property
122
+ def returnMaximum(self):
123
+ return self.indexBounds["returnMaximum"].value()
124
+
125
+ @property
126
+ def intensityLimits(self):
127
+ return self["intensityLimits"]
128
+
129
+ @property
130
+ def intensityMinimum(self):
131
+ return self.intensityLimits["intensityMinimum"].value()
132
+
133
+ @property
134
+ def intensityMaximum(self):
135
+ return self.intensityLimits["intensityMaximum"].value()
136
+
137
+ @property
138
+ def cartesianBounds(self):
139
+ return self["cartesianBounds"]
140
+
141
+ @property
142
+ def xMinimum(self):
143
+ return self.cartesianBounds["xMinimum"].value()
144
+
145
+ @property
146
+ def xMaximum(self):
147
+ return self.cartesianBounds["xMaximum"].value()
148
+
149
+ @property
150
+ def yMinimum(self):
151
+ return self.cartesianBounds["yMinimum"].value()
152
+
153
+ @property
154
+ def yMaximum(self):
155
+ return self.cartesianBounds["yMaximum"].value()
156
+
157
+ @property
158
+ def zMinimum(self):
159
+ return self.cartesianBounds["zMinimum"].value()
160
+
161
+ @property
162
+ def zMaximum(self):
163
+ return self.cartesianBounds["zMaximum"].value()
164
+
165
+ @property
166
+ def sphericalBounds(self):
167
+ return self["sphericalBounds"]
168
+
169
+ @property
170
+ def rangeMinimum(self):
171
+ return self.sphericalBounds["rangeMinimum"].value()
172
+
173
+ @property
174
+ def rangeMaximum(self):
175
+ return self.sphericalBounds["rangeMaximum"].value()
176
+
177
+ @property
178
+ def elevationMinimum(self):
179
+ return self.sphericalBounds["elevationMinimum"].value()
180
+
181
+ @property
182
+ def elevationMaximum(self):
183
+ return self.sphericalBounds["elevationMaximum"].value()
184
+
185
+ @property
186
+ def azimuthStart(self):
187
+ return self.sphericalBounds["azimuthStart"].value()
188
+
189
+ @property
190
+ def azimuthEnd(self):
191
+ return self.sphericalBounds["azimuthEnd"].value()
192
+
193
+ @property
194
+ def pose(self):
195
+ return self["pose"]
196
+
197
+ @property
198
+ def acquisitionStart(self):
199
+ return self["acquisitionStart"]
200
+
201
+ @property
202
+ def acquisitionStart_dateTimeValue(self):
203
+ return self.acquisitionStart["dateTimeValue"].value()
204
+
205
+ @property
206
+ def acquisitionStart_isAtomicClockReferenced(self):
207
+ return self.acquisitionStart["isAtomicClockReferenced"].value()
208
+
209
+ @property
210
+ def acquisitionEnd(self):
211
+ return self["acquisitionEnd"]
212
+
213
+ @property
214
+ def acquisitionEnd_dateTimeValue(self):
215
+ return self.acquisitionEnd["dateTimeValue"].value()
216
+
217
+ @property
218
+ def acquisitionEnd_isAtomicClockReferenced(self):
219
+ return self.acquisitionEnd["isAtomicClockReferenced"].value()
220
+
221
+ @property
222
+ def pointGroupingSchemes(self):
223
+ return self["pointGroupingSchemes"]
224
+
225
+ @property
226
+ def points(self):
227
+ return self["points"]
pye57/utils.py ADDED
@@ -0,0 +1,117 @@
1
+ from pye57 import libe57
2
+ from pye57.libe57 import NodeType
3
+
4
+ import numpy as np
5
+
6
+ def get_fields(node):
7
+ return [node.get(id_).elementName() for id_ in range(node.childCount())]
8
+
9
+
10
+ def get_node(node, name):
11
+ cast = {
12
+ NodeType.E57_BLOB: libe57.BlobNode,
13
+ NodeType.E57_COMPRESSED_VECTOR: libe57.CompressedVectorNode,
14
+ NodeType.E57_FLOAT: libe57.FloatNode,
15
+ NodeType.E57_INTEGER: libe57.IntegerNode,
16
+ NodeType.E57_SCALED_INTEGER: libe57.ScaledIntegerNode,
17
+ NodeType.E57_STRING: libe57.StringNode,
18
+ NodeType.E57_STRUCTURE: libe57.StructureNode,
19
+ NodeType.E57_VECTOR: libe57.VectorNode
20
+ }
21
+ n = node.get(name)
22
+ return cast[n.type()](n)
23
+
24
+ def convert_spherical_to_cartesian(rae):
25
+ """
26
+ Converts spherical(rae) to cartesian(xyz), where rae = range, azimuth(theta),
27
+ elevation(phi). Where range is in meters and angles are in radians.
28
+
29
+ Reference for formula: http://www.libe57.org/bestCoordinates.html (Note: the
30
+ formula is different from the one online, so please use formula at the above reference)
31
+ """
32
+ range_ = rae[:, :1]
33
+ theta = rae[:, 1:2]
34
+ phi = rae[:, 2:3]
35
+ range_cos_phi = range_ * np.cos(phi)
36
+ return np.concatenate((
37
+ range_cos_phi * np.cos(theta),
38
+ range_cos_phi * np.sin(theta),
39
+ range_ * np.sin(phi)
40
+ ), axis=1)
41
+
42
+
43
+ def copy_node(node, dest_image):
44
+ compressed_node_pairs = []
45
+ blob_node_pairs = []
46
+
47
+ out_node = None
48
+ # 'Element' Types
49
+ if (isinstance(node, libe57.FloatNode)):
50
+ out_node = libe57.FloatNode(
51
+ dest_image,
52
+ value=node.value(),
53
+ precision=node.precision(),
54
+ minimum=node.minimum(),
55
+ maximum=node.maximum())
56
+
57
+ elif (isinstance(node, libe57.IntegerNode)):
58
+ out_node = libe57.IntegerNode(
59
+ dest_image,
60
+ value=node.value(),
61
+ minimum=node.minimum(),
62
+ maximum=node.maximum())
63
+
64
+ elif (isinstance(node, libe57.ScaledIntegerNode)):
65
+ out_node = libe57.ScaledIntegerNode(
66
+ dest_image,
67
+ node.rawValue(),
68
+ minimum=node.minimum(),
69
+ maximum=node.maximum(),
70
+ scale=node.scale(),
71
+ offset=node.offset())
72
+
73
+ elif (isinstance(node, libe57.StringNode)):
74
+ out_node = libe57.StringNode(
75
+ dest_image,
76
+ node.value())
77
+
78
+ elif (isinstance(node, libe57.BlobNode)):
79
+ out_node = libe57.BlobNode(dest_image, node.byteCount())
80
+ blob_node_pairs.append({ 'in': node, 'out': out_node })
81
+
82
+ # 'Container' Types
83
+ elif (isinstance(node, libe57.CompressedVectorNode)):
84
+ in_prototype = libe57.StructureNode(node.prototype())
85
+ out_prototype, _, _ = copy_node(in_prototype, dest_image)
86
+ out_codecs, _, _ = copy_node(node.codecs(), dest_image)
87
+
88
+ out_node = libe57.CompressedVectorNode(dest_image, out_prototype, out_codecs)
89
+
90
+ compressed_node_pairs.append({
91
+ 'in': node,
92
+ 'out': out_node
93
+ })
94
+
95
+ elif isinstance(node, libe57.StructureNode):
96
+ out_node = libe57.StructureNode(dest_image)
97
+ for i in range(node.childCount()):
98
+ in_child = get_node(node, i)
99
+ in_child_name = in_child.elementName()
100
+ out_child, out_child_compressed_node_pairs, out_child_blob_node_pairs = copy_node(in_child, dest_image)
101
+
102
+ out_node.set(in_child_name, out_child)
103
+ compressed_node_pairs.extend(out_child_compressed_node_pairs)
104
+ blob_node_pairs.extend(out_child_blob_node_pairs)
105
+
106
+ elif isinstance(node, libe57.VectorNode):
107
+ out_node = libe57.VectorNode(dest_image, allowHeteroChildren=node.allowHeteroChildren())
108
+ for i in range(node.childCount()):
109
+ in_child = get_node(node, i)
110
+ in_child_name = f'{i}'
111
+ out_child, out_child_compressed_node_pairs, out_child_blob_node_pairs = copy_node(in_child, dest_image)
112
+
113
+ out_node.append(out_child)
114
+ compressed_node_pairs.extend(out_child_compressed_node_pairs)
115
+ blob_node_pairs.extend(out_child_blob_node_pairs)
116
+
117
+ return out_node, compressed_node_pairs, blob_node_pairs
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: pye57
3
+ Version: 0.4.18
4
+ Summary: Python .e57 files reader/writer
5
+ Home-page: https://www.github.com/davidcaron/pye57
6
+ Author: David Caron
7
+ Author-email: dcaron05@gmail.com
8
+ Maintainer: Graham Knapp
9
+ Maintainer-email: graham.knapp@gmail.com
10
+ License: MIT
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: Implementation :: CPython
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy
24
+ Requires-Dist: pyquaternion
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest; extra == "test"
27
+ Dynamic: author
28
+ Dynamic: author-email
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: license
34
+ Dynamic: license-file
35
+ Dynamic: maintainer
36
+ Dynamic: maintainer-email
37
+ Dynamic: provides-extra
38
+ Dynamic: requires-dist
39
+ Dynamic: requires-python
40
+ Dynamic: summary
41
+
42
+
43
+ # pye57
44
+
45
+ [![PyPI](https://img.shields.io/pypi/v/pye57.svg)](https://pypi.org/project/pye57)
46
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pye57.svg)](https://pypi.org/project/pye57)
47
+ ![GitHub](https://img.shields.io/github/actions/workflow/status/davidcaron/pye57/build.yml?branch=master)
48
+
49
+ Python wrapper of [LibE57Format](https://github.com/asmaloney/libE57Format) to read and write .e57 point cloud files
50
+
51
+ ## Example usage
52
+
53
+ ```python
54
+ import numpy as np
55
+ import pye57
56
+
57
+ e57 = pye57.E57("e57_file.e57")
58
+
59
+ # read scan at index 0
60
+ data = e57.read_scan(0)
61
+
62
+ # 'data' is a dictionary with the point types as keys
63
+ assert isinstance(data["cartesianX"], np.ndarray)
64
+ assert isinstance(data["cartesianY"], np.ndarray)
65
+ assert isinstance(data["cartesianZ"], np.ndarray)
66
+
67
+ # other attributes can be read using:
68
+ data = e57.read_scan(0, intensity=True, colors=True, row_column=True)
69
+ assert isinstance(data["cartesianX"], np.ndarray)
70
+ assert isinstance(data["cartesianY"], np.ndarray)
71
+ assert isinstance(data["cartesianZ"], np.ndarray)
72
+ assert isinstance(data["intensity"], np.ndarray)
73
+ assert isinstance(data["colorRed"], np.ndarray)
74
+ assert isinstance(data["colorGreen"], np.ndarray)
75
+ assert isinstance(data["colorBlue"], np.ndarray)
76
+ assert isinstance(data["rowIndex"], np.ndarray)
77
+ assert isinstance(data["columnIndex"], np.ndarray)
78
+
79
+ # the 'read_scan' method filters points using the 'cartesianInvalidState' field
80
+ # if you want to get everything as raw, untransformed data, use:
81
+ data_raw = e57.read_scan_raw(0)
82
+
83
+ # writing is also possible, but only using raw data for now
84
+ with pye57.E57("e57_file_write.e57", mode='w') as e57_write:
85
+ e57_write.write_scan_raw(data_raw)
86
+ # you can specify a header to copy information from
87
+ e57_write.write_scan_raw(data_raw, scan_header=e57.get_header(0))
88
+
89
+ # the ScanHeader object wraps most of the scan information:
90
+ header = e57.get_header(0)
91
+ print(header.point_count)
92
+ print(header.rotation_matrix)
93
+ print(header.translation)
94
+
95
+ # all the header information can be printed using:
96
+ for line in header.pretty_print():
97
+ print(line)
98
+
99
+ # the scan position can be accessed with:
100
+ position_scan_0 = e57.scan_position(0)
101
+
102
+ # the binding is very close to the E57Foundation API
103
+ # you can modify the nodes easily from python
104
+ imf = e57.image_file
105
+ root = imf.root()
106
+ data3d = root["data3D"]
107
+ scan_0 = data3d[0]
108
+ translation_x = scan_0["pose"]["translation"]["x"]
109
+ ```
110
+
111
+ ## Installation
112
+
113
+ On linux, Windows or Apple Silicon:
114
+
115
+ `python -m pip install pye57`
116
+
117
+ On macOS with Intel CPU you can try to build from source (advanced users):
118
+
119
+ ## Building from source (for developers)
120
+
121
+ ### Cloning the repository with required submodule
122
+
123
+ Clone a new repository along with the libe57Format submodule
124
+
125
+ `git clone https://github.com/davidcaron/pye57.git --recursive`
126
+
127
+ If the repository has already been previously cloned, but without the --recursive flag
128
+
129
+ ```Bash
130
+ cd pye57 # go to the cloned repository
131
+ git submodule init # this will initialise the submodules in the repository
132
+ git submodule update # this will update the submodules in the repository
133
+ ```
134
+
135
+ ### Dependencies on Linux
136
+
137
+ Install libxerces-c-dev first.
138
+
139
+ `sudo apt install libxerces-c-dev`
140
+
141
+ ### Dependencies on Windows
142
+
143
+ To get xerces-c, you can either build from source or if you're using conda:
144
+
145
+ `conda install -y xerces-c`
146
+
147
+ ### Dependencies on MacOS
148
+
149
+ To get xerces-c, run:
150
+
151
+ `bash ./scripts/install_xerces_c.sh`
152
+
153
+ ### Run `pip install` from the repo source
154
+
155
+ ```Bash
156
+ cd pye57
157
+ python -m pip install .
158
+ ```
159
+
160
+ ### Uninstalling
161
+
162
+ Use pip again
163
+
164
+ ```Bash
165
+ python -m pip uninstall pye57
166
+ ```
@@ -0,0 +1,13 @@
1
+ pye57/__init__.py,sha256=p-yo66Eblp6mIVKaupuS1E3BQGCNZrI96ApqoFusOi8,92
2
+ pye57/__version__.py,sha256=my_eRKQWrGqOjGcYUQ3l4O21n-LMG22J283Pb5L90kw,23
3
+ pye57/e57.py,sha256=iW8gC3N_4KFsYJtBDrSci7sl4VgO0p8eg9sv0AlMA6U,19674
4
+ pye57/exception.py,sha256=hjNBnDf-PJrBLU7E8aAlWll0aovVITwjJoN9ex4Lx3o,47
5
+ pye57/libe57.cpython-311-x86_64-linux-gnu.so,sha256=BOpRoklK4rjJKYvTXDNuZSOIYcSuHykXiWApjpNDEYI,34145065
6
+ pye57/scan_header.py,sha256=5L9gexIEob94UsDQdNd3cBYn745mbMVQwK4MhTCopqY,6349
7
+ pye57/utils.py,sha256=LvBz7-LW3E9WNGu7c2aurqrXi-iCdorY-VLAEcTKGbU,4181
8
+ pye57.libs/libxerces-c-3-252602d7.2.so,sha256=SfneBo9MezKlqkb-Bnkopuf_A9cqfgFtsdWTEswL9TQ,27006257
9
+ pye57-0.4.18.dist-info/METADATA,sha256=MHMMurcwNKrXVFPArwCBZTL_NqqMHqvbAnQDoyNzmhA,4781
10
+ pye57-0.4.18.dist-info/WHEEL,sha256=3daP3VhxT_uXQ4g9qxjrsHJLh9a9x1Dc522TvcSo1E0,152
11
+ pye57-0.4.18.dist-info/top_level.txt,sha256=xD9HDzQ3BfGMuz1kI2uNKUR0KXcR-RtNEKigrkh48Nk,6
12
+ pye57-0.4.18.dist-info/RECORD,,
13
+ pye57-0.4.18.dist-info/licenses/LICENSE,sha256=fk66gXDC1OVruix0TJ6tGGH6vaP_VOzBPETZ3xxnMsY,1050
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp311-cp311-manylinux_2_27_x86_64
5
+ Tag: cp311-cp311-manylinux_2_28_x86_64
6
+
@@ -0,0 +1,7 @@
1
+ Copyright 2018 David Caron
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ pye57
Binary file