labdata 0.0.3__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.
- labdata/__init__.py +17 -0
- labdata/cli.py +499 -0
- labdata/compute/__init__.py +27 -0
- labdata/compute/ec2.py +198 -0
- labdata/compute/ephys.py +469 -0
- labdata/compute/pose.py +281 -0
- labdata/compute/schedulers.py +194 -0
- labdata/compute/singularity.py +95 -0
- labdata/compute/utils.py +561 -0
- labdata/copy.py +351 -0
- labdata/rules/__init__.py +78 -0
- labdata/rules/ephys.py +188 -0
- labdata/rules/imaging.py +618 -0
- labdata/rules/utils.py +290 -0
- labdata/s3.py +317 -0
- labdata/schema/__init__.py +24 -0
- labdata/schema/ephys.py +547 -0
- labdata/schema/general.py +647 -0
- labdata/schema/histology.py +309 -0
- labdata/schema/onephoton.py +93 -0
- labdata/schema/procedures.py +102 -0
- labdata/schema/tasks.py +66 -0
- labdata/schema/twophoton.py +142 -0
- labdata/schema/utils.py +25 -0
- labdata/schema/video.py +243 -0
- labdata/stacks.py +182 -0
- labdata/utils.py +598 -0
- labdata/widgets.py +412 -0
- labdata-0.0.3.dist-info/METADATA +42 -0
- labdata-0.0.3.dist-info/RECORD +36 -0
- labdata-0.0.3.dist-info/WHEEL +5 -0
- labdata-0.0.3.dist-info/entry_points.txt +2 -0
- labdata-0.0.3.dist-info/licenses/LICENSE +674 -0
- labdata-0.0.3.dist-info/top_level.txt +2 -0
- labdata_frontend/Home.py +39 -0
- labdata_frontend/__init__.py +0 -0
labdata/rules/imaging.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
from .utils import *
|
|
2
|
+
|
|
3
|
+
class FixedBrainRule(UploadRule):
|
|
4
|
+
def __init__(self, job_id, prefs = None):
|
|
5
|
+
super(FixedBrainRule,self).__init__(job_id = job_id, prefs = prefs)
|
|
6
|
+
self.rule_name = 'fixed_brain'
|
|
7
|
+
self.metadata = dict(num_channels = None,
|
|
8
|
+
um_per_pixel = None,
|
|
9
|
+
hardware = 'unknown',
|
|
10
|
+
width = None,
|
|
11
|
+
height = None)
|
|
12
|
+
self.max_concurrent = 2
|
|
13
|
+
def parse_metadata(self,data):
|
|
14
|
+
'''
|
|
15
|
+
Reads metadata from a MultifolderTiffStack
|
|
16
|
+
'''
|
|
17
|
+
from tifffile import TiffFile
|
|
18
|
+
tf = TiffFile(data.filenames[0][0])
|
|
19
|
+
metadata = tf.ome_metadata
|
|
20
|
+
um_per_pixel = []
|
|
21
|
+
for t in metadata.split('\n'):
|
|
22
|
+
if 'Pixels' in t:
|
|
23
|
+
for p in t.strip('>').split(' '):
|
|
24
|
+
if 'PhysicalSize' in p:
|
|
25
|
+
um_per_pixel.append(float(p.split('=')[1].split('"')[1]))
|
|
26
|
+
self.metadata['um_per_pixel'] = um_per_pixel[::-1]
|
|
27
|
+
|
|
28
|
+
if 'UltraII' in metadata:
|
|
29
|
+
self.metadata['hardware'] = 'lightsheet UltraII'
|
|
30
|
+
|
|
31
|
+
self.metadata['num_channels'] = len(data.filenames)
|
|
32
|
+
self.metadata['height'] = data.shape[-2]
|
|
33
|
+
self.metadata['width'] = data.shape[-1]
|
|
34
|
+
|
|
35
|
+
def _apply_rule(self):
|
|
36
|
+
files_to_compress = list(filter(lambda x: ('.ome.tif' in x),
|
|
37
|
+
self.src_paths.src_path.values))
|
|
38
|
+
local_path = self.prefs['local_paths'][0]
|
|
39
|
+
local_path = Path(local_path)
|
|
40
|
+
|
|
41
|
+
new_files = []
|
|
42
|
+
full_file_paths = [local_path/f for f in files_to_compress]
|
|
43
|
+
channel_folders = np.sort(np.unique([p.parent for p in full_file_paths]))
|
|
44
|
+
data = MultifolderTiffStack(channel_folders=channel_folders)
|
|
45
|
+
|
|
46
|
+
compressed_file = channel_folders[0].parent.with_suffix('.zarr.zip')
|
|
47
|
+
# read the first file and get metadata
|
|
48
|
+
self.parse_metadata(data)
|
|
49
|
+
# compression to zarr.zip
|
|
50
|
+
self.set_job_status(job_status = 'WORKING',
|
|
51
|
+
job_log = datetime.now().strftime('Compressing data %Y %m %d %H:%M:%S'),
|
|
52
|
+
job_waiting = 0)
|
|
53
|
+
z1 = compress_imaging_stack(data,
|
|
54
|
+
compressed_file,
|
|
55
|
+
chunksize = 4,
|
|
56
|
+
compression = 'zstd',
|
|
57
|
+
clevel = 6,
|
|
58
|
+
shuffle = 1,
|
|
59
|
+
filters = [])
|
|
60
|
+
new_files.append(str(compressed_file).replace(str(local_path),'').strip(pathlib.os.sep))
|
|
61
|
+
if len(new_files):
|
|
62
|
+
self._handle_processed_and_src_paths(files_to_compress, new_files)
|
|
63
|
+
return self.src_paths
|
|
64
|
+
# need to implement post-upload for lightsheet.
|
|
65
|
+
def _post_upload(self):
|
|
66
|
+
if not self.dataset_key is None:
|
|
67
|
+
from ..schema import FixedBrain
|
|
68
|
+
if len(self.inserted_files) > 0:
|
|
69
|
+
storage = self.inserted_files[0]['storage']
|
|
70
|
+
filenames = [f['file_path'] for f in self.inserted_files if f['file_path'].endswith('.zarr.zip')]
|
|
71
|
+
if not len(filenames):
|
|
72
|
+
print('Could not find zarr compressed stack.')
|
|
73
|
+
FixedBrain().insert1(dict(self.dataset_key,
|
|
74
|
+
file_path = filenames[0],
|
|
75
|
+
storage = storage,
|
|
76
|
+
**self.metadata),allow_direct_insert=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OnePhotonRule(UploadRule):
|
|
80
|
+
def __init__(self, job_id, prefs = None):
|
|
81
|
+
super(OnePhotonRule,self).__init__(job_id = job_id, prefs = prefs)
|
|
82
|
+
self.rule_name = 'one_photon'
|
|
83
|
+
self.imaging_metadata = None
|
|
84
|
+
self.max_concurrent = 2
|
|
85
|
+
|
|
86
|
+
def _apply_rule(self):
|
|
87
|
+
files_to_compress = list(filter(lambda x: ('.dat' in x),
|
|
88
|
+
self.src_paths.src_path.values))
|
|
89
|
+
# TODO: make this work for tiff files also
|
|
90
|
+
local_path = self.prefs['local_paths'][0]
|
|
91
|
+
local_path = Path(local_path)
|
|
92
|
+
if len(files_to_compress):
|
|
93
|
+
if len(files_to_compress)>1:
|
|
94
|
+
raise(ValueError(f'Can only handle one file {files_to_compress}'))
|
|
95
|
+
new_files = []
|
|
96
|
+
for f in files_to_compress:
|
|
97
|
+
filename = local_path/f
|
|
98
|
+
data = mmap_wfield_binary(filename)
|
|
99
|
+
compressed_file = filename.with_suffix('.zarr.zip')
|
|
100
|
+
# compression to zarr.zip
|
|
101
|
+
self.set_job_status(job_status = 'WORKING',
|
|
102
|
+
job_log = datetime.now().strftime('Compressing data %Y %m %d %H:%M:%S'),
|
|
103
|
+
job_waiting = 0)
|
|
104
|
+
z1 = compress_imaging_stack(data,
|
|
105
|
+
compressed_file,
|
|
106
|
+
chunksize = 16,
|
|
107
|
+
compression = 'zstd',
|
|
108
|
+
clevel = 6,
|
|
109
|
+
shuffle = 1,
|
|
110
|
+
filters = [])
|
|
111
|
+
# pass the file path without the "local_path"!
|
|
112
|
+
new_files.append(str(compressed_file).replace(str(local_path),'').strip(pathlib.os.sep))
|
|
113
|
+
if len(new_files):
|
|
114
|
+
self._handle_processed_and_src_paths(files_to_compress, new_files)
|
|
115
|
+
return self.src_paths
|
|
116
|
+
# need to implement post-upload for widefield imaging.
|
|
117
|
+
def _post_upload(self):
|
|
118
|
+
if not self.dataset_key is None:
|
|
119
|
+
insert_widefield_dataset(self.dataset_key,local_paths=[self.local_path])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TwoPhotonRule(UploadRule):
|
|
123
|
+
def __init__(self, job_id,prefs = None):
|
|
124
|
+
super(TwoPhotonRule,self).__init__(job_id = job_id, prefs = prefs)
|
|
125
|
+
self.rule_name = 'two_photon'
|
|
126
|
+
self.recording_metadata = None
|
|
127
|
+
self.planes_metadata = []
|
|
128
|
+
self.max_concurrent = 2
|
|
129
|
+
|
|
130
|
+
def _apply_rule(self):
|
|
131
|
+
# compress ap files or lf files.
|
|
132
|
+
files_to_compress = list(filter(lambda x: ('.sbx' in x),
|
|
133
|
+
self.src_paths.src_path.values))
|
|
134
|
+
if not len(files_to_compress):
|
|
135
|
+
print('There are no sbx files.')
|
|
136
|
+
files_to_compress = list(filter(lambda x: ('.tif' in x),
|
|
137
|
+
self.src_paths.src_path.values))
|
|
138
|
+
if len(files_to_compress):
|
|
139
|
+
raise(ValueError('Tiff processing not implemented for this rule.'))
|
|
140
|
+
# TODO: make this work for tiff files also
|
|
141
|
+
local_path = self.prefs['local_paths'][0]
|
|
142
|
+
local_path = Path(local_path)
|
|
143
|
+
|
|
144
|
+
if len(files_to_compress):
|
|
145
|
+
if len(files_to_compress)>1:
|
|
146
|
+
raise(ValueError(f'Can only handle one file {files_to_compress}'))
|
|
147
|
+
new_files = []
|
|
148
|
+
for f in files_to_compress:
|
|
149
|
+
compressed_file = self.process_sbx(local_path/f)
|
|
150
|
+
# pass the file path without the "local_path"!
|
|
151
|
+
new_files.append(str(compressed_file).replace(str(local_path),'').strip(pathlib.os.sep))
|
|
152
|
+
if len(new_files):
|
|
153
|
+
self._handle_processed_and_src_paths(files_to_compress, new_files)
|
|
154
|
+
return self.src_paths
|
|
155
|
+
|
|
156
|
+
def process_sbx(self,sbxfile):
|
|
157
|
+
try:
|
|
158
|
+
from sbxreader import sbx_memmap
|
|
159
|
+
except:
|
|
160
|
+
msg = f'Could not import SBXREADER: "pip install sbxreader" on {pref["hostname"]}'
|
|
161
|
+
print(msg)
|
|
162
|
+
self.set_job_status(job_status = 'FAILED',job_log = msg,job_waiting = 0)
|
|
163
|
+
sbx = sbx_memmap(sbxfile)
|
|
164
|
+
compressed_file = sbxfile.with_suffix('.zarr.zip')
|
|
165
|
+
z1 = compress_imaging_stack(sbx,
|
|
166
|
+
compressed_file,
|
|
167
|
+
chunksize = 128,
|
|
168
|
+
compression = 'blosc2',
|
|
169
|
+
clevel = 6,
|
|
170
|
+
shuffle = 1)
|
|
171
|
+
nframes,nplanes,nchannels,H,W = z1.shape
|
|
172
|
+
|
|
173
|
+
pmt_gain = [sbx.metadata['pmt0_gain']]
|
|
174
|
+
if nchannels>1:
|
|
175
|
+
pmt_gain+= [sbx.metadata['pmt1_gain']],
|
|
176
|
+
|
|
177
|
+
self.recording_metadata = dict(n_planes = nplanes,
|
|
178
|
+
n_channels = nchannels,
|
|
179
|
+
n_frames = nframes,
|
|
180
|
+
width = W,
|
|
181
|
+
height = H,
|
|
182
|
+
magnification = sbx.metadata['magnification'],
|
|
183
|
+
objective_angle = sbx.metadata['stage_angle'],
|
|
184
|
+
objective = sbx.metadata['objective'],
|
|
185
|
+
um_per_pixel = np.array([sbx.metadata['um_per_pixel_x'],
|
|
186
|
+
sbx.metadata['um_per_pixel_y']]),
|
|
187
|
+
frame_rate = sbx.metadata['frame_rate']/nchannels,
|
|
188
|
+
scanning_mode = sbx.metadata['scanning_mode'],
|
|
189
|
+
pmt_gain = np.array(pmt_gain),
|
|
190
|
+
imaging_software = f"scanbox_{sbx.metadata['scanbox_version']}")
|
|
191
|
+
for iplane in range(nplanes):
|
|
192
|
+
depth = sbx.metadata['stage_pos'][-1]
|
|
193
|
+
if nplanes > 1:
|
|
194
|
+
depth += sbx.metadata['etl_pos'][iplane] - sbx.metadata['etl_pos'][0]
|
|
195
|
+
|
|
196
|
+
self.planes_metadata.append(dict(plane_num = iplane,
|
|
197
|
+
plane_depth = depth))
|
|
198
|
+
return compressed_file
|
|
199
|
+
|
|
200
|
+
def _post_upload(self):
|
|
201
|
+
if not self.dataset_key is None:
|
|
202
|
+
from ..schema import TwoPhoton
|
|
203
|
+
if len(self.inserted_files) > 0:
|
|
204
|
+
storage = self.inserted_files[0]['storage']
|
|
205
|
+
filenames = [f['file_path'] for f in self.inserted_files if f['file_path'].endswith('.zarr.zip')]
|
|
206
|
+
if not len(filenames):
|
|
207
|
+
print('Could not find zarr compressed stack.')
|
|
208
|
+
TwoPhoton().insert1(dict(self.dataset_key,
|
|
209
|
+
file_path = filenames[0],
|
|
210
|
+
storage = storage,
|
|
211
|
+
**self.recording_metadata),allow_direct_insert=True)
|
|
212
|
+
TwoPhoton.Plane().insert([dict(self.dataset_key,**d) for d in self.planes_metadata],allow_direct_insert=True)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class MiniscopeRule(UploadRule):
|
|
216
|
+
def __init__(self, job_id, prefs = None):
|
|
217
|
+
super(MiniscopeRule,self).__init__(job_id = job_id, prefs = prefs)
|
|
218
|
+
self.rule_name = 'miniscope'
|
|
219
|
+
self.imaging_metadata = None
|
|
220
|
+
self.max_concurrent = 4
|
|
221
|
+
|
|
222
|
+
def _apply_rule(self):
|
|
223
|
+
from natsort import natsorted
|
|
224
|
+
files_to_compress = natsorted([str(s) for s in list(filter(lambda x: ('.avi' in x),
|
|
225
|
+
self.src_paths.src_path.values))])
|
|
226
|
+
local_path = self.prefs['local_paths'][0]
|
|
227
|
+
local_path = Path(local_path)
|
|
228
|
+
|
|
229
|
+
new_files = []
|
|
230
|
+
files_to_process = [local_path/f for f in files_to_compress]
|
|
231
|
+
# open the video stack
|
|
232
|
+
from ..stacks import VideoStack
|
|
233
|
+
data = VideoStack(files_to_process) # this does not support multiple channels at the moment.
|
|
234
|
+
compressed_file = (Path(files_to_process[0]).parent/'miniscope_stack').with_suffix('.zarr.zip')
|
|
235
|
+
self.set_job_status(job_status = 'WORKING',
|
|
236
|
+
job_log = datetime.now().strftime('Compressing data %Y %m %d %H:%M:%S'),
|
|
237
|
+
job_waiting = 0)
|
|
238
|
+
z1 = compress_imaging_stack(data,
|
|
239
|
+
compressed_file,
|
|
240
|
+
chunksize = 512,
|
|
241
|
+
compression = 'zstd',
|
|
242
|
+
clevel = 6,
|
|
243
|
+
shuffle = 1,
|
|
244
|
+
filters = [])
|
|
245
|
+
new_files.append(str(compressed_file).replace(str(local_path),'').strip(pathlib.os.sep))
|
|
246
|
+
if len(new_files):
|
|
247
|
+
self._handle_processed_and_src_paths(files_to_compress, new_files)
|
|
248
|
+
return self.src_paths
|
|
249
|
+
|
|
250
|
+
def _post_upload(self):
|
|
251
|
+
if not self.dataset_key is None:
|
|
252
|
+
insert_miniscope_dataset(self.dataset_key, local_paths = [self.local_path])
|
|
253
|
+
|
|
254
|
+
############################################################################################################
|
|
255
|
+
############################################################################################################
|
|
256
|
+
|
|
257
|
+
def insert_miniscope_dataset(key, local_paths = None, skip_duplicates = False):
|
|
258
|
+
|
|
259
|
+
from labdata.schema import Dataset, DatasetEvents, Miniscope, File
|
|
260
|
+
if len(Miniscope & key):
|
|
261
|
+
from warnings import warn
|
|
262
|
+
warn(f'Dataset {key} was already inserted.')
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
filedict = (File & (Dataset.DataFiles & key) & 'file_path LIKE "%.zarr.zip"').proj().fetch1()
|
|
266
|
+
datafile = (File & (Dataset.DataFiles & key) & 'file_path LIKE "%.zarr.zip"').get(local_paths = local_paths)[0]
|
|
267
|
+
|
|
268
|
+
from ..utils import open_zarr
|
|
269
|
+
dat = open_zarr(datafile)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
metafile = (File & (Dataset.DataFiles & key) & 'file_path LIKE "%metaData.json"').get(local_paths = local_paths)[0]
|
|
273
|
+
tsfile = (File & (Dataset.DataFiles & key) & 'file_path LIKE "%timeStamps.csv"').get(local_paths = local_paths)[0]
|
|
274
|
+
orifile = (File & (Dataset.DataFiles & key) & 'file_path LIKE "%headOrientation.csv"').get(local_paths = local_paths)
|
|
275
|
+
# read the metadata
|
|
276
|
+
with open(metafile,'r') as d:
|
|
277
|
+
metadata = json.load(d)
|
|
278
|
+
|
|
279
|
+
frame_rate = metadata['frameRate']
|
|
280
|
+
if type(frame_rate) is str: # the frame rate changed in different versions
|
|
281
|
+
frame_rate = float(metadata['frameRate'].strip('FPS'))
|
|
282
|
+
gain = metadata['gain'] # the gain changed in differnet versions..
|
|
283
|
+
if type(gain) is str:
|
|
284
|
+
if gain == 'Low':
|
|
285
|
+
gain = 1
|
|
286
|
+
elif gain == 'High':
|
|
287
|
+
gain = 10
|
|
288
|
+
else:
|
|
289
|
+
gain = None
|
|
290
|
+
print('Could not read gain')
|
|
291
|
+
leds = [k for k in metadata.keys() if 'led' in k]
|
|
292
|
+
miniscope_dict = dict(key,
|
|
293
|
+
n_channels = len(leds),
|
|
294
|
+
n_frames = dat.shape[0],
|
|
295
|
+
width = dat.shape[1],
|
|
296
|
+
height = dat.shape[2],
|
|
297
|
+
device = metadata['deviceType'],
|
|
298
|
+
frame_rate = float(frame_rate),
|
|
299
|
+
sensor_gain = gain,
|
|
300
|
+
power = [metadata[k] for k in leds],
|
|
301
|
+
lens_tuning = metadata['ewl'],
|
|
302
|
+
**filedict)
|
|
303
|
+
# read the timestamps
|
|
304
|
+
timestamps = pd.read_csv(tsfile)
|
|
305
|
+
del dat
|
|
306
|
+
|
|
307
|
+
dataseteventsdict = [dict((Dataset.proj() & key).fetch1(),
|
|
308
|
+
stream_name = 'miniscope')]
|
|
309
|
+
dataseteventsstreams = [dict(dataseteventsdict[0],
|
|
310
|
+
event_name = 'clock',
|
|
311
|
+
event_timestamps = timestamps['Time Stamp (ms)'].values/1000.,
|
|
312
|
+
event_values = timestamps['Frame Number'].values)]
|
|
313
|
+
if len(orifile):
|
|
314
|
+
head = pd.read_csv(orifile[0])
|
|
315
|
+
for k in ['qw','qx','qy','qz']:
|
|
316
|
+
dataseteventsstreams.append(dict(dataseteventsdict[0],
|
|
317
|
+
event_name = k,
|
|
318
|
+
event_timestamps = head['Time Stamp (ms)'].values/1000.,
|
|
319
|
+
event_values = head[k].values))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
DatasetEvents.insert(dataseteventsdict, allow_direct_insert=True, skip_duplicates=skip_duplicates)
|
|
323
|
+
DatasetEvents.Digital.insert(dataseteventsstreams, allow_direct_insert=True, skip_duplicates=skip_duplicates)
|
|
324
|
+
Miniscope.insert1(miniscope_dict, allow_direct_insert=True, skip_duplicates = skip_duplicates)
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def compress_imaging_stack(stack, filename,
|
|
329
|
+
chunksize = 256,
|
|
330
|
+
compression = 'blosc2',
|
|
331
|
+
clevel = 6,
|
|
332
|
+
shuffle = 1,
|
|
333
|
+
filters = [],
|
|
334
|
+
zarr_format = 2,
|
|
335
|
+
scratch_path = None,
|
|
336
|
+
check_dataset = True):
|
|
337
|
+
'''
|
|
338
|
+
stack is in shape [nframes,nchan,H,W]
|
|
339
|
+
|
|
340
|
+
Typical use case for two photon datasets:
|
|
341
|
+
- blosc2 compression clevel 6, shuffle 1, no filters
|
|
342
|
+
this will take ~10min/25Gb and the result is a file 77% of the original file.
|
|
343
|
+
Typical use case for one photon datasets:
|
|
344
|
+
- blosc2 compression clevel 6, shuffle 1, no filters
|
|
345
|
+
Typical use for lightsheet imaging:
|
|
346
|
+
- blosc2 compression clevel 6, shuffle 1, no filters
|
|
347
|
+
|
|
348
|
+
TODO: implement a way of changing the chunksize of the inner dimensions..
|
|
349
|
+
TODO: implement a way to skip the zip
|
|
350
|
+
|
|
351
|
+
'''
|
|
352
|
+
|
|
353
|
+
import zarr
|
|
354
|
+
import string
|
|
355
|
+
from tqdm import tqdm
|
|
356
|
+
from zipfile import ZipFile
|
|
357
|
+
from pathlib import Path
|
|
358
|
+
import numcodecs
|
|
359
|
+
filt = []
|
|
360
|
+
if 'delta' in filters:
|
|
361
|
+
from numcodecs import Delta
|
|
362
|
+
#raise NotImplementedError('Filters are not implemented because of incompatibility issues with zarr v3.')
|
|
363
|
+
filt += [Delta(dtype=stack.dtype)]
|
|
364
|
+
if compression in ['zstd','lz4hc','lz4']:
|
|
365
|
+
if zarr_format == 3:
|
|
366
|
+
compressor = zarr.codecs.BloscCodec(cname = compression, clevel = clevel, shuffle = 'shuffle')
|
|
367
|
+
else: # zarr v2 is smaller and faster at least Feb 2025..
|
|
368
|
+
from numcodecs import Blosc
|
|
369
|
+
zarr_format = 2
|
|
370
|
+
compressor = Blosc(cname = compression, clevel = clevel, shuffle = shuffle)
|
|
371
|
+
elif compression == 'blosc2':
|
|
372
|
+
from imagecodecs.numcodecs import Blosc2
|
|
373
|
+
numcodecs.register_codec(Blosc2)
|
|
374
|
+
compressor = Blosc2(level=clevel, shuffle = shuffle)
|
|
375
|
+
zarr_format = 2
|
|
376
|
+
else:
|
|
377
|
+
raise NotImplementedError(f'Compression {compression} not implemented')
|
|
378
|
+
|
|
379
|
+
if scratch_path is None:
|
|
380
|
+
if 'scratch_path' in prefs:
|
|
381
|
+
scratch_path = Path(prefs['scratch_path'])
|
|
382
|
+
if scratch_path is None:
|
|
383
|
+
scratch_path = Path('.')
|
|
384
|
+
|
|
385
|
+
rand = ''.join(np.random.choice([s for s in string.ascii_lowercase + string.digits],9))
|
|
386
|
+
tmppath = Path(scratch_path/f'temporary_zarr_{rand}.zarr')
|
|
387
|
+
if len(stack.shape) == 4:
|
|
388
|
+
chunks = (chunksize, 1,*stack.shape[2:])
|
|
389
|
+
elif len(stack.shape) == 3:
|
|
390
|
+
chunks = (chunksize, *stack.shape[1:])
|
|
391
|
+
else:
|
|
392
|
+
raise(ValueError('Only 3d or 4d stacks are supported at the moment.'))
|
|
393
|
+
if zarr_format == 2:
|
|
394
|
+
z1 = zarr.open(tmppath, mode='w', shape = stack.shape,
|
|
395
|
+
chunks = chunks, dtype = stack.dtype,
|
|
396
|
+
compressor = compressor, filters = filt, zarr_format = 2)
|
|
397
|
+
elif zarr_format == 3:
|
|
398
|
+
z1 = zarr.create_array(store=tmppath,
|
|
399
|
+
shape=stack.shape, dtype=stack.dtype, chunks=chunks, compressors=compressor)
|
|
400
|
+
else:
|
|
401
|
+
raise NotImplementedError(f'The array format {zarr_format} is not implemented.')
|
|
402
|
+
for s in tqdm(chunk_indices(len(stack),chunksize),desc = 'Compressing to zarr:'):
|
|
403
|
+
z1[s[0]:s[1]] = np.array(stack[s[0]:s[1]])
|
|
404
|
+
with ZipFile(filename,'w') as z:
|
|
405
|
+
tmp = list(tmppath.rglob('*'))
|
|
406
|
+
tmp = list(filter(lambda x: x.is_file(),tmp)) # for compat with zarr3
|
|
407
|
+
[z.write(t,arcname=t.relative_to(tmppath)) for t in tqdm(tmp, desc='Saving to zip:')]
|
|
408
|
+
# delete the temporary
|
|
409
|
+
from shutil import rmtree
|
|
410
|
+
rmtree(tmppath)
|
|
411
|
+
# open the new array
|
|
412
|
+
z1 = open_zarr(filename,mode = 'r')
|
|
413
|
+
if check_dataset:
|
|
414
|
+
# check the new array
|
|
415
|
+
for s in tqdm(chunk_indices(len(stack),chunksize),desc = 'Checking data:'):
|
|
416
|
+
if not np.all(z1[s[0]:s[1]] == np.array(stack[s[0]:s[1]])):
|
|
417
|
+
raise(ValueError(f"Datasets are not equivalent, compression failed {filename}. "))
|
|
418
|
+
return z1
|
|
419
|
+
|
|
420
|
+
def _parse_wfield_fname(fname,lastidx=None, dtype = 'uint16', shape = None, sep = '_'):
|
|
421
|
+
'''
|
|
422
|
+
Gets the data type and the shape from the filename
|
|
423
|
+
This is a helper function to use in load_dat.
|
|
424
|
+
|
|
425
|
+
out = _parse_wfield_fname(fname)
|
|
426
|
+
|
|
427
|
+
With out default to:
|
|
428
|
+
out = dict(dtype=dtype, shape = shape, fnum = None)
|
|
429
|
+
|
|
430
|
+
this is from wfield - jcouto
|
|
431
|
+
'''
|
|
432
|
+
fn = os.path.splitext(os.path.basename(str(fname)))[0]
|
|
433
|
+
fnsplit = fn.split(sep)
|
|
434
|
+
fnum = None
|
|
435
|
+
if lastidx is None:
|
|
436
|
+
# find the datatype first (that is the first dtype string from last)
|
|
437
|
+
lastidx = -1
|
|
438
|
+
idx = np.where([not f.isnumeric() for f in fnsplit])[0]
|
|
439
|
+
for i in idx[::-1]:
|
|
440
|
+
try:
|
|
441
|
+
dtype = np.dtype(fnsplit[i])
|
|
442
|
+
lastidx = i
|
|
443
|
+
except TypeError:
|
|
444
|
+
pass
|
|
445
|
+
if dtype is None:
|
|
446
|
+
dtype = np.dtype(fnsplit[lastidx])
|
|
447
|
+
# further split in those before and after lastidx
|
|
448
|
+
before = [f for f in fnsplit[:lastidx] if f.isdigit()]
|
|
449
|
+
after = [f for f in fnsplit[lastidx:] if f.isdigit()]
|
|
450
|
+
if shape is None:
|
|
451
|
+
# then the shape are the last 3
|
|
452
|
+
shape = [int(t) for t in before[-3:]]
|
|
453
|
+
if len(after)>0:
|
|
454
|
+
fnum = [int(t) for t in after]
|
|
455
|
+
return dtype,shape,fnum
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def mmap_wfield_binary(filename,
|
|
459
|
+
mode = 'r',
|
|
460
|
+
nframes = None,
|
|
461
|
+
shape = None,
|
|
462
|
+
dtype='uint16'):
|
|
463
|
+
'''
|
|
464
|
+
Loads frames from a binary file as a memory map.
|
|
465
|
+
This is useful when the data does not fit to memory.
|
|
466
|
+
|
|
467
|
+
Inputs:
|
|
468
|
+
filename (str) : fileformat convention, file ends in _NCHANNELS_H_W_DTYPE.dat
|
|
469
|
+
mode (str) : memory map access mode (default 'r')
|
|
470
|
+
'r' | Open existing file for reading only.
|
|
471
|
+
'r+' | Open existing file for reading and writing.
|
|
472
|
+
nframes (int) : number of frames to read (default is None: the entire file)
|
|
473
|
+
offset (int) : offset frame number (default 0)
|
|
474
|
+
shape (list|tuple) : dimensions (NCHANNELS, HEIGHT, WIDTH) default is None
|
|
475
|
+
dtype (str) : datatype (default uint16)
|
|
476
|
+
Returns:
|
|
477
|
+
A memory mapped array with size (NFRAMES,NCHANNELS, HEIGHT, WIDTH).
|
|
478
|
+
|
|
479
|
+
Example:
|
|
480
|
+
dat = mmap_dat(filename)
|
|
481
|
+
|
|
482
|
+
This is from wfield - jcouto
|
|
483
|
+
'''
|
|
484
|
+
|
|
485
|
+
if not os.path.isfile(filename):
|
|
486
|
+
raise OSError('File {0} not found.'.format(filename))
|
|
487
|
+
if shape is None or dtype is None: # try to get it from the filename
|
|
488
|
+
dtype,shape,_ = _parse_wfield_fname(filename,
|
|
489
|
+
shape = shape,
|
|
490
|
+
dtype = dtype)
|
|
491
|
+
if type(dtype) is str:
|
|
492
|
+
dt = np.dtype(dtype)
|
|
493
|
+
else:
|
|
494
|
+
dt = dtype
|
|
495
|
+
if nframes is None:
|
|
496
|
+
# Get the number of samples from the file size
|
|
497
|
+
nframes = int(os.path.getsize(filename)/(np.prod(shape)*dt.itemsize))
|
|
498
|
+
dt = np.dtype(dtype)
|
|
499
|
+
return np.memmap(filename,
|
|
500
|
+
mode=mode,
|
|
501
|
+
dtype=dt,
|
|
502
|
+
shape = (int(nframes),*shape))
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class MultifolderTiffStack(object):
|
|
506
|
+
def __init__(self,channel_folders, extension = '.ome.tif'):
|
|
507
|
+
'''
|
|
508
|
+
Simple class to access tiff files that are organized in a folders
|
|
509
|
+
Each folder is a channel and contains multiple TIFF files.
|
|
510
|
+
|
|
511
|
+
This is the format of the lightsheet microscope for example.
|
|
512
|
+
It is a place-holder class that should be modified to work for scanimage files also.
|
|
513
|
+
'''
|
|
514
|
+
self.nchannels = len(channel_folders)
|
|
515
|
+
self.filenames = []
|
|
516
|
+
for folder in channel_folders:
|
|
517
|
+
self.filenames.append(natsorted(Path(folder).rglob('*' + extension)))
|
|
518
|
+
# read the first file
|
|
519
|
+
im = read_ome_tif(self.filenames[0][0])
|
|
520
|
+
if len(im.shape) == 2:
|
|
521
|
+
# then the number frames is the number of tiff files
|
|
522
|
+
self.nframes = len(self.filenames[0])
|
|
523
|
+
self.frame_dims = im.shape
|
|
524
|
+
self.dtype = im.dtype
|
|
525
|
+
else:
|
|
526
|
+
raise NotImplementedError('This needs to be changed to work with multiple-frame TIFF files.')
|
|
527
|
+
self.shape = (self.nframes,self.nchannels,*self.frame_dims)
|
|
528
|
+
def __len__(self):
|
|
529
|
+
return self.nframes
|
|
530
|
+
|
|
531
|
+
def __getitem__(self,*args):
|
|
532
|
+
index = args[0]
|
|
533
|
+
if type(index) is slice:
|
|
534
|
+
idx1 = range(*index.indices(self.nframes))
|
|
535
|
+
elif type(index) in [int,np.int32, np.int64]: # just one frame
|
|
536
|
+
idx1 = [index]
|
|
537
|
+
else: # np.array?
|
|
538
|
+
idx1 = index
|
|
539
|
+
img = []
|
|
540
|
+
for i in idx1:
|
|
541
|
+
img.append(self.get(i))
|
|
542
|
+
return np.stack(img).squeeze().reshape(len(img),self.nchannels, *self.frame_dims)
|
|
543
|
+
|
|
544
|
+
def get(self,idx):
|
|
545
|
+
return np.stack([read_ome_tif(files[idx]) for files in self.filenames])
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def read_ome_tif(file):
|
|
549
|
+
from tifffile import TiffFile
|
|
550
|
+
try:
|
|
551
|
+
res = TiffFile(file).pages.get(0).asarray()
|
|
552
|
+
except Exception as err:
|
|
553
|
+
print(f'TIFF error for {file}')
|
|
554
|
+
print(err) # raise the exception again
|
|
555
|
+
raise(OSError(f'TIFF error for {file}'))
|
|
556
|
+
return res
|
|
557
|
+
|
|
558
|
+
def insert_widefield_dataset(key, local_paths = None, skip_duplicates = False):
|
|
559
|
+
'''
|
|
560
|
+
Insert widefield dataset keys (assumes data were collected with labcams but there can be adapted to support other formats.)
|
|
561
|
+
ask jpcouto@gmail.com
|
|
562
|
+
'''
|
|
563
|
+
from labdata.schema import Dataset, DatasetEvents, Widefield, File
|
|
564
|
+
if len(Widefield & key):
|
|
565
|
+
from warnings import warn
|
|
566
|
+
warn(f'Dataset {key} was already inserted.')
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
filedict = (File & (Dataset.DataFiles & key) & 'file_path LIKE "%.zarr.zip"').proj().fetch1()
|
|
570
|
+
datafile = (File & (Dataset.DataFiles & key) & 'file_path LIKE "%.zarr.zip"').get(local_paths = local_paths)[0]
|
|
571
|
+
camlogfile = (File & (Dataset.DataFiles & key) & 'file_path LIKE "%.camlog"').get(local_paths = local_paths)[0]
|
|
572
|
+
|
|
573
|
+
from ..utils import open_zarr
|
|
574
|
+
dat = open_zarr(datafile)
|
|
575
|
+
|
|
576
|
+
dataseteventsdict = None
|
|
577
|
+
software = 'unknown'
|
|
578
|
+
try:
|
|
579
|
+
from ..schema.utils import read_camlog
|
|
580
|
+
comm,log = read_camlog(camlogfile)
|
|
581
|
+
led = [c.split('LED:')[-1] for c in comm if c.startswith('#LED')]
|
|
582
|
+
if len(led):
|
|
583
|
+
from io import StringIO
|
|
584
|
+
led = pd.read_csv(StringIO('\n'.join(led)),delimiter = ',',header = None,names= ['channel','frame_id','timestamp'])
|
|
585
|
+
frame_rate = np.nanmean(1000./np.diff(led.timestamp.values[::dat.shape[1]]))
|
|
586
|
+
dataseteventsdict = [dict((Dataset.proj() & key).fetch1(),
|
|
587
|
+
stream_name = 'widefield')]
|
|
588
|
+
dataseteventsstreams = [dict(dataseteventsdict[0],event_name = u,
|
|
589
|
+
event_timestamps = led[led.channel.values == u].timestamp.values,
|
|
590
|
+
event_values = led[led.channel.values == u].frame_id.values) for u in np.unique(led.channel.values)]
|
|
591
|
+
else:
|
|
592
|
+
print('There was no LED in the log, trying to get the frame rate from the camera log.')
|
|
593
|
+
frame_rate = np.nanmean(1./np.diff(log[1].values))
|
|
594
|
+
|
|
595
|
+
software = 'labcams'
|
|
596
|
+
lv = [l.split(':')[-1].strip(' ') for l in comm if 'labcams version' in l]
|
|
597
|
+
if len(lv):
|
|
598
|
+
software += ' '+lv[0]
|
|
599
|
+
|
|
600
|
+
except Exception as err:
|
|
601
|
+
from warnings import warn
|
|
602
|
+
warn('Could not parse the frame rate.')
|
|
603
|
+
print(err)
|
|
604
|
+
frame_rate = -1
|
|
605
|
+
widefielddict = dict((Dataset.proj() & key).fetch1(),
|
|
606
|
+
n_channels = dat.shape[1],
|
|
607
|
+
n_frames = dat.shape[0],
|
|
608
|
+
height = dat.shape[2],
|
|
609
|
+
width = dat.shape[3],
|
|
610
|
+
frame_rate = frame_rate,
|
|
611
|
+
imaging_software = software,
|
|
612
|
+
**filedict)
|
|
613
|
+
del dat
|
|
614
|
+
if not dataseteventsdict is None:
|
|
615
|
+
DatasetEvents.insert(dataseteventsdict, allow_direct_insert=True, skip_duplicates=skip_duplicates)
|
|
616
|
+
DatasetEvents.Digital.insert(dataseteventsstreams, allow_direct_insert=True, skip_duplicates=skip_duplicates)
|
|
617
|
+
Widefield.insert1(widefielddict, allow_direct_insert=True, skip_duplicates = skip_duplicates)
|
|
618
|
+
return
|