celldetective 1.4.2__py3-none-any.whl → 1.5.0b1__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 +403 -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/downloader.py +137 -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 +235 -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.0b1.dist-info}/METADATA +1 -1
- celldetective-1.5.0b1.dist-info/RECORD +187 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.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/downloader.py +0 -111
- 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-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/entry_points.txt +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from glob import glob
|
|
3
|
+
|
|
4
|
+
from celldetective.utils.downloaders import get_zenodo_files, download_zenodo_file
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def locate_signal_model(name, path=None, pairs=False):
|
|
8
|
+
"""
|
|
9
|
+
Locate a signal detection model by name, either locally or from Zenodo.
|
|
10
|
+
|
|
11
|
+
This function searches for a signal detection model with the specified name in the local
|
|
12
|
+
`celldetective` directory. If the model is not found locally, it attempts to download
|
|
13
|
+
the model from Zenodo.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
name : str
|
|
18
|
+
The name of the signal detection model to locate.
|
|
19
|
+
path : str, optional
|
|
20
|
+
An additional directory path to search for the model. If provided, this directory
|
|
21
|
+
is also scanned for matching models. Default is `None`.
|
|
22
|
+
pairs : bool, optional
|
|
23
|
+
If `True`, searches for paired signal detection models in the `pair_signal_detection`
|
|
24
|
+
subdirectory. If `False`, searches in the `signal_detection` subdirectory. Default is `False`.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
str or None
|
|
29
|
+
The full path to the located model directory if found, or `None` if the model is not available
|
|
30
|
+
locally or on Zenodo.
|
|
31
|
+
|
|
32
|
+
Notes
|
|
33
|
+
-----
|
|
34
|
+
- The function first searches in the `celldetective/models/signal_detection` or
|
|
35
|
+
`celldetective/models/pair_signal_detection` directory based on the `pairs` argument.
|
|
36
|
+
- If a `path` is specified, it is searched in addition to the default directories.
|
|
37
|
+
- If the model is not found locally, the function queries Zenodo for the model. If available,
|
|
38
|
+
the model is downloaded to the appropriate `celldetective` subdirectory.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
Search for a signal detection model locally:
|
|
43
|
+
|
|
44
|
+
>>> locate_signal_model("example_model")
|
|
45
|
+
'path/to/celldetective/models/signal_detection/example_model/'
|
|
46
|
+
|
|
47
|
+
Search for a paired signal detection model:
|
|
48
|
+
|
|
49
|
+
>>> locate_signal_model("paired_model", pairs=True)
|
|
50
|
+
'path/to/celldetective/models/pair_signal_detection/paired_model/'
|
|
51
|
+
|
|
52
|
+
Include an additional search path:
|
|
53
|
+
|
|
54
|
+
>>> locate_signal_model("custom_model", path="/additional/models/")
|
|
55
|
+
'/additional/models/custom_model/'
|
|
56
|
+
|
|
57
|
+
Handle a model available only on Zenodo:
|
|
58
|
+
|
|
59
|
+
>>> locate_signal_model("remote_model")
|
|
60
|
+
'path/to/celldetective/models/signal_detection/remote_model/'
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
main_dir = os.sep.join(
|
|
65
|
+
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]]
|
|
66
|
+
)
|
|
67
|
+
modelpath = os.sep.join([main_dir, "models", "signal_detection", os.sep])
|
|
68
|
+
if pairs:
|
|
69
|
+
modelpath = os.sep.join([main_dir, "models", "pair_signal_detection", os.sep])
|
|
70
|
+
print(f"Looking for {name} in {modelpath}")
|
|
71
|
+
models = glob(modelpath + f"*{os.sep}")
|
|
72
|
+
if path is not None:
|
|
73
|
+
if not path.endswith(os.sep):
|
|
74
|
+
path += os.sep
|
|
75
|
+
models += glob(path + f"*{os.sep}")
|
|
76
|
+
|
|
77
|
+
match = None
|
|
78
|
+
for m in models:
|
|
79
|
+
if name == m.replace("\\", os.sep).split(os.sep)[-2]:
|
|
80
|
+
match = m
|
|
81
|
+
return match
|
|
82
|
+
# else no match, try zenodo
|
|
83
|
+
files, categories = get_zenodo_files()
|
|
84
|
+
if name in files:
|
|
85
|
+
index = files.index(name)
|
|
86
|
+
cat = categories[index]
|
|
87
|
+
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
88
|
+
match = os.sep.join([main_dir, cat, name]) + os.sep
|
|
89
|
+
return match
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def locate_pair_signal_model(name, path=None):
|
|
93
|
+
"""
|
|
94
|
+
Locate a pair signal detection model by name.
|
|
95
|
+
|
|
96
|
+
This function searches for a pair signal detection model in the default
|
|
97
|
+
`celldetective` directory and optionally in an additional user-specified path.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
name : str
|
|
102
|
+
The name of the pair signal detection model to locate.
|
|
103
|
+
path : str, optional
|
|
104
|
+
An additional directory path to search for the model. If provided, this directory
|
|
105
|
+
is also scanned for matching models. Default is `None`.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
str or None
|
|
110
|
+
The full path to the located model directory if found, or `None` if no matching
|
|
111
|
+
model is located.
|
|
112
|
+
|
|
113
|
+
Notes
|
|
114
|
+
-----
|
|
115
|
+
- The function first searches in the default `celldetective/models/pair_signal_detection`
|
|
116
|
+
directory.
|
|
117
|
+
- If a `path` is specified, it is searched in addition to the default directory.
|
|
118
|
+
- The function prints the search path and model name during execution.
|
|
119
|
+
|
|
120
|
+
Examples
|
|
121
|
+
--------
|
|
122
|
+
Locate a model in the default directory:
|
|
123
|
+
|
|
124
|
+
>>> locate_pair_signal_model("example_model")
|
|
125
|
+
'path/to/celldetective/models/pair_signal_detection/example_model/'
|
|
126
|
+
|
|
127
|
+
Include an additional search directory:
|
|
128
|
+
|
|
129
|
+
>>> locate_pair_signal_model("custom_model", path="/additional/models/")
|
|
130
|
+
'/additional/models/custom_model/'
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
main_dir = os.sep.join(
|
|
135
|
+
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]]
|
|
136
|
+
)
|
|
137
|
+
modelpath = os.sep.join([main_dir, "models", "pair_signal_detection", os.sep])
|
|
138
|
+
print(f"Looking for {name} in {modelpath}")
|
|
139
|
+
models = glob(modelpath + f"*{os.sep}")
|
|
140
|
+
if path is not None:
|
|
141
|
+
if not path.endswith(os.sep):
|
|
142
|
+
path += os.sep
|
|
143
|
+
models += glob(path + f"*{os.sep}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def locate_segmentation_model(name, download=True):
|
|
147
|
+
"""
|
|
148
|
+
Locates a specified segmentation model within the local 'celldetective' directory or
|
|
149
|
+
downloads it from Zenodo if not found locally.
|
|
150
|
+
|
|
151
|
+
This function attempts to find a segmentation model by name within a predefined directory
|
|
152
|
+
structure starting from the 'celldetective/models/segmentation*' path. If the model is not
|
|
153
|
+
found locally, it then tries to locate and download the model from Zenodo, placing it into
|
|
154
|
+
the appropriate category directory within 'celldetective'. The function prints the search
|
|
155
|
+
directory path and returns the path to the found or downloaded model.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
name : str
|
|
160
|
+
The name of the segmentation model to locate.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
str or None
|
|
165
|
+
The full path to the located or downloaded segmentation model directory, or None if the
|
|
166
|
+
model could not be found or downloaded.
|
|
167
|
+
|
|
168
|
+
Raises
|
|
169
|
+
------
|
|
170
|
+
FileNotFoundError
|
|
171
|
+
If the model cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
main_dir = os.sep.join(
|
|
176
|
+
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]]
|
|
177
|
+
)
|
|
178
|
+
modelpath = os.sep.join([main_dir, "models", "segmentation*"]) + os.sep
|
|
179
|
+
# print(f'Looking for {name} in {modelpath}')
|
|
180
|
+
models = glob(modelpath + f"*{os.sep}")
|
|
181
|
+
|
|
182
|
+
match = None
|
|
183
|
+
for m in models:
|
|
184
|
+
if name == m.replace("\\", os.sep).split(os.sep)[-2]:
|
|
185
|
+
match = m
|
|
186
|
+
return match
|
|
187
|
+
if download:
|
|
188
|
+
# else no match, try zenodo
|
|
189
|
+
files, categories = get_zenodo_files()
|
|
190
|
+
if name in files:
|
|
191
|
+
index = files.index(name)
|
|
192
|
+
cat = categories[index]
|
|
193
|
+
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
194
|
+
match = os.sep.join([main_dir, cat, name]) + os.sep
|
|
195
|
+
|
|
196
|
+
return match
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def locate_segmentation_dataset(name):
|
|
200
|
+
"""
|
|
201
|
+
Locates a specified segmentation dataset within the local 'celldetective/datasets/segmentation_annotations' directory
|
|
202
|
+
or downloads it from Zenodo if not found locally.
|
|
203
|
+
|
|
204
|
+
This function attempts to find a segmentation dataset by name within a predefined directory structure. If the dataset
|
|
205
|
+
is not found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate
|
|
206
|
+
category directory within 'celldetective'. The function prints the search directory path and returns the path to the
|
|
207
|
+
found or downloaded dataset.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
name : str
|
|
212
|
+
The name of the segmentation dataset to locate.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
str or None
|
|
217
|
+
The full path to the located or downloaded segmentation dataset directory, or None if the dataset could not be
|
|
218
|
+
found or downloaded.
|
|
219
|
+
|
|
220
|
+
Raises
|
|
221
|
+
------
|
|
222
|
+
FileNotFoundError
|
|
223
|
+
If the dataset cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
main_dir = os.sep.join(
|
|
228
|
+
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]]
|
|
229
|
+
)
|
|
230
|
+
modelpath = os.sep.join([main_dir, "datasets", "segmentation_annotations", os.sep])
|
|
231
|
+
print(f"Looking for {name} in {modelpath}")
|
|
232
|
+
models = glob(modelpath + f"*{os.sep}")
|
|
233
|
+
|
|
234
|
+
match = None
|
|
235
|
+
for m in models:
|
|
236
|
+
if name == m.replace("\\", os.sep).split(os.sep)[-2]:
|
|
237
|
+
match = m
|
|
238
|
+
return match
|
|
239
|
+
# else no match, try zenodo
|
|
240
|
+
files, categories = get_zenodo_files()
|
|
241
|
+
if name in files:
|
|
242
|
+
index = files.index(name)
|
|
243
|
+
cat = categories[index]
|
|
244
|
+
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
245
|
+
match = os.sep.join([main_dir, cat, name]) + os.sep
|
|
246
|
+
return match
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def locate_signal_dataset(name):
|
|
250
|
+
"""
|
|
251
|
+
Locates a specified signal dataset within the local 'celldetective/datasets/signal_annotations' directory or downloads
|
|
252
|
+
it from Zenodo if not found locally.
|
|
253
|
+
|
|
254
|
+
This function attempts to find a signal dataset by name within a predefined directory structure. If the dataset is not
|
|
255
|
+
found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate category
|
|
256
|
+
directory within 'celldetective'. The function prints the search directory path and returns the path to the found or
|
|
257
|
+
downloaded dataset.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
name : str
|
|
262
|
+
The name of the signal dataset to locate.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
str or None
|
|
267
|
+
The full path to the located or downloaded signal dataset directory, or None if the dataset could not be found or
|
|
268
|
+
downloaded.
|
|
269
|
+
|
|
270
|
+
Raises
|
|
271
|
+
------
|
|
272
|
+
FileNotFoundError
|
|
273
|
+
If the dataset cannot be found locally and also cannot be found or downloaded from Zenodo.
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
main_dir = os.sep.join(
|
|
278
|
+
[os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]]
|
|
279
|
+
)
|
|
280
|
+
modelpath = os.sep.join([main_dir, "datasets", "signal_annotations", os.sep])
|
|
281
|
+
print(f"Looking for {name} in {modelpath}")
|
|
282
|
+
models = glob(modelpath + f"*{os.sep}")
|
|
283
|
+
|
|
284
|
+
match = None
|
|
285
|
+
for m in models:
|
|
286
|
+
if name == m.replace("\\", os.sep).split(os.sep)[-2]:
|
|
287
|
+
match = m
|
|
288
|
+
return match
|
|
289
|
+
# else no match, try zenodo
|
|
290
|
+
files, categories = get_zenodo_files()
|
|
291
|
+
if name in files:
|
|
292
|
+
index = files.index(name)
|
|
293
|
+
cat = categories[index]
|
|
294
|
+
download_zenodo_file(name, os.sep.join([main_dir, cat]))
|
|
295
|
+
match = os.sep.join([main_dir, cat, name]) + os.sep
|
|
296
|
+
return match
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def normalize_mi_ma(x, mi, ma, clip=False, eps=1e-20, dtype=np.float32):
|
|
7
|
+
# from csbdeep https://github.com/CSBDeep/CSBDeep/blob/main/csbdeep/utils/utils.py
|
|
8
|
+
if dtype is not None:
|
|
9
|
+
x = x.astype(dtype, copy=False)
|
|
10
|
+
mi = dtype(mi) if np.isscalar(mi) else mi.astype(dtype, copy=False)
|
|
11
|
+
ma = dtype(ma) if np.isscalar(ma) else ma.astype(dtype, copy=False)
|
|
12
|
+
eps = dtype(eps)
|
|
13
|
+
try:
|
|
14
|
+
import numexpr
|
|
15
|
+
|
|
16
|
+
x = numexpr.evaluate("(x - mi) / ( ma - mi + eps )")
|
|
17
|
+
except ImportError:
|
|
18
|
+
x = (x - mi) / (ma - mi + eps)
|
|
19
|
+
|
|
20
|
+
if clip:
|
|
21
|
+
x = np.clip(x, 0, 1)
|
|
22
|
+
|
|
23
|
+
return x
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def normalize(
|
|
27
|
+
frame,
|
|
28
|
+
percentiles=(0.0, 99.99),
|
|
29
|
+
values=None,
|
|
30
|
+
ignore_gray_value=0.0,
|
|
31
|
+
clip=False,
|
|
32
|
+
amplification=None,
|
|
33
|
+
dtype=float,
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
Normalize the intensity values of a frame.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
frame : ndarray
|
|
42
|
+
The input frame to be normalized.
|
|
43
|
+
percentiles : tuple, optional
|
|
44
|
+
The percentiles used to determine the minimum and maximum values for normalization. Default is (0.0, 99.99).
|
|
45
|
+
values : tuple or None, optional
|
|
46
|
+
The specific minimum and maximum values to use for normalization. If None, percentiles are used. Default is None.
|
|
47
|
+
ignore_gray_value : float or None, optional
|
|
48
|
+
The gray value to ignore during normalization. If specified, the pixels with this value will not be normalized. Default is 0.0.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
ndarray
|
|
53
|
+
The normalized frame.
|
|
54
|
+
|
|
55
|
+
Notes
|
|
56
|
+
-----
|
|
57
|
+
This function performs intensity normalization on a frame. It computes the minimum and maximum values for normalization either
|
|
58
|
+
using the specified values or by calculating percentiles from the frame. The frame is then normalized between the minimum and
|
|
59
|
+
maximum values using the `normalize_mi_ma` function. If `ignore_gray_value` is specified, the pixels with this value will be
|
|
60
|
+
left unmodified during normalization.
|
|
61
|
+
|
|
62
|
+
Examples
|
|
63
|
+
--------
|
|
64
|
+
>>> frame = np.array([[10, 20, 30],
|
|
65
|
+
[40, 50, 60],
|
|
66
|
+
[70, 80, 90]])
|
|
67
|
+
>>> normalized = normalize(frame)
|
|
68
|
+
>>> normalized
|
|
69
|
+
|
|
70
|
+
array([[0. , 0.2, 0.4],
|
|
71
|
+
[0.6, 0.8, 1. ],
|
|
72
|
+
[1.2, 1.4, 1.6]], dtype=float32)
|
|
73
|
+
|
|
74
|
+
>>> normalized = normalize(frame, percentiles=(10.0, 90.0))
|
|
75
|
+
>>> normalized
|
|
76
|
+
|
|
77
|
+
array([[0.33333334, 0.44444445, 0.5555556 ],
|
|
78
|
+
[0.6666667 , 0.7777778 , 0.8888889 ],
|
|
79
|
+
[1. , 1.1111112 , 1.2222222 ]], dtype=float32)
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
frame = frame.astype(float)
|
|
84
|
+
|
|
85
|
+
if ignore_gray_value is not None:
|
|
86
|
+
subframe = frame[frame != ignore_gray_value]
|
|
87
|
+
else:
|
|
88
|
+
subframe = frame.copy()
|
|
89
|
+
|
|
90
|
+
if values is not None:
|
|
91
|
+
mi = values[0]
|
|
92
|
+
ma = values[1]
|
|
93
|
+
else:
|
|
94
|
+
mi = np.nanpercentile(subframe.flatten(), percentiles[0], keepdims=True)
|
|
95
|
+
ma = np.nanpercentile(subframe.flatten(), percentiles[1], keepdims=True)
|
|
96
|
+
|
|
97
|
+
frame0 = frame.copy()
|
|
98
|
+
frame = normalize_mi_ma(frame0, mi, ma, clip=False, eps=1e-20, dtype=np.float32)
|
|
99
|
+
if amplification is not None:
|
|
100
|
+
frame *= amplification
|
|
101
|
+
if clip:
|
|
102
|
+
if amplification is None:
|
|
103
|
+
amplification = 1.0
|
|
104
|
+
frame[frame >= amplification] = amplification
|
|
105
|
+
frame[frame <= 0.0] = 0.0
|
|
106
|
+
if ignore_gray_value is not None:
|
|
107
|
+
frame[np.where(frame0) == ignore_gray_value] = ignore_gray_value
|
|
108
|
+
|
|
109
|
+
return frame.copy().astype(dtype)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def normalize_multichannel(
|
|
113
|
+
multichannel_frame: np.ndarray,
|
|
114
|
+
percentiles=None,
|
|
115
|
+
values=None,
|
|
116
|
+
ignore_gray_value=0.0,
|
|
117
|
+
clip=False,
|
|
118
|
+
amplification=None,
|
|
119
|
+
dtype=float,
|
|
120
|
+
):
|
|
121
|
+
"""
|
|
122
|
+
Normalizes a multichannel frame by adjusting the intensity values of each channel based on specified percentiles,
|
|
123
|
+
direct value ranges, or amplification factors, with options to ignore a specific gray value and to clip the output.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
multichannel_frame : ndarray
|
|
128
|
+
The input multichannel image frame to be normalized, expected to be a 3-dimensional array where the last dimension
|
|
129
|
+
represents the channels.
|
|
130
|
+
percentiles : list of tuples or tuple, optional
|
|
131
|
+
Percentile ranges (low, high) for each channel used to scale the intensity values. If a single tuple is provided,
|
|
132
|
+
it is applied to all channels. If None, the default percentile range of (0., 99.99) is used for each channel.
|
|
133
|
+
values : list of tuples or tuple, optional
|
|
134
|
+
Direct value ranges (min, max) for each channel to scale the intensity values. If a single tuple is provided, it
|
|
135
|
+
is applied to all channels. This parameter overrides `percentiles` if provided.
|
|
136
|
+
ignore_gray_value : float, optional
|
|
137
|
+
A specific gray value to ignore during normalization (default is 0.).
|
|
138
|
+
clip : bool, optional
|
|
139
|
+
If True, clips the output values to the range [0, 1] or the specified `dtype` range if `dtype` is not float
|
|
140
|
+
(default is False).
|
|
141
|
+
amplification : float, optional
|
|
142
|
+
A factor by which to amplify the intensity values after normalization. If None, no amplification is applied.
|
|
143
|
+
dtype : data-type, optional
|
|
144
|
+
The desired data-type for the output normalized frame. The default is float, but other types can be specified
|
|
145
|
+
to change the range of the output values.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
ndarray
|
|
150
|
+
The normalized multichannel frame as a 3-dimensional array of the same shape as `multichannel_frame`.
|
|
151
|
+
|
|
152
|
+
Raises
|
|
153
|
+
------
|
|
154
|
+
AssertionError
|
|
155
|
+
If the input `multichannel_frame` does not have 3 dimensions, or if the length of `values` does not match the
|
|
156
|
+
number of channels in `multichannel_frame`.
|
|
157
|
+
|
|
158
|
+
Notes
|
|
159
|
+
-----
|
|
160
|
+
- This function provides flexibility in normalization by allowing the use of percentile ranges, direct value ranges,
|
|
161
|
+
or amplification factors.
|
|
162
|
+
- The function makes a copy of the input frame to avoid altering the original data.
|
|
163
|
+
- When both `percentiles` and `values` are provided, `values` takes precedence for normalization.
|
|
164
|
+
|
|
165
|
+
Examples
|
|
166
|
+
--------
|
|
167
|
+
>>> multichannel_frame = np.random.rand(100, 100, 3) # Example multichannel frame
|
|
168
|
+
>>> normalized_frame = normalize_multichannel(multichannel_frame, percentiles=[(1, 99), (2, 98), (0, 100)])
|
|
169
|
+
# Normalizes each channel of the frame using specified percentile ranges.
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
mf = multichannel_frame.copy().astype(float)
|
|
174
|
+
assert mf.ndim == 3, f"Wrong shape for the multichannel frame: {mf.shape}."
|
|
175
|
+
if percentiles is None:
|
|
176
|
+
percentiles = [(0.0, 99.99)] * mf.shape[-1]
|
|
177
|
+
elif isinstance(percentiles, tuple):
|
|
178
|
+
percentiles = [percentiles] * mf.shape[-1]
|
|
179
|
+
if values is not None:
|
|
180
|
+
if isinstance(values, tuple):
|
|
181
|
+
values = [values] * mf.shape[-1]
|
|
182
|
+
assert (
|
|
183
|
+
len(values) == mf.shape[-1]
|
|
184
|
+
), "Mismatch between the normalization values provided and the number of channels."
|
|
185
|
+
|
|
186
|
+
mf_new = []
|
|
187
|
+
for c in range(mf.shape[-1]):
|
|
188
|
+
if values is not None:
|
|
189
|
+
v = values[c]
|
|
190
|
+
else:
|
|
191
|
+
v = None
|
|
192
|
+
|
|
193
|
+
if np.all(mf[:, :, c] == 0.0):
|
|
194
|
+
mf_new.append(mf[:, :, c].copy())
|
|
195
|
+
else:
|
|
196
|
+
norm = normalize(
|
|
197
|
+
mf[:, :, c].copy(),
|
|
198
|
+
percentiles=percentiles[c],
|
|
199
|
+
values=v,
|
|
200
|
+
ignore_gray_value=ignore_gray_value,
|
|
201
|
+
clip=clip,
|
|
202
|
+
amplification=amplification,
|
|
203
|
+
dtype=dtype,
|
|
204
|
+
)
|
|
205
|
+
mf_new.append(norm)
|
|
206
|
+
|
|
207
|
+
return np.moveaxis(mf_new, 0, -1)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_stack_normalization_values(stack, percentiles=None, ignore_gray_value=0.0):
|
|
211
|
+
"""
|
|
212
|
+
Computes the normalization value ranges (minimum and maximum) for each channel in a 4D stack based on specified percentiles.
|
|
213
|
+
|
|
214
|
+
This function calculates the value ranges for normalizing each channel within a 4-dimensional stack, with dimensions
|
|
215
|
+
expected to be in the order of Time (T), Y (height), X (width), and Channels (C). The normalization values are determined
|
|
216
|
+
by the specified percentiles for each channel. An option to ignore a specific gray value during computation is provided,
|
|
217
|
+
though its effect is not implemented in this snippet.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
stack : ndarray
|
|
222
|
+
The input 4D stack with dimensions TYXC from which to calculate normalization values.
|
|
223
|
+
percentiles : tuple, list of tuples, optional
|
|
224
|
+
The percentile values (low, high) used to calculate the normalization ranges for each channel. If a single tuple
|
|
225
|
+
is provided, it is applied to all channels. If a list of tuples is provided, each tuple is applied to the
|
|
226
|
+
corresponding channel. If None, defaults to (0., 99.99) for each channel.
|
|
227
|
+
ignore_gray_value : float, optional
|
|
228
|
+
A gray value to potentially ignore during the calculation. This parameter is provided for interface consistency
|
|
229
|
+
but is not utilized in the current implementation (default is 0.).
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
list of tuples
|
|
234
|
+
A list where each tuple contains the (minimum, maximum) values for normalizing each channel based on the specified
|
|
235
|
+
percentiles.
|
|
236
|
+
|
|
237
|
+
Raises
|
|
238
|
+
------
|
|
239
|
+
AssertionError
|
|
240
|
+
If the input stack does not have 4 dimensions, or if the length of the `percentiles` list does not match the number
|
|
241
|
+
of channels in the stack.
|
|
242
|
+
|
|
243
|
+
Notes
|
|
244
|
+
-----
|
|
245
|
+
- The function assumes the input stack is in TYXC format, where T is the time dimension, Y and X are spatial dimensions,
|
|
246
|
+
and C is the channel dimension.
|
|
247
|
+
- Memory management via `gc.collect()` is employed after calculating normalization values for each channel to mitigate
|
|
248
|
+
potential memory issues with large datasets.
|
|
249
|
+
|
|
250
|
+
Examples
|
|
251
|
+
--------
|
|
252
|
+
>>> stack = np.random.rand(5, 100, 100, 3) # Example 4D stack with 3 channels
|
|
253
|
+
>>> normalization_values = get_stack_normalization_values(stack, percentiles=((1, 99), (2, 98), (0, 100)))
|
|
254
|
+
# Calculates normalization ranges for each channel using the specified percentiles.
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
assert (
|
|
259
|
+
stack.ndim == 4
|
|
260
|
+
), f"Wrong number of dimensions for the stack, expect TYXC (4) got {stack.ndim}."
|
|
261
|
+
if percentiles is None:
|
|
262
|
+
percentiles = [(0.0, 99.99)] * stack.shape[-1]
|
|
263
|
+
elif isinstance(percentiles, tuple):
|
|
264
|
+
percentiles = [percentiles] * stack.shape[-1]
|
|
265
|
+
elif isinstance(percentiles, list):
|
|
266
|
+
assert (
|
|
267
|
+
len(percentiles) == stack.shape[-1]
|
|
268
|
+
), 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."
|
|
269
|
+
|
|
270
|
+
values = []
|
|
271
|
+
for c in range(stack.shape[-1]):
|
|
272
|
+
perc = percentiles[c]
|
|
273
|
+
mi = np.nanpercentile(stack[:, :, :, c].flatten(), perc[0], keepdims=True)[0]
|
|
274
|
+
ma = np.nanpercentile(stack[:, :, :, c].flatten(), perc[1], keepdims=True)[0]
|
|
275
|
+
values.append(tuple((mi, ma)))
|
|
276
|
+
gc.collect()
|
|
277
|
+
|
|
278
|
+
return values
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def normalize_per_channel(
|
|
282
|
+
X,
|
|
283
|
+
normalization_percentile_mode=True,
|
|
284
|
+
normalization_values=[0.1, 99.99],
|
|
285
|
+
normalization_clipping=False,
|
|
286
|
+
):
|
|
287
|
+
"""
|
|
288
|
+
Applies per-channel normalization to a list of multi-channel images.
|
|
289
|
+
|
|
290
|
+
This function normalizes each channel of every image in the list `X` based on either percentile values
|
|
291
|
+
or fixed min-max values. Optionally, it can also clip the normalized values to stay within the [0, 1] range.
|
|
292
|
+
The normalization can be applied in a percentile mode, where the lower and upper bounds for normalization
|
|
293
|
+
are determined based on the specified percentiles of the non-zero values in each channel.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
X : list of ndarray
|
|
298
|
+
A list of 3D numpy arrays, where each array represents a multi-channel image with dimensions
|
|
299
|
+
(height, width, channels).
|
|
300
|
+
normalization_percentile_mode : bool or list of bool, optional
|
|
301
|
+
If True (or a list of True values), normalization bounds are determined by percentiles specified
|
|
302
|
+
in `normalization_values` for each channel. If False, fixed `normalization_values` are used directly.
|
|
303
|
+
Default is True.
|
|
304
|
+
normalization_values : list of two floats or list of lists of two floats, optional
|
|
305
|
+
The percentile values [lower, upper] used for normalization in percentile mode, or the fixed
|
|
306
|
+
min-max values [min, max] for direct normalization. Default is [0.1, 99.99].
|
|
307
|
+
normalization_clipping : bool or list of bool, optional
|
|
308
|
+
Determines whether to clip the normalized values to the [0, 1] range for each channel. Default is False.
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
list of ndarray
|
|
313
|
+
The list of normalized multi-channel images.
|
|
314
|
+
|
|
315
|
+
Raises
|
|
316
|
+
------
|
|
317
|
+
AssertionError
|
|
318
|
+
If the input images do not have a channel dimension, or if the lengths of `normalization_values`,
|
|
319
|
+
`normalization_clipping`, and `normalization_percentile_mode` do not match the number of channels.
|
|
320
|
+
|
|
321
|
+
Notes
|
|
322
|
+
-----
|
|
323
|
+
- The normalization is applied in-place, modifying the input list `X`.
|
|
324
|
+
- This function is designed to handle multi-channel images commonly used in image processing and
|
|
325
|
+
computer vision tasks, particularly when different channels require separate normalization strategies.
|
|
326
|
+
|
|
327
|
+
Examples
|
|
328
|
+
--------
|
|
329
|
+
>>> X = [np.random.rand(100, 100, 3) for _ in range(5)] # Example list of 5 RGB images
|
|
330
|
+
>>> normalized_X = normalize_per_channel(X)
|
|
331
|
+
# Normalizes each channel of each image based on the default percentile values [0.1, 99.99].
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
assert X[0].ndim == 3, "Channel axis does not exist. Abort."
|
|
335
|
+
n_channels = X[0].shape[-1]
|
|
336
|
+
if isinstance(normalization_percentile_mode, bool):
|
|
337
|
+
normalization_percentile_mode = [normalization_percentile_mode] * n_channels
|
|
338
|
+
if isinstance(normalization_clipping, bool):
|
|
339
|
+
normalization_clipping = [normalization_clipping] * n_channels
|
|
340
|
+
if len(normalization_values) == 2 and not isinstance(normalization_values[0], list):
|
|
341
|
+
normalization_values = [normalization_values] * n_channels
|
|
342
|
+
|
|
343
|
+
assert len(normalization_values) == n_channels
|
|
344
|
+
assert len(normalization_clipping) == n_channels
|
|
345
|
+
assert len(normalization_percentile_mode) == n_channels
|
|
346
|
+
|
|
347
|
+
X_normalized = []
|
|
348
|
+
for i in range(len(X)):
|
|
349
|
+
x = X[i].copy()
|
|
350
|
+
loc_i, loc_j, loc_c = np.where(x == 0.0)
|
|
351
|
+
norm_x = np.zeros_like(x, dtype=np.float32)
|
|
352
|
+
for k in range(x.shape[-1]):
|
|
353
|
+
chan = x[:, :, k].copy()
|
|
354
|
+
if not np.all(chan.flatten() == 0):
|
|
355
|
+
if normalization_percentile_mode[k]:
|
|
356
|
+
min_val = np.nanpercentile(
|
|
357
|
+
chan[chan != 0.0].flatten(), normalization_values[k][0]
|
|
358
|
+
)
|
|
359
|
+
max_val = np.nanpercentile(
|
|
360
|
+
chan[chan != 0.0].flatten(), normalization_values[k][1]
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
min_val = normalization_values[k][0]
|
|
364
|
+
max_val = normalization_values[k][1]
|
|
365
|
+
|
|
366
|
+
clip_option = normalization_clipping[k]
|
|
367
|
+
norm_x[:, :, k] = normalize_mi_ma(
|
|
368
|
+
chan.astype(np.float32).copy(),
|
|
369
|
+
min_val,
|
|
370
|
+
max_val,
|
|
371
|
+
clip=clip_option,
|
|
372
|
+
eps=1e-20,
|
|
373
|
+
dtype=np.float32,
|
|
374
|
+
)
|
|
375
|
+
else:
|
|
376
|
+
norm_x[:, :, k] = 0.0
|
|
377
|
+
norm_x[loc_i, loc_j, loc_c] = 0.0
|
|
378
|
+
X_normalized.append(norm_x.copy())
|
|
379
|
+
|
|
380
|
+
return X_normalized
|