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
@@ -1,45 +1,45 @@
1
1
  """
2
- Copyright © 2022 Laboratoire Adhesion et Inflammation
2
+ Copyright © 2022 Laboratoire Adhesion et Inflammation
3
3
  Authored by R. Torro, K. Dervanova, L. Limozin
4
4
 
5
- This module defines additional measurement functions for use with `regionprops` via `measure_features`.
5
+ This module defines additional measurement functions for use with `regionprops` via `measure_features`.
6
6
 
7
7
  Usage
8
8
  -----
9
9
  Each function must follow these conventions:
10
10
 
11
- - **First argument:** `regionmask` (numpy array)
11
+ - **First argument:** `regionmask` (numpy array)
12
12
  A binary mask of the cell of interest, as provided by `regionprops`.
13
- - **Optional second argument:** `intensity_image` (numpy array)
14
- An image crop/bounding box associated with the cell (single-channel at a time).
13
+ - **Optional second argument:** `intensity_image` (numpy array)
14
+ An image crop/bounding box associated with the cell (single-channel at a time).
15
15
 
16
- Unlike the default `regionprops` from `scikit-image`, the cell image is **not** masked with zeros outside its boundaries.
17
- This allows thresholding techniques to be used in measurements.
16
+ Unlike the default `regionprops` from `scikit-image`, the cell image is **not** masked with zeros outside its boundaries.
17
+ This allows thresholding techniques to be used in measurements.
18
18
 
19
19
  Naming Conventions & Indexing
20
20
  ------------------------------
21
- - The measurement name is derived from the function name.
22
- - If a function returns multiple values (e.g., for multichannel images), outputs are labeled sequentially:
23
- `function-0`, `function-1`, etc.
24
- - To rename these outputs, use `rename_intensity_column` from `celldetective.utils`.
25
- - `"intensity"` in function names is automatically replaced with the actual channel name:
26
- - Example: `"intensity-0"` → `"brightfield_channel"`.
27
- - **Avoid digits smaller than the number of channels in function names** to prevent indexing conflicts.
28
- Prefer text-based names instead:
21
+ - The measurement name is derived from the function name.
22
+ - If a function returns multiple values (e.g., for multichannel images), outputs are labeled sequentially:
23
+ `function-0`, `function-1`, etc.
24
+ - To rename these outputs, use `rename_intensity_column` from `celldetective.utils`.
25
+ - `"intensity"` in function names is automatically replaced with the actual channel name:
26
+ - Example: `"intensity-0"` → `"brightfield_channel"`.
27
+ - **Avoid digits smaller than the number of channels in function names** to prevent indexing conflicts.
28
+ Prefer text-based names instead:
29
29
 
30
30
  .. code-block:: python
31
31
 
32
- # Bad practice:
33
- def intensity2(regionmask, intensity_image):
34
- pass
32
+ # Bad practice:
33
+ def intensity2(regionmask, intensity_image):
34
+ pass
35
35
 
36
- # Recommended:
37
- def intensity_two(regionmask, intensity_image):
38
- pass
36
+ # Recommended:
37
+ def intensity_two(regionmask, intensity_image):
38
+ pass
39
39
 
40
40
  GUI Integration
41
41
  ---------------
42
- New functions are **automatically** added to the list of available measurements in the graphical interface.
42
+ New functions are **automatically** added to the list of available measurements in the graphical interface.
43
43
  """
44
44
 
45
45
  import warnings
@@ -47,9 +47,10 @@ import warnings
47
47
  import numpy as np
48
48
  from scipy.ndimage import distance_transform_edt, center_of_mass
49
49
  from scipy.spatial.distance import euclidean
50
- from celldetective.utils import interpolate_nan, contour_of_instance_segmentation
50
+ from celldetective.utils.masks import contour_of_instance_segmentation
51
+ from celldetective.utils.image_cleaning import interpolate_nan
51
52
  import skimage.measure as skm
52
- from stardist import fill_label_holes
53
+ from celldetective.utils.mask_cleaning import fill_label_holes
53
54
  from celldetective.segmentation import segment_frame_from_thresholds
54
55
  from sklearn.metrics import r2_score
55
56
 
@@ -81,489 +82,566 @@ from sklearn.metrics import r2_score
81
82
  # "eccentricity > 0.99 or area < 60"
82
83
  # ],
83
84
  # }
84
-
85
+
85
86
  # lbl = segment_frame_from_thresholds(intensity_image, fill_holes=True, do_watershed=False, equalize_reference=None, edge_exclusion=False, **instructions)
86
87
  # lbl[lbl>0] = 1 # instance to binary
87
88
  # lbl[~regionmask] = 0 # make sure we don't measure stuff outside cell
88
89
 
89
90
  # return np.sum(lbl)
90
91
 
91
- def fraction_of_area_detected_in_intensity(regionmask, intensity_image, target_channel='adhesion_channel'):
92
-
93
- instructions = {
94
- "thresholds": [
95
- 0.02,
96
- 1000
97
- ],
98
- "filters": [
99
- [
100
- "subtract",
101
- 1
102
- ],
103
- [
104
- "abs",
105
- 2
106
- ],
107
- [
108
- "gauss",
109
- 0.8
110
- ]
111
- ],
112
- }
113
-
114
- lbl = segment_frame_from_thresholds(intensity_image, do_watershed=False, fill_holes=True, equalize_reference=None, edge_exclusion=False, **instructions)
115
- lbl[lbl>0] = 1 # instance to binary
116
- lbl[~regionmask] = 0 # make sure we don't measure stuff outside cell
117
-
118
- return float(np.sum(lbl)) / float(np.sum(regionmask))
119
-
120
- def area_detected_in_intensity(regionmask, intensity_image, target_channel='adhesion_channel'):
121
-
122
- """
123
- Computes the detected area within the regionmask based on threshold-based segmentation.
124
-
125
- The function applies a predefined filtering and thresholding pipeline to the intensity image (normalized adhesion channel)
126
- to detect significant regions. The resulting segmented regions are restricted to the
127
- `regionmask`, ensuring that only the relevant area is measured.
128
-
129
- Parameters
130
- ----------
131
- regionmask : ndarray
132
- A binary mask (2D array) where nonzero values define the region of interest.
133
- intensity_image : ndarray
134
- A 2D array of the same shape as `regionmask`, representing the intensity
135
- values associated with the region.
136
- target_channel : str, optional
137
- Name of the intensity channel used for measurement. Defaults to `'adhesion_channel'`.
138
-
139
- Returns
140
- -------
141
- detected_area : float
142
- The total area (number of pixels) detected based on intensity-based segmentation.
143
-
144
- Notes
145
- -----
146
- - The segmentation is performed using `segment_frame_from_thresholds()` with predefined parameters:
147
-
148
- - Thresholding range: `[0.02, 1000]`
149
- - Filters applied in sequence:
150
-
151
- - `"subtract"` with value `1` (subtract 1 from intensity values)
152
- - `"abs"` (take absolute value of intensities)
153
- - `"gauss"` with sigma `0.8` (apply Gauss filter with sigma `0.8`)
154
-
155
- - The segmentation includes hole filling.
156
- - The detected regions are converted to a binary mask (`lbl > 0`).
157
- - Any pixels outside the `regionmask` are excluded from the measurement.
158
-
159
- """
160
-
161
- instructions = {
162
- "thresholds": [
163
- 0.02,
164
- 1000
165
- ],
166
- "filters": [
167
- [
168
- "subtract",
169
- 1
170
- ],
171
- [
172
- "abs",
173
- 2
174
- ],
175
- [
176
- "gauss",
177
- 0.8
178
- ]
179
- ],
180
- }
181
-
182
- lbl = segment_frame_from_thresholds(intensity_image, do_watershed=False, fill_holes=True, equalize_reference=None, edge_exclusion=False, **instructions)
183
- lbl[lbl>0] = 1 # instance to binary
184
- lbl[~regionmask] = 0 # make sure we don't measure stuff outside cell
185
-
186
- return float(np.sum(lbl))
187
-
188
-
189
- def area_dark_intensity(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True, threshold=0.95): #, target_channel='adhesion_channel'
190
-
191
- """
192
- Computes the absolute area within the regionmask where the intensity is below a given threshold.
193
-
194
- This function identifies pixels in the region where the intensity is lower than `threshold`.
195
- If `fill_holes` is `True`, small enclosed holes in the detected dark regions are filled before
196
- computing the total area.
197
-
198
- Parameters
199
- ----------
200
- regionmask : ndarray
201
- A binary mask (2D array) where nonzero values define the region of interest.
202
- intensity_image : ndarray
203
- A 2D array of the same shape as `regionmask`, representing the intensity
204
- values associated with the region.
205
- target_channel : str, optional
206
- Name of the intensity channel used for measurement. Defaults to `'adhesion_channel'`.
207
- fill_holes : bool, optional
208
- If `True`, fills enclosed holes in the detected dark intensity regions before computing
209
- the area. Defaults to `True`.
210
- threshold : float, optional
211
- Intensity threshold below which a pixel is considered part of a dark region.
212
- Defaults to `0.95`.
213
-
214
- Returns
215
- -------
216
- dark_area : float
217
- The absolute area (number of pixels) where intensity values are below `threshold`, within the regionmask.
218
-
219
- Notes
220
- -----
221
- - The default threshold for defining "dark" intensity regions is `0.95`, but it can be adjusted.
222
- - If `fill_holes` is `True`, the function applies hole-filling to the detected dark regions
223
- using `skimage.measure.label` and `fill_label_holes()`.
224
- - The `target_channel` parameter tells regionprops to only measure this channel.
225
-
226
- """
227
-
228
- subregion = (intensity_image < threshold)*regionmask # under one, under 0.8, under 0.6, whatever value!
229
- if fill_holes:
230
- subregion = skm.label(subregion, connectivity=2, background=0)
231
- subregion = fill_label_holes(subregion)
232
- subregion[subregion>0] = 1
233
-
234
- return float(np.sum(subregion))
235
-
236
-
237
- def fraction_of_area_dark_intensity(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True, threshold=0.95): #, target_channel='adhesion_channel'
238
-
239
- subregion = (intensity_image < threshold)*regionmask # under one, under 0.8, under 0.6, whatever value!
240
- if fill_holes:
241
- subregion = skm.label(subregion, connectivity=2, background=0)
242
- subregion = fill_label_holes(subregion)
243
- subregion[subregion>0] = 1
244
-
245
- return float(np.sum(subregion)) / float(np.sum(regionmask))
246
-
247
-
248
- def area_dark_intensity_nintyfive(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True): #, target_channel='adhesion_channel'
249
-
250
- subregion = (intensity_image < 0.95)*regionmask # under one, under 0.8, under 0.6, whatever value!
251
- if fill_holes:
252
- subregion = skm.label(subregion, connectivity=2, background=0)
253
- subregion = fill_label_holes(subregion)
254
- subregion[subregion>0] = 1
255
-
256
- return float(np.sum(subregion))
257
-
258
- def area_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True): #, target_channel='adhesion_channel'
259
-
260
- subregion = (intensity_image < 0.90)*regionmask # under one, under 0.8, under 0.6, whatever value!
261
- if fill_holes:
262
- subregion = skm.label(subregion, connectivity=2, background=0)
263
- subregion = fill_label_holes(subregion)
264
- subregion[subregion>0] = 1
265
-
266
- return float(np.sum(subregion))
267
-
268
- def mean_dark_intensity_nintyfive(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True):
92
+
93
+ def fraction_of_area_detected_in_intensity(
94
+ regionmask, intensity_image, target_channel="adhesion_channel"
95
+ ):
96
+
97
+ instructions = {
98
+ "thresholds": [0.02, 1000],
99
+ "filters": [["subtract", 1], ["abs", 2], ["gauss", 0.8]],
100
+ }
101
+
102
+ lbl = segment_frame_from_thresholds(
103
+ intensity_image,
104
+ do_watershed=False,
105
+ fill_holes=True,
106
+ equalize_reference=None,
107
+ edge_exclusion=False,
108
+ **instructions
109
+ )
110
+ lbl[lbl > 0] = 1 # instance to binary
111
+ lbl[~regionmask] = 0 # make sure we don't measure stuff outside cell
112
+
113
+ return float(np.sum(lbl)) / float(np.sum(regionmask))
114
+
115
+
116
+ def area_detected_in_intensity(
117
+ regionmask, intensity_image, target_channel="adhesion_channel"
118
+ ):
119
+ """
120
+ Computes the detected area within the regionmask based on threshold-based segmentation.
121
+
122
+ The function applies a predefined filtering and thresholding pipeline to the intensity image (normalized adhesion channel)
123
+ to detect significant regions. The resulting segmented regions are restricted to the
124
+ `regionmask`, ensuring that only the relevant area is measured.
125
+
126
+ Parameters
127
+ ----------
128
+ regionmask : ndarray
129
+ A binary mask (2D array) where nonzero values define the region of interest.
130
+ intensity_image : ndarray
131
+ A 2D array of the same shape as `regionmask`, representing the intensity
132
+ values associated with the region.
133
+ target_channel : str, optional
134
+ Name of the intensity channel used for measurement. Defaults to `'adhesion_channel'`.
135
+
136
+ Returns
137
+ -------
138
+ detected_area : float
139
+ The total area (number of pixels) detected based on intensity-based segmentation.
140
+
141
+ Notes
142
+ -----
143
+ - The segmentation is performed using `segment_frame_from_thresholds()` with predefined parameters:
144
+
145
+ - Thresholding range: `[0.02, 1000]`
146
+ - Filters applied in sequence:
147
+
148
+ - `"subtract"` with value `1` (subtract 1 from intensity values)
149
+ - `"abs"` (take absolute value of intensities)
150
+ - `"gauss"` with sigma `0.8` (apply Gauss filter with sigma `0.8`)
151
+
152
+ - The segmentation includes hole filling.
153
+ - The detected regions are converted to a binary mask (`lbl > 0`).
154
+ - Any pixels outside the `regionmask` are excluded from the measurement.
155
+
156
+ """
157
+
158
+ instructions = {
159
+ "thresholds": [0.02, 1000],
160
+ "filters": [["subtract", 1], ["abs", 2], ["gauss", 0.8]],
161
+ }
162
+
163
+ lbl = segment_frame_from_thresholds(
164
+ intensity_image,
165
+ do_watershed=False,
166
+ fill_holes=True,
167
+ equalize_reference=None,
168
+ edge_exclusion=False,
169
+ **instructions
170
+ )
171
+ lbl[lbl > 0] = 1 # instance to binary
172
+ lbl[~regionmask] = 0 # make sure we don't measure stuff outside cell
173
+
174
+ return float(np.sum(lbl))
175
+
176
+
177
+ def area_dark_intensity(
178
+ regionmask,
179
+ intensity_image,
180
+ target_channel="adhesion_channel",
181
+ fill_holes=True,
182
+ threshold=0.95,
183
+ ): # , target_channel='adhesion_channel'
184
+ """
185
+ Computes the absolute area within the regionmask where the intensity is below a given threshold.
186
+
187
+ This function identifies pixels in the region where the intensity is lower than `threshold`.
188
+ If `fill_holes` is `True`, small enclosed holes in the detected dark regions are filled before
189
+ computing the total area.
190
+
191
+ Parameters
192
+ ----------
193
+ regionmask : ndarray
194
+ A binary mask (2D array) where nonzero values define the region of interest.
195
+ intensity_image : ndarray
196
+ A 2D array of the same shape as `regionmask`, representing the intensity
197
+ values associated with the region.
198
+ target_channel : str, optional
199
+ Name of the intensity channel used for measurement. Defaults to `'adhesion_channel'`.
200
+ fill_holes : bool, optional
201
+ If `True`, fills enclosed holes in the detected dark intensity regions before computing
202
+ the area. Defaults to `True`.
203
+ threshold : float, optional
204
+ Intensity threshold below which a pixel is considered part of a dark region.
205
+ Defaults to `0.95`.
206
+
207
+ Returns
208
+ -------
209
+ dark_area : float
210
+ The absolute area (number of pixels) where intensity values are below `threshold`, within the regionmask.
211
+
212
+ Notes
213
+ -----
214
+ - The default threshold for defining "dark" intensity regions is `0.95`, but it can be adjusted.
215
+ - If `fill_holes` is `True`, the function applies hole-filling to the detected dark regions
216
+ using `skimage.measure.label` and `fill_label_holes()`.
217
+ - The `target_channel` parameter tells regionprops to only measure this channel.
218
+
219
+ """
220
+
221
+ subregion = (
222
+ intensity_image < threshold
223
+ ) * regionmask # under one, under 0.8, under 0.6, whatever value!
224
+ if fill_holes:
225
+ subregion = skm.label(subregion, connectivity=2, background=0)
226
+ subregion = fill_label_holes(subregion)
227
+ subregion[subregion > 0] = 1
228
+
229
+ return float(np.sum(subregion))
230
+
231
+
232
+ def fraction_of_area_dark_intensity(
233
+ regionmask,
234
+ intensity_image,
235
+ target_channel="adhesion_channel",
236
+ fill_holes=True,
237
+ threshold=0.95,
238
+ ): # , target_channel='adhesion_channel'
239
+
240
+ subregion = (
241
+ intensity_image < threshold
242
+ ) * regionmask # under one, under 0.8, under 0.6, whatever value!
243
+ if fill_holes:
244
+ subregion = skm.label(subregion, connectivity=2, background=0)
245
+ subregion = fill_label_holes(subregion)
246
+ subregion[subregion > 0] = 1
247
+
248
+ return float(np.sum(subregion)) / float(np.sum(regionmask))
249
+
250
+
251
+ def area_dark_intensity_nintyfive(
252
+ regionmask, intensity_image, target_channel="adhesion_channel", fill_holes=True
253
+ ): # , target_channel='adhesion_channel'
254
+
255
+ subregion = (
256
+ intensity_image < 0.95
257
+ ) * regionmask # under one, under 0.8, under 0.6, whatever value!
258
+ if fill_holes:
259
+ subregion = skm.label(subregion, connectivity=2, background=0)
260
+ subregion = fill_label_holes(subregion)
261
+ subregion[subregion > 0] = 1
262
+
263
+ return float(np.sum(subregion))
264
+
265
+
266
+ def area_dark_intensity_ninty(
267
+ regionmask, intensity_image, target_channel="adhesion_channel", fill_holes=True
268
+ ): # , target_channel='adhesion_channel'
269
+
270
+ subregion = (
271
+ intensity_image < 0.90
272
+ ) * regionmask # under one, under 0.8, under 0.6, whatever value!
273
+ if fill_holes:
274
+ subregion = skm.label(subregion, connectivity=2, background=0)
275
+ subregion = fill_label_holes(subregion)
276
+ subregion[subregion > 0] = 1
277
+
278
+ return float(np.sum(subregion))
279
+
280
+
281
+ def mean_dark_intensity_nintyfive(
282
+ regionmask, intensity_image, target_channel="adhesion_channel", fill_holes=True
283
+ ):
269
284
  """
270
285
  Calculate the mean intensity in a dark subregion below 95, handling NaN values.
271
-
286
+
272
287
  """
273
288
  subregion = (intensity_image < 0.95) * regionmask
274
-
289
+
275
290
  if fill_holes:
276
291
  subregion = skm.label(subregion, connectivity=2, background=0)
277
292
  subregion = fill_label_holes(subregion)
278
293
  subregion[subregion > 0] = 1
279
-
280
-
294
+
281
295
  masked_intensity = intensity_image[subregion == 1]
282
-
296
+
283
297
  return float(np.nanmean(masked_intensity))
284
298
 
285
299
 
286
- def mean_dark_intensity_nintyfive_fillhole_false(regionmask, intensity_image, target_channel='adhesion_channel'):
300
+ def mean_dark_intensity_nintyfive_fillhole_false(
301
+ regionmask, intensity_image, target_channel="adhesion_channel"
302
+ ):
287
303
  """
288
304
  Calculate the mean intensity in a dark subregion below 95, handling NaN values.
289
305
  """
290
- subregion = (intensity_image < 0.95) * regionmask # Select dark regions within the mask
306
+ subregion = (
307
+ intensity_image < 0.95
308
+ ) * regionmask # Select dark regions within the mask
291
309
 
292
- masked_intensity = intensity_image[subregion == 1] # Extract pixel values from the selected region
310
+ masked_intensity = intensity_image[
311
+ subregion == 1
312
+ ] # Extract pixel values from the selected region
293
313
 
294
314
  return float(np.nanmean(masked_intensity)) # Compute mean, ignoring NaNs
295
315
 
296
- def mean_dark_intensity_ninty_fillhole_false(regionmask, intensity_image, target_channel='adhesion_channel'):
316
+
317
+ def mean_dark_intensity_ninty_fillhole_false(
318
+ regionmask, intensity_image, target_channel="adhesion_channel"
319
+ ):
297
320
  """
298
321
  Calculate the mean intensity in a dark subregion, handling NaN values.
299
322
  """
300
- subregion = (intensity_image < 0.90) * regionmask # Select dark regions within the mask
323
+ subregion = (
324
+ intensity_image < 0.90
325
+ ) * regionmask # Select dark regions within the mask
301
326
 
302
- masked_intensity = intensity_image[subregion == 1] # Extract pixel values from the selected region
327
+ masked_intensity = intensity_image[
328
+ subregion == 1
329
+ ] # Extract pixel values from the selected region
303
330
 
304
331
  return float(np.nanmean(masked_intensity)) # Compute mean, ignoring NaNs
305
332
 
306
333
 
307
- def mean_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True):
334
+ def mean_dark_intensity_ninty(
335
+ regionmask, intensity_image, target_channel="adhesion_channel", fill_holes=True
336
+ ):
308
337
  """
309
338
  Calculate the mean intensity in a dark subregion below 90, handling NaN values.
310
-
339
+
311
340
  """
312
341
  subregion = (intensity_image < 0.90) * regionmask
313
-
342
+
314
343
  if fill_holes:
315
344
  subregion = skm.label(subregion, connectivity=2, background=0)
316
345
  subregion = fill_label_holes(subregion)
317
346
  subregion[subregion > 0] = 1
318
-
319
-
347
+
320
348
  masked_intensity = intensity_image[subregion == 1]
321
-
349
+
322
350
  return float(np.nanmean(masked_intensity))
323
351
 
324
- def mean_dark_intensity_eight_five(regionmask, intensity_image, target_channel='adhesion_channel', fill_holes=True):
352
+
353
+ def mean_dark_intensity_eight_five(
354
+ regionmask, intensity_image, target_channel="adhesion_channel", fill_holes=True
355
+ ):
325
356
  """
326
357
  Calculate the mean intensity in a dark subregion below 85, handling NaN values.
327
-
358
+
328
359
  """
329
360
  subregion = (intensity_image < 0.85) * regionmask
330
-
361
+
331
362
  if fill_holes:
332
363
  subregion = skm.label(subregion, connectivity=2, background=0)
333
364
  subregion = fill_label_holes(subregion)
334
365
  subregion[subregion > 0] = 1
335
-
336
-
366
+
337
367
  masked_intensity = intensity_image[subregion == 1]
338
-
368
+
339
369
  return float(np.nanmean(masked_intensity))
340
370
 
341
371
 
342
- def mean_dark_intensity_eight_five_fillhole_false(regionmask, intensity_image, target_channel='adhesion_channel'):
372
+ def mean_dark_intensity_eight_five_fillhole_false(
373
+ regionmask, intensity_image, target_channel="adhesion_channel"
374
+ ):
343
375
 
344
- subregion = (intensity_image < 0.85) * regionmask # Select dark regions within the mask
376
+ subregion = (
377
+ intensity_image < 0.85
378
+ ) * regionmask # Select dark regions within the mask
345
379
 
346
- masked_intensity = intensity_image[subregion == 1] # Extract pixel values from the selected region
380
+ masked_intensity = intensity_image[
381
+ subregion == 1
382
+ ] # Extract pixel values from the selected region
347
383
 
348
384
  return float(np.nanmean(masked_intensity)) # Compute mean, ignoring NaNs
349
385
 
350
- def percentile_zero_one_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
386
+
387
+ def percentile_zero_one_dark_intensity_ninty(
388
+ regionmask, intensity_image, target_channel="adhesion_channel"
389
+ ):
351
390
 
352
391
  subregion = (intensity_image < 0.95) * regionmask
353
- return float(np.nanpercentile(intensity_image[subregion],0.1))
392
+ return float(np.nanpercentile(intensity_image[subregion], 0.1))
354
393
 
355
394
 
356
- def percentile_one_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
395
+ def percentile_one_dark_intensity_ninty(
396
+ regionmask, intensity_image, target_channel="adhesion_channel"
397
+ ):
357
398
 
358
399
  subregion = (intensity_image < 0.95) * regionmask
359
- return float(np.nanpercentile(intensity_image[subregion],1))
400
+ return float(np.nanpercentile(intensity_image[subregion], 1))
360
401
 
361
402
 
362
- def percentile_five_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
403
+ def percentile_five_dark_intensity_ninty(
404
+ regionmask, intensity_image, target_channel="adhesion_channel"
405
+ ):
363
406
 
364
407
  subregion = (intensity_image < 0.95) * regionmask
365
- return float(np.nanpercentile(intensity_image[subregion],5))
408
+ return float(np.nanpercentile(intensity_image[subregion], 5))
366
409
 
367
410
 
368
- def percentile_ten_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
411
+ def percentile_ten_dark_intensity_ninty(
412
+ regionmask, intensity_image, target_channel="adhesion_channel"
413
+ ):
369
414
 
370
415
  subregion = (intensity_image < 0.95) * regionmask
371
- return float(np.nanpercentile(intensity_image[subregion],10))
416
+ return float(np.nanpercentile(intensity_image[subregion], 10))
372
417
 
373
418
 
374
- def percentile_ninty_five_dark_intensity_ninty(regionmask, intensity_image, target_channel='adhesion_channel'):
419
+ def percentile_ninty_five_dark_intensity_ninty(
420
+ regionmask, intensity_image, target_channel="adhesion_channel"
421
+ ):
375
422
 
376
423
  subregion = (intensity_image < 0.95) * regionmask
377
- return float(np.nanpercentile(intensity_image[subregion],95))
378
-
424
+ return float(np.nanpercentile(intensity_image[subregion], 95))
425
+
379
426
 
380
427
  def intensity_percentile_ninety_nine(regionmask, intensity_image):
381
- return np.nanpercentile(intensity_image[regionmask],99)
428
+ return np.nanpercentile(intensity_image[regionmask], 99)
429
+
382
430
 
383
431
  def intensity_percentile_ninety_five(regionmask, intensity_image):
384
- return np.nanpercentile(intensity_image[regionmask],95)
432
+ return np.nanpercentile(intensity_image[regionmask], 95)
433
+
385
434
 
386
435
  def intensity_percentile_ninety(regionmask, intensity_image):
387
- return np.nanpercentile(intensity_image[regionmask],90)
436
+ return np.nanpercentile(intensity_image[regionmask], 90)
437
+
388
438
 
389
439
  def intensity_percentile_seventy_five(regionmask, intensity_image):
390
- return np.nanpercentile(intensity_image[regionmask],75)
440
+ return np.nanpercentile(intensity_image[regionmask], 75)
441
+
391
442
 
392
443
  def intensity_percentile_fifty(regionmask, intensity_image):
393
- return np.nanpercentile(intensity_image[regionmask],50)
444
+ return np.nanpercentile(intensity_image[regionmask], 50)
445
+
394
446
 
395
447
  def intensity_percentile_twenty_five(regionmask, intensity_image):
396
- return np.nanpercentile(intensity_image[regionmask],25)
448
+ return np.nanpercentile(intensity_image[regionmask], 25)
449
+
397
450
 
398
451
  # STD
399
452
 
453
+
400
454
  def intensity_std(regionmask, intensity_image):
401
- return np.nanstd(intensity_image[regionmask])
455
+ return np.nanstd(intensity_image[regionmask])
402
456
 
403
457
 
404
458
  def intensity_median(regionmask, intensity_image):
405
- return np.nanmedian(intensity_image[regionmask])
459
+ return np.nanmedian(intensity_image[regionmask])
460
+
406
461
 
407
462
  def intensity_nanmean(regionmask, intensity_image):
408
-
409
- if np.all(intensity_image==0):
410
- return np.nan
411
- else:
412
- return np.nanmean(intensity_image[regionmask])
463
+
464
+ if np.all(intensity_image == 0):
465
+ return np.nan
466
+ else:
467
+ return np.nanmean(intensity_image[regionmask])
468
+
413
469
 
414
470
  def intensity_center_of_mass_displacement(regionmask, intensity_image):
471
+ """
472
+ Computes the displacement between the geometric centroid and the
473
+ intensity-weighted center of mass of a region.
474
+
475
+ Parameters
476
+ ----------
477
+ regionmask : ndarray
478
+ A binary mask (2D array) where nonzero values indicate the region of interest.
479
+ intensity_image : ndarray
480
+ A 2D array of the same shape as `regionmask`, representing the intensity
481
+ values associated with the region.
482
+
483
+ Returns
484
+ -------
485
+ distance : float
486
+ Euclidean distance between the geometric centroid and the intensity-weighted center of mass.
487
+ direction_arctan : float
488
+ Angle (in degrees) of displacement from the geometric centroid to the intensity-weighted center of mass,
489
+ computed using `arctan2(delta_y, delta_x)`.
490
+ delta_x : float
491
+ Difference in x-coordinates (intensity-weighted centroid - geometric centroid).
492
+ delta_y : float
493
+ Difference in y-coordinates (intensity-weighted centroid - geometric centroid).
494
+
495
+ Notes
496
+ -----
497
+ - If the `intensity_image` contains NaN values, it is first processed using `interpolate_nan()`.
498
+ - Negative intensity values are set to zero to prevent misbehavior in center of mass calculation.
499
+ - If the intensity image is entirely zero, all outputs are `NaN`.
500
+
501
+ """
415
502
 
416
- """
417
- Computes the displacement between the geometric centroid and the
418
- intensity-weighted center of mass of a region.
419
-
420
- Parameters
421
- ----------
422
- regionmask : ndarray
423
- A binary mask (2D array) where nonzero values indicate the region of interest.
424
- intensity_image : ndarray
425
- A 2D array of the same shape as `regionmask`, representing the intensity
426
- values associated with the region.
427
-
428
- Returns
429
- -------
430
- distance : float
431
- Euclidean distance between the geometric centroid and the intensity-weighted center of mass.
432
- direction_arctan : float
433
- Angle (in degrees) of displacement from the geometric centroid to the intensity-weighted center of mass,
434
- computed using `arctan2(delta_y, delta_x)`.
435
- delta_x : float
436
- Difference in x-coordinates (intensity-weighted centroid - geometric centroid).
437
- delta_y : float
438
- Difference in y-coordinates (intensity-weighted centroid - geometric centroid).
439
-
440
- Notes
441
- -----
442
- - If the `intensity_image` contains NaN values, it is first processed using `interpolate_nan()`.
443
- - Negative intensity values are set to zero to prevent misbehavior in center of mass calculation.
444
- - If the intensity image is entirely zero, all outputs are `NaN`.
445
-
446
- """
447
-
448
- if np.any(intensity_image!=intensity_image):
449
- intensity_image = interpolate_nan(intensity_image.copy())
450
-
451
- if not np.all(intensity_image.flatten()==0):
452
-
453
- y, x = np.mgrid[:regionmask.shape[0], :regionmask.shape[1]]
454
- xtemp = x.copy()
455
- ytemp = y.copy()
456
-
457
- intensity_image[intensity_image<=0.] = 0. #important to clip as negative intensities misbehave with center of mass
458
- intensity_weighted_center = center_of_mass(intensity_image*regionmask, regionmask, 1)
459
- centroid_x = intensity_weighted_center[1]
460
- centroid_y = intensity_weighted_center[0]
461
-
462
- geometric_centroid_x = np.sum(xtemp * regionmask) / np.sum(regionmask)
463
- geometric_centroid_y = np.sum(ytemp * regionmask) / np.sum(regionmask)
464
- distance = np.sqrt((geometric_centroid_y - centroid_y)**2 + (geometric_centroid_x - centroid_x)**2)
465
-
466
- delta_x = geometric_centroid_x - centroid_x
467
- delta_y = geometric_centroid_y - centroid_y
468
- direction_arctan = np.arctan2(delta_y, delta_x) * 180 / np.pi
469
-
470
- return distance, direction_arctan, centroid_x - geometric_centroid_x, centroid_y - geometric_centroid_y
471
-
472
- else:
473
- return np.nan, np.nan, np.nan, np.nan
503
+ if np.any(intensity_image != intensity_image):
504
+ intensity_image = interpolate_nan(intensity_image.copy())
505
+
506
+ if not np.all(intensity_image.flatten() == 0):
507
+
508
+ y, x = np.mgrid[: regionmask.shape[0], : regionmask.shape[1]]
509
+ xtemp = x.copy()
510
+ ytemp = y.copy()
511
+
512
+ intensity_image[intensity_image <= 0.0] = (
513
+ 0.0 # important to clip as negative intensities misbehave with center of mass
514
+ )
515
+ intensity_weighted_center = center_of_mass(
516
+ intensity_image * regionmask, regionmask, 1
517
+ )
518
+ centroid_x = intensity_weighted_center[1]
519
+ centroid_y = intensity_weighted_center[0]
520
+
521
+ geometric_centroid_x = np.sum(xtemp * regionmask) / np.sum(regionmask)
522
+ geometric_centroid_y = np.sum(ytemp * regionmask) / np.sum(regionmask)
523
+ distance = np.sqrt(
524
+ (geometric_centroid_y - centroid_y) ** 2
525
+ + (geometric_centroid_x - centroid_x) ** 2
526
+ )
527
+
528
+ delta_x = geometric_centroid_x - centroid_x
529
+ delta_y = geometric_centroid_y - centroid_y
530
+ direction_arctan = np.arctan2(delta_y, delta_x) * 180 / np.pi
531
+
532
+ return (
533
+ distance,
534
+ direction_arctan,
535
+ centroid_x - geometric_centroid_x,
536
+ centroid_y - geometric_centroid_y,
537
+ )
538
+
539
+ else:
540
+ return np.nan, np.nan, np.nan, np.nan
474
541
 
475
542
 
476
543
  def intensity_center_of_mass_displacement_edge(regionmask, intensity_image):
477
544
 
478
- if np.any(intensity_image!=intensity_image):
479
- intensity_image = interpolate_nan(intensity_image.copy())
480
-
481
- edge_mask = contour_of_instance_segmentation(regionmask, 3)
482
-
483
- if not np.all(intensity_image.flatten()==0) and np.sum(edge_mask)>0:
484
-
485
- y, x = np.mgrid[:edge_mask.shape[0], :edge_mask.shape[1]]
486
- xtemp = x.copy()
487
- ytemp = y.copy()
488
-
489
- intensity_image[intensity_image<=0.] = 0. #important to clip as negative intensities misbehave with center of mass
490
- intensity_weighted_center = center_of_mass(intensity_image*edge_mask, edge_mask, 1)
491
- centroid_x = intensity_weighted_center[1]
492
- centroid_y = intensity_weighted_center[0]
493
-
494
- #centroid_x = np.sum(xtemp * intensity_image) / np.sum(intensity_image)
495
- geometric_centroid_x = np.sum(xtemp * regionmask) / np.sum(regionmask)
496
- geometric_centroid_y = np.sum(ytemp * regionmask) / np.sum(regionmask)
497
-
498
- distance = np.sqrt((geometric_centroid_y - centroid_y)**2 + (geometric_centroid_x - centroid_x)**2)
499
-
500
- delta_x = geometric_centroid_x - centroid_x
501
- delta_y = geometric_centroid_y - centroid_y
502
- direction_arctan = np.arctan2(delta_y, delta_x) * 180 / np.pi
503
-
504
- return distance, direction_arctan, centroid_x - geometric_centroid_x, centroid_y - geometric_centroid_y
505
- else:
506
- return np.nan, np.nan, np.nan, np.nan
545
+ if np.any(intensity_image != intensity_image):
546
+ intensity_image = interpolate_nan(intensity_image.copy())
547
+
548
+ edge_mask = contour_of_instance_segmentation(regionmask, 3)
549
+
550
+ if not np.all(intensity_image.flatten() == 0) and np.sum(edge_mask) > 0:
551
+
552
+ y, x = np.mgrid[: edge_mask.shape[0], : edge_mask.shape[1]]
553
+ xtemp = x.copy()
554
+ ytemp = y.copy()
555
+
556
+ intensity_image[intensity_image <= 0.0] = (
557
+ 0.0 # important to clip as negative intensities misbehave with center of mass
558
+ )
559
+ intensity_weighted_center = center_of_mass(
560
+ intensity_image * edge_mask, edge_mask, 1
561
+ )
562
+ centroid_x = intensity_weighted_center[1]
563
+ centroid_y = intensity_weighted_center[0]
564
+
565
+ # centroid_x = np.sum(xtemp * intensity_image) / np.sum(intensity_image)
566
+ geometric_centroid_x = np.sum(xtemp * regionmask) / np.sum(regionmask)
567
+ geometric_centroid_y = np.sum(ytemp * regionmask) / np.sum(regionmask)
568
+
569
+ distance = np.sqrt(
570
+ (geometric_centroid_y - centroid_y) ** 2
571
+ + (geometric_centroid_x - centroid_x) ** 2
572
+ )
573
+
574
+ delta_x = geometric_centroid_x - centroid_x
575
+ delta_y = geometric_centroid_y - centroid_y
576
+ direction_arctan = np.arctan2(delta_y, delta_x) * 180 / np.pi
577
+
578
+ return (
579
+ distance,
580
+ direction_arctan,
581
+ centroid_x - geometric_centroid_x,
582
+ centroid_y - geometric_centroid_y,
583
+ )
584
+ else:
585
+ return np.nan, np.nan, np.nan, np.nan
507
586
 
508
587
 
509
588
  def intensity_radial_gradient(regionmask, intensity_image):
589
+ """
590
+ Determines whether the intensity follows a radial gradient from the center to the edge of the cell.
591
+
592
+ The function fits a linear model to the intensity values as a function of distance from the center
593
+ (computed via the Euclidean distance transform). The slope of the fitted line indicates whether
594
+ the intensity is higher at the center or at the edges.
595
+
596
+ Parameters
597
+ ----------
598
+ regionmask : ndarray
599
+ A binary mask (2D array) where nonzero values define the region of interest.
600
+ intensity_image : ndarray
601
+ A 2D array of the same shape as `regionmask`, representing the intensity
602
+ values associated with the region.
603
+
604
+ Returns
605
+ -------
606
+ slope : float
607
+ Slope of the fitted linear model.
608
+
609
+ - If `slope > 0`: Intensity increases towards the edge.
610
+ - If `slope < 0`: Intensity is higher at the center.
611
+
612
+ intercept : float
613
+ Intercept of the fitted linear model.
614
+ r2 : float
615
+ Coefficient of determination (R²), indicating how well the linear model fits the intensity profile.
616
+
617
+ Notes
618
+ -----
619
+ - If the `intensity_image` contains NaN values, they are interpolated using `interpolate_nan()`.
620
+ - The Euclidean distance transform (`distance_transform_edt`) is used to compute the distance
621
+ of each pixel from the edge.
622
+ - The x-values for the linear fit are reversed so that the origin is at the center.
623
+ - A warning suppression is applied to ignore messages about poorly conditioned polynomial fits.
624
+
625
+ """
626
+
627
+ if np.any(intensity_image != intensity_image):
628
+ intensity_image = interpolate_nan(intensity_image.copy())
629
+
630
+ # try:
631
+ warnings.filterwarnings("ignore", message="Polyfit may be poorly conditioned")
632
+
633
+ # intensities
634
+ y = intensity_image[regionmask].flatten()
635
+
636
+ # distance to edge
637
+ x = distance_transform_edt(regionmask.copy())
638
+ x = x[regionmask].flatten()
639
+ x = max(x) - x # origin at center of cells
640
+
641
+ params = np.polyfit(x, y, 1)
642
+ line = np.poly1d(params)
643
+ # coef > 0 --> more signal at edge than center, coef < 0 --> more signal at center than edge
644
+
645
+ r2 = r2_score(y, line(x))
510
646
 
511
- """
512
- Determines whether the intensity follows a radial gradient from the center to the edge of the cell.
513
-
514
- The function fits a linear model to the intensity values as a function of distance from the center
515
- (computed via the Euclidean distance transform). The slope of the fitted line indicates whether
516
- the intensity is higher at the center or at the edges.
517
-
518
- Parameters
519
- ----------
520
- regionmask : ndarray
521
- A binary mask (2D array) where nonzero values define the region of interest.
522
- intensity_image : ndarray
523
- A 2D array of the same shape as `regionmask`, representing the intensity
524
- values associated with the region.
525
-
526
- Returns
527
- -------
528
- slope : float
529
- Slope of the fitted linear model.
530
-
531
- - If `slope > 0`: Intensity increases towards the edge.
532
- - If `slope < 0`: Intensity is higher at the center.
533
-
534
- intercept : float
535
- Intercept of the fitted linear model.
536
- r2 : float
537
- Coefficient of determination (R²), indicating how well the linear model fits the intensity profile.
538
-
539
- Notes
540
- -----
541
- - If the `intensity_image` contains NaN values, they are interpolated using `interpolate_nan()`.
542
- - The Euclidean distance transform (`distance_transform_edt`) is used to compute the distance
543
- of each pixel from the edge.
544
- - The x-values for the linear fit are reversed so that the origin is at the center.
545
- - A warning suppression is applied to ignore messages about poorly conditioned polynomial fits.
546
-
547
- """
548
-
549
- if np.any(intensity_image!=intensity_image):
550
- intensity_image = interpolate_nan(intensity_image.copy())
551
-
552
- # try:
553
- warnings.filterwarnings('ignore', message="Polyfit may be poorly conditioned")
554
-
555
- # intensities
556
- y = intensity_image[regionmask].flatten()
557
-
558
- # distance to edge
559
- x = distance_transform_edt(regionmask.copy())
560
- x = x[regionmask].flatten()
561
- x = max(x) - x # origin at center of cells
562
-
563
- params = np.polyfit(x, y, 1)
564
- line = np.poly1d(params)
565
- # coef > 0 --> more signal at edge than center, coef < 0 --> more signal at center than edge
566
-
567
- r2 = r2_score(y, line(x))
568
-
569
- return line.coefficients[0], line.coefficients[1], r2
647
+ return line.coefficients[0], line.coefficients[1], r2