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
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
# Bad practice:
|
|
33
|
+
def intensity2(regionmask, intensity_image):
|
|
34
|
+
pass
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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(
|
|
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 = (
|
|
306
|
+
subregion = (
|
|
307
|
+
intensity_image < 0.95
|
|
308
|
+
) * regionmask # Select dark regions within the mask
|
|
291
309
|
|
|
292
|
-
masked_intensity = intensity_image[
|
|
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
|
-
|
|
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 = (
|
|
323
|
+
subregion = (
|
|
324
|
+
intensity_image < 0.90
|
|
325
|
+
) * regionmask # Select dark regions within the mask
|
|
301
326
|
|
|
302
|
-
masked_intensity = intensity_image[
|
|
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(
|
|
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
|
-
|
|
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(
|
|
372
|
+
def mean_dark_intensity_eight_five_fillhole_false(
|
|
373
|
+
regionmask, intensity_image, target_channel="adhesion_channel"
|
|
374
|
+
):
|
|
343
375
|
|
|
344
|
-
subregion = (
|
|
376
|
+
subregion = (
|
|
377
|
+
intensity_image < 0.85
|
|
378
|
+
) * regionmask # Select dark regions within the mask
|
|
345
379
|
|
|
346
|
-
masked_intensity = intensity_image[
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
428
|
+
return np.nanpercentile(intensity_image[regionmask], 99)
|
|
429
|
+
|
|
382
430
|
|
|
383
431
|
def intensity_percentile_ninety_five(regionmask, intensity_image):
|
|
384
|
-
|
|
432
|
+
return np.nanpercentile(intensity_image[regionmask], 95)
|
|
433
|
+
|
|
385
434
|
|
|
386
435
|
def intensity_percentile_ninety(regionmask, intensity_image):
|
|
387
|
-
|
|
436
|
+
return np.nanpercentile(intensity_image[regionmask], 90)
|
|
437
|
+
|
|
388
438
|
|
|
389
439
|
def intensity_percentile_seventy_five(regionmask, intensity_image):
|
|
390
|
-
|
|
440
|
+
return np.nanpercentile(intensity_image[regionmask], 75)
|
|
441
|
+
|
|
391
442
|
|
|
392
443
|
def intensity_percentile_fifty(regionmask, intensity_image):
|
|
393
|
-
|
|
444
|
+
return np.nanpercentile(intensity_image[regionmask], 50)
|
|
445
|
+
|
|
394
446
|
|
|
395
447
|
def intensity_percentile_twenty_five(regionmask, intensity_image):
|
|
396
|
-
|
|
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
|
-
|
|
455
|
+
return np.nanstd(intensity_image[regionmask])
|
|
402
456
|
|
|
403
457
|
|
|
404
458
|
def intensity_median(regionmask, intensity_image):
|
|
405
|
-
|
|
459
|
+
return np.nanmedian(intensity_image[regionmask])
|
|
460
|
+
|
|
406
461
|
|
|
407
462
|
def intensity_nanmean(regionmask, intensity_image):
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|