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