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.
@@ -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