celldetective 1.0.2__py3-none-any.whl → 1.1.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 (56) hide show
  1. celldetective/__main__.py +2 -2
  2. celldetective/events.py +2 -44
  3. celldetective/filters.py +4 -5
  4. celldetective/gui/__init__.py +1 -1
  5. celldetective/gui/analyze_block.py +37 -10
  6. celldetective/gui/btrack_options.py +24 -23
  7. celldetective/gui/classifier_widget.py +62 -19
  8. celldetective/gui/configure_new_exp.py +32 -35
  9. celldetective/gui/control_panel.py +115 -81
  10. celldetective/gui/gui_utils.py +674 -396
  11. celldetective/gui/json_readers.py +7 -6
  12. celldetective/gui/layouts.py +755 -0
  13. celldetective/gui/measurement_options.py +168 -487
  14. celldetective/gui/neighborhood_options.py +322 -270
  15. celldetective/gui/plot_measurements.py +1114 -0
  16. celldetective/gui/plot_signals_ui.py +20 -20
  17. celldetective/gui/process_block.py +449 -169
  18. celldetective/gui/retrain_segmentation_model_options.py +27 -26
  19. celldetective/gui/retrain_signal_model_options.py +25 -24
  20. celldetective/gui/seg_model_loader.py +31 -27
  21. celldetective/gui/signal_annotator.py +2326 -2295
  22. celldetective/gui/signal_annotator_options.py +18 -16
  23. celldetective/gui/styles.py +16 -1
  24. celldetective/gui/survival_ui.py +61 -39
  25. celldetective/gui/tableUI.py +60 -23
  26. celldetective/gui/thresholds_gui.py +68 -66
  27. celldetective/gui/viewers.py +596 -0
  28. celldetective/io.py +234 -23
  29. celldetective/measure.py +37 -32
  30. celldetective/neighborhood.py +495 -27
  31. celldetective/preprocessing.py +683 -0
  32. celldetective/scripts/analyze_signals.py +7 -0
  33. celldetective/scripts/measure_cells.py +12 -0
  34. celldetective/scripts/segment_cells.py +5 -0
  35. celldetective/scripts/track_cells.py +11 -0
  36. celldetective/signals.py +221 -98
  37. celldetective/tracking.py +0 -1
  38. celldetective/utils.py +178 -36
  39. celldetective-1.1.0.dist-info/METADATA +305 -0
  40. celldetective-1.1.0.dist-info/RECORD +80 -0
  41. {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/top_level.txt +1 -0
  42. tests/__init__.py +0 -0
  43. tests/test_events.py +28 -0
  44. tests/test_filters.py +24 -0
  45. tests/test_io.py +70 -0
  46. tests/test_measure.py +141 -0
  47. tests/test_neighborhood.py +70 -0
  48. tests/test_segmentation.py +93 -0
  49. tests/test_signals.py +135 -0
  50. tests/test_tracking.py +164 -0
  51. tests/test_utils.py +71 -0
  52. celldetective-1.0.2.dist-info/METADATA +0 -192
  53. celldetective-1.0.2.dist-info/RECORD +0 -66
  54. {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/LICENSE +0 -0
  55. {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/WHEEL +0 -0
  56. {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,683 @@
1
+ """
2
+ Copright © 2024 Laboratoire Adhesion et Inflammation, Authored by Remy Torro & Ksenija Dervanova.
3
+ """
4
+
5
+ from tqdm import tqdm
6
+ import numpy as np
7
+ import os
8
+ from celldetective.io import get_config, get_experiment_wells, interpret_wells_and_positions, extract_well_name_and_number, get_positions_in_well, extract_position_name, get_position_movie_path, load_frames, auto_load_number_of_frames
9
+ from celldetective.utils import ConfigSectionMap, _extract_channel_indices_from_config, _extract_nbr_channels_from_config, _get_img_num_per_channel
10
+ from celldetective.filters import std_filter, gauss_filter
11
+ from stardist import fill_label_holes
12
+ from csbdeep.io import save_tiff_imagej_compatible
13
+ from gc import collect
14
+ from lmfit import Parameters, Model, models
15
+ import matplotlib.pyplot as plt
16
+
17
+ def estimate_background_per_condition(experiment, threshold_on_std=1, well_option='*', target_channel="channel_name", frame_range=[0,5], mode="timeseries", show_progress_per_pos=False, show_progress_per_well=True):
18
+
19
+ """
20
+ Estimate the background per condition in an experiment.
21
+
22
+ This function calculates the background of each well in the given experiment
23
+ by analyzing frames from the specified range. It supports two modes: "timeseries"
24
+ and "tiles". The function applies Gaussian and standard deviation filters to
25
+ identify and mask out high-variance areas, and computes the median background
26
+ across positions within each well.
27
+
28
+ Parameters
29
+ ----------
30
+ experiment : object
31
+ The experiment object containing well and position information.
32
+ threshold_on_std : float, optional
33
+ The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
34
+ well_option : str, int, or list of int, optional
35
+ The well selection option:
36
+ - '*' : Select all wells.
37
+ - int : Select a specific well by its index.
38
+ - list of int : Select multiple wells by their indices. Defaults to '*'.
39
+ target_channel : str
40
+ The specific channel to be analyzed.
41
+ frame_range : list of int, optional
42
+ The range of frames to be analyzed, specified as [start, end]. Defaults to [0, 5].
43
+ mode : {'timeseries', 'tiles'}, optional
44
+ The mode of analysis. "timeseries" averages frames before filtering, while "tiles" filters each frame individually. Defaults to "timeseries".
45
+ show_progress_per_pos : bool, optional
46
+ If True, display a progress bar for position processing. Defaults to False.
47
+ show_progress_per_well : bool, optional
48
+ If True, display a progress bar for well processing. Defaults to True.
49
+
50
+ Returns
51
+ -------
52
+ backgrounds : list of dict
53
+ A list of dictionaries, each containing:
54
+ - 'bg' : numpy.ndarray
55
+ The computed background for the well.
56
+ - 'well' : str
57
+ The path to the well.
58
+
59
+ Examples
60
+ --------
61
+ >>> experiment = ... # Some experiment object
62
+ >>> backgrounds = estimate_background_per_condition(experiment, threshold_on_std=1.5, well_option=[0, 1, 2], target_channel='DAPI', frame_range=[0, 10], mode="tiles")
63
+ >>> print(backgrounds[0]['bg']) # The background array for the first well
64
+ >>> print(backgrounds[0]['well']) # The path to the first well
65
+ """
66
+
67
+
68
+ config = get_config(experiment)
69
+ wells = get_experiment_wells(experiment)
70
+ len_movie = float(ConfigSectionMap(config,"MovieSettings")["len_movie"])
71
+ movie_prefix = ConfigSectionMap(config,"MovieSettings")["movie_prefix"]
72
+
73
+ well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, "*")
74
+
75
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
76
+ nbr_channels = _extract_nbr_channels_from_config(config)
77
+ img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
78
+
79
+ backgrounds = []
80
+
81
+ for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
82
+
83
+ well_name, _ = extract_well_name_and_number(well_path)
84
+ well_idx = well_indices[k]
85
+
86
+ positions = get_positions_in_well(well_path)
87
+ print(f"Reconstruct a background in well {well_name} from positions: {[extract_position_name(p) for p in positions]}...")
88
+
89
+ frame_mean_per_position = []
90
+
91
+ for l,pos_path in enumerate(tqdm(positions, disable=not show_progress_per_pos)):
92
+
93
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
94
+
95
+ if mode=="timeseries":
96
+
97
+ frames = load_frames(img_num_channels[0,frame_range[0]:frame_range[1]], stack_path, normalize_input=False)
98
+ frames = np.moveaxis(frames, -1, 0).astype(float)
99
+
100
+ for i in range(len(frames)):
101
+ if np.all(frames[i].flatten()==0):
102
+ frames[i,:,:] = np.nan
103
+
104
+ frame_mean = np.nanmean(frames, axis=0)
105
+
106
+ frame = frame_mean.copy().astype(float)
107
+ frame = gauss_filter(frame, 2)
108
+ std_frame = std_filter(frame, 4)
109
+
110
+ mask = std_frame > threshold_on_std
111
+ mask = fill_label_holes(mask)
112
+ frame[np.where(mask==1)] = np.nan
113
+
114
+ elif mode=="tiles":
115
+
116
+ frames = load_frames(img_num_channels[0,:], stack_path, normalize_input=False).astype(float)
117
+ frames = np.moveaxis(frames, -1, 0).astype(float)
118
+
119
+ for i in range(len(frames)):
120
+
121
+ if np.all(frames[i].flatten()==0):
122
+ frames[i,:,:] = np.nan
123
+ continue
124
+
125
+ f = frames[i].copy()
126
+ f = gauss_filter(f, 2)
127
+ std_frame = std_filter(f, 4)
128
+
129
+ mask = std_frame > threshold_on_std
130
+ mask = fill_label_holes(mask)
131
+ f[np.where(mask==1)] = np.nan
132
+
133
+ frames[i,:,:] = f
134
+
135
+ frame = np.nanmedian(frames, axis=0)
136
+
137
+ # store
138
+ frame_mean_per_position.append(frame)
139
+
140
+ background = np.nanmedian(frame_mean_per_position,axis=0)
141
+ backgrounds.append({"bg": background, "well": well_path})
142
+ print(f"Background successfully computed for well {well_name}...")
143
+
144
+ return backgrounds
145
+
146
+ def correct_background_model_free(
147
+ experiment,
148
+ well_option='*',
149
+ position_option='*',
150
+ target_channel="channel_name",
151
+ mode = "timeseries",
152
+ threshold_on_std = 1,
153
+ frame_range = [0,5],
154
+ optimize_option = False,
155
+ opt_coef_range = [0.95,1.05],
156
+ opt_coef_nbr = 100,
157
+ operation = 'divide',
158
+ clip = False,
159
+ show_progress_per_well = True,
160
+ show_progress_per_pos = False,
161
+ export = False,
162
+ return_stacks = False,
163
+ movie_prefix=None,
164
+ export_prefix='Corrected',
165
+ **kwargs,
166
+ ):
167
+
168
+ """
169
+ Correct the background of image stacks for a given experiment.
170
+
171
+ This function processes image stacks by estimating and correcting the background
172
+ for each well and position in the experiment. It supports different modes, such
173
+ as timeseries or tiles, and offers options for optimization and exporting the results.
174
+
175
+ Parameters
176
+ ----------
177
+ experiment : str
178
+ Path to the experiment configuration.
179
+ well_option : str, int, or list of int, optional
180
+ Selection of wells to process. '*' indicates all wells. Defaults to '*'.
181
+ position_option : str, int, or list of int, optional
182
+ Selection of positions to process within each well. '*' indicates all positions. Defaults to '*'.
183
+ target_channel : str, optional
184
+ The name of the target channel to be corrected. Defaults to "channel_name".
185
+ mode : {'timeseries', 'tiles'}, optional
186
+ The mode of processing. Defaults to "timeseries".
187
+ threshold_on_std : float, optional
188
+ The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
189
+ frame_range : list of int, optional
190
+ The range of frames to consider for background estimation. Defaults to [0, 5].
191
+ optimize_option : bool, optional
192
+ If True, optimize the correction coefficient. Defaults to False.
193
+ opt_coef_range : list of float, optional
194
+ The range of coefficients to try for optimization. Defaults to [0.95, 1.05].
195
+ opt_coef_nbr : int, optional
196
+ The number of coefficients to test within the optimization range. Defaults to 100.
197
+ operation : {'divide', 'subtract'}, optional
198
+ The operation to apply for background correction. Defaults to 'divide'.
199
+ clip : bool, optional
200
+ If True, clip the corrected values to be non-negative when using subtraction. Defaults to False.
201
+ show_progress_per_well : bool, optional
202
+ If True, show progress bar for each well. Defaults to True.
203
+ show_progress_per_pos : bool, optional
204
+ If True, show progress bar for each position. Defaults to False.
205
+ export : bool, optional
206
+ If True, export the corrected stacks to files. Defaults to False.
207
+ return_stacks : bool, optional
208
+ If True, return the corrected stacks as a list of numpy arrays. Defaults to False.
209
+
210
+ Returns
211
+ -------
212
+ list of numpy.ndarray, optional
213
+ A list of corrected image stacks if `return_stacks` is True.
214
+
215
+ Notes
216
+ -----
217
+ The function uses several helper functions, including `interpret_wells_and_positions`,
218
+ `estimate_background_per_condition`, and `apply_background_to_stack`.
219
+
220
+ Examples
221
+ --------
222
+ >>> experiment = "path/to/experiment/config"
223
+ >>> corrected_stacks = correct_background(experiment, well_option=[0, 1], position_option='*', target_channel="DAPI", mode="timeseries", threshold_on_std=2, frame_range=[0, 10], optimize_option=True, operation='subtract', clip=True, return_stacks=True)
224
+ >>> print(len(corrected_stacks))
225
+ 2
226
+
227
+ """
228
+
229
+ config = get_config(experiment)
230
+ wells = get_experiment_wells(experiment)
231
+ len_movie = float(ConfigSectionMap(config,"MovieSettings")["len_movie"])
232
+ if movie_prefix is None:
233
+ movie_prefix = ConfigSectionMap(config,"MovieSettings")["movie_prefix"]
234
+
235
+ well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
236
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
237
+ nbr_channels = _extract_nbr_channels_from_config(config)
238
+ img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
239
+
240
+ stacks = []
241
+
242
+ for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
243
+
244
+ well_name, _ = extract_well_name_and_number(well_path)
245
+
246
+ try:
247
+ background = estimate_background_per_condition(experiment, threshold_on_std=threshold_on_std, well_option=int(well_indices[k]), target_channel=target_channel, frame_range=frame_range, mode=mode, show_progress_per_pos=True, show_progress_per_well=False)
248
+ background = background[0]
249
+ background = background['bg']
250
+ except Exception as e:
251
+ print(f'Background could not be estimated due to error "{e}"... Skipping well {well_name}...')
252
+ continue
253
+
254
+ positions = get_positions_in_well(well_path)
255
+ selection = positions[position_indices]
256
+ if isinstance(selection[0],np.ndarray):
257
+ selection = selection[0]
258
+
259
+ for pidx,pos_path in enumerate(tqdm(selection, disable=not show_progress_per_pos)):
260
+
261
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
262
+ print(f'Applying the correction to position {extract_position_name(pos_path)}...')
263
+
264
+ corrected_stack = apply_background_to_stack(stack_path,
265
+ background,
266
+ target_channel_index=channel_indices[0],
267
+ nbr_channels=nbr_channels,
268
+ stack_length=len_movie,
269
+ threshold_on_std=threshold_on_std,
270
+ optimize_option=optimize_option,
271
+ opt_coef_range=opt_coef_range,
272
+ opt_coef_nbr=opt_coef_nbr,
273
+ operation=operation,
274
+ clip=clip,
275
+ export=export,
276
+ prefix=export_prefix,
277
+ )
278
+ print('Correction successful.')
279
+ if return_stacks:
280
+ stacks.append(corrected_stack)
281
+ else:
282
+ del corrected_stack
283
+ collect()
284
+
285
+ if return_stacks:
286
+ return stacks
287
+
288
+
289
+
290
+ def apply_background_to_stack(stack_path, background, target_channel_index=0, nbr_channels=1, stack_length=45, threshold_on_std=1, optimize_option=True, opt_coef_range=(0.95,1.05), opt_coef_nbr=100, operation='divide', clip=False, export=False, prefix="Corrected"):
291
+
292
+ """
293
+ Apply background correction to an image stack.
294
+
295
+ This function corrects the background of an image stack by applying a specified operation
296
+ (either division or subtraction) between the image stack and the background. It also supports
297
+ optimization of the correction coefficient through brute-force regression.
298
+
299
+ Parameters
300
+ ----------
301
+ stack_path : str
302
+ The path to the image stack file.
303
+ background : numpy.ndarray
304
+ The background image to be applied for correction.
305
+ target_channel_index : int, optional
306
+ The index of the target channel to be corrected. Defaults to 0.
307
+ nbr_channels : int, optional
308
+ The number of channels in the image stack. Defaults to 1.
309
+ stack_length : int, optional
310
+ The length of the image stack (number of frames). If None, the length is auto-detected. Defaults to 45.
311
+ threshold_on_std : float, optional
312
+ The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
313
+ optimize_option : bool, optional
314
+ If True, optimize the correction coefficient using a range of values. Defaults to True.
315
+ opt_coef_range : tuple of float, optional
316
+ The range of coefficients to try for optimization. Defaults to (0.95, 1.05).
317
+ opt_coef_nbr : int, optional
318
+ The number of coefficients to test within the optimization range. Defaults to 100.
319
+ operation : {'divide', 'subtract'}, optional
320
+ The operation to apply for background correction. Defaults to 'divide'.
321
+ clip : bool, optional
322
+ If True, clip the corrected values to be non-negative when using subtraction. Defaults to False.
323
+ export : bool, optional
324
+ If True, export the corrected stack to a file. Defaults to False.
325
+ prefix : str, optional
326
+ The prefix for the exported file name. Defaults to "Corrected".
327
+
328
+ Returns
329
+ -------
330
+ corrected_stack : numpy.ndarray
331
+ The background-corrected image stack.
332
+
333
+ Examples
334
+ --------
335
+ >>> stack_path = "path/to/stack.tif"
336
+ >>> background = np.zeros((512, 512)) # Example background
337
+ >>> corrected_stack = apply_background_to_stack(stack_path, background, target_channel_index=0, nbr_channels=3, stack_length=45, optimize_option=False, operation='subtract', clip=True)
338
+ >>> print(corrected_stack.shape)
339
+ (44, 512, 512, 3)
340
+
341
+ """
342
+
343
+ if stack_length is None:
344
+ stack_length = auto_load_number_of_frames(stack_path)
345
+ if stack_length is None:
346
+ print('stack length not provided')
347
+ return None
348
+
349
+ if optimize_option:
350
+ coefficients = np.linspace(opt_coef_range[0], opt_coef_range[1], int(opt_coef_nbr))
351
+ coefficients = np.append(coefficients, [1.0])
352
+ if export:
353
+ path,file = os.path.split(stack_path)
354
+ if prefix is None:
355
+ newfile = file
356
+ else:
357
+ newfile = '_'.join([prefix,file])
358
+
359
+ corrected_stack = []
360
+
361
+ for i in range(0,int(stack_length*nbr_channels),nbr_channels):
362
+
363
+ frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
364
+ target_img = frames[:,:,target_channel_index].copy()
365
+
366
+ if optimize_option:
367
+
368
+ target_copy = target_img.copy()
369
+ f = gauss_filter(target_img.copy(), 2)
370
+ std_frame = std_filter(f, 4)
371
+ mask = std_frame > threshold_on_std
372
+ mask = fill_label_holes(mask)
373
+ target_copy[np.where(mask==1)] = np.nan
374
+ loss = []
375
+
376
+ # brute-force regression, could do gradient descent instead
377
+ for c in coefficients:
378
+ diff = np.subtract(target_copy, c*background, where=target_copy==target_copy)
379
+ s = np.sum(np.abs(diff, where=diff==diff), where=diff==diff)
380
+ loss.append(s)
381
+ c = coefficients[np.argmin(loss)]
382
+ print(f"Frame: {i}; optimal coefficient: {c}...")
383
+ # if c==min(coefficients) or c==max(coefficients):
384
+ # print('Warning... The optimal coefficient is beyond the range provided... Please adjust your coefficient range...')
385
+ else:
386
+ c=1
387
+
388
+ if operation=="divide":
389
+ correction = np.divide(target_img, background*c, where=background==background)
390
+ correction[background!=background] = np.nan
391
+ correction[target_img!=target_img] = np.nan
392
+ fill_val = 1.0
393
+
394
+ elif operation=="subtract":
395
+ correction = np.subtract(target_img, background*c, where=background==background)
396
+ correction[background!=background] = np.nan
397
+ correction[target_img!=target_img] = np.nan
398
+ fill_val = 0.0
399
+ if clip:
400
+ correction[correction<=0.] = 0.
401
+
402
+ frames[:,:,target_channel_index] = correction
403
+ corrected_stack.append(frames)
404
+
405
+ corrected_stack = np.array(corrected_stack)
406
+
407
+ if export:
408
+ save_tiff_imagej_compatible(os.sep.join([path,newfile]), corrected_stack, axes='TYXC')
409
+
410
+ return corrected_stack
411
+
412
+ def paraboloid(x, y, a, b, c, d, e, g):
413
+ return a * x ** 2 + b * y ** 2 + c * x * y + d * x + e * y + g
414
+
415
+
416
+ def plane(x, y, a, b, c):
417
+ return a * x + b * y + c
418
+
419
+
420
+ def fit_plane(image, cell_masks=None):
421
+ """
422
+ Fit a plane to the given image data.
423
+
424
+ Parameters:
425
+ - image (numpy.ndarray): The input image data.
426
+ - cell_masks (numpy.ndarray, optional): An array specifying cell masks. If provided, areas covered by
427
+ cell masks will be excluded from the fitting process.
428
+
429
+ Returns:
430
+ - numpy.ndarray: The fitted plane.
431
+
432
+ This function fits a plane to the given image data using least squares regression. It constructs a mesh
433
+ grid based on the dimensions of the image and fits a plane model to the data points. If cell masks are
434
+ provided, areas covered by cell masks will be excluded from the fitting process.
435
+
436
+ Example:
437
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
438
+ >>> result = fit_plane(image)
439
+ >>> print(result)
440
+ [[1. 2. 3.]
441
+ [4. 5. 6.]
442
+ [7. 8. 9.]]
443
+
444
+ Note:
445
+ - The 'cell_masks' parameter allows excluding areas covered by cell masks from the fitting process.
446
+ """
447
+ data = np.empty(image.shape)
448
+ x = np.arange(0, image.shape[1])
449
+ y = np.arange(0, image.shape[0])
450
+ xx, yy = np.meshgrid(x, y)
451
+
452
+ params = Parameters()
453
+ params.add('a', value=1)
454
+ params.add('b', value=1)
455
+ params.add('c', value=1)
456
+
457
+ model = Model(plane, independent_vars=['x', 'y'])
458
+
459
+ weights = np.ones_like(xx, dtype=float)
460
+ weights[np.where(cell_masks > 0)] = 0.
461
+
462
+ result = model.fit(image,
463
+ x=xx,
464
+ y=yy,
465
+ weights=weights,
466
+ params=params, max_nfev=3000)
467
+ del model
468
+ collect()
469
+
470
+ return result.best_fit
471
+
472
+
473
+ def fit_paraboloid(image, cell_masks=None):
474
+ """
475
+ Fit a paraboloid to the given image data.
476
+
477
+ Parameters:
478
+ - image (numpy.ndarray): The input image data.
479
+ - cell_masks (numpy.ndarray, optional): An array specifying cell masks. If provided, areas covered by
480
+ cell masks will be excluded from the fitting process.
481
+
482
+ Returns:
483
+ - numpy.ndarray: The fitted paraboloid.
484
+
485
+ This function fits a paraboloid to the given image data using least squares regression. It constructs
486
+ a mesh grid based on the dimensions of the image and fits a paraboloid model to the data points. If cell
487
+ masks are provided, areas covered by cell masks will be excluded from the fitting process.
488
+
489
+ Example:
490
+ >>> image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
491
+ >>> result = fit_paraboloid(image)
492
+ >>> print(result)
493
+ [[1. 2. 3.]
494
+ [4. 5. 6.]
495
+ [7. 8. 9.]]
496
+
497
+ Note:
498
+ - The 'cell_masks' parameter allows excluding areas covered by cell masks from the fitting process.
499
+ """
500
+ data = np.empty(image.shape)
501
+ x = np.arange(0, image.shape[1])
502
+ y = np.arange(0, image.shape[0])
503
+ xx, yy = np.meshgrid(x, y)
504
+
505
+ params = Parameters()
506
+ params.add('a', value=1.0E-05)
507
+ params.add('b', value=1.0E-05)
508
+ params.add('c', value=1.0E-06)
509
+ params.add('d', value=0.01)
510
+ params.add('e', value=0.01)
511
+ params.add('g', value=100)
512
+
513
+ model = Model(paraboloid, independent_vars=['x', 'y'])
514
+
515
+ weights = np.ones_like(xx, dtype=float)
516
+ weights[np.where(cell_masks > 0)] = 0.
517
+
518
+ result = model.fit(image,
519
+ x=xx,
520
+ y=yy,
521
+ weights=weights,
522
+ params=params, max_nfev=3000)
523
+
524
+ #print(result.fit_report())
525
+ del model
526
+ collect()
527
+ return result.best_fit
528
+
529
+
530
+ def correct_background_model(
531
+ experiment,
532
+ well_option='*',
533
+ position_option='*',
534
+ target_channel="channel_name",
535
+ threshold_on_std = 1,
536
+ model = 'paraboloid',
537
+ operation = 'divide',
538
+ clip = False,
539
+ show_progress_per_well = True,
540
+ show_progress_per_pos = False,
541
+ export = False,
542
+ return_stacks = False,
543
+ movie_prefix=None,
544
+ export_prefix='Corrected',
545
+ **kwargs,
546
+ ):
547
+
548
+ config = get_config(experiment)
549
+ wells = get_experiment_wells(experiment)
550
+ len_movie = float(ConfigSectionMap(config,"MovieSettings")["len_movie"])
551
+ if movie_prefix is None:
552
+ movie_prefix = ConfigSectionMap(config,"MovieSettings")["movie_prefix"]
553
+
554
+ well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
555
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
556
+ nbr_channels = _extract_nbr_channels_from_config(config)
557
+ img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
558
+
559
+ stacks = []
560
+
561
+ for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
562
+
563
+ well_name, _ = extract_well_name_and_number(well_path)
564
+ positions = get_positions_in_well(well_path)
565
+ selection = positions[position_indices]
566
+ if isinstance(selection[0],np.ndarray):
567
+ selection = selection[0]
568
+
569
+ for pidx,pos_path in enumerate(tqdm(selection, disable=not show_progress_per_pos)):
570
+
571
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
572
+ print(f'Applying the correction to position {extract_position_name(pos_path)}...')
573
+ print(stack_path)
574
+
575
+ corrected_stack = fit_and_apply_model_background_to_stack(stack_path,
576
+ target_channel_index=channel_indices[0],
577
+ model = model,
578
+ nbr_channels=nbr_channels,
579
+ stack_length=len_movie,
580
+ threshold_on_std=threshold_on_std,
581
+ operation=operation,
582
+ clip=clip,
583
+ export=export,
584
+ prefix=export_prefix,
585
+ )
586
+ print('Correction successful.')
587
+ if return_stacks:
588
+ stacks.append(corrected_stack)
589
+ else:
590
+ del corrected_stack
591
+ collect()
592
+
593
+ if return_stacks:
594
+ return stacks
595
+
596
+ def fit_and_apply_model_background_to_stack(stack_path,
597
+ target_channel_index=0,
598
+ nbr_channels=1,
599
+ stack_length=45,
600
+ threshold_on_std=1,
601
+ operation='divide',
602
+ model='paraboloid',
603
+ clip=False,
604
+ export=False,
605
+ prefix="Corrected"
606
+ ):
607
+
608
+ stack_length_auto = auto_load_number_of_frames(stack_path)
609
+ if stack_length_auto is None and stack_length is None:
610
+ print('stack length not provided')
611
+ return None
612
+ if stack_length_auto is not None:
613
+ stack_length = stack_length_auto
614
+
615
+ if export:
616
+ path,file = os.path.split(stack_path)
617
+ if prefix is None:
618
+ newfile = file
619
+ else:
620
+ newfile = '_'.join([prefix,file])
621
+
622
+ corrected_stack = []
623
+
624
+ for i in tqdm(range(0,int(stack_length*nbr_channels),nbr_channels)):
625
+
626
+ frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
627
+ target_img = frames[:,:,target_channel_index].copy()
628
+ correction = field_correction(target_img, threshold_on_std=threshold_on_std, operation=operation, model=model, clip=clip)
629
+ frames[:,:,target_channel_index] = correction.copy()
630
+ corrected_stack.append(frames)
631
+
632
+ del frames
633
+ del target_img
634
+ del correction
635
+ collect()
636
+
637
+ corrected_stack = np.array(corrected_stack)
638
+
639
+ if export:
640
+ save_tiff_imagej_compatible(os.sep.join([path,newfile]), corrected_stack, axes='TYXC')
641
+
642
+ return corrected_stack
643
+
644
+ def field_correction(img, threshold_on_std=1, operation='divide', model='paraboloid', clip=False, return_bg=False):
645
+
646
+ target_copy = img.copy().astype(float)
647
+ f = gauss_filter(target_copy, 2)
648
+ std_frame = std_filter(f, 4)
649
+ masks = std_frame > threshold_on_std
650
+ masks = fill_label_holes(masks).astype(int)
651
+
652
+ background = fit_background_model(img, cell_masks=masks, model=model)
653
+
654
+ if operation=="divide":
655
+ correction = np.divide(img, background, where=background==background)
656
+ correction[background!=background] = np.nan
657
+ correction[img!=img] = np.nan
658
+ fill_val = 1.0
659
+
660
+ elif operation=="subtract":
661
+ correction = np.subtract(img, background, where=background==background)
662
+ correction[background!=background] = np.nan
663
+ correction[img!=img] = np.nan
664
+ fill_val = 0.0
665
+ if clip:
666
+ correction[correction<=0.] = 0.
667
+
668
+ if return_bg:
669
+ return correction.copy(), background
670
+ else:
671
+ return correction.copy()
672
+
673
+ def fit_background_model(img, cell_masks=None, model='paraboloid'):
674
+
675
+ if model == "paraboloid":
676
+ bg = fit_paraboloid(img.astype(float), cell_masks=cell_masks).astype(float)
677
+ elif model == "plane":
678
+ bg = fit_plane(img.astype(float), cell_masks=cell_masks).astype(float)
679
+
680
+ if bg is not None:
681
+ bg = np.array(bg)
682
+
683
+ return bg
@@ -3,6 +3,7 @@ Copright © 2022 Laboratoire Adhesion et Inflammation, Authored by Remy Torro.
3
3
  """
4
4
 
5
5
  import argparse
6
+ import datetime
6
7
  import os
7
8
  import gc
8
9
  from art import tprint
@@ -45,6 +46,12 @@ else:
45
46
  print('The trajectories table could not be found. Abort.')
46
47
  os.abort()
47
48
 
49
+ log=f'segmentation model: {model} \n'
50
+
51
+ with open(pos+f'log_{mode}.json', 'a') as f:
52
+ f.write(f'{datetime.datetime.now()} SIGNAL ANALYSIS \n')
53
+ f.write(log)
54
+
48
55
  trajectories = analyze_signals(trajectories.copy(), model, interpolate_na=True, selected_signals=None, column_labels = column_labels, plot_outcome=True,output_dir=pos+'output/')
49
56
  trajectories = trajectories.sort_values(by=[column_labels['track'], column_labels['time']])
50
57
  trajectories.to_csv(pos+os.sep.join(['output','tables', table_name]), index=False)