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.
Files changed (152) hide show
  1. celldetective/__init__.py +25 -0
  2. celldetective/__main__.py +62 -43
  3. celldetective/_version.py +1 -1
  4. celldetective/extra_properties.py +477 -399
  5. celldetective/filters.py +192 -97
  6. celldetective/gui/InitWindow.py +541 -411
  7. celldetective/gui/__init__.py +0 -15
  8. celldetective/gui/about.py +44 -39
  9. celldetective/gui/analyze_block.py +120 -84
  10. celldetective/gui/base/__init__.py +0 -0
  11. celldetective/gui/base/channel_norm_generator.py +335 -0
  12. celldetective/gui/base/components.py +249 -0
  13. celldetective/gui/base/feature_choice.py +92 -0
  14. celldetective/gui/base/figure_canvas.py +52 -0
  15. celldetective/gui/base/list_widget.py +133 -0
  16. celldetective/gui/{styles.py → base/styles.py} +92 -36
  17. celldetective/gui/base/utils.py +33 -0
  18. celldetective/gui/base_annotator.py +900 -767
  19. celldetective/gui/classifier_widget.py +6 -22
  20. celldetective/gui/configure_new_exp.py +777 -671
  21. celldetective/gui/control_panel.py +635 -524
  22. celldetective/gui/dynamic_progress.py +449 -0
  23. celldetective/gui/event_annotator.py +2023 -1662
  24. celldetective/gui/generic_signal_plot.py +1292 -944
  25. celldetective/gui/gui_utils.py +899 -1289
  26. celldetective/gui/interactions_block.py +658 -0
  27. celldetective/gui/interactive_timeseries_viewer.py +447 -0
  28. celldetective/gui/json_readers.py +48 -15
  29. celldetective/gui/layouts/__init__.py +5 -0
  30. celldetective/gui/layouts/background_model_free_layout.py +537 -0
  31. celldetective/gui/layouts/channel_offset_layout.py +134 -0
  32. celldetective/gui/layouts/local_correction_layout.py +91 -0
  33. celldetective/gui/layouts/model_fit_layout.py +372 -0
  34. celldetective/gui/layouts/operation_layout.py +68 -0
  35. celldetective/gui/layouts/protocol_designer_layout.py +96 -0
  36. celldetective/gui/pair_event_annotator.py +3130 -2435
  37. celldetective/gui/plot_measurements.py +586 -267
  38. celldetective/gui/plot_signals_ui.py +724 -506
  39. celldetective/gui/preprocessing_block.py +395 -0
  40. celldetective/gui/process_block.py +1678 -1831
  41. celldetective/gui/seg_model_loader.py +580 -473
  42. celldetective/gui/settings/__init__.py +0 -7
  43. celldetective/gui/settings/_cellpose_model_params.py +181 -0
  44. celldetective/gui/settings/_event_detection_model_params.py +95 -0
  45. celldetective/gui/settings/_segmentation_model_params.py +159 -0
  46. celldetective/gui/settings/_settings_base.py +77 -65
  47. celldetective/gui/settings/_settings_event_model_training.py +752 -526
  48. celldetective/gui/settings/_settings_measurements.py +1133 -964
  49. celldetective/gui/settings/_settings_neighborhood.py +574 -488
  50. celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
  51. celldetective/gui/settings/_settings_signal_annotator.py +329 -305
  52. celldetective/gui/settings/_settings_tracking.py +1304 -1094
  53. celldetective/gui/settings/_stardist_model_params.py +98 -0
  54. celldetective/gui/survival_ui.py +422 -312
  55. celldetective/gui/tableUI.py +1665 -1701
  56. celldetective/gui/table_ops/_maths.py +295 -0
  57. celldetective/gui/table_ops/_merge_groups.py +140 -0
  58. celldetective/gui/table_ops/_merge_one_hot.py +95 -0
  59. celldetective/gui/table_ops/_query_table.py +43 -0
  60. celldetective/gui/table_ops/_rename_col.py +44 -0
  61. celldetective/gui/thresholds_gui.py +382 -179
  62. celldetective/gui/viewers/__init__.py +0 -0
  63. celldetective/gui/viewers/base_viewer.py +700 -0
  64. celldetective/gui/viewers/channel_offset_viewer.py +331 -0
  65. celldetective/gui/viewers/contour_viewer.py +394 -0
  66. celldetective/gui/viewers/size_viewer.py +153 -0
  67. celldetective/gui/viewers/spot_detection_viewer.py +341 -0
  68. celldetective/gui/viewers/threshold_viewer.py +309 -0
  69. celldetective/gui/workers.py +403 -126
  70. celldetective/log_manager.py +92 -0
  71. celldetective/measure.py +1895 -1478
  72. celldetective/napari/__init__.py +0 -0
  73. celldetective/napari/utils.py +1025 -0
  74. celldetective/neighborhood.py +1914 -1448
  75. celldetective/preprocessing.py +1620 -1220
  76. celldetective/processes/__init__.py +0 -0
  77. celldetective/processes/background_correction.py +271 -0
  78. celldetective/processes/compute_neighborhood.py +894 -0
  79. celldetective/processes/detect_events.py +246 -0
  80. celldetective/processes/downloader.py +137 -0
  81. celldetective/processes/measure_cells.py +565 -0
  82. celldetective/processes/segment_cells.py +760 -0
  83. celldetective/processes/track_cells.py +435 -0
  84. celldetective/processes/train_segmentation_model.py +694 -0
  85. celldetective/processes/train_signal_model.py +265 -0
  86. celldetective/processes/unified_process.py +292 -0
  87. celldetective/regionprops/_regionprops.py +358 -317
  88. celldetective/relative_measurements.py +987 -710
  89. celldetective/scripts/measure_cells.py +313 -212
  90. celldetective/scripts/measure_relative.py +90 -46
  91. celldetective/scripts/segment_cells.py +165 -104
  92. celldetective/scripts/segment_cells_thresholds.py +96 -68
  93. celldetective/scripts/track_cells.py +198 -149
  94. celldetective/scripts/train_segmentation_model.py +324 -201
  95. celldetective/scripts/train_signal_model.py +87 -45
  96. celldetective/segmentation.py +844 -749
  97. celldetective/signals.py +3514 -2861
  98. celldetective/tracking.py +30 -15
  99. celldetective/utils/__init__.py +0 -0
  100. celldetective/utils/cellpose_utils/__init__.py +133 -0
  101. celldetective/utils/color_mappings.py +42 -0
  102. celldetective/utils/data_cleaning.py +630 -0
  103. celldetective/utils/data_loaders.py +450 -0
  104. celldetective/utils/dataset_helpers.py +207 -0
  105. celldetective/utils/downloaders.py +235 -0
  106. celldetective/utils/event_detection/__init__.py +8 -0
  107. celldetective/utils/experiment.py +1782 -0
  108. celldetective/utils/image_augmenters.py +308 -0
  109. celldetective/utils/image_cleaning.py +74 -0
  110. celldetective/utils/image_loaders.py +926 -0
  111. celldetective/utils/image_transforms.py +335 -0
  112. celldetective/utils/io.py +62 -0
  113. celldetective/utils/mask_cleaning.py +348 -0
  114. celldetective/utils/mask_transforms.py +5 -0
  115. celldetective/utils/masks.py +184 -0
  116. celldetective/utils/maths.py +351 -0
  117. celldetective/utils/model_getters.py +325 -0
  118. celldetective/utils/model_loaders.py +296 -0
  119. celldetective/utils/normalization.py +380 -0
  120. celldetective/utils/parsing.py +465 -0
  121. celldetective/utils/plots/__init__.py +0 -0
  122. celldetective/utils/plots/regression.py +53 -0
  123. celldetective/utils/resources.py +34 -0
  124. celldetective/utils/stardist_utils/__init__.py +104 -0
  125. celldetective/utils/stats.py +90 -0
  126. celldetective/utils/types.py +21 -0
  127. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/METADATA +1 -1
  128. celldetective-1.5.0b1.dist-info/RECORD +187 -0
  129. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/WHEEL +1 -1
  130. tests/gui/test_new_project.py +129 -117
  131. tests/gui/test_project.py +127 -79
  132. tests/test_filters.py +39 -15
  133. tests/test_notebooks.py +8 -0
  134. tests/test_tracking.py +232 -13
  135. tests/test_utils.py +123 -77
  136. celldetective/gui/base_components.py +0 -23
  137. celldetective/gui/layouts.py +0 -1602
  138. celldetective/gui/processes/compute_neighborhood.py +0 -594
  139. celldetective/gui/processes/downloader.py +0 -111
  140. celldetective/gui/processes/measure_cells.py +0 -360
  141. celldetective/gui/processes/segment_cells.py +0 -499
  142. celldetective/gui/processes/track_cells.py +0 -303
  143. celldetective/gui/processes/train_segmentation_model.py +0 -270
  144. celldetective/gui/processes/train_signal_model.py +0 -108
  145. celldetective/gui/table_ops/merge_groups.py +0 -118
  146. celldetective/gui/viewers.py +0 -1354
  147. celldetective/io.py +0 -3663
  148. celldetective/utils.py +0 -3108
  149. celldetective-1.4.2.dist-info/RECORD +0 -123
  150. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/entry_points.txt +0 -0
  151. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/licenses/LICENSE +0 -0
  152. {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