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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from .general import *
|
|
2
|
+
from .procedures import *
|
|
3
|
+
from .histology import *
|
|
4
|
+
|
|
5
|
+
@dataschema
|
|
6
|
+
class TwoPhoton(dj.Imported):
|
|
7
|
+
definition = '''
|
|
8
|
+
-> Dataset
|
|
9
|
+
---
|
|
10
|
+
n_planes : smallint # number of planes
|
|
11
|
+
n_channels : smallint # number of channels
|
|
12
|
+
n_frames : int # duration of the recording
|
|
13
|
+
width : int # width of each frame
|
|
14
|
+
height : int # height of each frame
|
|
15
|
+
frame_rate : double # frame rate
|
|
16
|
+
-> File # path to the stack
|
|
17
|
+
magnification = NULL : double # magnification
|
|
18
|
+
objective_angle = NULL : double # angle
|
|
19
|
+
objective = NULL : varchar(32) # objective
|
|
20
|
+
um_per_pixel = NULL : blob # XY scale conversion factors
|
|
21
|
+
scanning_mode = NULL : varchar(32) # bidirectional or unidirectional
|
|
22
|
+
pmt_gain = NULL : blob # pmt gains
|
|
23
|
+
imaging_software : varchar(32) # software and version
|
|
24
|
+
-> [nullable] Atlas.Region # target region (or the center of the imaging plane)
|
|
25
|
+
'''
|
|
26
|
+
|
|
27
|
+
class Plane(dj.Part):
|
|
28
|
+
definition = '''
|
|
29
|
+
-> master
|
|
30
|
+
plane_num : smallint # probe number
|
|
31
|
+
---
|
|
32
|
+
plane_depth = NULL : float # depth from surface
|
|
33
|
+
'''
|
|
34
|
+
|
|
35
|
+
def open(self):
|
|
36
|
+
# download and open a stack
|
|
37
|
+
if len(self) != 1:
|
|
38
|
+
raise(ValueError(f'Select only one dataset {self.proj().fetch(as_dict = True)}.'))
|
|
39
|
+
fname = (File() & self).get()[0]
|
|
40
|
+
return open_zarr(fname)
|
|
41
|
+
|
|
42
|
+
@analysisschema
|
|
43
|
+
class CellSegmentationParams(dj.Manual):
|
|
44
|
+
definition = '''
|
|
45
|
+
parameter_set_num : int # number of the parameters set
|
|
46
|
+
---
|
|
47
|
+
algorithm_name : varchar(64) # cell segmentation algorithm
|
|
48
|
+
parameter_description = NULL : varchar(256) # description or specific use case
|
|
49
|
+
parameters_dict : varchar(2000) # parameters json formatted dictionary "json.dumps"
|
|
50
|
+
code_link = NULL : varchar(300) # link to the code
|
|
51
|
+
'''
|
|
52
|
+
|
|
53
|
+
@analysisschema
|
|
54
|
+
class CellSegmentation(dj.Imported):
|
|
55
|
+
definition = '''
|
|
56
|
+
-> Dataset
|
|
57
|
+
-> CellSegmentationParams
|
|
58
|
+
---
|
|
59
|
+
algorithm_version : varchar(28) # version of the algorithm
|
|
60
|
+
num_rois : int # number of segmented ROIs
|
|
61
|
+
crop_region = NULL : blob # Region to crop
|
|
62
|
+
segmentation_datetime = NULL : datetime # date that the algorith was ran
|
|
63
|
+
-> [nullable] AnalysisFile # results file (optional)
|
|
64
|
+
'''
|
|
65
|
+
|
|
66
|
+
class Plane(dj.Part):
|
|
67
|
+
definition = '''
|
|
68
|
+
-> master
|
|
69
|
+
plane_num : smallint
|
|
70
|
+
---
|
|
71
|
+
plane_num_rois : int
|
|
72
|
+
'''
|
|
73
|
+
|
|
74
|
+
class MotionCorrection(dj.Part):
|
|
75
|
+
definition = '''
|
|
76
|
+
-> master.Plane
|
|
77
|
+
---
|
|
78
|
+
motion_block_size = 0 : int # 0 is rigid
|
|
79
|
+
displacement : longblob # storage of the motion displacement
|
|
80
|
+
'''
|
|
81
|
+
class Projection(dj.Part):
|
|
82
|
+
definition = '''
|
|
83
|
+
-> master.Plane
|
|
84
|
+
---
|
|
85
|
+
proj_name : varchar(16)
|
|
86
|
+
proj_im : longblob
|
|
87
|
+
'''
|
|
88
|
+
class ROI(dj.Part):
|
|
89
|
+
definition = '''
|
|
90
|
+
-> master.Plane
|
|
91
|
+
roi_num : int
|
|
92
|
+
---
|
|
93
|
+
roi_pixels : blob
|
|
94
|
+
roi_pixels_values = NULL : blob
|
|
95
|
+
neuropil_pixels = NULL : blob
|
|
96
|
+
'''
|
|
97
|
+
class RawTraces(dj.Part):
|
|
98
|
+
definition = '''
|
|
99
|
+
-> master.ROI
|
|
100
|
+
---
|
|
101
|
+
f_trace : longblob
|
|
102
|
+
f_neuropil = NULL : longblob
|
|
103
|
+
'''
|
|
104
|
+
class Traces(dj.Part):
|
|
105
|
+
definition = '''
|
|
106
|
+
-> master.ROI
|
|
107
|
+
---
|
|
108
|
+
dff : longblob
|
|
109
|
+
'''
|
|
110
|
+
class Deconvolved(dj.Part):
|
|
111
|
+
definition = '''
|
|
112
|
+
-> master.ROI
|
|
113
|
+
---
|
|
114
|
+
deconv : longblob
|
|
115
|
+
'''
|
|
116
|
+
|
|
117
|
+
class Selection(dj.Part):
|
|
118
|
+
definition = '''
|
|
119
|
+
-> master.ROI
|
|
120
|
+
selection_method = 'auto' : varchar(12)
|
|
121
|
+
---
|
|
122
|
+
selection : smallint # 0 not a cell, 1 is a cell
|
|
123
|
+
likelihood = NULL : float
|
|
124
|
+
'''
|
|
125
|
+
|
|
126
|
+
@analysisschema
|
|
127
|
+
class SegmentationROIMetrics(dj.Computed):
|
|
128
|
+
definition = '''
|
|
129
|
+
-> CellSegmentation.ROI
|
|
130
|
+
---
|
|
131
|
+
roi_center : blob
|
|
132
|
+
radius = NULL : float
|
|
133
|
+
snr = NULL : float
|
|
134
|
+
aspect_ratio = NULL : float
|
|
135
|
+
skewness = NULL : float
|
|
136
|
+
std = NULL : float
|
|
137
|
+
noise_level = NULL : float
|
|
138
|
+
'''
|
|
139
|
+
|
|
140
|
+
def make(self, key):
|
|
141
|
+
# 90th percentile/np.median(np.abs(np.diff(dff))) / np.sqrt(frame_rate)?
|
|
142
|
+
return
|
labdata/schema/utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from ..utils import *
|
|
2
|
+
# some functions used in the schema imports
|
|
3
|
+
|
|
4
|
+
__all__ = ['read_camlog']
|
|
5
|
+
|
|
6
|
+
def read_camlog(log):
|
|
7
|
+
'''
|
|
8
|
+
Adapted from github.com/jcouto/labcams
|
|
9
|
+
'''
|
|
10
|
+
|
|
11
|
+
logheaderkey = '# Log header:'
|
|
12
|
+
comments = []
|
|
13
|
+
with open(log,'r',encoding = 'utf-8') as fd:
|
|
14
|
+
for line in fd:
|
|
15
|
+
if line.startswith('#'):
|
|
16
|
+
line = line.strip('\n').strip('\r')
|
|
17
|
+
comments.append(line)
|
|
18
|
+
if line.startswith(logheaderkey):
|
|
19
|
+
columns = line.strip(logheaderkey).strip(' ').split(',')
|
|
20
|
+
camlog = pd.read_csv(log, delimiter = ',',
|
|
21
|
+
header = None,
|
|
22
|
+
comment = '#',
|
|
23
|
+
engine = 'c')
|
|
24
|
+
return comments,camlog
|
|
25
|
+
|
labdata/schema/video.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from .general import *
|
|
2
|
+
|
|
3
|
+
@dataschema
|
|
4
|
+
class DatasetVideo(dj.Manual):
|
|
5
|
+
definition = '''
|
|
6
|
+
-> Dataset
|
|
7
|
+
video_name : varchar(56)
|
|
8
|
+
---
|
|
9
|
+
frame_times = NULL : longblob
|
|
10
|
+
frame_rate = NULL : float
|
|
11
|
+
n_frames = NULL : float
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
class File(dj.Part):
|
|
15
|
+
definition = '''
|
|
16
|
+
-> master
|
|
17
|
+
-> File
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
class Frame(dj.Part):
|
|
21
|
+
definition = '''
|
|
22
|
+
-> master
|
|
23
|
+
frame_num : int
|
|
24
|
+
---
|
|
25
|
+
frame : longblob
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
######################################################
|
|
29
|
+
############ POSE ESTIMATION ###############
|
|
30
|
+
######################################################
|
|
31
|
+
@analysisschema
|
|
32
|
+
class PoseEstimationLabelSet(dj.Manual):
|
|
33
|
+
definition = '''
|
|
34
|
+
pose_label_set_num : int
|
|
35
|
+
---
|
|
36
|
+
description = NULL : varchar(512)
|
|
37
|
+
-> [nullable] LabMember.proj(labeler = "user_name")
|
|
38
|
+
'''
|
|
39
|
+
class Frame(dj.Part):
|
|
40
|
+
definition = '''
|
|
41
|
+
-> master
|
|
42
|
+
-> DatasetVideo
|
|
43
|
+
frame_num : int
|
|
44
|
+
---
|
|
45
|
+
frame : longblob
|
|
46
|
+
'''
|
|
47
|
+
class Label(dj.Part):
|
|
48
|
+
definition = '''
|
|
49
|
+
-> master
|
|
50
|
+
-> DatasetVideo
|
|
51
|
+
frame_num : int
|
|
52
|
+
label_name : varchar(54)
|
|
53
|
+
---
|
|
54
|
+
x : float
|
|
55
|
+
y : float
|
|
56
|
+
z = NULL : float
|
|
57
|
+
'''
|
|
58
|
+
def export_labeling(self, model_num = None, bodyparts = None, disperse_labels = False, export_only_labeled = False):
|
|
59
|
+
'''
|
|
60
|
+
Exports labeling for PoseEstimation (for use with napari-deeplabcut)
|
|
61
|
+
'''
|
|
62
|
+
assert len(self) == 1, ValueError('PoseEstimationLabelSet, select only one set to export.')
|
|
63
|
+
k = self.proj().fetch1()
|
|
64
|
+
|
|
65
|
+
if export_only_labeled:
|
|
66
|
+
frames = pd.DataFrame((PoseEstimationLabelSet()*PoseEstimationLabelSet.Frame() & (PoseEstimationLabelSet.Label() & k)).fetch())
|
|
67
|
+
else:
|
|
68
|
+
frames = pd.DataFrame((PoseEstimationLabelSet()*PoseEstimationLabelSet.Frame() & k).fetch())
|
|
69
|
+
frame_labels = pd.DataFrame((PoseEstimationLabelSet()*PoseEstimationLabelSet.Label() & k).fetch())
|
|
70
|
+
|
|
71
|
+
if model_num is None:
|
|
72
|
+
folder = (Path(prefs['local_paths'][0])/'pose_estimation_models')/f'pose_label_set_num_{k["pose_label_set_num"]}'
|
|
73
|
+
else:
|
|
74
|
+
folder = (Path(prefs['local_paths'][0])/'pose_estimation_models')/f'model_{model_num}'
|
|
75
|
+
data_path = (folder / "labeled-data") / f'label_set_{k["pose_label_set_num"]}'
|
|
76
|
+
data_path.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
if bodyparts is None:
|
|
78
|
+
bodyparts = np.unique(frame_labels.label_name.values)
|
|
79
|
+
from natsort import natsorted
|
|
80
|
+
bodyparts = natsorted(bodyparts) # this is an attempt to sort the labels
|
|
81
|
+
labeler = frames['labeler'].iloc[0]
|
|
82
|
+
from skimage.io import imsave
|
|
83
|
+
from tqdm import tqdm
|
|
84
|
+
todlc = []
|
|
85
|
+
for i,f in tqdm(enumerate(frames.frame_num.values),desc = "Exporting labeling dataset:",total = len(frames)):
|
|
86
|
+
im_name = 'im_{0:06d}_session{2}_frame{1:06d}'.format(i,f,frames.session_name.iloc[i])
|
|
87
|
+
for bpart in bodyparts:
|
|
88
|
+
t = (PoseEstimationLabelSet.Label & dict(
|
|
89
|
+
pose_label_set_num = k['pose_label_set_num'],
|
|
90
|
+
frame_num = f,
|
|
91
|
+
label_name = bpart)).fetch()
|
|
92
|
+
x = np.nan
|
|
93
|
+
y = np.nan
|
|
94
|
+
if disperse_labels:
|
|
95
|
+
if i == 0:
|
|
96
|
+
x = i*20
|
|
97
|
+
y = 100
|
|
98
|
+
if len(t):
|
|
99
|
+
x = t['x'][0]
|
|
100
|
+
y = t['y'][0]
|
|
101
|
+
todlc.append(dict(scorer = labeler,
|
|
102
|
+
bodyparts = bpart,
|
|
103
|
+
level_0 = 'labeled-data',
|
|
104
|
+
level_1 = f'label_set_{k["pose_label_set_num"]}',
|
|
105
|
+
level_2 = f'{im_name}.png',
|
|
106
|
+
x = x,
|
|
107
|
+
y = y))
|
|
108
|
+
fname = data_path/f'{im_name}.png'
|
|
109
|
+
if not fname.exists():
|
|
110
|
+
imsave(fname,frames.iloc[i].frame)
|
|
111
|
+
df = pd.DataFrame(todlc)
|
|
112
|
+
df = df.set_index(["scorer", "bodyparts","level_0","level_1","level_2"]).stack()
|
|
113
|
+
df.index.set_names("coords", level=-1, inplace=True)
|
|
114
|
+
df = df.unstack(["scorer", "bodyparts", "coords"])
|
|
115
|
+
df.index.name = None
|
|
116
|
+
df.to_hdf(data_path/f'CollectedData_{labeler}.h5',key='keypoints')
|
|
117
|
+
return data_path,frames,frame_labels
|
|
118
|
+
|
|
119
|
+
def update_labeling(self, labeling_file):
|
|
120
|
+
'''
|
|
121
|
+
(PoseEstimationLabelSet() & 'pose_label_set_num =3').update_labeling('filename.h5')
|
|
122
|
+
|
|
123
|
+
Updates the labels in the PoseEstimationLabelSet from a file.
|
|
124
|
+
Currently only DLC format is supported.
|
|
125
|
+
|
|
126
|
+
Reach out if you need other formats.
|
|
127
|
+
Joao Couto 2023
|
|
128
|
+
'''
|
|
129
|
+
dlcres = pd.read_hdf(labeling_file)
|
|
130
|
+
scorer = np.unique(dlcres.columns.get_level_values(0))[0]
|
|
131
|
+
bodyparts = np.unique(dlcres.columns.get_level_values(1))
|
|
132
|
+
frame_nums = [int(f.split('frame')[-1].strip('.png'))
|
|
133
|
+
for f in dlcres.reset_index()['level_2'].values]
|
|
134
|
+
frame_names = dlcres.reset_index()['level_2'].values
|
|
135
|
+
labels = []
|
|
136
|
+
from tqdm import tqdm
|
|
137
|
+
labels_to_insert = [] # insert the labels in parallel will be faster.
|
|
138
|
+
labels_to_delete = [] # need to delete all labels for a frame before adding the new ones
|
|
139
|
+
for iframe,frame_name in tqdm(enumerate(frame_names),desc = 'Updating labels',total = len(frame_names)):
|
|
140
|
+
frame_num = int(frame_name.split('frame')[-1].strip('.png'))
|
|
141
|
+
frame_key = dict(frame_num = frame_num)
|
|
142
|
+
if 'session' in frame_name: # get the session name so there are no conflicting frame numbers
|
|
143
|
+
frame_key['session_name'] = frame_name.split('session')[-1].split('_frame')[0]
|
|
144
|
+
frame_key = (PoseEstimationLabelSet.Frame() & self.proj().fetch1() & frame_key).proj().fetch1()
|
|
145
|
+
labels_to_delete.extend((PoseEstimationLabelSet.Label() & frame_key).proj().fetch(as_dict = True))
|
|
146
|
+
for dlcname in bodyparts:
|
|
147
|
+
if np.isnan(dlcres[scorer][dlcname].iloc[iframe]['x']):
|
|
148
|
+
continue # if it is NaN, don't add
|
|
149
|
+
if dlcres[scorer][dlcname].iloc[iframe]['x'] == 0 and dlcres[scorer][dlcname].iloc[iframe]['y'] == 0:
|
|
150
|
+
continue # if the label is at 0,0 don't add
|
|
151
|
+
label = dict(dict(frame_key,label_name = dlcname),
|
|
152
|
+
label_name = dlcname,
|
|
153
|
+
x = dlcres[scorer][dlcname].iloc[iframe]['x'],
|
|
154
|
+
y = dlcres[scorer][dlcname].iloc[iframe]['y'])
|
|
155
|
+
labels_to_insert.append(label)
|
|
156
|
+
(PoseEstimationLabelSet.Label() & labels_to_delete).delete(force = True) # ask the user to confirm
|
|
157
|
+
PoseEstimationLabelSet.Label.insert(labels_to_insert)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@analysisschema
|
|
161
|
+
class PoseEstimationModel(dj.Manual):
|
|
162
|
+
definition = '''
|
|
163
|
+
model_num : int
|
|
164
|
+
---
|
|
165
|
+
algorithm_name : varchar(24) # Algorithm for pose estimation
|
|
166
|
+
-> AnalysisFile # zipped model; no videos.
|
|
167
|
+
-> [nullable] PoseEstimationLabelSet
|
|
168
|
+
parameters_dict = NULL : varchar(2000) # parameters json formatted dictionary
|
|
169
|
+
training_datetime = NULL : datetime
|
|
170
|
+
container_name = NULL : varchar(64) # Name of the container to use
|
|
171
|
+
code_link = NULL : varchar(300) # link to the github of the algorithm
|
|
172
|
+
'''
|
|
173
|
+
|
|
174
|
+
def insert_model(self, model_num,
|
|
175
|
+
model_folder=None,
|
|
176
|
+
pose_label_set_num = None,
|
|
177
|
+
algorithm_name = None,
|
|
178
|
+
parameters = None,
|
|
179
|
+
training_datetime=None,
|
|
180
|
+
container_name = None,
|
|
181
|
+
code_link = None):
|
|
182
|
+
import shutil
|
|
183
|
+
if training_datetime is None:
|
|
184
|
+
today = datetime.now()
|
|
185
|
+
else:
|
|
186
|
+
today = training_datetime
|
|
187
|
+
dataset_name = datetime.strftime(today,'%Y%m%d_%H%M%S')
|
|
188
|
+
|
|
189
|
+
# check if this model_number exists for another pose_label_set_num
|
|
190
|
+
allmodels = pd.DataFrame(PoseEstimationModel.fetch())
|
|
191
|
+
sel = allmodels[(allmodels.model_num.values == model_num) & (allmodels.pose_label_set_num.values != pose_label_set_num)]
|
|
192
|
+
if len(sel):
|
|
193
|
+
model_num = np.max(allmodels.model_num.values)+1
|
|
194
|
+
|
|
195
|
+
if model_folder is None:
|
|
196
|
+
model_folder = ((Path(prefs['local_paths'][0])/'pose_estimation_models')/f'{dataset_name}')/f'model_{model_num}'
|
|
197
|
+
filepath = ((Path(prefs['local_paths'][0])/'pose_estimation_models')/f'{dataset_name}')/f'model_{model_num}'
|
|
198
|
+
print(f'Creating archive {filepath}')
|
|
199
|
+
shutil.make_archive(filepath, 'zip', model_folder)
|
|
200
|
+
filepath = filepath.with_suffix('.zip')
|
|
201
|
+
|
|
202
|
+
key = AnalysisFile().upload_files([filepath],dataset = dict(subject_name = 'pose_estimation_models',
|
|
203
|
+
session_name = f'model_{model_num}',
|
|
204
|
+
dataset_name = dataset_name))
|
|
205
|
+
key = dict((AnalysisFile & key).proj().fetch1(),
|
|
206
|
+
model_num = model_num,
|
|
207
|
+
algorithm_name = algorithm_name,
|
|
208
|
+
training_datetime = today,
|
|
209
|
+
pose_label_set_num = pose_label_set_num,
|
|
210
|
+
parameters_dict = json.dumps(parameters) if not parameters is None else None,
|
|
211
|
+
container_name = container_name,
|
|
212
|
+
code_link = code_link)
|
|
213
|
+
if not len(PoseEstimationModel & f'model_num = {model_num}'):
|
|
214
|
+
self.insert1(key)
|
|
215
|
+
else:
|
|
216
|
+
print(f'Model {model_num} already exists. Updating but keeping last version in AWS.')
|
|
217
|
+
oldentry = (PoseEstimationModel & f'model_num = {model_num}').fetch1()
|
|
218
|
+
for k in key.keys():
|
|
219
|
+
if key[k] is None:
|
|
220
|
+
key[k] = oldentry[k]
|
|
221
|
+
self.update1(key)
|
|
222
|
+
# need to add a model evaluation part table here
|
|
223
|
+
def get_model(self):
|
|
224
|
+
filepath = (AnalysisFile & self).get()
|
|
225
|
+
|
|
226
|
+
filepath = filepath[0]
|
|
227
|
+
if not (filepath.parent/'config.yaml').exists():
|
|
228
|
+
import shutil
|
|
229
|
+
shutil.unpack_archive(filepath,extract_dir = filepath.parent)
|
|
230
|
+
return filepath.parent/'config.yaml' # return the path to the config file.
|
|
231
|
+
|
|
232
|
+
@analysisschema
|
|
233
|
+
class PoseEstimation(dj.Manual):
|
|
234
|
+
definition = '''
|
|
235
|
+
-> PoseEstimationModel
|
|
236
|
+
-> DatasetVideo
|
|
237
|
+
label_name : varchar(54)
|
|
238
|
+
---
|
|
239
|
+
x : longblob
|
|
240
|
+
y : longblob
|
|
241
|
+
z = NULL : longblob
|
|
242
|
+
likelihood : longblob
|
|
243
|
+
'''
|
labdata/stacks.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# files to process or visualize imaging stacks
|
|
2
|
+
|
|
3
|
+
from .utils import *
|
|
4
|
+
|
|
5
|
+
def downsample_stack(stack, ratio = [0.3,1,0.40625,0.40625],n_jobs = DEFAULT_N_JOBS,order = 1):
|
|
6
|
+
'''
|
|
7
|
+
Downsamples a 4D stack using interpolation.
|
|
8
|
+
|
|
9
|
+
smaller_stack = downsample_stack(stack, ratio = [0.3,1,0.40625,0.40625],n_jobs = DEFAULT_N_JOBS)
|
|
10
|
+
|
|
11
|
+
'''
|
|
12
|
+
assert len(stack.shape) == 4, ValueError('Only downsampling 4D stacks is supported.')
|
|
13
|
+
from scipy.ndimage import zoom
|
|
14
|
+
from joblib import Parallel, delayed
|
|
15
|
+
from tqdm import tqdm
|
|
16
|
+
import numpy as np
|
|
17
|
+
assert len(stack.shape) == len(ratio), ValueError(f'Stack ({stack.shape}) and ratio {len(ratio)} need to match')
|
|
18
|
+
a = Parallel(n_jobs = n_jobs)(delayed(zoom)(
|
|
19
|
+
frame,np.array(ratio)[1:],order = order)
|
|
20
|
+
for frame in tqdm(stack,
|
|
21
|
+
total = stack.shape[0],desc = 'Downsampling inner dimensions:'))
|
|
22
|
+
na = np.stack(a)
|
|
23
|
+
if na.shape[1] == 1:
|
|
24
|
+
return na
|
|
25
|
+
del a
|
|
26
|
+
na = Parallel(n_jobs = n_jobs)(delayed(zoom)(
|
|
27
|
+
na[:,i],[ratio[0],1,1],order = order)
|
|
28
|
+
for i in tqdm(range(na.shape[1]),
|
|
29
|
+
total = stack.shape[1],desc = 'Downsampling outer dimensions:'))
|
|
30
|
+
return np.stack(na).transpose([1,0,2,3])
|
|
31
|
+
|
|
32
|
+
def rotate_stack(stack,
|
|
33
|
+
anglez = 0,
|
|
34
|
+
angley = 0,
|
|
35
|
+
anglex = 0,
|
|
36
|
+
flip_x = False,
|
|
37
|
+
flip_y = False,
|
|
38
|
+
n_jobs = DEFAULT_N_JOBS,
|
|
39
|
+
order = 1):
|
|
40
|
+
'''
|
|
41
|
+
Rotate a stack a 4d stack.
|
|
42
|
+
|
|
43
|
+
This can be used for instance to rotate a whole brain image. Channels are rotated in paralell, careful with the
|
|
44
|
+
To get the angles:
|
|
45
|
+
|
|
46
|
+
Joao Couto - adapted from deeptrace (2023)
|
|
47
|
+
'''
|
|
48
|
+
from tqdm import tqdm
|
|
49
|
+
na = stack.transpose(1,0,2,3)
|
|
50
|
+
from scipy.ndimage import rotate
|
|
51
|
+
if anglex != 0.0:
|
|
52
|
+
na = Parallel(n_jobs = n_jobs)(delayed(rotate)(
|
|
53
|
+
s, angle = anglex,order = order,axes = [1,2],reshape = False)
|
|
54
|
+
for s in tqdm(na, total = na.shape[0],desc = 'Rotating the x axis:'))
|
|
55
|
+
na = np.stack(na)
|
|
56
|
+
if angley != 0.0:
|
|
57
|
+
na = Parallel(n_jobs = n_jobs)(delayed(rotate)(
|
|
58
|
+
s, angle = angley,order = order,axes = [0,2],reshape = False)
|
|
59
|
+
for s in tqdm(na,
|
|
60
|
+
total = na.shape[0],desc = 'Rotating the y axis:'))
|
|
61
|
+
na = np.stack(na)
|
|
62
|
+
if anglez != 0.0:
|
|
63
|
+
na = Parallel(n_jobs = n_jobs)(delayed(rotate)(
|
|
64
|
+
s, angle = anglez,order = order,axes = [0,1],reshape = False)
|
|
65
|
+
for s in tqdm(na,
|
|
66
|
+
total = na.shape[0],desc = 'Rotating the z axis:'))
|
|
67
|
+
na = np.stack(na)
|
|
68
|
+
if flip_x:
|
|
69
|
+
na = na[:,:,:,::-1]
|
|
70
|
+
if flip_y:
|
|
71
|
+
na = na[:,:,::-1,:]
|
|
72
|
+
return na.transpose(1,0,2,3)
|
|
73
|
+
|
|
74
|
+
def napari_open(stack,**kwargs):
|
|
75
|
+
'''
|
|
76
|
+
Example:
|
|
77
|
+
|
|
78
|
+
napari_open(stack.transpose(1,0,2,3),contrast_limits=[0,65000],channel_axis = 1,multiscale=False)
|
|
79
|
+
|
|
80
|
+
'''
|
|
81
|
+
import napari
|
|
82
|
+
napari.view_image(stack,**kwargs)
|
|
83
|
+
|
|
84
|
+
class VideoStack():
|
|
85
|
+
'''
|
|
86
|
+
Class to read video file sequences.
|
|
87
|
+
'''
|
|
88
|
+
def __init__(self,files, use_fast_seek = False):
|
|
89
|
+
self.N = 0
|
|
90
|
+
self.W = None
|
|
91
|
+
self.H = None
|
|
92
|
+
self.files = files
|
|
93
|
+
self.file_offsets = []
|
|
94
|
+
self.current_file = None
|
|
95
|
+
self.current_offset = -1
|
|
96
|
+
self.use_fast_seek = use_fast_seek
|
|
97
|
+
self.cap = None
|
|
98
|
+
self._read_properties()
|
|
99
|
+
self.dtype = np.uint8
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _read_properties(self):
|
|
104
|
+
self.file_offsets = [0]
|
|
105
|
+
self.N = 0
|
|
106
|
+
self.W = None
|
|
107
|
+
self.H = None
|
|
108
|
+
import cv2
|
|
109
|
+
for f in self.files:
|
|
110
|
+
cap = cv2.VideoCapture(f)
|
|
111
|
+
N = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
112
|
+
self.N += N
|
|
113
|
+
self.file_offsets.append(N)
|
|
114
|
+
if self.W is None:
|
|
115
|
+
self.W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
116
|
+
self.H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
117
|
+
if self.W != int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)):
|
|
118
|
+
raise(ValueError(f'Video {f} has a different width.'))
|
|
119
|
+
if self.H != int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)):
|
|
120
|
+
raise(ValueError(f'Video {f} has a different height.'))
|
|
121
|
+
cap.release()
|
|
122
|
+
self.shape = (self.N,self.H,self.W)
|
|
123
|
+
self.file_offsets = np.cumsum(self.file_offsets)
|
|
124
|
+
def __len__(self):
|
|
125
|
+
return self.shape[0]
|
|
126
|
+
|
|
127
|
+
def __getitem__(self, *args):
|
|
128
|
+
index = args[0]
|
|
129
|
+
idx1 = None
|
|
130
|
+
idx2 = None
|
|
131
|
+
if type(index) is tuple: # then look for 2 channels
|
|
132
|
+
if type(index[1]) is slice:
|
|
133
|
+
idx2 = range(index[1].start, index[1].stop, index[1].step)
|
|
134
|
+
else:
|
|
135
|
+
idx2 = index[1]
|
|
136
|
+
index = index[0]
|
|
137
|
+
if type(index) is slice:
|
|
138
|
+
idx1 = range(*index.indices(self.N))#start, index.stop, index.step)
|
|
139
|
+
elif type(index) in [int,np.int32, np.int64]: # just a frame
|
|
140
|
+
idx1 = [index]
|
|
141
|
+
else: # np.array?
|
|
142
|
+
idx1 = index
|
|
143
|
+
img = np.empty((len(idx1),*self.shape[1:]), dtype = self.dtype)
|
|
144
|
+
# print(img.shape,idx1)
|
|
145
|
+
for i,ind in enumerate(idx1):
|
|
146
|
+
img[i] = self._get_frame(ind)
|
|
147
|
+
if not idx2 is None:
|
|
148
|
+
return img[:,idx2].squeeze()
|
|
149
|
+
else:
|
|
150
|
+
return img[:].squeeze()
|
|
151
|
+
|
|
152
|
+
def _open_file(self,ifile):
|
|
153
|
+
if not self.cap is None:
|
|
154
|
+
self.cap.release()
|
|
155
|
+
self.cap = None
|
|
156
|
+
import cv2
|
|
157
|
+
self.cap = cv2.VideoCapture(self.files[ifile])
|
|
158
|
+
self.current_offset = 0
|
|
159
|
+
self.current_file = ifile
|
|
160
|
+
# print(f'Opened file {self.files[ifile]}.')
|
|
161
|
+
|
|
162
|
+
def _get_frame(self,ind):
|
|
163
|
+
import cv2
|
|
164
|
+
ifile = np.where(self.file_offsets <= ind)[0][-1]
|
|
165
|
+
if not self.current_file == ifile:
|
|
166
|
+
self._open_file(ifile)
|
|
167
|
+
ind = ind - self.file_offsets[self.current_file]
|
|
168
|
+
if ind == 0:
|
|
169
|
+
self._open_file(ifile) # open twice if zero because we may need to reset for sequential access
|
|
170
|
+
if self.use_fast_seek and not self.current_offset == ind:
|
|
171
|
+
self.cap.set(cv2.CAP_PROP_POS_FRAMES,ind)
|
|
172
|
+
ret, frame = self.cap.read()
|
|
173
|
+
self._open_file(ifile)
|
|
174
|
+
return frame[...,0]
|
|
175
|
+
|
|
176
|
+
while True:
|
|
177
|
+
ret, frame = self.cap.read()
|
|
178
|
+
self.current_offset += 1
|
|
179
|
+
if self.current_offset-1 == ind:
|
|
180
|
+
return frame[...,0]
|
|
181
|
+
elif (self.current_offset-1) > ind: # re-open the file
|
|
182
|
+
self._open_file(ifile)
|