celldetective 1.1.1.post4__py3-none-any.whl → 1.2.0__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 +2 -1
- celldetective/extra_properties.py +62 -34
- celldetective/gui/__init__.py +1 -0
- celldetective/gui/analyze_block.py +2 -1
- celldetective/gui/classifier_widget.py +8 -7
- celldetective/gui/control_panel.py +50 -6
- celldetective/gui/layouts.py +5 -4
- celldetective/gui/neighborhood_options.py +10 -8
- celldetective/gui/plot_signals_ui.py +39 -11
- celldetective/gui/process_block.py +413 -95
- celldetective/gui/retrain_segmentation_model_options.py +17 -4
- celldetective/gui/retrain_signal_model_options.py +106 -6
- celldetective/gui/signal_annotator.py +25 -5
- celldetective/gui/signal_annotator2.py +2708 -0
- celldetective/gui/signal_annotator_options.py +3 -1
- celldetective/gui/survival_ui.py +15 -6
- celldetective/gui/tableUI.py +235 -39
- celldetective/io.py +537 -421
- celldetective/measure.py +919 -969
- celldetective/models/pair_signal_detection/blank +0 -0
- celldetective/neighborhood.py +426 -354
- celldetective/relative_measurements.py +648 -0
- celldetective/scripts/analyze_signals.py +1 -1
- celldetective/scripts/measure_cells.py +28 -8
- celldetective/scripts/measure_relative.py +103 -0
- celldetective/scripts/segment_cells.py +5 -5
- celldetective/scripts/track_cells.py +4 -1
- celldetective/scripts/train_segmentation_model.py +23 -18
- celldetective/scripts/train_signal_model.py +33 -0
- celldetective/signals.py +402 -8
- celldetective/tracking.py +8 -2
- celldetective/utils.py +93 -0
- {celldetective-1.1.1.post4.dist-info → celldetective-1.2.0.dist-info}/METADATA +8 -8
- {celldetective-1.1.1.post4.dist-info → celldetective-1.2.0.dist-info}/RECORD +38 -34
- {celldetective-1.1.1.post4.dist-info → celldetective-1.2.0.dist-info}/WHEEL +1 -1
- {celldetective-1.1.1.post4.dist-info → celldetective-1.2.0.dist-info}/LICENSE +0 -0
- {celldetective-1.1.1.post4.dist-info → celldetective-1.2.0.dist-info}/entry_points.txt +0 -0
- {celldetective-1.1.1.post4.dist-info → celldetective-1.2.0.dist-info}/top_level.txt +0 -0
celldetective/measure.py
CHANGED
|
@@ -26,7 +26,7 @@ from skimage.draw import disk as dsk
|
|
|
26
26
|
|
|
27
27
|
from celldetective.filters import std_filter, gauss_filter
|
|
28
28
|
from celldetective.utils import rename_intensity_column, create_patch_mask, remove_redundant_features, \
|
|
29
|
-
|
|
29
|
+
remove_trajectory_measurements, contour_of_instance_segmentation
|
|
30
30
|
from celldetective.preprocessing import field_correction
|
|
31
31
|
import celldetective.extra_properties as extra_properties
|
|
32
32
|
from celldetective.extra_properties import *
|
|
@@ -37,1002 +37,952 @@ from skimage.morphology import disk
|
|
|
37
37
|
abs_path = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], 'celldetective'])
|
|
38
38
|
|
|
39
39
|
def measure(stack=None, labels=None, trajectories=None, channel_names=None,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
40
|
+
features=None, intensity_measurement_radii=None, isotropic_operations=['mean'], border_distances=None,
|
|
41
|
+
haralick_options=None, column_labels={'track': "TRACK_ID", 'time': 'FRAME', 'x': 'POSITION_X', 'y': 'POSITION_Y'}, clear_previous=False):
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
Perform measurements on a stack of images or labels.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
stack : numpy array, optional
|
|
50
|
+
Stack of images with shape (T, Y, X, C), where T is the number of frames, Y and X are the spatial dimensions,
|
|
51
|
+
and C is the number of channels. Default is None.
|
|
52
|
+
labels : numpy array, optional
|
|
53
|
+
Label stack with shape (T, Y, X) representing cell segmentations. Default is None.
|
|
54
|
+
trajectories : pandas DataFrame, optional
|
|
55
|
+
DataFrame of cell trajectories with columns specified in `column_labels`. Default is None.
|
|
56
|
+
channel_names : list, optional
|
|
57
|
+
List of channel names corresponding to the image stack. Default is None.
|
|
58
|
+
features : list, optional
|
|
59
|
+
List of features to measure using the `measure_features` function. Default is None.
|
|
60
|
+
intensity_measurement_radii : int, float, or list, optional
|
|
61
|
+
Radius or list of radii specifying the size of the isotropic measurement area for intensity measurements.
|
|
62
|
+
If a single value is provided, a circular measurement area is used. If a list of values is provided, multiple
|
|
63
|
+
measurements are performed using ring-shaped measurement areas. Default is None.
|
|
64
|
+
isotropic_operations : list, optional
|
|
65
|
+
List of operations to perform on the isotropic intensity values. Default is ['mean'].
|
|
66
|
+
border_distances : int, float, or list, optional
|
|
67
|
+
Distance or list of distances specifying the size of the border region for intensity measurements.
|
|
68
|
+
If a single value is provided, measurements are performed at a fixed distance from the cell borders.
|
|
69
|
+
If a list of values is provided, measurements are performed at multiple border distances. Default is None.
|
|
70
|
+
haralick_options : dict, optional
|
|
71
|
+
Dictionary of options for Haralick feature measurements. Default is None.
|
|
72
|
+
column_labels : dict, optional
|
|
73
|
+
Dictionary containing the column labels for the DataFrame. Default is {'track': "TRACK_ID",
|
|
74
|
+
'time': 'FRAME', 'x': 'POSITION_X', 'y': 'POSITION_Y'}.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
pandas DataFrame
|
|
79
|
+
DataFrame containing the measured features and intensities.
|
|
80
|
+
|
|
81
|
+
Notes
|
|
82
|
+
-----
|
|
83
|
+
This function performs measurements on a stack of images or labels. If both `stack` and `labels` are provided,
|
|
84
|
+
measurements are performed on each frame of the stack. The measurements include isotropic intensity values, computed
|
|
85
|
+
using the `measure_isotropic_intensity` function, and additional features, computed using the `measure_features` function.
|
|
86
|
+
The intensity measurements are performed at the positions specified in the `trajectories` DataFrame, using the
|
|
87
|
+
specified `intensity_measurement_radii` and `border_distances`. The resulting measurements are combined into a single
|
|
88
|
+
DataFrame and returned.
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
>>> stack = np.random.rand(10, 100, 100, 3)
|
|
93
|
+
>>> labels = np.random.randint(0, 2, (10, 100, 100))
|
|
94
|
+
>>> trajectories = pd.DataFrame({'TRACK_ID': [1, 2, 3], 'FRAME': [1, 1, 1],
|
|
95
|
+
... 'POSITION_X': [10, 20, 30], 'POSITION_Y': [15, 25, 35]})
|
|
96
|
+
>>> channel_names = ['channel1', 'channel2', 'channel3']
|
|
97
|
+
>>> features = ['area', 'intensity_mean']
|
|
98
|
+
>>> intensity_measurement_radii = [5, 10]
|
|
99
|
+
>>> border_distances = 2
|
|
100
|
+
>>> measurements = measure(stack=stack, labels=labels, trajectories=trajectories, channel_names=channel_names,
|
|
101
|
+
... features=features, intensity_measurement_radii=intensity_measurement_radii,
|
|
102
|
+
... border_distances=border_distances)
|
|
103
|
+
# Perform measurements on the stack, labels, and trajectories, computing isotropic intensities and additional features.
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
do_iso_intensities = True
|
|
109
|
+
do_features = True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Check that conditions are satisfied to perform measurements
|
|
113
|
+
assert (labels is not None) or (stack is not None),'Please pass a stack and/or labels... Abort.'
|
|
114
|
+
if (labels is not None)*(stack is not None):
|
|
115
|
+
assert labels.shape==stack.shape[:-1],f"Shape mismatch between the stack of shape {stack.shape} and the segmentation {labels.shape}..."
|
|
116
|
+
|
|
117
|
+
# Condition to compute features
|
|
118
|
+
if labels is None:
|
|
119
|
+
do_features = False
|
|
120
|
+
nbr_frames = len(stack)
|
|
121
|
+
print('No labels were provided... Features will not be computed...')
|
|
122
|
+
else:
|
|
123
|
+
nbr_frames = len(labels)
|
|
124
|
+
|
|
125
|
+
# Condition to compute isotropic intensities
|
|
126
|
+
if (stack is None) or (trajectories is None) or (intensity_measurement_radii is None):
|
|
127
|
+
do_iso_intensities = False
|
|
128
|
+
print('Either no image, no positions or no radii were provided... Isotropic intensities will not be computed...')
|
|
129
|
+
|
|
130
|
+
# Compensate for non provided channel names
|
|
131
|
+
if (stack is not None)*(channel_names is None):
|
|
132
|
+
nbr_channels = stack.shape[-1]
|
|
133
|
+
channel_names = [f'intensity-{k}' for k in range(nbr_channels)]
|
|
134
|
+
|
|
135
|
+
if isinstance(intensity_measurement_radii, int) or isinstance(intensity_measurement_radii, float):
|
|
136
|
+
intensity_measurement_radii = [intensity_measurement_radii]
|
|
137
|
+
|
|
138
|
+
if isinstance(border_distances, int) or isinstance(border_distances, float):
|
|
139
|
+
border_distances = [border_distances]
|
|
140
|
+
|
|
141
|
+
if features is not None:
|
|
142
|
+
features = remove_redundant_features(features, trajectories.columns,
|
|
143
|
+
channel_names=channel_names)
|
|
144
|
+
|
|
145
|
+
if features is None:
|
|
146
|
+
features = []
|
|
147
|
+
|
|
148
|
+
# Prep for the case where no trajectory is provided but still want to measure isotropic intensities...
|
|
149
|
+
if (trajectories is None):
|
|
150
|
+
do_features = True
|
|
151
|
+
features += ['centroid']
|
|
152
|
+
else:
|
|
153
|
+
if clear_previous:
|
|
154
|
+
trajectories = remove_trajectory_measurements(trajectories, column_labels)
|
|
155
|
+
|
|
156
|
+
timestep_dataframes = []
|
|
157
|
+
|
|
158
|
+
for t in tqdm(range(nbr_frames),desc='frame'):
|
|
159
|
+
|
|
160
|
+
if stack is not None:
|
|
161
|
+
img = stack[t]
|
|
162
|
+
else:
|
|
163
|
+
img = None
|
|
164
|
+
if labels is not None:
|
|
165
|
+
lbl = labels[t]
|
|
166
|
+
else:
|
|
167
|
+
lbl = None
|
|
168
|
+
|
|
169
|
+
if trajectories is not None:
|
|
170
|
+
positions_at_t = trajectories.loc[trajectories[column_labels['time']]==t].copy()
|
|
171
|
+
|
|
172
|
+
if do_features:
|
|
173
|
+
feature_table = measure_features(img, lbl, features = features, border_dist=border_distances,
|
|
174
|
+
channels=channel_names, haralick_options=haralick_options, verbose=False)
|
|
175
|
+
if trajectories is None:
|
|
176
|
+
# Use the centroids as estimate for the location of the cells, to be passed to the measure_isotropic_intensity function.
|
|
177
|
+
positions_at_t = feature_table[['centroid-1', 'centroid-0','class_id']].copy()
|
|
178
|
+
positions_at_t['ID'] = np.arange(len(positions_at_t)) # temporary ID for the cells, that will be reset at the end since they are not tracked
|
|
179
|
+
positions_at_t.rename(columns={'centroid-1': 'POSITION_X', 'centroid-0': 'POSITION_Y'},inplace=True)
|
|
180
|
+
positions_at_t['FRAME'] = int(t)
|
|
181
|
+
column_labels = {'track': "ID", 'time': column_labels['time'], 'x': column_labels['x'], 'y': column_labels['y']}
|
|
182
|
+
|
|
183
|
+
center_of_mass_x_cols = [c for c in list(positions_at_t.columns) if c.endswith('centre_of_mass_x')]
|
|
184
|
+
center_of_mass_y_cols = [c for c in list(positions_at_t.columns) if c.endswith('centre_of_mass_y')]
|
|
185
|
+
for c in center_of_mass_x_cols:
|
|
186
|
+
positions_at_t.loc[:,c.replace('_x','_POSITION_X')] = positions_at_t[c] + positions_at_t['POSITION_X']
|
|
187
|
+
for c in center_of_mass_y_cols:
|
|
188
|
+
positions_at_t.loc[:,c.replace('_y','_POSITION_Y')] = positions_at_t[c] + positions_at_t['POSITION_Y']
|
|
189
|
+
positions_at_t = positions_at_t.drop(columns = center_of_mass_x_cols+center_of_mass_y_cols)
|
|
190
|
+
|
|
191
|
+
# Isotropic measurements (circle, ring)
|
|
192
|
+
if do_iso_intensities:
|
|
193
|
+
iso_table = measure_isotropic_intensity(positions_at_t, img, channels=channel_names, intensity_measurement_radii=intensity_measurement_radii,
|
|
194
|
+
column_labels=column_labels, operations=isotropic_operations, verbose=False)
|
|
195
|
+
|
|
196
|
+
if do_iso_intensities*do_features:
|
|
197
|
+
measurements_at_t = iso_table.merge(feature_table, how='outer', on='class_id')
|
|
198
|
+
elif do_iso_intensities*(not do_features):
|
|
199
|
+
measurements_at_t = iso_table
|
|
200
|
+
elif do_features*(trajectories is not None):
|
|
201
|
+
measurements_at_t = positions_at_t.merge(feature_table, how='outer', on='class_id')
|
|
202
|
+
elif do_features*(trajectories is None):
|
|
203
|
+
measurements_at_t = positions_at_t
|
|
204
|
+
|
|
205
|
+
timestep_dataframes.append(measurements_at_t)
|
|
206
|
+
|
|
207
|
+
measurements = pd.concat(timestep_dataframes)
|
|
208
|
+
if trajectories is not None:
|
|
209
|
+
measurements = measurements.sort_values(by=[column_labels['track'],column_labels['time']])
|
|
210
|
+
measurements = measurements.dropna(subset=[column_labels['track']])
|
|
211
|
+
else:
|
|
212
|
+
measurements['ID'] = np.arange(len(df))
|
|
213
|
+
|
|
214
|
+
measurements = measurements.reset_index(drop=True)
|
|
215
|
+
|
|
216
|
+
return measurements
|
|
209
217
|
|
|
210
218
|
def write_first_detection_class(tab, column_labels={'track': "TRACK_ID", 'time': 'FRAME', 'x': 'POSITION_X', 'y': 'POSITION_Y'}):
|
|
211
219
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def contour_of_instance_segmentation(label, distance):
|
|
234
|
-
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
Generate an instance mask containing the contour of the segmented objects.
|
|
238
|
-
|
|
239
|
-
Parameters
|
|
240
|
-
----------
|
|
241
|
-
label : ndarray
|
|
242
|
-
The instance segmentation labels.
|
|
243
|
-
distance : int, float, list, or tuple
|
|
244
|
-
The distance or range of distances from the edge of each instance to include in the contour.
|
|
245
|
-
If a single value is provided, it represents the maximum distance. If a tuple or list is provided,
|
|
246
|
-
it represents the minimum and maximum distances.
|
|
247
|
-
|
|
248
|
-
Returns
|
|
249
|
-
-------
|
|
250
|
-
border_label : ndarray
|
|
251
|
-
An instance mask containing the contour of the segmented objects.
|
|
252
|
-
|
|
253
|
-
Notes
|
|
254
|
-
-----
|
|
255
|
-
This function generates an instance mask representing the contour of the segmented instances in the label image.
|
|
256
|
-
It use the distance_transform_edt function from the scipy.ndimage module to compute the Euclidean distance transform.
|
|
257
|
-
The contour is defined based on the specified distance(s) from the edge of each instance.
|
|
258
|
-
The resulting mask, `border_label`, contains the contour regions, while the interior regions are set to zero.
|
|
259
|
-
|
|
260
|
-
Examples
|
|
261
|
-
--------
|
|
262
|
-
>>> border_label = contour_of_instance_segmentation(label, distance=3)
|
|
263
|
-
# Generate a binary mask containing the contour of the segmented instances with a maximum distance of 3 pixels.
|
|
264
|
-
|
|
265
|
-
"""
|
|
266
|
-
if isinstance(distance,(list,tuple)) or distance >= 0 :
|
|
267
|
-
|
|
268
|
-
edt = distance_transform_edt(label)
|
|
269
|
-
|
|
270
|
-
if isinstance(distance, list) or isinstance(distance, tuple):
|
|
271
|
-
min_distance = distance[0]; max_distance = distance[1]
|
|
272
|
-
|
|
273
|
-
elif isinstance(distance, (int, float)):
|
|
274
|
-
min_distance = 0
|
|
275
|
-
max_distance = distance
|
|
276
|
-
|
|
277
|
-
thresholded = (edt <= max_distance) * (edt > min_distance)
|
|
278
|
-
border_label = np.copy(label)
|
|
279
|
-
border_label[np.where(thresholded == 0)] = 0
|
|
280
|
-
|
|
281
|
-
else:
|
|
282
|
-
size = (2*abs(int(distance))+1, 2*abs(int(distance))+1)
|
|
283
|
-
dilated_image = ndimage.grey_dilation(label, footprint=disk(int(abs(distance)))) #size=size,
|
|
284
|
-
border_label=np.copy(dilated_image)
|
|
285
|
-
matching_cells = np.logical_and(dilated_image != 0, label == dilated_image)
|
|
286
|
-
border_label[np.where(matching_cells == True)] = 0
|
|
287
|
-
border_label[label!=0] = 0.
|
|
288
|
-
|
|
289
|
-
return border_label
|
|
220
|
+
tab = tab.sort_values(by=[column_labels['track'],column_labels['time']])
|
|
221
|
+
if 'area' in tab.columns:
|
|
222
|
+
for tid,track_group in tab.groupby(column_labels['track']):
|
|
223
|
+
indices = track_group.index
|
|
224
|
+
area = track_group['area'].values
|
|
225
|
+
timeline = track_group[column_labels['time']].values
|
|
226
|
+
if np.any(area==area):
|
|
227
|
+
t_first = timeline[area==area][0]
|
|
228
|
+
cclass = 1
|
|
229
|
+
if t_first==0:
|
|
230
|
+
t_first = 0
|
|
231
|
+
cclass = 2
|
|
232
|
+
else:
|
|
233
|
+
t_first = -1
|
|
234
|
+
cclass = 2
|
|
235
|
+
|
|
236
|
+
tab.loc[indices, 'class_firstdetection'] = cclass
|
|
237
|
+
tab.loc[indices, 't_firstdetection'] = t_first
|
|
238
|
+
return tab
|
|
239
|
+
|
|
290
240
|
|
|
291
241
|
def drop_tonal_features(features):
|
|
292
242
|
|
|
293
|
-
|
|
294
|
-
|
|
243
|
+
"""
|
|
244
|
+
Removes features related to intensity from a list of feature names.
|
|
295
245
|
|
|
296
|
-
|
|
297
|
-
|
|
246
|
+
This function iterates over a list of feature names and removes any feature that includes the term 'intensity' in its name.
|
|
247
|
+
The operation is performed in-place, meaning the original list of features is modified directly.
|
|
298
248
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
features : list of str
|
|
252
|
+
A list of feature names from which intensity-related features are to be removed.
|
|
303
253
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
list of str
|
|
257
|
+
The modified list of feature names with intensity-related features removed. Note that this operation modifies the
|
|
258
|
+
input list in-place, so the return value is the same list object with some elements removed.
|
|
309
259
|
|
|
310
|
-
|
|
260
|
+
"""
|
|
311
261
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
262
|
+
feat2 = features[:]
|
|
263
|
+
for f in features:
|
|
264
|
+
if 'intensity' in f:
|
|
265
|
+
feat2.remove(f)
|
|
266
|
+
return feat2
|
|
317
267
|
|
|
318
268
|
def measure_features(img, label, features=['area', 'intensity_mean'], channels=None,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
269
|
+
border_dist=None, haralick_options=None, verbose=True, normalisation_list=None,
|
|
270
|
+
radial_intensity=None,
|
|
271
|
+
radial_channel=None, spot_detection=None):
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
Measure features within segmented regions of an image.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
img : ndarray
|
|
279
|
+
The input image as a NumPy array.
|
|
280
|
+
label : ndarray
|
|
281
|
+
The segmentation labels corresponding to the image regions.
|
|
282
|
+
features : list, optional
|
|
283
|
+
The list of features to measure within the segmented regions. The default is ['area', 'intensity_mean'].
|
|
284
|
+
channels : list, optional
|
|
285
|
+
The list of channel names in the image. The default is ["brightfield_channel", "dead_nuclei_channel", "live_nuclei_channel"].
|
|
286
|
+
border_dist : int, float, or list, optional
|
|
287
|
+
The distance(s) in pixels from the edge of each segmented region to measure features. The default is None.
|
|
288
|
+
haralick_options : dict, optional
|
|
289
|
+
The options for computing Haralick features. The default is None.
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
df_props : DataFrame
|
|
294
|
+
A pandas DataFrame containing the measured features for each segmented region.
|
|
295
|
+
|
|
296
|
+
Notes
|
|
297
|
+
-----
|
|
298
|
+
This function measures features within segmented regions of an image.
|
|
299
|
+
It utilizes the regionprops_table function from the skimage.measure module for feature extraction.
|
|
300
|
+
The features to measure can be specified using the 'features' parameter.
|
|
301
|
+
Optional parameters such as 'channels' and 'border_dist' allow for additional measurements.
|
|
302
|
+
If provided, Haralick features can be computed using the 'haralick_options' parameter.
|
|
303
|
+
The results are returned as a pandas DataFrame.
|
|
304
|
+
|
|
305
|
+
Examples
|
|
306
|
+
--------
|
|
307
|
+
>>> df_props = measure_features(img, label, features=['area', 'intensity_mean'], channels=["brightfield_channel", "dead_nuclei_channel", "live_nuclei_channel"])
|
|
308
|
+
# Measure area and mean intensity within segmented regions of the image.
|
|
309
|
+
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
if features is None:
|
|
313
|
+
features = []
|
|
314
|
+
|
|
315
|
+
# Add label to have identity of mask
|
|
316
|
+
if 'label' not in features:
|
|
317
|
+
features.append('label')
|
|
318
|
+
|
|
319
|
+
if img is None:
|
|
320
|
+
if verbose:
|
|
321
|
+
print('No image was provided... Skip intensity measurements.')
|
|
322
|
+
border_dist = None;
|
|
323
|
+
haralick_options = None;
|
|
324
|
+
features = drop_tonal_features(features)
|
|
325
|
+
if img is not None:
|
|
326
|
+
if img.ndim == 2:
|
|
327
|
+
img = img[:, :, np.newaxis]
|
|
328
|
+
if channels is None:
|
|
329
|
+
channels = [f'intensity-{k}' for k in range(img.shape[-1])]
|
|
330
|
+
if (channels is not None) * (img.ndim == 3):
|
|
331
|
+
assert len(channels) == img.shape[
|
|
332
|
+
-1], "Mismatch between the provided channel names and the shape of the image"
|
|
333
|
+
|
|
334
|
+
if spot_detection is not None:
|
|
335
|
+
for index, channel in enumerate(channels):
|
|
336
|
+
if channel == spot_detection['channel']:
|
|
337
|
+
ind = index
|
|
338
|
+
blobs = blob_detection(img[:, :, ind], label, diameter=spot_detection['diameter'],
|
|
339
|
+
threshold=spot_detection['threshold'])
|
|
340
|
+
df_spots = pd.DataFrame.from_dict(blobs, orient='index',
|
|
341
|
+
columns=['count', 'spot_mean_intensity']).reset_index()
|
|
342
|
+
# Rename columns
|
|
343
|
+
df_spots.columns = ['label', 'spot_count', 'spot_mean_intensity']
|
|
344
|
+
|
|
345
|
+
if normalisation_list:
|
|
346
|
+
for norm in normalisation_list:
|
|
347
|
+
for index, channel in enumerate(channels):
|
|
348
|
+
if channel == norm['target_channel']:
|
|
349
|
+
ind = index
|
|
350
|
+
if norm['correction_type'] == 'local':
|
|
351
|
+
normalised_image = normalise_by_cell(img[:, :, ind].copy(), label,
|
|
352
|
+
distance=int(norm['distance']), model=norm['model'],
|
|
353
|
+
operation=norm['operation'], clip=norm['clip'])
|
|
354
|
+
img[:, :, ind] = normalised_image
|
|
355
|
+
else:
|
|
356
|
+
corrected_image = field_correction(img[:,:,ind].copy(), threshold_on_std=norm['threshold_on_std'], operation=norm['operation'], model=norm['model'], clip=norm['clip'])
|
|
357
|
+
img[:, :, ind] = corrected_image
|
|
358
|
+
|
|
359
|
+
extra_props = getmembers(extra_properties, isfunction)
|
|
360
|
+
extra_props = [extra_props[i][0] for i in range(len(extra_props))]
|
|
361
|
+
|
|
362
|
+
extra_props_list = []
|
|
363
|
+
feats = features.copy()
|
|
364
|
+
for f in features:
|
|
365
|
+
if f in extra_props:
|
|
366
|
+
feats.remove(f)
|
|
367
|
+
extra_props_list.append(getattr(extra_properties, f))
|
|
368
|
+
if len(extra_props_list) == 0:
|
|
369
|
+
extra_props_list = None
|
|
370
|
+
else:
|
|
371
|
+
extra_props_list = tuple(extra_props_list)
|
|
372
|
+
props = regionprops_table(label, intensity_image=img, properties=feats, extra_properties=extra_props_list)
|
|
373
|
+
df_props = pd.DataFrame(props)
|
|
374
|
+
if spot_detection is not None:
|
|
375
|
+
df_props = df_props.merge(df_spots, how='outer', on='label')
|
|
376
|
+
df_props['spot_count'] = df_props['spot_count'].replace(np.nan, 0).infer_objects(copy=False)
|
|
377
|
+
df_props['spot_mean_intensity'] = df_props['spot_mean_intensity'].replace(np.nan, 0).infer_objects(copy=False)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# if spot_detection is not None:
|
|
382
|
+
# for index, channel in enumerate(channels):
|
|
383
|
+
# if channel == spot_detection['channel']:
|
|
384
|
+
# ind = index
|
|
385
|
+
# blobs = blob_detection(img[:, :, ind], label, diameter=spot_detection['diameter'],
|
|
386
|
+
# threshold=spot_detection['threshold'])
|
|
387
|
+
# df_spots = pd.DataFrame.from_dict(blobs, orient='index', columns=['count', 'spot_mean_intensity']).reset_index()
|
|
388
|
+
# # Rename columns
|
|
389
|
+
# df_spots.columns = ['label', 'spot_count', 'spot_mean_intensity']
|
|
390
|
+
# df_props = df_props.merge(df_spots, how='outer', on='label')
|
|
391
|
+
# df_props['spot_count'] = df_props['spot_count'].replace(np.nan, 0)
|
|
392
|
+
# df_props['spot_mean_intensity'] = df_props['spot_mean_intensity'].replace(np.nan, 0)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
if border_dist is not None:
|
|
396
|
+
# automatically drop all non intensity features
|
|
397
|
+
intensity_features_test = [('intensity' in s and 'centroid' not in s and 'peripheral' not in s) for s in
|
|
398
|
+
features]
|
|
399
|
+
intensity_features = list(np.array(features)[np.array(intensity_features_test)])
|
|
400
|
+
# intensity_extra = [(s in extra_props_list)for s in intensity_features]
|
|
401
|
+
# print(intensity_extra)
|
|
402
|
+
intensity_extra = []
|
|
403
|
+
for s in intensity_features:
|
|
404
|
+
if s in extra_props:
|
|
405
|
+
intensity_extra.append(getattr(extra_properties, s))
|
|
406
|
+
intensity_features.remove(s)
|
|
407
|
+
# print(intensity_features)
|
|
408
|
+
# If no intensity feature was passed still measure mean intensity
|
|
409
|
+
if len(intensity_features) == 0:
|
|
410
|
+
if verbose:
|
|
411
|
+
print('No intensity feature was passed... Adding mean intensity for edge measurement...')
|
|
412
|
+
intensity_features = np.append(intensity_features, 'intensity_mean')
|
|
413
|
+
intensity_features = list(np.append(intensity_features, 'label'))
|
|
414
|
+
|
|
415
|
+
# Remove extra intensity properties from border measurements
|
|
416
|
+
new_intensity_features = intensity_features.copy()
|
|
417
|
+
for int_feat in intensity_features:
|
|
418
|
+
if int_feat in extra_props:
|
|
419
|
+
new_intensity_features.remove(int_feat)
|
|
420
|
+
intensity_features = new_intensity_features
|
|
421
|
+
|
|
422
|
+
if (isinstance(border_dist, int) or isinstance(border_dist, float)):
|
|
423
|
+
border_label = contour_of_instance_segmentation(label, border_dist)
|
|
424
|
+
props_border = regionprops_table(border_label, intensity_image=img, properties=intensity_features)
|
|
425
|
+
df_props_border = pd.DataFrame(props_border)
|
|
426
|
+
for c in df_props_border.columns:
|
|
427
|
+
if 'intensity' in c:
|
|
428
|
+
df_props_border = df_props_border.rename({c: c+f'_edge_{border_dist}px'},axis=1)
|
|
429
|
+
|
|
430
|
+
if isinstance(border_dist, list):
|
|
431
|
+
df_props_border_list = []
|
|
432
|
+
for d in border_dist:
|
|
433
|
+
border_label = contour_of_instance_segmentation(label, d)
|
|
434
|
+
props_border = regionprops_table(border_label, intensity_image=img, properties=intensity_features)
|
|
435
|
+
df_props_border_d = pd.DataFrame(props_border)
|
|
436
|
+
for c in df_props_border_d.columns:
|
|
437
|
+
if 'intensity' in c:
|
|
438
|
+
if '-' in str(d):
|
|
439
|
+
df_props_border_d = df_props_border_d.rename({c: c + f'_outer_edge_{d}px'}, axis=1)
|
|
440
|
+
else:
|
|
441
|
+
df_props_border_d = df_props_border_d.rename({c: c + f'_edge_{d}px'}, axis=1)
|
|
442
|
+
df_props_border_list.append(df_props_border_d)
|
|
443
|
+
|
|
444
|
+
df_props_border = reduce(lambda left,right: pd.merge(left,right,on=['label'],
|
|
445
|
+
how='outer'), df_props_border_list)
|
|
446
|
+
|
|
447
|
+
df_props = df_props.merge(df_props_border, how='outer', on='label')
|
|
448
|
+
|
|
449
|
+
if haralick_options is not None:
|
|
450
|
+
try:
|
|
451
|
+
df_haralick = compute_haralick_features(img, label, channels=channels, **haralick_options)
|
|
452
|
+
df_props = df_props.merge(df_haralick, left_on='label',right_on='cell_id')
|
|
453
|
+
#df_props = df_props.drop(columns=['cell_label'])
|
|
454
|
+
except Exception as e:
|
|
455
|
+
print(e)
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
if channels is not None:
|
|
459
|
+
df_props = rename_intensity_column(df_props, channels)
|
|
460
|
+
df_props.rename(columns={"label": "class_id"},inplace=True)
|
|
461
|
+
df_props['class_id'] = df_props['class_id'].astype(float)
|
|
462
|
+
|
|
463
|
+
return df_props
|
|
514
464
|
|
|
515
465
|
def compute_haralick_features(img, labels, channels=None, target_channel=0, scale_factor=1, percentiles=(0.01,99.99), clip_values=None,
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
466
|
+
n_intensity_bins=256, ignore_zero=True, return_mean=True, return_mean_ptp=False, distance=1, disable_progress_bar=False, return_norm_image_only=False, return_digit_image_only=False):
|
|
467
|
+
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
Compute Haralick texture features on each segmented region of an image.
|
|
471
|
+
|
|
472
|
+
Parameters
|
|
473
|
+
----------
|
|
474
|
+
img : ndarray
|
|
475
|
+
The input image as a NumPy array.
|
|
476
|
+
labels : ndarray
|
|
477
|
+
The segmentation labels corresponding to the image regions.
|
|
478
|
+
target_channel : int, optional
|
|
479
|
+
The target channel index of the image. The default is 0.
|
|
480
|
+
modality : str, optional
|
|
481
|
+
The modality or channel type of the image. The default is 'brightfield_channel'.
|
|
482
|
+
scale_factor : float, optional
|
|
483
|
+
The scale factor for resampling the image and labels. The default is 1.
|
|
484
|
+
percentiles : tuple of float, optional
|
|
485
|
+
The percentiles to use for image normalization. The default is (0.01, 99.99).
|
|
486
|
+
clip_values : tuple of float, optional
|
|
487
|
+
The minimum and maximum values to clip the image. If None, percentiles are used. The default is None.
|
|
488
|
+
n_intensity_bins : int, optional
|
|
489
|
+
The number of intensity bins for image normalization. The default is 255.
|
|
490
|
+
ignore_zero : bool, optional
|
|
491
|
+
Flag indicating whether to ignore zero values during feature computation. The default is True.
|
|
492
|
+
return_mean : bool, optional
|
|
493
|
+
Flag indicating whether to return the mean value of each Haralick feature. The default is True.
|
|
494
|
+
return_mean_ptp : bool, optional
|
|
495
|
+
Flag indicating whether to return the mean and peak-to-peak values of each Haralick feature. The default is False.
|
|
496
|
+
distance : int, optional
|
|
497
|
+
The distance parameter for Haralick feature computation. The default is 1.
|
|
498
|
+
|
|
499
|
+
Returns
|
|
500
|
+
-------
|
|
501
|
+
features : DataFrame
|
|
502
|
+
A pandas DataFrame containing the computed Haralick features for each segmented region.
|
|
503
|
+
|
|
504
|
+
Notes
|
|
505
|
+
-----
|
|
506
|
+
This function computes Haralick features on an image within segmented regions.
|
|
507
|
+
It uses the mahotas library for feature extraction and pandas DataFrame for storage.
|
|
508
|
+
The image is rescaled, normalized and digitized based on the specified parameters.
|
|
509
|
+
Haralick features are computed for each segmented region, and the results are returned as a DataFrame.
|
|
510
|
+
|
|
511
|
+
Examples
|
|
512
|
+
--------
|
|
513
|
+
>>> features = compute_haralick_features(img, labels, target_channel=0, modality="brightfield_channel")
|
|
514
|
+
# Compute Haralick features on the image within segmented regions.
|
|
515
|
+
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
assert ((img.ndim==2)|(img.ndim==3)),f'Invalid image shape to compute the Haralick features. Expected YXC, got {img.shape}...'
|
|
519
|
+
assert img.shape[:2]==labels.shape,f'Mismatch between image shape {img.shape} and labels shape {labels.shape}'
|
|
520
|
+
|
|
521
|
+
if img.ndim==2:
|
|
522
|
+
img = img[:,:,np.newaxis]
|
|
523
|
+
target_channel = 0
|
|
524
|
+
if isinstance(channels, list):
|
|
525
|
+
modality = channels[0]
|
|
526
|
+
elif isinstance(channels, str):
|
|
527
|
+
modality = channels
|
|
528
|
+
else:
|
|
529
|
+
print('Channel name unrecognized...')
|
|
530
|
+
modality=''
|
|
531
|
+
elif img.ndim==3:
|
|
532
|
+
assert target_channel is not None,"The image is multichannel. Please provide a target channel to compute the Haralick features. Abort."
|
|
533
|
+
modality = channels[target_channel]
|
|
534
|
+
|
|
535
|
+
haralick_labels = ["angular_second_moment",
|
|
536
|
+
"contrast",
|
|
537
|
+
"correlation",
|
|
538
|
+
"sum_of_square_variance",
|
|
539
|
+
"inverse_difference_moment",
|
|
540
|
+
"sum_average",
|
|
541
|
+
"sum_variance",
|
|
542
|
+
"sum_entropy",
|
|
543
|
+
"entropy",
|
|
544
|
+
"difference_variance",
|
|
545
|
+
"difference_entropy",
|
|
546
|
+
"information_measure_of_correlation_1",
|
|
547
|
+
"information_measure_of_correlation_2",
|
|
548
|
+
"maximal_correlation_coefficient"]
|
|
549
|
+
|
|
550
|
+
haralick_labels = ['haralick_'+h+"_"+modality for h in haralick_labels]
|
|
551
|
+
if len(img.shape)==3:
|
|
552
|
+
img = img[:,:,target_channel]
|
|
553
|
+
|
|
554
|
+
# Rescale image and mask
|
|
555
|
+
img = zoom(img,[scale_factor,scale_factor],order=3).astype(float)
|
|
556
|
+
labels = zoom(labels, [scale_factor,scale_factor],order=0)
|
|
557
|
+
|
|
558
|
+
# Normalize image
|
|
559
|
+
if clip_values is None:
|
|
560
|
+
min_value = np.nanpercentile(img[img!=0.].flatten(), percentiles[0])
|
|
561
|
+
max_value = np.nanpercentile(img[img!=0.].flatten(), percentiles[1])
|
|
562
|
+
else:
|
|
563
|
+
min_value = clip_values[0]; max_value = clip_values[1]
|
|
564
|
+
|
|
565
|
+
img -= min_value
|
|
566
|
+
img /= (max_value-min_value) / n_intensity_bins
|
|
567
|
+
img[img<=0.] = 0.
|
|
568
|
+
img[img>=n_intensity_bins] = n_intensity_bins
|
|
569
|
+
|
|
570
|
+
if return_norm_image_only:
|
|
571
|
+
return img
|
|
572
|
+
|
|
573
|
+
hist,bins = np.histogram(img.flatten(),bins=n_intensity_bins)
|
|
574
|
+
centered_bins = [bins[0]] + [bins[i] + (bins[i+1] - bins[i])/2. for i in range(len(bins)-1)]
|
|
575
|
+
|
|
576
|
+
digitized = np.digitize(img, bins)
|
|
577
|
+
img_binned = np.zeros_like(img)
|
|
578
|
+
for i in range(img.shape[0]):
|
|
579
|
+
for j in range(img.shape[1]):
|
|
580
|
+
img_binned[i,j] = centered_bins[digitized[i,j] - 1]
|
|
581
|
+
|
|
582
|
+
img = img_binned.astype(int)
|
|
583
|
+
if return_digit_image_only:
|
|
584
|
+
return img
|
|
585
|
+
|
|
586
|
+
haralick_properties = []
|
|
587
|
+
|
|
588
|
+
for cell in tqdm(np.unique(labels)[1:],disable=disable_progress_bar):
|
|
589
|
+
|
|
590
|
+
mask = labels==cell
|
|
591
|
+
f = img*mask
|
|
592
|
+
features = haralick(f, ignore_zeros=ignore_zero,return_mean=return_mean,distance=distance)
|
|
593
|
+
|
|
594
|
+
dictionary = {'cell_id': cell}
|
|
595
|
+
for k in range(len(features)):
|
|
596
|
+
dictionary.update({haralick_labels[k]: features[k]})
|
|
597
|
+
haralick_properties.append(dictionary)
|
|
598
|
+
|
|
599
|
+
assert len(haralick_properties)==(len(np.unique(labels))-1),'Some cells have not been measured...'
|
|
600
|
+
|
|
601
|
+
return pd.DataFrame(haralick_properties)
|
|
652
602
|
|
|
653
603
|
|
|
654
604
|
def measure_isotropic_intensity(positions, # Dataframe of cell positions @ t
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
605
|
+
img, # multichannel frame (YXC) @ t
|
|
606
|
+
channels=None, #channels, need labels to name measurements
|
|
607
|
+
intensity_measurement_radii=None, #list of radii, single value is circle, tuple is ring?
|
|
608
|
+
operations = ['mean'],
|
|
609
|
+
measurement_kernel = None,
|
|
610
|
+
pbar=None,
|
|
611
|
+
column_labels={'track': "TRACK_ID", 'time': 'FRAME', 'x': 'POSITION_X', 'y': 'POSITION_Y'},
|
|
612
|
+
verbose=True,
|
|
613
|
+
):
|
|
614
|
+
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
Measure isotropic intensity values around cell positions in an image.
|
|
618
|
+
|
|
619
|
+
Parameters
|
|
620
|
+
----------
|
|
621
|
+
positions : pandas DataFrame
|
|
622
|
+
DataFrame of cell positions at time 't' containing columns specified in `column_labels`.
|
|
623
|
+
img : numpy array
|
|
624
|
+
Multichannel frame (YXC) at time 't' used for intensity measurement.
|
|
625
|
+
channels : list or str, optional
|
|
626
|
+
List of channel names corresponding to the image channels. Default is None.
|
|
627
|
+
intensity_measurement_radii : int, list, or tuple
|
|
628
|
+
Radius or list of radii specifying the size of the isotropic measurement area.
|
|
629
|
+
If a single value is provided, a circular measurement area is used. If a list or tuple of two values
|
|
630
|
+
is provided, a ring-shaped measurement area is used. Default is None.
|
|
631
|
+
operations : list, optional
|
|
632
|
+
List of operations to perform on the intensity values. Default is ['mean'].
|
|
633
|
+
measurement_kernel : numpy array, optional
|
|
634
|
+
Kernel used for intensity measurement. If None, a circular or ring-shaped kernel is generated
|
|
635
|
+
based on the provided `intensity_measurement_radii`. Default is None.
|
|
636
|
+
pbar : tqdm progress bar, optional
|
|
637
|
+
Progress bar for tracking the measurement process. Default is None.
|
|
638
|
+
column_labels : dict, optional
|
|
639
|
+
Dictionary containing the column labels for the DataFrame. Default is {'track': "TRACK_ID",
|
|
640
|
+
'time': 'FRAME', 'x': 'POSITION_X', 'y': 'POSITION_Y'}.
|
|
641
|
+
verbose : bool, optional
|
|
642
|
+
If True, enables verbose output. Default is True.
|
|
643
|
+
|
|
644
|
+
Returns
|
|
645
|
+
-------
|
|
646
|
+
pandas DataFrame
|
|
647
|
+
The updated DataFrame `positions` with additional columns representing the measured intensity values.
|
|
648
|
+
|
|
649
|
+
Notes
|
|
650
|
+
-----
|
|
651
|
+
This function measures the isotropic intensity values around the cell positions specified in the `positions`
|
|
652
|
+
DataFrame using the provided image `img`. The intensity measurements are performed using circular or ring-shaped
|
|
653
|
+
measurement areas defined by the `intensity_measurement_radii`. The measurements are calculated for each channel
|
|
654
|
+
specified in the `channels` list. The resulting intensity values are stored in additional columns of the `positions`
|
|
655
|
+
DataFrame. The `operations` parameter allows specifying different operations to be performed on the intensity
|
|
656
|
+
values, such as 'mean', 'median', etc. The measurement kernel can be customized by providing the `measurement_kernel`
|
|
657
|
+
parameter. If not provided, the measurement kernel is automatically generated based on the `intensity_measurement_radii`.
|
|
658
|
+
The progress bar `pbar` can be used to track the measurement process. The `column_labels` dictionary is used to
|
|
659
|
+
specify the column labels for the DataFrame.
|
|
660
|
+
|
|
661
|
+
Examples
|
|
662
|
+
--------
|
|
663
|
+
>>> positions = pd.DataFrame({'TRACK_ID': [1, 2, 3], 'FRAME': [1, 1, 1],
|
|
664
|
+
... 'POSITION_X': [10, 20, 30], 'POSITION_Y': [15, 25, 35]})
|
|
665
|
+
>>> img = np.random.rand(100, 100, 3)
|
|
666
|
+
>>> channels = ['channel1', 'channel2', 'channel3']
|
|
667
|
+
>>> intensity_measurement_radii = 5
|
|
668
|
+
>>> positions = measure_isotropic_intensity(positions, img, channels=channels,
|
|
669
|
+
... intensity_measurement_radii=intensity_measurement_radii)
|
|
670
|
+
# Measure isotropic intensity values around cell positions in the image.
|
|
671
|
+
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
epsilon = -10000
|
|
675
|
+
assert ((img.ndim==2)|(img.ndim==3)),f'Invalid image shape to compute the Haralick features. Expected YXC, got {img.shape}...'
|
|
676
|
+
|
|
677
|
+
if img.ndim==2:
|
|
678
|
+
img = img[:,:,np.newaxis]
|
|
679
|
+
if isinstance(channels, str):
|
|
680
|
+
channels = [channels]
|
|
681
|
+
else:
|
|
682
|
+
if verbose:
|
|
683
|
+
print('Channel name unrecognized...')
|
|
684
|
+
channels=['intensity']
|
|
685
|
+
elif img.ndim==3:
|
|
686
|
+
assert channels is not None,"The image is multichannel. Please provide the list of channel names. Abort."
|
|
687
|
+
|
|
688
|
+
if isinstance(intensity_measurement_radii, int) or isinstance(intensity_measurement_radii, float):
|
|
689
|
+
intensity_measurement_radii = [intensity_measurement_radii]
|
|
690
|
+
|
|
691
|
+
if (measurement_kernel is None)*(intensity_measurement_radii is not None):
|
|
692
|
+
|
|
693
|
+
for r in intensity_measurement_radii:
|
|
694
|
+
|
|
695
|
+
if isinstance(r,list):
|
|
696
|
+
mask = create_patch_mask(2*max(r)+1,2*max(r)+1,((2*max(r))//2,(2*max(r))//2),radius=r)
|
|
697
|
+
else:
|
|
698
|
+
mask = create_patch_mask(2*r+1,2*r+1,((2*r)//2,(2*r)//2),r)
|
|
699
|
+
|
|
700
|
+
pad_value_x = mask.shape[0]//2 + 1
|
|
701
|
+
pad_value_y = mask.shape[1]//2 + 1
|
|
702
|
+
frame_padded = np.pad(img.astype(float), [(pad_value_x,pad_value_x),(pad_value_y,pad_value_y),(0,0)], constant_values=[(epsilon,epsilon),(epsilon,epsilon),(0,0)])
|
|
703
|
+
|
|
704
|
+
# Find a way to measure intensity in mask
|
|
705
|
+
for tid,group in positions.groupby(column_labels['track']):
|
|
706
|
+
|
|
707
|
+
x = group[column_labels['x']].to_numpy()[0]
|
|
708
|
+
y = group[column_labels['y']].to_numpy()[0]
|
|
709
|
+
|
|
710
|
+
xmin = int(x)
|
|
711
|
+
xmax = int(x) + 2*pad_value_y - 1
|
|
712
|
+
ymin = int(y)
|
|
713
|
+
ymax = int(y) + 2*pad_value_x - 1
|
|
714
|
+
|
|
715
|
+
assert frame_padded[ymin:ymax,xmin:xmax,0].shape == mask.shape,"Shape mismatch between the measurement kernel and the image..."
|
|
716
|
+
|
|
717
|
+
expanded_mask = np.expand_dims(mask, axis=-1) # shape: (X, Y, 1)
|
|
718
|
+
crop = frame_padded[ymin:ymax,xmin:xmax]
|
|
719
|
+
|
|
720
|
+
crop_temp = crop.copy()
|
|
721
|
+
crop_temp[crop_temp==epsilon] = 0.
|
|
722
|
+
projection = np.multiply(crop_temp, expanded_mask)
|
|
723
|
+
|
|
724
|
+
projection[crop==epsilon] = epsilon
|
|
725
|
+
projection[expanded_mask[:,:,0]==0.,:] = epsilon
|
|
726
|
+
|
|
727
|
+
for op in operations:
|
|
728
|
+
func = eval('np.'+op)
|
|
729
|
+
intensity_values = func(projection, axis=(0,1), where=projection>epsilon)
|
|
730
|
+
for k in range(crop.shape[-1]):
|
|
731
|
+
if isinstance(r,list):
|
|
732
|
+
positions.loc[group.index, f'{channels[k]}_ring_{min(r)}_{max(r)}_{op}'] = intensity_values[k]
|
|
733
|
+
else:
|
|
734
|
+
positions.loc[group.index, f'{channels[k]}_circle_{r}_{op}'] = intensity_values[k]
|
|
735
|
+
|
|
736
|
+
elif (measurement_kernel is not None):
|
|
737
|
+
# do something like this
|
|
738
|
+
mask = measurement_kernel
|
|
739
|
+
pad_value_x = mask.shape[0]//2 + 1
|
|
740
|
+
pad_value_y = mask.shape[1]//2 + 1
|
|
741
|
+
frame_padded = np.pad(img, [(pad_value_x,pad_value_x),(pad_value_y,pad_value_y),(0,0)])
|
|
742
|
+
|
|
743
|
+
for tid,group in positions.groupby(column_labels['track']):
|
|
744
|
+
|
|
745
|
+
x = group[column_labels['x']].to_numpy()[0]
|
|
746
|
+
y = group[column_labels['y']].to_numpy()[0]
|
|
747
|
+
|
|
748
|
+
xmin = int(x)
|
|
749
|
+
xmax = int(x) + 2*pad_value_y - 1
|
|
750
|
+
ymin = int(y)
|
|
751
|
+
ymax = int(y) + 2*pad_value_x - 1
|
|
752
|
+
|
|
753
|
+
assert frame_padded[ymin:ymax,xmin:xmax,0].shape == mask.shape,"Shape mismatch between the measurement kernel and the image..."
|
|
754
|
+
|
|
755
|
+
expanded_mask = np.expand_dims(mask, axis=-1) # shape: (X, Y, 1)
|
|
756
|
+
crop = frame_padded[ymin:ymax,xmin:xmax]
|
|
757
|
+
projection = np.multiply(crop, expanded_mask)
|
|
758
|
+
|
|
759
|
+
for op in operations:
|
|
760
|
+
func = eval('np.'+op)
|
|
761
|
+
intensity_values = func(projection, axis=(0,1), where=projection==projection)
|
|
762
|
+
for k in range(crop.shape[-1]):
|
|
763
|
+
positions.loc[group.index, f'{channels[k]}_custom_kernel_{op}'] = intensity_values[k]
|
|
764
|
+
|
|
765
|
+
if pbar is not None:
|
|
766
|
+
pbar.update(1)
|
|
767
|
+
positions['class_id'] = positions['class_id'].astype(float)
|
|
768
|
+
return positions
|
|
819
769
|
|
|
820
770
|
def measure_at_position(pos, mode, return_measurements=False, threads=1):
|
|
821
771
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
772
|
+
"""
|
|
773
|
+
Executes a measurement script at a specified position directory, optionally returning the measured data.
|
|
774
|
+
|
|
775
|
+
This function calls an external Python script to perform measurements on data
|
|
776
|
+
located in a specified position directory. The measurement mode determines the type of analysis performed by the script.
|
|
777
|
+
The function can either return the path to the resulting measurements table or load and return the measurements as a
|
|
778
|
+
pandas DataFrame.
|
|
779
|
+
|
|
780
|
+
Parameters
|
|
781
|
+
----------
|
|
782
|
+
pos : str
|
|
783
|
+
The path to the position directory where the measurements should be performed. The path should be a valid directory.
|
|
784
|
+
mode : str
|
|
785
|
+
The measurement mode to be used by the script. This determines the type of analysis performed (e.g., 'tracking',
|
|
786
|
+
'feature_extraction').
|
|
787
|
+
return_measurements : bool, optional
|
|
788
|
+
If True, the function loads the resulting measurements from a CSV file into a pandas DataFrame and returns it. If
|
|
789
|
+
False, the function returns None (default is False).
|
|
790
|
+
|
|
791
|
+
Returns
|
|
792
|
+
-------
|
|
793
|
+
pandas.DataFrame or None
|
|
794
|
+
If `return_measurements` is True, returns a pandas DataFrame containing the measurements. Otherwise, returns None.
|
|
795
|
+
|
|
796
|
+
"""
|
|
797
|
+
|
|
798
|
+
pos = pos.replace('\\','/')
|
|
799
|
+
pos = rf"{pos}"
|
|
800
|
+
assert os.path.exists(pos),f'Position {pos} is not a valid path.'
|
|
801
|
+
if not pos.endswith('/'):
|
|
802
|
+
pos += '/'
|
|
803
|
+
script_path = os.sep.join([abs_path, 'scripts', 'measure_cells.py'])
|
|
804
|
+
cmd = f'python "{script_path}" --pos "{pos}" --mode "{mode}" --threads "{threads}"'
|
|
805
|
+
subprocess.call(cmd, shell=True)
|
|
806
|
+
|
|
807
|
+
table = pos + os.sep.join(["output","tables",f"trajectories_{mode}.csv"])
|
|
808
|
+
if return_measurements:
|
|
809
|
+
df = pd.read_csv(table)
|
|
810
|
+
return df
|
|
811
|
+
else:
|
|
812
|
+
return None
|
|
863
813
|
|
|
864
814
|
|
|
865
815
|
def local_normalisation(image, labels, background_intensity, measurement='intensity_median', operation='subtract', clip=False):
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
816
|
+
"""
|
|
817
|
+
Perform local normalization on an image based on labels.
|
|
818
|
+
|
|
819
|
+
Parameters:
|
|
820
|
+
- image (numpy.ndarray): The input image.
|
|
821
|
+
- labels (numpy.ndarray): An array specifying the labels for different regions in the image.
|
|
822
|
+
- background_intensity (pandas.DataFrame): A DataFrame containing background intensity values
|
|
823
|
+
corresponding to each label.
|
|
824
|
+
- mode (str): The normalization mode ('Mean' or 'Median').
|
|
825
|
+
- operation (str): The operation to perform ('Subtract' or 'Divide').
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
- numpy.ndarray: The normalized image.
|
|
829
|
+
|
|
830
|
+
This function performs local normalization on an image based on the provided labels. It iterates over
|
|
831
|
+
each unique label, excluding the background label (0), and performs the specified operation with the
|
|
832
|
+
background intensity values corresponding to that label. The background intensity values are obtained
|
|
833
|
+
from the provided background_intensity DataFrame based on the normalization mode.
|
|
834
|
+
|
|
835
|
+
If the operation is 'Subtract', the background intensity is subtracted from the image pixel values.
|
|
836
|
+
If the operation is 'Divide', the image pixel values are divided by the background intensity.
|
|
837
|
+
|
|
838
|
+
Example:
|
|
839
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
840
|
+
>>> labels = np.array([[0, 1, 1], [2, 2, 3], [3, 3, 0]])
|
|
841
|
+
>>> background_intensity = pd.DataFrame({'intensity_mean': [10, 20, 30]})
|
|
842
|
+
>>> mode = 'Mean'
|
|
843
|
+
>>> operation = 'Subtract'
|
|
844
|
+
>>> result = local_normalisation(image, labels, background_intensity, mode, operation)
|
|
845
|
+
>>> print(result)
|
|
846
|
+
[[-9. -8. -7.]
|
|
847
|
+
[14. 15. 6.]
|
|
848
|
+
[27. 28. 9.]]
|
|
849
|
+
|
|
850
|
+
Note:
|
|
851
|
+
- The background intensity DataFrame should have columns named 'intensity_mean' or 'intensity_median'
|
|
852
|
+
based on the mode specified.
|
|
853
|
+
- The background intensity values should be provided in the same order as the labels.
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
for index, cell in enumerate(np.unique(labels)):
|
|
857
|
+
if cell == 0:
|
|
858
|
+
continue
|
|
859
|
+
if operation == 'subtract':
|
|
860
|
+
image[np.where(labels == cell)] = image[np.where(labels == cell)].astype(float) - \
|
|
861
|
+
background_intensity[measurement][index-1].astype(float)
|
|
862
|
+
elif operation == 'divide':
|
|
863
|
+
image[np.where(labels == cell)] = image[np.where(labels == cell)].astype(float) / \
|
|
864
|
+
background_intensity[measurement][index-1].astype(float)
|
|
865
|
+
if clip:
|
|
866
|
+
image[image<=0.] = 0.
|
|
867
|
+
|
|
868
|
+
return image.astype(float)
|
|
919
869
|
|
|
920
870
|
|
|
921
871
|
def normalise_by_cell(image, labels, distance=5, model='median', operation='subtract', clip=False):
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
872
|
+
"""
|
|
873
|
+
Normalize an image based on cell regions.
|
|
874
|
+
|
|
875
|
+
Parameters:
|
|
876
|
+
- image (numpy.ndarray): The input image.
|
|
877
|
+
- labels (numpy.ndarray): An array specifying the labels for different regions in the image.
|
|
878
|
+
- distance (float): The distance parameter for finding the contour of cell regions.
|
|
879
|
+
- mode (str): The normalization mode ('Mean' or 'Median').
|
|
880
|
+
- operation (str): The operation to perform ('Subtract' or 'Divide').
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
- numpy.ndarray: The normalized image.
|
|
884
|
+
|
|
885
|
+
This function normalizes an image based on cell regions defined by the provided labels. It calculates
|
|
886
|
+
the border of cell regions using the contour_of_instance_segmentation function with the specified
|
|
887
|
+
distance parameter. Then, it computes the background intensity of each cell region based on the mode
|
|
888
|
+
('Mean' or 'Median'). Finally, it performs local normalization using the local_normalisation function
|
|
889
|
+
and returns the normalized image.
|
|
890
|
+
|
|
891
|
+
Example:
|
|
892
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
893
|
+
>>> labels = np.array([[0, 1, 1], [2, 2, 3], [3, 3, 0]])
|
|
894
|
+
>>> distance = 2.0
|
|
895
|
+
>>> mode = 'Mean'
|
|
896
|
+
>>> operation = 'Subtract'
|
|
897
|
+
>>> result = normalise_by_cell(image, labels, distance, mode, operation)
|
|
898
|
+
>>> print(result)
|
|
899
|
+
[[-9. -8. -7.]
|
|
900
|
+
[14. 15. 6.]
|
|
901
|
+
[27. 28. 9.]]
|
|
902
|
+
|
|
903
|
+
Note:
|
|
904
|
+
- The contour of cell regions is calculated using the contour_of_instance_segmentation function.
|
|
905
|
+
- The background intensity is computed based on the specified mode ('Mean' or 'Median').
|
|
906
|
+
- The operation determines whether to subtract or divide the background intensity from the image.
|
|
907
|
+
"""
|
|
908
|
+
border = contour_of_instance_segmentation(label=labels, distance=distance * (-1))
|
|
909
|
+
if model == 'mean':
|
|
910
|
+
measurement = 'intensity_nanmean'
|
|
911
|
+
extra_props = [getattr(extra_properties, measurement)]
|
|
912
|
+
background_intensity = regionprops_table(intensity_image=image, label_image=border,
|
|
913
|
+
extra_properties=extra_props)
|
|
914
|
+
elif model == 'median':
|
|
915
|
+
measurement = 'intensity_median'
|
|
916
|
+
extra_props = [getattr(extra_properties, measurement)]
|
|
917
|
+
background_intensity = regionprops_table(intensity_image=image, label_image=border,
|
|
918
|
+
extra_properties=extra_props)
|
|
919
|
+
|
|
920
|
+
normalised_frame = local_normalisation(image=image.astype(float).copy(),
|
|
921
|
+
labels=labels, background_intensity=background_intensity, measurement=measurement,
|
|
922
|
+
operation=operation, clip=clip)
|
|
923
|
+
|
|
924
|
+
return normalised_frame
|
|
975
925
|
|
|
976
926
|
|
|
977
927
|
def blob_detection(image, label, threshold, diameter):
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
928
|
+
"""
|
|
929
|
+
Perform blob detection on an image based on labeled regions.
|
|
930
|
+
|
|
931
|
+
Parameters:
|
|
932
|
+
- image (numpy.ndarray): The input image data.
|
|
933
|
+
- label (numpy.ndarray): An array specifying labeled regions in the image.
|
|
934
|
+
- threshold (float): The threshold value for blob detection.
|
|
935
|
+
- diameter (float): The expected diameter of blobs.
|
|
936
|
+
|
|
937
|
+
Returns:
|
|
938
|
+
- dict: A dictionary containing information about detected blobs.
|
|
939
|
+
|
|
940
|
+
This function performs blob detection on an image based on labeled regions. It iterates over each labeled region
|
|
941
|
+
and detects blobs within the region using the Difference of Gaussians (DoG) method. Detected blobs are filtered
|
|
942
|
+
based on the specified threshold and expected diameter. The function returns a dictionary containing the number of
|
|
943
|
+
detected blobs and their mean intensity for each labeled region.
|
|
944
|
+
|
|
945
|
+
Example:
|
|
946
|
+
>>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
947
|
+
>>> label = np.array([[0, 1, 1], [2, 2, 0], [3, 3, 0]])
|
|
948
|
+
>>> threshold = 0.1
|
|
949
|
+
>>> diameter = 5.0
|
|
950
|
+
>>> result = blob_detection(image, label, threshold, diameter)
|
|
951
|
+
>>> print(result)
|
|
952
|
+
{1: [1, 4.0], 2: [0, nan], 3: [0, nan]}
|
|
953
|
+
|
|
954
|
+
Note:
|
|
955
|
+
- Blobs are detected using the Difference of Gaussians (DoG) method.
|
|
956
|
+
- Detected blobs are filtered based on the specified threshold and expected diameter.
|
|
957
|
+
- The returned dictionary contains information about the number of detected blobs and their mean intensity
|
|
958
|
+
for each labeled region.
|
|
959
|
+
"""
|
|
960
|
+
blob_labels = {}
|
|
961
|
+
dilated_image = ndimage.grey_dilation(label, footprint=disk(10))
|
|
962
|
+
for mask_index in np.unique(label):
|
|
963
|
+
if mask_index == 0:
|
|
964
|
+
continue
|
|
965
|
+
removed_background = image.copy()
|
|
966
|
+
one_mask = label.copy()
|
|
967
|
+
one_mask[np.where(label != mask_index)] = 0
|
|
968
|
+
dilated_copy = dilated_image.copy()
|
|
969
|
+
dilated_copy[np.where(dilated_image != mask_index)] = 0
|
|
970
|
+
removed_background[np.where(dilated_copy == 0)] = 0
|
|
971
|
+
min_sigma = (1 / (1 + math.sqrt(2))) * diameter
|
|
972
|
+
max_sigma = math.sqrt(2) * min_sigma
|
|
973
|
+
blobs = skimage.feature.blob_dog(removed_background, threshold=threshold, min_sigma=min_sigma,
|
|
974
|
+
max_sigma=max_sigma)
|
|
975
|
+
|
|
976
|
+
mask = np.array([one_mask[int(y), int(x)] != 0 for y, x, r in blobs])
|
|
977
|
+
if not np.any(mask):
|
|
978
|
+
continue
|
|
979
|
+
blobs_filtered = blobs[mask]
|
|
980
|
+
binary_blobs = np.zeros_like(label)
|
|
981
|
+
for blob in blobs_filtered:
|
|
982
|
+
y, x, r = blob
|
|
983
|
+
rr, cc = dsk((y, x), r, shape=binary_blobs.shape)
|
|
984
|
+
binary_blobs[rr, cc] = 1
|
|
985
|
+
spot_intensity = regionprops_table(binary_blobs, removed_background, ['intensity_mean'])
|
|
986
|
+
blob_labels[mask_index] = [blobs_filtered.shape[0], spot_intensity['intensity_mean'][0]]
|
|
987
|
+
return blob_labels
|
|
1038
988
|
|