biomedisa 2024.5.14__py3-none-any.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.
- biomedisa/__init__.py +53 -0
- biomedisa/__main__.py +18 -0
- biomedisa/biomedisa_features/DataGenerator.py +299 -0
- biomedisa/biomedisa_features/DataGeneratorCrop.py +121 -0
- biomedisa/biomedisa_features/PredictDataGenerator.py +87 -0
- biomedisa/biomedisa_features/PredictDataGeneratorCrop.py +74 -0
- biomedisa/biomedisa_features/__init__.py +0 -0
- biomedisa/biomedisa_features/active_contour.py +434 -0
- biomedisa/biomedisa_features/amira_to_np/__init__.py +0 -0
- biomedisa/biomedisa_features/amira_to_np/amira_data_stream.py +980 -0
- biomedisa/biomedisa_features/amira_to_np/amira_grammar.py +369 -0
- biomedisa/biomedisa_features/amira_to_np/amira_header.py +290 -0
- biomedisa/biomedisa_features/amira_to_np/amira_helper.py +72 -0
- biomedisa/biomedisa_features/assd.py +167 -0
- biomedisa/biomedisa_features/biomedisa_helper.py +801 -0
- biomedisa/biomedisa_features/create_slices.py +286 -0
- biomedisa/biomedisa_features/crop_helper.py +586 -0
- biomedisa/biomedisa_features/curvop_numba.py +149 -0
- biomedisa/biomedisa_features/django_env.py +172 -0
- biomedisa/biomedisa_features/keras_helper.py +1219 -0
- biomedisa/biomedisa_features/nc_reader.py +179 -0
- biomedisa/biomedisa_features/pid.py +52 -0
- biomedisa/biomedisa_features/process_image.py +253 -0
- biomedisa/biomedisa_features/pycuda_test.py +84 -0
- biomedisa/biomedisa_features/random_walk/__init__.py +0 -0
- biomedisa/biomedisa_features/random_walk/gpu_kernels.py +183 -0
- biomedisa/biomedisa_features/random_walk/pycuda_large.py +826 -0
- biomedisa/biomedisa_features/random_walk/pycuda_large_allx.py +806 -0
- biomedisa/biomedisa_features/random_walk/pycuda_small.py +414 -0
- biomedisa/biomedisa_features/random_walk/pycuda_small_allx.py +493 -0
- biomedisa/biomedisa_features/random_walk/pyopencl_large.py +760 -0
- biomedisa/biomedisa_features/random_walk/pyopencl_small.py +441 -0
- biomedisa/biomedisa_features/random_walk/rw_large.py +390 -0
- biomedisa/biomedisa_features/random_walk/rw_small.py +310 -0
- biomedisa/biomedisa_features/remove_outlier.py +399 -0
- biomedisa/biomedisa_features/split_volume.py +274 -0
- biomedisa/deeplearning.py +519 -0
- biomedisa/interpolation.py +371 -0
- biomedisa/mesh.py +406 -0
- biomedisa-2024.5.14.dist-info/LICENSE +191 -0
- biomedisa-2024.5.14.dist-info/METADATA +306 -0
- biomedisa-2024.5.14.dist-info/RECORD +44 -0
- biomedisa-2024.5.14.dist-info/WHEEL +5 -0
- biomedisa-2024.5.14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,980 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# data_stream
|
3
|
+
|
4
|
+
from collections import UserList
|
5
|
+
import numpy
|
6
|
+
import re
|
7
|
+
import struct
|
8
|
+
import time
|
9
|
+
|
10
|
+
from skimage.measure._find_contours import find_contours
|
11
|
+
from numba import jit
|
12
|
+
|
13
|
+
from .amira_header import AmiraHeader
|
14
|
+
|
15
|
+
# type of data to find in the stream
|
16
|
+
FIND = {
|
17
|
+
'decimal': '\d', # [0-9]
|
18
|
+
'alphanum_': '\w', # [a-aA-Z0-9_]
|
19
|
+
}
|
20
|
+
|
21
|
+
def byterle_decoder(input_data, output_size):
|
22
|
+
"""Python drop-in replacement for compiled equivalent
|
23
|
+
|
24
|
+
:param int output_size: the number of items when ``data`` is uncompressed
|
25
|
+
:param str data: a raw stream of data to be unpacked
|
26
|
+
:return numpy.array output: an array of ``numpy.uint8``
|
27
|
+
"""
|
28
|
+
|
29
|
+
# input_data = struct.unpack('<{}B'.format(len(data)), data)
|
30
|
+
# input_data = numpy.ndarray((len(data),), '<B', data)
|
31
|
+
output = numpy.zeros(output_size, dtype=numpy.uint8)
|
32
|
+
i = 0
|
33
|
+
count = True
|
34
|
+
repeat = False
|
35
|
+
no = None
|
36
|
+
j = 0
|
37
|
+
len_data = len(input_data)
|
38
|
+
while i < len_data:
|
39
|
+
if count:
|
40
|
+
no = input_data[i]
|
41
|
+
if no > 127:
|
42
|
+
no &= 0x7f # 2's complement
|
43
|
+
count = False
|
44
|
+
repeat = True
|
45
|
+
i += 1
|
46
|
+
continue
|
47
|
+
else:
|
48
|
+
i += 1
|
49
|
+
count = False
|
50
|
+
repeat = False
|
51
|
+
continue
|
52
|
+
elif not count:
|
53
|
+
if repeat:
|
54
|
+
value = input_data[i:i + no]
|
55
|
+
repeat = False
|
56
|
+
count = True
|
57
|
+
output[j:j+no] = numpy.array(value)
|
58
|
+
i += no
|
59
|
+
j += no
|
60
|
+
continue
|
61
|
+
elif not repeat:
|
62
|
+
value = input_data[i]
|
63
|
+
output[j:j+no] = value
|
64
|
+
i += 1
|
65
|
+
j += no
|
66
|
+
count = True
|
67
|
+
repeat = False
|
68
|
+
continue
|
69
|
+
|
70
|
+
assert j == output_size
|
71
|
+
return output
|
72
|
+
|
73
|
+
def byterle_encoder(data):
|
74
|
+
if len(data) == 0:
|
75
|
+
return ""
|
76
|
+
|
77
|
+
base = 128
|
78
|
+
max = 127
|
79
|
+
|
80
|
+
output = []
|
81
|
+
no_rep = []
|
82
|
+
i = 0
|
83
|
+
prev = None
|
84
|
+
cur = None
|
85
|
+
cnt = 0
|
86
|
+
len_data = len(data)
|
87
|
+
while i < len_data:
|
88
|
+
cur = data[i]
|
89
|
+
if prev == cur: # repeat
|
90
|
+
if len(no_rep) > 0:
|
91
|
+
output.append(base+len(no_rep))
|
92
|
+
output.extend(no_rep)
|
93
|
+
no_rep = []
|
94
|
+
cnt = 1 # including prev
|
95
|
+
while i < len_data:
|
96
|
+
cur = data[i]
|
97
|
+
if prev == cur:
|
98
|
+
cnt += 1
|
99
|
+
if cnt == max:
|
100
|
+
output.append(cnt)
|
101
|
+
output.append(cur)
|
102
|
+
prev = None
|
103
|
+
cur = None
|
104
|
+
cnt = 0
|
105
|
+
i += 1
|
106
|
+
break
|
107
|
+
else: # end of repeat
|
108
|
+
output.append(cnt)
|
109
|
+
output.append(prev)
|
110
|
+
prev = None
|
111
|
+
cnt = 0
|
112
|
+
break
|
113
|
+
prev = cur
|
114
|
+
i += 1
|
115
|
+
if cnt > 0: # end of file
|
116
|
+
output.append(cnt)
|
117
|
+
output.append(cur)
|
118
|
+
prev = None
|
119
|
+
cur = None
|
120
|
+
cnt = 0
|
121
|
+
else: # no repeat
|
122
|
+
if prev is not None:
|
123
|
+
no_rep.append(prev)
|
124
|
+
if len(no_rep) == max:
|
125
|
+
output.append(base+len(no_rep))
|
126
|
+
output.extend(no_rep)
|
127
|
+
no_rep = []
|
128
|
+
prev = cur
|
129
|
+
i += 1
|
130
|
+
|
131
|
+
if cur is not None:
|
132
|
+
no_rep.append(cur)
|
133
|
+
|
134
|
+
if len(no_rep) > 0:
|
135
|
+
output.append(base+len(no_rep))
|
136
|
+
output.extend(no_rep)
|
137
|
+
no_rep = []
|
138
|
+
|
139
|
+
final_result = bytearray(output)
|
140
|
+
return final_result
|
141
|
+
|
142
|
+
@jit(nopython=True)
|
143
|
+
def numba_array_create(dtype, base_size=1073741824):
|
144
|
+
assert(base_size > 0)
|
145
|
+
new_arr = numpy.zeros(base_size, dtype=dtype)
|
146
|
+
return new_arr
|
147
|
+
|
148
|
+
@jit(nopython=True)
|
149
|
+
def numba_array_copy(arr):
|
150
|
+
assert(arr.size > 0)
|
151
|
+
new_arr = numpy.zeros(arr.size, dtype=arr.dtype)
|
152
|
+
for i in range(arr.size):
|
153
|
+
new_arr[i] = arr[i]
|
154
|
+
return new_arr, new_arr.size
|
155
|
+
|
156
|
+
@jit(nopython=True)
|
157
|
+
def numba_array_resize(arr, base_size=1073741824):
|
158
|
+
assert(base_size > 0)
|
159
|
+
new_arr = numpy.zeros(arr.size+base_size, dtype=arr.dtype)
|
160
|
+
new_arr[:arr.size] = arr
|
161
|
+
return new_arr
|
162
|
+
|
163
|
+
@jit(nopython=True)
|
164
|
+
def numba_array_add(arr, idx, elem, base_size=1073741824):
|
165
|
+
assert(base_size > 0)
|
166
|
+
if idx >= arr.size:
|
167
|
+
arr = numba_array_resize(arr, base_size)
|
168
|
+
arr[idx] = elem
|
169
|
+
idx += 1
|
170
|
+
return arr, idx
|
171
|
+
|
172
|
+
@jit(nopython=True)
|
173
|
+
def numba_array_extend(arr, idx, elems, elems_len, base_size=1073741824):
|
174
|
+
assert(base_size > 0)
|
175
|
+
assert(elems.size <= base_size)
|
176
|
+
if idx+elems.size >= arr.size:
|
177
|
+
arr = numba_array_resize(arr, base_size)
|
178
|
+
for i in range(elems_len):
|
179
|
+
arr[idx+i] = elems[i]
|
180
|
+
idx += elems_len
|
181
|
+
return arr, idx
|
182
|
+
|
183
|
+
@jit(nopython=True)
|
184
|
+
def numba_array_extend_mult(arr, idx, elem, no_elem, base_size=1073741824):
|
185
|
+
assert(base_size > 0)
|
186
|
+
assert(no_elem <= base_size)
|
187
|
+
if idx+no_elem >= arr.size:
|
188
|
+
arr = numba_array_resize(arr, base_size)
|
189
|
+
arr[idx:idx+no_elem] = elem
|
190
|
+
idx += no_elem
|
191
|
+
return arr, idx
|
192
|
+
|
193
|
+
@jit(nopython=True)
|
194
|
+
def byterle_decoder_njit(data, len_data, output_size):
|
195
|
+
"""Python drop-in replacement for compiled equivalent
|
196
|
+
|
197
|
+
:param int output_size: the number of items when ``data`` is uncompressed
|
198
|
+
:param str data: a raw stream of data to be unpacked
|
199
|
+
:return numpy.array output: an array of ``numpy.uint8``
|
200
|
+
"""
|
201
|
+
|
202
|
+
output = numba_array_create(dtype=numpy.uint8)
|
203
|
+
output_idx = 0
|
204
|
+
|
205
|
+
count = True
|
206
|
+
no_repeat = False
|
207
|
+
no = None
|
208
|
+
i = 0
|
209
|
+
while i < len_data:
|
210
|
+
if count:
|
211
|
+
no = data[i]
|
212
|
+
if no > 127:
|
213
|
+
no &= 0x7f # 2's complement
|
214
|
+
no_repeat = True
|
215
|
+
else:
|
216
|
+
no_repeat = False
|
217
|
+
|
218
|
+
i += 1
|
219
|
+
count = False
|
220
|
+
continue
|
221
|
+
elif not count:
|
222
|
+
if no_repeat:
|
223
|
+
no_rep, no_rep_idx = numba_array_copy(data[i:i + no])
|
224
|
+
output, output_idx = numba_array_extend(output, output_idx, no_rep, no_rep_idx)
|
225
|
+
i += no
|
226
|
+
elif not no_repeat:
|
227
|
+
elem = data[i]
|
228
|
+
output, output_idx = numba_array_extend_mult(output, output_idx, elem, no)
|
229
|
+
i += 1
|
230
|
+
|
231
|
+
count = True
|
232
|
+
no_repeat = False
|
233
|
+
continue
|
234
|
+
|
235
|
+
output = output[:output_idx]
|
236
|
+
|
237
|
+
assert output_idx == output_size
|
238
|
+
return output
|
239
|
+
|
240
|
+
@jit(nopython=True)
|
241
|
+
def byterle_encoder_njit(data, len_data):
|
242
|
+
|
243
|
+
BASE = 128
|
244
|
+
MAX = 127
|
245
|
+
|
246
|
+
output = numba_array_create(dtype=numpy.uint8)
|
247
|
+
no_rep = numba_array_create(dtype=numpy.uint8, base_size=MAX)
|
248
|
+
output_idx = 0
|
249
|
+
no_rep_idx = 0
|
250
|
+
|
251
|
+
prev = None
|
252
|
+
cur = None
|
253
|
+
cnt = 0
|
254
|
+
i = 0
|
255
|
+
while i < len_data:
|
256
|
+
cur = data[i]
|
257
|
+
if prev is not None and prev == cur: # repeat
|
258
|
+
if no_rep_idx > 0:
|
259
|
+
output, output_idx = numba_array_add(output, output_idx, BASE+no_rep_idx)
|
260
|
+
output, output_idx = numba_array_extend(output, output_idx, no_rep, no_rep_idx)
|
261
|
+
no_rep_idx = 0
|
262
|
+
|
263
|
+
# find all repeat
|
264
|
+
cnt = 1 # prev
|
265
|
+
while i < len_data:
|
266
|
+
cur = data[i]
|
267
|
+
if prev == cur:
|
268
|
+
cnt += 1
|
269
|
+
if cnt == MAX:
|
270
|
+
output, output_idx = numba_array_add(output, output_idx, cnt)
|
271
|
+
output, output_idx = numba_array_add(output, output_idx, cur)
|
272
|
+
prev = None
|
273
|
+
cur = None
|
274
|
+
cnt = 0
|
275
|
+
i += 1
|
276
|
+
break
|
277
|
+
|
278
|
+
else: # end of repeat
|
279
|
+
output, output_idx = numba_array_add(output, output_idx, cnt)
|
280
|
+
output, output_idx = numba_array_add(output, output_idx, prev)
|
281
|
+
prev = None
|
282
|
+
cnt = 0
|
283
|
+
break
|
284
|
+
|
285
|
+
prev = cur
|
286
|
+
i += 1
|
287
|
+
|
288
|
+
if cnt > 0: # end of file
|
289
|
+
output, output_idx = numba_array_add(output, output_idx, cnt)
|
290
|
+
output, output_idx = numba_array_add(output, output_idx, cur)
|
291
|
+
prev = None
|
292
|
+
cur = None
|
293
|
+
cnt = 0
|
294
|
+
|
295
|
+
else: # no repeat
|
296
|
+
if prev is not None:
|
297
|
+
no_rep, no_rep_idx = numba_array_add(no_rep, no_rep_idx, prev)
|
298
|
+
if no_rep_idx == MAX:
|
299
|
+
output, output_idx = numba_array_add(output, output_idx, BASE+no_rep_idx)
|
300
|
+
output, output_idx = numba_array_extend(output, output_idx, no_rep, no_rep_idx)
|
301
|
+
no_rep_idx = 0
|
302
|
+
|
303
|
+
prev = cur
|
304
|
+
i += 1
|
305
|
+
|
306
|
+
if cur is not None:
|
307
|
+
no_rep, no_rep_idx = numba_array_add(no_rep, no_rep_idx, cur)
|
308
|
+
|
309
|
+
if no_rep_idx > 0:
|
310
|
+
output, output_idx = numba_array_add(output, output_idx, BASE+no_rep_idx)
|
311
|
+
output, output_idx = numba_array_extend(output, output_idx, no_rep, no_rep_idx)
|
312
|
+
no_rep_idx = 0
|
313
|
+
|
314
|
+
output = output[:output_idx]
|
315
|
+
return output
|
316
|
+
|
317
|
+
def hxbyterle_decode(output_size, data):
|
318
|
+
"""Decode HxRLE data stream
|
319
|
+
|
320
|
+
If C-extension is not compiled it will use a (slower) Python equivalent
|
321
|
+
|
322
|
+
:param int output_size: the number of items when ``data`` is uncompressed
|
323
|
+
:param str data: a raw stream of data to be unpacked
|
324
|
+
:return numpy.array output: an array of ``numpy.uint8``
|
325
|
+
"""
|
326
|
+
|
327
|
+
# input_data = struct.unpack('<{}B'.format(len(data)), data)
|
328
|
+
input_data = numpy.ndarray((len(data),), '<B', data)
|
329
|
+
|
330
|
+
output = byterle_decoder_njit(input_data, len(input_data), output_size)
|
331
|
+
|
332
|
+
assert len(output) == output_size
|
333
|
+
return output
|
334
|
+
|
335
|
+
def hxbyterle_encode(data):
|
336
|
+
"""Encode HxRLE data
|
337
|
+
|
338
|
+
:param numpy.array data: an array of ``numpy.uint8``
|
339
|
+
:return str output: packed data stream
|
340
|
+
"""
|
341
|
+
|
342
|
+
buf = data.tobytes()
|
343
|
+
# buf = data.astype('<B').tostring()
|
344
|
+
|
345
|
+
if len(buf) == 0:
|
346
|
+
return ""
|
347
|
+
|
348
|
+
# output = byterle_encoder(buf)
|
349
|
+
output = byterle_encoder_njit(buf, len(buf))
|
350
|
+
output = output.tolist()
|
351
|
+
|
352
|
+
final_output = bytearray(output)
|
353
|
+
return final_output
|
354
|
+
|
355
|
+
|
356
|
+
def hxzip_decode(data_size, data):
|
357
|
+
"""Decode HxZip data stream
|
358
|
+
|
359
|
+
:param int data_size: the number of items when ``data`` is uncompressed
|
360
|
+
:param str data: a raw stream of data to be unpacked
|
361
|
+
:return numpy.array output: an array of ``numpy.uint8``
|
362
|
+
"""
|
363
|
+
import zlib
|
364
|
+
data_stream = zlib.decompress(data)
|
365
|
+
output = numpy.array(struct.unpack('<{}B'.format(len(data_stream)), data_stream), dtype=numpy.uint8)
|
366
|
+
# output = numpy.ndarray((data_size,), '<B', data)
|
367
|
+
|
368
|
+
assert len(output) == data_size
|
369
|
+
return output
|
370
|
+
|
371
|
+
def hxzip_encode(data):
|
372
|
+
"""Encode HxZip data stream
|
373
|
+
|
374
|
+
:param numpy.array data: an array of ``numpy.uint8``
|
375
|
+
:return str output: packed data stream
|
376
|
+
"""
|
377
|
+
import zlib
|
378
|
+
|
379
|
+
buf = data.tobytes()
|
380
|
+
# buf = data.astype('<B').tostring()
|
381
|
+
output = zlib.compress(buf)
|
382
|
+
return output
|
383
|
+
|
384
|
+
def to_numpy_dtype(data_type):
|
385
|
+
|
386
|
+
# assume little endian
|
387
|
+
if data_type == "float":
|
388
|
+
dtype = "<f"
|
389
|
+
elif data_type == "int":
|
390
|
+
dtype = "<i"
|
391
|
+
elif data_type == "uint":
|
392
|
+
dtype = "<I"
|
393
|
+
elif data_type == "short":
|
394
|
+
dtype = "<h"
|
395
|
+
elif data_type == "ushort":
|
396
|
+
dtype = "<H"
|
397
|
+
elif data_type == "long":
|
398
|
+
dtype = "<l"
|
399
|
+
elif data_type == "ulong":
|
400
|
+
dtype = "<L"
|
401
|
+
elif data_type == "byte":
|
402
|
+
# dtype = "<b"
|
403
|
+
dtype = "<B" # changed to unsigned char
|
404
|
+
|
405
|
+
return dtype
|
406
|
+
|
407
|
+
def unpack_binary(data_pointer, definitions, data):
|
408
|
+
"""Unpack binary data using ``struct.unpack``
|
409
|
+
|
410
|
+
:param data_pointer: metadata for the ``data_pointer`` attribute for this data stream
|
411
|
+
:type data_pointer: ``ahds.header.Block``
|
412
|
+
:param definitions: definitions specified in the header
|
413
|
+
:type definitions: ``ahds.header.Block``
|
414
|
+
:param bytes data: raw binary data to be unpacked
|
415
|
+
:return tuple output: unpacked data
|
416
|
+
"""
|
417
|
+
|
418
|
+
if data_pointer.data_dimension:
|
419
|
+
data_dimension = data_pointer.data_dimension
|
420
|
+
else:
|
421
|
+
data_dimension = 1 # if data_dimension is None
|
422
|
+
|
423
|
+
data_type = to_numpy_dtype(data_pointer.data_type)
|
424
|
+
|
425
|
+
# get this streams size from the definitions
|
426
|
+
x, y, z = definitions.Lattice
|
427
|
+
data_length = x * y * z
|
428
|
+
|
429
|
+
# output = numpy.array(struct.unpack('<' + '{}'.format(data_type) * data_length, data)) # assume little-endian
|
430
|
+
output = numpy.ndarray((data_length,), data_type, data)
|
431
|
+
output = output.reshape(data_length, data_dimension)
|
432
|
+
return output
|
433
|
+
|
434
|
+
def pack_binary(data_pointer, definitions, data):
|
435
|
+
|
436
|
+
data_type = to_numpy_dtype(data_pointer.data_type)
|
437
|
+
|
438
|
+
output = data.astype(data_type).tostring()
|
439
|
+
return output
|
440
|
+
|
441
|
+
def unpack_ascii(data):
|
442
|
+
"""Unpack ASCII data using string methods``
|
443
|
+
|
444
|
+
:param data_pointer: metadata for the ``data_pointer`` attribute for this data stream
|
445
|
+
:type data_pointer: ``ahds.header.Block``
|
446
|
+
:param definitions: definitions specified in the header
|
447
|
+
:type definitions: ``ahds.header.Block``
|
448
|
+
:param bytes data: raw binary data to be unpacked
|
449
|
+
:return list output: unpacked data
|
450
|
+
"""
|
451
|
+
# string: split at newlines -> exclude last list item -> strip space from each
|
452
|
+
numstrings = map(lambda s: s.strip(), data.split('\n')[:-1])
|
453
|
+
|
454
|
+
# check if string is digit (integer); otherwise float
|
455
|
+
if len(numstrings) == len(filter(lambda n: n.isdigit(), numstrings)):
|
456
|
+
output = map(int, numstrings)
|
457
|
+
else:
|
458
|
+
output = map(float, numstrings)
|
459
|
+
return output
|
460
|
+
|
461
|
+
def pack_ascii(data):
|
462
|
+
return data.tobytes()
|
463
|
+
|
464
|
+
def encode_data(fmt, dp, defn, data):
|
465
|
+
if fmt == "HxByteRLE":
|
466
|
+
return hxbyterle_encode(data)
|
467
|
+
elif fmt == "HxZip":
|
468
|
+
return hxzip_encode(data)
|
469
|
+
elif fmt == "ASCII":
|
470
|
+
return pack_ascii(data)
|
471
|
+
elif fmt is None: # try to pack data
|
472
|
+
return pack_binary(dp, defn, data)
|
473
|
+
else:
|
474
|
+
return None
|
475
|
+
|
476
|
+
def write_amira(fname, header, data):
|
477
|
+
header = header.tobytes() # convert to byte array
|
478
|
+
header_obj = AmiraHeader.from_str(header)
|
479
|
+
|
480
|
+
raw_header = header_obj.raw_header
|
481
|
+
file_format = header_obj.designation.format
|
482
|
+
data_attr = header_obj.data_pointers
|
483
|
+
definitions = header_obj.definitions
|
484
|
+
|
485
|
+
if file_format.find("BINARY") == -1:
|
486
|
+
raise ValueError("write_amira: unsupported file format %r" % file_format)
|
487
|
+
|
488
|
+
num_stream = len(data)
|
489
|
+
raw_data = []
|
490
|
+
for i in range(num_stream):
|
491
|
+
|
492
|
+
dp = getattr(data_attr, "data_pointer_{}".format(i+1))
|
493
|
+
encoding = getattr(dp, "data_format")
|
494
|
+
size = getattr(dp, "data_length")
|
495
|
+
if size is None:
|
496
|
+
x, y, z = definitions.Lattice
|
497
|
+
size = x * y * z
|
498
|
+
|
499
|
+
raw_str = encode_data(encoding, dp, definitions, data[i])
|
500
|
+
raw_data.append(raw_str)
|
501
|
+
|
502
|
+
new_size = len(raw_str)
|
503
|
+
if new_size != size and encoding is not None: # update header for new size
|
504
|
+
enc_size = '@'+str(i+1)+'('+encoding+','+str(size)+')'
|
505
|
+
new_enc_size = '@'+str(i+1)+'('+encoding+','+str(new_size)+')'
|
506
|
+
raw_header = raw_header.replace(enc_size.encode("utf-8"), new_enc_size.encode("utf-8"))
|
507
|
+
|
508
|
+
with open(fname, 'wb') as f:
|
509
|
+
f.write(raw_header)
|
510
|
+
|
511
|
+
for i in range(num_stream):
|
512
|
+
f.write(b"\n@%d\n" % (i+1))
|
513
|
+
f.write(raw_data[i])
|
514
|
+
f.write(b'\n')
|
515
|
+
|
516
|
+
class Image(object):
|
517
|
+
"""Encapsulates individual images"""
|
518
|
+
def __init__(self, z, array):
|
519
|
+
self.z = z
|
520
|
+
self.__array = array
|
521
|
+
self.__byte_values = set(self.__array.flatten().tolist())
|
522
|
+
@property
|
523
|
+
def byte_values(self):
|
524
|
+
return self.__byte_values
|
525
|
+
@property
|
526
|
+
def array(self):
|
527
|
+
"""Accessor to underlying array data"""
|
528
|
+
return self.__array
|
529
|
+
def equalise(self):
|
530
|
+
"""Increase the dynamic range of the image"""
|
531
|
+
multiplier = 255 // len(self.__byte_values)
|
532
|
+
return self.__array * multiplier
|
533
|
+
@property
|
534
|
+
def as_contours(self):
|
535
|
+
"""A dictionary of lists of contours keyed by byte_value"""
|
536
|
+
contours = dict()
|
537
|
+
for byte_value in self.__byte_values:
|
538
|
+
if byte_value == 0:
|
539
|
+
continue
|
540
|
+
mask = (self.__array == byte_value) * 255
|
541
|
+
found_contours = find_contours(mask, 254, fully_connected='high') # a list of array
|
542
|
+
contours[byte_value] = ContourSet(found_contours)
|
543
|
+
return contours
|
544
|
+
@property
|
545
|
+
def as_segments(self):
|
546
|
+
return {self.z: self.as_contours}
|
547
|
+
def show(self):
|
548
|
+
"""Display the image"""
|
549
|
+
with_matplotlib = True
|
550
|
+
try:
|
551
|
+
import matplotlib.pyplot as plt
|
552
|
+
except RuntimeError:
|
553
|
+
import skimage.io as io
|
554
|
+
with_matplotlib = False
|
555
|
+
|
556
|
+
if with_matplotlib:
|
557
|
+
equalised_img = self.equalise()
|
558
|
+
|
559
|
+
_, ax = plt.subplots()
|
560
|
+
|
561
|
+
ax.imshow(equalised_img, cmap='gray')
|
562
|
+
|
563
|
+
import random
|
564
|
+
|
565
|
+
for contour_set in self.as_contours.itervalues():
|
566
|
+
r, g, b = random.random(), random.random(), random.random()
|
567
|
+
[ax.plot(contour[:,1], contour[:,0], linewidth=2, color=(r,g,b,1)) for contour in contour_set]
|
568
|
+
|
569
|
+
ax.axis('image')
|
570
|
+
ax.set_xticks([])
|
571
|
+
ax.set_yticks([])
|
572
|
+
|
573
|
+
plt.show()
|
574
|
+
else:
|
575
|
+
io.imshow(self.equalise())
|
576
|
+
io.show()
|
577
|
+
def __repr__(self):
|
578
|
+
return "<Image with dimensions {}>".format(self.array.shape)
|
579
|
+
def __str__(self):
|
580
|
+
return "<Image with dimensions {}>".format(self.array.shape)
|
581
|
+
|
582
|
+
|
583
|
+
class ImageSet(UserList):
|
584
|
+
"""Encapsulation for set of ``Image`` objects"""
|
585
|
+
def __getitem__(self, index):
|
586
|
+
return Image(index, self.data[index])
|
587
|
+
@property
|
588
|
+
def segments(self):
|
589
|
+
"""A dictionary of lists of contours keyed by z-index"""
|
590
|
+
segments = dict()
|
591
|
+
for i in xrange(len(self)):
|
592
|
+
image = self[i]
|
593
|
+
for z, contour in image.as_segments.iteritems():
|
594
|
+
for byte_value, contour_set in contour.iteritems():
|
595
|
+
if byte_value not in segments:
|
596
|
+
segments[byte_value] = dict()
|
597
|
+
if z not in segments[byte_value]:
|
598
|
+
segments[byte_value][z] = contour_set
|
599
|
+
else:
|
600
|
+
segments[byte_value][z] += contour_set
|
601
|
+
|
602
|
+
return segments
|
603
|
+
def __repr__(self):
|
604
|
+
return "<ImageSet with {} images>".format(len(self))
|
605
|
+
|
606
|
+
|
607
|
+
class ContourSet(UserList):
|
608
|
+
"""Encapsulation for a set of ``Contour`` objects"""
|
609
|
+
def __getitem__(self, index):
|
610
|
+
return Contour(index, self.data[index])
|
611
|
+
def __repr__(self):
|
612
|
+
string = "{} with {} contours".format(self.__class__, len(self))
|
613
|
+
return string
|
614
|
+
|
615
|
+
|
616
|
+
class Contour(object):
|
617
|
+
"""Encapsulates the array representing a contour"""
|
618
|
+
def __init__(self, z, array):
|
619
|
+
self.z = z
|
620
|
+
self.__array = array
|
621
|
+
def __len__(self):
|
622
|
+
return len(self.__array)
|
623
|
+
def __iter__(self):
|
624
|
+
return iter(self.__array)
|
625
|
+
@staticmethod
|
626
|
+
def string_repr(self):
|
627
|
+
string = "<Contour at z={} with {} points>".format(self.z, len(self))
|
628
|
+
return string
|
629
|
+
def __repr__(self):
|
630
|
+
return self.string_repr(self)
|
631
|
+
def __str__(self):
|
632
|
+
return self.string_repr(self)
|
633
|
+
|
634
|
+
|
635
|
+
class AmiraDataStream(object):
|
636
|
+
"""Base class for all Amira DataStreams"""
|
637
|
+
match = None
|
638
|
+
regex = None
|
639
|
+
bytes_per_datatype = 4
|
640
|
+
dimension = 1
|
641
|
+
datatype = None
|
642
|
+
find_type = FIND['decimal']
|
643
|
+
def __init__(self, amira_header, data_pointer, stream_data):
|
644
|
+
self.__amira_header = amira_header
|
645
|
+
self.__data_pointer = data_pointer
|
646
|
+
self.__stream_data = stream_data
|
647
|
+
self.__decoded_length = 0
|
648
|
+
@property
|
649
|
+
def header(self):
|
650
|
+
"""An :py:class:``ahds.header.AmiraHeader`` object"""
|
651
|
+
return self.__amira_header
|
652
|
+
@property
|
653
|
+
def data_pointer(self):
|
654
|
+
"""The data pointer for this data stream"""
|
655
|
+
return self.__data_pointer
|
656
|
+
@property
|
657
|
+
def stream_data(self):
|
658
|
+
"""All the raw data from the file"""
|
659
|
+
return self.__stream_data
|
660
|
+
@property
|
661
|
+
def encoded_data(self):
|
662
|
+
"""Encoded raw data in this stream"""
|
663
|
+
return None
|
664
|
+
@property
|
665
|
+
def decoded_data(self):
|
666
|
+
"""Decoded data for this stream"""
|
667
|
+
return None
|
668
|
+
@property
|
669
|
+
def decoded_length(self):
|
670
|
+
"""The length of the decoded stream data in relevant units e.g. tuples, integers (not bytes)"""
|
671
|
+
return self.__decoded_length
|
672
|
+
@decoded_length.setter
|
673
|
+
def decoded_length(self, value):
|
674
|
+
self.__decoded_length = value
|
675
|
+
def __repr__(self):
|
676
|
+
return "{} object of {:,} bytes".format(self.__class__, len(self.stream_data))
|
677
|
+
|
678
|
+
|
679
|
+
class AmiraMeshDataStream(AmiraDataStream):
|
680
|
+
"""Class encapsulating an AmiraMesh/Avizo data stream"""
|
681
|
+
last_stream = False
|
682
|
+
match = b'stream'
|
683
|
+
def __init__(self, *args, **kwargs):
|
684
|
+
if self.last_stream:
|
685
|
+
self.regex = b'\n@{}\n(?P<%s>.*)' % self.match
|
686
|
+
else:
|
687
|
+
self.regex = b'\n@{}\n(?P<%s>.*)\n@{}' % self.match
|
688
|
+
super(AmiraMeshDataStream, self).__init__(*args, **kwargs)
|
689
|
+
if hasattr(self.header.definitions, 'Lattice'):
|
690
|
+
X, Y, Z = self.header.definitions.Lattice
|
691
|
+
data_size = X * Y * Z
|
692
|
+
self.decoded_length = data_size
|
693
|
+
elif hasattr(self.header.definitions, 'Vertices'):
|
694
|
+
self.decoded_length = None
|
695
|
+
elif self.header.parameters.ContentType == "\"HxSpreadSheet\"":
|
696
|
+
pass
|
697
|
+
elif self.header.parameters.ContentType == "\"SurfaceField\",":
|
698
|
+
pass
|
699
|
+
else:
|
700
|
+
raise ValueError("Unable to determine data size")
|
701
|
+
@property
|
702
|
+
def encoded_data(self):
|
703
|
+
i = self.data_pointer.data_index
|
704
|
+
regex = self.regex.replace(b'{}', b'%d')
|
705
|
+
cnt = regex.count(b"%d")
|
706
|
+
if cnt == 1:
|
707
|
+
regex = regex % (i)
|
708
|
+
elif cnt == 2:
|
709
|
+
regex = regex % (i, i+1)
|
710
|
+
else:
|
711
|
+
return None
|
712
|
+
m = re.search(regex, self.stream_data, flags=re.S)
|
713
|
+
return m.group(str(self.match).strip("b'"))
|
714
|
+
@property
|
715
|
+
def decoded_data(self):
|
716
|
+
if self.data_pointer.data_format == "HxByteRLE":
|
717
|
+
return hxbyterle_decode(self.decoded_length, self.encoded_data)
|
718
|
+
elif self.data_pointer.data_format == "HxZip":
|
719
|
+
return hxzip_decode(self.decoded_length, self.encoded_data)
|
720
|
+
elif self.header.designation.format == "ASCII":
|
721
|
+
return unpack_ascii(self.encoded_data)
|
722
|
+
elif self.data_pointer.data_format is None: # try to unpack data
|
723
|
+
return unpack_binary(self.data_pointer, self.header.definitions, self.encoded_data)
|
724
|
+
else:
|
725
|
+
return None
|
726
|
+
def get_format(self):
|
727
|
+
return self.data_pointer.data_format
|
728
|
+
def to_images(self):
|
729
|
+
if hasattr(self.header.definitions, 'Lattice'):
|
730
|
+
X, Y, Z = self.header.definitions.Lattice
|
731
|
+
else:
|
732
|
+
raise ValueError("Unable to determine data size")
|
733
|
+
image_data = self.decoded_data.reshape(Z, Y, X)
|
734
|
+
|
735
|
+
imgs = ImageSet(image_data[:])
|
736
|
+
return imgs
|
737
|
+
def to_volume(self):
|
738
|
+
"""Return a 3D volume of the data"""
|
739
|
+
if hasattr(self.header.definitions, "Lattice"):
|
740
|
+
X, Y, Z = self.header.definitions.Lattice
|
741
|
+
else:
|
742
|
+
raise ValueError("Unable to determine data size")
|
743
|
+
|
744
|
+
volume = self.decoded_data.reshape(Z, Y, X)
|
745
|
+
return volume
|
746
|
+
|
747
|
+
|
748
|
+
class AmiraHxSurfaceDataStream(AmiraDataStream):
|
749
|
+
"""Base class for all HyperSurface data streams that inherits from ``AmiraDataStream``"""
|
750
|
+
def __init__(self, *args, **kwargs):
|
751
|
+
self.regex = r"%s (?P<%s>%s+)\n" % (self.match, self.match.lower(), self.find_type)
|
752
|
+
super(AmiraHxSurfaceDataStream, self).__init__(*args, **kwargs)
|
753
|
+
self.__match = re.search(self.regex, self.stream_data)
|
754
|
+
self.__name = None
|
755
|
+
self.__count = None
|
756
|
+
self.__start_offset = None
|
757
|
+
self.__end_offset = None
|
758
|
+
@property
|
759
|
+
def name(self):
|
760
|
+
return self.__name
|
761
|
+
@name.setter
|
762
|
+
def name(self, value):
|
763
|
+
self.__name = value
|
764
|
+
@property
|
765
|
+
def count(self):
|
766
|
+
return self.__count
|
767
|
+
@count.setter
|
768
|
+
def count(self, value):
|
769
|
+
self.__count = value
|
770
|
+
@property
|
771
|
+
def start_offset(self):
|
772
|
+
return self.__start_offset
|
773
|
+
@start_offset.setter
|
774
|
+
def start_offset(self, value):
|
775
|
+
self.__start_offset = value
|
776
|
+
@property
|
777
|
+
def end_offset(self):
|
778
|
+
return self.__end_offset
|
779
|
+
@end_offset.setter
|
780
|
+
def end_offset(self, value):
|
781
|
+
self.__end_offset = value
|
782
|
+
@property
|
783
|
+
def match_object(self):
|
784
|
+
return self.__match
|
785
|
+
def __str__(self):
|
786
|
+
return """\
|
787
|
+
\r{} object
|
788
|
+
\r\tname: {}
|
789
|
+
\r\tcount: {}
|
790
|
+
\r\tstart_offset: {}
|
791
|
+
\r\tend_offset: {}
|
792
|
+
\r\tmatch_object: {}""".format(
|
793
|
+
self.__class__,
|
794
|
+
self.name,
|
795
|
+
self.count,
|
796
|
+
self.start_offset,
|
797
|
+
self.end_offset,
|
798
|
+
self.match_object,
|
799
|
+
)
|
800
|
+
|
801
|
+
|
802
|
+
class VoidDataStream(AmiraHxSurfaceDataStream):
|
803
|
+
def __init__(self, *args, **kwargs):
|
804
|
+
super(VoidDataStream, self).__init__(*args, **kwargs)
|
805
|
+
@property
|
806
|
+
def encoded_data(self):
|
807
|
+
return []
|
808
|
+
@property
|
809
|
+
def decoded_data(self):
|
810
|
+
return []
|
811
|
+
|
812
|
+
|
813
|
+
class NamedDataStream(VoidDataStream):
|
814
|
+
find_type = FIND['alphanum_']
|
815
|
+
def __init__(self, *args, **kwargs):
|
816
|
+
super(NamedDataStream, self).__init__(*args, **kwargs)
|
817
|
+
self.name = self.match_object.group(self.match.lower())
|
818
|
+
|
819
|
+
|
820
|
+
class ValuedDataStream(VoidDataStream):
|
821
|
+
def __init__(self, *args, **kwargs):
|
822
|
+
super(ValuedDataStream, self).__init__(*args, **kwargs)
|
823
|
+
self.count = int(self.match_object.group(self.match.lower()))
|
824
|
+
|
825
|
+
|
826
|
+
class LoadedDataStream(AmiraHxSurfaceDataStream):
|
827
|
+
def __init__(self, *args, **kwargs):
|
828
|
+
super(LoadedDataStream, self).__init__(*args, **kwargs)
|
829
|
+
self.count = int(self.match_object.group(self.match.lower()))
|
830
|
+
self.start_offset = self.match_object.end()
|
831
|
+
self.end_offset = max([self.start_offset, self.start_offset + self.count * (self.bytes_per_datatype * self.dimension)])
|
832
|
+
@property
|
833
|
+
def encoded_data(self):
|
834
|
+
return self.stream_data[self.start_offset:self.end_offset]
|
835
|
+
@property
|
836
|
+
def decoded_data(self):
|
837
|
+
points = struct.unpack('>' + ((self.datatype * self.dimension) * self.count), self.encoded_data)
|
838
|
+
x, y, z = (points[::3], points[1::3], points[2::3])
|
839
|
+
return zip(x, y, z)
|
840
|
+
|
841
|
+
|
842
|
+
class VerticesDataStream(LoadedDataStream):
|
843
|
+
match = "Vertices"
|
844
|
+
datatype = 'f'
|
845
|
+
dimension = 3
|
846
|
+
|
847
|
+
|
848
|
+
class NBranchingPointsDataStream(ValuedDataStream):
|
849
|
+
match = "NBranchingPoints"
|
850
|
+
|
851
|
+
|
852
|
+
class NVerticesOnCurvesDataStream(ValuedDataStream):
|
853
|
+
match = "NVerticesOnCurves"
|
854
|
+
|
855
|
+
|
856
|
+
class BoundaryCurvesDataStream(ValuedDataStream):
|
857
|
+
match = "BoundaryCurves"
|
858
|
+
|
859
|
+
|
860
|
+
class PatchesInnerRegionDataStream(NamedDataStream):
|
861
|
+
match = "InnerRegion"
|
862
|
+
|
863
|
+
|
864
|
+
class PatchesOuterRegionDataStream(NamedDataStream):
|
865
|
+
match = "OuterRegion"
|
866
|
+
|
867
|
+
|
868
|
+
class PatchesBoundaryIDDataStream(ValuedDataStream):
|
869
|
+
match = "BoundaryID"
|
870
|
+
|
871
|
+
|
872
|
+
class PatchesBranchingPointsDataStream(ValuedDataStream):
|
873
|
+
match = "BranchingPoints"
|
874
|
+
|
875
|
+
|
876
|
+
class PatchesTrianglesDataStream(LoadedDataStream):
|
877
|
+
match = "Triangles"
|
878
|
+
datatype = 'i'
|
879
|
+
dimension = 3
|
880
|
+
|
881
|
+
|
882
|
+
class PatchesDataStream(LoadedDataStream):
|
883
|
+
match = "Patches"
|
884
|
+
def __init__(self, *args, **kwargs):
|
885
|
+
super(PatchesDataStream, self).__init__(*args, **kwargs)
|
886
|
+
self.__patches = dict()
|
887
|
+
for _ in xrange(self.count):
|
888
|
+
# in order of appearance
|
889
|
+
inner_region = PatchesInnerRegionDataStream(self.header, None, self.stream_data[self.start_offset:])
|
890
|
+
outer_region = PatchesOuterRegionDataStream(self.header, None, self.stream_data[self.start_offset:])
|
891
|
+
boundary_id = PatchesBoundaryIDDataStream(self.header, None, self.stream_data[self.start_offset:])
|
892
|
+
branching_points = PatchesBranchingPointsDataStream(self.header, None, self.stream_data[self.start_offset:])
|
893
|
+
triangles = PatchesTrianglesDataStream(self.header, None, self.stream_data[self.start_offset:])
|
894
|
+
patch = {
|
895
|
+
'InnerRegion':inner_region,
|
896
|
+
'OuterRegion':outer_region,
|
897
|
+
'BoundaryID':boundary_id,
|
898
|
+
'BranchingPoints':branching_points,
|
899
|
+
'Triangles':triangles,
|
900
|
+
}
|
901
|
+
if inner_region.name not in self.__patches:
|
902
|
+
self.__patches[inner_region.name] = [patch]
|
903
|
+
else:
|
904
|
+
self.__patches[inner_region.name] += [patch]
|
905
|
+
# start searching from the end of the last search
|
906
|
+
self.start_offset = self.__patches[inner_region.name][-1]['Triangles'].end_offset
|
907
|
+
self.end_offset = None
|
908
|
+
def __iter__(self):
|
909
|
+
return iter(self.__patches.keys())
|
910
|
+
def __getitem__(self, index):
|
911
|
+
return self.__patches[index]
|
912
|
+
def __len__(self):
|
913
|
+
return len(self.__patches)
|
914
|
+
@property
|
915
|
+
def encoded_data(self):
|
916
|
+
return None
|
917
|
+
@property
|
918
|
+
def decoded_data(self):
|
919
|
+
return None
|
920
|
+
|
921
|
+
|
922
|
+
class DataStreams(object):
|
923
|
+
"""Class to encapsulate all the above functionality"""
|
924
|
+
def __init__(self, fn, header, *args, **kwargs):
|
925
|
+
# private attrs
|
926
|
+
self.__fn = fn # property
|
927
|
+
if header is None:
|
928
|
+
self.__amira_header = AmiraHeader.from_file(fn) # property
|
929
|
+
else:
|
930
|
+
self.__amira_header = header
|
931
|
+
self.__data_streams = dict()
|
932
|
+
self.__filetype = None
|
933
|
+
self.__stream_data = None
|
934
|
+
self.__data_streams = self.__configure()
|
935
|
+
def __configure(self):
|
936
|
+
with open(self.__fn, 'rb') as f:
|
937
|
+
self.__stream_data = f.read() #.strip(b'\n')
|
938
|
+
if self.__amira_header.designation.filetype == "AmiraMesh" or self.__amira_header.designation.filetype == "Avizo":
|
939
|
+
self.__filetype = self.__amira_header.designation.filetype
|
940
|
+
i = 0
|
941
|
+
while i < len(self.__amira_header.data_pointers.attrs) - 1: # refactor
|
942
|
+
data_pointer = getattr(self.__amira_header.data_pointers, 'data_pointer_{}'.format(i + 1))
|
943
|
+
self.__data_streams[i + 1] = AmiraMeshDataStream(self.__amira_header, data_pointer, self.__stream_data)
|
944
|
+
i += 1
|
945
|
+
AmiraMeshDataStream.last_stream = True
|
946
|
+
data_pointer = getattr(self.__amira_header.data_pointers, 'data_pointer_{}'. format(i + 1))
|
947
|
+
self.__data_streams[i + 1] = AmiraMeshDataStream(self.__amira_header, data_pointer, self.__stream_data)
|
948
|
+
# reset AmiraMeshDataStream.last_stream
|
949
|
+
AmiraMeshDataStream.last_stream = False
|
950
|
+
elif self.__amira_header.designation.filetype == "HyperSurface":
|
951
|
+
self.__filetype = "HyperSurface"
|
952
|
+
if self.__amira_header.designation.format == "BINARY":
|
953
|
+
self.__data_streams['Vertices'] = VerticesDataStream(self.__amira_header, None, self.__stream_data)
|
954
|
+
self.__data_streams['NBranchingPoints'] = NBranchingPointsDataStream(self.__amira_header, None, self.__stream_data)
|
955
|
+
self.__data_streams['NVerticesOnCurves'] = NVerticesOnCurvesDataStream(self.__amira_header, None, self.__stream_data)
|
956
|
+
self.__data_streams['BoundaryCurves'] = BoundaryCurvesDataStream(self.__amira_header, None, self.__stream_data)
|
957
|
+
self.__data_streams['Patches'] = PatchesDataStream(self.__amira_header, None, self.__stream_data)
|
958
|
+
elif self.__amira_header.designation.format == "ASCII":
|
959
|
+
self.__data_streams['Vertices'] = VerticesDataStream(self.__amira_header, None, self.__stream_data)
|
960
|
+
return self.__data_streams
|
961
|
+
@property
|
962
|
+
def file(self): return self.__fn
|
963
|
+
@property
|
964
|
+
def header(self): return self.__amira_header
|
965
|
+
@property
|
966
|
+
def stream_data(self): return self.__stream_data
|
967
|
+
@property
|
968
|
+
def filetype(self): return self.__filetype
|
969
|
+
def __iter__(self):
|
970
|
+
return iter(self.__data_streams.values())
|
971
|
+
def __len__(self):
|
972
|
+
return len(self.__data_streams)
|
973
|
+
def __getitem__(self, key):
|
974
|
+
return self.__data_streams[key]
|
975
|
+
def __repr__(self):
|
976
|
+
return "{} object with {} stream(s): {}".format(
|
977
|
+
self.__class__,
|
978
|
+
len(self),
|
979
|
+
", ".join(map(str, self.__data_streams.keys())),
|
980
|
+
)
|