pye57 0.4.15__cp313-cp313-macosx_11_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.

Potentially problematic release.


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

Binary file
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.15"
pye57/e57.py ADDED
@@ -0,0 +1,422 @@
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
+ if field_name not in SUPPORTED_POINT_FIELDS:
114
+ raise ValueError("Unsupported point field: %s" % field_name)
115
+
116
+ np_array = np.empty(capacity, SUPPORTED_POINT_FIELDS[field_name])
117
+ buffer = libe57.SourceDestBuffer(self.image_file,
118
+ field_name,
119
+ np_array,
120
+ capacity,
121
+ do_conversion,
122
+ do_scaling)
123
+ return np_array, buffer
124
+
125
+ def make_buffers(self, field_names, capacity, do_conversion=True, do_scaling=True):
126
+ data = {}
127
+ buffers = libe57.VectorSourceDestBuffer()
128
+ for field in field_names:
129
+ d, b = self.make_buffer(field, capacity, do_conversion=do_conversion, do_scaling=do_scaling)
130
+ data[field] = d
131
+ buffers.append(b)
132
+ return data, buffers
133
+
134
+ def read_scan_raw(self, index) -> Dict:
135
+ header = self.get_header(index)
136
+ data = {}
137
+ buffers = libe57.VectorSourceDestBuffer()
138
+ for field in header.point_fields:
139
+ np_array, buffer = self.make_buffer(field, header.point_count)
140
+ data[field] = np_array
141
+ buffers.append(buffer)
142
+
143
+ header.points.reader(buffers).read()
144
+
145
+ return data
146
+
147
+ def scan_position(self, index):
148
+ pt = np.array([[0, 0, 0]])
149
+ header = self.get_header(index)
150
+ return self.to_global(pt, header.rotation, header.translation)
151
+
152
+ @staticmethod
153
+ def to_global(points, rotation, translation):
154
+ rotation_matrix = Quaternion(rotation).rotation_matrix
155
+ return (np.dot(rotation_matrix, points.T) + translation.reshape(3, 1)).T
156
+
157
+ def read_scan(self,
158
+ index,
159
+ *,
160
+ intensity=False,
161
+ colors=False,
162
+ row_column=False,
163
+ transform=True,
164
+ ignore_missing_fields=False) -> Dict:
165
+ header = self.get_header(index)
166
+ n_points = header.point_count
167
+
168
+ coordinate_system = header.get_coordinate_system(COORDINATE_SYSTEMS)
169
+ if coordinate_system is COORDINATE_SYSTEMS.CARTESIAN:
170
+ validState = "cartesianInvalidState"
171
+ fields = list(SUPPORTED_CARTESIAN_POINT_FIELDS.keys())
172
+ elif coordinate_system is COORDINATE_SYSTEMS.SPHERICAL:
173
+ validState = "sphericalInvalidState"
174
+ fields = list(SUPPORTED_SPHERICAL_POINT_FIELDS.keys())
175
+ if intensity:
176
+ fields.append("intensity")
177
+ if colors:
178
+ fields.append("colorRed")
179
+ fields.append("colorGreen")
180
+ fields.append("colorBlue")
181
+ if row_column:
182
+ fields.append("rowIndex")
183
+ fields.append("columnIndex")
184
+ fields.append(validState)
185
+
186
+ for field in fields[:]:
187
+ if field not in header.point_fields:
188
+ if ignore_missing_fields:
189
+ fields.remove(field)
190
+ else:
191
+ raise ValueError("Requested to read a field (%s) with is absent from the e57 file. "
192
+ "Consider using 'ignore_missing_fields' to skip it." % field)
193
+
194
+ data, buffers = self.make_buffers(fields, n_points)
195
+ header.points.reader(buffers).read()
196
+
197
+ if validState in data:
198
+ valid = ~data[validState].astype("?")
199
+
200
+ for field in data:
201
+ data[field] = data[field][valid]
202
+
203
+ del data[validState]
204
+
205
+ if transform:
206
+ if coordinate_system is COORDINATE_SYSTEMS.CARTESIAN:
207
+ xyz = np.array([data["cartesianX"], data["cartesianY"], data["cartesianZ"]]).T
208
+ elif coordinate_system is COORDINATE_SYSTEMS.SPHERICAL:
209
+ rae = np.array([data["sphericalRange"], data["sphericalAzimuth"], data["sphericalElevation"]]).T
210
+ # rae to xyz
211
+ xyz = convert_spherical_to_cartesian(rae)
212
+ # translation to global coordinates
213
+ if header.has_pose():
214
+ xyz = self.to_global(xyz, header.rotation, header.translation)
215
+ data["cartesianX"] = xyz[:, 0]
216
+ data["cartesianY"] = xyz[:, 1]
217
+ data["cartesianZ"] = xyz[:, 2]
218
+ return data
219
+
220
+ def write_scan_raw(self, data: Dict, *, name=None, rotation=None, translation=None, scan_header=None):
221
+ for field in data.keys():
222
+ if field not in SUPPORTED_POINT_FIELDS:
223
+ raise ValueError("Unsupported point field: %s" % field)
224
+
225
+ if rotation is None:
226
+ rotation = getattr(scan_header, "rotation", np.array([1, 0, 0, 0]))
227
+
228
+ if translation is None:
229
+ translation = getattr(scan_header, "translation", np.array([0, 0, 0]))
230
+
231
+ if name is None:
232
+ name = getattr(scan_header, "name", "Scan %s" % len(self.data3d))
233
+
234
+ temperature = getattr(scan_header, "temperature", 0)
235
+ relativeHumidity = getattr(scan_header, "relativeHumidity", 0)
236
+ atmosphericPressure = getattr(scan_header, "atmosphericPressure", 0)
237
+
238
+ scan_node = libe57.StructureNode(self.image_file)
239
+ scan_node.set("guid", libe57.StringNode(self.image_file, "{%s}" % uuid.uuid4()))
240
+ scan_node.set("name", libe57.StringNode(self.image_file, name))
241
+ scan_node.set("temperature", libe57.FloatNode(self.image_file, temperature))
242
+ scan_node.set("relativeHumidity", libe57.FloatNode(self.image_file, relativeHumidity))
243
+ scan_node.set("atmosphericPressure", libe57.FloatNode(self.image_file, atmosphericPressure))
244
+ scan_node.set("description", libe57.StringNode(self.image_file, "pye57 v%s" % __version__))
245
+
246
+ n_points = data["cartesianX"].shape[0]
247
+
248
+ ibox = libe57.StructureNode(self.image_file)
249
+ if "rowIndex" in data and "columnIndex" in data:
250
+ min_row = np.min(data["rowIndex"])
251
+ max_row = np.max(data["rowIndex"])
252
+ min_col = np.min(data["columnIndex"])
253
+ max_col = np.max(data["columnIndex"])
254
+ ibox.set("rowMinimum", libe57.IntegerNode(self.image_file, min_row))
255
+ ibox.set("rowMaximum", libe57.IntegerNode(self.image_file, max_row))
256
+ ibox.set("columnMinimum", libe57.IntegerNode(self.image_file, min_col))
257
+ ibox.set("columnMaximum", libe57.IntegerNode(self.image_file, max_col))
258
+ else:
259
+ ibox.set("rowMinimum", libe57.IntegerNode(self.image_file, 0))
260
+ ibox.set("rowMaximum", libe57.IntegerNode(self.image_file, n_points - 1))
261
+ ibox.set("columnMinimum", libe57.IntegerNode(self.image_file, 0))
262
+ ibox.set("columnMaximum", libe57.IntegerNode(self.image_file, 0))
263
+ ibox.set("returnMinimum", libe57.IntegerNode(self.image_file, 0))
264
+ ibox.set("returnMaximum", libe57.IntegerNode(self.image_file, 0))
265
+ scan_node.set("indexBounds", ibox)
266
+
267
+ if "intensity" in data:
268
+ int_min = getattr(scan_header, "intensityMinimum", np.min(data["intensity"]))
269
+ int_max = getattr(scan_header, "intensityMaximum", np.max(data["intensity"]))
270
+ intbox = libe57.StructureNode(self.image_file)
271
+ intbox.set("intensityMinimum", libe57.FloatNode(self.image_file, int_min))
272
+ intbox.set("intensityMaximum", libe57.FloatNode(self.image_file, int_max))
273
+ scan_node.set("intensityLimits", intbox)
274
+
275
+ color = all(c in data for c in ["colorRed", "colorGreen", "colorBlue"])
276
+ if color:
277
+ colorbox = libe57.StructureNode(self.image_file)
278
+ colorbox.set("colorRedMinimum", libe57.IntegerNode(self.image_file, 0))
279
+ colorbox.set("colorRedMaximum", libe57.IntegerNode(self.image_file, 255))
280
+ colorbox.set("colorGreenMinimum", libe57.IntegerNode(self.image_file, 0))
281
+ colorbox.set("colorGreenMaximum", libe57.IntegerNode(self.image_file, 255))
282
+ colorbox.set("colorBlueMinimum", libe57.IntegerNode(self.image_file, 0))
283
+ colorbox.set("colorBlueMaximum", libe57.IntegerNode(self.image_file, 255))
284
+ scan_node.set("colorLimits", colorbox)
285
+
286
+ bbox_node = libe57.StructureNode(self.image_file)
287
+ x, y, z = data["cartesianX"], data["cartesianY"], data["cartesianZ"]
288
+ valid = None
289
+ if "cartesianInvalidState" in data:
290
+ valid = ~data["cartesianInvalidState"].astype("?")
291
+ x, y, z = x[valid], y[valid], z[valid]
292
+ bb_min = np.array([x.min(), y.min(), z.min()])
293
+ bb_max = np.array([x.max(), y.max(), z.max()])
294
+ del valid, x, y, z
295
+
296
+ if scan_header is not None:
297
+ bb_min_scaled = np.array([scan_header.xMinimum, scan_header.yMinimum, scan_header.zMinimum])
298
+ bb_max_scaled = np.array([scan_header.xMaximum, scan_header.yMaximum, scan_header.zMaximum])
299
+ else:
300
+ bb_min_scaled = self.to_global(bb_min.reshape(-1, 3), rotation, translation)[0]
301
+ bb_max_scaled = self.to_global(bb_max.reshape(-1, 3), rotation, translation)[0]
302
+
303
+ bbox_node.set("xMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[0]))
304
+ bbox_node.set("xMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[0]))
305
+ bbox_node.set("yMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[1]))
306
+ bbox_node.set("yMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[1]))
307
+ bbox_node.set("zMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[2]))
308
+ bbox_node.set("zMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[2]))
309
+ scan_node.set("cartesianBounds", bbox_node)
310
+
311
+ if rotation is not None and translation is not None:
312
+ pose_node = libe57.StructureNode(self.image_file)
313
+ scan_node.set("pose", pose_node)
314
+ rotation_node = libe57.StructureNode(self.image_file)
315
+ rotation_node.set("w", libe57.FloatNode(self.image_file, rotation[0]))
316
+ rotation_node.set("x", libe57.FloatNode(self.image_file, rotation[1]))
317
+ rotation_node.set("y", libe57.FloatNode(self.image_file, rotation[2]))
318
+ rotation_node.set("z", libe57.FloatNode(self.image_file, rotation[3]))
319
+ pose_node.set("rotation", rotation_node)
320
+ translation_node = libe57.StructureNode(self.image_file)
321
+ translation_node.set("x", libe57.FloatNode(self.image_file, translation[0]))
322
+ translation_node.set("y", libe57.FloatNode(self.image_file, translation[1]))
323
+ translation_node.set("z", libe57.FloatNode(self.image_file, translation[2]))
324
+ pose_node.set("translation", translation_node)
325
+
326
+ start_datetime = getattr(scan_header, "acquisitionStart_dateTimeValue", 0)
327
+ start_atomic = getattr(scan_header, "acquisitionStart_isAtomicClockReferenced", False)
328
+ end_datetime = getattr(scan_header, "acquisitionEnd_dateTimeValue", 0)
329
+ end_atomic = getattr(scan_header, "acquisitionEnd_isAtomicClockReferenced", False)
330
+ acquisition_start = libe57.StructureNode(self.image_file)
331
+ scan_node.set("acquisitionStart", acquisition_start)
332
+ acquisition_start.set("dateTimeValue", libe57.FloatNode(self.image_file, start_datetime))
333
+ acquisition_start.set("isAtomicClockReferenced", libe57.IntegerNode(self.image_file, start_atomic))
334
+ acquisition_end = libe57.StructureNode(self.image_file)
335
+ scan_node.set("acquisitionEnd", acquisition_end)
336
+ acquisition_end.set("dateTimeValue", libe57.FloatNode(self.image_file, end_datetime))
337
+ acquisition_end.set("isAtomicClockReferenced", libe57.IntegerNode(self.image_file, end_atomic))
338
+
339
+ # todo: pointGroupingSchemes
340
+
341
+ points_prototype = libe57.StructureNode(self.image_file)
342
+
343
+ is_scaled = False
344
+ precision = libe57.E57_DOUBLE if is_scaled else libe57.E57_SINGLE
345
+
346
+ center = (bb_max + bb_min) / 2
347
+
348
+ chunk_size = 5000000
349
+
350
+ x_node = libe57.FloatNode(self.image_file, center[0], precision, bb_min[0], bb_max[0])
351
+ y_node = libe57.FloatNode(self.image_file, center[1], precision, bb_min[1], bb_max[1])
352
+ z_node = libe57.FloatNode(self.image_file, center[2], precision, bb_min[2], bb_max[2])
353
+ points_prototype.set("cartesianX", x_node)
354
+ points_prototype.set("cartesianY", y_node)
355
+ points_prototype.set("cartesianZ", z_node)
356
+
357
+ field_names = ["cartesianX", "cartesianY", "cartesianZ"]
358
+
359
+ if "intensity" in data:
360
+ intensity_min = np.min(data["intensity"])
361
+ intensity_max = np.max(data["intensity"])
362
+ intensity_node = libe57.FloatNode(self.image_file, intensity_min, precision, intensity_min, intensity_max)
363
+ points_prototype.set("intensity", intensity_node)
364
+ field_names.append("intensity")
365
+
366
+ if all(color in data for color in ["colorRed", "colorGreen", "colorBlue"]):
367
+ points_prototype.set("colorRed", libe57.IntegerNode(self.image_file, 0, 0, 255))
368
+ points_prototype.set("colorGreen", libe57.IntegerNode(self.image_file, 0, 0, 255))
369
+ points_prototype.set("colorBlue", libe57.IntegerNode(self.image_file, 0, 0, 255))
370
+ field_names.append("colorRed")
371
+ field_names.append("colorGreen")
372
+ field_names.append("colorBlue")
373
+
374
+ if "rowIndex" in data and "columnIndex" in data:
375
+ min_row = np.min(data["rowIndex"])
376
+ max_row = np.max(data["rowIndex"])
377
+ min_col = np.min(data["columnIndex"])
378
+ max_col = np.max(data["columnIndex"])
379
+ points_prototype.set("rowIndex", libe57.IntegerNode(self.image_file, 0, min_row, max_row))
380
+ field_names.append("rowIndex")
381
+ points_prototype.set("columnIndex", libe57.IntegerNode(self.image_file, 0, min_col, max_col))
382
+ field_names.append("columnIndex")
383
+
384
+ if "cartesianInvalidState" in data:
385
+ min_state = np.min(data["cartesianInvalidState"])
386
+ max_state = np.max(data["cartesianInvalidState"])
387
+ points_prototype.set("cartesianInvalidState", libe57.IntegerNode(self.image_file, 0, min_state, max_state))
388
+ field_names.append("cartesianInvalidState")
389
+
390
+ # other fields
391
+ # // "sphericalRange"
392
+ # // "sphericalAzimuth"
393
+ # // "sphericalElevation"
394
+ # // "timeStamp"
395
+ # // "sphericalInvalidState"
396
+ # // "isColorInvalid"
397
+ # // "isIntensityInvalid"
398
+ # // "isTimeStampInvalid"
399
+
400
+ arrays, buffers = self.make_buffers(field_names, chunk_size)
401
+
402
+ codecs = libe57.VectorNode(self.image_file, True)
403
+ points = libe57.CompressedVectorNode(self.image_file, points_prototype, codecs)
404
+ scan_node.set("points", points)
405
+
406
+ self.data3d.append(scan_node)
407
+
408
+ writer = points.writer(buffers)
409
+
410
+ current_index = 0
411
+ while current_index != n_points:
412
+ current_chunk = min(n_points - current_index, chunk_size)
413
+
414
+ for type_ in SUPPORTED_POINT_FIELDS:
415
+ if type_ in arrays:
416
+ arrays[type_][:current_chunk] = data[type_][current_index:current_index + current_chunk]
417
+
418
+ writer.write(current_chunk)
419
+
420
+ current_index += current_chunk
421
+
422
+ writer.close()
pye57/exception.py ADDED
@@ -0,0 +1,4 @@
1
+
2
+
3
+ class PyE57Exception(BaseException):
4
+ pass
Binary file
Binary file
pye57/scan_header.py ADDED
@@ -0,0 +1,219 @@
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
+ q = Quaternion([e.value() for e in self.node["pose"]["rotation"]])
32
+ return q.rotation_matrix
33
+
34
+ @property
35
+ def rotation(self) -> np.array:
36
+ try:
37
+ rotation = self.node["pose"]["rotation"]
38
+ q = Quaternion([e.value() for e in rotation])
39
+ except KeyError:
40
+ q = Quaternion()
41
+ return q.elements
42
+
43
+ @property
44
+ def translation(self):
45
+ return np.array([e.value() for e in self.node["pose"]["translation"]])
46
+
47
+ def pretty_print(self, node=None, indent=""):
48
+ if node is None:
49
+ node = self.node
50
+ lines = []
51
+ for field in get_fields(node):
52
+ child_node = node[field]
53
+ value = ""
54
+ if hasattr(child_node, "value"):
55
+ value = ": %s" % child_node.value()
56
+ lines.append(indent + str(child_node) + value)
57
+ if isinstance(child_node, libe57.StructureNode):
58
+ lines += self.pretty_print(child_node, indent + " ")
59
+ return lines
60
+
61
+ def __getitem__(self, item):
62
+ return self.node[item]
63
+
64
+ def get_coordinate_system(self, COORDINATE_SYSTEMS):
65
+ if all(x in self.point_fields for x in COORDINATE_SYSTEMS.CARTESIAN.value):
66
+ coordinate_system = COORDINATE_SYSTEMS.CARTESIAN
67
+ elif all(x in self.point_fields for x in COORDINATE_SYSTEMS.SPHERICAL.value):
68
+ coordinate_system = COORDINATE_SYSTEMS.SPHERICAL
69
+ else:
70
+ raise Exception(f"Scans coordinate system not supported, unsupported point field {self.point_fields}")
71
+ return coordinate_system
72
+
73
+ @property
74
+ def guid(self):
75
+ return self["guid"].value()
76
+
77
+ @property
78
+ def temperature(self):
79
+ return self["temperature"].value()
80
+
81
+ @property
82
+ def relativeHumidity(self):
83
+ return self["relativeHumidity"].value()
84
+
85
+ @property
86
+ def atmosphericPressure(self):
87
+ return self["atmosphericPressure"].value()
88
+
89
+ @property
90
+ def indexBounds(self):
91
+ return self["indexBounds"]
92
+
93
+ @property
94
+ def rowMinimum(self):
95
+ return self.indexBounds["rowMinimum"].value()
96
+
97
+ @property
98
+ def rowMaximum(self):
99
+ return self.indexBounds["rowMaximum"].value()
100
+
101
+ @property
102
+ def columnMinimum(self):
103
+ return self.indexBounds["columnMinimum"].value()
104
+
105
+ @property
106
+ def columnMaximum(self):
107
+ return self.indexBounds["columnMaximum"].value()
108
+
109
+ @property
110
+ def returnMinimum(self):
111
+ return self.indexBounds["returnMinimum"].value()
112
+
113
+ @property
114
+ def returnMaximum(self):
115
+ return self.indexBounds["returnMaximum"].value()
116
+
117
+ @property
118
+ def intensityLimits(self):
119
+ return self["intensityLimits"]
120
+
121
+ @property
122
+ def intensityMinimum(self):
123
+ return self.intensityLimits["intensityMinimum"].value()
124
+
125
+ @property
126
+ def intensityMaximum(self):
127
+ return self.intensityLimits["intensityMaximum"].value()
128
+
129
+ @property
130
+ def cartesianBounds(self):
131
+ return self["cartesianBounds"]
132
+
133
+ @property
134
+ def xMinimum(self):
135
+ return self.cartesianBounds["xMinimum"].value()
136
+
137
+ @property
138
+ def xMaximum(self):
139
+ return self.cartesianBounds["xMaximum"].value()
140
+
141
+ @property
142
+ def yMinimum(self):
143
+ return self.cartesianBounds["yMinimum"].value()
144
+
145
+ @property
146
+ def yMaximum(self):
147
+ return self.cartesianBounds["yMaximum"].value()
148
+
149
+ @property
150
+ def zMinimum(self):
151
+ return self.cartesianBounds["zMinimum"].value()
152
+
153
+ @property
154
+ def zMaximum(self):
155
+ return self.cartesianBounds["zMaximum"].value()
156
+
157
+ @property
158
+ def sphericalBounds(self):
159
+ return self["sphericalBounds"]
160
+
161
+ @property
162
+ def rangeMinimum(self):
163
+ return self.sphericalBounds["rangeMinimum"].value()
164
+
165
+ @property
166
+ def rangeMaximum(self):
167
+ return self.sphericalBounds["rangeMaximum"].value()
168
+
169
+ @property
170
+ def elevationMinimum(self):
171
+ return self.sphericalBounds["elevationMinimum"].value()
172
+
173
+ @property
174
+ def elevationMaximum(self):
175
+ return self.sphericalBounds["elevationMaximum"].value()
176
+
177
+ @property
178
+ def azimuthStart(self):
179
+ return self.sphericalBounds["azimuthStart"].value()
180
+
181
+ @property
182
+ def azimuthEnd(self):
183
+ return self.sphericalBounds["azimuthEnd"].value()
184
+
185
+ @property
186
+ def pose(self):
187
+ return self["pose"]
188
+
189
+ @property
190
+ def acquisitionStart(self):
191
+ return self["acquisitionStart"]
192
+
193
+ @property
194
+ def acquisitionStart_dateTimeValue(self):
195
+ return self.acquisitionStart["dateTimeValue"].value()
196
+
197
+ @property
198
+ def acquisitionStart_isAtomicClockReferenced(self):
199
+ return self.acquisitionStart["isAtomicClockReferenced"].value()
200
+
201
+ @property
202
+ def acquisitionEnd(self):
203
+ return self["acquisitionEnd"]
204
+
205
+ @property
206
+ def acquisitionEnd_dateTimeValue(self):
207
+ return self.acquisitionEnd["dateTimeValue"].value()
208
+
209
+ @property
210
+ def acquisitionEnd_isAtomicClockReferenced(self):
211
+ return self.acquisitionEnd["isAtomicClockReferenced"].value()
212
+
213
+ @property
214
+ def pointGroupingSchemes(self):
215
+ return self["pointGroupingSchemes"]
216
+
217
+ @property
218
+ def points(self):
219
+ 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,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,153 @@
1
+ Metadata-Version: 2.1
2
+ Name: pye57
3
+ Version: 0.4.15
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.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy
25
+ Requires-Dist: pyquaternion
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest; extra == "test"
28
+
29
+
30
+ # pye57
31
+
32
+ [![PyPI](https://img.shields.io/pypi/v/pye57.svg)](https://pypi.org/project/pye57)
33
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pye57.svg)](https://pypi.org/project/pye57)
34
+ ![GitHub](https://img.shields.io/github/actions/workflow/status/davidcaron/pye57/build.yml?branch=master)
35
+
36
+ Python wrapper of [LibE57Format](https://github.com/asmaloney/libE57Format) to read and write .e57 point cloud files
37
+
38
+ ## Example usage
39
+
40
+ ```python
41
+ import numpy as np
42
+ import pye57
43
+
44
+ e57 = pye57.E57("e57_file.e57")
45
+
46
+ # read scan at index 0
47
+ data = e57.read_scan(0)
48
+
49
+ # 'data' is a dictionary with the point types as keys
50
+ assert isinstance(data["cartesianX"], np.ndarray)
51
+ assert isinstance(data["cartesianY"], np.ndarray)
52
+ assert isinstance(data["cartesianZ"], np.ndarray)
53
+
54
+ # other attributes can be read using:
55
+ data = e57.read_scan(0, intensity=True, colors=True, row_column=True)
56
+ assert isinstance(data["cartesianX"], np.ndarray)
57
+ assert isinstance(data["cartesianY"], np.ndarray)
58
+ assert isinstance(data["cartesianZ"], np.ndarray)
59
+ assert isinstance(data["intensity"], np.ndarray)
60
+ assert isinstance(data["colorRed"], np.ndarray)
61
+ assert isinstance(data["colorGreen"], np.ndarray)
62
+ assert isinstance(data["colorBlue"], np.ndarray)
63
+ assert isinstance(data["rowIndex"], np.ndarray)
64
+ assert isinstance(data["columnIndex"], np.ndarray)
65
+
66
+ # the 'read_scan' method filters points using the 'cartesianInvalidState' field
67
+ # if you want to get everything as raw, untransformed data, use:
68
+ data_raw = e57.read_scan_raw(0)
69
+
70
+ # writing is also possible, but only using raw data for now
71
+ e57_write = pye57.E57("e57_file_write.e57", mode='w')
72
+ e57_write.write_scan_raw(data_raw)
73
+ # you can specify a header to copy information from
74
+ e57_write.write_scan_raw(data_raw, scan_header=e57.get_header(0))
75
+
76
+ # the ScanHeader object wraps most of the scan information:
77
+ header = e57.get_header(0)
78
+ print(header.point_count)
79
+ print(header.rotation_matrix)
80
+ print(header.translation)
81
+
82
+ # all the header information can be printed using:
83
+ for line in header.pretty_print():
84
+ print(line)
85
+
86
+ # the scan position can be accessed with:
87
+ position_scan_0 = e57.scan_position(0)
88
+
89
+ # the binding is very close to the E57Foundation API
90
+ # you can modify the nodes easily from python
91
+ imf = e57.image_file
92
+ root = imf.root()
93
+ data3d = root["data3D"]
94
+ scan_0 = data3d[0]
95
+ translation_x = scan_0["pose"]["translation"]["x"]
96
+ ```
97
+
98
+ ## Installation
99
+
100
+ On linux, Windows or Apple Silicon:
101
+
102
+ `python -m pip install pye57`
103
+
104
+ On macOS with Intel CPU you can try to build from source (advanced users):
105
+
106
+ ## Building from source (for developers)
107
+
108
+ ### Cloning the repository with required submodule
109
+
110
+ Clone a new repository along with the libe57Format submodule
111
+
112
+ `git clone https://github.com/davidcaron/pye57.git --recursive`
113
+
114
+ If the repository has already been previously cloned, but without the --recursive flag
115
+
116
+ ```Bash
117
+ cd pye57 # go to the cloned repository
118
+ git submodule init # this will initialise the submodules in the repository
119
+ git submodule update # this will update the submodules in the repository
120
+ ```
121
+
122
+ ### Dependencies on Linux
123
+
124
+ Install libxerces-c-dev first.
125
+
126
+ `sudo apt install libxerces-c-dev`
127
+
128
+ ### Dependencies on Windows
129
+
130
+ To get xerces-c, you can either build from source or if you're using conda:
131
+
132
+ `conda install -y xerces-c`
133
+
134
+ ### Dependencies on MacOS
135
+
136
+ To get xerces-c, run:
137
+
138
+ `bash ./scripts/install_xerces_c.sh`
139
+
140
+ ### Run `pip install` from the repo source
141
+
142
+ ```Bash
143
+ cd pye57
144
+ python -m pip install .
145
+ ```
146
+
147
+ ### Uninstalling
148
+
149
+ Use pip again
150
+
151
+ ```Bash
152
+ python -m pip uninstall pye57
153
+ ```
@@ -0,0 +1,14 @@
1
+ pye57/libxerces-c-3.2.dylib,sha256=oHSDt168NG3Ey_nk98fQR24MZIaJQj6TuQcI-PuX2_A,3905272
2
+ pye57/exception.py,sha256=hjNBnDf-PJrBLU7E8aAlWll0aovVITwjJoN9ex4Lx3o,47
3
+ pye57/libe57.cpython-313-darwin.so,sha256=54FolLkbEV_qd7r21Uvy-4BvoC-grQfHT5J34KQ_YOs,1951936
4
+ pye57/__init__.py,sha256=p-yo66Eblp6mIVKaupuS1E3BQGCNZrI96ApqoFusOi8,92
5
+ pye57/__version__.py,sha256=yHOqz5A9VYGVIjRAkE5ZWR9IpLeDo8sygF-I11UMLv0,23
6
+ pye57/e57.py,sha256=q0lsfI19sreOx3YIEFn-lWDaSStezPGwn5lUzxgh20Q,18528
7
+ pye57/utils.py,sha256=LvBz7-LW3E9WNGu7c2aurqrXi-iCdorY-VLAEcTKGbU,4181
8
+ pye57/scan_header.py,sha256=qO1NhT917ST1uLCl2Hq5lbqkh2343VbyHvTWxjkWxXo,6082
9
+ pye57/.dylibs/libxerces-c-3.2.dylib,sha256=XnM1C3kVUXjstRK2ikoanz0vxvuolNsTZcbZpua8PvY,3923408
10
+ pye57-0.4.15.dist-info/RECORD,,
11
+ pye57-0.4.15.dist-info/LICENSE,sha256=fk66gXDC1OVruix0TJ6tGGH6vaP_VOzBPETZ3xxnMsY,1050
12
+ pye57-0.4.15.dist-info/WHEEL,sha256=L5Yzp3Ie3qzhKJYO8ywXtZfSISbcpXt4-U2h3P52Hng,109
13
+ pye57-0.4.15.dist-info/top_level.txt,sha256=xD9HDzQ3BfGMuz1kI2uNKUR0KXcR-RtNEKigrkh48Nk,6
14
+ pye57-0.4.15.dist-info/METADATA,sha256=0fH1_u1j0ddivczthoYmN_y8CajyAN3LOQTjwmtMmko,4506
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.1.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-macosx_11_0_arm64
5
+
@@ -0,0 +1 @@
1
+ pye57