pye57 0.4.19__cp312-cp312-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.
- pye57/__init__.py +3 -0
- pye57/__version__.py +1 -0
- pye57/e57.py +438 -0
- pye57/exception.py +4 -0
- pye57/libe57.cpython-312-x86_64-linux-gnu.so +0 -0
- pye57/scan_header.py +227 -0
- pye57/utils.py +117 -0
- pye57-0.4.19.dist-info/METADATA +166 -0
- pye57-0.4.19.dist-info/RECORD +13 -0
- pye57-0.4.19.dist-info/WHEEL +6 -0
- pye57-0.4.19.dist-info/licenses/LICENSE +7 -0
- pye57-0.4.19.dist-info/top_level.txt +1 -0
- pye57.libs/libxerces-c-3-252602d7.2.so +0 -0
pye57/__init__.py
ADDED
pye57/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.19"
|
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
|
Binary file
|
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.19
|
|
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.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
|
+
Requires-Python: >=3.10
|
|
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
|
+
[](https://pypi.org/project/pye57)
|
|
46
|
+
[](https://pypi.org/project/pye57)
|
|
47
|
+

|
|
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=mfToEOXd_f_mFG0ts1iWNTvKhsjPQqwRFaAmcx61xJo,23
|
|
3
|
+
pye57/e57.py,sha256=iW8gC3N_4KFsYJtBDrSci7sl4VgO0p8eg9sv0AlMA6U,19674
|
|
4
|
+
pye57/exception.py,sha256=hjNBnDf-PJrBLU7E8aAlWll0aovVITwjJoN9ex4Lx3o,47
|
|
5
|
+
pye57/libe57.cpython-312-x86_64-linux-gnu.so,sha256=gpeASmLyKlNhNgskdzpnWpybJePubeYs369RrYwcUag,34329377
|
|
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.19.dist-info/METADATA,sha256=Rk9fVBrgWOHq9ftuM0S-IlSaZiCR0alYOvoD93okmfU,4783
|
|
10
|
+
pye57-0.4.19.dist-info/WHEEL,sha256=1crAxrAH5rUbvWUY1UR0ly3o7KnT1jo0_98V8RY5-FM,152
|
|
11
|
+
pye57-0.4.19.dist-info/top_level.txt,sha256=xD9HDzQ3BfGMuz1kI2uNKUR0KXcR-RtNEKigrkh48Nk,6
|
|
12
|
+
pye57-0.4.19.dist-info/RECORD,,
|
|
13
|
+
pye57-0.4.19.dist-info/licenses/LICENSE,sha256=fk66gXDC1OVruix0TJ6tGGH6vaP_VOzBPETZ3xxnMsY,1050
|
|
@@ -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
|