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.
Files changed (41) hide show
  1. celldetective/_version.py +1 -1
  2. celldetective/events.py +4 -0
  3. celldetective/filters.py +11 -2
  4. celldetective/gui/InitWindow.py +23 -9
  5. celldetective/gui/control_panel.py +19 -11
  6. celldetective/gui/generic_signal_plot.py +5 -0
  7. celldetective/gui/gui_utils.py +2 -2
  8. celldetective/gui/help/DL-segmentation-strategy.json +17 -17
  9. celldetective/gui/help/Threshold-vs-DL.json +11 -11
  10. celldetective/gui/help/cell-populations.json +5 -5
  11. celldetective/gui/help/exp-structure.json +15 -15
  12. celldetective/gui/help/feature-btrack.json +5 -5
  13. celldetective/gui/help/neighborhood.json +7 -7
  14. celldetective/gui/help/prefilter-for-segmentation.json +7 -7
  15. celldetective/gui/help/preprocessing.json +19 -19
  16. celldetective/gui/help/propagate-classification.json +7 -7
  17. celldetective/gui/neighborhood_options.py +1 -1
  18. celldetective/gui/plot_signals_ui.py +13 -9
  19. celldetective/gui/process_block.py +63 -14
  20. celldetective/gui/retrain_segmentation_model_options.py +21 -8
  21. celldetective/gui/retrain_signal_model_options.py +12 -2
  22. celldetective/gui/signal_annotator.py +9 -0
  23. celldetective/gui/signal_annotator2.py +25 -17
  24. celldetective/gui/styles.py +1 -0
  25. celldetective/gui/tableUI.py +1 -1
  26. celldetective/gui/workers.py +136 -0
  27. celldetective/io.py +54 -28
  28. celldetective/measure.py +112 -14
  29. celldetective/scripts/measure_cells.py +36 -46
  30. celldetective/scripts/segment_cells.py +35 -78
  31. celldetective/scripts/segment_cells_thresholds.py +21 -22
  32. celldetective/scripts/track_cells.py +43 -32
  33. celldetective/segmentation.py +16 -62
  34. celldetective/signals.py +11 -7
  35. celldetective/utils.py +587 -67
  36. {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/METADATA +1 -1
  37. {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/RECORD +41 -40
  38. {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/LICENSE +0 -0
  39. {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/WHEEL +0 -0
  40. {celldetective-1.3.6.post1.dist-info → celldetective-1.3.7.dist-info}/entry_points.txt +0 -0
  41. {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
- if isinstance(array,int) or isinstance(array,float):
45
- if value<=0.:
46
- return np.nan
47
- else:
48
- return np.log10(value)
49
- else:
50
- if isinstance(array, list):
51
- array = np.array(array)
52
- output_array = np.zeros_like(array).astype(float)
53
- output_array[:] = np.nan
54
- output_array[array==array] = np.log10(array[array==array])
55
- return output_array
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
- if 'TRACK_ID' in list(trajectories.columns):
118
- if not np.all(trajectories['TRACK_ID'].isnull()):
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
- return id_col
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 - 1
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+1] - x[t-window//2]) / (timeline[t+window//2+1] - timeline[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(e,' channels could not be found in the config... Skipping image.')
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']