celldetective 1.4.2__py3-none-any.whl → 1.5.0b0__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 +25 -0
- celldetective/__main__.py +62 -43
- celldetective/_version.py +1 -1
- celldetective/extra_properties.py +477 -399
- celldetective/filters.py +192 -97
- celldetective/gui/InitWindow.py +541 -411
- celldetective/gui/__init__.py +0 -15
- celldetective/gui/about.py +44 -39
- celldetective/gui/analyze_block.py +120 -84
- celldetective/gui/base/__init__.py +0 -0
- celldetective/gui/base/channel_norm_generator.py +335 -0
- celldetective/gui/base/components.py +249 -0
- celldetective/gui/base/feature_choice.py +92 -0
- celldetective/gui/base/figure_canvas.py +52 -0
- celldetective/gui/base/list_widget.py +133 -0
- celldetective/gui/{styles.py → base/styles.py} +92 -36
- celldetective/gui/base/utils.py +33 -0
- celldetective/gui/base_annotator.py +900 -767
- celldetective/gui/classifier_widget.py +6 -22
- celldetective/gui/configure_new_exp.py +777 -671
- celldetective/gui/control_panel.py +635 -524
- celldetective/gui/dynamic_progress.py +449 -0
- celldetective/gui/event_annotator.py +2023 -1662
- celldetective/gui/generic_signal_plot.py +1292 -944
- celldetective/gui/gui_utils.py +899 -1289
- celldetective/gui/interactions_block.py +658 -0
- celldetective/gui/interactive_timeseries_viewer.py +447 -0
- celldetective/gui/json_readers.py +48 -15
- celldetective/gui/layouts/__init__.py +5 -0
- celldetective/gui/layouts/background_model_free_layout.py +537 -0
- celldetective/gui/layouts/channel_offset_layout.py +134 -0
- celldetective/gui/layouts/local_correction_layout.py +91 -0
- celldetective/gui/layouts/model_fit_layout.py +372 -0
- celldetective/gui/layouts/operation_layout.py +68 -0
- celldetective/gui/layouts/protocol_designer_layout.py +96 -0
- celldetective/gui/pair_event_annotator.py +3130 -2435
- celldetective/gui/plot_measurements.py +586 -267
- celldetective/gui/plot_signals_ui.py +724 -506
- celldetective/gui/preprocessing_block.py +395 -0
- celldetective/gui/process_block.py +1678 -1831
- celldetective/gui/seg_model_loader.py +580 -473
- celldetective/gui/settings/__init__.py +0 -7
- celldetective/gui/settings/_cellpose_model_params.py +181 -0
- celldetective/gui/settings/_event_detection_model_params.py +95 -0
- celldetective/gui/settings/_segmentation_model_params.py +159 -0
- celldetective/gui/settings/_settings_base.py +77 -65
- celldetective/gui/settings/_settings_event_model_training.py +752 -526
- celldetective/gui/settings/_settings_measurements.py +1133 -964
- celldetective/gui/settings/_settings_neighborhood.py +574 -488
- celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
- celldetective/gui/settings/_settings_signal_annotator.py +329 -305
- celldetective/gui/settings/_settings_tracking.py +1304 -1094
- celldetective/gui/settings/_stardist_model_params.py +98 -0
- celldetective/gui/survival_ui.py +422 -312
- celldetective/gui/tableUI.py +1665 -1701
- celldetective/gui/table_ops/_maths.py +295 -0
- celldetective/gui/table_ops/_merge_groups.py +140 -0
- celldetective/gui/table_ops/_merge_one_hot.py +95 -0
- celldetective/gui/table_ops/_query_table.py +43 -0
- celldetective/gui/table_ops/_rename_col.py +44 -0
- celldetective/gui/thresholds_gui.py +382 -179
- celldetective/gui/viewers/__init__.py +0 -0
- celldetective/gui/viewers/base_viewer.py +700 -0
- celldetective/gui/viewers/channel_offset_viewer.py +331 -0
- celldetective/gui/viewers/contour_viewer.py +394 -0
- celldetective/gui/viewers/size_viewer.py +153 -0
- celldetective/gui/viewers/spot_detection_viewer.py +341 -0
- celldetective/gui/viewers/threshold_viewer.py +309 -0
- celldetective/gui/workers.py +304 -126
- celldetective/log_manager.py +92 -0
- celldetective/measure.py +1895 -1478
- celldetective/napari/__init__.py +0 -0
- celldetective/napari/utils.py +1025 -0
- celldetective/neighborhood.py +1914 -1448
- celldetective/preprocessing.py +1620 -1220
- celldetective/processes/__init__.py +0 -0
- celldetective/processes/background_correction.py +271 -0
- celldetective/processes/compute_neighborhood.py +894 -0
- celldetective/processes/detect_events.py +246 -0
- celldetective/processes/measure_cells.py +565 -0
- celldetective/processes/segment_cells.py +760 -0
- celldetective/processes/track_cells.py +435 -0
- celldetective/processes/train_segmentation_model.py +694 -0
- celldetective/processes/train_signal_model.py +265 -0
- celldetective/processes/unified_process.py +292 -0
- celldetective/regionprops/_regionprops.py +358 -317
- celldetective/relative_measurements.py +987 -710
- celldetective/scripts/measure_cells.py +313 -212
- celldetective/scripts/measure_relative.py +90 -46
- celldetective/scripts/segment_cells.py +165 -104
- celldetective/scripts/segment_cells_thresholds.py +96 -68
- celldetective/scripts/track_cells.py +198 -149
- celldetective/scripts/train_segmentation_model.py +324 -201
- celldetective/scripts/train_signal_model.py +87 -45
- celldetective/segmentation.py +844 -749
- celldetective/signals.py +3514 -2861
- celldetective/tracking.py +30 -15
- celldetective/utils/__init__.py +0 -0
- celldetective/utils/cellpose_utils/__init__.py +133 -0
- celldetective/utils/color_mappings.py +42 -0
- celldetective/utils/data_cleaning.py +630 -0
- celldetective/utils/data_loaders.py +450 -0
- celldetective/utils/dataset_helpers.py +207 -0
- celldetective/utils/downloaders.py +197 -0
- celldetective/utils/event_detection/__init__.py +8 -0
- celldetective/utils/experiment.py +1782 -0
- celldetective/utils/image_augmenters.py +308 -0
- celldetective/utils/image_cleaning.py +74 -0
- celldetective/utils/image_loaders.py +926 -0
- celldetective/utils/image_transforms.py +335 -0
- celldetective/utils/io.py +62 -0
- celldetective/utils/mask_cleaning.py +348 -0
- celldetective/utils/mask_transforms.py +5 -0
- celldetective/utils/masks.py +184 -0
- celldetective/utils/maths.py +351 -0
- celldetective/utils/model_getters.py +325 -0
- celldetective/utils/model_loaders.py +296 -0
- celldetective/utils/normalization.py +380 -0
- celldetective/utils/parsing.py +465 -0
- celldetective/utils/plots/__init__.py +0 -0
- celldetective/utils/plots/regression.py +53 -0
- celldetective/utils/resources.py +34 -0
- celldetective/utils/stardist_utils/__init__.py +104 -0
- celldetective/utils/stats.py +90 -0
- celldetective/utils/types.py +21 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
- celldetective-1.5.0b0.dist-info/RECORD +187 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
- tests/gui/test_new_project.py +129 -117
- tests/gui/test_project.py +127 -79
- tests/test_filters.py +39 -15
- tests/test_notebooks.py +8 -0
- tests/test_tracking.py +232 -13
- tests/test_utils.py +123 -77
- celldetective/gui/base_components.py +0 -23
- celldetective/gui/layouts.py +0 -1602
- celldetective/gui/processes/compute_neighborhood.py +0 -594
- celldetective/gui/processes/measure_cells.py +0 -360
- celldetective/gui/processes/segment_cells.py +0 -499
- celldetective/gui/processes/track_cells.py +0 -303
- celldetective/gui/processes/train_segmentation_model.py +0 -270
- celldetective/gui/processes/train_signal_model.py +0 -108
- celldetective/gui/table_ops/merge_groups.py +0 -118
- celldetective/gui/viewers.py +0 -1354
- celldetective/io.py +0 -3663
- celldetective/utils.py +0 -3108
- celldetective-1.4.2.dist-info/RECORD +0 -123
- /celldetective/{gui/processes → processes}/downloader.py +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
celldetective/io.py
DELETED
|
@@ -1,3663 +0,0 @@
|
|
|
1
|
-
from natsort import natsorted
|
|
2
|
-
from PyQt5.QtWidgets import QMessageBox
|
|
3
|
-
from glob import glob
|
|
4
|
-
from tifffile import imread, TiffFile
|
|
5
|
-
import numpy as np
|
|
6
|
-
import os
|
|
7
|
-
import pandas as pd
|
|
8
|
-
import napari
|
|
9
|
-
import json
|
|
10
|
-
|
|
11
|
-
import gc
|
|
12
|
-
from tqdm import tqdm
|
|
13
|
-
import threading
|
|
14
|
-
import concurrent.futures
|
|
15
|
-
|
|
16
|
-
from csbdeep.utils import normalize_mi_ma
|
|
17
|
-
from csbdeep.io import save_tiff_imagej_compatible
|
|
18
|
-
|
|
19
|
-
import imageio.v2 as imageio
|
|
20
|
-
from skimage.measure import regionprops_table, label
|
|
21
|
-
|
|
22
|
-
from btrack.datasets import cell_config
|
|
23
|
-
from magicgui import magicgui
|
|
24
|
-
from pathlib import Path, PurePath
|
|
25
|
-
from shutil import copyfile, rmtree
|
|
26
|
-
|
|
27
|
-
from celldetective.utils import _rearrange_multichannel_frame, _fix_no_contrast, zoom_multiframes, \
|
|
28
|
-
config_section_to_dict, extract_experiment_channels, _extract_labels_from_config, get_zenodo_files, \
|
|
29
|
-
download_zenodo_file
|
|
30
|
-
from celldetective.utils import interpolate_nan_multichannel, get_config
|
|
31
|
-
|
|
32
|
-
from stardist import fill_label_holes
|
|
33
|
-
from skimage.transform import resize
|
|
34
|
-
import re
|
|
35
|
-
|
|
36
|
-
from typing import List, Tuple, Union
|
|
37
|
-
import numbers
|
|
38
|
-
|
|
39
|
-
def extract_experiment_from_well(well_path):
|
|
40
|
-
|
|
41
|
-
"""
|
|
42
|
-
Extracts the experiment directory path from a given well directory path.
|
|
43
|
-
|
|
44
|
-
Parameters
|
|
45
|
-
----------
|
|
46
|
-
well_path : str
|
|
47
|
-
The file system path to a well directory. The path should end with the well folder,
|
|
48
|
-
but it does not need to include a trailing separator.
|
|
49
|
-
|
|
50
|
-
Returns
|
|
51
|
-
-------
|
|
52
|
-
str
|
|
53
|
-
The path to the experiment directory, which is assumed to be two levels above the well directory.
|
|
54
|
-
|
|
55
|
-
Notes
|
|
56
|
-
-----
|
|
57
|
-
- This function expects the well directory to be organized such that the experiment directory is
|
|
58
|
-
two levels above it in the file system hierarchy.
|
|
59
|
-
- If the input path does not end with a file separator (`os.sep`), one is appended before processing.
|
|
60
|
-
|
|
61
|
-
Example
|
|
62
|
-
-------
|
|
63
|
-
>>> well_path = "/path/to/experiment/plate/well"
|
|
64
|
-
>>> extract_experiment_from_well(well_path)
|
|
65
|
-
'/path/to/experiment'
|
|
66
|
-
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
if not well_path.endswith(os.sep):
|
|
70
|
-
well_path += os.sep
|
|
71
|
-
exp_path_blocks = well_path.split(os.sep)[:-2]
|
|
72
|
-
experiment = os.sep.join(exp_path_blocks)
|
|
73
|
-
return experiment
|
|
74
|
-
|
|
75
|
-
def extract_well_from_position(pos_path):
|
|
76
|
-
|
|
77
|
-
"""
|
|
78
|
-
Extracts the well directory path from a given position directory path.
|
|
79
|
-
|
|
80
|
-
Parameters
|
|
81
|
-
----------
|
|
82
|
-
pos_path : str
|
|
83
|
-
The file system path to a position directory. The path should end with the position folder,
|
|
84
|
-
but it does not need to include a trailing separator.
|
|
85
|
-
|
|
86
|
-
Returns
|
|
87
|
-
-------
|
|
88
|
-
str
|
|
89
|
-
The path to the well directory, which is assumed to be two levels above the position directory,
|
|
90
|
-
with a trailing separator appended.
|
|
91
|
-
|
|
92
|
-
Notes
|
|
93
|
-
-----
|
|
94
|
-
- This function expects the position directory to be organized such that the well directory is
|
|
95
|
-
two levels above it in the file system hierarchy.
|
|
96
|
-
- If the input path does not end with a file separator (`os.sep`), one is appended before processing.
|
|
97
|
-
|
|
98
|
-
Example
|
|
99
|
-
-------
|
|
100
|
-
>>> pos_path = "/path/to/experiment/plate/well/position"
|
|
101
|
-
>>> extract_well_from_position(pos_path)
|
|
102
|
-
'/path/to/experiment/plate/well/'
|
|
103
|
-
|
|
104
|
-
"""
|
|
105
|
-
|
|
106
|
-
if not pos_path.endswith(os.sep):
|
|
107
|
-
pos_path += os.sep
|
|
108
|
-
well_path_blocks = pos_path.split(os.sep)[:-2]
|
|
109
|
-
well_path = os.sep.join(well_path_blocks)+os.sep
|
|
110
|
-
return well_path
|
|
111
|
-
|
|
112
|
-
def extract_experiment_from_position(pos_path):
|
|
113
|
-
|
|
114
|
-
"""
|
|
115
|
-
Extracts the experiment directory path from a given position directory path.
|
|
116
|
-
|
|
117
|
-
Parameters
|
|
118
|
-
----------
|
|
119
|
-
pos_path : str
|
|
120
|
-
The file system path to a position directory. The path should end with the position folder,
|
|
121
|
-
but it does not need to include a trailing separator.
|
|
122
|
-
|
|
123
|
-
Returns
|
|
124
|
-
-------
|
|
125
|
-
str
|
|
126
|
-
The path to the experiment directory, which is assumed to be three levels above the position directory.
|
|
127
|
-
|
|
128
|
-
Notes
|
|
129
|
-
-----
|
|
130
|
-
- This function expects the position directory to be organized hierarchically such that the experiment directory
|
|
131
|
-
is three levels above it in the file system hierarchy.
|
|
132
|
-
- If the input path does not end with a file separator (`os.sep`), one is appended before processing.
|
|
133
|
-
|
|
134
|
-
Example
|
|
135
|
-
-------
|
|
136
|
-
>>> pos_path = "/path/to/experiment/plate/well/position"
|
|
137
|
-
>>> extract_experiment_from_position(pos_path)
|
|
138
|
-
'/path/to/experiment'
|
|
139
|
-
|
|
140
|
-
"""
|
|
141
|
-
|
|
142
|
-
pos_path = pos_path.replace(os.sep, '/')
|
|
143
|
-
if not pos_path.endswith('/'):
|
|
144
|
-
pos_path += '/'
|
|
145
|
-
exp_path_blocks = pos_path.split('/')[:-3]
|
|
146
|
-
experiment = os.sep.join(exp_path_blocks)
|
|
147
|
-
|
|
148
|
-
return experiment
|
|
149
|
-
|
|
150
|
-
def collect_experiment_metadata(pos_path=None, well_path=None):
|
|
151
|
-
|
|
152
|
-
"""
|
|
153
|
-
Collects and organizes metadata for an experiment based on a given position or well directory path.
|
|
154
|
-
|
|
155
|
-
Parameters
|
|
156
|
-
----------
|
|
157
|
-
pos_path : str, optional
|
|
158
|
-
The file system path to a position directory. If provided, it will be used to extract metadata.
|
|
159
|
-
This parameter takes precedence over `well_path`.
|
|
160
|
-
well_path : str, optional
|
|
161
|
-
The file system path to a well directory. If `pos_path` is not provided, this path will be used to extract metadata.
|
|
162
|
-
|
|
163
|
-
Returns
|
|
164
|
-
-------
|
|
165
|
-
dict
|
|
166
|
-
A dictionary containing the following metadata:
|
|
167
|
-
- `"pos_path"`: The path to the position directory (or `None` if not provided).
|
|
168
|
-
- `"position"`: The same as `pos_path`.
|
|
169
|
-
- `"pos_name"`: The name of the position (or `0` if `pos_path` is not provided).
|
|
170
|
-
- `"well_path"`: The path to the well directory.
|
|
171
|
-
- `"well_name"`: The name of the well.
|
|
172
|
-
- `"well_nbr"`: The numerical identifier of the well.
|
|
173
|
-
- `"experiment"`: The path to the experiment directory.
|
|
174
|
-
- `"antibody"`: The antibody associated with the well.
|
|
175
|
-
- `"concentration"`: The concentration associated with the well.
|
|
176
|
-
- `"cell_type"`: The cell type associated with the well.
|
|
177
|
-
- `"pharmaceutical_agent"`: The pharmaceutical agent associated with the well.
|
|
178
|
-
|
|
179
|
-
Notes
|
|
180
|
-
-----
|
|
181
|
-
- At least one of `pos_path` or `well_path` must be provided.
|
|
182
|
-
- The function determines the experiment path by navigating the directory structure and extracts metadata for the
|
|
183
|
-
corresponding well and position.
|
|
184
|
-
- The metadata is derived using helper functions like `extract_experiment_from_position`, `extract_well_from_position`,
|
|
185
|
-
and `get_experiment_*` family of functions.
|
|
186
|
-
|
|
187
|
-
Example
|
|
188
|
-
-------
|
|
189
|
-
>>> pos_path = "/path/to/experiment/plate/well/position"
|
|
190
|
-
>>> metadata = collect_experiment_metadata(pos_path=pos_path)
|
|
191
|
-
>>> metadata["well_name"]
|
|
192
|
-
'W1'
|
|
193
|
-
|
|
194
|
-
>>> well_path = "/path/to/experiment/plate/well"
|
|
195
|
-
>>> metadata = collect_experiment_metadata(well_path=well_path)
|
|
196
|
-
>>> metadata["concentration"]
|
|
197
|
-
10.0
|
|
198
|
-
|
|
199
|
-
"""
|
|
200
|
-
|
|
201
|
-
if pos_path is not None:
|
|
202
|
-
if not pos_path.endswith(os.sep):
|
|
203
|
-
pos_path += os.sep
|
|
204
|
-
experiment = extract_experiment_from_position(pos_path)
|
|
205
|
-
well_path = extract_well_from_position(pos_path)
|
|
206
|
-
elif well_path is not None:
|
|
207
|
-
if not well_path.endswith(os.sep):
|
|
208
|
-
well_path += os.sep
|
|
209
|
-
experiment = extract_experiment_from_well(well_path)
|
|
210
|
-
else:
|
|
211
|
-
print("Please provide a position or well path...")
|
|
212
|
-
return None
|
|
213
|
-
|
|
214
|
-
wells = list(get_experiment_wells(experiment))
|
|
215
|
-
idx = wells.index(well_path)
|
|
216
|
-
well_name, well_nbr = extract_well_name_and_number(well_path)
|
|
217
|
-
if pos_path is not None:
|
|
218
|
-
pos_name = extract_position_name(pos_path)
|
|
219
|
-
else:
|
|
220
|
-
pos_name = 0
|
|
221
|
-
|
|
222
|
-
dico = {"pos_path": pos_path,
|
|
223
|
-
"position": pos_path,
|
|
224
|
-
"pos_name": pos_name,
|
|
225
|
-
"well_path": well_path,
|
|
226
|
-
"well_name": well_name,
|
|
227
|
-
"well_nbr": well_nbr,
|
|
228
|
-
"experiment": experiment,
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
meta = get_experiment_metadata(experiment) # None or dict of metadata
|
|
232
|
-
if meta is not None:
|
|
233
|
-
keys = list(meta.keys())
|
|
234
|
-
for k in keys:
|
|
235
|
-
dico.update({k: meta[k]})
|
|
236
|
-
|
|
237
|
-
labels = get_experiment_labels(experiment)
|
|
238
|
-
for k in list(labels.keys()):
|
|
239
|
-
values = labels[k]
|
|
240
|
-
try:
|
|
241
|
-
dico.update({k: values[idx]})
|
|
242
|
-
except Exception as e:
|
|
243
|
-
print(f"{e=}")
|
|
244
|
-
|
|
245
|
-
return dico
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def get_experiment_wells(experiment):
|
|
249
|
-
|
|
250
|
-
"""
|
|
251
|
-
Retrieves the list of well directories from a given experiment directory, sorted
|
|
252
|
-
naturally and returned as a NumPy array of strings.
|
|
253
|
-
|
|
254
|
-
Parameters
|
|
255
|
-
----------
|
|
256
|
-
experiment : str
|
|
257
|
-
The path to the experiment directory from which to retrieve well directories.
|
|
258
|
-
|
|
259
|
-
Returns
|
|
260
|
-
-------
|
|
261
|
-
np.ndarray
|
|
262
|
-
An array of strings, each representing the full path to a well directory within the specified
|
|
263
|
-
experiment. The array is empty if no well directories are found.
|
|
264
|
-
|
|
265
|
-
Notes
|
|
266
|
-
-----
|
|
267
|
-
- The function assumes well directories are prefixed with 'W' and uses this to filter directories
|
|
268
|
-
within the experiment folder.
|
|
269
|
-
|
|
270
|
-
- Natural sorting is applied to the list of wells to ensure that the order is intuitive (e.g., 'W2'
|
|
271
|
-
comes before 'W10'). This sorting method is especially useful when dealing with numerical sequences
|
|
272
|
-
that are part of the directory names.
|
|
273
|
-
|
|
274
|
-
"""
|
|
275
|
-
|
|
276
|
-
if not experiment.endswith(os.sep):
|
|
277
|
-
experiment += os.sep
|
|
278
|
-
|
|
279
|
-
wells = natsorted(glob(experiment + "W*" + os.sep))
|
|
280
|
-
return np.array(wells, dtype=str)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def get_spatial_calibration(experiment):
|
|
285
|
-
|
|
286
|
-
"""
|
|
287
|
-
Retrieves the spatial calibration factor for an experiment.
|
|
288
|
-
|
|
289
|
-
Parameters
|
|
290
|
-
----------
|
|
291
|
-
experiment : str
|
|
292
|
-
The file system path to the experiment directory.
|
|
293
|
-
|
|
294
|
-
Returns
|
|
295
|
-
-------
|
|
296
|
-
float
|
|
297
|
-
The spatial calibration factor (pixels to micrometers conversion), extracted from the experiment's configuration file.
|
|
298
|
-
|
|
299
|
-
Raises
|
|
300
|
-
------
|
|
301
|
-
AssertionError
|
|
302
|
-
If the configuration file (`config.ini`) does not exist in the specified experiment directory.
|
|
303
|
-
KeyError
|
|
304
|
-
If the "pxtoum" key is not found under the "MovieSettings" section in the configuration file.
|
|
305
|
-
ValueError
|
|
306
|
-
If the retrieved "pxtoum" value cannot be converted to a float.
|
|
307
|
-
|
|
308
|
-
Notes
|
|
309
|
-
-----
|
|
310
|
-
- The function retrieves the calibration factor by first locating the configuration file for the experiment using `get_config()`.
|
|
311
|
-
- It expects the configuration file to have a section named `MovieSettings` containing the key `pxtoum`.
|
|
312
|
-
- This factor defines the conversion from pixels to micrometers for spatial measurements.
|
|
313
|
-
|
|
314
|
-
Example
|
|
315
|
-
-------
|
|
316
|
-
>>> experiment = "/path/to/experiment"
|
|
317
|
-
>>> calibration = get_spatial_calibration(experiment)
|
|
318
|
-
>>> print(calibration)
|
|
319
|
-
0.325 # pixels-to-micrometers conversion factor
|
|
320
|
-
|
|
321
|
-
"""
|
|
322
|
-
|
|
323
|
-
config = get_config(experiment)
|
|
324
|
-
px_to_um = float(config_section_to_dict(config, "MovieSettings")["pxtoum"])
|
|
325
|
-
|
|
326
|
-
return px_to_um
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def get_temporal_calibration(experiment):
|
|
330
|
-
|
|
331
|
-
"""
|
|
332
|
-
Retrieves the temporal calibration factor for an experiment.
|
|
333
|
-
|
|
334
|
-
Parameters
|
|
335
|
-
----------
|
|
336
|
-
experiment : str
|
|
337
|
-
The file system path to the experiment directory.
|
|
338
|
-
|
|
339
|
-
Returns
|
|
340
|
-
-------
|
|
341
|
-
float
|
|
342
|
-
The temporal calibration factor (frames to minutes conversion), extracted from the experiment's configuration file.
|
|
343
|
-
|
|
344
|
-
Raises
|
|
345
|
-
------
|
|
346
|
-
AssertionError
|
|
347
|
-
If the configuration file (`config.ini`) does not exist in the specified experiment directory.
|
|
348
|
-
KeyError
|
|
349
|
-
If the "frametomin" key is not found under the "MovieSettings" section in the configuration file.
|
|
350
|
-
ValueError
|
|
351
|
-
If the retrieved "frametomin" value cannot be converted to a float.
|
|
352
|
-
|
|
353
|
-
Notes
|
|
354
|
-
-----
|
|
355
|
-
- The function retrieves the calibration factor by locating the configuration file for the experiment using `get_config()`.
|
|
356
|
-
- It expects the configuration file to have a section named `MovieSettings` containing the key `frametomin`.
|
|
357
|
-
- This factor defines the conversion from frames to minutes for temporal measurements.
|
|
358
|
-
|
|
359
|
-
Example
|
|
360
|
-
-------
|
|
361
|
-
>>> experiment = "/path/to/experiment"
|
|
362
|
-
>>> calibration = get_temporal_calibration(experiment)
|
|
363
|
-
>>> print(calibration)
|
|
364
|
-
0.5 # frames-to-minutes conversion factor
|
|
365
|
-
|
|
366
|
-
"""
|
|
367
|
-
|
|
368
|
-
config = get_config(experiment)
|
|
369
|
-
frame_to_min = float(config_section_to_dict(config, "MovieSettings")["frametomin"])
|
|
370
|
-
|
|
371
|
-
return frame_to_min
|
|
372
|
-
|
|
373
|
-
def get_experiment_metadata(experiment):
|
|
374
|
-
|
|
375
|
-
config = get_config(experiment)
|
|
376
|
-
metadata = config_section_to_dict(config, "Metadata")
|
|
377
|
-
return metadata
|
|
378
|
-
|
|
379
|
-
def get_experiment_labels(experiment):
|
|
380
|
-
|
|
381
|
-
config = get_config(experiment)
|
|
382
|
-
wells = get_experiment_wells(experiment)
|
|
383
|
-
nbr_of_wells = len(wells)
|
|
384
|
-
|
|
385
|
-
labels = config_section_to_dict(config, "Labels")
|
|
386
|
-
for k in list(labels.keys()):
|
|
387
|
-
values = labels[k].split(',')
|
|
388
|
-
if nbr_of_wells != len(values):
|
|
389
|
-
values = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
|
|
390
|
-
if np.all(np.array([s.isnumeric() for s in values])):
|
|
391
|
-
values = [float(s) for s in values]
|
|
392
|
-
labels.update({k: values})
|
|
393
|
-
|
|
394
|
-
return labels
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def get_experiment_concentrations(experiment, dtype=str):
|
|
398
|
-
|
|
399
|
-
"""
|
|
400
|
-
Retrieves the concentrations associated with each well in an experiment.
|
|
401
|
-
|
|
402
|
-
Parameters
|
|
403
|
-
----------
|
|
404
|
-
experiment : str
|
|
405
|
-
The file system path to the experiment directory.
|
|
406
|
-
dtype : type, optional
|
|
407
|
-
The data type to which the concentrations should be converted (default is `str`).
|
|
408
|
-
|
|
409
|
-
Returns
|
|
410
|
-
-------
|
|
411
|
-
numpy.ndarray
|
|
412
|
-
An array of concentrations for each well, converted to the specified data type.
|
|
413
|
-
|
|
414
|
-
Raises
|
|
415
|
-
------
|
|
416
|
-
AssertionError
|
|
417
|
-
If the configuration file (`config.ini`) does not exist in the specified experiment directory.
|
|
418
|
-
KeyError
|
|
419
|
-
If the "concentrations" key is not found under the "Labels" section in the configuration file.
|
|
420
|
-
ValueError
|
|
421
|
-
If the retrieved concentrations cannot be converted to the specified data type.
|
|
422
|
-
|
|
423
|
-
Notes
|
|
424
|
-
-----
|
|
425
|
-
- The function retrieves the configuration file using `get_config()` and expects a section `Labels` containing
|
|
426
|
-
a key `concentrations`.
|
|
427
|
-
- The concentrations are assumed to be comma-separated values.
|
|
428
|
-
- If the number of wells does not match the number of concentrations, the function generates a default set
|
|
429
|
-
of values ranging from 0 to the number of wells minus 1.
|
|
430
|
-
- The resulting concentrations are converted to the specified `dtype` before being returned.
|
|
431
|
-
|
|
432
|
-
Example
|
|
433
|
-
-------
|
|
434
|
-
>>> experiment = "/path/to/experiment"
|
|
435
|
-
>>> concentrations = get_experiment_concentrations(experiment, dtype=float)
|
|
436
|
-
>>> print(concentrations)
|
|
437
|
-
[0.1, 0.2, 0.5, 1.0]
|
|
438
|
-
|
|
439
|
-
"""
|
|
440
|
-
|
|
441
|
-
config = get_config(experiment)
|
|
442
|
-
wells = get_experiment_wells(experiment)
|
|
443
|
-
nbr_of_wells = len(wells)
|
|
444
|
-
|
|
445
|
-
concentrations = config_section_to_dict(config, "Labels")["concentrations"].split(",")
|
|
446
|
-
if nbr_of_wells != len(concentrations):
|
|
447
|
-
concentrations = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
|
|
448
|
-
|
|
449
|
-
return np.array([dtype(c) for c in concentrations])
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
def get_experiment_cell_types(experiment, dtype=str):
|
|
453
|
-
|
|
454
|
-
"""
|
|
455
|
-
Retrieves the cell types associated with each well in an experiment.
|
|
456
|
-
|
|
457
|
-
Parameters
|
|
458
|
-
----------
|
|
459
|
-
experiment : str
|
|
460
|
-
The file system path to the experiment directory.
|
|
461
|
-
dtype : type, optional
|
|
462
|
-
The data type to which the cell types should be converted (default is `str`).
|
|
463
|
-
|
|
464
|
-
Returns
|
|
465
|
-
-------
|
|
466
|
-
numpy.ndarray
|
|
467
|
-
An array of cell types for each well, converted to the specified data type.
|
|
468
|
-
|
|
469
|
-
Raises
|
|
470
|
-
------
|
|
471
|
-
AssertionError
|
|
472
|
-
If the configuration file (`config.ini`) does not exist in the specified experiment directory.
|
|
473
|
-
KeyError
|
|
474
|
-
If the "cell_types" key is not found under the "Labels" section in the configuration file.
|
|
475
|
-
ValueError
|
|
476
|
-
If the retrieved cell types cannot be converted to the specified data type.
|
|
477
|
-
|
|
478
|
-
Notes
|
|
479
|
-
-----
|
|
480
|
-
- The function retrieves the configuration file using `get_config()` and expects a section `Labels` containing
|
|
481
|
-
a key `cell_types`.
|
|
482
|
-
- The cell types are assumed to be comma-separated values.
|
|
483
|
-
- If the number of wells does not match the number of cell types, the function generates a default set
|
|
484
|
-
of values ranging from 0 to the number of wells minus 1.
|
|
485
|
-
- The resulting cell types are converted to the specified `dtype` before being returned.
|
|
486
|
-
|
|
487
|
-
Example
|
|
488
|
-
-------
|
|
489
|
-
>>> experiment = "/path/to/experiment"
|
|
490
|
-
>>> cell_types = get_experiment_cell_types(experiment, dtype=str)
|
|
491
|
-
>>> print(cell_types)
|
|
492
|
-
['TypeA', 'TypeB', 'TypeC', 'TypeD']
|
|
493
|
-
|
|
494
|
-
"""
|
|
495
|
-
|
|
496
|
-
config = get_config(experiment)
|
|
497
|
-
wells = get_experiment_wells(experiment)
|
|
498
|
-
nbr_of_wells = len(wells)
|
|
499
|
-
|
|
500
|
-
cell_types = config_section_to_dict(config, "Labels")["cell_types"].split(",")
|
|
501
|
-
if nbr_of_wells != len(cell_types):
|
|
502
|
-
cell_types = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
|
|
503
|
-
|
|
504
|
-
return np.array([dtype(c) for c in cell_types])
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
def get_experiment_antibodies(experiment, dtype=str):
|
|
508
|
-
|
|
509
|
-
"""
|
|
510
|
-
Retrieve the list of antibodies used in an experiment.
|
|
511
|
-
|
|
512
|
-
This function extracts antibody labels for the wells in the given experiment
|
|
513
|
-
based on the configuration file. If the number of wells does not match the
|
|
514
|
-
number of antibody labels provided in the configuration, it generates a
|
|
515
|
-
sequence of default numeric labels.
|
|
516
|
-
|
|
517
|
-
Parameters
|
|
518
|
-
----------
|
|
519
|
-
experiment : str
|
|
520
|
-
The identifier or name of the experiment to retrieve antibodies for.
|
|
521
|
-
dtype : type, optional
|
|
522
|
-
The data type to which the antibody labels should be cast. Default is `str`.
|
|
523
|
-
|
|
524
|
-
Returns
|
|
525
|
-
-------
|
|
526
|
-
numpy.ndarray
|
|
527
|
-
An array of antibody labels with the specified data type. If no antibodies
|
|
528
|
-
are specified or there is a mismatch, numeric labels are generated instead.
|
|
529
|
-
|
|
530
|
-
Notes
|
|
531
|
-
-----
|
|
532
|
-
- The function assumes the experiment's configuration can be loaded using
|
|
533
|
-
`get_config` and that the antibodies are listed under the "Labels" section
|
|
534
|
-
with the key `"antibodies"`.
|
|
535
|
-
- A mismatch between the number of wells and antibody labels will result in
|
|
536
|
-
numeric labels generated using `numpy.linspace`.
|
|
537
|
-
|
|
538
|
-
Examples
|
|
539
|
-
--------
|
|
540
|
-
>>> get_experiment_antibodies("path/to/experiment1")
|
|
541
|
-
array(['A1', 'A2', 'A3'], dtype='<U2')
|
|
542
|
-
|
|
543
|
-
>>> get_experiment_antibodies("path/to/experiment2", dtype=int)
|
|
544
|
-
array([0, 1, 2])
|
|
545
|
-
|
|
546
|
-
"""
|
|
547
|
-
|
|
548
|
-
config = get_config(experiment)
|
|
549
|
-
wells = get_experiment_wells(experiment)
|
|
550
|
-
nbr_of_wells = len(wells)
|
|
551
|
-
|
|
552
|
-
antibodies = config_section_to_dict(config, "Labels")["antibodies"].split(",")
|
|
553
|
-
if nbr_of_wells != len(antibodies):
|
|
554
|
-
antibodies = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
|
|
555
|
-
|
|
556
|
-
return np.array([dtype(c) for c in antibodies])
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
def get_experiment_pharmaceutical_agents(experiment, dtype=str):
|
|
560
|
-
|
|
561
|
-
"""
|
|
562
|
-
Retrieves the antibodies associated with each well in an experiment.
|
|
563
|
-
|
|
564
|
-
Parameters
|
|
565
|
-
----------
|
|
566
|
-
experiment : str
|
|
567
|
-
The file system path to the experiment directory.
|
|
568
|
-
dtype : type, optional
|
|
569
|
-
The data type to which the antibodies should be converted (default is `str`).
|
|
570
|
-
|
|
571
|
-
Returns
|
|
572
|
-
-------
|
|
573
|
-
numpy.ndarray
|
|
574
|
-
An array of antibodies for each well, converted to the specified data type.
|
|
575
|
-
|
|
576
|
-
Raises
|
|
577
|
-
------
|
|
578
|
-
AssertionError
|
|
579
|
-
If the configuration file (`config.ini`) does not exist in the specified experiment directory.
|
|
580
|
-
KeyError
|
|
581
|
-
If the "antibodies" key is not found under the "Labels" section in the configuration file.
|
|
582
|
-
ValueError
|
|
583
|
-
If the retrieved antibody values cannot be converted to the specified data type.
|
|
584
|
-
|
|
585
|
-
Notes
|
|
586
|
-
-----
|
|
587
|
-
- The function retrieves the configuration file using `get_config()` and expects a section `Labels` containing
|
|
588
|
-
a key `antibodies`.
|
|
589
|
-
- The antibody names are assumed to be comma-separated values.
|
|
590
|
-
- If the number of wells does not match the number of antibodies, the function generates a default set
|
|
591
|
-
of values ranging from 0 to the number of wells minus 1.
|
|
592
|
-
- The resulting antibody names are converted to the specified `dtype` before being returned.
|
|
593
|
-
|
|
594
|
-
Example
|
|
595
|
-
-------
|
|
596
|
-
>>> experiment = "/path/to/experiment"
|
|
597
|
-
>>> antibodies = get_experiment_antibodies(experiment, dtype=str)
|
|
598
|
-
>>> print(antibodies)
|
|
599
|
-
['AntibodyA', 'AntibodyB', 'AntibodyC', 'AntibodyD']
|
|
600
|
-
|
|
601
|
-
"""
|
|
602
|
-
|
|
603
|
-
config = get_config(experiment)
|
|
604
|
-
wells = get_experiment_wells(experiment)
|
|
605
|
-
nbr_of_wells = len(wells)
|
|
606
|
-
|
|
607
|
-
pharmaceutical_agents = config_section_to_dict(config, "Labels")["pharmaceutical_agents"].split(",")
|
|
608
|
-
if nbr_of_wells != len(pharmaceutical_agents):
|
|
609
|
-
pharmaceutical_agents = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
|
|
610
|
-
|
|
611
|
-
return np.array([dtype(c) for c in pharmaceutical_agents])
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def get_experiment_populations(experiment, dtype=str):
|
|
615
|
-
|
|
616
|
-
config = get_config(experiment)
|
|
617
|
-
populations_str = config_section_to_dict(config, "Populations")
|
|
618
|
-
if populations_str is not None:
|
|
619
|
-
populations = populations_str['populations'].split(',')
|
|
620
|
-
else:
|
|
621
|
-
populations = ['effectors','targets']
|
|
622
|
-
return list([dtype(c) for c in populations])
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def interpret_wells_and_positions(experiment: str, well_option: Union[str,int,List[int]], position_option: Union[str,int,List[int]]) -> Union[Tuple[List[int], List[int]], None]:
|
|
626
|
-
"""
|
|
627
|
-
Interpret well and position options for a given experiment.
|
|
628
|
-
|
|
629
|
-
This function takes an experiment and well/position options to return the selected
|
|
630
|
-
wells and positions. It supports selection of all wells or specific wells/positions
|
|
631
|
-
as specified. The well numbering starts from 0 (i.e., Well 0 is W1 and so on).
|
|
632
|
-
|
|
633
|
-
Parameters
|
|
634
|
-
----------
|
|
635
|
-
experiment : str
|
|
636
|
-
The experiment path containing well information.
|
|
637
|
-
well_option : str, int, or list of int
|
|
638
|
-
The well selection option:
|
|
639
|
-
- '*' : Select all wells.
|
|
640
|
-
- int : Select a specific well by its index.
|
|
641
|
-
- list of int : Select multiple wells by their indices.
|
|
642
|
-
position_option : str, int, or list of int
|
|
643
|
-
The position selection option:
|
|
644
|
-
- '*' : Select all positions (returns None).
|
|
645
|
-
- int : Select a specific position by its index.
|
|
646
|
-
- list of int : Select multiple positions by their indices.
|
|
647
|
-
|
|
648
|
-
Returns
|
|
649
|
-
-------
|
|
650
|
-
well_indices : numpy.ndarray or list of int
|
|
651
|
-
The indices of the selected wells.
|
|
652
|
-
position_indices : numpy.ndarray or list of int or None
|
|
653
|
-
The indices of the selected positions. Returns None if all positions are selected.
|
|
654
|
-
|
|
655
|
-
Examples
|
|
656
|
-
--------
|
|
657
|
-
>>> experiment = ... # Some experiment object
|
|
658
|
-
>>> interpret_wells_and_positions(experiment, '*', '*')
|
|
659
|
-
(array([0, 1, 2, ..., n-1]), None)
|
|
660
|
-
|
|
661
|
-
>>> interpret_wells_and_positions(experiment, 2, '*')
|
|
662
|
-
([2], None)
|
|
663
|
-
|
|
664
|
-
>>> interpret_wells_and_positions(experiment, [1, 3, 5], 2)
|
|
665
|
-
([1, 3, 5], array([2]))
|
|
666
|
-
|
|
667
|
-
"""
|
|
668
|
-
|
|
669
|
-
wells = get_experiment_wells(experiment)
|
|
670
|
-
nbr_of_wells = len(wells)
|
|
671
|
-
|
|
672
|
-
if well_option == '*':
|
|
673
|
-
well_indices = np.arange(nbr_of_wells)
|
|
674
|
-
elif isinstance(well_option, int) or isinstance(well_option, np.int_):
|
|
675
|
-
well_indices = [int(well_option)]
|
|
676
|
-
elif isinstance(well_option, list):
|
|
677
|
-
well_indices = well_option
|
|
678
|
-
else:
|
|
679
|
-
print("Well indices could not be interpreted...")
|
|
680
|
-
return None
|
|
681
|
-
|
|
682
|
-
if position_option == '*':
|
|
683
|
-
position_indices = None
|
|
684
|
-
elif isinstance(position_option, int):
|
|
685
|
-
position_indices = np.array([position_option], dtype=int)
|
|
686
|
-
elif isinstance(position_option, list):
|
|
687
|
-
position_indices = position_option
|
|
688
|
-
else:
|
|
689
|
-
print("Position indices could not be interpreted...")
|
|
690
|
-
return None
|
|
691
|
-
|
|
692
|
-
return well_indices, position_indices
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
def extract_well_name_and_number(well):
|
|
696
|
-
"""
|
|
697
|
-
Extract the well name and number from a given well path.
|
|
698
|
-
|
|
699
|
-
This function takes a well path string, splits it by the OS-specific path separator,
|
|
700
|
-
and extracts the well name and number. The well name is the last component of the path,
|
|
701
|
-
and the well number is derived by removing the 'W' prefix and converting the remaining
|
|
702
|
-
part to an integer.
|
|
703
|
-
|
|
704
|
-
Parameters
|
|
705
|
-
----------
|
|
706
|
-
well : str
|
|
707
|
-
The well path string, where the well name is the last component.
|
|
708
|
-
|
|
709
|
-
Returns
|
|
710
|
-
-------
|
|
711
|
-
well_name : str
|
|
712
|
-
The name of the well, extracted from the last component of the path.
|
|
713
|
-
well_number : int
|
|
714
|
-
The well number, obtained by stripping the 'W' prefix from the well name
|
|
715
|
-
and converting the remainder to an integer.
|
|
716
|
-
|
|
717
|
-
Examples
|
|
718
|
-
--------
|
|
719
|
-
>>> well_path = "path/to/W23"
|
|
720
|
-
>>> extract_well_name_and_number(well_path)
|
|
721
|
-
('W23', 23)
|
|
722
|
-
|
|
723
|
-
>>> well_path = "another/path/W1"
|
|
724
|
-
>>> extract_well_name_and_number(well_path)
|
|
725
|
-
('W1', 1)
|
|
726
|
-
|
|
727
|
-
"""
|
|
728
|
-
|
|
729
|
-
split_well_path = well.split(os.sep)
|
|
730
|
-
split_well_path = list(filter(None, split_well_path))
|
|
731
|
-
well_name = split_well_path[-1]
|
|
732
|
-
well_number = int(split_well_path[-1].replace('W', ''))
|
|
733
|
-
|
|
734
|
-
return well_name, well_number
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
def extract_position_name(pos):
|
|
738
|
-
|
|
739
|
-
"""
|
|
740
|
-
Extract the position name from a given position path.
|
|
741
|
-
|
|
742
|
-
This function takes a position path string, splits it by the OS-specific path separator,
|
|
743
|
-
filters out any empty components, and extracts the position name, which is the last
|
|
744
|
-
component of the path.
|
|
745
|
-
|
|
746
|
-
Parameters
|
|
747
|
-
----------
|
|
748
|
-
pos : str
|
|
749
|
-
The position path string, where the position name is the last component.
|
|
750
|
-
|
|
751
|
-
Returns
|
|
752
|
-
-------
|
|
753
|
-
pos_name : str
|
|
754
|
-
The name of the position, extracted from the last component of the path.
|
|
755
|
-
|
|
756
|
-
Examples
|
|
757
|
-
--------
|
|
758
|
-
>>> pos_path = "path/to/position1"
|
|
759
|
-
>>> extract_position_name(pos_path)
|
|
760
|
-
'position1'
|
|
761
|
-
|
|
762
|
-
>>> pos_path = "another/path/positionA"
|
|
763
|
-
>>> extract_position_name(pos_path)
|
|
764
|
-
'positionA'
|
|
765
|
-
|
|
766
|
-
"""
|
|
767
|
-
|
|
768
|
-
split_pos_path = pos.split(os.sep)
|
|
769
|
-
split_pos_path = list(filter(None, split_pos_path))
|
|
770
|
-
pos_name = split_pos_path[-1]
|
|
771
|
-
|
|
772
|
-
return pos_name
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
def get_position_table(pos, population, return_path=False):
|
|
776
|
-
|
|
777
|
-
"""
|
|
778
|
-
Retrieves the data table for a specified population at a given position, optionally returning the table's file path.
|
|
779
|
-
|
|
780
|
-
This function locates and loads a CSV data table associated with a specific population (e.g., 'targets', 'cells')
|
|
781
|
-
from a specified position directory. The position directory should contain an 'output/tables' subdirectory where
|
|
782
|
-
the CSV file named 'trajectories_{population}.csv' is expected to be found. If the file exists, it is loaded into
|
|
783
|
-
a pandas DataFrame; otherwise, None is returned.
|
|
784
|
-
|
|
785
|
-
Parameters
|
|
786
|
-
----------
|
|
787
|
-
pos : str
|
|
788
|
-
The path to the position directory from which to load the data table.
|
|
789
|
-
population : str
|
|
790
|
-
The name of the population for which the data table is to be retrieved. This name is used to construct the
|
|
791
|
-
file name of the CSV file to be loaded.
|
|
792
|
-
return_path : bool, optional
|
|
793
|
-
If True, returns a tuple containing the loaded data table (or None) and the path to the CSV file. If False,
|
|
794
|
-
only the loaded data table (or None) is returned (default is False).
|
|
795
|
-
|
|
796
|
-
Returns
|
|
797
|
-
-------
|
|
798
|
-
pandas.DataFrame or None, or (pandas.DataFrame or None, str)
|
|
799
|
-
If return_path is False, returns the loaded data table as a pandas DataFrame, or None if the table file does
|
|
800
|
-
not exist. If return_path is True, returns a tuple where the first element is the data table (or None) and the
|
|
801
|
-
second element is the path to the CSV file.
|
|
802
|
-
|
|
803
|
-
Examples
|
|
804
|
-
--------
|
|
805
|
-
>>> df_pos = get_position_table('/path/to/position', 'targets')
|
|
806
|
-
# This will load the 'trajectories_targets.csv' table from the specified position directory into a pandas DataFrame.
|
|
807
|
-
|
|
808
|
-
>>> df_pos, table_path = get_position_table('/path/to/position', 'targets', return_path=True)
|
|
809
|
-
# This will load the 'trajectories_targets.csv' table and also return the path to the CSV file.
|
|
810
|
-
|
|
811
|
-
"""
|
|
812
|
-
|
|
813
|
-
if not pos.endswith(os.sep):
|
|
814
|
-
table = os.sep.join([pos, 'output', 'tables', f'trajectories_{population}.csv'])
|
|
815
|
-
else:
|
|
816
|
-
table = pos + os.sep.join(['output', 'tables', f'trajectories_{population}.csv'])
|
|
817
|
-
|
|
818
|
-
if os.path.exists(table):
|
|
819
|
-
try:
|
|
820
|
-
df_pos = pd.read_csv(table, low_memory=False)
|
|
821
|
-
except Exception as e:
|
|
822
|
-
print(e)
|
|
823
|
-
df_pos = None
|
|
824
|
-
else:
|
|
825
|
-
df_pos = None
|
|
826
|
-
|
|
827
|
-
if return_path:
|
|
828
|
-
return df_pos, table
|
|
829
|
-
else:
|
|
830
|
-
return df_pos
|
|
831
|
-
|
|
832
|
-
def get_position_pickle(pos, population, return_path=False):
|
|
833
|
-
|
|
834
|
-
"""
|
|
835
|
-
Retrieves the data table for a specified population at a given position, optionally returning the table's file path.
|
|
836
|
-
|
|
837
|
-
This function locates and loads a CSV data table associated with a specific population (e.g., 'targets', 'cells')
|
|
838
|
-
from a specified position directory. The position directory should contain an 'output/tables' subdirectory where
|
|
839
|
-
the CSV file named 'trajectories_{population}.csv' is expected to be found. If the file exists, it is loaded into
|
|
840
|
-
a pandas DataFrame; otherwise, None is returned.
|
|
841
|
-
|
|
842
|
-
Parameters
|
|
843
|
-
----------
|
|
844
|
-
pos : str
|
|
845
|
-
The path to the position directory from which to load the data table.
|
|
846
|
-
population : str
|
|
847
|
-
The name of the population for which the data table is to be retrieved. This name is used to construct the
|
|
848
|
-
file name of the CSV file to be loaded.
|
|
849
|
-
return_path : bool, optional
|
|
850
|
-
If True, returns a tuple containing the loaded data table (or None) and the path to the CSV file. If False,
|
|
851
|
-
only the loaded data table (or None) is returned (default is False).
|
|
852
|
-
|
|
853
|
-
Returns
|
|
854
|
-
-------
|
|
855
|
-
pandas.DataFrame or None, or (pandas.DataFrame or None, str)
|
|
856
|
-
If return_path is False, returns the loaded data table as a pandas DataFrame, or None if the table file does
|
|
857
|
-
not exist. If return_path is True, returns a tuple where the first element is the data table (or None) and the
|
|
858
|
-
second element is the path to the CSV file.
|
|
859
|
-
|
|
860
|
-
Examples
|
|
861
|
-
--------
|
|
862
|
-
>>> df_pos = get_position_table('/path/to/position', 'targets')
|
|
863
|
-
# This will load the 'trajectories_targets.csv' table from the specified position directory into a pandas DataFrame.
|
|
864
|
-
|
|
865
|
-
>>> df_pos, table_path = get_position_table('/path/to/position', 'targets', return_path=True)
|
|
866
|
-
# This will load the 'trajectories_targets.csv' table and also return the path to the CSV file.
|
|
867
|
-
|
|
868
|
-
"""
|
|
869
|
-
|
|
870
|
-
if not pos.endswith(os.sep):
|
|
871
|
-
table = os.sep.join([pos,'output','tables',f'trajectories_{population}.pkl'])
|
|
872
|
-
else:
|
|
873
|
-
table = pos + os.sep.join(['output','tables',f'trajectories_{population}.pkl'])
|
|
874
|
-
|
|
875
|
-
if os.path.exists(table):
|
|
876
|
-
df_pos = np.load(table, allow_pickle=True)
|
|
877
|
-
else:
|
|
878
|
-
df_pos = None
|
|
879
|
-
|
|
880
|
-
if return_path:
|
|
881
|
-
return df_pos, table
|
|
882
|
-
else:
|
|
883
|
-
return df_pos
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
def get_position_movie_path(pos, prefix=''):
|
|
887
|
-
|
|
888
|
-
"""
|
|
889
|
-
Get the path of the movie file for a given position.
|
|
890
|
-
|
|
891
|
-
This function constructs the path to a movie file within a given position directory.
|
|
892
|
-
It searches for TIFF files that match the specified prefix. If multiple matching files
|
|
893
|
-
are found, the first one is returned.
|
|
894
|
-
|
|
895
|
-
Parameters
|
|
896
|
-
----------
|
|
897
|
-
pos : str
|
|
898
|
-
The directory path for the position.
|
|
899
|
-
prefix : str, optional
|
|
900
|
-
The prefix to filter movie files. Defaults to an empty string.
|
|
901
|
-
|
|
902
|
-
Returns
|
|
903
|
-
-------
|
|
904
|
-
stack_path : str or None
|
|
905
|
-
The path to the first matching movie file, or None if no matching file is found.
|
|
906
|
-
|
|
907
|
-
Examples
|
|
908
|
-
--------
|
|
909
|
-
>>> pos_path = "path/to/position1"
|
|
910
|
-
>>> get_position_movie_path(pos_path, prefix='experiment_')
|
|
911
|
-
'path/to/position1/movie/experiment_001.tif'
|
|
912
|
-
|
|
913
|
-
>>> pos_path = "another/path/positionA"
|
|
914
|
-
>>> get_position_movie_path(pos_path)
|
|
915
|
-
'another/path/positionA/movie/001.tif'
|
|
916
|
-
|
|
917
|
-
>>> pos_path = "nonexistent/path"
|
|
918
|
-
>>> get_position_movie_path(pos_path)
|
|
919
|
-
None
|
|
920
|
-
|
|
921
|
-
"""
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if not pos.endswith(os.sep):
|
|
925
|
-
pos += os.sep
|
|
926
|
-
movies = glob(pos + os.sep.join(['movie', prefix + '*.tif']))
|
|
927
|
-
if len(movies) > 0:
|
|
928
|
-
stack_path = movies[0]
|
|
929
|
-
else:
|
|
930
|
-
stack_path = None
|
|
931
|
-
|
|
932
|
-
return stack_path
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
def load_experiment_tables(experiment, population='targets', well_option='*', position_option='*',
|
|
936
|
-
return_pos_info=False, load_pickle=False):
|
|
937
|
-
|
|
938
|
-
"""
|
|
939
|
-
Load tabular data for an experiment, optionally including position-level information.
|
|
940
|
-
|
|
941
|
-
This function retrieves and processes tables associated with positions in an experiment.
|
|
942
|
-
It supports filtering by wells and positions, and can load either CSV data or pickle files.
|
|
943
|
-
|
|
944
|
-
Parameters
|
|
945
|
-
----------
|
|
946
|
-
experiment : str
|
|
947
|
-
Path to the experiment folder to load data for.
|
|
948
|
-
population : str, optional
|
|
949
|
-
The population to extract from the position tables (`'targets'` or `'effectors'`). Default is `'targets'`.
|
|
950
|
-
well_option : str or list, optional
|
|
951
|
-
Specifies which wells to include. Default is `'*'`, meaning all wells.
|
|
952
|
-
position_option : str or list, optional
|
|
953
|
-
Specifies which positions to include within selected wells. Default is `'*'`, meaning all positions.
|
|
954
|
-
return_pos_info : bool, optional
|
|
955
|
-
If `True`, also returns a DataFrame containing position-level metadata. Default is `False`.
|
|
956
|
-
load_pickle : bool, optional
|
|
957
|
-
If `True`, loads pre-processed pickle files for the positions instead of raw data. Default is `False`.
|
|
958
|
-
|
|
959
|
-
Returns
|
|
960
|
-
-------
|
|
961
|
-
df : pandas.DataFrame or None
|
|
962
|
-
A DataFrame containing aggregated data for the specified wells and positions, or `None` if no data is found.
|
|
963
|
-
The DataFrame includes metadata such as well and position identifiers, concentrations, antibodies, and other
|
|
964
|
-
experimental parameters.
|
|
965
|
-
df_pos_info : pandas.DataFrame, optional
|
|
966
|
-
A DataFrame with metadata for each position, including file paths and experimental details. Returned only
|
|
967
|
-
if `return_pos_info=True`.
|
|
968
|
-
|
|
969
|
-
Notes
|
|
970
|
-
-----
|
|
971
|
-
- The function assumes the experiment's configuration includes details about movie prefixes, concentrations,
|
|
972
|
-
cell types, antibodies, and pharmaceutical agents.
|
|
973
|
-
- Wells and positions can be filtered using `well_option` and `position_option`, respectively. If filtering
|
|
974
|
-
fails or is invalid, those specific wells/positions are skipped.
|
|
975
|
-
- Position-level metadata is assembled into `df_pos_info` and includes paths to data and movies.
|
|
976
|
-
|
|
977
|
-
Examples
|
|
978
|
-
--------
|
|
979
|
-
Load all data for an experiment:
|
|
980
|
-
|
|
981
|
-
>>> df = load_experiment_tables("path/to/experiment1")
|
|
982
|
-
|
|
983
|
-
Load data for specific wells and positions, including position metadata:
|
|
984
|
-
|
|
985
|
-
>>> df, df_pos_info = load_experiment_tables(
|
|
986
|
-
... "experiment_01", well_option=["A1", "B1"], position_option=[0, 1], return_pos_info=True
|
|
987
|
-
... )
|
|
988
|
-
|
|
989
|
-
Use pickle files for faster loading:
|
|
990
|
-
|
|
991
|
-
>>> df = load_experiment_tables("experiment_01", load_pickle=True)
|
|
992
|
-
|
|
993
|
-
"""
|
|
994
|
-
|
|
995
|
-
config = get_config(experiment)
|
|
996
|
-
wells = get_experiment_wells(experiment)
|
|
997
|
-
|
|
998
|
-
movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
|
|
999
|
-
|
|
1000
|
-
labels = get_experiment_labels(experiment)
|
|
1001
|
-
metadata = get_experiment_metadata(experiment) # None or dict of metadata
|
|
1002
|
-
well_labels = _extract_labels_from_config(config, len(wells))
|
|
1003
|
-
|
|
1004
|
-
well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
|
|
1005
|
-
|
|
1006
|
-
df = []
|
|
1007
|
-
df_pos_info = []
|
|
1008
|
-
real_well_index = 0
|
|
1009
|
-
|
|
1010
|
-
for k, well_path in enumerate(tqdm(wells[well_indices])):
|
|
1011
|
-
|
|
1012
|
-
any_table = False # assume no table
|
|
1013
|
-
|
|
1014
|
-
well_name, well_number = extract_well_name_and_number(well_path)
|
|
1015
|
-
widx = well_indices[k]
|
|
1016
|
-
well_alias = well_labels[widx]
|
|
1017
|
-
|
|
1018
|
-
positions = get_positions_in_well(well_path)
|
|
1019
|
-
if position_indices is not None:
|
|
1020
|
-
try:
|
|
1021
|
-
positions = positions[position_indices]
|
|
1022
|
-
except Exception as e:
|
|
1023
|
-
print(e)
|
|
1024
|
-
continue
|
|
1025
|
-
|
|
1026
|
-
real_pos_index = 0
|
|
1027
|
-
for pidx, pos_path in enumerate(positions):
|
|
1028
|
-
|
|
1029
|
-
pos_name = extract_position_name(pos_path)
|
|
1030
|
-
|
|
1031
|
-
stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
|
|
1032
|
-
|
|
1033
|
-
if not load_pickle:
|
|
1034
|
-
df_pos, table = get_position_table(pos_path, population=population, return_path=True)
|
|
1035
|
-
else:
|
|
1036
|
-
df_pos, table = get_position_pickle(pos_path, population=population, return_path=True)
|
|
1037
|
-
|
|
1038
|
-
if df_pos is not None:
|
|
1039
|
-
|
|
1040
|
-
df_pos['position'] = pos_path
|
|
1041
|
-
df_pos['well'] = well_path
|
|
1042
|
-
df_pos['well_index'] = well_number
|
|
1043
|
-
df_pos['well_name'] = well_name
|
|
1044
|
-
df_pos['pos_name'] = pos_name
|
|
1045
|
-
|
|
1046
|
-
for k in list(labels.keys()):
|
|
1047
|
-
values = labels[k]
|
|
1048
|
-
try:
|
|
1049
|
-
df_pos[k] = values[widx]
|
|
1050
|
-
except Exception as e:
|
|
1051
|
-
print(f"{e=}")
|
|
1052
|
-
|
|
1053
|
-
if metadata is not None:
|
|
1054
|
-
keys = list(metadata.keys())
|
|
1055
|
-
for key in keys:
|
|
1056
|
-
df_pos[key] = metadata[key]
|
|
1057
|
-
|
|
1058
|
-
df.append(df_pos)
|
|
1059
|
-
any_table = True
|
|
1060
|
-
|
|
1061
|
-
pos_dict = {'pos_path': pos_path,
|
|
1062
|
-
'pos_index': real_pos_index,
|
|
1063
|
-
'pos_name': pos_name,
|
|
1064
|
-
'table_path': table,
|
|
1065
|
-
'stack_path': stack_path,
|
|
1066
|
-
'well_path': well_path,
|
|
1067
|
-
'well_index': real_well_index,
|
|
1068
|
-
'well_name': well_name,
|
|
1069
|
-
'well_number': well_number,
|
|
1070
|
-
'well_alias': well_alias,
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
df_pos_info.append(pos_dict)
|
|
1074
|
-
|
|
1075
|
-
real_pos_index += 1
|
|
1076
|
-
|
|
1077
|
-
if any_table:
|
|
1078
|
-
real_well_index += 1
|
|
1079
|
-
|
|
1080
|
-
df_pos_info = pd.DataFrame(df_pos_info)
|
|
1081
|
-
if len(df) > 0:
|
|
1082
|
-
df = pd.concat(df)
|
|
1083
|
-
df = df.reset_index(drop=True)
|
|
1084
|
-
else:
|
|
1085
|
-
df = None
|
|
1086
|
-
|
|
1087
|
-
if return_pos_info:
|
|
1088
|
-
return df, df_pos_info
|
|
1089
|
-
else:
|
|
1090
|
-
return df
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
def locate_stack(position, prefix='Aligned'):
|
|
1094
|
-
|
|
1095
|
-
"""
|
|
1096
|
-
|
|
1097
|
-
Locate and load a stack of images.
|
|
1098
|
-
|
|
1099
|
-
Parameters
|
|
1100
|
-
----------
|
|
1101
|
-
position : str
|
|
1102
|
-
The position folder within the well where the stack is located.
|
|
1103
|
-
prefix : str, optional
|
|
1104
|
-
The prefix used to identify the stack. The default is 'Aligned'.
|
|
1105
|
-
|
|
1106
|
-
Returns
|
|
1107
|
-
-------
|
|
1108
|
-
stack : ndarray
|
|
1109
|
-
The loaded stack as a NumPy array.
|
|
1110
|
-
|
|
1111
|
-
Raises
|
|
1112
|
-
------
|
|
1113
|
-
AssertionError
|
|
1114
|
-
If no stack with the specified prefix is found.
|
|
1115
|
-
|
|
1116
|
-
Notes
|
|
1117
|
-
-----
|
|
1118
|
-
This function locates and loads a stack of images based on the specified position and prefix.
|
|
1119
|
-
It assumes that the stack is stored in a directory named 'movie' within the specified position.
|
|
1120
|
-
The function loads the stack as a NumPy array and performs shape manipulation to have the channels
|
|
1121
|
-
at the end.
|
|
1122
|
-
|
|
1123
|
-
Examples
|
|
1124
|
-
--------
|
|
1125
|
-
>>> stack = locate_stack(position, prefix='Aligned')
|
|
1126
|
-
# Locate and load a stack of images for further processing.
|
|
1127
|
-
|
|
1128
|
-
"""
|
|
1129
|
-
|
|
1130
|
-
if not position.endswith(os.sep):
|
|
1131
|
-
position += os.sep
|
|
1132
|
-
|
|
1133
|
-
stack_path = glob(position + os.sep.join(['movie', f'{prefix}*.tif']))
|
|
1134
|
-
if not stack_path:
|
|
1135
|
-
raise FileNotFoundError(f"No movie with prefix {prefix} found...")
|
|
1136
|
-
|
|
1137
|
-
stack = imread(stack_path[0].replace('\\', '/'))
|
|
1138
|
-
stack_length = auto_load_number_of_frames(stack_path[0])
|
|
1139
|
-
|
|
1140
|
-
if stack.ndim == 4:
|
|
1141
|
-
stack = np.moveaxis(stack, 1, -1)
|
|
1142
|
-
elif stack.ndim == 3:
|
|
1143
|
-
if min(stack.shape)!=stack_length:
|
|
1144
|
-
channel_axis = np.argmin(stack.shape)
|
|
1145
|
-
if channel_axis!=(stack.ndim-1):
|
|
1146
|
-
stack = np.moveaxis(stack, channel_axis, -1)
|
|
1147
|
-
stack = stack[np.newaxis, :, :, :]
|
|
1148
|
-
else:
|
|
1149
|
-
stack = stack[:, :, :, np.newaxis]
|
|
1150
|
-
elif stack.ndim==2:
|
|
1151
|
-
stack = stack[np.newaxis, :, :, np.newaxis]
|
|
1152
|
-
|
|
1153
|
-
return stack
|
|
1154
|
-
|
|
1155
|
-
def locate_labels(position, population='target', frames=None):
|
|
1156
|
-
|
|
1157
|
-
"""
|
|
1158
|
-
Locate and load label images for a given position and population in an experiment.
|
|
1159
|
-
|
|
1160
|
-
This function retrieves and optionally loads labeled images (e.g., targets or effectors)
|
|
1161
|
-
for a specified position in an experiment. It supports loading all frames, a specific
|
|
1162
|
-
frame, or a list of frames.
|
|
1163
|
-
|
|
1164
|
-
Parameters
|
|
1165
|
-
----------
|
|
1166
|
-
position : str
|
|
1167
|
-
Path to the position directory containing label images.
|
|
1168
|
-
population : str, optional
|
|
1169
|
-
The population to load labels for. Options are `'target'` (or `'targets'`) and
|
|
1170
|
-
`'effector'` (or `'effectors'`). Default is `'target'`.
|
|
1171
|
-
frames : int, list of int, numpy.ndarray, or None, optional
|
|
1172
|
-
Specifies which frames to load:
|
|
1173
|
-
- `None`: Load all frames (default).
|
|
1174
|
-
- `int`: Load a single frame, identified by its index.
|
|
1175
|
-
- `list` or `numpy.ndarray`: Load multiple specific frames.
|
|
1176
|
-
|
|
1177
|
-
Returns
|
|
1178
|
-
-------
|
|
1179
|
-
numpy.ndarray or list of numpy.ndarray
|
|
1180
|
-
If `frames` is `None` or a single integer, returns a NumPy array of the corresponding
|
|
1181
|
-
labels. If `frames` is a list or array, returns a list of NumPy arrays for each frame.
|
|
1182
|
-
If a frame is not found, `None` is returned for that frame.
|
|
1183
|
-
|
|
1184
|
-
Notes
|
|
1185
|
-
-----
|
|
1186
|
-
- The function assumes label images are stored in subdirectories named `"labels_targets"`
|
|
1187
|
-
or `"labels_effectors"`, with filenames formatted as `####.tif` (e.g., `0001.tif`).
|
|
1188
|
-
- Frame indices are zero-padded to four digits for matching.
|
|
1189
|
-
- If `frames` is invalid or a frame is not found, `None` is returned for that frame.
|
|
1190
|
-
|
|
1191
|
-
Examples
|
|
1192
|
-
--------
|
|
1193
|
-
Load all label images for a position:
|
|
1194
|
-
|
|
1195
|
-
>>> labels = locate_labels("/path/to/position", population="target")
|
|
1196
|
-
|
|
1197
|
-
Load a single frame (frame index 3):
|
|
1198
|
-
|
|
1199
|
-
>>> label = locate_labels("/path/to/position", population="effector", frames=3)
|
|
1200
|
-
|
|
1201
|
-
Load multiple specific frames:
|
|
1202
|
-
|
|
1203
|
-
>>> labels = locate_labels("/path/to/position", population="target", frames=[0, 1, 2])
|
|
1204
|
-
|
|
1205
|
-
"""
|
|
1206
|
-
|
|
1207
|
-
if not position.endswith(os.sep):
|
|
1208
|
-
position += os.sep
|
|
1209
|
-
|
|
1210
|
-
if population.lower() == "target" or population.lower() == "targets":
|
|
1211
|
-
label_path = natsorted(glob(position + os.sep.join(["labels_targets", "*.tif"])))
|
|
1212
|
-
elif population.lower() == "effector" or population.lower() == "effectors":
|
|
1213
|
-
label_path = natsorted(glob(position + os.sep.join(["labels_effectors", "*.tif"])))
|
|
1214
|
-
else:
|
|
1215
|
-
label_path = natsorted(glob(position + os.sep.join([f"labels_{population}", "*.tif"])))
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
label_names = [os.path.split(lbl)[-1] for lbl in label_path]
|
|
1219
|
-
|
|
1220
|
-
if frames is None:
|
|
1221
|
-
|
|
1222
|
-
labels = np.array([imread(i.replace('\\', '/')) for i in label_path])
|
|
1223
|
-
|
|
1224
|
-
elif isinstance(frames, (int,float, np.int_)):
|
|
1225
|
-
|
|
1226
|
-
tzfill = str(int(frames)).zfill(4)
|
|
1227
|
-
try:
|
|
1228
|
-
idx = label_names.index(f"{tzfill}.tif")
|
|
1229
|
-
except:
|
|
1230
|
-
idx = -1
|
|
1231
|
-
|
|
1232
|
-
if idx==-1:
|
|
1233
|
-
labels = None
|
|
1234
|
-
else:
|
|
1235
|
-
labels = np.array(imread(label_path[idx].replace('\\', '/')))
|
|
1236
|
-
|
|
1237
|
-
elif isinstance(frames, (list,np.ndarray)):
|
|
1238
|
-
labels = []
|
|
1239
|
-
for f in frames:
|
|
1240
|
-
tzfill = str(int(f)).zfill(4)
|
|
1241
|
-
try:
|
|
1242
|
-
idx = label_names.index(f"{tzfill}.tif")
|
|
1243
|
-
except:
|
|
1244
|
-
idx = -1
|
|
1245
|
-
|
|
1246
|
-
if idx==-1:
|
|
1247
|
-
labels.append(None)
|
|
1248
|
-
else:
|
|
1249
|
-
labels.append(np.array(imread(label_path[idx].replace('\\', '/'))))
|
|
1250
|
-
else:
|
|
1251
|
-
print('Frames argument must be None, int or list...')
|
|
1252
|
-
|
|
1253
|
-
return labels
|
|
1254
|
-
|
|
1255
|
-
def fix_missing_labels(position, population='target', prefix='Aligned'):
|
|
1256
|
-
|
|
1257
|
-
"""
|
|
1258
|
-
Fix missing label files by creating empty label images for frames that do not have corresponding label files.
|
|
1259
|
-
|
|
1260
|
-
This function locates missing label files in a sequence of frames and creates empty labels (filled with zeros)
|
|
1261
|
-
for the frames that are missing. The function works for two types of populations: 'target' or 'effector'.
|
|
1262
|
-
|
|
1263
|
-
Parameters
|
|
1264
|
-
----------
|
|
1265
|
-
position : str
|
|
1266
|
-
The file path to the folder containing the images/label files. This is the root directory where
|
|
1267
|
-
the label files are expected to be found.
|
|
1268
|
-
population : str, optional
|
|
1269
|
-
Specifies whether to look for 'target' or 'effector' labels. Accepts 'target' or 'effector'
|
|
1270
|
-
as valid values. Default is 'target'.
|
|
1271
|
-
prefix : str, optional
|
|
1272
|
-
The prefix used to locate the image stack (default is 'Aligned').
|
|
1273
|
-
|
|
1274
|
-
Returns
|
|
1275
|
-
-------
|
|
1276
|
-
None
|
|
1277
|
-
The function creates new label files in the corresponding folder for any frames missing label files.
|
|
1278
|
-
|
|
1279
|
-
"""
|
|
1280
|
-
|
|
1281
|
-
if not position.endswith(os.sep):
|
|
1282
|
-
position += os.sep
|
|
1283
|
-
|
|
1284
|
-
stack = locate_stack(position, prefix=prefix)
|
|
1285
|
-
template = np.zeros((stack[0].shape[0], stack[0].shape[1]),dtype=int)
|
|
1286
|
-
all_frames = np.arange(len(stack))
|
|
1287
|
-
|
|
1288
|
-
if population.lower() == "target" or population.lower() == "targets":
|
|
1289
|
-
label_path = natsorted(glob(position + os.sep.join(["labels_targets", "*.tif"])))
|
|
1290
|
-
path = position + os.sep + "labels_targets"
|
|
1291
|
-
elif population.lower() == "effector" or population.lower() == "effectors":
|
|
1292
|
-
label_path = natsorted(glob(position + os.sep.join(["labels_effectors", "*.tif"])))
|
|
1293
|
-
path = position + os.sep + "labels_effectors"
|
|
1294
|
-
else:
|
|
1295
|
-
label_path = natsorted(glob(position + os.sep.join([f"labels_{population}", "*.tif"])))
|
|
1296
|
-
path = position + os.sep + f"labels_{population}"
|
|
1297
|
-
|
|
1298
|
-
if label_path!=[]:
|
|
1299
|
-
#path = os.path.split(label_path[0])[0]
|
|
1300
|
-
int_valid = [int(lbl.split(os.sep)[-1].split('.')[0]) for lbl in label_path]
|
|
1301
|
-
to_create = [x for x in all_frames if x not in int_valid]
|
|
1302
|
-
else:
|
|
1303
|
-
to_create = all_frames
|
|
1304
|
-
to_create = [str(x).zfill(4)+'.tif' for x in to_create]
|
|
1305
|
-
for file in to_create:
|
|
1306
|
-
save_tiff_imagej_compatible(os.sep.join([path, file]), template.astype(np.int16), axes='YX')
|
|
1307
|
-
#imwrite(os.sep.join([path, file]), template.astype(int))
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
def locate_stack_and_labels(position, prefix='Aligned', population="target"):
|
|
1311
|
-
|
|
1312
|
-
"""
|
|
1313
|
-
|
|
1314
|
-
Locate and load the stack and corresponding segmentation labels.
|
|
1315
|
-
|
|
1316
|
-
Parameters
|
|
1317
|
-
----------
|
|
1318
|
-
position : str
|
|
1319
|
-
The position or directory path where the stack and labels are located.
|
|
1320
|
-
prefix : str, optional
|
|
1321
|
-
The prefix used to identify the stack. The default is 'Aligned'.
|
|
1322
|
-
population : str, optional
|
|
1323
|
-
The population for which the segmentation must be located. The default is 'target'.
|
|
1324
|
-
|
|
1325
|
-
Returns
|
|
1326
|
-
-------
|
|
1327
|
-
stack : ndarray
|
|
1328
|
-
The loaded stack as a NumPy array.
|
|
1329
|
-
labels : ndarray
|
|
1330
|
-
The loaded segmentation labels as a NumPy array.
|
|
1331
|
-
|
|
1332
|
-
Raises
|
|
1333
|
-
------
|
|
1334
|
-
AssertionError
|
|
1335
|
-
If no stack with the specified prefix is found or if the shape of the stack and labels do not match.
|
|
1336
|
-
|
|
1337
|
-
Notes
|
|
1338
|
-
-----
|
|
1339
|
-
This function locates the stack and corresponding segmentation labels based on the specified position and population.
|
|
1340
|
-
It assumes that the stack and labels are stored in separate directories: 'movie' for the stack and 'labels' or 'labels_effectors' for the labels.
|
|
1341
|
-
The function loads the stack and labels as NumPy arrays and performs shape validation.
|
|
1342
|
-
|
|
1343
|
-
Examples
|
|
1344
|
-
--------
|
|
1345
|
-
>>> stack, labels = locate_stack_and_labels(position, prefix='Aligned', population="target")
|
|
1346
|
-
# Locate and load the stack and segmentation labels for further processing.
|
|
1347
|
-
|
|
1348
|
-
"""
|
|
1349
|
-
|
|
1350
|
-
position = position.replace('\\', '/')
|
|
1351
|
-
labels = locate_labels(position, population=population)
|
|
1352
|
-
stack = locate_stack(position, prefix=prefix)
|
|
1353
|
-
if len(labels) < len(stack):
|
|
1354
|
-
fix_missing_labels(position, population=population, prefix=prefix)
|
|
1355
|
-
labels = locate_labels(position, population=population)
|
|
1356
|
-
assert len(stack) == len(
|
|
1357
|
-
labels), f"The shape of the stack {stack.shape} does not match with the shape of the labels {labels.shape}"
|
|
1358
|
-
|
|
1359
|
-
return stack, labels
|
|
1360
|
-
|
|
1361
|
-
def load_tracking_data(position, prefix="Aligned", population="target"):
|
|
1362
|
-
"""
|
|
1363
|
-
|
|
1364
|
-
Load the tracking data, labels, and stack for a given position and population.
|
|
1365
|
-
|
|
1366
|
-
Parameters
|
|
1367
|
-
----------
|
|
1368
|
-
position : str
|
|
1369
|
-
The position or directory where the data is located.
|
|
1370
|
-
prefix : str, optional
|
|
1371
|
-
The prefix used in the filenames of the stack images (default is "Aligned").
|
|
1372
|
-
population : str, optional
|
|
1373
|
-
The population to load the data for. Options are "target" or "effector" (default is "target").
|
|
1374
|
-
|
|
1375
|
-
Returns
|
|
1376
|
-
-------
|
|
1377
|
-
trajectories : DataFrame
|
|
1378
|
-
The tracking data loaded as a pandas DataFrame.
|
|
1379
|
-
labels : ndarray
|
|
1380
|
-
The segmentation labels loaded as a numpy ndarray.
|
|
1381
|
-
stack : ndarray
|
|
1382
|
-
The image stack loaded as a numpy ndarray.
|
|
1383
|
-
|
|
1384
|
-
Notes
|
|
1385
|
-
-----
|
|
1386
|
-
This function loads the tracking data, labels, and stack for a given position and population.
|
|
1387
|
-
It reads the trajectories from the appropriate CSV file based on the specified population.
|
|
1388
|
-
The stack and labels are located using the `locate_stack_and_labels` function.
|
|
1389
|
-
The resulting tracking data is returned as a pandas DataFrame, and the labels and stack are returned as numpy ndarrays.
|
|
1390
|
-
|
|
1391
|
-
Examples
|
|
1392
|
-
--------
|
|
1393
|
-
>>> trajectories, labels, stack = load_tracking_data(position, population="target")
|
|
1394
|
-
# Load the tracking data, labels, and stack for the specified position and target population.
|
|
1395
|
-
|
|
1396
|
-
"""
|
|
1397
|
-
|
|
1398
|
-
position = position.replace('\\', '/')
|
|
1399
|
-
if population.lower() == "target" or population.lower() == "targets":
|
|
1400
|
-
trajectories = pd.read_csv(position + os.sep.join(['output', 'tables', 'trajectories_targets.csv']))
|
|
1401
|
-
elif population.lower() == "effector" or population.lower() == "effectors":
|
|
1402
|
-
trajectories = pd.read_csv(position + os.sep.join(['output', 'tables', 'trajectories_effectors.csv']))
|
|
1403
|
-
else:
|
|
1404
|
-
trajectories = pd.read_csv(position + os.sep.join(['output', 'tables', f'trajectories_{population}.csv']))
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
stack, labels = locate_stack_and_labels(position, prefix=prefix, population=population)
|
|
1408
|
-
|
|
1409
|
-
return trajectories, labels, stack
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
def auto_load_number_of_frames(stack_path):
|
|
1413
|
-
|
|
1414
|
-
"""
|
|
1415
|
-
Automatically determine the number of frames in a TIFF image stack.
|
|
1416
|
-
|
|
1417
|
-
This function extracts the number of frames (time slices) from the metadata of a TIFF file
|
|
1418
|
-
or infers it from the stack dimensions when metadata is unavailable. It is robust to
|
|
1419
|
-
variations in metadata structure and handles multi-channel images.
|
|
1420
|
-
|
|
1421
|
-
Parameters
|
|
1422
|
-
----------
|
|
1423
|
-
stack_path : str
|
|
1424
|
-
Path to the TIFF image stack file.
|
|
1425
|
-
|
|
1426
|
-
Returns
|
|
1427
|
-
-------
|
|
1428
|
-
int or None
|
|
1429
|
-
The number of frames in the image stack. Returns `None` if the path is `None`
|
|
1430
|
-
or the frame count cannot be determined.
|
|
1431
|
-
|
|
1432
|
-
Notes
|
|
1433
|
-
-----
|
|
1434
|
-
- The function attempts to extract the `frames` or `slices` attributes from the
|
|
1435
|
-
TIFF metadata, specifically the `ImageDescription` tag.
|
|
1436
|
-
- If metadata extraction fails, the function reads the image stack and infers
|
|
1437
|
-
the number of frames based on the stack dimensions.
|
|
1438
|
-
- Multi-channel stacks are handled by assuming the number of channels is specified
|
|
1439
|
-
in the metadata under the `channels` attribute.
|
|
1440
|
-
|
|
1441
|
-
Examples
|
|
1442
|
-
--------
|
|
1443
|
-
Automatically detect the number of frames in a TIFF stack:
|
|
1444
|
-
|
|
1445
|
-
>>> frames = auto_load_number_of_frames("experiment_stack.tif")
|
|
1446
|
-
Automatically detected stack length: 120...
|
|
1447
|
-
|
|
1448
|
-
Handle a single-frame TIFF:
|
|
1449
|
-
|
|
1450
|
-
>>> frames = auto_load_number_of_frames("single_frame_stack.tif")
|
|
1451
|
-
Automatically detected stack length: 1...
|
|
1452
|
-
|
|
1453
|
-
Handle invalid or missing paths gracefully:
|
|
1454
|
-
|
|
1455
|
-
>>> frames = auto_load_number_of_frames("stack.tif")
|
|
1456
|
-
>>> print(frames)
|
|
1457
|
-
None
|
|
1458
|
-
|
|
1459
|
-
"""
|
|
1460
|
-
|
|
1461
|
-
if stack_path is None:
|
|
1462
|
-
return None
|
|
1463
|
-
|
|
1464
|
-
stack_path = stack_path.replace('\\','/')
|
|
1465
|
-
n_channels=1
|
|
1466
|
-
|
|
1467
|
-
with TiffFile(stack_path) as tif:
|
|
1468
|
-
try:
|
|
1469
|
-
tif_tags = {}
|
|
1470
|
-
for tag in tif.pages[0].tags.values():
|
|
1471
|
-
name, value = tag.name, tag.value
|
|
1472
|
-
tif_tags[name] = value
|
|
1473
|
-
img_desc = tif_tags["ImageDescription"]
|
|
1474
|
-
attr = img_desc.split("\n")
|
|
1475
|
-
n_channels = int(attr[np.argmax([s.startswith("channels") for s in attr])].split("=")[-1])
|
|
1476
|
-
except Exception as e:
|
|
1477
|
-
pass
|
|
1478
|
-
try:
|
|
1479
|
-
# Try nframes
|
|
1480
|
-
nslices = int(attr[np.argmax([s.startswith("frames") for s in attr])].split("=")[-1])
|
|
1481
|
-
if nslices > 1:
|
|
1482
|
-
len_movie = nslices
|
|
1483
|
-
else:
|
|
1484
|
-
break_the_code()
|
|
1485
|
-
except:
|
|
1486
|
-
try:
|
|
1487
|
-
# try nslices
|
|
1488
|
-
frames = int(attr[np.argmax([s.startswith("slices") for s in attr])].split("=")[-1])
|
|
1489
|
-
len_movie = frames
|
|
1490
|
-
except:
|
|
1491
|
-
pass
|
|
1492
|
-
|
|
1493
|
-
try:
|
|
1494
|
-
del tif;
|
|
1495
|
-
del tif_tags;
|
|
1496
|
-
del img_desc;
|
|
1497
|
-
except:
|
|
1498
|
-
pass
|
|
1499
|
-
|
|
1500
|
-
if 'len_movie' not in locals():
|
|
1501
|
-
stack = imread(stack_path)
|
|
1502
|
-
len_movie = len(stack)
|
|
1503
|
-
if len_movie==n_channels and stack.ndim==3:
|
|
1504
|
-
len_movie = 1
|
|
1505
|
-
if stack.ndim==2:
|
|
1506
|
-
len_movie = 1
|
|
1507
|
-
del stack
|
|
1508
|
-
gc.collect()
|
|
1509
|
-
|
|
1510
|
-
print(f'Automatically detected stack length: {len_movie}...')
|
|
1511
|
-
|
|
1512
|
-
return len_movie if 'len_movie' in locals() else None
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
def parse_isotropic_radii(string):
|
|
1516
|
-
|
|
1517
|
-
"""
|
|
1518
|
-
Parse a string representing isotropic radii into a structured list.
|
|
1519
|
-
|
|
1520
|
-
This function extracts integer values and ranges (denoted by square brackets)
|
|
1521
|
-
from a string input and returns them as a list. Single values are stored as integers,
|
|
1522
|
-
while ranges are represented as lists of two integers.
|
|
1523
|
-
|
|
1524
|
-
Parameters
|
|
1525
|
-
----------
|
|
1526
|
-
string : str
|
|
1527
|
-
The input string containing radii and ranges, separated by commas or spaces.
|
|
1528
|
-
Ranges should be enclosed in square brackets, e.g., `[1 2]`.
|
|
1529
|
-
|
|
1530
|
-
Returns
|
|
1531
|
-
-------
|
|
1532
|
-
list
|
|
1533
|
-
A list of parsed radii where:
|
|
1534
|
-
- Single integers are included as `int`.
|
|
1535
|
-
- Ranges are included as two-element lists `[start, end]`.
|
|
1536
|
-
|
|
1537
|
-
Examples
|
|
1538
|
-
--------
|
|
1539
|
-
Parse a string with single radii and ranges:
|
|
1540
|
-
|
|
1541
|
-
>>> parse_isotropic_radii("1, [2 3], 4")
|
|
1542
|
-
[1, [2, 3], 4]
|
|
1543
|
-
|
|
1544
|
-
Handle inputs with mixed delimiters:
|
|
1545
|
-
|
|
1546
|
-
>>> parse_isotropic_radii("5 [6 7], 8")
|
|
1547
|
-
[5, [6, 7], 8]
|
|
1548
|
-
|
|
1549
|
-
Notes
|
|
1550
|
-
-----
|
|
1551
|
-
- The function splits the input string by commas or spaces.
|
|
1552
|
-
- It identifies ranges using square brackets and assumes that ranges are always
|
|
1553
|
-
two consecutive values.
|
|
1554
|
-
- Non-integer sections of the string are ignored.
|
|
1555
|
-
|
|
1556
|
-
"""
|
|
1557
|
-
|
|
1558
|
-
sections = re.split(r"[ ,]", string)
|
|
1559
|
-
radii = []
|
|
1560
|
-
for k, s in enumerate(sections):
|
|
1561
|
-
if s.isdigit():
|
|
1562
|
-
radii.append(int(s))
|
|
1563
|
-
if '[' in s:
|
|
1564
|
-
ring = [int(s.replace('[', '')), int(sections[k + 1].replace(']', ''))]
|
|
1565
|
-
radii.append(ring)
|
|
1566
|
-
else:
|
|
1567
|
-
pass
|
|
1568
|
-
return radii
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
def get_tracking_configs_list(return_path=False):
|
|
1572
|
-
"""
|
|
1573
|
-
|
|
1574
|
-
Retrieve a list of available tracking configurations.
|
|
1575
|
-
|
|
1576
|
-
Parameters
|
|
1577
|
-
----------
|
|
1578
|
-
return_path : bool, optional
|
|
1579
|
-
If True, also returns the path to the models. Default is False.
|
|
1580
|
-
|
|
1581
|
-
Returns
|
|
1582
|
-
-------
|
|
1583
|
-
list or tuple
|
|
1584
|
-
If return_path is False, returns a list of available tracking configurations.
|
|
1585
|
-
If return_path is True, returns a tuple containing the list of models and the path to the models.
|
|
1586
|
-
|
|
1587
|
-
Notes
|
|
1588
|
-
-----
|
|
1589
|
-
This function retrieves the list of available tracking configurations by searching for model directories
|
|
1590
|
-
in the predefined model path. The model path is derived from the parent directory of the current script
|
|
1591
|
-
location and the path to the model directory. By default, it returns only the names of the models.
|
|
1592
|
-
If return_path is set to True, it also returns the path to the models.
|
|
1593
|
-
|
|
1594
|
-
Examples
|
|
1595
|
-
--------
|
|
1596
|
-
>>> models = get_tracking_configs_list()
|
|
1597
|
-
# Retrieve a list of available tracking configurations.
|
|
1598
|
-
|
|
1599
|
-
>>> models, path = get_tracking_configs_list(return_path=True)
|
|
1600
|
-
# Retrieve a list of available tracking configurations.
|
|
1601
|
-
|
|
1602
|
-
"""
|
|
1603
|
-
|
|
1604
|
-
modelpath = os.sep.join(
|
|
1605
|
-
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models", "tracking_configs",
|
|
1606
|
-
os.sep])
|
|
1607
|
-
available_models = glob(modelpath + '*.json')
|
|
1608
|
-
available_models = [m.replace('\\', '/').split('/')[-1] for m in available_models]
|
|
1609
|
-
available_models = [m.replace('\\', '/').split('.')[0] for m in available_models]
|
|
1610
|
-
|
|
1611
|
-
if not return_path:
|
|
1612
|
-
return available_models
|
|
1613
|
-
else:
|
|
1614
|
-
return available_models, modelpath
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
def interpret_tracking_configuration(config):
|
|
1618
|
-
|
|
1619
|
-
"""
|
|
1620
|
-
Interpret and resolve the path for a tracking configuration file.
|
|
1621
|
-
|
|
1622
|
-
This function determines the appropriate configuration file path based on the input.
|
|
1623
|
-
If the input is a string representing an existing path or a known configuration name,
|
|
1624
|
-
it resolves to the correct file path. If the input is invalid or `None`, a default
|
|
1625
|
-
configuration is returned.
|
|
1626
|
-
|
|
1627
|
-
Parameters
|
|
1628
|
-
----------
|
|
1629
|
-
config : str or None
|
|
1630
|
-
The input configuration, which can be:
|
|
1631
|
-
- A string representing the full path to a configuration file.
|
|
1632
|
-
- A short name of a configuration file without the `.json` extension.
|
|
1633
|
-
- `None` to use a default configuration.
|
|
1634
|
-
|
|
1635
|
-
Returns
|
|
1636
|
-
-------
|
|
1637
|
-
str
|
|
1638
|
-
The resolved path to the configuration file.
|
|
1639
|
-
|
|
1640
|
-
Notes
|
|
1641
|
-
-----
|
|
1642
|
-
- If `config` is a string and the specified path exists, it is returned as-is.
|
|
1643
|
-
- If `config` is a name, the function searches in the `tracking_configs` directory
|
|
1644
|
-
within the `celldetective` models folder.
|
|
1645
|
-
- If the file or name is not found, or if `config` is `None`, the function falls
|
|
1646
|
-
back to a default configuration using `cell_config()`.
|
|
1647
|
-
|
|
1648
|
-
Examples
|
|
1649
|
-
--------
|
|
1650
|
-
Resolve a full path:
|
|
1651
|
-
|
|
1652
|
-
>>> interpret_tracking_configuration("/path/to/config.json")
|
|
1653
|
-
'/path/to/config.json'
|
|
1654
|
-
|
|
1655
|
-
Resolve a named configuration:
|
|
1656
|
-
|
|
1657
|
-
>>> interpret_tracking_configuration("default_tracking")
|
|
1658
|
-
'/path/to/celldetective/models/tracking_configs/default_tracking.json'
|
|
1659
|
-
|
|
1660
|
-
Handle `None` to return the default configuration:
|
|
1661
|
-
|
|
1662
|
-
>>> interpret_tracking_configuration(None)
|
|
1663
|
-
'/path/to/default/config.json'
|
|
1664
|
-
|
|
1665
|
-
"""
|
|
1666
|
-
|
|
1667
|
-
if isinstance(config, str):
|
|
1668
|
-
if os.path.exists(config):
|
|
1669
|
-
return config
|
|
1670
|
-
else:
|
|
1671
|
-
modelpath = os.sep.join(
|
|
1672
|
-
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models",
|
|
1673
|
-
"tracking_configs", os.sep])
|
|
1674
|
-
if os.path.exists(modelpath + config + '.json'):
|
|
1675
|
-
return modelpath + config + '.json'
|
|
1676
|
-
else:
|
|
1677
|
-
config = cell_config()
|
|
1678
|
-
elif config is None:
|
|
1679
|
-
config = cell_config()
|
|
1680
|
-
|
|
1681
|
-
return config
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
def get_signal_models_list(return_path=False):
|
|
1685
|
-
|
|
1686
|
-
"""
|
|
1687
|
-
|
|
1688
|
-
Retrieve a list of available signal detection models.
|
|
1689
|
-
|
|
1690
|
-
Parameters
|
|
1691
|
-
----------
|
|
1692
|
-
return_path : bool, optional
|
|
1693
|
-
If True, also returns the path to the models. Default is False.
|
|
1694
|
-
|
|
1695
|
-
Returns
|
|
1696
|
-
-------
|
|
1697
|
-
list or tuple
|
|
1698
|
-
If return_path is False, returns a list of available signal detection models.
|
|
1699
|
-
If return_path is True, returns a tuple containing the list of models and the path to the models.
|
|
1700
|
-
|
|
1701
|
-
Notes
|
|
1702
|
-
-----
|
|
1703
|
-
This function retrieves the list of available signal detection models by searching for model directories
|
|
1704
|
-
in the predefined model path. The model path is derived from the parent directory of the current script
|
|
1705
|
-
location and the path to the model directory. By default, it returns only the names of the models.
|
|
1706
|
-
If return_path is set to True, it also returns the path to the models.
|
|
1707
|
-
|
|
1708
|
-
Examples
|
|
1709
|
-
--------
|
|
1710
|
-
>>> models = get_signal_models_list()
|
|
1711
|
-
# Retrieve a list of available signal detection models.
|
|
1712
|
-
|
|
1713
|
-
>>> models, path = get_signal_models_list(return_path=True)
|
|
1714
|
-
# Retrieve a list of available signal detection models and the path to the models.
|
|
1715
|
-
|
|
1716
|
-
"""
|
|
1717
|
-
|
|
1718
|
-
modelpath = os.sep.join(
|
|
1719
|
-
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models", "signal_detection",
|
|
1720
|
-
os.sep])
|
|
1721
|
-
repository_models = get_zenodo_files(cat=os.sep.join(["models", "signal_detection"]))
|
|
1722
|
-
|
|
1723
|
-
available_models = glob(modelpath + f'*{os.sep}')
|
|
1724
|
-
available_models = [m.replace('\\', '/').split('/')[-2] for m in available_models]
|
|
1725
|
-
for rm in repository_models:
|
|
1726
|
-
if rm not in available_models:
|
|
1727
|
-
available_models.append(rm)
|
|
1728
|
-
|
|
1729
|
-
if not return_path:
|
|
1730
|
-
return available_models
|
|
1731
|
-
else:
|
|
1732
|
-
return available_models, modelpath
|
|
1733
|
-
|
|
1734
|
-
def get_pair_signal_models_list(return_path=False):
|
|
1735
|
-
"""
|
|
1736
|
-
|
|
1737
|
-
Retrieve a list of available signal detection models.
|
|
1738
|
-
|
|
1739
|
-
Parameters
|
|
1740
|
-
----------
|
|
1741
|
-
return_path : bool, optional
|
|
1742
|
-
If True, also returns the path to the models. Default is False.
|
|
1743
|
-
|
|
1744
|
-
Returns
|
|
1745
|
-
-------
|
|
1746
|
-
list or tuple
|
|
1747
|
-
If return_path is False, returns a list of available signal detection models.
|
|
1748
|
-
If return_path is True, returns a tuple containing the list of models and the path to the models.
|
|
1749
|
-
|
|
1750
|
-
Notes
|
|
1751
|
-
-----
|
|
1752
|
-
This function retrieves the list of available signal detection models by searching for model directories
|
|
1753
|
-
in the predefined model path. The model path is derived from the parent directory of the current script
|
|
1754
|
-
location and the path to the model directory. By default, it returns only the names of the models.
|
|
1755
|
-
If return_path is set to True, it also returns the path to the models.
|
|
1756
|
-
|
|
1757
|
-
Examples
|
|
1758
|
-
--------
|
|
1759
|
-
>>> models = get_signal_models_list()
|
|
1760
|
-
# Retrieve a list of available signal detection models.
|
|
1761
|
-
|
|
1762
|
-
>>> models, path = get_signal_models_list(return_path=True)
|
|
1763
|
-
# Retrieve a list of available signal detection models and the path to the models.
|
|
1764
|
-
|
|
1765
|
-
"""
|
|
1766
|
-
|
|
1767
|
-
modelpath = os.sep.join(
|
|
1768
|
-
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models", "pair_signal_detection",
|
|
1769
|
-
os.sep])
|
|
1770
|
-
#repository_models = get_zenodo_files(cat=os.sep.join(["models", "pair_signal_detection"]))
|
|
1771
|
-
|
|
1772
|
-
available_models = glob(modelpath + f'*{os.sep}')
|
|
1773
|
-
available_models = [m.replace('\\', '/').split('/')[-2] for m in available_models]
|
|
1774
|
-
#for rm in repository_models:
|
|
1775
|
-
# if rm not in available_models:
|
|
1776
|
-
# available_models.append(rm)
|
|
1777
|
-
|
|
1778
|
-
if not return_path:
|
|
1779
|
-
return available_models
|
|
1780
|
-
else:
|
|
1781
|
-
return available_models, modelpath
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
def locate_signal_model(name, path=None, pairs=False):
|
|
1785
|
-
|
|
1786
|
-
"""
|
|
1787
|
-
Locate a signal detection model by name, either locally or from Zenodo.
|
|
1788
|
-
|
|
1789
|
-
This function searches for a signal detection model with the specified name in the local
|
|
1790
|
-
`celldetective` directory. If the model is not found locally, it attempts to download
|
|
1791
|
-
the model from Zenodo.
|
|
1792
|
-
|
|
1793
|
-
Parameters
|
|
1794
|
-
----------
|
|
1795
|
-
name : str
|
|
1796
|
-
The name of the signal detection model to locate.
|
|
1797
|
-
path : str, optional
|
|
1798
|
-
An additional directory path to search for the model. If provided, this directory
|
|
1799
|
-
is also scanned for matching models. Default is `None`.
|
|
1800
|
-
pairs : bool, optional
|
|
1801
|
-
If `True`, searches for paired signal detection models in the `pair_signal_detection`
|
|
1802
|
-
subdirectory. If `False`, searches in the `signal_detection` subdirectory. Default is `False`.
|
|
1803
|
-
|
|
1804
|
-
Returns
|
|
1805
|
-
-------
|
|
1806
|
-
str or None
|
|
1807
|
-
The full path to the located model directory if found, or `None` if the model is not available
|
|
1808
|
-
locally or on Zenodo.
|
|
1809
|
-
|
|
1810
|
-
Notes
|
|
1811
|
-
-----
|
|
1812
|
-
- The function first searches in the `celldetective/models/signal_detection` or
|
|
1813
|
-
`celldetective/models/pair_signal_detection` directory based on the `pairs` argument.
|
|
1814
|
-
- If a `path` is specified, it is searched in addition to the default directories.
|
|
1815
|
-
- If the model is not found locally, the function queries Zenodo for the model. If available,
|
|
1816
|
-
the model is downloaded to the appropriate `celldetective` subdirectory.
|
|
1817
|
-
|
|
1818
|
-
Examples
|
|
1819
|
-
--------
|
|
1820
|
-
Search for a signal detection model locally:
|
|
1821
|
-
|
|
1822
|
-
>>> locate_signal_model("example_model")
|
|
1823
|
-
'path/to/celldetective/models/signal_detection/example_model/'
|
|
1824
|
-
|
|
1825
|
-
Search for a paired signal detection model:
|
|
1826
|
-
|
|
1827
|
-
>>> locate_signal_model("paired_model", pairs=True)
|
|
1828
|
-
'path/to/celldetective/models/pair_signal_detection/paired_model/'
|
|
1829
|
-
|
|
1830
|
-
Include an additional search path:
|
|
1831
|
-
|
|
1832
|
-
>>> locate_signal_model("custom_model", path="/additional/models/")
|
|
1833
|
-
'/additional/models/custom_model/'
|
|
1834
|
-
|
|
1835
|
-
Handle a model available only on Zenodo:
|
|
1836
|
-
|
|
1837
|
-
>>> locate_signal_model("remote_model")
|
|
1838
|
-
'path/to/celldetective/models/signal_detection/remote_model/'
|
|
1839
|
-
|
|
1840
|
-
"""
|
|
1841
|
-
|
|
1842
|
-
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
|
|
1843
|
-
modelpath = os.sep.join([main_dir, "models", "signal_detection", os.sep])
|
|
1844
|
-
if pairs:
|
|
1845
|
-
modelpath = os.sep.join([main_dir, "models", "pair_signal_detection", os.sep])
|
|
1846
|
-
print(f'Looking for {name} in {modelpath}')
|
|
1847
|
-
models = glob(modelpath + f'*{os.sep}')
|
|
1848
|
-
if path is not None:
|
|
1849
|
-
if not path.endswith(os.sep):
|
|
1850
|
-
path += os.sep
|
|
1851
|
-
models += glob(path + f'*{os.sep}')
|
|
1852
|
-
|
|
1853
|
-
match = None
|
|
1854
|
-
for m in models:
|
|
1855
|
-
if name == m.replace('\\', os.sep).split(os.sep)[-2]:
|
|
1856
|
-
match = m
|
|
1857
|
-
return match
|
|
1858
|
-
# else no match, try zenodo
|
|
1859
|
-
files, categories = get_zenodo_files()
|
|
1860
|
-
if name in files:
|
|
1861
|
-
index = files.index(name)
|
|
1862
|
-
cat = categories[index]
|
|
1863
|
-
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
1864
|
-
match = os.sep.join([main_dir, cat, name]) + os.sep
|
|
1865
|
-
return match
|
|
1866
|
-
|
|
1867
|
-
def locate_pair_signal_model(name, path=None):
|
|
1868
|
-
|
|
1869
|
-
"""
|
|
1870
|
-
Locate a pair signal detection model by name.
|
|
1871
|
-
|
|
1872
|
-
This function searches for a pair signal detection model in the default
|
|
1873
|
-
`celldetective` directory and optionally in an additional user-specified path.
|
|
1874
|
-
|
|
1875
|
-
Parameters
|
|
1876
|
-
----------
|
|
1877
|
-
name : str
|
|
1878
|
-
The name of the pair signal detection model to locate.
|
|
1879
|
-
path : str, optional
|
|
1880
|
-
An additional directory path to search for the model. If provided, this directory
|
|
1881
|
-
is also scanned for matching models. Default is `None`.
|
|
1882
|
-
|
|
1883
|
-
Returns
|
|
1884
|
-
-------
|
|
1885
|
-
str or None
|
|
1886
|
-
The full path to the located model directory if found, or `None` if no matching
|
|
1887
|
-
model is located.
|
|
1888
|
-
|
|
1889
|
-
Notes
|
|
1890
|
-
-----
|
|
1891
|
-
- The function first searches in the default `celldetective/models/pair_signal_detection`
|
|
1892
|
-
directory.
|
|
1893
|
-
- If a `path` is specified, it is searched in addition to the default directory.
|
|
1894
|
-
- The function prints the search path and model name during execution.
|
|
1895
|
-
|
|
1896
|
-
Examples
|
|
1897
|
-
--------
|
|
1898
|
-
Locate a model in the default directory:
|
|
1899
|
-
|
|
1900
|
-
>>> locate_pair_signal_model("example_model")
|
|
1901
|
-
'path/to/celldetective/models/pair_signal_detection/example_model/'
|
|
1902
|
-
|
|
1903
|
-
Include an additional search directory:
|
|
1904
|
-
|
|
1905
|
-
>>> locate_pair_signal_model("custom_model", path="/additional/models/")
|
|
1906
|
-
'/additional/models/custom_model/'
|
|
1907
|
-
|
|
1908
|
-
"""
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
|
|
1912
|
-
modelpath = os.sep.join([main_dir, "models", "pair_signal_detection", os.sep])
|
|
1913
|
-
print(f'Looking for {name} in {modelpath}')
|
|
1914
|
-
models = glob(modelpath + f'*{os.sep}')
|
|
1915
|
-
if path is not None:
|
|
1916
|
-
if not path.endswith(os.sep):
|
|
1917
|
-
path += os.sep
|
|
1918
|
-
models += glob(path + f'*{os.sep}')
|
|
1919
|
-
|
|
1920
|
-
def relabel_segmentation(labels, df, exclude_nans=True, column_labels={'track': "TRACK_ID", 'frame': 'FRAME', 'y': 'POSITION_Y', 'x': 'POSITION_X', 'label': 'class_id'}, threads=1):
|
|
1921
|
-
|
|
1922
|
-
"""
|
|
1923
|
-
Relabel the segmentation labels with the tracking IDs from the tracks.
|
|
1924
|
-
|
|
1925
|
-
The function reassigns the mask value for each cell with the associated `TRACK_ID`, if it exists
|
|
1926
|
-
in the trajectory table (`df`). If no track uses the cell mask, a new track with a single point
|
|
1927
|
-
is generated on the fly (max of `TRACK_ID` values + i, for i=0 to N such cells). It supports
|
|
1928
|
-
multithreaded processing for faster execution on large datasets.
|
|
1929
|
-
|
|
1930
|
-
Parameters
|
|
1931
|
-
----------
|
|
1932
|
-
labels : np.ndarray
|
|
1933
|
-
A (TYX) array where each frame contains a 2D segmentation mask. Each unique
|
|
1934
|
-
non-zero integer represents a labeled object.
|
|
1935
|
-
df : pandas.DataFrame
|
|
1936
|
-
A DataFrame containing tracking information with columns
|
|
1937
|
-
specified in `column_labels`. Must include at least frame, track ID, and object ID.
|
|
1938
|
-
exclude_nans : bool, optional
|
|
1939
|
-
Whether to exclude rows in `df` with NaN values in the column specified by
|
|
1940
|
-
`column_labels['label']`. Default is `True`.
|
|
1941
|
-
column_labels : dict, optional
|
|
1942
|
-
A dictionary specifying the column names in `df`. Default is:
|
|
1943
|
-
- `'track'`: Track ID column name (`"TRACK_ID"`)
|
|
1944
|
-
- `'frame'`: Frame column name (`"FRAME"`)
|
|
1945
|
-
- `'y'`: Y-coordinate column name (`"POSITION_Y"`)
|
|
1946
|
-
- `'x'`: X-coordinate column name (`"POSITION_X"`)
|
|
1947
|
-
- `'label'`: Object ID column name (`"class_id"`)
|
|
1948
|
-
threads : int, optional
|
|
1949
|
-
Number of threads to use for multithreaded processing. Default is `1`.
|
|
1950
|
-
|
|
1951
|
-
Returns
|
|
1952
|
-
-------
|
|
1953
|
-
np.ndarray
|
|
1954
|
-
A new (TYX) array with the same shape as `labels`, where objects are relabeled
|
|
1955
|
-
according to their tracking identity in `df`.
|
|
1956
|
-
|
|
1957
|
-
Notes
|
|
1958
|
-
-----
|
|
1959
|
-
- For frames where labeled objects in `labels` do not match any entries in the `df`,
|
|
1960
|
-
new track IDs are generated for the unmatched labels.
|
|
1961
|
-
- The relabeling process maintains synchronization across threads using a shared
|
|
1962
|
-
counter for generating unique track IDs.
|
|
1963
|
-
|
|
1964
|
-
Examples
|
|
1965
|
-
--------
|
|
1966
|
-
Relabel segmentation using tracking data:
|
|
1967
|
-
|
|
1968
|
-
>>> labels = np.random.randint(0, 5, (10, 100, 100))
|
|
1969
|
-
>>> df = pd.DataFrame({
|
|
1970
|
-
... "TRACK_ID": [1, 2, 1, 2],
|
|
1971
|
-
... "FRAME": [0, 0, 1, 1],
|
|
1972
|
-
... "class_id": [1, 2, 1, 2],
|
|
1973
|
-
... })
|
|
1974
|
-
>>> new_labels = relabel_segmentation(labels, df, threads=2)
|
|
1975
|
-
Done.
|
|
1976
|
-
|
|
1977
|
-
Use custom column labels and exclude rows with NaNs:
|
|
1978
|
-
|
|
1979
|
-
>>> column_labels = {
|
|
1980
|
-
... 'track': "track_id",
|
|
1981
|
-
... 'frame': "time",
|
|
1982
|
-
... 'label': "object_id"
|
|
1983
|
-
... }
|
|
1984
|
-
>>> new_labels = relabel_segmentation(labels, df, column_labels=column_labels, exclude_nans=True)
|
|
1985
|
-
Done.
|
|
1986
|
-
|
|
1987
|
-
"""
|
|
1988
|
-
|
|
1989
|
-
n_threads = threads
|
|
1990
|
-
df = df.sort_values(by=[column_labels['track'],column_labels['frame']])
|
|
1991
|
-
if exclude_nans:
|
|
1992
|
-
df = df.dropna(subset=column_labels['label'])
|
|
1993
|
-
|
|
1994
|
-
new_labels = np.zeros_like(labels)
|
|
1995
|
-
shared_data = {"s": 0}
|
|
1996
|
-
|
|
1997
|
-
def rewrite_labels(indices):
|
|
1998
|
-
|
|
1999
|
-
all_track_ids = df[column_labels['track']].dropna().unique()
|
|
2000
|
-
|
|
2001
|
-
for t in tqdm(indices):
|
|
2002
|
-
|
|
2003
|
-
f = int(t)
|
|
2004
|
-
cells = df.loc[df[column_labels['frame']] == f, [column_labels['track'], column_labels['label']]].to_numpy()
|
|
2005
|
-
tracks_at_t = list(cells[:,0])
|
|
2006
|
-
identities = list(cells[:,1])
|
|
2007
|
-
|
|
2008
|
-
labels_at_t = list(np.unique(labels[f]))
|
|
2009
|
-
if 0 in labels_at_t:
|
|
2010
|
-
labels_at_t.remove(0)
|
|
2011
|
-
labels_not_in_df = [lbl for lbl in labels_at_t if lbl not in identities]
|
|
2012
|
-
for lbl in labels_not_in_df:
|
|
2013
|
-
with threading.Lock(): # Synchronize access to `shared_data["s"]`
|
|
2014
|
-
track_id = max(all_track_ids) + shared_data["s"]
|
|
2015
|
-
shared_data["s"] += 1
|
|
2016
|
-
tracks_at_t.append(track_id)
|
|
2017
|
-
identities.append(lbl)
|
|
2018
|
-
|
|
2019
|
-
# exclude NaN
|
|
2020
|
-
tracks_at_t = np.array(tracks_at_t)
|
|
2021
|
-
identities = np.array(identities)
|
|
2022
|
-
|
|
2023
|
-
tracks_at_t = tracks_at_t[identities == identities]
|
|
2024
|
-
identities = identities[identities == identities]
|
|
2025
|
-
|
|
2026
|
-
for k in range(len(identities)):
|
|
2027
|
-
|
|
2028
|
-
# need routine to check values from labels not in class_id of this frame and add new track id
|
|
2029
|
-
|
|
2030
|
-
loc_i, loc_j = np.where(labels[f] == identities[k])
|
|
2031
|
-
track_id = tracks_at_t[k]
|
|
2032
|
-
|
|
2033
|
-
if track_id==track_id:
|
|
2034
|
-
new_labels[f, loc_i, loc_j] = round(track_id)
|
|
2035
|
-
|
|
2036
|
-
# Multithreading
|
|
2037
|
-
indices = list(df[column_labels['frame']].dropna().unique())
|
|
2038
|
-
chunks = np.array_split(indices, n_threads)
|
|
2039
|
-
|
|
2040
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
|
|
2041
|
-
|
|
2042
|
-
results = executor.map(rewrite_labels, chunks) #list(map(lambda x: executor.submit(self.parallel_job, x), chunks))
|
|
2043
|
-
try:
|
|
2044
|
-
for i,return_value in enumerate(results):
|
|
2045
|
-
print(f"Thread {i} output check: ",return_value)
|
|
2046
|
-
except Exception as e:
|
|
2047
|
-
print("Exception: ", e)
|
|
2048
|
-
|
|
2049
|
-
print("\nDone.")
|
|
2050
|
-
|
|
2051
|
-
return new_labels
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
def control_tracks(position, prefix="Aligned", population="target", relabel=True, flush_memory=True, threads=1):
|
|
2055
|
-
|
|
2056
|
-
"""
|
|
2057
|
-
Controls the tracking of cells or objects within a given position by locating the relevant image stack and label data,
|
|
2058
|
-
and then visualizing and managing the tracks in the Napari viewer.
|
|
2059
|
-
|
|
2060
|
-
Parameters
|
|
2061
|
-
----------
|
|
2062
|
-
position : str
|
|
2063
|
-
The path to the directory containing the position's data. The function will ensure the path uses forward slashes.
|
|
2064
|
-
|
|
2065
|
-
prefix : str, optional, default="Aligned"
|
|
2066
|
-
The prefix of the file names for the image stack and labels. This parameter helps locate the relevant data files.
|
|
2067
|
-
|
|
2068
|
-
population : str, optional, default="target"
|
|
2069
|
-
The population to be tracked, typically either "target" or "effectors". This is used to identify the group of interest for tracking.
|
|
2070
|
-
|
|
2071
|
-
relabel : bool, optional, default=True
|
|
2072
|
-
If True, will relabel the tracks, potentially assigning new track IDs to the detected objects.
|
|
2073
|
-
|
|
2074
|
-
flush_memory : bool, optional, default=True
|
|
2075
|
-
If True, will flush memory after processing to free up resources.
|
|
2076
|
-
|
|
2077
|
-
threads : int, optional, default=1
|
|
2078
|
-
The number of threads to use for processing. This can speed up the task in multi-threaded environments.
|
|
2079
|
-
|
|
2080
|
-
Returns
|
|
2081
|
-
-------
|
|
2082
|
-
None
|
|
2083
|
-
The function performs visualization and management of tracks in the Napari viewer. It does not return any value.
|
|
2084
|
-
|
|
2085
|
-
Notes
|
|
2086
|
-
-----
|
|
2087
|
-
- This function assumes that the necessary data for tracking (stack and labels) are located in the specified position directory.
|
|
2088
|
-
- The `locate_stack_and_labels` function is used to retrieve the image stack and labels from the specified directory.
|
|
2089
|
-
- The tracks are visualized using the `view_tracks_in_napari` function, which handles the display in the Napari viewer.
|
|
2090
|
-
- The function can be used for tracking biological entities (e.g., cells) and their movement across time frames in an image stack.
|
|
2091
|
-
|
|
2092
|
-
Example
|
|
2093
|
-
-------
|
|
2094
|
-
>>> control_tracks("/path/to/data/position_1", prefix="Aligned", population="target", relabel=True, flush_memory=True, threads=4)
|
|
2095
|
-
|
|
2096
|
-
"""
|
|
2097
|
-
|
|
2098
|
-
if not position.endswith(os.sep):
|
|
2099
|
-
position += os.sep
|
|
2100
|
-
|
|
2101
|
-
position = position.replace('\\','/')
|
|
2102
|
-
stack, labels = locate_stack_and_labels(position, prefix=prefix, population=population)
|
|
2103
|
-
|
|
2104
|
-
view_tracks_in_napari(position, population, labels=labels, stack=stack, relabel=relabel,
|
|
2105
|
-
flush_memory=flush_memory, threads=threads)
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
def tracks_to_btrack(df, exclude_nans=False):
|
|
2109
|
-
|
|
2110
|
-
"""
|
|
2111
|
-
Converts a dataframe of tracked objects into the bTrack output format.
|
|
2112
|
-
The function prepares tracking data, properties, and an empty graph structure for further processing.
|
|
2113
|
-
|
|
2114
|
-
Parameters
|
|
2115
|
-
----------
|
|
2116
|
-
df : pandas.DataFrame
|
|
2117
|
-
A dataframe containing tracking information. The dataframe must have columns for `TRACK_ID`,
|
|
2118
|
-
`FRAME`, `POSITION_Y`, `POSITION_X`, and `class_id` (among others).
|
|
2119
|
-
|
|
2120
|
-
exclude_nans : bool, optional, default=False
|
|
2121
|
-
If True, rows with NaN values in the `class_id` column will be excluded from the dataset.
|
|
2122
|
-
If False, the dataframe will retain all rows, including those with NaN in `class_id`.
|
|
2123
|
-
|
|
2124
|
-
Returns
|
|
2125
|
-
-------
|
|
2126
|
-
data : numpy.ndarray
|
|
2127
|
-
A 2D numpy array containing the tracking data with columns `[TRACK_ID, FRAME, z, POSITION_Y, POSITION_X]`.
|
|
2128
|
-
The `z` column is set to zero for all rows.
|
|
2129
|
-
|
|
2130
|
-
properties : dict
|
|
2131
|
-
A dictionary where keys are property names (e.g., 'FRAME', 'state', 'generation', etc.) and values are numpy arrays
|
|
2132
|
-
containing the corresponding values from the dataframe.
|
|
2133
|
-
|
|
2134
|
-
graph : dict
|
|
2135
|
-
An empty dictionary intended to store graph-related information for the tracking data. It can be extended
|
|
2136
|
-
later to represent relationships between different tracking objects.
|
|
2137
|
-
|
|
2138
|
-
Notes
|
|
2139
|
-
-----
|
|
2140
|
-
- The function assumes that the dataframe contains specific columns: `TRACK_ID`, `FRAME`, `POSITION_Y`, `POSITION_X`,
|
|
2141
|
-
and `class_id`. These columns are used to construct the tracking data and properties.
|
|
2142
|
-
- The `z` coordinate is set to 0 for all tracks since the function does not process 3D data.
|
|
2143
|
-
- This function is useful for transforming tracking data into a format that can be used by tracking graph algorithms.
|
|
2144
|
-
|
|
2145
|
-
Example
|
|
2146
|
-
-------
|
|
2147
|
-
>>> data, properties, graph = tracks_to_btrack(df, exclude_nans=True)
|
|
2148
|
-
|
|
2149
|
-
"""
|
|
2150
|
-
|
|
2151
|
-
graph = {}
|
|
2152
|
-
if exclude_nans:
|
|
2153
|
-
df.dropna(subset='class_id',inplace=True)
|
|
2154
|
-
df.dropna(subset='TRACK_ID',inplace=True)
|
|
2155
|
-
|
|
2156
|
-
df["z"] = 0.
|
|
2157
|
-
data = df[["TRACK_ID","FRAME","z","POSITION_Y","POSITION_X"]].to_numpy()
|
|
2158
|
-
|
|
2159
|
-
df['dummy'] = False
|
|
2160
|
-
prop_cols = ['FRAME','state','generation','root','parent','dummy','class_id']
|
|
2161
|
-
properties = {}
|
|
2162
|
-
for col in prop_cols:
|
|
2163
|
-
properties.update({col: df[col].to_numpy()})
|
|
2164
|
-
|
|
2165
|
-
return data, properties, graph
|
|
2166
|
-
|
|
2167
|
-
def tracks_to_napari(df, exclude_nans=False):
|
|
2168
|
-
|
|
2169
|
-
data, properties, graph = tracks_to_btrack(df, exclude_nans=exclude_nans)
|
|
2170
|
-
vertices = data[:, [1,-2,-1]]
|
|
2171
|
-
if data.shape[1]==4:
|
|
2172
|
-
tracks = data
|
|
2173
|
-
else:
|
|
2174
|
-
tracks = data[:,[0,1,3,4]]
|
|
2175
|
-
return vertices, tracks, properties, graph
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
def view_tracks_in_napari(position, population, stack=None, labels=None, relabel=True, flush_memory=True, threads=1):
|
|
2179
|
-
|
|
2180
|
-
"""
|
|
2181
|
-
Updated
|
|
2182
|
-
"""
|
|
2183
|
-
|
|
2184
|
-
df, df_path = get_position_table(position, population=population, return_path=True)
|
|
2185
|
-
if df is None:
|
|
2186
|
-
print('Please compute trajectories first... Abort...')
|
|
2187
|
-
return None
|
|
2188
|
-
shared_data = {"df": df, "path": df_path, "position": position, "population": population, 'selected_frame': None}
|
|
2189
|
-
|
|
2190
|
-
if (labels is not None) * relabel:
|
|
2191
|
-
print('Replacing the cell mask labels with the track ID...')
|
|
2192
|
-
labels = relabel_segmentation(labels, df, exclude_nans=True, threads=threads)
|
|
2193
|
-
|
|
2194
|
-
vertices, tracks, properties, graph = tracks_to_napari(df, exclude_nans=True)
|
|
2195
|
-
|
|
2196
|
-
viewer = napari.Viewer()
|
|
2197
|
-
if stack is not None:
|
|
2198
|
-
viewer.add_image(stack, channel_axis=-1, colormap=["gray"] * stack.shape[-1])
|
|
2199
|
-
if labels is not None:
|
|
2200
|
-
labels_layer = viewer.add_labels(labels.astype(int), name='segmentation', opacity=0.4)
|
|
2201
|
-
viewer.add_points(vertices, size=4, name='points', opacity=0.3)
|
|
2202
|
-
viewer.add_tracks(tracks, properties=properties, graph=graph, name='tracks')
|
|
2203
|
-
|
|
2204
|
-
def lock_controls(layer, widgets=(), locked=True):
|
|
2205
|
-
qctrl = viewer.window.qt_viewer.controls.widgets[layer]
|
|
2206
|
-
for wdg in widgets:
|
|
2207
|
-
try:
|
|
2208
|
-
getattr(qctrl, wdg).setEnabled(not locked)
|
|
2209
|
-
except:
|
|
2210
|
-
pass
|
|
2211
|
-
|
|
2212
|
-
label_widget_list = ['paint_button', 'erase_button', 'fill_button', 'polygon_button', 'transform_button']
|
|
2213
|
-
lock_controls(viewer.layers['segmentation'], label_widget_list)
|
|
2214
|
-
|
|
2215
|
-
point_widget_list = ['addition_button', 'delete_button', 'select_button', 'transform_button']
|
|
2216
|
-
lock_controls(viewer.layers['points'], point_widget_list)
|
|
2217
|
-
|
|
2218
|
-
track_widget_list = ['transform_button']
|
|
2219
|
-
lock_controls(viewer.layers['tracks'], track_widget_list)
|
|
2220
|
-
|
|
2221
|
-
# Initialize selected frame
|
|
2222
|
-
selected_frame = viewer.dims.current_step[0]
|
|
2223
|
-
shared_data['selected_frame'] = selected_frame
|
|
2224
|
-
|
|
2225
|
-
def export_modifications():
|
|
2226
|
-
|
|
2227
|
-
from celldetective.tracking import write_first_detection_class, clean_trajectories
|
|
2228
|
-
from celldetective.utils import velocity_per_track
|
|
2229
|
-
|
|
2230
|
-
df = shared_data['df']
|
|
2231
|
-
position = shared_data['position']
|
|
2232
|
-
population = shared_data['population']
|
|
2233
|
-
df = velocity_per_track(df, window_size=3, mode='bi')
|
|
2234
|
-
df = write_first_detection_class(df, img_shape=labels[0].shape)
|
|
2235
|
-
|
|
2236
|
-
experiment = extract_experiment_from_position(position)
|
|
2237
|
-
instruction_file = "/".join([experiment,"configs", f"tracking_instructions_{population}.json"])
|
|
2238
|
-
print(f"{instruction_file=}")
|
|
2239
|
-
if os.path.exists(instruction_file):
|
|
2240
|
-
print('Tracking configuration file found...')
|
|
2241
|
-
with open(instruction_file, 'r') as f:
|
|
2242
|
-
instructions = json.load(f)
|
|
2243
|
-
if 'post_processing_options' in instructions:
|
|
2244
|
-
post_processing_options = instructions['post_processing_options']
|
|
2245
|
-
print(f'Applying the following track postprocessing: {post_processing_options}...')
|
|
2246
|
-
df = clean_trajectories(df.copy(),**post_processing_options)
|
|
2247
|
-
unnamed_cols = [c for c in list(df.columns) if c.startswith('Unnamed')]
|
|
2248
|
-
df = df.drop(unnamed_cols, axis=1)
|
|
2249
|
-
print(f"{list(df.columns)=}")
|
|
2250
|
-
df.to_csv(shared_data['path'], index=False)
|
|
2251
|
-
print('Done...')
|
|
2252
|
-
|
|
2253
|
-
@magicgui(call_button='Export the modified\ntracks...')
|
|
2254
|
-
def export_table_widget():
|
|
2255
|
-
return export_modifications()
|
|
2256
|
-
|
|
2257
|
-
def label_changed(event):
|
|
2258
|
-
|
|
2259
|
-
value = viewer.layers['segmentation'].selected_label
|
|
2260
|
-
if value != 0:
|
|
2261
|
-
selected_frame = viewer.dims.current_step[0]
|
|
2262
|
-
shared_data['selected_frame'] = selected_frame
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
viewer.layers['segmentation'].events.selected_label.connect(label_changed)
|
|
2266
|
-
|
|
2267
|
-
viewer.window.add_dock_widget(export_table_widget, area='right')
|
|
2268
|
-
|
|
2269
|
-
@labels_layer.mouse_double_click_callbacks.append
|
|
2270
|
-
def on_second_click_of_double_click(layer, event):
|
|
2271
|
-
|
|
2272
|
-
df = shared_data['df']
|
|
2273
|
-
position = shared_data['position']
|
|
2274
|
-
population = shared_data['population']
|
|
2275
|
-
|
|
2276
|
-
frame, x, y = event.position
|
|
2277
|
-
try:
|
|
2278
|
-
value_under = viewer.layers['segmentation'].data[int(frame), int(x), int(y)] #labels[0,int(y),int(x)]
|
|
2279
|
-
if value_under==0:
|
|
2280
|
-
return None
|
|
2281
|
-
except:
|
|
2282
|
-
print('Invalid mask value...')
|
|
2283
|
-
return None
|
|
2284
|
-
|
|
2285
|
-
target_track_id = viewer.layers['segmentation'].selected_label
|
|
2286
|
-
|
|
2287
|
-
msgBox = QMessageBox()
|
|
2288
|
-
msgBox.setIcon(QMessageBox.Question)
|
|
2289
|
-
msgBox.setText(f"Do you want to propagate track {target_track_id} to the cell under the mouse, track {value_under}?")
|
|
2290
|
-
msgBox.setWindowTitle("Info")
|
|
2291
|
-
msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
|
2292
|
-
returnValue = msgBox.exec()
|
|
2293
|
-
if returnValue == QMessageBox.No:
|
|
2294
|
-
return None
|
|
2295
|
-
else:
|
|
2296
|
-
|
|
2297
|
-
if target_track_id not in df['TRACK_ID'].unique() and target_track_id in np.unique(viewer.layers['segmentation'].data[shared_data['selected_frame']]):
|
|
2298
|
-
# the selected cell in frame -1 is not in the table... we can add it to DataFrame
|
|
2299
|
-
current_labelm1 = viewer.layers['segmentation'].data[shared_data['selected_frame']]
|
|
2300
|
-
original_labelm1 = locate_labels(position, population=population, frames=shared_data['selected_frame'])
|
|
2301
|
-
original_labelm1[current_labelm1!=target_track_id] = 0
|
|
2302
|
-
props = regionprops_table(original_labelm1, intensity_image=None, properties=['centroid', 'label'])
|
|
2303
|
-
props = pd.DataFrame(props)
|
|
2304
|
-
new_cell = props[['centroid-1', 'centroid-0','label']].copy()
|
|
2305
|
-
new_cell.rename(columns={'centroid-1': 'POSITION_X', 'centroid-0': 'POSITION_Y', 'label': 'class_id'},inplace=True)
|
|
2306
|
-
new_cell['FRAME'] = shared_data['selected_frame']
|
|
2307
|
-
new_cell['TRACK_ID'] = target_track_id
|
|
2308
|
-
df = pd.concat([df, new_cell], ignore_index=True)
|
|
2309
|
-
|
|
2310
|
-
if value_under not in df['TRACK_ID'].unique():
|
|
2311
|
-
# the cell to add is not currently part of DataFrame, need to add measurement
|
|
2312
|
-
|
|
2313
|
-
current_label = viewer.layers['segmentation'].data[int(frame)]
|
|
2314
|
-
original_label = locate_labels(position, population=population, frames=int(frame))
|
|
2315
|
-
|
|
2316
|
-
new_datapoint = {'TRACK_ID': value_under, 'FRAME': frame, 'POSITION_X': np.nan, 'POSITION_Y': np.nan, 'class_id': np.nan}
|
|
2317
|
-
|
|
2318
|
-
original_label[current_label!=value_under] = 0
|
|
2319
|
-
|
|
2320
|
-
props = regionprops_table(original_label, intensity_image=None, properties=['centroid', 'label'])
|
|
2321
|
-
props = pd.DataFrame(props)
|
|
2322
|
-
|
|
2323
|
-
new_cell = props[['centroid-1', 'centroid-0','label']].copy()
|
|
2324
|
-
new_cell.rename(columns={'centroid-1': 'POSITION_X', 'centroid-0': 'POSITION_Y', 'label': 'class_id'},inplace=True)
|
|
2325
|
-
new_cell['FRAME'] = int(frame)
|
|
2326
|
-
new_cell['TRACK_ID'] = value_under
|
|
2327
|
-
df = pd.concat([df, new_cell], ignore_index=True)
|
|
2328
|
-
|
|
2329
|
-
relabel = np.amax(viewer.layers['segmentation'].data) + 1
|
|
2330
|
-
for f in viewer.layers['segmentation'].data[int(frame):]:
|
|
2331
|
-
if target_track_id!=0:
|
|
2332
|
-
f[np.where(f==target_track_id)] = relabel
|
|
2333
|
-
f[np.where(f==value_under)] = target_track_id
|
|
2334
|
-
|
|
2335
|
-
if target_track_id!=0:
|
|
2336
|
-
df.loc[(df['FRAME']>=frame)&(df['TRACK_ID']==target_track_id),'TRACK_ID'] = relabel
|
|
2337
|
-
df.loc[(df['FRAME']>=frame)&(df['TRACK_ID']==value_under),'TRACK_ID'] = target_track_id
|
|
2338
|
-
df = df.loc[~(df['TRACK_ID']==0),:]
|
|
2339
|
-
df = df.sort_values(by=['TRACK_ID','FRAME'])
|
|
2340
|
-
|
|
2341
|
-
vertices, tracks, properties, graph = tracks_to_napari(df, exclude_nans=True)
|
|
2342
|
-
|
|
2343
|
-
viewer.layers['tracks'].data = tracks
|
|
2344
|
-
viewer.layers['tracks'].properties = properties
|
|
2345
|
-
viewer.layers['tracks'].graph = graph
|
|
2346
|
-
|
|
2347
|
-
viewer.layers['points'].data = vertices
|
|
2348
|
-
|
|
2349
|
-
viewer.layers['segmentation'].refresh()
|
|
2350
|
-
viewer.layers['tracks'].refresh()
|
|
2351
|
-
viewer.layers['points'].refresh()
|
|
2352
|
-
|
|
2353
|
-
shared_data['df'] = df
|
|
2354
|
-
|
|
2355
|
-
viewer.show(block=True)
|
|
2356
|
-
|
|
2357
|
-
if flush_memory:
|
|
2358
|
-
|
|
2359
|
-
# temporary fix for slight napari memory leak
|
|
2360
|
-
for i in range(10000):
|
|
2361
|
-
try:
|
|
2362
|
-
viewer.layers.pop()
|
|
2363
|
-
except:
|
|
2364
|
-
pass
|
|
2365
|
-
|
|
2366
|
-
del viewer
|
|
2367
|
-
del stack
|
|
2368
|
-
del labels
|
|
2369
|
-
gc.collect()
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
def load_napari_data(position, prefix="Aligned", population="target", return_stack=True):
|
|
2373
|
-
|
|
2374
|
-
"""
|
|
2375
|
-
Load the necessary data for visualization in napari.
|
|
2376
|
-
|
|
2377
|
-
Parameters
|
|
2378
|
-
----------
|
|
2379
|
-
position : str
|
|
2380
|
-
The path to the position or experiment directory.
|
|
2381
|
-
prefix : str, optional
|
|
2382
|
-
The prefix used to identify the movie file. The default is "Aligned".
|
|
2383
|
-
population : str, optional
|
|
2384
|
-
The population type to load, either "target" or "effector". The default is "target".
|
|
2385
|
-
|
|
2386
|
-
Returns
|
|
2387
|
-
-------
|
|
2388
|
-
tuple
|
|
2389
|
-
A tuple containing the loaded data, properties, graph, labels, and stack.
|
|
2390
|
-
|
|
2391
|
-
Examples
|
|
2392
|
-
--------
|
|
2393
|
-
>>> data, properties, graph, labels, stack = load_napari_data("path/to/position")
|
|
2394
|
-
# Load the necessary data for visualization of target trajectories.
|
|
2395
|
-
|
|
2396
|
-
"""
|
|
2397
|
-
|
|
2398
|
-
if not position.endswith(os.sep):
|
|
2399
|
-
position += os.sep
|
|
2400
|
-
|
|
2401
|
-
position = position.replace('\\','/')
|
|
2402
|
-
if population.lower()=="target" or population.lower()=="targets":
|
|
2403
|
-
if os.path.exists(position+os.sep.join(['output','tables','napari_target_trajectories.npy'])):
|
|
2404
|
-
napari_data = np.load(position+os.sep.join(['output','tables','napari_target_trajectories.npy']), allow_pickle=True)
|
|
2405
|
-
else:
|
|
2406
|
-
napari_data = None
|
|
2407
|
-
elif population.lower()=="effector" or population.lower()=="effectors":
|
|
2408
|
-
if os.path.exists(position+os.sep.join(['output', 'tables', 'napari_effector_trajectories.npy'])):
|
|
2409
|
-
napari_data = np.load(position+os.sep.join(['output', 'tables', 'napari_effector_trajectories.npy']), allow_pickle=True)
|
|
2410
|
-
else:
|
|
2411
|
-
napari_data = None
|
|
2412
|
-
else:
|
|
2413
|
-
if os.path.exists(position+os.sep.join(['output', 'tables', f'napari_{population}_trajectories.npy'])):
|
|
2414
|
-
napari_data = np.load(position+os.sep.join(['output', 'tables', f'napari_{population}_trajectories.npy']), allow_pickle=True)
|
|
2415
|
-
else:
|
|
2416
|
-
napari_data = None
|
|
2417
|
-
|
|
2418
|
-
if napari_data is not None:
|
|
2419
|
-
data = napari_data.item()['data']
|
|
2420
|
-
properties = napari_data.item()['properties']
|
|
2421
|
-
graph = napari_data.item()['graph']
|
|
2422
|
-
else:
|
|
2423
|
-
data = None
|
|
2424
|
-
properties = None
|
|
2425
|
-
graph = None
|
|
2426
|
-
if return_stack:
|
|
2427
|
-
stack, labels = locate_stack_and_labels(position, prefix=prefix, population=population)
|
|
2428
|
-
else:
|
|
2429
|
-
labels = locate_labels(position, population=population)
|
|
2430
|
-
stack = None
|
|
2431
|
-
return data, properties, graph, labels, stack
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
def auto_correct_masks(masks, bbox_factor: float = 1.75, min_area: int = 9, fill_labels: bool = False):
|
|
2435
|
-
|
|
2436
|
-
"""
|
|
2437
|
-
Correct segmentation masks to ensure consistency and remove anomalies.
|
|
2438
|
-
|
|
2439
|
-
This function processes a labeled mask image to correct anomalies and reassign labels.
|
|
2440
|
-
It performs the following operations:
|
|
2441
|
-
|
|
2442
|
-
1. Corrects negative mask values by taking their absolute values.
|
|
2443
|
-
2. Identifies and corrects segmented objects with a bounding box area that is disproportionately
|
|
2444
|
-
larger than the actual object area. This indicates potential segmentation errors where separate objects
|
|
2445
|
-
share the same label.
|
|
2446
|
-
3. Removes small objects that are considered noise (default threshold is an area of less than 9 pixels).
|
|
2447
|
-
4. Reorders the labels so they are consecutive from 1 up to the number of remaining objects (to avoid encoding errors).
|
|
2448
|
-
|
|
2449
|
-
Parameters
|
|
2450
|
-
----------
|
|
2451
|
-
masks : np.ndarray
|
|
2452
|
-
A 2D array representing the segmented mask image with labeled regions. Each unique value
|
|
2453
|
-
in the array represents a different object or cell.
|
|
2454
|
-
bbox_factor : float, optional
|
|
2455
|
-
A factor on cell area that is compared directly to the bounding box area of the cell, to detect remote cells
|
|
2456
|
-
sharing a same label value. The default is `1.75`.
|
|
2457
|
-
min_area : int, optional
|
|
2458
|
-
Discard cells that have an area smaller than this minimum area (px²). The default is `9` (3x3 pixels).
|
|
2459
|
-
fill_labels : bool, optional
|
|
2460
|
-
Fill holes within cell masks automatically. The default is `False`.
|
|
2461
|
-
|
|
2462
|
-
Returns
|
|
2463
|
-
-------
|
|
2464
|
-
clean_labels : np.ndarray
|
|
2465
|
-
A corrected version of the input mask, with anomalies corrected, small objects removed,
|
|
2466
|
-
and labels reordered to be consecutive integers.
|
|
2467
|
-
|
|
2468
|
-
Notes
|
|
2469
|
-
-----
|
|
2470
|
-
- This function is useful for post-processing segmentation outputs to ensure high-quality
|
|
2471
|
-
object detection, particularly in applications such as cell segmentation in microscopy images.
|
|
2472
|
-
- The function assumes that the input masks contain integer labels and that the background
|
|
2473
|
-
is represented by 0.
|
|
2474
|
-
|
|
2475
|
-
Examples
|
|
2476
|
-
--------
|
|
2477
|
-
>>> masks = np.array([[0, 0, 1, 1], [0, 2, 2, 1], [0, 2, 0, 0]])
|
|
2478
|
-
>>> corrected_masks = auto_correct_masks(masks)
|
|
2479
|
-
>>> corrected_masks
|
|
2480
|
-
array([[0, 0, 1, 1],
|
|
2481
|
-
[0, 2, 2, 1],
|
|
2482
|
-
[0, 2, 0, 0]])
|
|
2483
|
-
"""
|
|
2484
|
-
|
|
2485
|
-
assert masks.ndim==2,"`masks` should be a 2D numpy array..."
|
|
2486
|
-
|
|
2487
|
-
# Avoid negative mask values
|
|
2488
|
-
masks[masks<0] = np.abs(masks[masks<0])
|
|
2489
|
-
|
|
2490
|
-
props = pd.DataFrame(regionprops_table(masks, properties=('label', 'area', 'area_bbox')))
|
|
2491
|
-
max_lbl = props['label'].max()
|
|
2492
|
-
corrected_lbl = masks.copy() #.astype(int)
|
|
2493
|
-
|
|
2494
|
-
for cell in props['label'].unique():
|
|
2495
|
-
|
|
2496
|
-
bbox_area = props.loc[props['label'] == cell, 'area_bbox'].values
|
|
2497
|
-
area = props.loc[props['label'] == cell, 'area'].values
|
|
2498
|
-
|
|
2499
|
-
if bbox_area > bbox_factor * area: # condition for anomaly
|
|
2500
|
-
|
|
2501
|
-
lbl = masks == cell
|
|
2502
|
-
lbl = lbl.astype(int)
|
|
2503
|
-
|
|
2504
|
-
relabelled = label(lbl, connectivity=2)
|
|
2505
|
-
relabelled += max_lbl
|
|
2506
|
-
relabelled[np.where(lbl == 0)] = 0
|
|
2507
|
-
|
|
2508
|
-
corrected_lbl[np.where(relabelled != 0)] = relabelled[np.where(relabelled != 0)]
|
|
2509
|
-
|
|
2510
|
-
max_lbl = np.amax(corrected_lbl)
|
|
2511
|
-
|
|
2512
|
-
# Second routine to eliminate objects too small
|
|
2513
|
-
props2 = pd.DataFrame(regionprops_table(corrected_lbl, properties=('label', 'area', 'area_bbox')))
|
|
2514
|
-
for cell in props2['label'].unique():
|
|
2515
|
-
area = props2.loc[props2['label'] == cell, 'area'].values
|
|
2516
|
-
lbl = corrected_lbl == cell
|
|
2517
|
-
if area < min_area:
|
|
2518
|
-
corrected_lbl[lbl] = 0
|
|
2519
|
-
|
|
2520
|
-
# Additionnal routine to reorder labels from 1 to number of cells
|
|
2521
|
-
label_ids = np.unique(corrected_lbl)[1:]
|
|
2522
|
-
clean_labels = corrected_lbl.copy()
|
|
2523
|
-
|
|
2524
|
-
for k,lbl in enumerate(label_ids):
|
|
2525
|
-
clean_labels[corrected_lbl==lbl] = k+1
|
|
2526
|
-
|
|
2527
|
-
clean_labels = clean_labels.astype(int)
|
|
2528
|
-
|
|
2529
|
-
if fill_labels:
|
|
2530
|
-
clean_labels = fill_label_holes(clean_labels)
|
|
2531
|
-
|
|
2532
|
-
return clean_labels
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
def control_segmentation_napari(position, prefix='Aligned', population="target", flush_memory=False):
|
|
2537
|
-
|
|
2538
|
-
"""
|
|
2539
|
-
|
|
2540
|
-
Control the visualization of segmentation labels using the napari viewer.
|
|
2541
|
-
|
|
2542
|
-
Parameters
|
|
2543
|
-
----------
|
|
2544
|
-
position : str
|
|
2545
|
-
The position or directory path where the segmentation labels and stack are located.
|
|
2546
|
-
prefix : str, optional
|
|
2547
|
-
The prefix used to identify the stack. The default is 'Aligned'.
|
|
2548
|
-
population : str, optional
|
|
2549
|
-
The population type for which the segmentation is performed. The default is 'target'.
|
|
2550
|
-
flush_memory : bool, optional
|
|
2551
|
-
Pop napari layers upon closing the viewer to empty the memory footprint. The default is `False`.
|
|
2552
|
-
|
|
2553
|
-
Notes
|
|
2554
|
-
-----
|
|
2555
|
-
This function loads the segmentation labels and stack corresponding to the specified position and population.
|
|
2556
|
-
It then creates a napari viewer and adds the stack and labels as layers for visualization.
|
|
2557
|
-
|
|
2558
|
-
Examples
|
|
2559
|
-
--------
|
|
2560
|
-
>>> control_segmentation_napari(position, prefix='Aligned', population="target")
|
|
2561
|
-
# Control the visualization of segmentation labels using the napari viewer.
|
|
2562
|
-
|
|
2563
|
-
"""
|
|
2564
|
-
|
|
2565
|
-
def export_labels():
|
|
2566
|
-
labels_layer = viewer.layers['segmentation'].data
|
|
2567
|
-
if not os.path.exists(output_folder):
|
|
2568
|
-
os.mkdir(output_folder)
|
|
2569
|
-
|
|
2570
|
-
for t, im in enumerate(tqdm(labels_layer)):
|
|
2571
|
-
|
|
2572
|
-
try:
|
|
2573
|
-
im = auto_correct_masks(im)
|
|
2574
|
-
except Exception as e:
|
|
2575
|
-
print(e)
|
|
2576
|
-
|
|
2577
|
-
save_tiff_imagej_compatible(output_folder + f"{str(t).zfill(4)}.tif", im.astype(np.int16), axes='YX')
|
|
2578
|
-
print("The labels have been successfully rewritten.")
|
|
2579
|
-
|
|
2580
|
-
def export_annotation():
|
|
2581
|
-
|
|
2582
|
-
# Locate experiment config
|
|
2583
|
-
parent1 = Path(position).parent
|
|
2584
|
-
expfolder = parent1.parent
|
|
2585
|
-
config = PurePath(expfolder, Path("config.ini"))
|
|
2586
|
-
expfolder = str(expfolder)
|
|
2587
|
-
exp_name = os.path.split(expfolder)[-1]
|
|
2588
|
-
|
|
2589
|
-
wells = get_experiment_wells(expfolder)
|
|
2590
|
-
well_idx = list(wells).index(str(parent1)+os.sep)
|
|
2591
|
-
|
|
2592
|
-
label_info = get_experiment_labels(expfolder)
|
|
2593
|
-
metadata_info = get_experiment_metadata(expfolder)
|
|
2594
|
-
|
|
2595
|
-
info = {}
|
|
2596
|
-
for k in list(label_info.keys()):
|
|
2597
|
-
values = label_info[k]
|
|
2598
|
-
try:
|
|
2599
|
-
info.update({k: values[well_idx]})
|
|
2600
|
-
except Exception as e:
|
|
2601
|
-
print(f"{e=}")
|
|
2602
|
-
|
|
2603
|
-
if metadata_info is not None:
|
|
2604
|
-
keys = list(metadata_info.keys())
|
|
2605
|
-
for k in keys:
|
|
2606
|
-
info.update({k: metadata_info[k]})
|
|
2607
|
-
|
|
2608
|
-
spatial_calibration = float(config_section_to_dict(config, "MovieSettings")["pxtoum"])
|
|
2609
|
-
channel_names, channel_indices = extract_experiment_channels(expfolder)
|
|
2610
|
-
|
|
2611
|
-
annotation_folder = expfolder + os.sep + f'annotations_{population}' + os.sep
|
|
2612
|
-
if not os.path.exists(annotation_folder):
|
|
2613
|
-
os.mkdir(annotation_folder)
|
|
2614
|
-
|
|
2615
|
-
print('Exporting!')
|
|
2616
|
-
t = viewer.dims.current_step[0]
|
|
2617
|
-
labels_layer = viewer.layers['segmentation'].data[t] # at current time
|
|
2618
|
-
|
|
2619
|
-
try:
|
|
2620
|
-
labels_layer = auto_correct_masks(labels_layer)
|
|
2621
|
-
except Exception as e:
|
|
2622
|
-
print(e)
|
|
2623
|
-
|
|
2624
|
-
fov_export = True
|
|
2625
|
-
|
|
2626
|
-
if "Shapes" in viewer.layers:
|
|
2627
|
-
squares = viewer.layers['Shapes'].data
|
|
2628
|
-
test_in_frame = np.array([squares[i][0, 0] == t and len(squares[i]) == 4 for i in range(len(squares))])
|
|
2629
|
-
squares = np.array(squares)
|
|
2630
|
-
squares = squares[test_in_frame]
|
|
2631
|
-
nbr_squares = len(squares)
|
|
2632
|
-
print(f"Found {nbr_squares} ROIs...")
|
|
2633
|
-
if nbr_squares > 0:
|
|
2634
|
-
# deactivate field of view mode
|
|
2635
|
-
fov_export = False
|
|
2636
|
-
|
|
2637
|
-
for k, sq in enumerate(squares):
|
|
2638
|
-
print(f"ROI: {sq}")
|
|
2639
|
-
pad_to_256=False
|
|
2640
|
-
|
|
2641
|
-
xmin = int(sq[0, 1])
|
|
2642
|
-
xmax = int(sq[2, 1])
|
|
2643
|
-
if xmax < xmin:
|
|
2644
|
-
xmax, xmin = xmin, xmax
|
|
2645
|
-
ymin = int(sq[0, 2])
|
|
2646
|
-
ymax = int(sq[1, 2])
|
|
2647
|
-
if ymax < ymin:
|
|
2648
|
-
ymax, ymin = ymin, ymax
|
|
2649
|
-
print(f"{xmin=};{xmax=};{ymin=};{ymax=}")
|
|
2650
|
-
frame = viewer.layers['Image'].data[t][xmin:xmax, ymin:ymax]
|
|
2651
|
-
if frame.shape[1] < 256 or frame.shape[0] < 256:
|
|
2652
|
-
pad_to_256 = True
|
|
2653
|
-
print("Crop too small! Padding with zeros to reach 256*256 pixels...")
|
|
2654
|
-
#continue
|
|
2655
|
-
multichannel = [frame]
|
|
2656
|
-
for i in range(len(channel_indices) - 1):
|
|
2657
|
-
try:
|
|
2658
|
-
frame = viewer.layers[f'Image [{i + 1}]'].data[t][xmin:xmax, ymin:ymax]
|
|
2659
|
-
multichannel.append(frame)
|
|
2660
|
-
except:
|
|
2661
|
-
pass
|
|
2662
|
-
multichannel = np.array(multichannel)
|
|
2663
|
-
lab = labels_layer[xmin:xmax,ymin:ymax].astype(np.int16)
|
|
2664
|
-
if pad_to_256:
|
|
2665
|
-
shape = multichannel.shape
|
|
2666
|
-
pad_length_x = max([0,256 - multichannel.shape[1]])
|
|
2667
|
-
if pad_length_x>0 and pad_length_x%2==1:
|
|
2668
|
-
pad_length_x += 1
|
|
2669
|
-
pad_length_y = max([0,256 - multichannel.shape[2]])
|
|
2670
|
-
if pad_length_y>0 and pad_length_y%2==1:
|
|
2671
|
-
pad_length_y += 1
|
|
2672
|
-
padded_image = np.array([np.pad(im, ((pad_length_x//2,pad_length_x//2), (pad_length_y//2,pad_length_y//2)), mode='constant') for im in multichannel])
|
|
2673
|
-
padded_label = np.pad(lab,((pad_length_x//2,pad_length_x//2), (pad_length_y//2,pad_length_y//2)), mode='constant')
|
|
2674
|
-
lab = padded_label; multichannel = padded_image;
|
|
2675
|
-
|
|
2676
|
-
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", lab, axes='YX')
|
|
2677
|
-
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')
|
|
2678
|
-
|
|
2679
|
-
info.update({"spatial_calibration": spatial_calibration, "channels": list(channel_names), 'frame': t})
|
|
2680
|
-
|
|
2681
|
-
info_name = annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_roi_{xmin}_{xmax}_{ymin}_{ymax}.json"
|
|
2682
|
-
with open(info_name, 'w') as f:
|
|
2683
|
-
json.dump(info, f, indent=4)
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
if fov_export:
|
|
2687
|
-
frame = viewer.layers['Image'].data[t]
|
|
2688
|
-
multichannel = [frame]
|
|
2689
|
-
for i in range(len(channel_indices) - 1):
|
|
2690
|
-
try:
|
|
2691
|
-
frame = viewer.layers[f'Image [{i + 1}]'].data[t]
|
|
2692
|
-
multichannel.append(frame)
|
|
2693
|
-
except:
|
|
2694
|
-
pass
|
|
2695
|
-
multichannel = np.array(multichannel)
|
|
2696
|
-
save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_labelled.tif", labels_layer, axes='YX')
|
|
2697
|
-
save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}.tif", multichannel, axes='CYX')
|
|
2698
|
-
|
|
2699
|
-
info.update({"spatial_calibration": spatial_calibration, "channels": list(channel_names), 'frame': t})
|
|
2700
|
-
|
|
2701
|
-
info_name = annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}.json"
|
|
2702
|
-
with open(info_name, 'w') as f:
|
|
2703
|
-
json.dump(info, f, indent=4)
|
|
2704
|
-
|
|
2705
|
-
print('Done.')
|
|
2706
|
-
|
|
2707
|
-
@magicgui(call_button='Save the modified labels')
|
|
2708
|
-
def save_widget():
|
|
2709
|
-
return export_labels()
|
|
2710
|
-
|
|
2711
|
-
@magicgui(call_button='Export the annotation\nof the current frame')
|
|
2712
|
-
def export_widget():
|
|
2713
|
-
return export_annotation()
|
|
2714
|
-
|
|
2715
|
-
stack, labels = locate_stack_and_labels(position, prefix=prefix, population=population)
|
|
2716
|
-
output_folder = position + f'labels_{population}{os.sep}'
|
|
2717
|
-
print(f"Shape of the loaded image stack: {stack.shape}...")
|
|
2718
|
-
|
|
2719
|
-
viewer = napari.Viewer()
|
|
2720
|
-
viewer.add_image(stack, channel_axis=-1, colormap=["gray"] * stack.shape[-1])
|
|
2721
|
-
viewer.add_labels(labels.astype(int), name='segmentation', opacity=0.4)
|
|
2722
|
-
viewer.window.add_dock_widget(save_widget, area='right')
|
|
2723
|
-
viewer.window.add_dock_widget(export_widget, area='right')
|
|
2724
|
-
|
|
2725
|
-
def lock_controls(layer, widgets=(), locked=True):
|
|
2726
|
-
qctrl = viewer.window.qt_viewer.controls.widgets[layer]
|
|
2727
|
-
for wdg in widgets:
|
|
2728
|
-
try:
|
|
2729
|
-
getattr(qctrl, wdg).setEnabled(not locked)
|
|
2730
|
-
except:
|
|
2731
|
-
pass
|
|
2732
|
-
|
|
2733
|
-
label_widget_list = ['polygon_button', 'transform_button']
|
|
2734
|
-
lock_controls(viewer.layers['segmentation'], label_widget_list)
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
viewer.show(block=True)
|
|
2738
|
-
|
|
2739
|
-
if flush_memory:
|
|
2740
|
-
# temporary fix for slight napari memory leak
|
|
2741
|
-
for i in range(10000):
|
|
2742
|
-
try:
|
|
2743
|
-
viewer.layers.pop()
|
|
2744
|
-
except:
|
|
2745
|
-
pass
|
|
2746
|
-
|
|
2747
|
-
del viewer
|
|
2748
|
-
del stack
|
|
2749
|
-
del labels
|
|
2750
|
-
gc.collect()
|
|
2751
|
-
|
|
2752
|
-
print("napari viewer was successfully closed...")
|
|
2753
|
-
|
|
2754
|
-
def correct_annotation(filename):
|
|
2755
|
-
|
|
2756
|
-
"""
|
|
2757
|
-
New function to reannotate an annotation image in post, using napari and save update inplace.
|
|
2758
|
-
"""
|
|
2759
|
-
|
|
2760
|
-
def export_labels():
|
|
2761
|
-
labels_layer = viewer.layers['segmentation'].data
|
|
2762
|
-
for t,im in enumerate(tqdm(labels_layer)):
|
|
2763
|
-
|
|
2764
|
-
try:
|
|
2765
|
-
im = auto_correct_masks(im)
|
|
2766
|
-
except Exception as e:
|
|
2767
|
-
print(e)
|
|
2768
|
-
|
|
2769
|
-
save_tiff_imagej_compatible(existing_lbl, im.astype(np.int16), axes='YX')
|
|
2770
|
-
print("The labels have been successfully rewritten.")
|
|
2771
|
-
|
|
2772
|
-
@magicgui(call_button='Save the modified labels')
|
|
2773
|
-
def save_widget():
|
|
2774
|
-
return export_labels()
|
|
2775
|
-
|
|
2776
|
-
if filename.endswith("_labelled.tif"):
|
|
2777
|
-
filename = filename.replace("_labelled.tif",".tif")
|
|
2778
|
-
if filename.endswith(".json"):
|
|
2779
|
-
filename = filename.replace('.json',".tif")
|
|
2780
|
-
assert os.path.exists(filename),f"Image {filename} does not seem to exist..."
|
|
2781
|
-
|
|
2782
|
-
img = imread(filename.replace('\\','/'))
|
|
2783
|
-
if img.ndim==3:
|
|
2784
|
-
img = np.moveaxis(img, 0, -1)
|
|
2785
|
-
elif img.ndim==2:
|
|
2786
|
-
img = img[:,:,np.newaxis]
|
|
2787
|
-
|
|
2788
|
-
existing_lbl = filename.replace('.tif','_labelled.tif')
|
|
2789
|
-
if os.path.exists(existing_lbl):
|
|
2790
|
-
labels = imread(existing_lbl)[np.newaxis,:,:].astype(int)
|
|
2791
|
-
else:
|
|
2792
|
-
labels = np.zeros_like(img[:,:,0]).astype(int)[np.newaxis,:,:]
|
|
2793
|
-
|
|
2794
|
-
stack = img[np.newaxis,:,:,:]
|
|
2795
|
-
|
|
2796
|
-
viewer = napari.Viewer()
|
|
2797
|
-
viewer.add_image(stack,channel_axis=-1,colormap=["gray"]*stack.shape[-1])
|
|
2798
|
-
viewer.add_labels(labels, name='segmentation',opacity=0.4)
|
|
2799
|
-
viewer.window.add_dock_widget(save_widget, area='right')
|
|
2800
|
-
viewer.show(block=True)
|
|
2801
|
-
|
|
2802
|
-
# temporary fix for slight napari memory leak
|
|
2803
|
-
for i in range(100):
|
|
2804
|
-
try:
|
|
2805
|
-
viewer.layers.pop()
|
|
2806
|
-
except:
|
|
2807
|
-
pass
|
|
2808
|
-
del viewer
|
|
2809
|
-
del stack
|
|
2810
|
-
del labels
|
|
2811
|
-
gc.collect()
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
def _view_on_napari(tracks=None, stack=None, labels=None):
|
|
2815
|
-
|
|
2816
|
-
"""
|
|
2817
|
-
|
|
2818
|
-
Visualize tracks, stack, and labels using Napari.
|
|
2819
|
-
|
|
2820
|
-
Parameters
|
|
2821
|
-
----------
|
|
2822
|
-
tracks : pandas DataFrame
|
|
2823
|
-
DataFrame containing track information.
|
|
2824
|
-
stack : numpy array, optional
|
|
2825
|
-
Stack of images with shape (T, Y, X, C), where T is the number of frames, Y and X are the spatial dimensions,
|
|
2826
|
-
and C is the number of channels. Default is None.
|
|
2827
|
-
labels : numpy array, optional
|
|
2828
|
-
Label stack with shape (T, Y, X) representing cell segmentations. Default is None.
|
|
2829
|
-
|
|
2830
|
-
Returns
|
|
2831
|
-
-------
|
|
2832
|
-
None
|
|
2833
|
-
|
|
2834
|
-
Notes
|
|
2835
|
-
-----
|
|
2836
|
-
This function visualizes tracks, stack, and labels using Napari, an interactive multi-dimensional image viewer.
|
|
2837
|
-
The tracks are represented as line segments on the viewer. If a stack is provided, it is displayed as an image.
|
|
2838
|
-
If labels are provided, they are displayed as a segmentation overlay on the stack.
|
|
2839
|
-
|
|
2840
|
-
Examples
|
|
2841
|
-
--------
|
|
2842
|
-
>>> tracks = pd.DataFrame({'track': [1, 2, 3], 'time': [1, 1, 1],
|
|
2843
|
-
... 'x': [10, 20, 30], 'y': [15, 25, 35]})
|
|
2844
|
-
>>> stack = np.random.rand(100, 100, 3)
|
|
2845
|
-
>>> labels = np.random.randint(0, 2, (100, 100))
|
|
2846
|
-
>>> _view_on_napari(tracks, stack=stack, labels=labels)
|
|
2847
|
-
# Visualize tracks, stack, and labels using Napari.
|
|
2848
|
-
|
|
2849
|
-
"""
|
|
2850
|
-
|
|
2851
|
-
viewer = napari.Viewer()
|
|
2852
|
-
if stack is not None:
|
|
2853
|
-
viewer.add_image(stack, channel_axis=-1, colormap=["gray"] * stack.shape[-1])
|
|
2854
|
-
if labels is not None:
|
|
2855
|
-
viewer.add_labels(labels, name='segmentation', opacity=0.4)
|
|
2856
|
-
if tracks is not None:
|
|
2857
|
-
viewer.add_tracks(tracks, name='tracks')
|
|
2858
|
-
viewer.show(block=True)
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
def control_tracking_table(position, calibration=1, prefix="Aligned", population="target",
|
|
2862
|
-
column_labels={'track': "TRACK_ID", 'frame': 'FRAME', 'y': 'POSITION_Y', 'x': 'POSITION_X',
|
|
2863
|
-
'label': 'class_id'}):
|
|
2864
|
-
"""
|
|
2865
|
-
|
|
2866
|
-
Control the tracking table and visualize tracks using Napari.
|
|
2867
|
-
|
|
2868
|
-
Parameters
|
|
2869
|
-
----------
|
|
2870
|
-
position : str
|
|
2871
|
-
The position or directory of the tracking data.
|
|
2872
|
-
calibration : float, optional
|
|
2873
|
-
Calibration factor for converting pixel coordinates to physical units. Default is 1.
|
|
2874
|
-
prefix : str, optional
|
|
2875
|
-
Prefix used for the tracking data file. Default is "Aligned".
|
|
2876
|
-
population : str, optional
|
|
2877
|
-
Population type, either "target" or "effector". Default is "target".
|
|
2878
|
-
column_labels : dict, optional
|
|
2879
|
-
Dictionary containing the column labels for the tracking table. Default is
|
|
2880
|
-
{'track': "TRACK_ID", 'frame': 'FRAME', 'y': 'POSITION_Y', 'x': 'POSITION_X', 'label': 'class_id'}.
|
|
2881
|
-
|
|
2882
|
-
Returns
|
|
2883
|
-
-------
|
|
2884
|
-
None
|
|
2885
|
-
|
|
2886
|
-
Notes
|
|
2887
|
-
-----
|
|
2888
|
-
This function loads the tracking data, applies calibration to the spatial coordinates, and visualizes the tracks
|
|
2889
|
-
using Napari. The tracking data is loaded from the specified `position` directory with the given `prefix` and
|
|
2890
|
-
`population`. The spatial coordinates (x, y) in the tracking table are divided by the `calibration` factor to
|
|
2891
|
-
convert them from pixel units to the specified physical units. The tracks are then visualized using Napari.
|
|
2892
|
-
|
|
2893
|
-
Examples
|
|
2894
|
-
--------
|
|
2895
|
-
>>> control_tracking_table('path/to/tracking_data', calibration=0.1, prefix='Aligned', population='target')
|
|
2896
|
-
# Control the tracking table and visualize tracks using Napari.
|
|
2897
|
-
|
|
2898
|
-
"""
|
|
2899
|
-
|
|
2900
|
-
position = position.replace('\\', '/')
|
|
2901
|
-
tracks, labels, stack = load_tracking_data(position, prefix=prefix, population=population)
|
|
2902
|
-
tracks = tracks.loc[:,
|
|
2903
|
-
[column_labels['track'], column_labels['frame'], column_labels['y'], column_labels['x']]].to_numpy()
|
|
2904
|
-
tracks[:, -2:] /= calibration
|
|
2905
|
-
_view_on_napari(tracks, labels=labels, stack=stack)
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
def get_segmentation_models_list(mode='targets', return_path=False):
|
|
2909
|
-
|
|
2910
|
-
modelpath = os.sep.join(
|
|
2911
|
-
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models",
|
|
2912
|
-
f"segmentation_{mode}", os.sep])
|
|
2913
|
-
if not os.path.exists(modelpath):
|
|
2914
|
-
os.mkdir(modelpath)
|
|
2915
|
-
repository_models = []
|
|
2916
|
-
else:
|
|
2917
|
-
repository_models = get_zenodo_files(cat=os.sep.join(["models", f"segmentation_{mode}"]))
|
|
2918
|
-
|
|
2919
|
-
available_models = natsorted(glob(modelpath + '*/'))
|
|
2920
|
-
available_models = [m.replace('\\', '/').split('/')[-2] for m in available_models]
|
|
2921
|
-
|
|
2922
|
-
# Auto model cleanup
|
|
2923
|
-
to_remove = []
|
|
2924
|
-
for model in available_models:
|
|
2925
|
-
path = modelpath + model
|
|
2926
|
-
files = glob(path+os.sep+"*")
|
|
2927
|
-
if path+os.sep+"config_input.json" not in files:
|
|
2928
|
-
rmtree(path)
|
|
2929
|
-
to_remove.append(model)
|
|
2930
|
-
for m in to_remove:
|
|
2931
|
-
available_models.remove(m)
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
for rm in repository_models:
|
|
2935
|
-
if rm not in available_models:
|
|
2936
|
-
available_models.append(rm)
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
if not return_path:
|
|
2940
|
-
return available_models
|
|
2941
|
-
else:
|
|
2942
|
-
return available_models, modelpath
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
def locate_segmentation_model(name, download=True):
|
|
2946
|
-
|
|
2947
|
-
"""
|
|
2948
|
-
Locates a specified segmentation model within the local 'celldetective' directory or
|
|
2949
|
-
downloads it from Zenodo if not found locally.
|
|
2950
|
-
|
|
2951
|
-
This function attempts to find a segmentation model by name within a predefined directory
|
|
2952
|
-
structure starting from the 'celldetective/models/segmentation*' path. If the model is not
|
|
2953
|
-
found locally, it then tries to locate and download the model from Zenodo, placing it into
|
|
2954
|
-
the appropriate category directory within 'celldetective'. The function prints the search
|
|
2955
|
-
directory path and returns the path to the found or downloaded model.
|
|
2956
|
-
|
|
2957
|
-
Parameters
|
|
2958
|
-
----------
|
|
2959
|
-
name : str
|
|
2960
|
-
The name of the segmentation model to locate.
|
|
2961
|
-
|
|
2962
|
-
Returns
|
|
2963
|
-
-------
|
|
2964
|
-
str or None
|
|
2965
|
-
The full path to the located or downloaded segmentation model directory, or None if the
|
|
2966
|
-
model could not be found or downloaded.
|
|
2967
|
-
|
|
2968
|
-
Raises
|
|
2969
|
-
------
|
|
2970
|
-
FileNotFoundError
|
|
2971
|
-
If the model cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
2972
|
-
|
|
2973
|
-
"""
|
|
2974
|
-
|
|
2975
|
-
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
|
|
2976
|
-
modelpath = os.sep.join([main_dir, "models", "segmentation*"]) + os.sep
|
|
2977
|
-
#print(f'Looking for {name} in {modelpath}')
|
|
2978
|
-
models = glob(modelpath + f'*{os.sep}')
|
|
2979
|
-
|
|
2980
|
-
match = None
|
|
2981
|
-
for m in models:
|
|
2982
|
-
if name == m.replace('\\', os.sep).split(os.sep)[-2]:
|
|
2983
|
-
match = m
|
|
2984
|
-
return match
|
|
2985
|
-
if download:
|
|
2986
|
-
# else no match, try zenodo
|
|
2987
|
-
files, categories = get_zenodo_files()
|
|
2988
|
-
if name in files:
|
|
2989
|
-
index = files.index(name)
|
|
2990
|
-
cat = categories[index]
|
|
2991
|
-
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
2992
|
-
match = os.sep.join([main_dir, cat, name]) + os.sep
|
|
2993
|
-
|
|
2994
|
-
return match
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
def get_segmentation_datasets_list(return_path=False):
|
|
2998
|
-
"""
|
|
2999
|
-
Retrieves a list of available segmentation datasets from both the local 'celldetective/datasets/segmentation_annotations'
|
|
3000
|
-
directory and a Zenodo repository, optionally returning the path to the local datasets directory.
|
|
3001
|
-
|
|
3002
|
-
This function compiles a list of available segmentation datasets by first identifying datasets stored locally
|
|
3003
|
-
within a specified path related to the script's directory. It then extends this list with datasets available
|
|
3004
|
-
in a Zenodo repository, ensuring no duplicates are added. The function can return just the list of dataset
|
|
3005
|
-
names or, if specified, also return the path to the local datasets directory.
|
|
3006
|
-
|
|
3007
|
-
Parameters
|
|
3008
|
-
----------
|
|
3009
|
-
return_path : bool, optional
|
|
3010
|
-
If True, the function returns a tuple containing the list of available dataset names and the path to the
|
|
3011
|
-
local datasets directory. If False, only the list of dataset names is returned (default is False).
|
|
3012
|
-
|
|
3013
|
-
Returns
|
|
3014
|
-
-------
|
|
3015
|
-
list or (list, str)
|
|
3016
|
-
If return_path is False, returns a list of strings, each string being the name of an available dataset.
|
|
3017
|
-
If return_path is True, returns a tuple where the first element is this list and the second element is a
|
|
3018
|
-
string representing the path to the local datasets directory.
|
|
3019
|
-
|
|
3020
|
-
"""
|
|
3021
|
-
|
|
3022
|
-
datasets_path = os.sep.join(
|
|
3023
|
-
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "datasets",
|
|
3024
|
-
"segmentation_annotations", os.sep])
|
|
3025
|
-
repository_datasets = get_zenodo_files(cat=os.sep.join(["datasets", "segmentation_annotations"]))
|
|
3026
|
-
|
|
3027
|
-
available_datasets = natsorted(glob(datasets_path + '*/'))
|
|
3028
|
-
available_datasets = [m.replace('\\', '/').split('/')[-2] for m in available_datasets]
|
|
3029
|
-
for rm in repository_datasets:
|
|
3030
|
-
if rm not in available_datasets:
|
|
3031
|
-
available_datasets.append(rm)
|
|
3032
|
-
|
|
3033
|
-
if not return_path:
|
|
3034
|
-
return available_datasets
|
|
3035
|
-
else:
|
|
3036
|
-
return available_datasets, datasets_path
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
def locate_segmentation_dataset(name):
|
|
3041
|
-
|
|
3042
|
-
"""
|
|
3043
|
-
Locates a specified segmentation dataset within the local 'celldetective/datasets/segmentation_annotations' directory
|
|
3044
|
-
or downloads it from Zenodo if not found locally.
|
|
3045
|
-
|
|
3046
|
-
This function attempts to find a segmentation dataset by name within a predefined directory structure. If the dataset
|
|
3047
|
-
is not found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate
|
|
3048
|
-
category directory within 'celldetective'. The function prints the search directory path and returns the path to the
|
|
3049
|
-
found or downloaded dataset.
|
|
3050
|
-
|
|
3051
|
-
Parameters
|
|
3052
|
-
----------
|
|
3053
|
-
name : str
|
|
3054
|
-
The name of the segmentation dataset to locate.
|
|
3055
|
-
|
|
3056
|
-
Returns
|
|
3057
|
-
-------
|
|
3058
|
-
str or None
|
|
3059
|
-
The full path to the located or downloaded segmentation dataset directory, or None if the dataset could not be
|
|
3060
|
-
found or downloaded.
|
|
3061
|
-
|
|
3062
|
-
Raises
|
|
3063
|
-
------
|
|
3064
|
-
FileNotFoundError
|
|
3065
|
-
If the dataset cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
3066
|
-
|
|
3067
|
-
"""
|
|
3068
|
-
|
|
3069
|
-
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
|
|
3070
|
-
modelpath = os.sep.join([main_dir, "datasets", "segmentation_annotations", os.sep])
|
|
3071
|
-
print(f'Looking for {name} in {modelpath}')
|
|
3072
|
-
models = glob(modelpath + f'*{os.sep}')
|
|
3073
|
-
|
|
3074
|
-
match = None
|
|
3075
|
-
for m in models:
|
|
3076
|
-
if name == m.replace('\\', os.sep).split(os.sep)[-2]:
|
|
3077
|
-
match = m
|
|
3078
|
-
return match
|
|
3079
|
-
# else no match, try zenodo
|
|
3080
|
-
files, categories = get_zenodo_files()
|
|
3081
|
-
if name in files:
|
|
3082
|
-
index = files.index(name)
|
|
3083
|
-
cat = categories[index]
|
|
3084
|
-
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
3085
|
-
match = os.sep.join([main_dir, cat, name]) + os.sep
|
|
3086
|
-
return match
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
def get_signal_datasets_list(return_path=False):
|
|
3090
|
-
|
|
3091
|
-
"""
|
|
3092
|
-
Retrieves a list of available signal datasets from both the local 'celldetective/datasets/signal_annotations' directory
|
|
3093
|
-
and a Zenodo repository, optionally returning the path to the local datasets directory.
|
|
3094
|
-
|
|
3095
|
-
This function compiles a list of available signal datasets by first identifying datasets stored locally within a specified
|
|
3096
|
-
path related to the script's directory. It then extends this list with datasets available in a Zenodo repository, ensuring
|
|
3097
|
-
no duplicates are added. The function can return just the list of dataset names or, if specified, also return the path to
|
|
3098
|
-
the local datasets directory.
|
|
3099
|
-
|
|
3100
|
-
Parameters
|
|
3101
|
-
----------
|
|
3102
|
-
return_path : bool, optional
|
|
3103
|
-
If True, the function returns a tuple containing the list of available dataset names and the path to the local datasets
|
|
3104
|
-
directory. If False, only the list of dataset names is returned (default is False).
|
|
3105
|
-
|
|
3106
|
-
Returns
|
|
3107
|
-
-------
|
|
3108
|
-
list or (list, str)
|
|
3109
|
-
If return_path is False, returns a list of strings, each string being the name of an available dataset. If return_path
|
|
3110
|
-
is True, returns a tuple where the first element is this list and the second element is a string representing the path
|
|
3111
|
-
to the local datasets directory.
|
|
3112
|
-
|
|
3113
|
-
"""
|
|
3114
|
-
|
|
3115
|
-
datasets_path = os.sep.join(
|
|
3116
|
-
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "datasets",
|
|
3117
|
-
"signal_annotations", os.sep])
|
|
3118
|
-
repository_datasets = get_zenodo_files(cat=os.sep.join(["datasets", "signal_annotations"]))
|
|
3119
|
-
|
|
3120
|
-
available_datasets = natsorted(glob(datasets_path + '*/'))
|
|
3121
|
-
available_datasets = [m.replace('\\', '/').split('/')[-2] for m in available_datasets]
|
|
3122
|
-
for rm in repository_datasets:
|
|
3123
|
-
if rm not in available_datasets:
|
|
3124
|
-
available_datasets.append(rm)
|
|
3125
|
-
|
|
3126
|
-
if not return_path:
|
|
3127
|
-
return available_datasets
|
|
3128
|
-
else:
|
|
3129
|
-
return available_datasets, datasets_path
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
def locate_signal_dataset(name):
|
|
3133
|
-
|
|
3134
|
-
"""
|
|
3135
|
-
Locates a specified signal dataset within the local 'celldetective/datasets/signal_annotations' directory or downloads
|
|
3136
|
-
it from Zenodo if not found locally.
|
|
3137
|
-
|
|
3138
|
-
This function attempts to find a signal dataset by name within a predefined directory structure. If the dataset is not
|
|
3139
|
-
found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate category
|
|
3140
|
-
directory within 'celldetective'. The function prints the search directory path and returns the path to the found or
|
|
3141
|
-
downloaded dataset.
|
|
3142
|
-
|
|
3143
|
-
Parameters
|
|
3144
|
-
----------
|
|
3145
|
-
name : str
|
|
3146
|
-
The name of the signal dataset to locate.
|
|
3147
|
-
|
|
3148
|
-
Returns
|
|
3149
|
-
-------
|
|
3150
|
-
str or None
|
|
3151
|
-
The full path to the located or downloaded signal dataset directory, or None if the dataset could not be found or
|
|
3152
|
-
downloaded.
|
|
3153
|
-
|
|
3154
|
-
Raises
|
|
3155
|
-
------
|
|
3156
|
-
FileNotFoundError
|
|
3157
|
-
If the dataset cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
3158
|
-
|
|
3159
|
-
"""
|
|
3160
|
-
|
|
3161
|
-
main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
|
|
3162
|
-
modelpath = os.sep.join([main_dir, "datasets", "signal_annotations", os.sep])
|
|
3163
|
-
print(f'Looking for {name} in {modelpath}')
|
|
3164
|
-
models = glob(modelpath + f'*{os.sep}')
|
|
3165
|
-
|
|
3166
|
-
match = None
|
|
3167
|
-
for m in models:
|
|
3168
|
-
if name == m.replace('\\', os.sep).split(os.sep)[-2]:
|
|
3169
|
-
match = m
|
|
3170
|
-
return match
|
|
3171
|
-
# else no match, try zenodo
|
|
3172
|
-
files, categories = get_zenodo_files()
|
|
3173
|
-
if name in files:
|
|
3174
|
-
index = files.index(name)
|
|
3175
|
-
cat = categories[index]
|
|
3176
|
-
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
3177
|
-
match = os.sep.join([main_dir, cat, name]) + os.sep
|
|
3178
|
-
return match
|
|
3179
|
-
|
|
3180
|
-
def normalize(frame, percentiles=(0.0,99.99), values=None, ignore_gray_value=0., clip=False, amplification=None, dtype=float):
|
|
3181
|
-
|
|
3182
|
-
"""
|
|
3183
|
-
|
|
3184
|
-
Normalize the intensity values of a frame.
|
|
3185
|
-
|
|
3186
|
-
Parameters
|
|
3187
|
-
----------
|
|
3188
|
-
frame : ndarray
|
|
3189
|
-
The input frame to be normalized.
|
|
3190
|
-
percentiles : tuple, optional
|
|
3191
|
-
The percentiles used to determine the minimum and maximum values for normalization. Default is (0.0, 99.99).
|
|
3192
|
-
values : tuple or None, optional
|
|
3193
|
-
The specific minimum and maximum values to use for normalization. If None, percentiles are used. Default is None.
|
|
3194
|
-
ignore_gray_value : float or None, optional
|
|
3195
|
-
The gray value to ignore during normalization. If specified, the pixels with this value will not be normalized. Default is 0.0.
|
|
3196
|
-
|
|
3197
|
-
Returns
|
|
3198
|
-
-------
|
|
3199
|
-
ndarray
|
|
3200
|
-
The normalized frame.
|
|
3201
|
-
|
|
3202
|
-
Notes
|
|
3203
|
-
-----
|
|
3204
|
-
This function performs intensity normalization on a frame. It computes the minimum and maximum values for normalization either
|
|
3205
|
-
using the specified values or by calculating percentiles from the frame. The frame is then normalized between the minimum and
|
|
3206
|
-
maximum values using the `normalize_mi_ma` function. If `ignore_gray_value` is specified, the pixels with this value will be
|
|
3207
|
-
left unmodified during normalization.
|
|
3208
|
-
|
|
3209
|
-
Examples
|
|
3210
|
-
--------
|
|
3211
|
-
>>> frame = np.array([[10, 20, 30],
|
|
3212
|
-
[40, 50, 60],
|
|
3213
|
-
[70, 80, 90]])
|
|
3214
|
-
>>> normalized = normalize(frame)
|
|
3215
|
-
>>> normalized
|
|
3216
|
-
|
|
3217
|
-
array([[0. , 0.2, 0.4],
|
|
3218
|
-
[0.6, 0.8, 1. ],
|
|
3219
|
-
[1.2, 1.4, 1.6]], dtype=float32)
|
|
3220
|
-
|
|
3221
|
-
>>> normalized = normalize(frame, percentiles=(10.0, 90.0))
|
|
3222
|
-
>>> normalized
|
|
3223
|
-
|
|
3224
|
-
array([[0.33333334, 0.44444445, 0.5555556 ],
|
|
3225
|
-
[0.6666667 , 0.7777778 , 0.8888889 ],
|
|
3226
|
-
[1. , 1.1111112 , 1.2222222 ]], dtype=float32)
|
|
3227
|
-
|
|
3228
|
-
"""
|
|
3229
|
-
|
|
3230
|
-
frame = frame.astype(float)
|
|
3231
|
-
|
|
3232
|
-
if ignore_gray_value is not None:
|
|
3233
|
-
subframe = frame[frame != ignore_gray_value]
|
|
3234
|
-
else:
|
|
3235
|
-
subframe = frame.copy()
|
|
3236
|
-
|
|
3237
|
-
if values is not None:
|
|
3238
|
-
mi = values[0]
|
|
3239
|
-
ma = values[1]
|
|
3240
|
-
else:
|
|
3241
|
-
mi = np.nanpercentile(subframe.flatten(), percentiles[0], keepdims=True)
|
|
3242
|
-
ma = np.nanpercentile(subframe.flatten(), percentiles[1], keepdims=True)
|
|
3243
|
-
|
|
3244
|
-
frame0 = frame.copy()
|
|
3245
|
-
frame = normalize_mi_ma(frame0, mi, ma, clip=False, eps=1e-20, dtype=np.float32)
|
|
3246
|
-
if amplification is not None:
|
|
3247
|
-
frame *= amplification
|
|
3248
|
-
if clip:
|
|
3249
|
-
if amplification is None:
|
|
3250
|
-
amplification = 1.
|
|
3251
|
-
frame[frame >= amplification] = amplification
|
|
3252
|
-
frame[frame <= 0.] = 0.
|
|
3253
|
-
if ignore_gray_value is not None:
|
|
3254
|
-
frame[np.where(frame0) == ignore_gray_value] = ignore_gray_value
|
|
3255
|
-
|
|
3256
|
-
return frame.copy().astype(dtype)
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
def normalize_multichannel(multichannel_frame: np.ndarray,
|
|
3260
|
-
percentiles=None,
|
|
3261
|
-
values=None,
|
|
3262
|
-
ignore_gray_value=0.,
|
|
3263
|
-
clip=False,
|
|
3264
|
-
amplification=None,
|
|
3265
|
-
dtype=float,
|
|
3266
|
-
):
|
|
3267
|
-
|
|
3268
|
-
"""
|
|
3269
|
-
Normalizes a multichannel frame by adjusting the intensity values of each channel based on specified percentiles,
|
|
3270
|
-
direct value ranges, or amplification factors, with options to ignore a specific gray value and to clip the output.
|
|
3271
|
-
|
|
3272
|
-
Parameters
|
|
3273
|
-
----------
|
|
3274
|
-
multichannel_frame : ndarray
|
|
3275
|
-
The input multichannel image frame to be normalized, expected to be a 3-dimensional array where the last dimension
|
|
3276
|
-
represents the channels.
|
|
3277
|
-
percentiles : list of tuples or tuple, optional
|
|
3278
|
-
Percentile ranges (low, high) for each channel used to scale the intensity values. If a single tuple is provided,
|
|
3279
|
-
it is applied to all channels. If None, the default percentile range of (0., 99.99) is used for each channel.
|
|
3280
|
-
values : list of tuples or tuple, optional
|
|
3281
|
-
Direct value ranges (min, max) for each channel to scale the intensity values. If a single tuple is provided, it
|
|
3282
|
-
is applied to all channels. This parameter overrides `percentiles` if provided.
|
|
3283
|
-
ignore_gray_value : float, optional
|
|
3284
|
-
A specific gray value to ignore during normalization (default is 0.).
|
|
3285
|
-
clip : bool, optional
|
|
3286
|
-
If True, clips the output values to the range [0, 1] or the specified `dtype` range if `dtype` is not float
|
|
3287
|
-
(default is False).
|
|
3288
|
-
amplification : float, optional
|
|
3289
|
-
A factor by which to amplify the intensity values after normalization. If None, no amplification is applied.
|
|
3290
|
-
dtype : data-type, optional
|
|
3291
|
-
The desired data-type for the output normalized frame. The default is float, but other types can be specified
|
|
3292
|
-
to change the range of the output values.
|
|
3293
|
-
|
|
3294
|
-
Returns
|
|
3295
|
-
-------
|
|
3296
|
-
ndarray
|
|
3297
|
-
The normalized multichannel frame as a 3-dimensional array of the same shape as `multichannel_frame`.
|
|
3298
|
-
|
|
3299
|
-
Raises
|
|
3300
|
-
------
|
|
3301
|
-
AssertionError
|
|
3302
|
-
If the input `multichannel_frame` does not have 3 dimensions, or if the length of `values` does not match the
|
|
3303
|
-
number of channels in `multichannel_frame`.
|
|
3304
|
-
|
|
3305
|
-
Notes
|
|
3306
|
-
-----
|
|
3307
|
-
- This function provides flexibility in normalization by allowing the use of percentile ranges, direct value ranges,
|
|
3308
|
-
or amplification factors.
|
|
3309
|
-
- The function makes a copy of the input frame to avoid altering the original data.
|
|
3310
|
-
- When both `percentiles` and `values` are provided, `values` takes precedence for normalization.
|
|
3311
|
-
|
|
3312
|
-
Examples
|
|
3313
|
-
--------
|
|
3314
|
-
>>> multichannel_frame = np.random.rand(100, 100, 3) # Example multichannel frame
|
|
3315
|
-
>>> normalized_frame = normalize_multichannel(multichannel_frame, percentiles=[(1, 99), (2, 98), (0, 100)])
|
|
3316
|
-
# Normalizes each channel of the frame using specified percentile ranges.
|
|
3317
|
-
|
|
3318
|
-
"""
|
|
3319
|
-
|
|
3320
|
-
mf = multichannel_frame.copy().astype(float)
|
|
3321
|
-
assert mf.ndim == 3, f'Wrong shape for the multichannel frame: {mf.shape}.'
|
|
3322
|
-
if percentiles is None:
|
|
3323
|
-
percentiles = [(0., 99.99)] * mf.shape[-1]
|
|
3324
|
-
elif isinstance(percentiles, tuple):
|
|
3325
|
-
percentiles = [percentiles] * mf.shape[-1]
|
|
3326
|
-
if values is not None:
|
|
3327
|
-
if isinstance(values, tuple):
|
|
3328
|
-
values = [values] * mf.shape[-1]
|
|
3329
|
-
assert len(values) == mf.shape[
|
|
3330
|
-
-1], 'Mismatch between the normalization values provided and the number of channels.'
|
|
3331
|
-
|
|
3332
|
-
mf_new = []
|
|
3333
|
-
for c in range(mf.shape[-1]):
|
|
3334
|
-
if values is not None:
|
|
3335
|
-
v = values[c]
|
|
3336
|
-
else:
|
|
3337
|
-
v = None
|
|
3338
|
-
|
|
3339
|
-
if np.all(mf[:,:,c]==0.):
|
|
3340
|
-
mf_new.append(mf[:,:,c].copy())
|
|
3341
|
-
else:
|
|
3342
|
-
norm = normalize(mf[:,:,c].copy(),
|
|
3343
|
-
percentiles=percentiles[c],
|
|
3344
|
-
values=v,
|
|
3345
|
-
ignore_gray_value=ignore_gray_value,
|
|
3346
|
-
clip=clip,
|
|
3347
|
-
amplification=amplification,
|
|
3348
|
-
dtype=dtype,
|
|
3349
|
-
)
|
|
3350
|
-
mf_new.append(norm)
|
|
3351
|
-
|
|
3352
|
-
return np.moveaxis(mf_new,0,-1)
|
|
3353
|
-
|
|
3354
|
-
def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=np.float64, normalize_kwargs={"percentiles": (0.,99.99)}):
|
|
3355
|
-
|
|
3356
|
-
"""
|
|
3357
|
-
Loads and optionally normalizes and rescales specified frames from a stack located at a given path.
|
|
3358
|
-
|
|
3359
|
-
This function reads specified frames from a stack file, applying systematic adjustments to ensure
|
|
3360
|
-
the channel axis is last. It supports optional normalization of the input frames and rescaling. An
|
|
3361
|
-
artificial pixel modification is applied to frames with uniform values to prevent errors during
|
|
3362
|
-
normalization.
|
|
3363
|
-
|
|
3364
|
-
Parameters
|
|
3365
|
-
----------
|
|
3366
|
-
img_nums : int or list of int
|
|
3367
|
-
The index (or indices) of the image frame(s) to load from the stack.
|
|
3368
|
-
stack_path : str
|
|
3369
|
-
The file path to the stack from which frames are to be loaded.
|
|
3370
|
-
scale : float, optional
|
|
3371
|
-
The scaling factor to apply to the frames. If None, no scaling is applied (default is None).
|
|
3372
|
-
normalize_input : bool, optional
|
|
3373
|
-
Whether to normalize the loaded frames. If True, normalization is applied according to
|
|
3374
|
-
`normalize_kwargs` (default is True).
|
|
3375
|
-
dtype : data-type, optional
|
|
3376
|
-
The desired data-type for the output frames (default is float).
|
|
3377
|
-
normalize_kwargs : dict, optional
|
|
3378
|
-
Keyword arguments to pass to the normalization function (default is {"percentiles": (0., 99.99)}).
|
|
3379
|
-
|
|
3380
|
-
Returns
|
|
3381
|
-
-------
|
|
3382
|
-
ndarray or None
|
|
3383
|
-
The loaded, and possibly normalized and rescaled, frames as a NumPy array. Returns None if there
|
|
3384
|
-
is an error in loading the frames.
|
|
3385
|
-
|
|
3386
|
-
Raises
|
|
3387
|
-
------
|
|
3388
|
-
Exception
|
|
3389
|
-
Prints an error message if the specified frames cannot be loaded or if there is a mismatch between
|
|
3390
|
-
the provided experiment channel information and the stack format.
|
|
3391
|
-
|
|
3392
|
-
Notes
|
|
3393
|
-
-----
|
|
3394
|
-
- The function uses scikit-image for reading frames and supports multi-frame TIFF stacks.
|
|
3395
|
-
- Normalization and scaling are optional and can be customized through function parameters.
|
|
3396
|
-
- A workaround is implemented for frames with uniform pixel values to prevent normalization errors by
|
|
3397
|
-
adding a 'fake' pixel.
|
|
3398
|
-
|
|
3399
|
-
Examples
|
|
3400
|
-
--------
|
|
3401
|
-
>>> frames = load_frames([0, 1, 2], '/path/to/stack.tif', scale=0.5, normalize_input=True, dtype=np.uint8)
|
|
3402
|
-
# Loads the first three frames from '/path/to/stack.tif', normalizes them, rescales by a factor of 0.5,
|
|
3403
|
-
# and converts them to uint8 data type.
|
|
3404
|
-
|
|
3405
|
-
"""
|
|
3406
|
-
|
|
3407
|
-
try:
|
|
3408
|
-
frames = imageio.imread(stack_path, key=img_nums)
|
|
3409
|
-
except Exception as e:
|
|
3410
|
-
print(
|
|
3411
|
-
f'Error in loading the frame {img_nums} {e}. Please check that the experiment channel information is consistent with the movie being read.')
|
|
3412
|
-
return None
|
|
3413
|
-
try:
|
|
3414
|
-
frames[np.isinf(frames)] = np.nan
|
|
3415
|
-
except Exception as e:
|
|
3416
|
-
print(e)
|
|
3417
|
-
|
|
3418
|
-
frames = _rearrange_multichannel_frame(frames)
|
|
3419
|
-
|
|
3420
|
-
if normalize_input:
|
|
3421
|
-
frames = normalize_multichannel(frames, **normalize_kwargs)
|
|
3422
|
-
|
|
3423
|
-
if scale is not None:
|
|
3424
|
-
frames = zoom_multiframes(frames, scale)
|
|
3425
|
-
|
|
3426
|
-
# add a fake pixel to prevent auto normalization errors on images that are uniform
|
|
3427
|
-
frames = _fix_no_contrast(frames)
|
|
3428
|
-
|
|
3429
|
-
return frames.astype(dtype)
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
def get_stack_normalization_values(stack, percentiles=None, ignore_gray_value=0.):
|
|
3433
|
-
|
|
3434
|
-
"""
|
|
3435
|
-
Computes the normalization value ranges (minimum and maximum) for each channel in a 4D stack based on specified percentiles.
|
|
3436
|
-
|
|
3437
|
-
This function calculates the value ranges for normalizing each channel within a 4-dimensional stack, with dimensions
|
|
3438
|
-
expected to be in the order of Time (T), Y (height), X (width), and Channels (C). The normalization values are determined
|
|
3439
|
-
by the specified percentiles for each channel. An option to ignore a specific gray value during computation is provided,
|
|
3440
|
-
though its effect is not implemented in this snippet.
|
|
3441
|
-
|
|
3442
|
-
Parameters
|
|
3443
|
-
----------
|
|
3444
|
-
stack : ndarray
|
|
3445
|
-
The input 4D stack with dimensions TYXC from which to calculate normalization values.
|
|
3446
|
-
percentiles : tuple, list of tuples, optional
|
|
3447
|
-
The percentile values (low, high) used to calculate the normalization ranges for each channel. If a single tuple
|
|
3448
|
-
is provided, it is applied to all channels. If a list of tuples is provided, each tuple is applied to the
|
|
3449
|
-
corresponding channel. If None, defaults to (0., 99.99) for each channel.
|
|
3450
|
-
ignore_gray_value : float, optional
|
|
3451
|
-
A gray value to potentially ignore during the calculation. This parameter is provided for interface consistency
|
|
3452
|
-
but is not utilized in the current implementation (default is 0.).
|
|
3453
|
-
|
|
3454
|
-
Returns
|
|
3455
|
-
-------
|
|
3456
|
-
list of tuples
|
|
3457
|
-
A list where each tuple contains the (minimum, maximum) values for normalizing each channel based on the specified
|
|
3458
|
-
percentiles.
|
|
3459
|
-
|
|
3460
|
-
Raises
|
|
3461
|
-
------
|
|
3462
|
-
AssertionError
|
|
3463
|
-
If the input stack does not have 4 dimensions, or if the length of the `percentiles` list does not match the number
|
|
3464
|
-
of channels in the stack.
|
|
3465
|
-
|
|
3466
|
-
Notes
|
|
3467
|
-
-----
|
|
3468
|
-
- The function assumes the input stack is in TYXC format, where T is the time dimension, Y and X are spatial dimensions,
|
|
3469
|
-
and C is the channel dimension.
|
|
3470
|
-
- Memory management via `gc.collect()` is employed after calculating normalization values for each channel to mitigate
|
|
3471
|
-
potential memory issues with large datasets.
|
|
3472
|
-
|
|
3473
|
-
Examples
|
|
3474
|
-
--------
|
|
3475
|
-
>>> stack = np.random.rand(5, 100, 100, 3) # Example 4D stack with 3 channels
|
|
3476
|
-
>>> normalization_values = get_stack_normalization_values(stack, percentiles=((1, 99), (2, 98), (0, 100)))
|
|
3477
|
-
# Calculates normalization ranges for each channel using the specified percentiles.
|
|
3478
|
-
|
|
3479
|
-
"""
|
|
3480
|
-
|
|
3481
|
-
assert stack.ndim == 4, f'Wrong number of dimensions for the stack, expect TYXC (4) got {stack.ndim}.'
|
|
3482
|
-
if percentiles is None:
|
|
3483
|
-
percentiles = [(0., 99.99)] * stack.shape[-1]
|
|
3484
|
-
elif isinstance(percentiles, tuple):
|
|
3485
|
-
percentiles = [percentiles] * stack.shape[-1]
|
|
3486
|
-
elif isinstance(percentiles, list):
|
|
3487
|
-
assert len(percentiles) == stack.shape[
|
|
3488
|
-
-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.'
|
|
3489
|
-
|
|
3490
|
-
values = []
|
|
3491
|
-
for c in range(stack.shape[-1]):
|
|
3492
|
-
perc = percentiles[c]
|
|
3493
|
-
mi = np.nanpercentile(stack[:, :, :, c].flatten(), perc[0], keepdims=True)[0]
|
|
3494
|
-
ma = np.nanpercentile(stack[:, :, :, c].flatten(), perc[1], keepdims=True)[0]
|
|
3495
|
-
values.append(tuple((mi, ma)))
|
|
3496
|
-
gc.collect()
|
|
3497
|
-
|
|
3498
|
-
return values
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
def get_positions_in_well(well):
|
|
3502
|
-
|
|
3503
|
-
"""
|
|
3504
|
-
Retrieves the list of position directories within a specified well directory,
|
|
3505
|
-
formatted as a NumPy array of strings.
|
|
3506
|
-
|
|
3507
|
-
This function identifies position directories based on their naming convention,
|
|
3508
|
-
which must include a numeric identifier following the well's name. The well's name
|
|
3509
|
-
is expected to start with 'W' (e.g., 'W1'), followed by a numeric identifier. Position
|
|
3510
|
-
directories are assumed to be named with this numeric identifier directly after the well
|
|
3511
|
-
identifier, without the 'W'. For example, positions within well 'W1' might be named
|
|
3512
|
-
'101', '102', etc. This function will glob these directories and return their full
|
|
3513
|
-
paths as a NumPy array.
|
|
3514
|
-
|
|
3515
|
-
Parameters
|
|
3516
|
-
----------
|
|
3517
|
-
well : str
|
|
3518
|
-
The path to the well directory from which to retrieve position directories.
|
|
3519
|
-
|
|
3520
|
-
Returns
|
|
3521
|
-
-------
|
|
3522
|
-
np.ndarray
|
|
3523
|
-
An array of strings, each representing the full path to a position directory within
|
|
3524
|
-
the specified well. The array is empty if no position directories are found.
|
|
3525
|
-
|
|
3526
|
-
Notes
|
|
3527
|
-
-----
|
|
3528
|
-
- This function relies on a specific naming convention for wells and positions. It assumes
|
|
3529
|
-
that each well directory is prefixed with 'W' followed by a numeric identifier, and
|
|
3530
|
-
position directories are named starting with this numeric identifier directly.
|
|
3531
|
-
|
|
3532
|
-
Examples
|
|
3533
|
-
--------
|
|
3534
|
-
>>> get_positions_in_well('/path/to/experiment/W1')
|
|
3535
|
-
# This might return an array like array(['/path/to/experiment/W1/101', '/path/to/experiment/W1/102'])
|
|
3536
|
-
if position directories '101' and '102' exist within the well 'W1' directory.
|
|
3537
|
-
|
|
3538
|
-
"""
|
|
3539
|
-
|
|
3540
|
-
if well.endswith(os.sep):
|
|
3541
|
-
well = well[:-1]
|
|
3542
|
-
|
|
3543
|
-
w_numeric = os.path.split(well)[-1].replace('W', '')
|
|
3544
|
-
positions = natsorted(glob(os.sep.join([well, f'{w_numeric}*{os.sep}'])))
|
|
3545
|
-
|
|
3546
|
-
return np.array(positions, dtype=str)
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
def extract_experiment_folder_output(experiment_folder, destination_folder):
|
|
3550
|
-
|
|
3551
|
-
"""
|
|
3552
|
-
Copies the output subfolder and associated tables from an experiment folder to a new location,
|
|
3553
|
-
making the experiment folder much lighter by only keeping essential data.
|
|
3554
|
-
|
|
3555
|
-
This function takes the path to an experiment folder and a destination folder as input.
|
|
3556
|
-
It creates a copy of the experiment folder at the destination, but only includes the output subfolders
|
|
3557
|
-
and their associated tables for each well and position within the experiment.
|
|
3558
|
-
This operation significantly reduces the size of the experiment data by excluding non-essential files.
|
|
3559
|
-
|
|
3560
|
-
The structure of the copied experiment folder is preserved, including the configuration file,
|
|
3561
|
-
well directories, and position directories within each well.
|
|
3562
|
-
Only the 'output' subfolder and its 'tables' subdirectory are copied for each position.
|
|
3563
|
-
|
|
3564
|
-
Parameters
|
|
3565
|
-
----------
|
|
3566
|
-
experiment_folder : str
|
|
3567
|
-
The path to the source experiment folder from which to extract data.
|
|
3568
|
-
destination_folder : str
|
|
3569
|
-
The path to the destination folder where the reduced copy of the experiment
|
|
3570
|
-
will be created.
|
|
3571
|
-
|
|
3572
|
-
Notes
|
|
3573
|
-
-----
|
|
3574
|
-
- This function assumes that the structure of the experiment folder is consistent,
|
|
3575
|
-
with wells organized in subdirectories and each containing a position subdirectory.
|
|
3576
|
-
Each position subdirectory should have an 'output' folder and a 'tables' subfolder within it.
|
|
3577
|
-
|
|
3578
|
-
- The function also assumes the existence of a configuration file in the root of the
|
|
3579
|
-
experiment folder, which is copied to the root of the destination experiment folder.
|
|
3580
|
-
|
|
3581
|
-
Examples
|
|
3582
|
-
--------
|
|
3583
|
-
>>> extract_experiment_folder_output('/path/to/experiment_folder', '/path/to/destination_folder')
|
|
3584
|
-
# This will copy the 'experiment_folder' to 'destination_folder', including only
|
|
3585
|
-
# the output subfolders and their tables for each well and position.
|
|
3586
|
-
|
|
3587
|
-
"""
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
if experiment_folder.endswith(os.sep):
|
|
3591
|
-
experiment_folder = experiment_folder[:-1]
|
|
3592
|
-
if destination_folder.endswith(os.sep):
|
|
3593
|
-
destination_folder = destination_folder[:-1]
|
|
3594
|
-
|
|
3595
|
-
exp_name = experiment_folder.split(os.sep)[-1]
|
|
3596
|
-
output_path = os.sep.join([destination_folder, exp_name])
|
|
3597
|
-
if not os.path.exists(output_path):
|
|
3598
|
-
os.mkdir(output_path)
|
|
3599
|
-
|
|
3600
|
-
config = get_config(experiment_folder)
|
|
3601
|
-
copyfile(config, os.sep.join([output_path, os.path.split(config)[-1]]))
|
|
3602
|
-
|
|
3603
|
-
wells_src = get_experiment_wells(experiment_folder)
|
|
3604
|
-
wells = [w.split(os.sep)[-2] for w in wells_src]
|
|
3605
|
-
|
|
3606
|
-
for k, w in enumerate(wells):
|
|
3607
|
-
|
|
3608
|
-
well_output_path = os.sep.join([output_path, w])
|
|
3609
|
-
if not os.path.exists(well_output_path):
|
|
3610
|
-
os.mkdir(well_output_path)
|
|
3611
|
-
|
|
3612
|
-
positions = get_positions_in_well(wells_src[k])
|
|
3613
|
-
|
|
3614
|
-
for pos in positions:
|
|
3615
|
-
pos_name = extract_position_name(pos)
|
|
3616
|
-
output_pos = os.sep.join([well_output_path, pos_name])
|
|
3617
|
-
if not os.path.exists(output_pos):
|
|
3618
|
-
os.mkdir(output_pos)
|
|
3619
|
-
output_folder = os.sep.join([output_pos, 'output'])
|
|
3620
|
-
output_tables_folder = os.sep.join([output_folder, 'tables'])
|
|
3621
|
-
|
|
3622
|
-
if not os.path.exists(output_folder):
|
|
3623
|
-
os.mkdir(output_folder)
|
|
3624
|
-
|
|
3625
|
-
if not os.path.exists(output_tables_folder):
|
|
3626
|
-
os.mkdir(output_tables_folder)
|
|
3627
|
-
|
|
3628
|
-
tab_path = glob(pos + os.sep.join(['output', 'tables', f'*']))
|
|
3629
|
-
|
|
3630
|
-
for t in tab_path:
|
|
3631
|
-
copyfile(t, os.sep.join([output_tables_folder, os.path.split(t)[-1]]))
|
|
3632
|
-
|
|
3633
|
-
def _load_frames_to_segment(file, indices, scale_model=None, normalize_kwargs=None):
|
|
3634
|
-
|
|
3635
|
-
frames = load_frames(indices, file, scale=scale_model, normalize_input=True, normalize_kwargs=normalize_kwargs)
|
|
3636
|
-
frames = interpolate_nan_multichannel(frames)
|
|
3637
|
-
|
|
3638
|
-
if np.any(indices==-1):
|
|
3639
|
-
frames[:,:,np.where(indices==-1)[0]] = 0.
|
|
3640
|
-
|
|
3641
|
-
return frames
|
|
3642
|
-
|
|
3643
|
-
def _load_frames_to_measure(file, indices):
|
|
3644
|
-
return load_frames(indices, file, scale=None, normalize_input=False)
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
def _check_label_dims(lbl, file=None, template=None):
|
|
3648
|
-
|
|
3649
|
-
if file is not None:
|
|
3650
|
-
template = load_frames(0,file,scale=1,normalize_input=False)
|
|
3651
|
-
elif template is not None:
|
|
3652
|
-
template = template
|
|
3653
|
-
else:
|
|
3654
|
-
return lbl
|
|
3655
|
-
|
|
3656
|
-
if lbl.shape != template.shape[:2]:
|
|
3657
|
-
lbl = resize(lbl, template.shape[:2], order=0)
|
|
3658
|
-
return lbl
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
if __name__ == '__main__':
|
|
3662
|
-
control_segmentation_napari("/home/limozin/Documents/Experiments/MinimumJan/W4/401/", prefix='Aligned',
|
|
3663
|
-
population="target", flush_memory=False)
|