celldetective 1.0.2.post1__py3-none-any.whl → 1.1.1__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 (63) hide show
  1. celldetective/__main__.py +7 -21
  2. celldetective/events.py +2 -44
  3. celldetective/extra_properties.py +62 -52
  4. celldetective/filters.py +4 -5
  5. celldetective/gui/__init__.py +1 -1
  6. celldetective/gui/analyze_block.py +37 -10
  7. celldetective/gui/btrack_options.py +24 -23
  8. celldetective/gui/classifier_widget.py +62 -19
  9. celldetective/gui/configure_new_exp.py +32 -35
  10. celldetective/gui/control_panel.py +120 -81
  11. celldetective/gui/gui_utils.py +674 -396
  12. celldetective/gui/json_readers.py +7 -6
  13. celldetective/gui/layouts.py +756 -0
  14. celldetective/gui/measurement_options.py +98 -513
  15. celldetective/gui/neighborhood_options.py +322 -270
  16. celldetective/gui/plot_measurements.py +1114 -0
  17. celldetective/gui/plot_signals_ui.py +21 -20
  18. celldetective/gui/process_block.py +449 -169
  19. celldetective/gui/retrain_segmentation_model_options.py +27 -26
  20. celldetective/gui/retrain_signal_model_options.py +25 -24
  21. celldetective/gui/seg_model_loader.py +31 -27
  22. celldetective/gui/signal_annotator.py +2326 -2295
  23. celldetective/gui/signal_annotator_options.py +18 -16
  24. celldetective/gui/styles.py +16 -1
  25. celldetective/gui/survival_ui.py +67 -39
  26. celldetective/gui/tableUI.py +337 -48
  27. celldetective/gui/thresholds_gui.py +75 -71
  28. celldetective/gui/viewers.py +743 -0
  29. celldetective/io.py +247 -27
  30. celldetective/measure.py +43 -263
  31. celldetective/models/segmentation_effectors/primNK_cfse/config_input.json +29 -0
  32. celldetective/models/segmentation_effectors/primNK_cfse/cp-cfse-transfer +0 -0
  33. celldetective/models/segmentation_effectors/primNK_cfse/training_instructions.json +37 -0
  34. celldetective/neighborhood.py +498 -27
  35. celldetective/preprocessing.py +1023 -0
  36. celldetective/scripts/analyze_signals.py +7 -0
  37. celldetective/scripts/measure_cells.py +12 -0
  38. celldetective/scripts/segment_cells.py +20 -4
  39. celldetective/scripts/track_cells.py +11 -0
  40. celldetective/scripts/train_segmentation_model.py +35 -34
  41. celldetective/segmentation.py +14 -9
  42. celldetective/signals.py +234 -329
  43. celldetective/tracking.py +2 -2
  44. celldetective/utils.py +602 -49
  45. celldetective-1.1.1.dist-info/METADATA +305 -0
  46. celldetective-1.1.1.dist-info/RECORD +84 -0
  47. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/top_level.txt +1 -0
  48. tests/__init__.py +0 -0
  49. tests/test_events.py +28 -0
  50. tests/test_filters.py +24 -0
  51. tests/test_io.py +70 -0
  52. tests/test_measure.py +141 -0
  53. tests/test_neighborhood.py +70 -0
  54. tests/test_preprocessing.py +37 -0
  55. tests/test_segmentation.py +93 -0
  56. tests/test_signals.py +135 -0
  57. tests/test_tracking.py +164 -0
  58. tests/test_utils.py +118 -0
  59. celldetective-1.0.2.post1.dist-info/METADATA +0 -221
  60. celldetective-1.0.2.post1.dist-info/RECORD +0 -66
  61. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/LICENSE +0 -0
  62. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/WHEEL +0 -0
  63. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1023 @@
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 estimate_unreliable_edge, unpad, mask_edges, 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 celldetective.segmentation import filter_image, threshold_image
12
+ from stardist import fill_label_holes
13
+ from csbdeep.io import save_tiff_imagej_compatible
14
+ from gc import collect
15
+ from lmfit import Parameters, Model, models
16
+ import tifffile.tifffile as tiff
17
+
18
+ def estimate_background_per_condition(experiment, threshold_on_std=1, well_option='*', target_channel="channel_name", frame_range=[0,5], mode="timeseries", activation_protocol=[['gauss',2],['std',4]], show_progress_per_pos=False, show_progress_per_well=True):
19
+
20
+ """
21
+ Estimate the background for each condition in an experiment.
22
+
23
+ This function calculates the background for each well within
24
+ a given experiment by processing image frames using a specified activation
25
+ protocol. It supports time-series and tile-based modes for background
26
+ estimation.
27
+
28
+ Parameters
29
+ ----------
30
+ experiment : str
31
+ The path to the experiment directory.
32
+ threshold_on_std : float, optional
33
+ The threshold value on the standard deviation for masking (default is 1).
34
+ well_option : str, optional
35
+ The option to select specific wells (default is '*').
36
+ target_channel : str, optional
37
+ The name of the target channel for background estimation (default is "channel_name").
38
+ frame_range : list of int, optional
39
+ The range of frames to consider for background estimation (default is [0, 5]).
40
+ mode : str, optional
41
+ The mode of background estimation, either "timeseries" or "tiles" (default is "timeseries").
42
+ activation_protocol : list of list, optional
43
+ The activation protocol consisting of filters and their respective parameters (default is [['gauss', 2], ['std', 4]]).
44
+ show_progress_per_pos : bool, optional
45
+ Whether to show progress for each position (default is False).
46
+ show_progress_per_well : bool, optional
47
+ Whether to show progress for each well (default is True).
48
+
49
+ Returns
50
+ -------
51
+ list of dict
52
+ A list of dictionaries, each containing the background image (`bg`) and the corresponding well path (`well`).
53
+
54
+ See Also
55
+ --------
56
+ estimate_unreliable_edge : Estimates the unreliable edge value from the activation protocol.
57
+ threshold_image : Thresholds an image based on the specified criteria.
58
+
59
+ Notes
60
+ -----
61
+ This function assumes that the experiment directory structure and the configuration
62
+ files follow a specific format expected by the helper functions used within.
63
+
64
+ Examples
65
+ --------
66
+ >>> experiment_path = "path/to/experiment"
67
+ >>> backgrounds = estimate_background_per_condition(experiment_path, threshold_on_std=1.5, target_channel="GFP", frame_range=[0, 10], mode="tiles")
68
+ >>> for bg in backgrounds:
69
+ ... print(bg["well"], bg["bg"].shape)
70
+ """
71
+
72
+ config = get_config(experiment)
73
+ wells = get_experiment_wells(experiment)
74
+ len_movie = float(ConfigSectionMap(config,"MovieSettings")["len_movie"])
75
+ movie_prefix = ConfigSectionMap(config,"MovieSettings")["movie_prefix"]
76
+
77
+ well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, "*")
78
+
79
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
80
+ nbr_channels = _extract_nbr_channels_from_config(config)
81
+ img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
82
+
83
+ backgrounds = []
84
+
85
+ for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
86
+
87
+ well_name, _ = extract_well_name_and_number(well_path)
88
+ well_idx = well_indices[k]
89
+
90
+ positions = get_positions_in_well(well_path)
91
+ print(f"Reconstruct a background in well {well_name} from positions: {[extract_position_name(p) for p in positions]}...")
92
+
93
+ frame_mean_per_position = []
94
+
95
+ for l,pos_path in enumerate(tqdm(positions, disable=not show_progress_per_pos)):
96
+
97
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
98
+
99
+ if mode=="timeseries":
100
+
101
+ frames = load_frames(img_num_channels[0,frame_range[0]:frame_range[1]], stack_path, normalize_input=False)
102
+ frames = np.moveaxis(frames, -1, 0).astype(float)
103
+
104
+ for i in range(len(frames)):
105
+ if np.all(frames[i].flatten()==0):
106
+ frames[i,:,:] = np.nan
107
+
108
+ frame_mean = np.nanmean(frames, axis=0)
109
+
110
+ frame = frame_mean.copy().astype(float)
111
+ std_frame = filter_image(frame.copy(),filters=activation_protocol)
112
+ edge = estimate_unreliable_edge(activation_protocol)
113
+ mask = threshold_image(std_frame, threshold_on_std, 1.0E06, foreground_value=1, edge_exclusion=edge)
114
+ frame[np.where(mask.astype(int)==1)] = np.nan
115
+
116
+ elif mode=="tiles":
117
+
118
+ frames = load_frames(img_num_channels[0,:], stack_path, normalize_input=False).astype(float)
119
+ frames = np.moveaxis(frames, -1, 0).astype(float)
120
+
121
+ for i in range(len(frames)):
122
+
123
+ if np.all(frames[i].flatten()==0):
124
+ frames[i,:,:] = np.nan
125
+ continue
126
+
127
+ f = frames[i].copy()
128
+ std_frame = filter_image(f.copy(),filters=activation_protocol)
129
+ edge = estimate_unreliable_edge(activation_protocol)
130
+ mask = threshold_image(std_frame, threshold_on_std, 1.0E06, foreground_value=1, edge_exclusion=edge)
131
+ f[np.where(mask.astype(int)==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
+ activation_protocol=[['gauss',2],['std',4]],
165
+ export_prefix='Corrected',
166
+ **kwargs,
167
+ ):
168
+
169
+ """
170
+ Correct the background of image stacks for a given experiment.
171
+
172
+ This function processes image stacks by estimating and correcting the background
173
+ for each well and position in the experiment. It supports different modes, such
174
+ as timeseries or tiles, and offers options for optimization and exporting the results.
175
+
176
+ Parameters
177
+ ----------
178
+ experiment : str
179
+ Path to the experiment configuration.
180
+ well_option : str, int, or list of int, optional
181
+ Selection of wells to process. '*' indicates all wells. Defaults to '*'.
182
+ position_option : str, int, or list of int, optional
183
+ Selection of positions to process within each well. '*' indicates all positions. Defaults to '*'.
184
+ target_channel : str, optional
185
+ The name of the target channel to be corrected. Defaults to "channel_name".
186
+ mode : {'timeseries', 'tiles'}, optional
187
+ The mode of processing. Defaults to "timeseries".
188
+ threshold_on_std : float, optional
189
+ The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
190
+ frame_range : list of int, optional
191
+ The range of frames to consider for background estimation. Defaults to [0, 5].
192
+ optimize_option : bool, optional
193
+ If True, optimize the correction coefficient. Defaults to False.
194
+ opt_coef_range : list of float, optional
195
+ The range of coefficients to try for optimization. Defaults to [0.95, 1.05].
196
+ opt_coef_nbr : int, optional
197
+ The number of coefficients to test within the optimization range. Defaults to 100.
198
+ operation : {'divide', 'subtract'}, optional
199
+ The operation to apply for background correction. Defaults to 'divide'.
200
+ clip : bool, optional
201
+ If True, clip the corrected values to be non-negative when using subtraction. Defaults to False.
202
+ show_progress_per_well : bool, optional
203
+ If True, show progress bar for each well. Defaults to True.
204
+ show_progress_per_pos : bool, optional
205
+ If True, show progress bar for each position. Defaults to False.
206
+ export : bool, optional
207
+ If True, export the corrected stacks to files. Defaults to False.
208
+ return_stacks : bool, optional
209
+ If True, return the corrected stacks as a list of numpy arrays. Defaults to False.
210
+
211
+ Returns
212
+ -------
213
+ list of numpy.ndarray, optional
214
+ A list of corrected image stacks if `return_stacks` is True.
215
+
216
+ Notes
217
+ -----
218
+ The function uses several helper functions, including `interpret_wells_and_positions`,
219
+ `estimate_background_per_condition`, and `apply_background_to_stack`.
220
+
221
+ Examples
222
+ --------
223
+ >>> experiment = "path/to/experiment/config"
224
+ >>> 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)
225
+ >>> print(len(corrected_stacks))
226
+ 2
227
+
228
+ """
229
+
230
+ config = get_config(experiment)
231
+ wells = get_experiment_wells(experiment)
232
+ len_movie = float(ConfigSectionMap(config,"MovieSettings")["len_movie"])
233
+ if movie_prefix is None:
234
+ movie_prefix = ConfigSectionMap(config,"MovieSettings")["movie_prefix"]
235
+
236
+ well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
237
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
238
+ nbr_channels = _extract_nbr_channels_from_config(config)
239
+ img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
240
+
241
+ stacks = []
242
+
243
+ for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
244
+
245
+ well_name, _ = extract_well_name_and_number(well_path)
246
+
247
+ try:
248
+ 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, activation_protocol=activation_protocol)
249
+ background = background[0]
250
+ background = background['bg']
251
+ except Exception as e:
252
+ print(f'Background could not be estimated due to error "{e}"... Skipping well {well_name}...')
253
+ continue
254
+
255
+ positions = get_positions_in_well(well_path)
256
+ selection = positions[position_indices]
257
+ if isinstance(selection[0],np.ndarray):
258
+ selection = selection[0]
259
+
260
+ for pidx,pos_path in enumerate(tqdm(selection, disable=not show_progress_per_pos)):
261
+
262
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
263
+ print(f'Applying the correction to position {extract_position_name(pos_path)}...')
264
+
265
+ corrected_stack = apply_background_to_stack(stack_path,
266
+ background,
267
+ target_channel_index=channel_indices[0],
268
+ nbr_channels=nbr_channels,
269
+ stack_length=len_movie,
270
+ threshold_on_std=threshold_on_std,
271
+ optimize_option=optimize_option,
272
+ opt_coef_range=opt_coef_range,
273
+ opt_coef_nbr=opt_coef_nbr,
274
+ operation=operation,
275
+ clip=clip,
276
+ export=export,
277
+ activation_protocol=activation_protocol,
278
+ prefix=export_prefix,
279
+ )
280
+ print('Correction successful.')
281
+ if return_stacks:
282
+ stacks.append(corrected_stack)
283
+ else:
284
+ del corrected_stack
285
+ collect()
286
+
287
+ if return_stacks:
288
+ return stacks
289
+
290
+
291
+
292
+ def apply_background_to_stack(stack_path, background, target_channel_index=0, nbr_channels=1, stack_length=45,activation_protocol=[['gauss',2],['std',4]], 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"):
293
+
294
+ """
295
+ Apply background correction to an image stack.
296
+
297
+ This function corrects the background of an image stack by applying a specified operation
298
+ (either division or subtraction) between the image stack and the background. It also supports
299
+ optimization of the correction coefficient through brute-force regression.
300
+
301
+ Parameters
302
+ ----------
303
+ stack_path : str
304
+ The path to the image stack file.
305
+ background : numpy.ndarray
306
+ The background image to be applied for correction.
307
+ target_channel_index : int, optional
308
+ The index of the target channel to be corrected. Defaults to 0.
309
+ nbr_channels : int, optional
310
+ The number of channels in the image stack. Defaults to 1.
311
+ stack_length : int, optional
312
+ The length of the image stack (number of frames). If None, the length is auto-detected. Defaults to 45.
313
+ threshold_on_std : float, optional
314
+ The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
315
+ optimize_option : bool, optional
316
+ If True, optimize the correction coefficient using a range of values. Defaults to True.
317
+ opt_coef_range : tuple of float, optional
318
+ The range of coefficients to try for optimization. Defaults to (0.95, 1.05).
319
+ opt_coef_nbr : int, optional
320
+ The number of coefficients to test within the optimization range. Defaults to 100.
321
+ operation : {'divide', 'subtract'}, optional
322
+ The operation to apply for background correction. Defaults to 'divide'.
323
+ clip : bool, optional
324
+ If True, clip the corrected values to be non-negative when using subtraction. Defaults to False.
325
+ export : bool, optional
326
+ If True, export the corrected stack to a file. Defaults to False.
327
+ prefix : str, optional
328
+ The prefix for the exported file name. Defaults to "Corrected".
329
+
330
+ Returns
331
+ -------
332
+ corrected_stack : numpy.ndarray
333
+ The background-corrected image stack.
334
+
335
+ Examples
336
+ --------
337
+ >>> stack_path = "path/to/stack.tif"
338
+ >>> background = np.zeros((512, 512)) # Example background
339
+ >>> 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)
340
+ >>> print(corrected_stack.shape)
341
+ (44, 512, 512, 3)
342
+
343
+ """
344
+
345
+ if stack_length is None:
346
+ stack_length = auto_load_number_of_frames(stack_path)
347
+ if stack_length is None:
348
+ print('stack length not provided')
349
+ return None
350
+
351
+ if optimize_option:
352
+ coefficients = np.linspace(opt_coef_range[0], opt_coef_range[1], int(opt_coef_nbr))
353
+ coefficients = np.append(coefficients, [1.0])
354
+ if export:
355
+ path,file = os.path.split(stack_path)
356
+ if prefix is None:
357
+ newfile = file
358
+ else:
359
+ newfile = '_'.join([prefix,file])
360
+
361
+ corrected_stack = []
362
+
363
+ for i in range(0,int(stack_length*nbr_channels),nbr_channels):
364
+
365
+ frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
366
+ target_img = frames[:,:,target_channel_index].copy()
367
+
368
+ if optimize_option:
369
+
370
+ target_copy = target_img.copy()
371
+
372
+ std_frame = filter_image(target_copy.copy(),filters=activation_protocol)
373
+ edge = estimate_unreliable_edge(activation_protocol)
374
+ mask = threshold_image(std_frame, threshold_on_std, 1.0E06, foreground_value=1, edge_exclusion=edge)
375
+ target_copy[np.where(mask.astype(int)==1)] = np.nan
376
+
377
+ loss = []
378
+
379
+ # brute-force regression, could do gradient descent instead
380
+ for c in coefficients:
381
+ target_crop = unpad(target_copy,edge)
382
+ bg_crop = unpad(background, edge)
383
+ diff = np.subtract(target_crop, c*bg_crop, where=target_crop==target_crop)
384
+ s = np.sum(np.abs(diff, where=diff==diff), where=diff==diff)
385
+ loss.append(s)
386
+ c = coefficients[np.argmin(loss)]
387
+ print(f"Frame: {i}; optimal coefficient: {c}...")
388
+ # if c==min(coefficients) or c==max(coefficients):
389
+ # print('Warning... The optimal coefficient is beyond the range provided... Please adjust your coefficient range...')
390
+ else:
391
+ c=1
392
+
393
+ if operation=="divide":
394
+ correction = np.divide(target_img, background*c, where=background==background)
395
+ correction[background!=background] = np.nan
396
+ correction[target_img!=target_img] = np.nan
397
+ fill_val = 1.0
398
+
399
+ elif operation=="subtract":
400
+ correction = np.subtract(target_img, background*c, where=background==background)
401
+ correction[background!=background] = np.nan
402
+ correction[target_img!=target_img] = np.nan
403
+ fill_val = 0.0
404
+ if clip:
405
+ correction[correction<=0.] = 0.
406
+
407
+ frames[:,:,target_channel_index] = correction
408
+ corrected_stack.append(frames)
409
+
410
+ corrected_stack = np.array(corrected_stack)
411
+
412
+ if export:
413
+ save_tiff_imagej_compatible(os.sep.join([path,newfile]), corrected_stack, axes='TYXC')
414
+
415
+ return corrected_stack
416
+
417
+ def paraboloid(x, y, a, b, c, d, e, g):
418
+
419
+ """
420
+ Compute the value of a 2D paraboloid function.
421
+
422
+ This function evaluates a paraboloid defined by the equation:
423
+ `a * x ** 2 + b * y ** 2 + c * x * y + d * x + e * y + g`.
424
+
425
+ Parameters
426
+ ----------
427
+ x : float or ndarray
428
+ The x-coordinate(s) at which to evaluate the paraboloid.
429
+ y : float or ndarray
430
+ The y-coordinate(s) at which to evaluate the paraboloid.
431
+ a : float
432
+ The coefficient of the x^2 term.
433
+ b : float
434
+ The coefficient of the y^2 term.
435
+ c : float
436
+ The coefficient of the x*y term.
437
+ d : float
438
+ The coefficient of the x term.
439
+ e : float
440
+ The coefficient of the y term.
441
+ g : float
442
+ The constant term.
443
+
444
+ Returns
445
+ -------
446
+ float or ndarray
447
+ The value of the paraboloid at the given (x, y) coordinates. If `x` and
448
+ `y` are arrays, the result is an array of the same shape.
449
+
450
+ Examples
451
+ --------
452
+ >>> paraboloid(1, 2, 1, 1, 0, 0, 0, 0)
453
+ 5
454
+ >>> paraboloid(np.array([1, 2]), np.array([3, 4]), 1, 1, 0, 0, 0, 0)
455
+ array([10, 20])
456
+
457
+ Notes
458
+ -----
459
+ The paraboloid function is a quadratic function in two variables, commonly used
460
+ to model surfaces in three-dimensional space.
461
+ """
462
+
463
+ return a * x ** 2 + b * y ** 2 + c * x * y + d * x + e * y + g
464
+
465
+
466
+ def plane(x, y, a, b, c):
467
+
468
+ """
469
+ Compute the value of a plane function.
470
+
471
+ This function evaluates a plane defined by the equation:
472
+ `a * x + b * y + c`.
473
+
474
+ Parameters
475
+ ----------
476
+ x : float or ndarray
477
+ The x-coordinate(s) at which to evaluate the plane.
478
+ y : float or ndarray
479
+ The y-coordinate(s) at which to evaluate the plane.
480
+ a : float
481
+ The coefficient of the x term.
482
+ b : float
483
+ The coefficient of the y term.
484
+ c : float
485
+ The constant term.
486
+
487
+ Returns
488
+ -------
489
+ float or ndarray
490
+ The value of the plane at the given (x, y) coordinates. If `x` and
491
+ `y` are arrays, the result is an array of the same shape.
492
+
493
+ Examples
494
+ --------
495
+ >>> plane(1, 2, 3, 4, 5)
496
+ 16
497
+ >>> plane(np.array([1, 2]), np.array([3, 4]), 3, 4, 5)
498
+ array([20, 27])
499
+
500
+ Notes
501
+ -----
502
+ The plane function is a linear function in two variables, commonly used
503
+ to model flat surfaces in three-dimensional space.
504
+ """
505
+
506
+ return a * x + b * y + c
507
+
508
+
509
+ def fit_plane(image, cell_masks=None, edge_exclusion=None):
510
+
511
+ """
512
+ Fit a plane to the given image data.
513
+
514
+ This function fits a plane to the provided image data using least squares
515
+ regression. It constructs a mesh grid based on the dimensions of the image
516
+ and fits a plane model to the data points. If cell masks are provided,
517
+ areas covered by cell masks will be excluded from the fitting process.
518
+
519
+ Parameters
520
+ ----------
521
+ image : numpy.ndarray
522
+ The input image data.
523
+ cell_masks : numpy.ndarray, optional
524
+ An array specifying cell masks. If provided, areas covered by cell masks
525
+ will be excluded from the fitting process (default is None).
526
+ edge_exclusion : int, optional
527
+ The size of the edge to exclude from the fitting process (default is None).
528
+
529
+ Returns
530
+ -------
531
+ numpy.ndarray
532
+ The fitted plane.
533
+
534
+ Notes
535
+ -----
536
+ - The `cell_masks` parameter allows excluding areas covered by cell masks from
537
+ the fitting process.
538
+ - The `edge_exclusion` parameter allows excluding edges of the specified size
539
+ from the fitting process to avoid boundary effects.
540
+
541
+ See Also
542
+ --------
543
+ plane : The plane function used for fitting.
544
+ """
545
+
546
+ data = np.empty(image.shape)
547
+ x = np.arange(0, image.shape[1])
548
+ y = np.arange(0, image.shape[0])
549
+ xx, yy = np.meshgrid(x, y)
550
+
551
+ params = Parameters()
552
+ params.add('a', value=1)
553
+ params.add('b', value=1)
554
+ params.add('c', value=1)
555
+
556
+ model = Model(plane, independent_vars=['x', 'y'])
557
+
558
+ weights = np.ones_like(xx, dtype=float)
559
+ if cell_masks is not None:
560
+ weights[np.where(cell_masks > 0)] = 0.
561
+
562
+ if edge_exclusion is not None:
563
+ xx = unpad(xx, edge_exclusion)
564
+ yy = unpad(yy, edge_exclusion)
565
+ weights = unpad(weights, edge_exclusion)
566
+ image = unpad(image, edge_exclusion)
567
+
568
+ result = model.fit(image,
569
+ x=xx,
570
+ y=yy,
571
+ weights=weights,
572
+ params=params, max_nfev=3000)
573
+ del model
574
+ collect()
575
+
576
+ xx, yy = np.meshgrid(x, y)
577
+
578
+ return plane(xx, yy, **result.params)
579
+
580
+
581
+ def fit_paraboloid(image, cell_masks=None, edge_exclusion=None):
582
+
583
+ """
584
+ Fit a paraboloid to the given image data.
585
+
586
+ This function fits a paraboloid to the provided image data using least squares
587
+ regression. It constructs a mesh grid based on the dimensions of the image
588
+ and fits a paraboloid model to the data points. If cell masks are provided,
589
+ areas covered by cell masks will be excluded from the fitting process.
590
+
591
+ Parameters
592
+ ----------
593
+ image : numpy.ndarray
594
+ The input image data.
595
+ cell_masks : numpy.ndarray, optional
596
+ An array specifying cell masks. If provided, areas covered by cell masks
597
+ will be excluded from the fitting process (default is None).
598
+ edge_exclusion : int, optional
599
+ The size of the edge to exclude from the fitting process (default is None).
600
+
601
+ Returns
602
+ -------
603
+ numpy.ndarray
604
+ The fitted paraboloid.
605
+
606
+ Notes
607
+ -----
608
+ - The `cell_masks` parameter allows excluding areas covered by cell masks from
609
+ the fitting process.
610
+ - The `edge_exclusion` parameter allows excluding edges of the specified size
611
+ from the fitting process to avoid boundary effects.
612
+
613
+ See Also
614
+ --------
615
+ paraboloid : The paraboloid function used for fitting.
616
+ """
617
+
618
+ data = np.empty(image.shape)
619
+ x = np.arange(0, image.shape[1])
620
+ y = np.arange(0, image.shape[0])
621
+ xx, yy = np.meshgrid(x, y)
622
+
623
+ params = Parameters()
624
+ params.add('a', value=1.0E-05)
625
+ params.add('b', value=1.0E-05)
626
+ params.add('c', value=1.0E-06)
627
+ params.add('d', value=0.01)
628
+ params.add('e', value=0.01)
629
+ params.add('g', value=100)
630
+
631
+ model = Model(paraboloid, independent_vars=['x', 'y'])
632
+
633
+ weights = np.ones_like(xx, dtype=float)
634
+ if cell_masks is not None:
635
+ weights[np.where(cell_masks > 0)] = 0.
636
+
637
+ if edge_exclusion is not None:
638
+ xx = unpad(xx, edge_exclusion)
639
+ yy = unpad(yy, edge_exclusion)
640
+ weights = unpad(weights, edge_exclusion)
641
+ image = unpad(image, edge_exclusion)
642
+
643
+ result = model.fit(image,
644
+ x=xx,
645
+ y=yy,
646
+ weights=weights,
647
+ params=params, max_nfev=3000)
648
+
649
+ del model
650
+ collect()
651
+
652
+ xx, yy = np.meshgrid(x, y)
653
+
654
+ return paraboloid(xx, yy, **result.params)
655
+
656
+
657
+ def correct_background_model(
658
+ experiment,
659
+ well_option='*',
660
+ position_option='*',
661
+ target_channel="channel_name",
662
+ threshold_on_std = 1,
663
+ model = 'paraboloid',
664
+ operation = 'divide',
665
+ clip = False,
666
+ show_progress_per_well = True,
667
+ show_progress_per_pos = False,
668
+ export = False,
669
+ return_stacks = False,
670
+ movie_prefix=None,
671
+ activation_protocol=[['gauss',2],['std',4]],
672
+ export_prefix='Corrected',
673
+ return_stack = True,
674
+ **kwargs,
675
+ ):
676
+
677
+ """
678
+ Correct background in image stacks using a specified model.
679
+
680
+ This function corrects the background in image stacks obtained from an experiment
681
+ using a specified background correction model. It supports various options for
682
+ specifying wells, positions, target channel, and background correction parameters.
683
+
684
+ Parameters
685
+ ----------
686
+ experiment : str
687
+ The path to the experiment directory.
688
+ well_option : str, optional
689
+ The option to select specific wells (default is '*').
690
+ position_option : str, optional
691
+ The option to select specific positions (default is '*').
692
+ target_channel : str, optional
693
+ The name of the target channel for background correction (default is "channel_name").
694
+ threshold_on_std : float, optional
695
+ The threshold value on the standard deviation for masking (default is 1).
696
+ model : str, optional
697
+ The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
698
+ operation : str, optional
699
+ The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
700
+ clip : bool, optional
701
+ Whether to clip the corrected image to ensure non-negative values (default is False).
702
+ show_progress_per_well : bool, optional
703
+ Whether to show progress for each well (default is True).
704
+ show_progress_per_pos : bool, optional
705
+ Whether to show progress for each position (default is False).
706
+ export : bool, optional
707
+ Whether to export the corrected stacks (default is False).
708
+ return_stacks : bool, optional
709
+ Whether to return the corrected stacks (default is False).
710
+ movie_prefix : str, optional
711
+ The prefix for the movie files (default is None).
712
+ activation_protocol : list of list, optional
713
+ The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
714
+ export_prefix : str, optional
715
+ The prefix for exported corrected stacks (default is 'Corrected').
716
+ **kwargs : dict
717
+ Additional keyword arguments to be passed to the underlying correction function.
718
+
719
+ Returns
720
+ -------
721
+ list of numpy.ndarray
722
+ A list of corrected image stacks if `return_stacks` is True, otherwise None.
723
+
724
+ Notes
725
+ -----
726
+ - This function assumes that the experiment directory structure and the configuration
727
+ files follow a specific format expected by the helper functions used within.
728
+ - Supported background correction models are 'paraboloid' and 'plane'.
729
+ - Supported background correction operations are 'divide' and 'subtract'.
730
+
731
+ See Also
732
+ --------
733
+ fit_and_apply_model_background_to_stack : Function to fit and apply background correction to an image stack.
734
+ """
735
+
736
+ config = get_config(experiment)
737
+ wells = get_experiment_wells(experiment)
738
+ len_movie = float(ConfigSectionMap(config,"MovieSettings")["len_movie"])
739
+ if movie_prefix is None:
740
+ movie_prefix = ConfigSectionMap(config,"MovieSettings")["movie_prefix"]
741
+
742
+ well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
743
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
744
+ nbr_channels = _extract_nbr_channels_from_config(config)
745
+ img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
746
+
747
+ stacks = []
748
+
749
+ for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
750
+
751
+ well_name, _ = extract_well_name_and_number(well_path)
752
+ positions = get_positions_in_well(well_path)
753
+ selection = positions[position_indices]
754
+ if isinstance(selection[0],np.ndarray):
755
+ selection = selection[0]
756
+
757
+ for pidx,pos_path in enumerate(tqdm(selection, disable=not show_progress_per_pos)):
758
+
759
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
760
+ print(f'Applying the correction to position {extract_position_name(pos_path)}...')
761
+ print(stack_path)
762
+
763
+ corrected_stack = fit_and_apply_model_background_to_stack(stack_path,
764
+ target_channel_index=channel_indices[0],
765
+ model = model,
766
+ nbr_channels=nbr_channels,
767
+ stack_length=len_movie,
768
+ threshold_on_std=threshold_on_std,
769
+ operation=operation,
770
+ clip=clip,
771
+ export=export,
772
+ prefix=export_prefix,
773
+ activation_protocol=activation_protocol,
774
+ return_stacks = return_stacks,
775
+ )
776
+ print('Correction successful.')
777
+ if return_stacks:
778
+ stacks.append(corrected_stack)
779
+ else:
780
+ del corrected_stack
781
+ collect()
782
+
783
+ if return_stacks:
784
+ return stacks
785
+
786
+ def fit_and_apply_model_background_to_stack(stack_path,
787
+ target_channel_index=0,
788
+ nbr_channels=1,
789
+ stack_length=45,
790
+ threshold_on_std=1,
791
+ operation='divide',
792
+ model='paraboloid',
793
+ clip=False,
794
+ export=False,
795
+ activation_protocol=[['gauss',2],['std',4]],
796
+ prefix="Corrected",
797
+ return_stacks=True,
798
+ ):
799
+
800
+ """
801
+ Fit and apply a background correction model to an image stack.
802
+
803
+ This function fits a background correction model to each frame of the image stack
804
+ and applies the correction accordingly. It supports various options for specifying
805
+ the target channel, number of channels, stack length, threshold on standard deviation,
806
+ correction operation, correction model, clipping, and export.
807
+
808
+ Parameters
809
+ ----------
810
+ stack_path : str
811
+ The path to the image stack.
812
+ target_channel_index : int, optional
813
+ The index of the target channel for background correction (default is 0).
814
+ nbr_channels : int, optional
815
+ The number of channels in the image stack (default is 1).
816
+ stack_length : int, optional
817
+ The length of the stack (default is 45).
818
+ threshold_on_std : float, optional
819
+ The threshold value on the standard deviation for masking (default is 1).
820
+ operation : str, optional
821
+ The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
822
+ model : str, optional
823
+ The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
824
+ clip : bool, optional
825
+ Whether to clip the corrected image to ensure non-negative values (default is False).
826
+ export : bool, optional
827
+ Whether to export the corrected image stack (default is False).
828
+ activation_protocol : list of list, optional
829
+ The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
830
+ prefix : str, optional
831
+ The prefix for exported corrected stacks (default is 'Corrected').
832
+
833
+ Returns
834
+ -------
835
+ numpy.ndarray
836
+ The corrected image stack.
837
+
838
+ Notes
839
+ -----
840
+ - The function loads frames from the image stack, applies background correction to each frame,
841
+ and stores the corrected frames in a new stack.
842
+ - Supported background correction models are 'paraboloid' and 'plane'.
843
+ - Supported background correction operations are 'divide' and 'subtract'.
844
+
845
+ See Also
846
+ --------
847
+ field_correction : Function to apply background correction to an image.
848
+ """
849
+
850
+ stack_length_auto = auto_load_number_of_frames(stack_path)
851
+ if stack_length_auto is None and stack_length is None:
852
+ print('stack length not provided')
853
+ return None
854
+ if stack_length_auto is not None:
855
+ stack_length = stack_length_auto
856
+
857
+ corrected_stack = []
858
+
859
+ if export:
860
+ path,file = os.path.split(stack_path)
861
+ if prefix is None:
862
+ newfile = 'temp_'+file
863
+ else:
864
+ newfile = '_'.join([prefix,file])
865
+
866
+ with tiff.TiffWriter(os.sep.join([path,newfile]),imagej=True) as tif:
867
+
868
+ for i in tqdm(range(0,int(stack_length*nbr_channels),nbr_channels)):
869
+
870
+ frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
871
+ target_img = frames[:,:,target_channel_index].copy()
872
+ correction = field_correction(target_img, threshold_on_std=threshold_on_std, operation=operation, model=model, clip=clip, activation_protocol=activation_protocol)
873
+ frames[:,:,target_channel_index] = correction.copy()
874
+
875
+ if return_stacks:
876
+ corrected_stack.append(frames)
877
+
878
+ if export:
879
+ tif.write(np.moveaxis(frames,-1,0).astype(np.dtype('f')), contiguous=True)
880
+ del frames
881
+ del target_img
882
+ del correction
883
+ collect()
884
+
885
+ if prefix is None:
886
+ os.replace(os.sep.join([path,newfile]), os.sep.join([path,file]))
887
+ else:
888
+ for i in tqdm(range(0,int(stack_length*nbr_channels),nbr_channels)):
889
+
890
+ frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
891
+ target_img = frames[:,:,target_channel_index].copy()
892
+ correction = field_correction(target_img, threshold_on_std=threshold_on_std, operation=operation, model=model, clip=clip, activation_protocol=activation_protocol)
893
+ frames[:,:,target_channel_index] = correction.copy()
894
+
895
+ corrected_stack.append(frames)
896
+
897
+ del frames
898
+ del target_img
899
+ del correction
900
+ collect()
901
+
902
+ if return_stacks:
903
+ return np.array(corrected_stack)
904
+ else:
905
+ return None
906
+
907
+ def field_correction(img, threshold_on_std=1, operation='divide', model='paraboloid', clip=False, return_bg=False, activation_protocol=[['gauss',2],['std',4]]):
908
+
909
+ """
910
+ Apply field correction to an image.
911
+
912
+ This function applies field correction to the given image based on the specified parameters
913
+ including the threshold on standard deviation, operation, background correction model, clipping,
914
+ and activation protocol.
915
+
916
+ Parameters
917
+ ----------
918
+ img : numpy.ndarray
919
+ The input image to be corrected.
920
+ threshold_on_std : float, optional
921
+ The threshold value on the standard deviation for masking (default is 1).
922
+ operation : str, optional
923
+ The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
924
+ model : str, optional
925
+ The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
926
+ clip : bool, optional
927
+ Whether to clip the corrected image to ensure non-negative values (default is False).
928
+ return_bg : bool, optional
929
+ Whether to return the background along with the corrected image (default is False).
930
+ activation_protocol : list of list, optional
931
+ The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
932
+
933
+ Returns
934
+ -------
935
+ numpy.ndarray or tuple
936
+ The corrected image or a tuple containing the corrected image and the background, depending on the value of `return_bg`.
937
+
938
+ Notes
939
+ -----
940
+ - This function first estimates the unreliable edge based on the activation protocol.
941
+ - It then applies thresholding to obtain a mask for the background.
942
+ - Next, it fits a background model to the image using the specified model.
943
+ - Depending on the operation specified, it either divides or subtracts the background from the image.
944
+ - If `clip` is True and operation is 'subtract', negative values in the corrected image are clipped to 0.
945
+ - If `return_bg` is True, the function returns a tuple containing the corrected image and the background.
946
+
947
+ See Also
948
+ --------
949
+ fit_background_model : Function to fit a background model to an image.
950
+ threshold_image : Function to apply thresholding to an image.
951
+ """
952
+
953
+ target_copy = img.copy().astype(float)
954
+
955
+ std_frame = filter_image(target_copy,filters=activation_protocol)
956
+ edge = estimate_unreliable_edge(activation_protocol)
957
+ mask = threshold_image(std_frame, threshold_on_std, 1.0E06, foreground_value=1, edge_exclusion=edge).astype(int)
958
+ background = fit_background_model(img, cell_masks=mask, model=model, edge_exclusion=edge)
959
+
960
+ if operation=="divide":
961
+ correction = np.divide(img, background, where=background==background)
962
+ correction[background!=background] = np.nan
963
+ correction[img!=img] = np.nan
964
+ fill_val = 1.0
965
+
966
+ elif operation=="subtract":
967
+ correction = np.subtract(img, background, where=background==background)
968
+ correction[background!=background] = np.nan
969
+ correction[img!=img] = np.nan
970
+ fill_val = 0.0
971
+ if clip:
972
+ correction[correction<=0.] = 0.
973
+
974
+ if return_bg:
975
+ return correction.copy(), background
976
+ else:
977
+ return correction.copy()
978
+
979
+ def fit_background_model(img, cell_masks=None, model='paraboloid', edge_exclusion=None):
980
+
981
+ """
982
+ Fit a background model to the given image.
983
+
984
+ This function fits a background model to the given image using either a paraboloid or plane model.
985
+ It supports optional cell masks and edge exclusion for fitting.
986
+
987
+ Parameters
988
+ ----------
989
+ img : numpy.ndarray
990
+ The input image data.
991
+ cell_masks : numpy.ndarray, optional
992
+ An array specifying cell masks. If provided, areas covered by cell masks will be excluded from the fitting process.
993
+ model : str, optional
994
+ The background model to fit, either 'paraboloid' or 'plane' (default is 'paraboloid').
995
+ edge_exclusion : int or None, optional
996
+ The size of the border to exclude from fitting (default is None).
997
+
998
+ Returns
999
+ -------
1000
+ numpy.ndarray or None
1001
+ The fitted background model as a numpy array if successful, otherwise None.
1002
+
1003
+ Notes
1004
+ -----
1005
+ - This function fits a background model to the image using either a paraboloid or plane model based on the specified `model`.
1006
+ - If `cell_masks` are provided, areas covered by cell masks will be excluded from the fitting process.
1007
+ - If `edge_exclusion` is provided, a border of the specified size will be excluded from fitting.
1008
+
1009
+ See Also
1010
+ --------
1011
+ fit_paraboloid : Function to fit a paraboloid model to an image.
1012
+ fit_plane : Function to fit a plane model to an image.
1013
+ """
1014
+
1015
+ if model == "paraboloid":
1016
+ bg = fit_paraboloid(img.astype(float), cell_masks=cell_masks, edge_exclusion=edge_exclusion).astype(float)
1017
+ elif model == "plane":
1018
+ bg = fit_plane(img.astype(float), cell_masks=cell_masks, edge_exclusion=edge_exclusion).astype(float)
1019
+
1020
+ if bg is not None:
1021
+ bg = np.array(bg)
1022
+
1023
+ return bg