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.
- celldetective/__main__.py +2 -2
- celldetective/events.py +2 -44
- celldetective/filters.py +4 -5
- celldetective/gui/__init__.py +1 -1
- celldetective/gui/analyze_block.py +37 -10
- celldetective/gui/btrack_options.py +24 -23
- celldetective/gui/classifier_widget.py +62 -19
- celldetective/gui/configure_new_exp.py +32 -35
- celldetective/gui/control_panel.py +115 -81
- celldetective/gui/gui_utils.py +674 -396
- celldetective/gui/json_readers.py +7 -6
- celldetective/gui/layouts.py +755 -0
- celldetective/gui/measurement_options.py +168 -487
- celldetective/gui/neighborhood_options.py +322 -270
- celldetective/gui/plot_measurements.py +1114 -0
- celldetective/gui/plot_signals_ui.py +20 -20
- celldetective/gui/process_block.py +449 -169
- celldetective/gui/retrain_segmentation_model_options.py +27 -26
- celldetective/gui/retrain_signal_model_options.py +25 -24
- celldetective/gui/seg_model_loader.py +31 -27
- celldetective/gui/signal_annotator.py +2326 -2295
- celldetective/gui/signal_annotator_options.py +18 -16
- celldetective/gui/styles.py +16 -1
- celldetective/gui/survival_ui.py +61 -39
- celldetective/gui/tableUI.py +60 -23
- celldetective/gui/thresholds_gui.py +68 -66
- celldetective/gui/viewers.py +596 -0
- celldetective/io.py +234 -23
- celldetective/measure.py +37 -32
- celldetective/neighborhood.py +495 -27
- celldetective/preprocessing.py +683 -0
- celldetective/scripts/analyze_signals.py +7 -0
- celldetective/scripts/measure_cells.py +12 -0
- celldetective/scripts/segment_cells.py +5 -0
- celldetective/scripts/track_cells.py +11 -0
- celldetective/signals.py +221 -98
- celldetective/tracking.py +0 -1
- celldetective/utils.py +178 -36
- celldetective-1.1.0.dist-info/METADATA +305 -0
- celldetective-1.1.0.dist-info/RECORD +80 -0
- {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/top_level.txt +1 -0
- tests/__init__.py +0 -0
- tests/test_events.py +28 -0
- tests/test_filters.py +24 -0
- tests/test_io.py +70 -0
- tests/test_measure.py +141 -0
- tests/test_neighborhood.py +70 -0
- tests/test_segmentation.py +93 -0
- tests/test_signals.py +135 -0
- tests/test_tracking.py +164 -0
- tests/test_utils.py +71 -0
- celldetective-1.0.2.dist-info/METADATA +0 -192
- celldetective-1.0.2.dist-info/RECORD +0 -66
- {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/LICENSE +0 -0
- {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/WHEEL +0 -0
- {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)
|