celldetective 1.0.2__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.
- celldetective/__init__.py +2 -0
- celldetective/__main__.py +432 -0
- celldetective/datasets/segmentation_annotations/blank +0 -0
- celldetective/datasets/signal_annotations/blank +0 -0
- celldetective/events.py +149 -0
- celldetective/extra_properties.py +100 -0
- celldetective/filters.py +89 -0
- celldetective/gui/__init__.py +20 -0
- celldetective/gui/about.py +44 -0
- celldetective/gui/analyze_block.py +563 -0
- celldetective/gui/btrack_options.py +898 -0
- celldetective/gui/classifier_widget.py +386 -0
- celldetective/gui/configure_new_exp.py +532 -0
- celldetective/gui/control_panel.py +438 -0
- celldetective/gui/gui_utils.py +495 -0
- celldetective/gui/json_readers.py +113 -0
- celldetective/gui/measurement_options.py +1425 -0
- celldetective/gui/neighborhood_options.py +452 -0
- celldetective/gui/plot_signals_ui.py +1042 -0
- celldetective/gui/process_block.py +1055 -0
- celldetective/gui/retrain_segmentation_model_options.py +706 -0
- celldetective/gui/retrain_signal_model_options.py +643 -0
- celldetective/gui/seg_model_loader.py +460 -0
- celldetective/gui/signal_annotator.py +2388 -0
- celldetective/gui/signal_annotator_options.py +340 -0
- celldetective/gui/styles.py +217 -0
- celldetective/gui/survival_ui.py +903 -0
- celldetective/gui/tableUI.py +608 -0
- celldetective/gui/thresholds_gui.py +1300 -0
- celldetective/icons/logo-large.png +0 -0
- celldetective/icons/logo.png +0 -0
- celldetective/icons/signals_icon.png +0 -0
- celldetective/icons/splash-test.png +0 -0
- celldetective/icons/splash.png +0 -0
- celldetective/icons/splash0.png +0 -0
- celldetective/icons/survival2.png +0 -0
- celldetective/icons/vignette_signals2.png +0 -0
- celldetective/icons/vignette_signals2.svg +114 -0
- celldetective/io.py +2050 -0
- celldetective/links/zenodo.json +561 -0
- celldetective/measure.py +1258 -0
- celldetective/models/segmentation_effectors/blank +0 -0
- celldetective/models/segmentation_generic/blank +0 -0
- celldetective/models/segmentation_targets/blank +0 -0
- celldetective/models/signal_detection/blank +0 -0
- celldetective/models/tracking_configs/mcf7.json +68 -0
- celldetective/models/tracking_configs/ricm.json +203 -0
- celldetective/models/tracking_configs/ricm2.json +203 -0
- celldetective/neighborhood.py +717 -0
- celldetective/scripts/analyze_signals.py +51 -0
- celldetective/scripts/measure_cells.py +275 -0
- celldetective/scripts/segment_cells.py +212 -0
- celldetective/scripts/segment_cells_thresholds.py +140 -0
- celldetective/scripts/track_cells.py +206 -0
- celldetective/scripts/train_segmentation_model.py +246 -0
- celldetective/scripts/train_signal_model.py +49 -0
- celldetective/segmentation.py +712 -0
- celldetective/signals.py +2826 -0
- celldetective/tracking.py +974 -0
- celldetective/utils.py +1681 -0
- celldetective-1.0.2.dist-info/LICENSE +674 -0
- celldetective-1.0.2.dist-info/METADATA +192 -0
- celldetective-1.0.2.dist-info/RECORD +66 -0
- celldetective-1.0.2.dist-info/WHEEL +5 -0
- celldetective-1.0.2.dist-info/entry_points.txt +2 -0
- celldetective-1.0.2.dist-info/top_level.txt +1 -0
celldetective/io.py
ADDED
|
@@ -0,0 +1,2050 @@
|
|
|
1
|
+
from natsort import natsorted
|
|
2
|
+
from glob import glob
|
|
3
|
+
from tifffile import imread, TiffFile
|
|
4
|
+
import numpy as np
|
|
5
|
+
import os
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import napari
|
|
8
|
+
import gc
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
from csbdeep.utils import normalize_mi_ma
|
|
11
|
+
import skimage.io as skio
|
|
12
|
+
from scipy.ndimage import zoom
|
|
13
|
+
from btrack.datasets import cell_config
|
|
14
|
+
from magicgui import magicgui
|
|
15
|
+
from csbdeep.io import save_tiff_imagej_compatible
|
|
16
|
+
from pathlib import Path, PurePath
|
|
17
|
+
from shutil import copyfile
|
|
18
|
+
from celldetective.utils import ConfigSectionMap, extract_experiment_channels, _extract_labels_from_config, get_zenodo_files, download_zenodo_file
|
|
19
|
+
import json
|
|
20
|
+
import threading
|
|
21
|
+
from skimage.measure import regionprops_table
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_experiment_wells(experiment):
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
Retrieves the list of well directories from a given experiment directory, sorted
|
|
28
|
+
naturally and returned as a NumPy array of strings.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
experiment : str
|
|
33
|
+
The path to the experiment directory from which to retrieve well directories.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
np.ndarray
|
|
38
|
+
An array of strings, each representing the full path to a well directory within the specified
|
|
39
|
+
experiment. The array is empty if no well directories are found.
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
- The function assumes well directories are prefixed with 'W' and uses this to filter directories
|
|
44
|
+
within the experiment folder.
|
|
45
|
+
|
|
46
|
+
- Natural sorting is applied to the list of wells to ensure that the order is intuitive (e.g., 'W2'
|
|
47
|
+
comes before 'W10'). This sorting method is especially useful when dealing with numerical sequences
|
|
48
|
+
that are part of the directory names.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
if not experiment.endswith(os.sep):
|
|
53
|
+
experiment += os.sep
|
|
54
|
+
|
|
55
|
+
wells = natsorted(glob(experiment + "W*" + os.sep))
|
|
56
|
+
return np.array(wells,dtype=str)
|
|
57
|
+
|
|
58
|
+
def get_config(experiment):
|
|
59
|
+
|
|
60
|
+
if not experiment.endswith(os.sep):
|
|
61
|
+
experiment += os.sep
|
|
62
|
+
|
|
63
|
+
config = experiment + 'config.ini'
|
|
64
|
+
config = rf"{config}"
|
|
65
|
+
assert os.path.exists(config),'The experiment configuration could not be located...'
|
|
66
|
+
return config
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_spatial_calibration(experiment):
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
config = get_config(experiment)
|
|
73
|
+
PxToUm = float(ConfigSectionMap(config,"MovieSettings")["pxtoum"])
|
|
74
|
+
|
|
75
|
+
return PxToUm
|
|
76
|
+
|
|
77
|
+
def get_temporal_calibration(experiment):
|
|
78
|
+
|
|
79
|
+
config = get_config(experiment)
|
|
80
|
+
FrameToMin = float(ConfigSectionMap(config,"MovieSettings")["frametomin"])
|
|
81
|
+
|
|
82
|
+
return FrameToMin
|
|
83
|
+
|
|
84
|
+
def get_experiment_concentrations(experiment, dtype=str):
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
config = get_config(experiment)
|
|
88
|
+
wells = get_experiment_wells(experiment)
|
|
89
|
+
nbr_of_wells = len(wells)
|
|
90
|
+
|
|
91
|
+
concentrations = ConfigSectionMap(config,"Labels")["concentrations"].split(",")
|
|
92
|
+
if nbr_of_wells != len(concentrations):
|
|
93
|
+
concentrations = [str(s) for s in np.linspace(0,nbr_of_wells-1,nbr_of_wells)]
|
|
94
|
+
|
|
95
|
+
return np.array([dtype(c) for c in concentrations])
|
|
96
|
+
|
|
97
|
+
def get_experiment_cell_types(experiment, dtype=str):
|
|
98
|
+
|
|
99
|
+
config = get_config(experiment)
|
|
100
|
+
wells = get_experiment_wells(experiment)
|
|
101
|
+
nbr_of_wells = len(wells)
|
|
102
|
+
|
|
103
|
+
cell_types = ConfigSectionMap(config,"Labels")["cell_types"].split(",")
|
|
104
|
+
if nbr_of_wells != len(cell_types):
|
|
105
|
+
cell_types = [str(s) for s in np.linspace(0,nbr_of_wells-1,nbr_of_wells)]
|
|
106
|
+
|
|
107
|
+
return np.array([dtype(c) for c in cell_types])
|
|
108
|
+
|
|
109
|
+
def get_experiment_antibodies(experiment, dtype=str):
|
|
110
|
+
|
|
111
|
+
config = get_config(experiment)
|
|
112
|
+
wells = get_experiment_wells(experiment)
|
|
113
|
+
nbr_of_wells = len(wells)
|
|
114
|
+
|
|
115
|
+
antibodies = ConfigSectionMap(config,"Labels")["antibodies"].split(",")
|
|
116
|
+
if nbr_of_wells != len(antibodies):
|
|
117
|
+
antibodies = [str(s) for s in np.linspace(0,nbr_of_wells-1,nbr_of_wells)]
|
|
118
|
+
|
|
119
|
+
return np.array([dtype(c) for c in antibodies])
|
|
120
|
+
|
|
121
|
+
def get_experiment_pharmaceutical_agents(experiment, dtype=str):
|
|
122
|
+
|
|
123
|
+
config = get_config(experiment)
|
|
124
|
+
wells = get_experiment_wells(experiment)
|
|
125
|
+
nbr_of_wells = len(wells)
|
|
126
|
+
|
|
127
|
+
pharmaceutical_agents = ConfigSectionMap(config,"Labels")["pharmaceutical_agents"].split(",")
|
|
128
|
+
if nbr_of_wells != len(pharmaceutical_agents):
|
|
129
|
+
pharmaceutical_agents = [str(s) for s in np.linspace(0,nbr_of_wells-1,nbr_of_wells)]
|
|
130
|
+
|
|
131
|
+
return np.array([dtype(c) for c in pharmaceutical_agents])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _interpret_wells_and_positions(experiment, well_option, position_option):
|
|
135
|
+
|
|
136
|
+
wells = get_experiment_wells(experiment)
|
|
137
|
+
nbr_of_wells = len(wells)
|
|
138
|
+
|
|
139
|
+
if well_option=='*':
|
|
140
|
+
well_indices = np.arange(len(wells))
|
|
141
|
+
elif isinstance(well_option, int):
|
|
142
|
+
well_indices = np.array([well_option], dtype=int)
|
|
143
|
+
elif isinstance(well_option, list):
|
|
144
|
+
well_indices = well_option
|
|
145
|
+
|
|
146
|
+
if position_option=='*':
|
|
147
|
+
position_indices = None
|
|
148
|
+
elif isinstance(position_option, int):
|
|
149
|
+
position_indices = np.array([position_option], dtype=int)
|
|
150
|
+
elif isinstance(position_option, list):
|
|
151
|
+
position_indices = position_option
|
|
152
|
+
|
|
153
|
+
return well_indices, position_indices
|
|
154
|
+
|
|
155
|
+
def extract_well_name_and_number(well):
|
|
156
|
+
|
|
157
|
+
split_well_path = well.split(os.sep)
|
|
158
|
+
split_well_path = list(filter(None, split_well_path))
|
|
159
|
+
well_name = split_well_path[-1]
|
|
160
|
+
well_number = int(split_well_path[-1].replace('W',''))
|
|
161
|
+
|
|
162
|
+
return well_name, well_number
|
|
163
|
+
|
|
164
|
+
def extract_position_name(pos):
|
|
165
|
+
|
|
166
|
+
split_pos_path = pos.split(os.sep)
|
|
167
|
+
split_pos_path = list(filter(None, split_pos_path))
|
|
168
|
+
pos_name = split_pos_path[-1]
|
|
169
|
+
|
|
170
|
+
return pos_name
|
|
171
|
+
|
|
172
|
+
def get_position_table(pos, population, return_path=False):
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
Retrieves the data table for a specified population at a given position, optionally returning the table's file path.
|
|
176
|
+
|
|
177
|
+
This function locates and loads a CSV data table associated with a specific population (e.g., 'targets', 'cells')
|
|
178
|
+
from a specified position directory. The position directory should contain an 'output/tables' subdirectory where
|
|
179
|
+
the CSV file named 'trajectories_{population}.csv' is expected to be found. If the file exists, it is loaded into
|
|
180
|
+
a pandas DataFrame; otherwise, None is returned.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
pos : str
|
|
185
|
+
The path to the position directory from which to load the data table.
|
|
186
|
+
population : str
|
|
187
|
+
The name of the population for which the data table is to be retrieved. This name is used to construct the
|
|
188
|
+
file name of the CSV file to be loaded.
|
|
189
|
+
return_path : bool, optional
|
|
190
|
+
If True, returns a tuple containing the loaded data table (or None) and the path to the CSV file. If False,
|
|
191
|
+
only the loaded data table (or None) is returned (default is False).
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
pandas.DataFrame or None, or (pandas.DataFrame or None, str)
|
|
196
|
+
If return_path is False, returns the loaded data table as a pandas DataFrame, or None if the table file does
|
|
197
|
+
not exist. If return_path is True, returns a tuple where the first element is the data table (or None) and the
|
|
198
|
+
second element is the path to the CSV file.
|
|
199
|
+
|
|
200
|
+
Examples
|
|
201
|
+
--------
|
|
202
|
+
>>> df_pos = get_position_table('/path/to/position', 'targets')
|
|
203
|
+
# This will load the 'trajectories_targets.csv' table from the specified position directory into a pandas DataFrame.
|
|
204
|
+
|
|
205
|
+
>>> df_pos, table_path = get_position_table('/path/to/position', 'targets', return_path=True)
|
|
206
|
+
# This will load the 'trajectories_targets.csv' table and also return the path to the CSV file.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
if not pos.endswith(os.sep):
|
|
211
|
+
table = os.sep.join([pos,'output','tables',f'trajectories_{population}.csv'])
|
|
212
|
+
else:
|
|
213
|
+
table = pos + os.sep.join(['output','tables',f'trajectories_{population}.csv'])
|
|
214
|
+
|
|
215
|
+
if os.path.exists(table):
|
|
216
|
+
df_pos = pd.read_csv(table, low_memory=False)
|
|
217
|
+
else:
|
|
218
|
+
df_pos = None
|
|
219
|
+
|
|
220
|
+
if return_path:
|
|
221
|
+
return df_pos, table
|
|
222
|
+
else:
|
|
223
|
+
return df_pos
|
|
224
|
+
|
|
225
|
+
def get_position_movie_path(pos, prefix=''):
|
|
226
|
+
|
|
227
|
+
if not pos.endswith(os.sep):
|
|
228
|
+
pos+=os.sep
|
|
229
|
+
movies = glob(pos+os.sep.join(['movie',prefix+'*.tif']))
|
|
230
|
+
if len(movies)>0:
|
|
231
|
+
stack_path = movies[0]
|
|
232
|
+
else:
|
|
233
|
+
stack_path = np.nan
|
|
234
|
+
|
|
235
|
+
return stack_path
|
|
236
|
+
|
|
237
|
+
def load_experiment_tables(experiment, population='targets', well_option='*',position_option='*', return_pos_info=False):
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
Loads and aggregates data tables for specified wells and positions within an experiment,
|
|
242
|
+
optionally returning position information alongside the aggregated data table.
|
|
243
|
+
|
|
244
|
+
This function collects data from tables associated with specific population types across
|
|
245
|
+
various wells and positions within an experiment. It uses the experiment's configuration
|
|
246
|
+
to gather metadata such as movie prefix, concentrations, cell types, antibodies, and
|
|
247
|
+
pharmaceutical agents. Users can specify which wells and positions to include in the
|
|
248
|
+
aggregation through pattern matching, and whether to include detailed position information
|
|
249
|
+
in the output.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
experiment : str
|
|
254
|
+
The path to the experiment directory.
|
|
255
|
+
population : str, optional
|
|
256
|
+
The population type to filter the tables by (default is 'targets' among 'targets and "effectors').
|
|
257
|
+
well_option : str, optional
|
|
258
|
+
A pattern to specify which wells to include (default is '*', which includes all wells).
|
|
259
|
+
position_option : str, optional
|
|
260
|
+
A pattern to specify which positions to include (default is '*', which includes all positions).
|
|
261
|
+
return_pos_info : bool, optional
|
|
262
|
+
If True, returns a tuple where the first element is the aggregated data table and the
|
|
263
|
+
second element is detailed position information (default is False).
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
pandas.DataFrame or (pandas.DataFrame, pandas.DataFrame)
|
|
268
|
+
If return_pos_info is False, returns a pandas DataFrame aggregating the data from the
|
|
269
|
+
specified tables. If return_pos_info is True, returns a tuple where the first element
|
|
270
|
+
is the aggregated data table and the second element is a DataFrame with detailed position
|
|
271
|
+
information.
|
|
272
|
+
|
|
273
|
+
Raises
|
|
274
|
+
------
|
|
275
|
+
FileNotFoundError
|
|
276
|
+
If the experiment directory does not exist or specified files within the directory cannot be found.
|
|
277
|
+
ValueError
|
|
278
|
+
If the specified well or position patterns do not match any directories.
|
|
279
|
+
|
|
280
|
+
Notes
|
|
281
|
+
-----
|
|
282
|
+
- This function assumes that the naming conventions and directory structure of the experiment
|
|
283
|
+
follow a specific format, as outlined in the experiment's configuration file.
|
|
284
|
+
- The function utilizes several helper functions to extract metadata, interpret well and
|
|
285
|
+
position patterns, and load individual position tables. Errors in these helper functions
|
|
286
|
+
may propagate up and affect the behavior of this function.
|
|
287
|
+
|
|
288
|
+
Examples
|
|
289
|
+
--------
|
|
290
|
+
>>> load_experiment_tables('/path/to/experiment', population='targets', well_option='W1', position_option='1-*')
|
|
291
|
+
# This will load and aggregate tables for the 'targets' population within well 'W1' and positions matching '1-*'.
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
config = get_config(experiment)
|
|
297
|
+
wells = get_experiment_wells(experiment)
|
|
298
|
+
|
|
299
|
+
movie_prefix = ConfigSectionMap(config,"MovieSettings")["movie_prefix"]
|
|
300
|
+
concentrations = get_experiment_concentrations(experiment, dtype=float)
|
|
301
|
+
cell_types = get_experiment_cell_types(experiment)
|
|
302
|
+
antibodies = get_experiment_antibodies(experiment)
|
|
303
|
+
pharmaceutical_agents = get_experiment_pharmaceutical_agents(experiment)
|
|
304
|
+
well_labels = _extract_labels_from_config(config,len(wells))
|
|
305
|
+
|
|
306
|
+
well_indices, position_indices = _interpret_wells_and_positions(experiment, well_option, position_option)
|
|
307
|
+
|
|
308
|
+
df = []
|
|
309
|
+
df_pos_info = []
|
|
310
|
+
real_well_index = 0
|
|
311
|
+
|
|
312
|
+
for widx, well_path in enumerate(tqdm(wells[well_indices])):
|
|
313
|
+
|
|
314
|
+
any_table = False # assume no table
|
|
315
|
+
|
|
316
|
+
well_index = widx
|
|
317
|
+
well_name, well_number = extract_well_name_and_number(well_path)
|
|
318
|
+
well_alias = well_labels[widx]
|
|
319
|
+
|
|
320
|
+
well_concentration = concentrations[widx]
|
|
321
|
+
well_antibody = antibodies[widx]
|
|
322
|
+
well_cell_type = cell_types[widx]
|
|
323
|
+
well_pharmaceutical_agent = pharmaceutical_agents[widx]
|
|
324
|
+
|
|
325
|
+
positions = np.array(natsorted(glob(well_path+'*'+os.sep)),dtype=str)
|
|
326
|
+
if position_indices is not None:
|
|
327
|
+
try:
|
|
328
|
+
positions = positions[position_indices]
|
|
329
|
+
except Exception as e:
|
|
330
|
+
print(e)
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
real_pos_index = 0
|
|
334
|
+
for pidx,pos_path in enumerate(positions):
|
|
335
|
+
|
|
336
|
+
pos_name = extract_position_name(pos_path)
|
|
337
|
+
|
|
338
|
+
stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
|
|
339
|
+
|
|
340
|
+
df_pos,table = get_position_table(pos_path, population=population, return_path=True)
|
|
341
|
+
if df_pos is not None:
|
|
342
|
+
|
|
343
|
+
df_pos['position'] = pos_path
|
|
344
|
+
df_pos['well'] = well_path
|
|
345
|
+
df_pos['well_index'] = well_number
|
|
346
|
+
df_pos['well_name'] = well_name
|
|
347
|
+
df_pos['pos_name'] = pos_name
|
|
348
|
+
|
|
349
|
+
df_pos['concentration'] = well_concentration
|
|
350
|
+
df_pos['antibody'] = well_antibody
|
|
351
|
+
df_pos['cell_type'] = well_cell_type
|
|
352
|
+
df_pos['pharmaceutical_agent'] = well_pharmaceutical_agent
|
|
353
|
+
|
|
354
|
+
df.append(df_pos)
|
|
355
|
+
any_table = True
|
|
356
|
+
|
|
357
|
+
df_pos_info.append({'pos_path': pos_path, 'pos_index': real_pos_index, 'pos_name': pos_name, 'table_path': table, 'stack_path': stack_path,
|
|
358
|
+
'well_path': well_path, 'well_index': real_well_index, 'well_name': well_name, 'well_number': well_number, 'well_alias': well_alias})
|
|
359
|
+
|
|
360
|
+
real_pos_index+=1
|
|
361
|
+
|
|
362
|
+
if any_table:
|
|
363
|
+
real_well_index += 1
|
|
364
|
+
|
|
365
|
+
df_pos_info = pd.DataFrame(df_pos_info)
|
|
366
|
+
if len(df)>0:
|
|
367
|
+
df = pd.concat(df)
|
|
368
|
+
df = df.reset_index(drop=True)
|
|
369
|
+
else:
|
|
370
|
+
df = None
|
|
371
|
+
|
|
372
|
+
if return_pos_info:
|
|
373
|
+
return df, df_pos_info
|
|
374
|
+
else:
|
|
375
|
+
return df
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def locate_stack(position, prefix='Aligned'):
|
|
380
|
+
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
Locate and load a stack of images.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
position : str
|
|
388
|
+
The position folder within the well where the stack is located.
|
|
389
|
+
prefix : str, optional
|
|
390
|
+
The prefix used to identify the stack. The default is 'Aligned'.
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
stack : ndarray
|
|
395
|
+
The loaded stack as a NumPy array.
|
|
396
|
+
|
|
397
|
+
Raises
|
|
398
|
+
------
|
|
399
|
+
AssertionError
|
|
400
|
+
If no stack with the specified prefix is found.
|
|
401
|
+
|
|
402
|
+
Notes
|
|
403
|
+
-----
|
|
404
|
+
This function locates and loads a stack of images based on the specified position and prefix.
|
|
405
|
+
It assumes that the stack is stored in a directory named 'movie' within the specified position.
|
|
406
|
+
The function loads the stack as a NumPy array and performs shape manipulation to have the channels
|
|
407
|
+
at the end.
|
|
408
|
+
|
|
409
|
+
Examples
|
|
410
|
+
--------
|
|
411
|
+
>>> stack = locate_stack(position, prefix='Aligned')
|
|
412
|
+
# Locate and load a stack of images for further processing.
|
|
413
|
+
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
stack_path = glob(position+os.sep.join(['movie', f'{prefix}*.tif']))
|
|
417
|
+
assert len(stack_path)>0,f"No movie with prefix {prefix} found..."
|
|
418
|
+
stack = imread(stack_path[0].replace('\\','/'))
|
|
419
|
+
if stack.ndim==4:
|
|
420
|
+
stack = np.moveaxis(stack, 1, -1)
|
|
421
|
+
elif stack.ndim==3:
|
|
422
|
+
stack = stack[:,:,:,np.newaxis]
|
|
423
|
+
|
|
424
|
+
return stack
|
|
425
|
+
|
|
426
|
+
def locate_labels(position, population='target'):
|
|
427
|
+
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
Locate and load labels for a specific population.
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
position : str
|
|
435
|
+
The position folder within the well where the stack is located.
|
|
436
|
+
population : str, optional
|
|
437
|
+
The population for which to locate the labels.
|
|
438
|
+
Valid options are 'target' and 'effector'.
|
|
439
|
+
The default is 'target'.
|
|
440
|
+
|
|
441
|
+
Returns
|
|
442
|
+
-------
|
|
443
|
+
labels : ndarray
|
|
444
|
+
The loaded labels as a NumPy array.
|
|
445
|
+
|
|
446
|
+
Notes
|
|
447
|
+
-----
|
|
448
|
+
This function locates and loads the labels for a specific population based on the specified position.
|
|
449
|
+
It assumes that the labels are stored in a directory named 'labels' or 'labels_effectors'
|
|
450
|
+
within the specified position, depending on the population.
|
|
451
|
+
The function loads the labels as a NumPy array.
|
|
452
|
+
|
|
453
|
+
Examples
|
|
454
|
+
--------
|
|
455
|
+
>>> labels = locate_labels(position, population='target')
|
|
456
|
+
# Locate and load labels for the target population.
|
|
457
|
+
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
if population.lower()=="target" or population.lower()=="targets":
|
|
462
|
+
label_path = natsorted(glob(position+os.sep.join(["labels_targets", "*.tif"])))
|
|
463
|
+
elif population.lower()=="effector" or population.lower()=="effectors":
|
|
464
|
+
label_path = natsorted(glob(position+os.sep.join(["labels_effectors", "*.tif"])))
|
|
465
|
+
labels = np.array([imread(i.replace('\\','/')) for i in label_path])
|
|
466
|
+
|
|
467
|
+
return labels
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def locate_stack_and_labels(position, prefix='Aligned', population="target"):
|
|
472
|
+
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
Locate and load the stack and corresponding segmentation labels.
|
|
476
|
+
|
|
477
|
+
Parameters
|
|
478
|
+
----------
|
|
479
|
+
position : str
|
|
480
|
+
The position or directory path where the stack and labels are located.
|
|
481
|
+
prefix : str, optional
|
|
482
|
+
The prefix used to identify the stack. The default is 'Aligned'.
|
|
483
|
+
population : str, optional
|
|
484
|
+
The population for which the segmentation must be located. The default is 'target'.
|
|
485
|
+
|
|
486
|
+
Returns
|
|
487
|
+
-------
|
|
488
|
+
stack : ndarray
|
|
489
|
+
The loaded stack as a NumPy array.
|
|
490
|
+
labels : ndarray
|
|
491
|
+
The loaded segmentation labels as a NumPy array.
|
|
492
|
+
|
|
493
|
+
Raises
|
|
494
|
+
------
|
|
495
|
+
AssertionError
|
|
496
|
+
If no stack with the specified prefix is found or if the shape of the stack and labels do not match.
|
|
497
|
+
|
|
498
|
+
Notes
|
|
499
|
+
-----
|
|
500
|
+
This function locates the stack and corresponding segmentation labels based on the specified position and population.
|
|
501
|
+
It assumes that the stack and labels are stored in separate directories: 'movie' for the stack and 'labels' or 'labels_effectors' for the labels.
|
|
502
|
+
The function loads the stack and labels as NumPy arrays and performs shape validation.
|
|
503
|
+
|
|
504
|
+
Examples
|
|
505
|
+
--------
|
|
506
|
+
>>> stack, labels = locate_stack_and_labels(position, prefix='Aligned', population="target")
|
|
507
|
+
# Locate and load the stack and segmentation labels for further processing.
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
position = position.replace('\\','/')
|
|
512
|
+
labels = locate_labels(position, population=population)
|
|
513
|
+
stack = locate_stack(position, prefix=prefix)
|
|
514
|
+
assert len(stack)==len(labels),f"The shape of the stack {stack.shape} does not match with the shape of the labels {labels.shape}"
|
|
515
|
+
|
|
516
|
+
return stack,labels
|
|
517
|
+
|
|
518
|
+
def load_tracking_data(position, prefix="Aligned", population="target"):
|
|
519
|
+
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
Load the tracking data, labels, and stack for a given position and population.
|
|
523
|
+
|
|
524
|
+
Parameters
|
|
525
|
+
----------
|
|
526
|
+
position : str
|
|
527
|
+
The position or directory where the data is located.
|
|
528
|
+
prefix : str, optional
|
|
529
|
+
The prefix used in the filenames of the stack images (default is "Aligned").
|
|
530
|
+
population : str, optional
|
|
531
|
+
The population to load the data for. Options are "target" or "effector" (default is "target").
|
|
532
|
+
|
|
533
|
+
Returns
|
|
534
|
+
-------
|
|
535
|
+
trajectories : DataFrame
|
|
536
|
+
The tracking data loaded as a pandas DataFrame.
|
|
537
|
+
labels : ndarray
|
|
538
|
+
The segmentation labels loaded as a numpy ndarray.
|
|
539
|
+
stack : ndarray
|
|
540
|
+
The image stack loaded as a numpy ndarray.
|
|
541
|
+
|
|
542
|
+
Notes
|
|
543
|
+
-----
|
|
544
|
+
This function loads the tracking data, labels, and stack for a given position and population.
|
|
545
|
+
It reads the trajectories from the appropriate CSV file based on the specified population.
|
|
546
|
+
The stack and labels are located using the `locate_stack_and_labels` function.
|
|
547
|
+
The resulting tracking data is returned as a pandas DataFrame, and the labels and stack are returned as numpy ndarrays.
|
|
548
|
+
|
|
549
|
+
Examples
|
|
550
|
+
--------
|
|
551
|
+
>>> trajectories, labels, stack = load_tracking_data(position, population="target")
|
|
552
|
+
# Load the tracking data, labels, and stack for the specified position and target population.
|
|
553
|
+
|
|
554
|
+
"""
|
|
555
|
+
|
|
556
|
+
position = position.replace('\\','/')
|
|
557
|
+
if population.lower()=="target" or population.lower()=="targets":
|
|
558
|
+
trajectories = pd.read_csv(position+os.sep.join(['output', 'tables', 'trajectories_targets.csv']))
|
|
559
|
+
elif population.lower()=="effector" or population.lower()=="effectors":
|
|
560
|
+
trajectories = pd.read_csv(position+os.sep.join(['output', 'tables', 'trajectories_effectors.csv']))
|
|
561
|
+
|
|
562
|
+
stack,labels = locate_stack_and_labels(position, prefix=prefix, population=population)
|
|
563
|
+
|
|
564
|
+
return trajectories,labels,stack
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def auto_load_number_of_frames(stack_path):
|
|
568
|
+
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
Automatically estimate the number of frames in a stack.
|
|
572
|
+
|
|
573
|
+
Parameters
|
|
574
|
+
----------
|
|
575
|
+
stack_path : str
|
|
576
|
+
The file path to the stack.
|
|
577
|
+
|
|
578
|
+
Returns
|
|
579
|
+
-------
|
|
580
|
+
int or None
|
|
581
|
+
The estimated number of frames in the stack. Returns None if the number of frames cannot be determined.
|
|
582
|
+
|
|
583
|
+
Notes
|
|
584
|
+
-----
|
|
585
|
+
This function attempts to estimate the number of frames in a stack by parsing the image description metadata.
|
|
586
|
+
It reads the stack file using the TiffFile from the tifffile library.
|
|
587
|
+
It searches for metadata fields containing information about the number of slices or frames.
|
|
588
|
+
If the number of slices or frames is found, it returns the estimated length of the movie.
|
|
589
|
+
If the number of slices or frames cannot be determined, it returns None.
|
|
590
|
+
|
|
591
|
+
Examples
|
|
592
|
+
--------
|
|
593
|
+
>>> len_movie = auto_load_number_of_frames(stack_path)
|
|
594
|
+
# Automatically estimate the number of frames in the stack.
|
|
595
|
+
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
# Try to estimate automatically # frames
|
|
599
|
+
stack_path = stack_path.replace('\\','/')
|
|
600
|
+
|
|
601
|
+
with TiffFile(stack_path) as tif:
|
|
602
|
+
try:
|
|
603
|
+
tif_tags = {}
|
|
604
|
+
for tag in tif.pages[0].tags.values():
|
|
605
|
+
name, value = tag.name, tag.value
|
|
606
|
+
tif_tags[name] = value
|
|
607
|
+
img_desc = tif_tags["ImageDescription"]
|
|
608
|
+
attr = img_desc.split("\n")
|
|
609
|
+
except:
|
|
610
|
+
pass
|
|
611
|
+
try:
|
|
612
|
+
# Try nframes
|
|
613
|
+
nslices = int(attr[np.argmax([s.startswith("frames") for s in attr])].split("=")[-1])
|
|
614
|
+
if nslices>1:
|
|
615
|
+
len_movie = nslices
|
|
616
|
+
print(f"Auto-detected movie length movie: {len_movie}")
|
|
617
|
+
else:
|
|
618
|
+
break_the_code()
|
|
619
|
+
except:
|
|
620
|
+
try:
|
|
621
|
+
# try nslices
|
|
622
|
+
frames = int(attr[np.argmax([s.startswith("slices") for s in attr])].split("=")[-1])
|
|
623
|
+
len_movie = frames
|
|
624
|
+
print(f"Auto-detected movie length movie: {len_movie}")
|
|
625
|
+
except:
|
|
626
|
+
pass
|
|
627
|
+
|
|
628
|
+
try:
|
|
629
|
+
del tif;
|
|
630
|
+
del tif_tags;
|
|
631
|
+
del img_desc;
|
|
632
|
+
except:
|
|
633
|
+
pass
|
|
634
|
+
gc.collect()
|
|
635
|
+
|
|
636
|
+
return len_movie if 'len_movie' in locals() else None
|
|
637
|
+
|
|
638
|
+
def parse_isotropic_radii(string):
|
|
639
|
+
sections = re.split(',| ', string)
|
|
640
|
+
radii = []
|
|
641
|
+
for k,s in enumerate(sections):
|
|
642
|
+
if s.isdigit():
|
|
643
|
+
radii.append(int(s))
|
|
644
|
+
if '[' in s:
|
|
645
|
+
ring = [int(s.replace('[','')), int(sections[k+1].replace(']',''))]
|
|
646
|
+
radii.append(ring)
|
|
647
|
+
else:
|
|
648
|
+
pass
|
|
649
|
+
return radii
|
|
650
|
+
|
|
651
|
+
def get_tracking_configs_list(return_path=False):
|
|
652
|
+
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
Retrieve a list of available tracking configurations.
|
|
656
|
+
|
|
657
|
+
Parameters
|
|
658
|
+
----------
|
|
659
|
+
return_path : bool, optional
|
|
660
|
+
If True, also returns the path to the models. Default is False.
|
|
661
|
+
|
|
662
|
+
Returns
|
|
663
|
+
-------
|
|
664
|
+
list or tuple
|
|
665
|
+
If return_path is False, returns a list of available tracking configurations.
|
|
666
|
+
If return_path is True, returns a tuple containing the list of models and the path to the models.
|
|
667
|
+
|
|
668
|
+
Notes
|
|
669
|
+
-----
|
|
670
|
+
This function retrieves the list of available tracking configurations by searching for model directories
|
|
671
|
+
in the predefined model path. The model path is derived from the parent directory of the current script
|
|
672
|
+
location and the path to the model directory. By default, it returns only the names of the models.
|
|
673
|
+
If return_path is set to True, it also returns the path to the models.
|
|
674
|
+
|
|
675
|
+
Examples
|
|
676
|
+
--------
|
|
677
|
+
>>> models = get_tracking_configs_list()
|
|
678
|
+
# Retrieve a list of available tracking configurations.
|
|
679
|
+
|
|
680
|
+
>>> models, path = get_tracking_configs_list(return_path=True)
|
|
681
|
+
# Retrieve a list of available tracking configurations.
|
|
682
|
+
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "tracking_configs", os.sep])
|
|
686
|
+
available_models = glob(modelpath+'*.json')
|
|
687
|
+
available_models = [m.replace('\\','/').split('/')[-1] for m in available_models]
|
|
688
|
+
available_models = [m.replace('\\','/').split('.')[0] for m in available_models]
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
if not return_path:
|
|
692
|
+
return available_models
|
|
693
|
+
else:
|
|
694
|
+
return available_models, modelpath
|
|
695
|
+
|
|
696
|
+
def interpret_tracking_configuration(config):
|
|
697
|
+
|
|
698
|
+
if isinstance(config, str):
|
|
699
|
+
if os.path.exists(config):
|
|
700
|
+
return config
|
|
701
|
+
else:
|
|
702
|
+
modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "tracking_configs", os.sep])
|
|
703
|
+
if os.path.exists(modelpath+config+'.json'):
|
|
704
|
+
return modelpath+config+'.json'
|
|
705
|
+
else:
|
|
706
|
+
config = cell_config()
|
|
707
|
+
elif config is None:
|
|
708
|
+
config = cell_config()
|
|
709
|
+
|
|
710
|
+
return config
|
|
711
|
+
|
|
712
|
+
def get_signal_models_list(return_path=False):
|
|
713
|
+
|
|
714
|
+
"""
|
|
715
|
+
|
|
716
|
+
Retrieve a list of available signal detection models.
|
|
717
|
+
|
|
718
|
+
Parameters
|
|
719
|
+
----------
|
|
720
|
+
return_path : bool, optional
|
|
721
|
+
If True, also returns the path to the models. Default is False.
|
|
722
|
+
|
|
723
|
+
Returns
|
|
724
|
+
-------
|
|
725
|
+
list or tuple
|
|
726
|
+
If return_path is False, returns a list of available signal detection models.
|
|
727
|
+
If return_path is True, returns a tuple containing the list of models and the path to the models.
|
|
728
|
+
|
|
729
|
+
Notes
|
|
730
|
+
-----
|
|
731
|
+
This function retrieves the list of available signal detection models by searching for model directories
|
|
732
|
+
in the predefined model path. The model path is derived from the parent directory of the current script
|
|
733
|
+
location and the path to the model directory. By default, it returns only the names of the models.
|
|
734
|
+
If return_path is set to True, it also returns the path to the models.
|
|
735
|
+
|
|
736
|
+
Examples
|
|
737
|
+
--------
|
|
738
|
+
>>> models = get_signal_models_list()
|
|
739
|
+
# Retrieve a list of available signal detection models.
|
|
740
|
+
|
|
741
|
+
>>> models, path = get_signal_models_list(return_path=True)
|
|
742
|
+
# Retrieve a list of available signal detection models and the path to the models.
|
|
743
|
+
|
|
744
|
+
"""
|
|
745
|
+
|
|
746
|
+
modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "signal_detection", os.sep])
|
|
747
|
+
repository_models = get_zenodo_files(cat=os.sep.join(["models","signal_detection"]))
|
|
748
|
+
|
|
749
|
+
available_models = glob(modelpath+f'*{os.sep}')
|
|
750
|
+
available_models = [m.replace('\\','/').split('/')[-2] for m in available_models]
|
|
751
|
+
for rm in repository_models:
|
|
752
|
+
if rm not in available_models:
|
|
753
|
+
available_models.append(rm)
|
|
754
|
+
|
|
755
|
+
if not return_path:
|
|
756
|
+
return available_models
|
|
757
|
+
else:
|
|
758
|
+
return available_models, modelpath
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def locate_signal_model(name):
|
|
762
|
+
|
|
763
|
+
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
|
|
764
|
+
modelpath = os.sep.join([main_dir, "models", "signal_detection", os.sep])
|
|
765
|
+
print(f'Looking for {name} in {modelpath}')
|
|
766
|
+
models = glob(modelpath+f'*{os.sep}')
|
|
767
|
+
|
|
768
|
+
match=None
|
|
769
|
+
for m in models:
|
|
770
|
+
if name==m.replace('\\',os.sep).split(os.sep)[-2]:
|
|
771
|
+
match = m
|
|
772
|
+
return match
|
|
773
|
+
# else no match, try zenodo
|
|
774
|
+
files, categories = get_zenodo_files()
|
|
775
|
+
if name in files:
|
|
776
|
+
index = files.index(name)
|
|
777
|
+
cat = categories[index]
|
|
778
|
+
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
779
|
+
match = os.sep.join([main_dir, cat, name])+os.sep
|
|
780
|
+
return match
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def relabel_segmentation(labels, data, properties, column_labels={'track': "track", 'frame': 'frame', 'y': 'y', 'x': 'x', 'label': 'class_id'}, threads=1):
|
|
784
|
+
|
|
785
|
+
"""
|
|
786
|
+
|
|
787
|
+
Relabel the segmentation labels based on the provided tracking data and properties.
|
|
788
|
+
|
|
789
|
+
Parameters
|
|
790
|
+
----------
|
|
791
|
+
labels : ndarray
|
|
792
|
+
The original segmentation labels.
|
|
793
|
+
data : ndarray
|
|
794
|
+
The tracking data containing information about tracks, frames, y-coordinates, and x-coordinates.
|
|
795
|
+
properties : ndarray
|
|
796
|
+
The properties associated with the tracking data.
|
|
797
|
+
column_labels : dict, optional
|
|
798
|
+
A dictionary specifying the column labels for the tracking data. The default is {'track': "track",
|
|
799
|
+
'frame': 'frame', 'y': 'y', 'x': 'x', 'label': 'class_id'}.
|
|
800
|
+
|
|
801
|
+
Returns
|
|
802
|
+
-------
|
|
803
|
+
ndarray
|
|
804
|
+
The relabeled segmentation labels.
|
|
805
|
+
|
|
806
|
+
Notes
|
|
807
|
+
-----
|
|
808
|
+
This function relabels the segmentation labels based on the provided tracking data and properties.
|
|
809
|
+
It creates a DataFrame from the tracking data and properties, merges them based on the indices, and sorts them by track and frame.
|
|
810
|
+
Then, it iterates over unique frames in the DataFrame, retrieves the tracks and identities at each frame,
|
|
811
|
+
and updates the corresponding labels with the new track values.
|
|
812
|
+
|
|
813
|
+
Examples
|
|
814
|
+
--------
|
|
815
|
+
>>> relabeled = relabel_segmentation(labels, data, properties, column_labels={'track': "track", 'frame': 'frame',
|
|
816
|
+
... 'y': 'y', 'x': 'x', 'label': 'class_id'})
|
|
817
|
+
# Relabel the segmentation labels based on the provided tracking data and properties.
|
|
818
|
+
|
|
819
|
+
"""
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
n_threads = threads
|
|
823
|
+
df = pd.DataFrame(data,columns=[column_labels['track'],column_labels['frame'],column_labels['y'],column_labels['x']])
|
|
824
|
+
df = df.merge(pd.DataFrame(properties),left_index=True, right_index=True)
|
|
825
|
+
df = df.sort_values(by=[column_labels['track'],column_labels['frame']])
|
|
826
|
+
|
|
827
|
+
new_labels = np.zeros_like(labels)
|
|
828
|
+
|
|
829
|
+
def rewrite_labels(indices):
|
|
830
|
+
|
|
831
|
+
for t in tqdm(indices):
|
|
832
|
+
f = int(t)
|
|
833
|
+
tracks_at_t = df.loc[df[column_labels['frame']]==f, column_labels['track']].to_numpy()
|
|
834
|
+
identities = df.loc[df[column_labels['frame']]==f, column_labels['label']].to_numpy()
|
|
835
|
+
|
|
836
|
+
tracks_at_t = tracks_at_t[identities==identities]
|
|
837
|
+
identities = identities[identities==identities]
|
|
838
|
+
|
|
839
|
+
for k in range(len(identities)):
|
|
840
|
+
loc_i,loc_j = np.where(labels[f]==identities[k])
|
|
841
|
+
new_labels[f,loc_i,loc_j] = int(tracks_at_t[k])
|
|
842
|
+
|
|
843
|
+
# Multithreading
|
|
844
|
+
indices = list(df[column_labels['frame']].unique())
|
|
845
|
+
chunks = np.array_split(indices, n_threads)
|
|
846
|
+
threads = []
|
|
847
|
+
for i in range(n_threads):
|
|
848
|
+
thread_i = threading.Thread(target=rewrite_labels, args=[chunks[i]])
|
|
849
|
+
threads.append(thread_i)
|
|
850
|
+
for th in threads:
|
|
851
|
+
th.start()
|
|
852
|
+
for th in threads:
|
|
853
|
+
th.join()
|
|
854
|
+
|
|
855
|
+
return new_labels
|
|
856
|
+
|
|
857
|
+
# def relabel_segmentation(labels, data, properties, column_labels={'track': "track", 'frame': 'frame', 'y': 'y', 'x': 'x', 'label': 'class_id'}, threads=1):
|
|
858
|
+
|
|
859
|
+
# """
|
|
860
|
+
|
|
861
|
+
# Relabel the segmentation labels based on the provided tracking data and properties.
|
|
862
|
+
|
|
863
|
+
# Parameters
|
|
864
|
+
# ----------
|
|
865
|
+
# labels : ndarray
|
|
866
|
+
# The original segmentation labels.
|
|
867
|
+
# data : ndarray
|
|
868
|
+
# The tracking data containing information about tracks, frames, y-coordinates, and x-coordinates.
|
|
869
|
+
# properties : ndarray
|
|
870
|
+
# The properties associated with the tracking data.
|
|
871
|
+
# column_labels : dict, optional
|
|
872
|
+
# A dictionary specifying the column labels for the tracking data. The default is {'track': "track",
|
|
873
|
+
# 'frame': 'frame', 'y': 'y', 'x': 'x', 'label': 'class_id'}.
|
|
874
|
+
|
|
875
|
+
# Returns
|
|
876
|
+
# -------
|
|
877
|
+
# ndarray
|
|
878
|
+
# The relabeled segmentation labels.
|
|
879
|
+
|
|
880
|
+
# Notes
|
|
881
|
+
# -----
|
|
882
|
+
# This function relabels the segmentation labels based on the provided tracking data and properties.
|
|
883
|
+
# It creates a DataFrame from the tracking data and properties, merges them based on the indices, and sorts them by track and frame.
|
|
884
|
+
# Then, it iterates over unique frames in the DataFrame, retrieves the tracks and identities at each frame,
|
|
885
|
+
# and updates the corresponding labels with the new track values.
|
|
886
|
+
|
|
887
|
+
# Examples
|
|
888
|
+
# --------
|
|
889
|
+
# >>> relabeled = relabel_segmentation(labels, data, properties, column_labels={'track': "track", 'frame': 'frame',
|
|
890
|
+
# ... 'y': 'y', 'x': 'x', 'label': 'class_id'})
|
|
891
|
+
# # Relabel the segmentation labels based on the provided tracking data and properties.
|
|
892
|
+
|
|
893
|
+
# """
|
|
894
|
+
|
|
895
|
+
# df = pd.DataFrame(data,columns=[column_labels['track'],column_labels['frame'],column_labels['y'],column_labels['x']])
|
|
896
|
+
# df = df.merge(pd.DataFrame(properties),left_index=True, right_index=True)
|
|
897
|
+
# df = df.sort_values(by=[column_labels['track'],column_labels['frame']])
|
|
898
|
+
|
|
899
|
+
# new_labels = np.zeros_like(labels)
|
|
900
|
+
|
|
901
|
+
# for t in tqdm(df[column_labels['frame']].unique()):
|
|
902
|
+
# f = int(t)
|
|
903
|
+
# tracks_at_t = df.loc[df[column_labels['frame']]==f, column_labels['track']].to_numpy()
|
|
904
|
+
# identities = df.loc[df[column_labels['frame']]==f, column_labels['label']].to_numpy()
|
|
905
|
+
|
|
906
|
+
# tracks_at_t = tracks_at_t[identities==identities]
|
|
907
|
+
# identities = identities[identities==identities]
|
|
908
|
+
|
|
909
|
+
# for k in range(len(identities)):
|
|
910
|
+
# loc_i,loc_j = np.where(labels[f]==identities[k])
|
|
911
|
+
# new_labels[f,loc_i,loc_j] = int(tracks_at_t[k])
|
|
912
|
+
|
|
913
|
+
# return new_labels
|
|
914
|
+
|
|
915
|
+
def control_tracking_btrack(position, prefix="Aligned", population="target", relabel=True, flush_memory=True, threads=1):
|
|
916
|
+
|
|
917
|
+
"""
|
|
918
|
+
Load the necessary data for visualization of bTrack trajectories in napari.
|
|
919
|
+
|
|
920
|
+
Parameters
|
|
921
|
+
----------
|
|
922
|
+
position : str
|
|
923
|
+
The path to the position directory.
|
|
924
|
+
prefix : str, optional
|
|
925
|
+
The prefix used to identify the movie file. The default is "Aligned".
|
|
926
|
+
population : str, optional
|
|
927
|
+
The population type to load, either "target" or "effector". The default is "target".
|
|
928
|
+
|
|
929
|
+
Returns
|
|
930
|
+
-------
|
|
931
|
+
None
|
|
932
|
+
This function displays the data in Napari for visualization and analysis.
|
|
933
|
+
|
|
934
|
+
Examples
|
|
935
|
+
--------
|
|
936
|
+
>>> control_tracking_btrack("path/to/position", population="target")
|
|
937
|
+
# Executes napari for visualization of target trajectories.
|
|
938
|
+
|
|
939
|
+
"""
|
|
940
|
+
|
|
941
|
+
data,properties,graph,labels,stack = load_napari_data(position, prefix=prefix, population=population)
|
|
942
|
+
view_on_napari_btrack(data,properties,graph,labels=labels, stack=stack, relabel=relabel, flush_memory=flush_memory, threads=threads)
|
|
943
|
+
|
|
944
|
+
def view_on_napari_btrack(data,properties,graph,stack=None,labels=None,relabel=True, flush_memory=True, position=None, threads=1):
|
|
945
|
+
|
|
946
|
+
"""
|
|
947
|
+
|
|
948
|
+
Visualize btrack data, including stack, labels, points, and tracks, using the napari viewer.
|
|
949
|
+
|
|
950
|
+
Parameters
|
|
951
|
+
----------
|
|
952
|
+
data : ndarray
|
|
953
|
+
The btrack data containing information about tracks.
|
|
954
|
+
properties : ndarray
|
|
955
|
+
The properties associated with the btrack data.
|
|
956
|
+
graph : Graph
|
|
957
|
+
The btrack graph containing information about track connections.
|
|
958
|
+
stack : ndarray, optional
|
|
959
|
+
The stack of images to visualize. The default is None.
|
|
960
|
+
labels : ndarray, optional
|
|
961
|
+
The segmentation labels to visualize. The default is None.
|
|
962
|
+
relabel : bool, optional
|
|
963
|
+
Specify whether to relabel the segmentation labels using the provided data. The default is True.
|
|
964
|
+
|
|
965
|
+
Notes
|
|
966
|
+
-----
|
|
967
|
+
This function visualizes btrack data using the napari viewer. It adds the stack, labels, points,
|
|
968
|
+
and tracks to the viewer for visualization. If `relabel` is True and labels are provided, it calls
|
|
969
|
+
the `relabel_segmentation` function to relabel the segmentation labels based on the provided data.
|
|
970
|
+
|
|
971
|
+
Examples
|
|
972
|
+
--------
|
|
973
|
+
>>> view_on_napari_btrack(data, properties, graph, stack=stack, labels=labels, relabel=True)
|
|
974
|
+
# Visualize btrack data, including stack, labels, points, and tracks, using the napari viewer.
|
|
975
|
+
|
|
976
|
+
"""
|
|
977
|
+
|
|
978
|
+
if (labels is not None)*relabel:
|
|
979
|
+
print('Relabeling the cell masks with the track ID.')
|
|
980
|
+
labels = relabel_segmentation(labels, data, properties, threads=threads)
|
|
981
|
+
|
|
982
|
+
vertices = data[:, 1:]
|
|
983
|
+
viewer = napari.Viewer()
|
|
984
|
+
if stack is not None:
|
|
985
|
+
viewer.add_image(stack,channel_axis=-1,colormap=["gray"]*stack.shape[-1])
|
|
986
|
+
if labels is not None:
|
|
987
|
+
viewer.add_labels(labels, name='segmentation',opacity=0.4)
|
|
988
|
+
viewer.add_points(vertices, size=4, name='points', opacity=0.3)
|
|
989
|
+
viewer.add_tracks(data, properties=properties, graph=graph, name='tracks')
|
|
990
|
+
viewer.show(block=True)
|
|
991
|
+
|
|
992
|
+
if flush_memory:
|
|
993
|
+
# temporary fix for slight napari memory leak
|
|
994
|
+
for i in range(10000):
|
|
995
|
+
try:
|
|
996
|
+
viewer.layers.pop()
|
|
997
|
+
except:
|
|
998
|
+
pass
|
|
999
|
+
|
|
1000
|
+
del viewer
|
|
1001
|
+
del stack
|
|
1002
|
+
del labels
|
|
1003
|
+
gc.collect()
|
|
1004
|
+
|
|
1005
|
+
def load_napari_data(position, prefix="Aligned", population="target"):
|
|
1006
|
+
|
|
1007
|
+
"""
|
|
1008
|
+
Load the necessary data for visualization in napari.
|
|
1009
|
+
|
|
1010
|
+
Parameters
|
|
1011
|
+
----------
|
|
1012
|
+
position : str
|
|
1013
|
+
The path to the position or experiment directory.
|
|
1014
|
+
prefix : str, optional
|
|
1015
|
+
The prefix used to identify the the movie file. The default is "Aligned".
|
|
1016
|
+
population : str, optional
|
|
1017
|
+
The population type to load, either "target" or "effector". The default is "target".
|
|
1018
|
+
|
|
1019
|
+
Returns
|
|
1020
|
+
-------
|
|
1021
|
+
tuple
|
|
1022
|
+
A tuple containing the loaded data, properties, graph, labels, and stack.
|
|
1023
|
+
|
|
1024
|
+
Examples
|
|
1025
|
+
--------
|
|
1026
|
+
>>> data, properties, graph, labels, stack = load_napari_data("path/to/position")
|
|
1027
|
+
# Load the necessary data for visualization of target trajectories.
|
|
1028
|
+
|
|
1029
|
+
"""
|
|
1030
|
+
position = position.replace('\\','/')
|
|
1031
|
+
if population.lower()=="target" or population.lower()=="targets":
|
|
1032
|
+
napari_data = np.load(position+os.sep.join(['output','tables','napari_target_trajectories.npy']), allow_pickle=True)
|
|
1033
|
+
elif population.lower()=="effector" or population.lower()=="effectors":
|
|
1034
|
+
napari_data = np.load(position+os.sep.join(['output', 'tables', 'napari_effector_trajectories.npy']), allow_pickle=True)
|
|
1035
|
+
data = napari_data.item()['data']
|
|
1036
|
+
properties = napari_data.item()['properties']
|
|
1037
|
+
graph = napari_data.item()['graph']
|
|
1038
|
+
|
|
1039
|
+
stack,labels = locate_stack_and_labels(position, prefix=prefix, population=population)
|
|
1040
|
+
|
|
1041
|
+
return data,properties,graph,labels,stack
|
|
1042
|
+
|
|
1043
|
+
from skimage.measure import label
|
|
1044
|
+
|
|
1045
|
+
def auto_correct_masks(masks):
|
|
1046
|
+
|
|
1047
|
+
"""
|
|
1048
|
+
Automatically corrects segmentation masks by splitting disconnected objects sharing the same label.
|
|
1049
|
+
|
|
1050
|
+
This function examines each labeled object in the input masks and splits objects whose bounding box
|
|
1051
|
+
area is significantly larger than their actual area, indicating potential merging of multiple objects.
|
|
1052
|
+
It uses geometric properties to identify such cases and applies a relabeling to separate merged objects.
|
|
1053
|
+
|
|
1054
|
+
Parameters
|
|
1055
|
+
----------
|
|
1056
|
+
masks : ndarray
|
|
1057
|
+
A 2D numpy array representing the segmentation masks, where each object is labeled with a unique integer.
|
|
1058
|
+
|
|
1059
|
+
Returns
|
|
1060
|
+
-------
|
|
1061
|
+
ndarray
|
|
1062
|
+
A 2D numpy array of the corrected segmentation masks with potentially merged objects separated and
|
|
1063
|
+
relabeled.
|
|
1064
|
+
|
|
1065
|
+
Notes
|
|
1066
|
+
-----
|
|
1067
|
+
- The function uses bounding box area and actual object area to identify potentially merged objects.
|
|
1068
|
+
Objects are considered potentially merged if their bounding box area is more than twice their actual area.
|
|
1069
|
+
- Relabeling of objects is done sequentially, adding to the maximum label number found in the original
|
|
1070
|
+
masks to ensure new labels do not overlap with existing ones.
|
|
1071
|
+
- This function relies on `skimage.measure.label` for relabeling and `skimage.measure.regionprops_table`
|
|
1072
|
+
for calculating object properties.
|
|
1073
|
+
|
|
1074
|
+
"""
|
|
1075
|
+
|
|
1076
|
+
props = pd.DataFrame(regionprops_table(masks,properties=('label','area','area_bbox')))
|
|
1077
|
+
max_lbl = props['label'].max()
|
|
1078
|
+
corrected_lbl = masks.copy().astype(int)
|
|
1079
|
+
|
|
1080
|
+
for cell in props['label'].unique():
|
|
1081
|
+
|
|
1082
|
+
bbox_area = props.loc[props['label']==cell, 'area_bbox'].values
|
|
1083
|
+
area = props.loc[props['label']==cell, 'area'].values
|
|
1084
|
+
|
|
1085
|
+
if bbox_area > 1.75*area: #condition for anomaly
|
|
1086
|
+
|
|
1087
|
+
lbl = masks==cell
|
|
1088
|
+
lbl = lbl.astype(int)
|
|
1089
|
+
|
|
1090
|
+
relabelled = label(lbl,connectivity=2)
|
|
1091
|
+
relabelled += max_lbl
|
|
1092
|
+
relabelled[np.where(lbl==0)] = 0
|
|
1093
|
+
|
|
1094
|
+
corrected_lbl[np.where(relabelled != 0)] = relabelled[np.where(relabelled!=0)]
|
|
1095
|
+
|
|
1096
|
+
max_lbl = np.amax(corrected_lbl)
|
|
1097
|
+
|
|
1098
|
+
return corrected_lbl
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def control_segmentation_napari(position, prefix='Aligned', population="target", flush_memory=False):
|
|
1103
|
+
|
|
1104
|
+
"""
|
|
1105
|
+
|
|
1106
|
+
Control the visualization of segmentation labels using the napari viewer.
|
|
1107
|
+
|
|
1108
|
+
Parameters
|
|
1109
|
+
----------
|
|
1110
|
+
position : str
|
|
1111
|
+
The position or directory path where the segmentation labels and stack are located.
|
|
1112
|
+
prefix : str, optional
|
|
1113
|
+
The prefix used to identify the stack. The default is 'Aligned'.
|
|
1114
|
+
population : str, optional
|
|
1115
|
+
The population type for which the segmentation is performed. The default is 'target'.
|
|
1116
|
+
|
|
1117
|
+
Notes
|
|
1118
|
+
-----
|
|
1119
|
+
This function loads the segmentation labels and stack corresponding to the specified position and population.
|
|
1120
|
+
It then creates a napari viewer and adds the stack and labels as layers for visualization.
|
|
1121
|
+
|
|
1122
|
+
Examples
|
|
1123
|
+
--------
|
|
1124
|
+
>>> control_segmentation_napari(position, prefix='Aligned', population="target")
|
|
1125
|
+
# Control the visualization of segmentation labels using the napari viewer.
|
|
1126
|
+
|
|
1127
|
+
"""
|
|
1128
|
+
|
|
1129
|
+
def export_labels():
|
|
1130
|
+
labels_layer = viewer.layers['segmentation'].data
|
|
1131
|
+
for t,im in enumerate(tqdm(labels_layer)):
|
|
1132
|
+
|
|
1133
|
+
try:
|
|
1134
|
+
im = auto_correct_masks(im)
|
|
1135
|
+
except Exception as e:
|
|
1136
|
+
print(e)
|
|
1137
|
+
|
|
1138
|
+
save_tiff_imagej_compatible(output_folder+f"{str(t).zfill(4)}.tif", im.astype(np.int16), axes='YX')
|
|
1139
|
+
print("The labels have been successfully rewritten.")
|
|
1140
|
+
|
|
1141
|
+
def export_annotation():
|
|
1142
|
+
|
|
1143
|
+
# Locate experiment config
|
|
1144
|
+
parent1 = Path(position).parent
|
|
1145
|
+
expfolder = parent1.parent
|
|
1146
|
+
config = PurePath(expfolder,Path("config.ini"))
|
|
1147
|
+
expfolder = str(expfolder)
|
|
1148
|
+
exp_name = os.path.split(expfolder)[-1]
|
|
1149
|
+
print(exp_name)
|
|
1150
|
+
|
|
1151
|
+
spatial_calibration = float(ConfigSectionMap(config,"MovieSettings")["pxtoum"])
|
|
1152
|
+
channel_names, channel_indices = extract_experiment_channels(config)
|
|
1153
|
+
|
|
1154
|
+
annotation_folder = expfolder + os.sep + f'annotations_{population}' + os.sep
|
|
1155
|
+
if not os.path.exists(annotation_folder):
|
|
1156
|
+
os.mkdir(annotation_folder)
|
|
1157
|
+
|
|
1158
|
+
print('exporting!')
|
|
1159
|
+
t = viewer.dims.current_step[0]
|
|
1160
|
+
labels_layer = viewer.layers['segmentation'].data[t] # at current time
|
|
1161
|
+
|
|
1162
|
+
try:
|
|
1163
|
+
labels_layer = auto_correct_masks(labels_layer)
|
|
1164
|
+
except Exception as e:
|
|
1165
|
+
print(e)
|
|
1166
|
+
|
|
1167
|
+
fov_export = True
|
|
1168
|
+
|
|
1169
|
+
if "Shapes" in viewer.layers:
|
|
1170
|
+
squares = viewer.layers['Shapes'].data
|
|
1171
|
+
test_in_frame = np.array([squares[i][0,0]==t and len(squares[i])==4 for i in range(len(squares))])
|
|
1172
|
+
squares = np.array(squares)
|
|
1173
|
+
squares = squares[test_in_frame]
|
|
1174
|
+
nbr_squares = len(squares)
|
|
1175
|
+
print(f"Found {nbr_squares} ROIS")
|
|
1176
|
+
if nbr_squares>0:
|
|
1177
|
+
# deactivate field of view mode
|
|
1178
|
+
fov_export = False
|
|
1179
|
+
|
|
1180
|
+
for k,sq in enumerate(squares):
|
|
1181
|
+
print(f"ROI: {sq}")
|
|
1182
|
+
xmin = int(sq[0,1])
|
|
1183
|
+
xmax = int(sq[2,1])
|
|
1184
|
+
if xmax<xmin:
|
|
1185
|
+
xmax,xmin = xmin,xmax
|
|
1186
|
+
ymin = int(sq[0,2])
|
|
1187
|
+
ymax = int(sq[1,2])
|
|
1188
|
+
if ymax<ymin:
|
|
1189
|
+
ymax,ymin = ymin,ymax
|
|
1190
|
+
print(f"{xmin=};{xmax=};{ymin=};{ymax=}")
|
|
1191
|
+
frame = viewer.layers['Image'].data[t][xmin:xmax,ymin:ymax]
|
|
1192
|
+
if frame.shape[1] < 256 or frame.shape[0] < 256:
|
|
1193
|
+
print("crop too small!")
|
|
1194
|
+
continue
|
|
1195
|
+
multichannel = [frame]
|
|
1196
|
+
for i in range(len(channel_indices)-1):
|
|
1197
|
+
try:
|
|
1198
|
+
frame = viewer.layers[f'Image [{i+1}]'].data[t][xmin:xmax,ymin:ymax]
|
|
1199
|
+
multichannel.append(frame)
|
|
1200
|
+
except:
|
|
1201
|
+
pass
|
|
1202
|
+
multichannel = np.array(multichannel)
|
|
1203
|
+
save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_roi_{xmin}_{xmax}_{ymin}_{ymax}_labelled.tif", labels_layer[xmin:xmax,ymin:ymax].astype(np.int16), axes='YX')
|
|
1204
|
+
save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_roi_{xmin}_{xmax}_{ymin}_{ymax}.tif", multichannel, axes='CYX')
|
|
1205
|
+
info = {"spatial_calibration": spatial_calibration, "channels": list(channel_names)}
|
|
1206
|
+
info_name = annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_roi_{xmin}_{xmax}_{ymin}_{ymax}.json"
|
|
1207
|
+
with open(info_name, 'w') as f:
|
|
1208
|
+
json.dump(info, f, indent=4)
|
|
1209
|
+
|
|
1210
|
+
if fov_export:
|
|
1211
|
+
frame = viewer.layers['Image'].data[t]
|
|
1212
|
+
multichannel = [frame]
|
|
1213
|
+
for i in range(len(channel_indices)-1):
|
|
1214
|
+
try:
|
|
1215
|
+
frame = viewer.layers[f'Image [{i+1}]'].data[t]
|
|
1216
|
+
multichannel.append(frame)
|
|
1217
|
+
except:
|
|
1218
|
+
pass
|
|
1219
|
+
multichannel = np.array(multichannel)
|
|
1220
|
+
save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_labelled.tif", labels_layer, axes='YX')
|
|
1221
|
+
save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}.tif", multichannel, axes='CYX')
|
|
1222
|
+
info = {"spatial_calibration": spatial_calibration, "channels": list(channel_names)}
|
|
1223
|
+
info_name = annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}.json"
|
|
1224
|
+
with open(info_name, 'w') as f:
|
|
1225
|
+
json.dump(info, f, indent=4)
|
|
1226
|
+
print('Done.')
|
|
1227
|
+
|
|
1228
|
+
@magicgui(call_button='Save the modified labels')
|
|
1229
|
+
def save_widget():
|
|
1230
|
+
return export_labels()
|
|
1231
|
+
|
|
1232
|
+
@magicgui(call_button='Export the annotation\nof the current frame')
|
|
1233
|
+
def export_widget():
|
|
1234
|
+
return export_annotation()
|
|
1235
|
+
|
|
1236
|
+
stack,labels = locate_stack_and_labels(position, prefix=prefix, population=population)
|
|
1237
|
+
|
|
1238
|
+
if not population.endswith('s'):
|
|
1239
|
+
population+='s'
|
|
1240
|
+
output_folder = position+f'labels_{population}{os.sep}'
|
|
1241
|
+
|
|
1242
|
+
viewer = napari.Viewer()
|
|
1243
|
+
viewer.add_image(stack,channel_axis=-1,colormap=["gray"]*stack.shape[-1])
|
|
1244
|
+
viewer.add_labels(labels.astype(int), name='segmentation',opacity=0.4)
|
|
1245
|
+
viewer.window.add_dock_widget(save_widget, area='right')
|
|
1246
|
+
viewer.window.add_dock_widget(export_widget, area='right')
|
|
1247
|
+
viewer.show(block=True)
|
|
1248
|
+
|
|
1249
|
+
if flush_memory:
|
|
1250
|
+
# temporary fix for slight napari memory leak
|
|
1251
|
+
for i in range(10000):
|
|
1252
|
+
try:
|
|
1253
|
+
viewer.layers.pop()
|
|
1254
|
+
except:
|
|
1255
|
+
pass
|
|
1256
|
+
|
|
1257
|
+
del viewer
|
|
1258
|
+
del stack
|
|
1259
|
+
del labels
|
|
1260
|
+
gc.collect()
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _view_on_napari(tracks=None, stack=None, labels=None):
|
|
1264
|
+
|
|
1265
|
+
"""
|
|
1266
|
+
|
|
1267
|
+
Visualize tracks, stack, and labels using Napari.
|
|
1268
|
+
|
|
1269
|
+
Parameters
|
|
1270
|
+
----------
|
|
1271
|
+
tracks : pandas DataFrame
|
|
1272
|
+
DataFrame containing track information.
|
|
1273
|
+
stack : numpy array, optional
|
|
1274
|
+
Stack of images with shape (T, Y, X, C), where T is the number of frames, Y and X are the spatial dimensions,
|
|
1275
|
+
and C is the number of channels. Default is None.
|
|
1276
|
+
labels : numpy array, optional
|
|
1277
|
+
Label stack with shape (T, Y, X) representing cell segmentations. Default is None.
|
|
1278
|
+
|
|
1279
|
+
Returns
|
|
1280
|
+
-------
|
|
1281
|
+
None
|
|
1282
|
+
|
|
1283
|
+
Notes
|
|
1284
|
+
-----
|
|
1285
|
+
This function visualizes tracks, stack, and labels using Napari, an interactive multi-dimensional image viewer.
|
|
1286
|
+
The tracks are represented as line segments on the viewer. If a stack is provided, it is displayed as an image.
|
|
1287
|
+
If labels are provided, they are displayed as a segmentation overlay on the stack.
|
|
1288
|
+
|
|
1289
|
+
Examples
|
|
1290
|
+
--------
|
|
1291
|
+
>>> tracks = pd.DataFrame({'track': [1, 2, 3], 'time': [1, 1, 1],
|
|
1292
|
+
... 'x': [10, 20, 30], 'y': [15, 25, 35]})
|
|
1293
|
+
>>> stack = np.random.rand(100, 100, 3)
|
|
1294
|
+
>>> labels = np.random.randint(0, 2, (100, 100))
|
|
1295
|
+
>>> view_on_napari(tracks, stack=stack, labels=labels)
|
|
1296
|
+
# Visualize tracks, stack, and labels using Napari.
|
|
1297
|
+
|
|
1298
|
+
"""
|
|
1299
|
+
|
|
1300
|
+
viewer = napari.Viewer()
|
|
1301
|
+
if stack is not None:
|
|
1302
|
+
viewer.add_image(stack,channel_axis=-1,colormap=["gray"]*stack.shape[-1])
|
|
1303
|
+
if labels is not None:
|
|
1304
|
+
viewer.add_labels(labels, name='segmentation',opacity=0.4)
|
|
1305
|
+
if tracks is not None:
|
|
1306
|
+
viewer.add_tracks(tracks, name='tracks')
|
|
1307
|
+
viewer.show(block=True)
|
|
1308
|
+
|
|
1309
|
+
def control_tracking_table(position, calibration=1, prefix="Aligned", population="target",
|
|
1310
|
+
column_labels={'track': "TRACK_ID", 'frame': 'FRAME', 'y': 'POSITION_Y', 'x': 'POSITION_X', 'label': 'class_id'}):
|
|
1311
|
+
|
|
1312
|
+
"""
|
|
1313
|
+
|
|
1314
|
+
Control the tracking table and visualize tracks using Napari.
|
|
1315
|
+
|
|
1316
|
+
Parameters
|
|
1317
|
+
----------
|
|
1318
|
+
position : str
|
|
1319
|
+
The position or directory of the tracking data.
|
|
1320
|
+
calibration : float, optional
|
|
1321
|
+
Calibration factor for converting pixel coordinates to physical units. Default is 1.
|
|
1322
|
+
prefix : str, optional
|
|
1323
|
+
Prefix used for the tracking data file. Default is "Aligned".
|
|
1324
|
+
population : str, optional
|
|
1325
|
+
Population type, either "target" or "effector". Default is "target".
|
|
1326
|
+
column_labels : dict, optional
|
|
1327
|
+
Dictionary containing the column labels for the tracking table. Default is
|
|
1328
|
+
{'track': "TRACK_ID", 'frame': 'FRAME', 'y': 'POSITION_Y', 'x': 'POSITION_X', 'label': 'class_id'}.
|
|
1329
|
+
|
|
1330
|
+
Returns
|
|
1331
|
+
-------
|
|
1332
|
+
None
|
|
1333
|
+
|
|
1334
|
+
Notes
|
|
1335
|
+
-----
|
|
1336
|
+
This function loads the tracking data, applies calibration to the spatial coordinates, and visualizes the tracks
|
|
1337
|
+
using Napari. The tracking data is loaded from the specified `position` directory with the given `prefix` and
|
|
1338
|
+
`population`. The spatial coordinates (x, y) in the tracking table are divided by the `calibration` factor to
|
|
1339
|
+
convert them from pixel units to the specified physical units. The tracks are then visualized using Napari.
|
|
1340
|
+
|
|
1341
|
+
Examples
|
|
1342
|
+
--------
|
|
1343
|
+
>>> control_tracking_table('path/to/tracking_data', calibration=0.1, prefix='Aligned', population='target')
|
|
1344
|
+
# Control the tracking table and visualize tracks using Napari.
|
|
1345
|
+
|
|
1346
|
+
"""
|
|
1347
|
+
|
|
1348
|
+
position = position.replace('\\','/')
|
|
1349
|
+
tracks,labels,stack = load_tracking_data(position, prefix=prefix, population=population)
|
|
1350
|
+
tracks = tracks.loc[:, [column_labels['track'], column_labels['frame'], column_labels['y'], column_labels['x']]].to_numpy()
|
|
1351
|
+
tracks[:,-2:] /= calibration
|
|
1352
|
+
_view_on_napari(tracks,labels=labels, stack=stack)
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def get_segmentation_models_list(mode='targets', return_path=False):
|
|
1356
|
+
|
|
1357
|
+
if mode=='targets':
|
|
1358
|
+
modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "segmentation_targets", os.sep])
|
|
1359
|
+
repository_models = get_zenodo_files(cat=os.sep.join(["models","segmentation_targets"]))
|
|
1360
|
+
elif mode=='effectors':
|
|
1361
|
+
modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "segmentation_effectors", os.sep])
|
|
1362
|
+
repository_models = get_zenodo_files(cat=os.sep.join(["models","segmentation_effectors"]))
|
|
1363
|
+
elif mode=='generic':
|
|
1364
|
+
modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "segmentation_generic", os.sep])
|
|
1365
|
+
repository_models = get_zenodo_files(cat=os.sep.join(["models","segmentation_generic"]))
|
|
1366
|
+
|
|
1367
|
+
available_models = natsorted(glob(modelpath+'*/'))
|
|
1368
|
+
available_models = [m.replace('\\','/').split('/')[-2] for m in available_models]
|
|
1369
|
+
for rm in repository_models:
|
|
1370
|
+
if rm not in available_models:
|
|
1371
|
+
available_models.append(rm)
|
|
1372
|
+
|
|
1373
|
+
if not return_path:
|
|
1374
|
+
return available_models
|
|
1375
|
+
else:
|
|
1376
|
+
return available_models, modelpath
|
|
1377
|
+
|
|
1378
|
+
def locate_segmentation_model(name):
|
|
1379
|
+
|
|
1380
|
+
"""
|
|
1381
|
+
Locates a specified segmentation model within the local 'celldetective' directory or
|
|
1382
|
+
downloads it from Zenodo if not found locally.
|
|
1383
|
+
|
|
1384
|
+
This function attempts to find a segmentation model by name within a predefined directory
|
|
1385
|
+
structure starting from the 'celldetective/models/segmentation*' path. If the model is not
|
|
1386
|
+
found locally, it then tries to locate and download the model from Zenodo, placing it into
|
|
1387
|
+
the appropriate category directory within 'celldetective'. The function prints the search
|
|
1388
|
+
directory path and returns the path to the found or downloaded model.
|
|
1389
|
+
|
|
1390
|
+
Parameters
|
|
1391
|
+
----------
|
|
1392
|
+
name : str
|
|
1393
|
+
The name of the segmentation model to locate.
|
|
1394
|
+
|
|
1395
|
+
Returns
|
|
1396
|
+
-------
|
|
1397
|
+
str or None
|
|
1398
|
+
The full path to the located or downloaded segmentation model directory, or None if the
|
|
1399
|
+
model could not be found or downloaded.
|
|
1400
|
+
|
|
1401
|
+
Raises
|
|
1402
|
+
------
|
|
1403
|
+
FileNotFoundError
|
|
1404
|
+
If the model cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
1405
|
+
|
|
1406
|
+
"""
|
|
1407
|
+
|
|
1408
|
+
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
|
|
1409
|
+
modelpath = os.sep.join([main_dir, "models", "segmentation*", os.sep])
|
|
1410
|
+
print(f'Looking for {name} in {modelpath}')
|
|
1411
|
+
models = glob(modelpath+f'*{os.sep}')
|
|
1412
|
+
|
|
1413
|
+
match=None
|
|
1414
|
+
for m in models:
|
|
1415
|
+
if name==m.replace('\\',os.sep).split(os.sep)[-2]:
|
|
1416
|
+
match = m
|
|
1417
|
+
return match
|
|
1418
|
+
# else no match, try zenodo
|
|
1419
|
+
files, categories = get_zenodo_files()
|
|
1420
|
+
if name in files:
|
|
1421
|
+
index = files.index(name)
|
|
1422
|
+
cat = categories[index]
|
|
1423
|
+
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
1424
|
+
match = os.sep.join([main_dir, cat, name])+os.sep
|
|
1425
|
+
return match
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def get_segmentation_datasets_list(return_path=False):
|
|
1429
|
+
|
|
1430
|
+
"""
|
|
1431
|
+
Retrieves a list of available segmentation datasets from both the local 'celldetective/datasets/segmentation_annotations'
|
|
1432
|
+
directory and a Zenodo repository, optionally returning the path to the local datasets directory.
|
|
1433
|
+
|
|
1434
|
+
This function compiles a list of available segmentation datasets by first identifying datasets stored locally
|
|
1435
|
+
within a specified path related to the script's directory. It then extends this list with datasets available
|
|
1436
|
+
in a Zenodo repository, ensuring no duplicates are added. The function can return just the list of dataset
|
|
1437
|
+
names or, if specified, also return the path to the local datasets directory.
|
|
1438
|
+
|
|
1439
|
+
Parameters
|
|
1440
|
+
----------
|
|
1441
|
+
return_path : bool, optional
|
|
1442
|
+
If True, the function returns a tuple containing the list of available dataset names and the path to the
|
|
1443
|
+
local datasets directory. If False, only the list of dataset names is returned (default is False).
|
|
1444
|
+
|
|
1445
|
+
Returns
|
|
1446
|
+
-------
|
|
1447
|
+
list or (list, str)
|
|
1448
|
+
If return_path is False, returns a list of strings, each string being the name of an available dataset.
|
|
1449
|
+
If return_path is True, returns a tuple where the first element is this list and the second element is a
|
|
1450
|
+
string representing the path to the local datasets directory.
|
|
1451
|
+
|
|
1452
|
+
"""
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
datasets_path = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "datasets", "segmentation_annotations", os.sep])
|
|
1456
|
+
repository_datasets = get_zenodo_files(cat=os.sep.join(["datasets","segmentation_annotations"]))
|
|
1457
|
+
|
|
1458
|
+
available_datasets = natsorted(glob(datasets_path+'*/'))
|
|
1459
|
+
available_datasets = [m.replace('\\','/').split('/')[-2] for m in available_datasets]
|
|
1460
|
+
for rm in repository_datasets:
|
|
1461
|
+
if rm not in available_datasets:
|
|
1462
|
+
available_datasets.append(rm)
|
|
1463
|
+
|
|
1464
|
+
if not return_path:
|
|
1465
|
+
return available_datasets
|
|
1466
|
+
else:
|
|
1467
|
+
return available_datasets, datasets_path
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
def locate_segmentation_dataset(name):
|
|
1472
|
+
|
|
1473
|
+
"""
|
|
1474
|
+
Locates a specified segmentation dataset within the local 'celldetective/datasets/segmentation_annotations' directory
|
|
1475
|
+
or downloads it from Zenodo if not found locally.
|
|
1476
|
+
|
|
1477
|
+
This function attempts to find a segmentation dataset by name within a predefined directory structure. If the dataset
|
|
1478
|
+
is not found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate
|
|
1479
|
+
category directory within 'celldetective'. The function prints the search directory path and returns the path to the
|
|
1480
|
+
found or downloaded dataset.
|
|
1481
|
+
|
|
1482
|
+
Parameters
|
|
1483
|
+
----------
|
|
1484
|
+
name : str
|
|
1485
|
+
The name of the segmentation dataset to locate.
|
|
1486
|
+
|
|
1487
|
+
Returns
|
|
1488
|
+
-------
|
|
1489
|
+
str or None
|
|
1490
|
+
The full path to the located or downloaded segmentation dataset directory, or None if the dataset could not be
|
|
1491
|
+
found or downloaded.
|
|
1492
|
+
|
|
1493
|
+
Raises
|
|
1494
|
+
------
|
|
1495
|
+
FileNotFoundError
|
|
1496
|
+
If the dataset cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
1497
|
+
|
|
1498
|
+
"""
|
|
1499
|
+
|
|
1500
|
+
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
|
|
1501
|
+
modelpath = os.sep.join([main_dir, "datasets", "segmentation_annotations", os.sep])
|
|
1502
|
+
print(f'Looking for {name} in {modelpath}')
|
|
1503
|
+
models = glob(modelpath+f'*{os.sep}')
|
|
1504
|
+
|
|
1505
|
+
match=None
|
|
1506
|
+
for m in models:
|
|
1507
|
+
if name==m.replace('\\',os.sep).split(os.sep)[-2]:
|
|
1508
|
+
match = m
|
|
1509
|
+
return match
|
|
1510
|
+
# else no match, try zenodo
|
|
1511
|
+
files, categories = get_zenodo_files()
|
|
1512
|
+
if name in files:
|
|
1513
|
+
index = files.index(name)
|
|
1514
|
+
cat = categories[index]
|
|
1515
|
+
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
1516
|
+
match = os.sep.join([main_dir, cat, name])+os.sep
|
|
1517
|
+
return match
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def get_signal_datasets_list(return_path=False):
|
|
1521
|
+
|
|
1522
|
+
"""
|
|
1523
|
+
Retrieves a list of available signal datasets from both the local 'celldetective/datasets/signal_annotations' directory
|
|
1524
|
+
and a Zenodo repository, optionally returning the path to the local datasets directory.
|
|
1525
|
+
|
|
1526
|
+
This function compiles a list of available signal datasets by first identifying datasets stored locally within a specified
|
|
1527
|
+
path related to the script's directory. It then extends this list with datasets available in a Zenodo repository, ensuring
|
|
1528
|
+
no duplicates are added. The function can return just the list of dataset names or, if specified, also return the path to
|
|
1529
|
+
the local datasets directory.
|
|
1530
|
+
|
|
1531
|
+
Parameters
|
|
1532
|
+
----------
|
|
1533
|
+
return_path : bool, optional
|
|
1534
|
+
If True, the function returns a tuple containing the list of available dataset names and the path to the local datasets
|
|
1535
|
+
directory. If False, only the list of dataset names is returned (default is False).
|
|
1536
|
+
|
|
1537
|
+
Returns
|
|
1538
|
+
-------
|
|
1539
|
+
list or (list, str)
|
|
1540
|
+
If return_path is False, returns a list of strings, each string being the name of an available dataset. If return_path
|
|
1541
|
+
is True, returns a tuple where the first element is this list and the second element is a string representing the path
|
|
1542
|
+
to the local datasets directory.
|
|
1543
|
+
|
|
1544
|
+
"""
|
|
1545
|
+
|
|
1546
|
+
datasets_path = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "datasets", "signal_annotations", os.sep])
|
|
1547
|
+
repository_datasets = get_zenodo_files(cat=os.sep.join(["datasets","signal_annotations"]))
|
|
1548
|
+
|
|
1549
|
+
available_datasets = natsorted(glob(datasets_path+'*/'))
|
|
1550
|
+
available_datasets = [m.replace('\\','/').split('/')[-2] for m in available_datasets]
|
|
1551
|
+
for rm in repository_datasets:
|
|
1552
|
+
if rm not in available_datasets:
|
|
1553
|
+
available_datasets.append(rm)
|
|
1554
|
+
|
|
1555
|
+
if not return_path:
|
|
1556
|
+
return available_datasets
|
|
1557
|
+
else:
|
|
1558
|
+
return available_datasets, datasets_path
|
|
1559
|
+
|
|
1560
|
+
def locate_signal_dataset(name):
|
|
1561
|
+
|
|
1562
|
+
"""
|
|
1563
|
+
Locates a specified signal dataset within the local 'celldetective/datasets/signal_annotations' directory or downloads
|
|
1564
|
+
it from Zenodo if not found locally.
|
|
1565
|
+
|
|
1566
|
+
This function attempts to find a signal dataset by name within a predefined directory structure. If the dataset is not
|
|
1567
|
+
found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate category
|
|
1568
|
+
directory within 'celldetective'. The function prints the search directory path and returns the path to the found or
|
|
1569
|
+
downloaded dataset.
|
|
1570
|
+
|
|
1571
|
+
Parameters
|
|
1572
|
+
----------
|
|
1573
|
+
name : str
|
|
1574
|
+
The name of the signal dataset to locate.
|
|
1575
|
+
|
|
1576
|
+
Returns
|
|
1577
|
+
-------
|
|
1578
|
+
str or None
|
|
1579
|
+
The full path to the located or downloaded signal dataset directory, or None if the dataset could not be found or
|
|
1580
|
+
downloaded.
|
|
1581
|
+
|
|
1582
|
+
Raises
|
|
1583
|
+
------
|
|
1584
|
+
FileNotFoundError
|
|
1585
|
+
If the dataset cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
1586
|
+
|
|
1587
|
+
"""
|
|
1588
|
+
|
|
1589
|
+
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
|
|
1590
|
+
modelpath = os.sep.join([main_dir, "datasets", "signal_annotations", os.sep])
|
|
1591
|
+
print(f'Looking for {name} in {modelpath}')
|
|
1592
|
+
models = glob(modelpath+f'*{os.sep}')
|
|
1593
|
+
|
|
1594
|
+
match=None
|
|
1595
|
+
for m in models:
|
|
1596
|
+
if name==m.replace('\\',os.sep).split(os.sep)[-2]:
|
|
1597
|
+
match = m
|
|
1598
|
+
return match
|
|
1599
|
+
# else no match, try zenodo
|
|
1600
|
+
files, categories = get_zenodo_files()
|
|
1601
|
+
if name in files:
|
|
1602
|
+
index = files.index(name)
|
|
1603
|
+
cat = categories[index]
|
|
1604
|
+
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
1605
|
+
match = os.sep.join([main_dir, cat, name])+os.sep
|
|
1606
|
+
return match
|
|
1607
|
+
|
|
1608
|
+
def normalize(frame, percentiles=(0.0,99.99), values=None, ignore_gray_value=0., clip=False, amplification=None, dtype=float):
|
|
1609
|
+
|
|
1610
|
+
"""
|
|
1611
|
+
|
|
1612
|
+
Normalize the intensity values of a frame.
|
|
1613
|
+
|
|
1614
|
+
Parameters
|
|
1615
|
+
----------
|
|
1616
|
+
frame : ndarray
|
|
1617
|
+
The input frame to be normalized.
|
|
1618
|
+
percentiles : tuple, optional
|
|
1619
|
+
The percentiles used to determine the minimum and maximum values for normalization. Default is (0.0, 99.99).
|
|
1620
|
+
values : tuple or None, optional
|
|
1621
|
+
The specific minimum and maximum values to use for normalization. If None, percentiles are used. Default is None.
|
|
1622
|
+
ignore_gray_value : float or None, optional
|
|
1623
|
+
The gray value to ignore during normalization. If specified, the pixels with this value will not be normalized. Default is 0.0.
|
|
1624
|
+
|
|
1625
|
+
Returns
|
|
1626
|
+
-------
|
|
1627
|
+
ndarray
|
|
1628
|
+
The normalized frame.
|
|
1629
|
+
|
|
1630
|
+
Notes
|
|
1631
|
+
-----
|
|
1632
|
+
This function performs intensity normalization on a frame. It computes the minimum and maximum values for normalization either
|
|
1633
|
+
using the specified values or by calculating percentiles from the frame. The frame is then normalized between the minimum and
|
|
1634
|
+
maximum values using the `normalize_mi_ma` function. If `ignore_gray_value` is specified, the pixels with this value will be
|
|
1635
|
+
left unmodified during normalization.
|
|
1636
|
+
|
|
1637
|
+
Examples
|
|
1638
|
+
--------
|
|
1639
|
+
>>> frame = np.array([[10, 20, 30],
|
|
1640
|
+
[40, 50, 60],
|
|
1641
|
+
[70, 80, 90]])
|
|
1642
|
+
>>> normalized = normalize(frame)
|
|
1643
|
+
>>> normalized
|
|
1644
|
+
|
|
1645
|
+
array([[0. , 0.2, 0.4],
|
|
1646
|
+
[0.6, 0.8, 1. ],
|
|
1647
|
+
[1.2, 1.4, 1.6]], dtype=float32)
|
|
1648
|
+
|
|
1649
|
+
>>> normalized = normalize(frame, percentiles=(10.0, 90.0))
|
|
1650
|
+
>>> normalized
|
|
1651
|
+
|
|
1652
|
+
array([[0.33333334, 0.44444445, 0.5555556 ],
|
|
1653
|
+
[0.6666667 , 0.7777778 , 0.8888889 ],
|
|
1654
|
+
[1. , 1.1111112 , 1.2222222 ]], dtype=float32)
|
|
1655
|
+
|
|
1656
|
+
"""
|
|
1657
|
+
|
|
1658
|
+
frame = frame.astype(float)
|
|
1659
|
+
|
|
1660
|
+
if ignore_gray_value is not None:
|
|
1661
|
+
subframe = frame[frame!=ignore_gray_value]
|
|
1662
|
+
else:
|
|
1663
|
+
subframe = frame.copy()
|
|
1664
|
+
|
|
1665
|
+
if values is not None:
|
|
1666
|
+
mi = values[0]; ma = values[1]
|
|
1667
|
+
else:
|
|
1668
|
+
mi = np.nanpercentile(subframe.flatten(),percentiles[0],keepdims=True)
|
|
1669
|
+
ma = np.nanpercentile(subframe.flatten(),percentiles[1],keepdims=True)
|
|
1670
|
+
|
|
1671
|
+
frame0 = frame.copy()
|
|
1672
|
+
frame = normalize_mi_ma(frame0, mi, ma, clip=False, eps=1e-20, dtype=np.float32)
|
|
1673
|
+
if amplification is not None:
|
|
1674
|
+
frame *= amplification
|
|
1675
|
+
if clip:
|
|
1676
|
+
if amplification is None:
|
|
1677
|
+
amplification = 1.
|
|
1678
|
+
frame[frame>=amplification] = amplification
|
|
1679
|
+
frame[frame<=0.] = 0.
|
|
1680
|
+
if ignore_gray_value is not None:
|
|
1681
|
+
frame[np.where(frame0)==ignore_gray_value] = ignore_gray_value
|
|
1682
|
+
|
|
1683
|
+
return frame.copy().astype(dtype)
|
|
1684
|
+
|
|
1685
|
+
def normalize_multichannel(multichannel_frame, percentiles=None,
|
|
1686
|
+
values=None, ignore_gray_value=0., clip=False,
|
|
1687
|
+
amplification=None, dtype=float):
|
|
1688
|
+
|
|
1689
|
+
"""
|
|
1690
|
+
Normalizes a multichannel frame by adjusting the intensity values of each channel based on specified percentiles,
|
|
1691
|
+
direct value ranges, or amplification factors, with options to ignore a specific gray value and to clip the output.
|
|
1692
|
+
|
|
1693
|
+
Parameters
|
|
1694
|
+
----------
|
|
1695
|
+
multichannel_frame : ndarray
|
|
1696
|
+
The input multichannel image frame to be normalized, expected to be a 3-dimensional array where the last dimension
|
|
1697
|
+
represents the channels.
|
|
1698
|
+
percentiles : list of tuples or tuple, optional
|
|
1699
|
+
Percentile ranges (low, high) for each channel used to scale the intensity values. If a single tuple is provided,
|
|
1700
|
+
it is applied to all channels. If None, the default percentile range of (0., 99.99) is used for each channel.
|
|
1701
|
+
values : list of tuples or tuple, optional
|
|
1702
|
+
Direct value ranges (min, max) for each channel to scale the intensity values. If a single tuple is provided, it
|
|
1703
|
+
is applied to all channels. This parameter overrides `percentiles` if provided.
|
|
1704
|
+
ignore_gray_value : float, optional
|
|
1705
|
+
A specific gray value to ignore during normalization (default is 0.).
|
|
1706
|
+
clip : bool, optional
|
|
1707
|
+
If True, clips the output values to the range [0, 1] or the specified `dtype` range if `dtype` is not float
|
|
1708
|
+
(default is False).
|
|
1709
|
+
amplification : float, optional
|
|
1710
|
+
A factor by which to amplify the intensity values after normalization. If None, no amplification is applied.
|
|
1711
|
+
dtype : data-type, optional
|
|
1712
|
+
The desired data-type for the output normalized frame. The default is float, but other types can be specified
|
|
1713
|
+
to change the range of the output values.
|
|
1714
|
+
|
|
1715
|
+
Returns
|
|
1716
|
+
-------
|
|
1717
|
+
ndarray
|
|
1718
|
+
The normalized multichannel frame as a 3-dimensional array of the same shape as `multichannel_frame`.
|
|
1719
|
+
|
|
1720
|
+
Raises
|
|
1721
|
+
------
|
|
1722
|
+
AssertionError
|
|
1723
|
+
If the input `multichannel_frame` does not have 3 dimensions, or if the length of `values` does not match the
|
|
1724
|
+
number of channels in `multichannel_frame`.
|
|
1725
|
+
|
|
1726
|
+
Notes
|
|
1727
|
+
-----
|
|
1728
|
+
- This function provides flexibility in normalization by allowing the use of percentile ranges, direct value ranges,
|
|
1729
|
+
or amplification factors.
|
|
1730
|
+
- The function makes a copy of the input frame to avoid altering the original data.
|
|
1731
|
+
- When both `percentiles` and `values` are provided, `values` takes precedence for normalization.
|
|
1732
|
+
|
|
1733
|
+
Examples
|
|
1734
|
+
--------
|
|
1735
|
+
>>> multichannel_frame = np.random.rand(100, 100, 3) # Example multichannel frame
|
|
1736
|
+
>>> normalized_frame = normalize_multichannel(multichannel_frame, percentiles=((1, 99), (2, 98), (0, 100)))
|
|
1737
|
+
# Normalizes each channel of the frame using specified percentile ranges.
|
|
1738
|
+
|
|
1739
|
+
"""
|
|
1740
|
+
|
|
1741
|
+
mf = multichannel_frame.copy().astype(float)
|
|
1742
|
+
assert mf.ndim==3,f'Wrong shape for the multichannel frame: {mf.shape}.'
|
|
1743
|
+
if percentiles is None:
|
|
1744
|
+
percentiles = [(0.,99.99)]*mf.shape[-1]
|
|
1745
|
+
elif isinstance(percentiles,tuple):
|
|
1746
|
+
percentiles = [percentiles]*mf.shape[-1]
|
|
1747
|
+
if values is not None:
|
|
1748
|
+
if isinstance(values, tuple):
|
|
1749
|
+
values = [values]*mf.shape[-1]
|
|
1750
|
+
assert len(values)==mf.shape[-1],'Mismatch between the normalization values provided and the number of channels.'
|
|
1751
|
+
|
|
1752
|
+
for c in range(mf.shape[-1]):
|
|
1753
|
+
if values is not None:
|
|
1754
|
+
v = values[c]
|
|
1755
|
+
else:
|
|
1756
|
+
v = None
|
|
1757
|
+
mf[:,:,c] = normalize(mf[:,:,c].copy(),
|
|
1758
|
+
percentiles=percentiles[c],
|
|
1759
|
+
values=v,
|
|
1760
|
+
ignore_gray_value=ignore_gray_value,
|
|
1761
|
+
clip=clip,
|
|
1762
|
+
amplification=amplification,
|
|
1763
|
+
dtype=dtype,
|
|
1764
|
+
)
|
|
1765
|
+
return mf
|
|
1766
|
+
|
|
1767
|
+
def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=float, normalize_kwargs={"percentiles": (0.,99.99)}):
|
|
1768
|
+
|
|
1769
|
+
"""
|
|
1770
|
+
Loads and optionally normalizes and rescales specified frames from a stack located at a given path.
|
|
1771
|
+
|
|
1772
|
+
This function reads specified frames from a stack file, applying systematic adjustments to ensure
|
|
1773
|
+
the channel axis is last. It supports optional normalization of the input frames and rescaling. An
|
|
1774
|
+
artificial pixel modification is applied to frames with uniform values to prevent errors during
|
|
1775
|
+
normalization.
|
|
1776
|
+
|
|
1777
|
+
Parameters
|
|
1778
|
+
----------
|
|
1779
|
+
img_nums : int or list of int
|
|
1780
|
+
The index (or indices) of the image frame(s) to load from the stack.
|
|
1781
|
+
stack_path : str
|
|
1782
|
+
The file path to the stack from which frames are to be loaded.
|
|
1783
|
+
scale : float, optional
|
|
1784
|
+
The scaling factor to apply to the frames. If None, no scaling is applied (default is None).
|
|
1785
|
+
normalize_input : bool, optional
|
|
1786
|
+
Whether to normalize the loaded frames. If True, normalization is applied according to
|
|
1787
|
+
`normalize_kwargs` (default is True).
|
|
1788
|
+
dtype : data-type, optional
|
|
1789
|
+
The desired data-type for the output frames (default is float).
|
|
1790
|
+
normalize_kwargs : dict, optional
|
|
1791
|
+
Keyword arguments to pass to the normalization function (default is {"percentiles": (0., 99.99)}).
|
|
1792
|
+
|
|
1793
|
+
Returns
|
|
1794
|
+
-------
|
|
1795
|
+
ndarray or None
|
|
1796
|
+
The loaded, and possibly normalized and rescaled, frames as a NumPy array. Returns None if there
|
|
1797
|
+
is an error in loading the frames.
|
|
1798
|
+
|
|
1799
|
+
Raises
|
|
1800
|
+
------
|
|
1801
|
+
Exception
|
|
1802
|
+
Prints an error message if the specified frames cannot be loaded or if there is a mismatch between
|
|
1803
|
+
the provided experiment channel information and the stack format.
|
|
1804
|
+
|
|
1805
|
+
Notes
|
|
1806
|
+
-----
|
|
1807
|
+
- The function uses scikit-image for reading frames and supports multi-frame TIFF stacks.
|
|
1808
|
+
- Normalization and scaling are optional and can be customized through function parameters.
|
|
1809
|
+
- A workaround is implemented for frames with uniform pixel values to prevent normalization errors by
|
|
1810
|
+
adding a 'fake' pixel.
|
|
1811
|
+
|
|
1812
|
+
Examples
|
|
1813
|
+
--------
|
|
1814
|
+
>>> frames = load_frames([0, 1, 2], '/path/to/stack.tif', scale=0.5, normalize_input=True, dtype=np.uint8)
|
|
1815
|
+
# Loads the first three frames from '/path/to/stack.tif', normalizes them, rescales by a factor of 0.5,
|
|
1816
|
+
# and converts them to uint8 data type.
|
|
1817
|
+
|
|
1818
|
+
"""
|
|
1819
|
+
|
|
1820
|
+
try:
|
|
1821
|
+
frames = skio.imread(stack_path, img_num=img_nums, plugin="tifffile")
|
|
1822
|
+
except Exception as e:
|
|
1823
|
+
print(f'Error in loading the frame {img_nums} {e}. Please check that the experiment channel information is consistent with the movie being read.')
|
|
1824
|
+
return None
|
|
1825
|
+
|
|
1826
|
+
if frames.ndim==3:
|
|
1827
|
+
# Systematically move channel axis to the end
|
|
1828
|
+
channel_axis = np.argmin(frames.shape)
|
|
1829
|
+
frames = np.moveaxis(frames, channel_axis, -1)
|
|
1830
|
+
if frames.ndim==2:
|
|
1831
|
+
frames = frames[:,:,np.newaxis]
|
|
1832
|
+
if normalize_input:
|
|
1833
|
+
frames = normalize_multichannel(frames, **normalize_kwargs)
|
|
1834
|
+
if scale is not None:
|
|
1835
|
+
frames = zoom(frames, [scale,scale,1], order=3)
|
|
1836
|
+
|
|
1837
|
+
# add a fake pixel to prevent auto normalization errors on images that are uniform
|
|
1838
|
+
# to revisit
|
|
1839
|
+
for k in range(frames.shape[2]):
|
|
1840
|
+
unique_values = np.unique(frames[:,:,k])
|
|
1841
|
+
if len(unique_values)==1:
|
|
1842
|
+
frames[0,0,k] += 1
|
|
1843
|
+
|
|
1844
|
+
return frames.astype(dtype)
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
def get_stack_normalization_values(stack, percentiles=None, ignore_gray_value=0.):
|
|
1848
|
+
|
|
1849
|
+
"""
|
|
1850
|
+
Computes the normalization value ranges (minimum and maximum) for each channel in a 4D stack based on specified percentiles.
|
|
1851
|
+
|
|
1852
|
+
This function calculates the value ranges for normalizing each channel within a 4-dimensional stack, with dimensions
|
|
1853
|
+
expected to be in the order of Time (T), Y (height), X (width), and Channels (C). The normalization values are determined
|
|
1854
|
+
by the specified percentiles for each channel. An option to ignore a specific gray value during computation is provided,
|
|
1855
|
+
though its effect is not implemented in this snippet.
|
|
1856
|
+
|
|
1857
|
+
Parameters
|
|
1858
|
+
----------
|
|
1859
|
+
stack : ndarray
|
|
1860
|
+
The input 4D stack with dimensions TYXC from which to calculate normalization values.
|
|
1861
|
+
percentiles : tuple, list of tuples, optional
|
|
1862
|
+
The percentile values (low, high) used to calculate the normalization ranges for each channel. If a single tuple
|
|
1863
|
+
is provided, it is applied to all channels. If a list of tuples is provided, each tuple is applied to the
|
|
1864
|
+
corresponding channel. If None, defaults to (0., 99.99) for each channel.
|
|
1865
|
+
ignore_gray_value : float, optional
|
|
1866
|
+
A gray value to potentially ignore during the calculation. This parameter is provided for interface consistency
|
|
1867
|
+
but is not utilized in the current implementation (default is 0.).
|
|
1868
|
+
|
|
1869
|
+
Returns
|
|
1870
|
+
-------
|
|
1871
|
+
list of tuples
|
|
1872
|
+
A list where each tuple contains the (minimum, maximum) values for normalizing each channel based on the specified
|
|
1873
|
+
percentiles.
|
|
1874
|
+
|
|
1875
|
+
Raises
|
|
1876
|
+
------
|
|
1877
|
+
AssertionError
|
|
1878
|
+
If the input stack does not have 4 dimensions, or if the length of the `percentiles` list does not match the number
|
|
1879
|
+
of channels in the stack.
|
|
1880
|
+
|
|
1881
|
+
Notes
|
|
1882
|
+
-----
|
|
1883
|
+
- The function assumes the input stack is in TYXC format, where T is the time dimension, Y and X are spatial dimensions,
|
|
1884
|
+
and C is the channel dimension.
|
|
1885
|
+
- Memory management via `gc.collect()` is employed after calculating normalization values for each channel to mitigate
|
|
1886
|
+
potential memory issues with large datasets.
|
|
1887
|
+
|
|
1888
|
+
Examples
|
|
1889
|
+
--------
|
|
1890
|
+
>>> stack = np.random.rand(5, 100, 100, 3) # Example 4D stack with 3 channels
|
|
1891
|
+
>>> normalization_values = get_stack_normalization_values(stack, percentiles=((1, 99), (2, 98), (0, 100)))
|
|
1892
|
+
# Calculates normalization ranges for each channel using the specified percentiles.
|
|
1893
|
+
|
|
1894
|
+
"""
|
|
1895
|
+
|
|
1896
|
+
assert stack.ndim==4,f'Wrong number of dimensions for the stack, expect TYXC (4) got {stack.ndim}.'
|
|
1897
|
+
if percentiles is None:
|
|
1898
|
+
percentiles = [(0.,99.99)]*stack.shape[-1]
|
|
1899
|
+
elif isinstance(percentiles,tuple):
|
|
1900
|
+
percentiles = [percentiles]*stack.shape[-1]
|
|
1901
|
+
elif isinstance(percentiles,list):
|
|
1902
|
+
assert len(percentiles)==stack.shape[-1],f'Mismatch between the provided percentiles and the number of channels {stack.shape[-1]}. If you meant to apply the same percentiles to all channels, please provide a single tuple.'
|
|
1903
|
+
|
|
1904
|
+
values = []
|
|
1905
|
+
for c in range(stack.shape[-1]):
|
|
1906
|
+
perc = percentiles[c]
|
|
1907
|
+
mi = np.nanpercentile(stack[:,:,:,c].flatten(),perc[0],keepdims=True)[0]
|
|
1908
|
+
ma = np.nanpercentile(stack[:,:,:,c].flatten(),perc[1],keepdims=True)[0]
|
|
1909
|
+
values.append(tuple((mi,ma)))
|
|
1910
|
+
gc.collect()
|
|
1911
|
+
|
|
1912
|
+
return values
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
def get_positions_in_well(well):
|
|
1916
|
+
|
|
1917
|
+
"""
|
|
1918
|
+
Retrieves the list of position directories within a specified well directory,
|
|
1919
|
+
formatted as a NumPy array of strings.
|
|
1920
|
+
|
|
1921
|
+
This function identifies position directories based on their naming convention,
|
|
1922
|
+
which must include a numeric identifier following the well's name. The well's name
|
|
1923
|
+
is expected to start with 'W' (e.g., 'W1'), followed by a numeric identifier. Position
|
|
1924
|
+
directories are assumed to be named with this numeric identifier directly after the well
|
|
1925
|
+
identifier, without the 'W'. For example, positions within well 'W1' might be named
|
|
1926
|
+
'101', '102', etc. This function will glob these directories and return their full
|
|
1927
|
+
paths as a NumPy array.
|
|
1928
|
+
|
|
1929
|
+
Parameters
|
|
1930
|
+
----------
|
|
1931
|
+
well : str
|
|
1932
|
+
The path to the well directory from which to retrieve position directories.
|
|
1933
|
+
|
|
1934
|
+
Returns
|
|
1935
|
+
-------
|
|
1936
|
+
np.ndarray
|
|
1937
|
+
An array of strings, each representing the full path to a position directory within
|
|
1938
|
+
the specified well. The array is empty if no position directories are found.
|
|
1939
|
+
|
|
1940
|
+
Notes
|
|
1941
|
+
-----
|
|
1942
|
+
- This function relies on a specific naming convention for wells and positions. It assumes
|
|
1943
|
+
that each well directory is prefixed with 'W' followed by a numeric identifier, and
|
|
1944
|
+
position directories are named starting with this numeric identifier directly.
|
|
1945
|
+
|
|
1946
|
+
Examples
|
|
1947
|
+
--------
|
|
1948
|
+
>>> get_positions_in_well('/path/to/experiment/W1')
|
|
1949
|
+
# This might return an array like array(['/path/to/experiment/W1/101', '/path/to/experiment/W1/102'])
|
|
1950
|
+
if position directories '101' and '102' exist within the well 'W1' directory.
|
|
1951
|
+
|
|
1952
|
+
"""
|
|
1953
|
+
|
|
1954
|
+
if well.endswith(os.sep):
|
|
1955
|
+
well = well[:-1]
|
|
1956
|
+
|
|
1957
|
+
w_numeric = os.path.split(well)[-1].replace('W','')
|
|
1958
|
+
positions = glob(os.sep.join([well,f'{w_numeric}*{os.sep}']))
|
|
1959
|
+
|
|
1960
|
+
return np.array(positions,dtype=str)
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
def extract_experiment_folder_output(experiment_folder, destination_folder):
|
|
1964
|
+
|
|
1965
|
+
"""
|
|
1966
|
+
Copies the output subfolder and associated tables from an experiment folder to a new location,
|
|
1967
|
+
making the experiment folder much lighter by only keeping essential data.
|
|
1968
|
+
|
|
1969
|
+
This function takes the path to an experiment folder and a destination folder as input.
|
|
1970
|
+
It creates a copy of the experiment folder at the destination, but only includes the output subfolders
|
|
1971
|
+
and their associated tables for each well and position within the experiment.
|
|
1972
|
+
This operation significantly reduces the size of the experiment data by excluding non-essential files.
|
|
1973
|
+
|
|
1974
|
+
The structure of the copied experiment folder is preserved, including the configuration file,
|
|
1975
|
+
well directories, and position directories within each well.
|
|
1976
|
+
Only the 'output' subfolder and its 'tables' subdirectory are copied for each position.
|
|
1977
|
+
|
|
1978
|
+
Parameters
|
|
1979
|
+
----------
|
|
1980
|
+
experiment_folder : str
|
|
1981
|
+
The path to the source experiment folder from which to extract data.
|
|
1982
|
+
destination_folder : str
|
|
1983
|
+
The path to the destination folder where the reduced copy of the experiment
|
|
1984
|
+
will be created.
|
|
1985
|
+
|
|
1986
|
+
Notes
|
|
1987
|
+
-----
|
|
1988
|
+
- This function assumes that the structure of the experiment folder is consistent,
|
|
1989
|
+
with wells organized in subdirectories and each containing a position subdirectory.
|
|
1990
|
+
Each position subdirectory should have an 'output' folder and a 'tables' subfolder within it.
|
|
1991
|
+
|
|
1992
|
+
- The function also assumes the existence of a configuration file in the root of the
|
|
1993
|
+
experiment folder, which is copied to the root of the destination experiment folder.
|
|
1994
|
+
|
|
1995
|
+
Examples
|
|
1996
|
+
--------
|
|
1997
|
+
>>> extract_experiment_folder_output('/path/to/experiment_folder', '/path/to/destination_folder')
|
|
1998
|
+
# This will copy the 'experiment_folder' to 'destination_folder', including only
|
|
1999
|
+
# the output subfolders and their tables for each well and position.
|
|
2000
|
+
|
|
2001
|
+
"""
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
if experiment_folder.endswith(os.sep):
|
|
2005
|
+
experiment_folder = experiment_folder[:-1]
|
|
2006
|
+
if destination_folder.endswith(os.sep):
|
|
2007
|
+
destination_folder = destination_folder[:-1]
|
|
2008
|
+
|
|
2009
|
+
exp_name = experiment_folder.split(os.sep)[-1]
|
|
2010
|
+
output_path = os.sep.join([destination_folder, exp_name])
|
|
2011
|
+
if not os.path.exists(output_path):
|
|
2012
|
+
os.mkdir(output_path)
|
|
2013
|
+
|
|
2014
|
+
config = get_config(experiment_folder)
|
|
2015
|
+
copyfile(config,os.sep.join([output_path,os.path.split(config)[-1]]))
|
|
2016
|
+
|
|
2017
|
+
wells_src = get_experiment_wells(experiment_folder)
|
|
2018
|
+
wells = [w.split(os.sep)[-2] for w in wells_src]
|
|
2019
|
+
|
|
2020
|
+
for k,w in enumerate(wells):
|
|
2021
|
+
|
|
2022
|
+
well_output_path = os.sep.join([output_path,w])
|
|
2023
|
+
if not os.path.exists(well_output_path):
|
|
2024
|
+
os.mkdir(well_output_path)
|
|
2025
|
+
|
|
2026
|
+
positions = get_positions_in_well(wells_src[k])
|
|
2027
|
+
|
|
2028
|
+
for pos in positions:
|
|
2029
|
+
pos_name = extract_position_name(pos)
|
|
2030
|
+
output_pos = os.sep.join([well_output_path, pos_name])
|
|
2031
|
+
if not os.path.exists(output_pos):
|
|
2032
|
+
os.mkdir(output_pos)
|
|
2033
|
+
output_folder = os.sep.join([output_pos, 'output'])
|
|
2034
|
+
output_tables_folder = os.sep.join([output_folder, 'tables'])
|
|
2035
|
+
|
|
2036
|
+
if not os.path.exists(output_folder):
|
|
2037
|
+
os.mkdir(output_folder)
|
|
2038
|
+
|
|
2039
|
+
if not os.path.exists(output_tables_folder):
|
|
2040
|
+
os.mkdir(output_tables_folder)
|
|
2041
|
+
|
|
2042
|
+
tab_path = glob(pos+os.sep.join(['output','tables',f'*']))
|
|
2043
|
+
|
|
2044
|
+
for t in tab_path:
|
|
2045
|
+
copyfile(t,os.sep.join([output_tables_folder,os.path.split(t)[-1]]))
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
if __name__ == '__main__':
|
|
2050
|
+
control_segmentation_napari("/home/limozin/Documents/Experiments/MinimumJan/W4/401/", prefix='Aligned', population="target", flush_memory=False)
|