celldetective 1.3.6.post1__py3-none-any.whl → 1.3.7__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/_version.py +1 -1
- celldetective/events.py +4 -0
- celldetective/filters.py +11 -2
- celldetective/gui/InitWindow.py +23 -9
- celldetective/gui/control_panel.py +19 -11
- celldetective/gui/generic_signal_plot.py +5 -0
- celldetective/gui/gui_utils.py +2 -2
- celldetective/gui/help/DL-segmentation-strategy.json +17 -17
- celldetective/gui/help/Threshold-vs-DL.json +11 -11
- celldetective/gui/help/cell-populations.json +5 -5
- celldetective/gui/help/exp-structure.json +15 -15
- celldetective/gui/help/feature-btrack.json +5 -5
- celldetective/gui/help/neighborhood.json +7 -7
- celldetective/gui/help/prefilter-for-segmentation.json +7 -7
- celldetective/gui/help/preprocessing.json +19 -19
- celldetective/gui/help/propagate-classification.json +7 -7
- celldetective/gui/neighborhood_options.py +1 -1
- celldetective/gui/plot_signals_ui.py +13 -9
- celldetective/gui/process_block.py +63 -14
- celldetective/gui/retrain_segmentation_model_options.py +21 -8
- celldetective/gui/retrain_signal_model_options.py +12 -2
- celldetective/gui/signal_annotator.py +9 -0
- celldetective/gui/signal_annotator2.py +25 -17
- celldetective/gui/styles.py +1 -0
- celldetective/gui/tableUI.py +1 -1
- celldetective/gui/workers.py +136 -0
- celldetective/io.py +54 -28
- celldetective/measure.py +112 -14
- celldetective/scripts/measure_cells.py +36 -46
- celldetective/scripts/segment_cells.py +35 -78
- celldetective/scripts/segment_cells_thresholds.py +21 -22
- celldetective/scripts/track_cells.py +43 -32
- celldetective/segmentation.py +16 -62
- celldetective/signals.py +11 -7
- celldetective/utils.py +587 -67
- {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/METADATA +1 -1
- {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/RECORD +41 -40
- {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/LICENSE +0 -0
- {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/WHEEL +0 -0
- {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/entry_points.txt +0 -0
- {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/top_level.txt +0 -0
celldetective/utils.py
CHANGED
|
@@ -29,9 +29,513 @@ from scipy import ndimage
|
|
|
29
29
|
from skimage.morphology import disk
|
|
30
30
|
from scipy.stats import ks_2samp
|
|
31
31
|
from cliffs_delta import cliffs_delta
|
|
32
|
+
from stardist.models import StarDist2D
|
|
33
|
+
from cellpose.models import CellposeModel
|
|
32
34
|
|
|
35
|
+
def _remove_invalid_cols(df):
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
Removes invalid columns from a DataFrame.
|
|
39
|
+
|
|
40
|
+
This function identifies and removes columns in the DataFrame whose names
|
|
41
|
+
start with "Unnamed", which often indicate extraneous or improperly
|
|
42
|
+
formatted columns (e.g., leftover columns from improperly read CSV files).
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
df : pandas.DataFrame
|
|
47
|
+
The input DataFrame from which invalid columns will be removed.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
pandas.DataFrame
|
|
52
|
+
A new DataFrame with the invalid columns removed. If no invalid
|
|
53
|
+
columns are found, the original DataFrame is returned unchanged.
|
|
54
|
+
|
|
55
|
+
Notes
|
|
56
|
+
-----
|
|
57
|
+
- This function does not modify the original DataFrame in place; instead,
|
|
58
|
+
it returns a new DataFrame.
|
|
59
|
+
- Columns starting with "Unnamed" are commonly introduced when saving
|
|
60
|
+
or loading data files with misaligned headers.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
invalid_cols = [c for c in list(df.columns) if c.startswith('Unnamed')]
|
|
64
|
+
if len(invalid_cols)>0:
|
|
65
|
+
df = df.drop(invalid_cols, axis=1)
|
|
66
|
+
return df
|
|
67
|
+
|
|
68
|
+
def _extract_coordinates_from_features(features, timepoint):
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
Extracts spatial coordinates and other relevant metadata from a features DataFrame.
|
|
72
|
+
|
|
73
|
+
This function processes a DataFrame of features to extract and rename centroid
|
|
74
|
+
coordinates, assign a unique identifier to each feature, and add a frame (timepoint)
|
|
75
|
+
column. The resulting DataFrame is structured for use in trajectory analysis.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
features : pandas.DataFrame
|
|
80
|
+
A DataFrame containing feature data, including columns for centroids
|
|
81
|
+
(`'centroid-1'` and `'centroid-0'`) and feature classes (`'class_id'`).
|
|
82
|
+
timepoint : int
|
|
83
|
+
The timepoint (frame) to assign to all features. This is used to populate
|
|
84
|
+
the `'FRAME'` column in the output.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
pandas.DataFrame
|
|
89
|
+
A DataFrame containing the extracted coordinates and additional metadata,
|
|
90
|
+
with the following columns:
|
|
91
|
+
- `'POSITION_X'`: X-coordinate of the centroid.
|
|
92
|
+
- `'POSITION_Y'`: Y-coordinate of the centroid.
|
|
93
|
+
- `'class_id'`: The label associated to the cell mask.
|
|
94
|
+
- `'ID'`: A unique identifier for each cell (index-based).
|
|
95
|
+
- `'FRAME'`: The timepoint associated with the features.
|
|
96
|
+
|
|
97
|
+
Notes
|
|
98
|
+
-----
|
|
99
|
+
- The function assumes that the input DataFrame contains columns `'centroid-1'`,
|
|
100
|
+
`'centroid-0'`, and `'class_id'`. Missing columns will raise a KeyError.
|
|
101
|
+
- The `'ID'` column is created based on the index of the input DataFrame.
|
|
102
|
+
- This function renames `'centroid-1'` to `'POSITION_X'` and `'centroid-0'`
|
|
103
|
+
to `'POSITION_Y'`.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
coords = features[['centroid-1', 'centroid-0', 'class_id']].copy()
|
|
107
|
+
coords['ID'] = np.arange(len(coords))
|
|
108
|
+
coords.rename(columns={'centroid-1': 'POSITION_X', 'centroid-0': 'POSITION_Y'}, inplace=True)
|
|
109
|
+
coords['FRAME'] = int(timepoint)
|
|
110
|
+
|
|
111
|
+
return coords
|
|
112
|
+
|
|
113
|
+
def _mask_intensity_measurements(df, mask_channels):
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
Removes columns from a DataFrame that match specific channel name patterns.
|
|
117
|
+
|
|
118
|
+
This function filters out intensity measurement columns in a DataFrame based on
|
|
119
|
+
specified channel names. It identifies columns containing the channel
|
|
120
|
+
names as substrings and drops them from the DataFrame.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
df : pandas.DataFrame
|
|
125
|
+
The input DataFrame containing intensity measurement data. Column names should
|
|
126
|
+
include the mask channel names if they are to be filtered.
|
|
127
|
+
mask_channels : list of str or None
|
|
128
|
+
A list of channel names (as substrings) to use for identifying columns
|
|
129
|
+
to remove. If `None`, no filtering is applied, and the original DataFrame is
|
|
130
|
+
returned.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
pandas.DataFrame
|
|
135
|
+
The modified DataFrame with specified columns removed. If no columns match
|
|
136
|
+
the mask channels, the original DataFrame is returned.
|
|
137
|
+
|
|
138
|
+
Notes
|
|
139
|
+
-----
|
|
140
|
+
- The function searches for mask channel substrings in column names.
|
|
141
|
+
Partial matches are sufficient to mark a column for removal.
|
|
142
|
+
- If no mask channels are specified (`mask_channels` is `None`), the function
|
|
143
|
+
does not modify the input DataFrame.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
if mask_channels is not None:
|
|
147
|
+
|
|
148
|
+
cols_to_drop = []
|
|
149
|
+
columns = df.columns
|
|
150
|
+
|
|
151
|
+
for mc in mask_channels:
|
|
152
|
+
cols_to_remove = [c for c in columns if mc in c]
|
|
153
|
+
cols_to_drop.extend(cols_to_remove)
|
|
154
|
+
|
|
155
|
+
if len(cols_to_drop)>0:
|
|
156
|
+
df = df.drop(cols_to_drop, axis=1)
|
|
157
|
+
return df
|
|
158
|
+
|
|
159
|
+
def _rearrange_multichannel_frame(frame):
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
Rearranges the axes of a multi-channel frame to ensure the channel axis is at the end.
|
|
163
|
+
|
|
164
|
+
This function standardizes the input frame to ensure that the channel axis (if present)
|
|
165
|
+
is moved to the last position. For 2D frames, it adds a singleton channel axis at the end.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
frame : ndarray
|
|
170
|
+
The input frame to be rearranged. Can be 2D or 3D.
|
|
171
|
+
- If 3D, the function identifies the channel axis (assumed to be the axis with the smallest size)
|
|
172
|
+
and moves it to the last position.
|
|
173
|
+
- If 2D, the function adds a singleton channel axis to make it compatible with 3D processing.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
ndarray
|
|
178
|
+
The rearranged frame with the channel axis at the end.
|
|
179
|
+
- For 3D frames, the output shape will have the channel axis as the last dimension.
|
|
180
|
+
- For 2D frames, the output will have shape `(H, W, 1)` where `H` and `W` are the height and width of the frame.
|
|
181
|
+
|
|
182
|
+
Notes
|
|
183
|
+
-----
|
|
184
|
+
- This function assumes that in a 3D input, the channel axis is the one with the smallest size.
|
|
185
|
+
- For 2D frames, this function ensures compatibility with multi-channel processing pipelines by
|
|
186
|
+
adding a singleton dimension for the channel axis.
|
|
187
|
+
|
|
188
|
+
Examples
|
|
189
|
+
--------
|
|
190
|
+
Rearranging a 3D multi-channel frame:
|
|
191
|
+
>>> frame = np.zeros((10, 10, 3)) # Already channel-last
|
|
192
|
+
>>> _rearrange_multichannel_frame(frame).shape
|
|
193
|
+
(10, 10, 3)
|
|
194
|
+
|
|
195
|
+
Rearranging a 3D frame with channel axis not at the end:
|
|
196
|
+
>>> frame = np.zeros((3, 10, 10)) # Channel-first
|
|
197
|
+
>>> _rearrange_multichannel_frame(frame).shape
|
|
198
|
+
(10, 10, 3)
|
|
199
|
+
|
|
200
|
+
Converting a 2D frame to have a channel axis:
|
|
201
|
+
>>> frame = np.zeros((10, 10)) # Grayscale image
|
|
202
|
+
>>> _rearrange_multichannel_frame(frame).shape
|
|
203
|
+
(10, 10, 1)
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if frame.ndim == 3:
|
|
208
|
+
# Systematically move channel axis to the end
|
|
209
|
+
channel_axis = np.argmin(frame.shape)
|
|
210
|
+
frame = np.moveaxis(frame, channel_axis, -1)
|
|
211
|
+
|
|
212
|
+
if frame.ndim==2:
|
|
213
|
+
frame = frame[:,:,np.newaxis]
|
|
214
|
+
|
|
215
|
+
return frame
|
|
216
|
+
|
|
217
|
+
def _fix_no_contrast(frames, value=1):
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
Ensures that frames with no contrast (i.e., containing only a single unique value) are adjusted.
|
|
221
|
+
|
|
222
|
+
This function modifies frames that lack contrast by adding a small value to the first pixel in
|
|
223
|
+
the affected frame. This prevents downstream issues in image processing pipelines that require
|
|
224
|
+
a minimum level of contrast.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
frames : ndarray
|
|
229
|
+
A 3D array of shape `(H, W, N)`, where:
|
|
230
|
+
- `H` is the height of the frame,
|
|
231
|
+
- `W` is the width of the frame,
|
|
232
|
+
- `N` is the number of frames or channels.
|
|
233
|
+
Each frame (or channel) is independently checked for contrast.
|
|
234
|
+
value : int or float, optional
|
|
235
|
+
The value to add to the first pixel (`frames[0, 0, k]`) of any frame that lacks contrast.
|
|
236
|
+
Default is `1`.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
ndarray
|
|
241
|
+
The modified `frames` array, where frames with no contrast have been adjusted.
|
|
242
|
+
|
|
243
|
+
Notes
|
|
244
|
+
-----
|
|
245
|
+
- A frame is determined to have "no contrast" if all its pixel values are identical.
|
|
246
|
+
- Only the first pixel (`[0, 0, k]`) of a no-contrast frame is modified, leaving the rest
|
|
247
|
+
of the frame unchanged.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
for k in range(frames.shape[2]):
|
|
251
|
+
unique_values = np.unique(frames[:,:,k])
|
|
252
|
+
if len(unique_values)==1:
|
|
253
|
+
frames[0,0,k] += value
|
|
254
|
+
return frames
|
|
255
|
+
|
|
256
|
+
def zoom_multiframes(frames, zoom_factor):
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
Applies zooming to each frame (channel) in a multi-frame image.
|
|
260
|
+
|
|
261
|
+
This function resizes each channel of a multi-frame image independently using a specified zoom factor.
|
|
262
|
+
The zoom is applied using spline interpolation of the specified order, and the channels are combined
|
|
263
|
+
back into the original format.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
frames : ndarray
|
|
268
|
+
A multi-frame image with dimensions `(height, width, channels)`. The last axis represents different
|
|
269
|
+
channels.
|
|
270
|
+
zoom_factor : float
|
|
271
|
+
The zoom factor to apply to each channel. Values greater than 1 increase the size, and values
|
|
272
|
+
between 0 and 1 decrease the size.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
ndarray
|
|
277
|
+
A new multi-frame image with the same number of channels as the input, but with the height and width
|
|
278
|
+
scaled by the zoom factor.
|
|
279
|
+
|
|
280
|
+
Notes
|
|
281
|
+
-----
|
|
282
|
+
- The function uses spline interpolation (order 3) for resizing, which provides smooth results.
|
|
283
|
+
- `prefilter=False` is used to prevent additional filtering during the zoom operation.
|
|
284
|
+
- The function assumes that the input is in `height x width x channels` format, with channels along the
|
|
285
|
+
last axis.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
frames = [zoom(frames[:,:,c].copy(), [zoom_factor,zoom_factor], order=3, prefilter=False) for c in range(frames.shape[-1])]
|
|
289
|
+
frames = np.moveaxis(frames,0,-1)
|
|
290
|
+
return frames
|
|
291
|
+
|
|
292
|
+
def _prep_stardist_model(model_name, path, use_gpu=False, scale=1):
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
Prepares and loads a StarDist2D model for segmentation tasks.
|
|
296
|
+
|
|
297
|
+
This function initializes a StarDist2D model with the specified parameters, sets GPU usage if desired,
|
|
298
|
+
and allows scaling to adapt the model for specific applications.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
model_name : str
|
|
303
|
+
The name of the StarDist2D model to load. This name should match the model saved in the specified path.
|
|
304
|
+
path : str
|
|
305
|
+
The directory where the model is stored.
|
|
306
|
+
use_gpu : bool, optional
|
|
307
|
+
If `True`, the model will be configured to use GPU acceleration for computations. Default is `False`.
|
|
308
|
+
scale : int or float, optional
|
|
309
|
+
A scaling factor for the model. This can be used to adapt the model for specific image resolutions.
|
|
310
|
+
Default is `1`.
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
tuple
|
|
315
|
+
- model : StarDist2D
|
|
316
|
+
The loaded StarDist2D model configured with the specified parameters.
|
|
317
|
+
- scale_model : int or float
|
|
318
|
+
The scaling factor passed to the function.
|
|
319
|
+
|
|
320
|
+
Notes
|
|
321
|
+
-----
|
|
322
|
+
- Ensure the StarDist2D package is installed and the model files are correctly stored in the provided path.
|
|
323
|
+
- GPU support depends on the availability of compatible hardware and software setup.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
model = StarDist2D(None, name=model_name, basedir=path)
|
|
327
|
+
model.config.use_gpu = use_gpu
|
|
328
|
+
model.use_gpu = use_gpu
|
|
329
|
+
scale_model = scale
|
|
330
|
+
print(f"StarDist model {model_name} successfully loaded...")
|
|
331
|
+
return model, scale_model
|
|
332
|
+
|
|
333
|
+
def _prep_cellpose_model(model_name, path, use_gpu=False, n_channels=2, scale=None):
|
|
334
|
+
|
|
335
|
+
"""
|
|
336
|
+
Prepares and loads a Cellpose model for segmentation tasks.
|
|
337
|
+
|
|
338
|
+
This function initializes a Cellpose model with the specified parameters, configures GPU usage if available,
|
|
339
|
+
and calculates or applies a scaling factor for the model based on image resolution.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
model_name : str
|
|
344
|
+
The name of the pretrained Cellpose model to load.
|
|
345
|
+
path : str
|
|
346
|
+
The directory where the model is stored.
|
|
347
|
+
use_gpu : bool, optional
|
|
348
|
+
If `True`, the model will use GPU acceleration for computations. Default is `False`.
|
|
349
|
+
n_channels : int, optional
|
|
350
|
+
The number of input channels expected by the model. Default is `2`.
|
|
351
|
+
scale : float, optional
|
|
352
|
+
A scaling factor to adjust the model's output to match the image resolution. If not provided, the scale is
|
|
353
|
+
automatically calculated based on the model's diameter parameters.
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
tuple
|
|
358
|
+
- model : CellposeModel
|
|
359
|
+
The loaded Cellpose model configured with the specified parameters.
|
|
360
|
+
- scale_model : float
|
|
361
|
+
The scaling factor applied to the model, calculated or provided.
|
|
362
|
+
|
|
363
|
+
Notes
|
|
364
|
+
-----
|
|
365
|
+
- Ensure the Cellpose package is installed and the model files are correctly stored in the provided path.
|
|
366
|
+
- GPU support depends on the availability of compatible hardware and software setup.
|
|
367
|
+
- The scale is calculated as `(diam_mean / diam_labels)` if `scale` is not provided, where `diam_mean` and
|
|
368
|
+
`diam_labels` are attributes of the model.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
import torch
|
|
372
|
+
if not use_gpu:
|
|
373
|
+
device = torch.device("cpu")
|
|
374
|
+
else:
|
|
375
|
+
device = torch.device("cuda")
|
|
376
|
+
|
|
377
|
+
model = CellposeModel(gpu=use_gpu, device=device, pretrained_model=path+model_name, model_type=None, nchan=n_channels) #diam_mean=30.0,
|
|
378
|
+
if scale is None:
|
|
379
|
+
scale_model = model.diam_mean / model.diam_labels
|
|
380
|
+
else:
|
|
381
|
+
scale_model = scale * model.diam_mean / model.diam_labels
|
|
382
|
+
|
|
383
|
+
print(f"Diam mean: {model.diam_mean}; Diam labels: {model.diam_labels}; Final rescaling: {scale_model}...")
|
|
384
|
+
print(f'Cellpose model {model_name} successfully loaded...')
|
|
385
|
+
return model, scale_model
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _get_normalize_kwargs_from_config(config):
|
|
389
|
+
|
|
390
|
+
if isinstance(config, str):
|
|
391
|
+
if os.path.exists(config):
|
|
392
|
+
with open(config) as cfg:
|
|
393
|
+
config = json.load(cfg)
|
|
394
|
+
else:
|
|
395
|
+
print('Configuration could not be loaded...')
|
|
396
|
+
os.abort()
|
|
397
|
+
|
|
398
|
+
normalization_percentile = config['normalization_percentile']
|
|
399
|
+
normalization_clip = config['normalization_clip']
|
|
400
|
+
normalization_values = config['normalization_values']
|
|
401
|
+
normalize_kwargs = _get_normalize_kwargs(normalization_percentile, normalization_values, normalization_clip)
|
|
402
|
+
|
|
403
|
+
return normalize_kwargs
|
|
404
|
+
|
|
405
|
+
def _get_normalize_kwargs(normalization_percentile, normalization_values, normalization_clip):
|
|
406
|
+
|
|
407
|
+
values = []
|
|
408
|
+
percentiles = []
|
|
409
|
+
for k in range(len(normalization_percentile)):
|
|
410
|
+
if normalization_percentile[k]:
|
|
411
|
+
percentiles.append(normalization_values[k])
|
|
412
|
+
values.append(None)
|
|
413
|
+
else:
|
|
414
|
+
percentiles.append(None)
|
|
415
|
+
values.append(normalization_values[k])
|
|
416
|
+
|
|
417
|
+
return {"percentiles": percentiles, 'values': values, 'clip': normalization_clip}
|
|
418
|
+
|
|
419
|
+
def _segment_image_with_cellpose_model(img, model=None, diameter=None, cellprob_threshold=None, flow_threshold=None, channel_axis=-1):
|
|
420
|
+
|
|
421
|
+
"""
|
|
422
|
+
Segments an input image using a Cellpose model.
|
|
423
|
+
|
|
424
|
+
This function applies a preloaded Cellpose model to segment an input image and returns the resulting labeled mask.
|
|
425
|
+
The image is rearranged into the format expected by the Cellpose model, with the specified channel axis moved to the first dimension.
|
|
426
|
+
|
|
427
|
+
Parameters
|
|
428
|
+
----------
|
|
429
|
+
img : ndarray
|
|
430
|
+
The input image to be segmented. It is expected to have a channel axis specified by `channel_axis`.
|
|
431
|
+
model : CellposeModel, optional
|
|
432
|
+
A preloaded Cellpose model instance used for segmentation.
|
|
433
|
+
diameter : float, optional
|
|
434
|
+
The diameter of objects to segment. If `None`, the model's default diameter is used.
|
|
435
|
+
cellprob_threshold : float, optional
|
|
436
|
+
The threshold for the probability of cells used during segmentation. If `None`, the default threshold is used.
|
|
437
|
+
flow_threshold : float, optional
|
|
438
|
+
The threshold for flow error during segmentation. If `None`, the default threshold is used.
|
|
439
|
+
channel_axis : int, optional
|
|
440
|
+
The axis of the input image that represents the channels. Default is `-1` (channel-last format).
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
ndarray
|
|
445
|
+
A labeled mask of the same spatial dimensions as the input image, with segmented regions assigned unique
|
|
446
|
+
integer labels. The dtype of the mask is `uint16`.
|
|
447
|
+
|
|
448
|
+
Notes
|
|
449
|
+
-----
|
|
450
|
+
- The `img` array is internally rearranged to move the specified `channel_axis` to the first dimension to comply
|
|
451
|
+
with the Cellpose model's input requirements.
|
|
452
|
+
- Ensure the provided `model` is a properly initialized Cellpose model instance.
|
|
453
|
+
- Parameters `diameter`, `cellprob_threshold`, and `flow_threshold` allow fine-tuning of the segmentation process.
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
img = np.moveaxis(img, channel_axis, 0)
|
|
457
|
+
lbl, _, _ = model.eval(img, diameter = diameter, cellprob_threshold=cellprob_threshold, flow_threshold=flow_threshold, channels=None, normalize=False)
|
|
458
|
+
|
|
459
|
+
return lbl.astype(np.uint16)
|
|
460
|
+
|
|
461
|
+
def _segment_image_with_stardist_model(img, model=None, return_details=False, channel_axis=-1):
|
|
462
|
+
|
|
463
|
+
"""
|
|
464
|
+
Segments an input image using a StarDist model.
|
|
465
|
+
|
|
466
|
+
This function applies a preloaded StarDist model to segment an input image and returns the resulting labeled mask.
|
|
467
|
+
Optionally, additional details about the segmentation can also be returned.
|
|
468
|
+
|
|
469
|
+
Parameters
|
|
470
|
+
----------
|
|
471
|
+
img : ndarray
|
|
472
|
+
The input image to be segmented. It is expected to have a channel axis specified by `channel_axis`.
|
|
473
|
+
model : StarDist2D, optional
|
|
474
|
+
A preloaded StarDist model instance used for segmentation.
|
|
475
|
+
return_details : bool, optional
|
|
476
|
+
Whether to return additional details from the model alongside the labeled mask. Default is `False`.
|
|
477
|
+
channel_axis : int, optional
|
|
478
|
+
The axis of the input image that represents the channels. Default is `-1` (channel-last format).
|
|
479
|
+
|
|
480
|
+
Returns
|
|
481
|
+
-------
|
|
482
|
+
ndarray
|
|
483
|
+
A labeled mask of the same spatial dimensions as the input image, with segmented regions assigned unique
|
|
484
|
+
integer labels. The dtype of the mask is `uint16`.
|
|
485
|
+
tuple of (ndarray, dict), optional
|
|
486
|
+
If `return_details` is `True`, returns a tuple where the first element is the labeled mask and the second
|
|
487
|
+
element is a dictionary containing additional details about the segmentation.
|
|
488
|
+
|
|
489
|
+
Notes
|
|
490
|
+
-----
|
|
491
|
+
- The `img` array is internally rearranged to move the specified `channel_axis` to the last dimension to comply
|
|
492
|
+
with the StarDist model's input requirements.
|
|
493
|
+
- Ensure the provided `model` is a properly initialized StarDist model instance.
|
|
494
|
+
- The model automatically determines the number of tiles (`n_tiles`) required for processing large images.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
if channel_axis!=-1:
|
|
498
|
+
img = np.moveaxis(img, channel_axis, -1)
|
|
499
|
+
|
|
500
|
+
lbl, details = model.predict_instances(img, n_tiles=model._guess_n_tiles(img), show_tile_progress=False, verbose=False)
|
|
501
|
+
if not return_details:
|
|
502
|
+
return lbl.astype(np.uint16)
|
|
503
|
+
else:
|
|
504
|
+
return lbl.astype(np.uint16), details
|
|
505
|
+
|
|
506
|
+
def _rescale_labels(lbl, scale_model=1):
|
|
507
|
+
return zoom(lbl, [1./scale_model, 1./scale_model], order=0)
|
|
33
508
|
|
|
34
509
|
def extract_cols_from_table_list(tables, nrows=1):
|
|
510
|
+
|
|
511
|
+
"""
|
|
512
|
+
Extracts a unique list of column names from a list of CSV tables.
|
|
513
|
+
|
|
514
|
+
Parameters
|
|
515
|
+
----------
|
|
516
|
+
tables : list of str
|
|
517
|
+
A list of file paths to the CSV tables from which to extract column names.
|
|
518
|
+
nrows : int, optional
|
|
519
|
+
The number of rows to read from each table to identify the columns.
|
|
520
|
+
Default is 1.
|
|
521
|
+
|
|
522
|
+
Returns
|
|
523
|
+
-------
|
|
524
|
+
numpy.ndarray
|
|
525
|
+
An array of unique column names found across all the tables.
|
|
526
|
+
|
|
527
|
+
Notes
|
|
528
|
+
-----
|
|
529
|
+
- This function reads only the first `nrows` rows of each table to improve performance when dealing with large files.
|
|
530
|
+
- The function ensures that column names are unique by consolidating them using `numpy.unique`.
|
|
531
|
+
|
|
532
|
+
Examples
|
|
533
|
+
--------
|
|
534
|
+
>>> tables = ["table1.csv", "table2.csv"]
|
|
535
|
+
>>> extract_cols_from_table_list(tables)
|
|
536
|
+
array(['Column1', 'Column2', 'Column3'], dtype='<U8')
|
|
537
|
+
"""
|
|
538
|
+
|
|
35
539
|
all_columns = []
|
|
36
540
|
for tab in tables:
|
|
37
541
|
cols = pd.read_csv(tab, nrows=1).columns.tolist()
|
|
@@ -41,18 +545,48 @@ def extract_cols_from_table_list(tables, nrows=1):
|
|
|
41
545
|
|
|
42
546
|
def safe_log(array):
|
|
43
547
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
548
|
+
"""
|
|
549
|
+
Safely computes the base-10 logarithm for numeric inputs, handling invalid or non-positive values.
|
|
550
|
+
|
|
551
|
+
Parameters
|
|
552
|
+
----------
|
|
553
|
+
array : int, float, list, or numpy.ndarray
|
|
554
|
+
The input value or array for which to compute the logarithm.
|
|
555
|
+
Can be a single number (int or float), a list, or a numpy array.
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
float or numpy.ndarray
|
|
560
|
+
- If the input is a single numeric value, returns the base-10 logarithm as a float, or `np.nan` if the value is non-positive.
|
|
561
|
+
- If the input is a list or numpy array, returns a numpy array with the base-10 logarithm of each element.
|
|
562
|
+
Invalid or non-positive values are replaced with `np.nan`.
|
|
563
|
+
|
|
564
|
+
Notes
|
|
565
|
+
-----
|
|
566
|
+
- Non-positive values (`<= 0`) are considered invalid and will result in `np.nan`.
|
|
567
|
+
- NaN values in the input array are preserved in the output.
|
|
568
|
+
- If the input is a list, it is converted to a numpy array for processing.
|
|
569
|
+
|
|
570
|
+
Examples
|
|
571
|
+
--------
|
|
572
|
+
>>> safe_log(10)
|
|
573
|
+
1.0
|
|
574
|
+
|
|
575
|
+
>>> safe_log(-5)
|
|
576
|
+
nan
|
|
577
|
+
|
|
578
|
+
>>> safe_log([10, 0, -5, 100])
|
|
579
|
+
array([1.0, nan, nan, 2.0])
|
|
580
|
+
|
|
581
|
+
>>> import numpy as np
|
|
582
|
+
>>> safe_log(np.array([1, 10, 100]))
|
|
583
|
+
array([0.0, 1.0, 2.0])
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
array = np.asarray(array, dtype=float)
|
|
587
|
+
result = np.where(array > 0, np.log10(array), np.nan)
|
|
588
|
+
|
|
589
|
+
return result.item() if np.isscalar(array) else result
|
|
56
590
|
|
|
57
591
|
def contour_of_instance_segmentation(label, distance):
|
|
58
592
|
|
|
@@ -114,20 +648,35 @@ def contour_of_instance_segmentation(label, distance):
|
|
|
114
648
|
|
|
115
649
|
def extract_identity_col(trajectories):
|
|
116
650
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
id_col = 'TRACK_ID'
|
|
120
|
-
else:
|
|
121
|
-
if 'ID' in list(trajectories.columns):
|
|
122
|
-
id_col = 'ID'
|
|
123
|
-
elif 'ID' in list(trajectories.columns):
|
|
124
|
-
|
|
125
|
-
id_col = 'ID'
|
|
126
|
-
else:
|
|
127
|
-
print('ID or TRACK ID column could not be found in the table...')
|
|
128
|
-
id_col = None
|
|
651
|
+
"""
|
|
652
|
+
Determines the identity column name in a DataFrame of trajectories.
|
|
129
653
|
|
|
130
|
-
|
|
654
|
+
This function checks the provided DataFrame for the presence of a column
|
|
655
|
+
that can serve as the identity column. It first looks for the column
|
|
656
|
+
'TRACK_ID'. If 'TRACK_ID' exists but contains only null values, it checks
|
|
657
|
+
for the column 'ID' instead. If neither column is found, the function
|
|
658
|
+
returns `None` and prints a message indicating the issue.
|
|
659
|
+
|
|
660
|
+
Parameters
|
|
661
|
+
----------
|
|
662
|
+
trajectories : pandas.DataFrame
|
|
663
|
+
A DataFrame containing trajectory data. The function assumes that
|
|
664
|
+
the identity of each trajectory might be stored in either the
|
|
665
|
+
'TRACK_ID' or 'ID' column.
|
|
666
|
+
|
|
667
|
+
Returns
|
|
668
|
+
-------
|
|
669
|
+
str or None
|
|
670
|
+
The name of the identity column ('TRACK_ID' or 'ID') if found;
|
|
671
|
+
otherwise, `None`.
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
for col in ['TRACK_ID', 'ID']:
|
|
675
|
+
if col in trajectories.columns and not trajectories[col].isnull().all():
|
|
676
|
+
return col
|
|
677
|
+
|
|
678
|
+
print('ID or TRACK_ID column could not be found in the table...')
|
|
679
|
+
return None
|
|
131
680
|
|
|
132
681
|
def derivative(x, timeline, window, mode='bi'):
|
|
133
682
|
|
|
@@ -187,7 +736,7 @@ def derivative(x, timeline, window, mode='bi'):
|
|
|
187
736
|
if mode=='bi':
|
|
188
737
|
assert window%2==1,'Please set an odd window for the bidirectional mode'
|
|
189
738
|
lower_bound = window//2
|
|
190
|
-
upper_bound = len(x) - window//2
|
|
739
|
+
upper_bound = len(x) - window//2
|
|
191
740
|
elif mode=='forward':
|
|
192
741
|
lower_bound = 0
|
|
193
742
|
upper_bound = len(x) - window
|
|
@@ -197,7 +746,7 @@ def derivative(x, timeline, window, mode='bi'):
|
|
|
197
746
|
|
|
198
747
|
for t in range(lower_bound,upper_bound):
|
|
199
748
|
if mode=='bi':
|
|
200
|
-
dxdt[t] = (x[t+window//2
|
|
749
|
+
dxdt[t] = (x[t+window//2] - x[t-window//2]) / (timeline[t+window//2] - timeline[t-window//2])
|
|
201
750
|
elif mode=='forward':
|
|
202
751
|
dxdt[t] = (x[t+window] - x[t]) / (timeline[t+window] - timeline[t])
|
|
203
752
|
elif mode=='backward':
|
|
@@ -533,6 +1082,11 @@ def mask_edges(binary_mask, border_size):
|
|
|
533
1082
|
|
|
534
1083
|
return binary_mask
|
|
535
1084
|
|
|
1085
|
+
def demangle_column_name(name):
|
|
1086
|
+
if name.startswith("BACKTICK_QUOTED_STRING_"):
|
|
1087
|
+
# Unquote backtick-quoted string.
|
|
1088
|
+
return name[len("BACKTICK_QUOTED_STRING_"):].replace("_DOT_", ".").replace("_SLASH_", "/")
|
|
1089
|
+
return name
|
|
536
1090
|
|
|
537
1091
|
def extract_cols_from_query(query: str):
|
|
538
1092
|
|
|
@@ -556,13 +1110,6 @@ def extract_cols_from_query(query: str):
|
|
|
556
1110
|
# Add the name to the globals dictionary with a dummy value.
|
|
557
1111
|
variables[name] = None
|
|
558
1112
|
|
|
559
|
-
# Reverse mangling for special characters in column names.
|
|
560
|
-
def demangle_column_name(name):
|
|
561
|
-
if name.startswith("BACKTICK_QUOTED_STRING_"):
|
|
562
|
-
# Unquote backtick-quoted string.
|
|
563
|
-
return name[len("BACKTICK_QUOTED_STRING_"):].replace("_DOT_", ".").replace("_SLASH_", "/")
|
|
564
|
-
return name
|
|
565
|
-
|
|
566
1113
|
return [demangle_column_name(name) for name in variables.keys()]
|
|
567
1114
|
|
|
568
1115
|
def create_patch_mask(h, w, center=None, radius=None):
|
|
@@ -1307,7 +1854,6 @@ def _extract_channel_indices_from_config(config, channels_to_extract):
|
|
|
1307
1854
|
# [1, 2] or None if an error occurs or the channels are not found.
|
|
1308
1855
|
"""
|
|
1309
1856
|
|
|
1310
|
-
# V2
|
|
1311
1857
|
channels = []
|
|
1312
1858
|
for c in channels_to_extract:
|
|
1313
1859
|
try:
|
|
@@ -1319,30 +1865,6 @@ def _extract_channel_indices_from_config(config, channels_to_extract):
|
|
|
1319
1865
|
if np.all([c is None for c in channels]):
|
|
1320
1866
|
channels = None
|
|
1321
1867
|
|
|
1322
|
-
# channels = []
|
|
1323
|
-
# for c in channels_to_extract:
|
|
1324
|
-
# if c!='None' and c is not None:
|
|
1325
|
-
# try:
|
|
1326
|
-
# c1 = int(ConfigSectionMap(config,"Channels")[c])
|
|
1327
|
-
# channels.append(c1)
|
|
1328
|
-
# except Exception as e:
|
|
1329
|
-
# print(f"Error {e}. The channel required by the model is not available in your data... Check the configuration file.")
|
|
1330
|
-
# channels = None
|
|
1331
|
-
# break
|
|
1332
|
-
# else:
|
|
1333
|
-
# channels.append(None)
|
|
1334
|
-
|
|
1335
|
-
# LEGACY
|
|
1336
|
-
if channels is None:
|
|
1337
|
-
channels = []
|
|
1338
|
-
for c in channels_to_extract:
|
|
1339
|
-
try:
|
|
1340
|
-
c1 = int(ConfigSectionMap(config,"MovieSettings")[c])
|
|
1341
|
-
channels.append(c1)
|
|
1342
|
-
except Exception as e:
|
|
1343
|
-
print(f"Error {e}. The channel required by the model is not available in your data... Check the configuration file.")
|
|
1344
|
-
channels = None
|
|
1345
|
-
break
|
|
1346
1868
|
return channels
|
|
1347
1869
|
|
|
1348
1870
|
def _extract_nbr_channels_from_config(config, return_names=False):
|
|
@@ -2242,7 +2764,7 @@ def load_image_dataset(datasets, channels, train_spatial_calibration=None, mask_
|
|
|
2242
2764
|
intersection = list(set(list(channels)) & set(list(existing_channels)))
|
|
2243
2765
|
print(f'{existing_channels=} {intersection=}')
|
|
2244
2766
|
if len(intersection)==0:
|
|
2245
|
-
print(
|
|
2767
|
+
print('Channels could not be found in the config... Skipping image.')
|
|
2246
2768
|
continue
|
|
2247
2769
|
else:
|
|
2248
2770
|
ch_idx = []
|
|
@@ -2384,13 +2906,6 @@ def download_zenodo_file(file, output_dir):
|
|
|
2384
2906
|
if len(file_to_rename)>0 and not file_to_rename[0].endswith(os.sep) and not file.startswith('demo'):
|
|
2385
2907
|
os.rename(file_to_rename[0], os.sep.join([output_dir,file,file]))
|
|
2386
2908
|
|
|
2387
|
-
#if file.startswith('db_'):
|
|
2388
|
-
# os.rename(os.sep.join([output_dir,file.replace('db_','')]), os.sep.join([output_dir,file]))
|
|
2389
|
-
#if file=='db-si-NucPI':
|
|
2390
|
-
# os.rename(os.sep.join([output_dir,'db2-NucPI']), os.sep.join([output_dir,file]))
|
|
2391
|
-
#if file=='db-si-NucCondensation':
|
|
2392
|
-
# os.rename(os.sep.join([output_dir,'db1-NucCondensation']), os.sep.join([output_dir,file]))
|
|
2393
|
-
|
|
2394
2909
|
os.remove(path_to_zip_file)
|
|
2395
2910
|
|
|
2396
2911
|
def interpolate_nan(img, method='nearest'):
|
|
@@ -2415,6 +2930,11 @@ def interpolate_nan(img, method='nearest'):
|
|
|
2415
2930
|
else:
|
|
2416
2931
|
return img
|
|
2417
2932
|
|
|
2933
|
+
|
|
2934
|
+
def interpolate_nan_multichannel(frames):
|
|
2935
|
+
frames = np.moveaxis([interpolate_nan(frames[:,:,c].copy()) for c in range(frames.shape[-1])],0,-1)
|
|
2936
|
+
return frames
|
|
2937
|
+
|
|
2418
2938
|
def collapse_trajectories_by_status(df, status=None, projection='mean', population='effectors', groupby_columns=['position','TRACK_ID']):
|
|
2419
2939
|
|
|
2420
2940
|
static_columns = ['well_index', 'well_name', 'pos_name', 'position', 'well', 'status', 't0', 'class','cell_type','concentration', 'antibody', 'pharmaceutical_agent','TRACK_ID','position', 'neighbor_population', 'reference_population', 'NEIGHBOR_ID', 'REFERENCE_ID', 'FRAME']
|