celldetective 1.4.2__py3-none-any.whl → 1.5.0b1__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 (152) hide show
  1. celldetective/__init__.py +25 -0
  2. celldetective/__main__.py +62 -43
  3. celldetective/_version.py +1 -1
  4. celldetective/extra_properties.py +477 -399
  5. celldetective/filters.py +192 -97
  6. celldetective/gui/InitWindow.py +541 -411
  7. celldetective/gui/__init__.py +0 -15
  8. celldetective/gui/about.py +44 -39
  9. celldetective/gui/analyze_block.py +120 -84
  10. celldetective/gui/base/__init__.py +0 -0
  11. celldetective/gui/base/channel_norm_generator.py +335 -0
  12. celldetective/gui/base/components.py +249 -0
  13. celldetective/gui/base/feature_choice.py +92 -0
  14. celldetective/gui/base/figure_canvas.py +52 -0
  15. celldetective/gui/base/list_widget.py +133 -0
  16. celldetective/gui/{styles.py → base/styles.py} +92 -36
  17. celldetective/gui/base/utils.py +33 -0
  18. celldetective/gui/base_annotator.py +900 -767
  19. celldetective/gui/classifier_widget.py +6 -22
  20. celldetective/gui/configure_new_exp.py +777 -671
  21. celldetective/gui/control_panel.py +635 -524
  22. celldetective/gui/dynamic_progress.py +449 -0
  23. celldetective/gui/event_annotator.py +2023 -1662
  24. celldetective/gui/generic_signal_plot.py +1292 -944
  25. celldetective/gui/gui_utils.py +899 -1289
  26. celldetective/gui/interactions_block.py +658 -0
  27. celldetective/gui/interactive_timeseries_viewer.py +447 -0
  28. celldetective/gui/json_readers.py +48 -15
  29. celldetective/gui/layouts/__init__.py +5 -0
  30. celldetective/gui/layouts/background_model_free_layout.py +537 -0
  31. celldetective/gui/layouts/channel_offset_layout.py +134 -0
  32. celldetective/gui/layouts/local_correction_layout.py +91 -0
  33. celldetective/gui/layouts/model_fit_layout.py +372 -0
  34. celldetective/gui/layouts/operation_layout.py +68 -0
  35. celldetective/gui/layouts/protocol_designer_layout.py +96 -0
  36. celldetective/gui/pair_event_annotator.py +3130 -2435
  37. celldetective/gui/plot_measurements.py +586 -267
  38. celldetective/gui/plot_signals_ui.py +724 -506
  39. celldetective/gui/preprocessing_block.py +395 -0
  40. celldetective/gui/process_block.py +1678 -1831
  41. celldetective/gui/seg_model_loader.py +580 -473
  42. celldetective/gui/settings/__init__.py +0 -7
  43. celldetective/gui/settings/_cellpose_model_params.py +181 -0
  44. celldetective/gui/settings/_event_detection_model_params.py +95 -0
  45. celldetective/gui/settings/_segmentation_model_params.py +159 -0
  46. celldetective/gui/settings/_settings_base.py +77 -65
  47. celldetective/gui/settings/_settings_event_model_training.py +752 -526
  48. celldetective/gui/settings/_settings_measurements.py +1133 -964
  49. celldetective/gui/settings/_settings_neighborhood.py +574 -488
  50. celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
  51. celldetective/gui/settings/_settings_signal_annotator.py +329 -305
  52. celldetective/gui/settings/_settings_tracking.py +1304 -1094
  53. celldetective/gui/settings/_stardist_model_params.py +98 -0
  54. celldetective/gui/survival_ui.py +422 -312
  55. celldetective/gui/tableUI.py +1665 -1701
  56. celldetective/gui/table_ops/_maths.py +295 -0
  57. celldetective/gui/table_ops/_merge_groups.py +140 -0
  58. celldetective/gui/table_ops/_merge_one_hot.py +95 -0
  59. celldetective/gui/table_ops/_query_table.py +43 -0
  60. celldetective/gui/table_ops/_rename_col.py +44 -0
  61. celldetective/gui/thresholds_gui.py +382 -179
  62. celldetective/gui/viewers/__init__.py +0 -0
  63. celldetective/gui/viewers/base_viewer.py +700 -0
  64. celldetective/gui/viewers/channel_offset_viewer.py +331 -0
  65. celldetective/gui/viewers/contour_viewer.py +394 -0
  66. celldetective/gui/viewers/size_viewer.py +153 -0
  67. celldetective/gui/viewers/spot_detection_viewer.py +341 -0
  68. celldetective/gui/viewers/threshold_viewer.py +309 -0
  69. celldetective/gui/workers.py +403 -126
  70. celldetective/log_manager.py +92 -0
  71. celldetective/measure.py +1895 -1478
  72. celldetective/napari/__init__.py +0 -0
  73. celldetective/napari/utils.py +1025 -0
  74. celldetective/neighborhood.py +1914 -1448
  75. celldetective/preprocessing.py +1620 -1220
  76. celldetective/processes/__init__.py +0 -0
  77. celldetective/processes/background_correction.py +271 -0
  78. celldetective/processes/compute_neighborhood.py +894 -0
  79. celldetective/processes/detect_events.py +246 -0
  80. celldetective/processes/downloader.py +137 -0
  81. celldetective/processes/measure_cells.py +565 -0
  82. celldetective/processes/segment_cells.py +760 -0
  83. celldetective/processes/track_cells.py +435 -0
  84. celldetective/processes/train_segmentation_model.py +694 -0
  85. celldetective/processes/train_signal_model.py +265 -0
  86. celldetective/processes/unified_process.py +292 -0
  87. celldetective/regionprops/_regionprops.py +358 -317
  88. celldetective/relative_measurements.py +987 -710
  89. celldetective/scripts/measure_cells.py +313 -212
  90. celldetective/scripts/measure_relative.py +90 -46
  91. celldetective/scripts/segment_cells.py +165 -104
  92. celldetective/scripts/segment_cells_thresholds.py +96 -68
  93. celldetective/scripts/track_cells.py +198 -149
  94. celldetective/scripts/train_segmentation_model.py +324 -201
  95. celldetective/scripts/train_signal_model.py +87 -45
  96. celldetective/segmentation.py +844 -749
  97. celldetective/signals.py +3514 -2861
  98. celldetective/tracking.py +30 -15
  99. celldetective/utils/__init__.py +0 -0
  100. celldetective/utils/cellpose_utils/__init__.py +133 -0
  101. celldetective/utils/color_mappings.py +42 -0
  102. celldetective/utils/data_cleaning.py +630 -0
  103. celldetective/utils/data_loaders.py +450 -0
  104. celldetective/utils/dataset_helpers.py +207 -0
  105. celldetective/utils/downloaders.py +235 -0
  106. celldetective/utils/event_detection/__init__.py +8 -0
  107. celldetective/utils/experiment.py +1782 -0
  108. celldetective/utils/image_augmenters.py +308 -0
  109. celldetective/utils/image_cleaning.py +74 -0
  110. celldetective/utils/image_loaders.py +926 -0
  111. celldetective/utils/image_transforms.py +335 -0
  112. celldetective/utils/io.py +62 -0
  113. celldetective/utils/mask_cleaning.py +348 -0
  114. celldetective/utils/mask_transforms.py +5 -0
  115. celldetective/utils/masks.py +184 -0
  116. celldetective/utils/maths.py +351 -0
  117. celldetective/utils/model_getters.py +325 -0
  118. celldetective/utils/model_loaders.py +296 -0
  119. celldetective/utils/normalization.py +380 -0
  120. celldetective/utils/parsing.py +465 -0
  121. celldetective/utils/plots/__init__.py +0 -0
  122. celldetective/utils/plots/regression.py +53 -0
  123. celldetective/utils/resources.py +34 -0
  124. celldetective/utils/stardist_utils/__init__.py +104 -0
  125. celldetective/utils/stats.py +90 -0
  126. celldetective/utils/types.py +21 -0
  127. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/METADATA +1 -1
  128. celldetective-1.5.0b1.dist-info/RECORD +187 -0
  129. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/WHEEL +1 -1
  130. tests/gui/test_new_project.py +129 -117
  131. tests/gui/test_project.py +127 -79
  132. tests/test_filters.py +39 -15
  133. tests/test_notebooks.py +8 -0
  134. tests/test_tracking.py +232 -13
  135. tests/test_utils.py +123 -77
  136. celldetective/gui/base_components.py +0 -23
  137. celldetective/gui/layouts.py +0 -1602
  138. celldetective/gui/processes/compute_neighborhood.py +0 -594
  139. celldetective/gui/processes/downloader.py +0 -111
  140. celldetective/gui/processes/measure_cells.py +0 -360
  141. celldetective/gui/processes/segment_cells.py +0 -499
  142. celldetective/gui/processes/track_cells.py +0 -303
  143. celldetective/gui/processes/train_segmentation_model.py +0 -270
  144. celldetective/gui/processes/train_signal_model.py +0 -108
  145. celldetective/gui/table_ops/merge_groups.py +0 -118
  146. celldetective/gui/viewers.py +0 -1354
  147. celldetective/io.py +0 -3663
  148. celldetective/utils.py +0 -3108
  149. celldetective-1.4.2.dist-info/RECORD +0 -123
  150. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/entry_points.txt +0 -0
  151. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/licenses/LICENSE +0 -0
  152. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/top_level.txt +0 -0
@@ -1,1243 +1,1643 @@
1
1
  """
2
2
  Copright © 2024 Laboratoire Adhesion et Inflammation, Authored by Remy Torro & Ksenija Dervanova.
3
3
  """
4
- from typing import List
5
4
 
6
- from tqdm import tqdm
5
+ from typing import List
7
6
  import numpy as np
8
7
  import os
9
- 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
10
- from celldetective.utils import interpolate_nan, estimate_unreliable_edge, unpad, config_section_to_dict, _extract_channel_indices_from_config, _extract_nbr_channels_from_config, _get_img_num_per_channel
11
- from celldetective.segmentation import filter_image, threshold_image
12
- from csbdeep.io import save_tiff_imagej_compatible
8
+ from celldetective.utils.image_loaders import (
9
+ auto_load_number_of_frames,
10
+ load_frames,
11
+ _get_img_num_per_channel,
12
+ )
13
+ from celldetective.utils.image_cleaning import interpolate_nan
14
+ from celldetective.utils.experiment import (
15
+ get_experiment_wells,
16
+ extract_well_name_and_number,
17
+ extract_position_name,
18
+ get_config,
19
+ interpret_wells_and_positions,
20
+ get_position_movie_path,
21
+ get_positions_in_well,
22
+ )
23
+ from celldetective.utils.image_transforms import (
24
+ estimate_unreliable_edge,
25
+ unpad,
26
+ threshold_image,
27
+ )
28
+ from celldetective.utils.parsing import (
29
+ config_section_to_dict,
30
+ _extract_channel_indices_from_config,
31
+ _extract_nbr_channels_from_config,
32
+ )
13
33
  from gc import collect
14
- from lmfit import Parameters, Model
15
- import tifffile.tifffile as tiff
16
- from scipy.ndimage import shift
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, offset=None, fix_nan: bool = False):
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
-
73
- config = get_config(experiment)
74
- wells = get_experiment_wells(experiment)
75
- len_movie = float(config_section_to_dict(config, "MovieSettings")["len_movie"])
76
- movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
77
-
78
- well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, "*")
79
-
80
- channel_indices = _extract_channel_indices_from_config(config, [target_channel])
81
- nbr_channels = _extract_nbr_channels_from_config(config)
82
- img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
83
-
84
- backgrounds = []
85
-
86
- for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
87
-
88
- well_name, _ = extract_well_name_and_number(well_path)
89
- well_idx = well_indices[k]
90
-
91
- positions = get_positions_in_well(well_path)
92
- print(f"Reconstruct a background in well {well_name} from positions: {[extract_position_name(p) for p in positions]}...")
93
-
94
- frame_mean_per_position = []
95
-
96
- for l,pos_path in enumerate(tqdm(positions, disable=not show_progress_per_pos)):
97
-
98
- stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
99
- if stack_path is not None:
100
- len_movie_auto = auto_load_number_of_frames(stack_path)
101
- if len_movie_auto is not None:
102
- len_movie = len_movie_auto
103
- img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
104
-
105
- if mode=="timeseries":
106
-
107
- frames = load_frames(img_num_channels[0,frame_range[0]:frame_range[1]], stack_path, normalize_input=False)
108
- frames = np.moveaxis(frames, -1, 0).astype(float)
109
-
110
- for i in range(len(frames)):
111
- if np.all(frames[i].flatten()==0):
112
- frames[i,:,:] = np.nan
113
-
114
- frame_mean = np.nanmean(frames, axis=0)
115
-
116
- frame = frame_mean.copy().astype(float)
117
- std_frame = filter_image(frame.copy(),filters=activation_protocol)
118
- edge = estimate_unreliable_edge(activation_protocol)
119
- mask = threshold_image(std_frame, threshold_on_std, np.inf, foreground_value=1, edge_exclusion=edge)
120
- frame[np.where(mask.astype(int)==1)] = np.nan
121
-
122
- elif mode=="tiles":
123
-
124
- frames = load_frames(img_num_channels[0,:], stack_path, normalize_input=False).astype(float)
125
- frames = np.moveaxis(frames, -1, 0).astype(float)
126
-
127
- new_frames = []
128
- for i in range(len(frames)):
129
-
130
- if np.all(frames[i].flatten()==0):
131
- empty_frame = np.zeros_like(frames[i])
132
- empty_frame[:,:] = np.nan
133
- new_frames.append(empty_frame)
134
- continue
135
-
136
- f = frames[i].copy()
137
- std_frame = filter_image(f.copy(),filters=activation_protocol)
138
- edge = estimate_unreliable_edge(activation_protocol)
139
- mask = threshold_image(std_frame, threshold_on_std, np.inf, foreground_value=1, edge_exclusion=edge)
140
- f[np.where(mask.astype(int)==1)] = np.nan
141
- new_frames.append(f.copy())
142
-
143
- frame = np.nanmedian(new_frames, axis=0)
144
- else:
145
- print(f'Stack not found for position {pos_path}...')
146
- frame = []
147
-
148
- # store
149
- frame_mean_per_position.append(frame)
150
-
151
- try:
152
- background = np.nanmedian(frame_mean_per_position,axis=0)
153
- if offset is not None:
154
- #print("The offset is applied to background...")
155
- background -= offset
156
- if fix_nan:
157
- background = interpolate_nan(background.copy().astype(float))
158
- backgrounds.append({"bg": background, "well": well_path})
159
- print(f"Background successfully computed for well {well_name}...")
160
- except Exception as e:
161
- print(e)
162
- backgrounds.append(None)
163
-
164
- return backgrounds
34
+ from tqdm import tqdm
35
+ from celldetective import get_logger
36
+
37
+ logger = get_logger(__name__)
38
+
39
+
40
+ def estimate_background_per_condition(
41
+ experiment,
42
+ threshold_on_std=1,
43
+ well_option="*",
44
+ target_channel="channel_name",
45
+ frame_range=[0, 5],
46
+ mode="timeseries",
47
+ activation_protocol=[["gauss", 2], ["std", 4]],
48
+ show_progress_per_pos=False,
49
+ show_progress_per_well=True,
50
+ offset=None,
51
+ fix_nan: bool = False,
52
+ progress_callback=None,
53
+ ):
54
+ """
55
+ Estimate the background for each condition in an experiment.
56
+
57
+ This function calculates the background for each well within
58
+ a given experiment by processing image frames using a specified activation
59
+ protocol. It supports time-series and tile-based modes for background
60
+ estimation.
61
+
62
+ Parameters
63
+ ----------
64
+ experiment : str
65
+ The path to the experiment directory.
66
+ threshold_on_std : float, optional
67
+ The threshold value on the standard deviation for masking (default is 1).
68
+ well_option : str, optional
69
+ The option to select specific wells (default is '*').
70
+ target_channel : str, optional
71
+ The name of the target channel for background estimation (default is "channel_name").
72
+ frame_range : list of int, optional
73
+ The range of frames to consider for background estimation (default is [0, 5]).
74
+ mode : str, optional
75
+ The mode of background estimation, either "timeseries" or "tiles" (default is "timeseries").
76
+ activation_protocol : list of list, optional
77
+ The activation protocol consisting of filters and their respective parameters (default is [['gauss', 2], ['std', 4]]).
78
+ show_progress_per_pos : bool, optional
79
+ Whether to show progress for each position (default is False).
80
+ show_progress_per_well : bool, optional
81
+ Whether to show progress for each well (default is True).
82
+ progress_callback : callable, optional
83
+ A callback function to be called at each step of the process (default is None).
84
+
85
+ Returns
86
+ -------
87
+ list of dict
88
+ A list of dictionaries, each containing the background image (`bg`) and the corresponding well path (`well`).
89
+
90
+ See Also
91
+ --------
92
+ estimate_unreliable_edge : Estimates the unreliable edge value from the activation protocol.
93
+ threshold_image : Thresholds an image based on the specified criteria.
94
+
95
+ Notes
96
+ -----
97
+ This function assumes that the experiment directory structure and the configuration
98
+ files follow a specific format expected by the helper functions used within.
99
+
100
+ Examples
101
+ --------
102
+ >>> experiment_path = "path/to/experiment"
103
+ >>> backgrounds = estimate_background_per_condition(experiment_path, threshold_on_std=1.5, target_channel="GFP", frame_range=[0, 10], mode="tiles")
104
+ >>> for bg in backgrounds:
105
+ ... print(bg["well"], bg["bg"].shape)
106
+ """
107
+
108
+ config = get_config(experiment)
109
+ wells = get_experiment_wells(experiment)
110
+ len_movie = float(config_section_to_dict(config, "MovieSettings")["len_movie"])
111
+ movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
112
+
113
+ well_indices, position_indices = interpret_wells_and_positions(
114
+ experiment, well_option, "*"
115
+ )
116
+
117
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
118
+ nbr_channels = _extract_nbr_channels_from_config(config)
119
+ img_num_channels = _get_img_num_per_channel(
120
+ channel_indices, int(len_movie), nbr_channels
121
+ )
122
+
123
+ backgrounds = []
124
+
125
+ for k, well_path in enumerate(
126
+ tqdm(wells[well_indices], disable=not show_progress_per_well)
127
+ ):
128
+
129
+ well_name, _ = extract_well_name_and_number(well_path)
130
+ well_idx = well_indices[k]
131
+
132
+ positions = get_positions_in_well(well_path)
133
+ logger.info(
134
+ f"Reconstruct a background in well {well_name} from positions: {[extract_position_name(p) for p in positions]}..."
135
+ )
136
+
137
+ frame_mean_per_position = []
138
+
139
+ for l, pos_path in enumerate(
140
+ tqdm(positions, disable=not show_progress_per_pos)
141
+ ):
142
+ if progress_callback is not None:
143
+ should_continue = progress_callback(
144
+ level="position", iter=l, total=len(positions)
145
+ )
146
+ if should_continue is False:
147
+ logger.info("Background estimation cancelled by user.")
148
+ return None
149
+
150
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
151
+ if stack_path is not None:
152
+ len_movie_auto = auto_load_number_of_frames(stack_path)
153
+ if len_movie_auto is not None:
154
+ len_movie = len_movie_auto
155
+ img_num_channels = _get_img_num_per_channel(
156
+ channel_indices, int(len_movie), nbr_channels
157
+ )
158
+
159
+ from celldetective.filters import filter_image
160
+
161
+ if mode == "timeseries":
162
+
163
+ frames = load_frames(
164
+ img_num_channels[0, frame_range[0] : frame_range[1]],
165
+ stack_path,
166
+ normalize_input=False,
167
+ )
168
+ frames = np.moveaxis(frames, -1, 0).astype(float)
169
+
170
+ for i in range(len(frames)):
171
+ if np.all(frames[i].flatten() == 0):
172
+ frames[i, :, :] = np.nan
173
+
174
+ frame_mean = np.nanmean(frames, axis=0)
175
+
176
+ frame = frame_mean.copy().astype(float)
177
+
178
+ std_frame = filter_image(frame.copy(), filters=activation_protocol)
179
+ edge = estimate_unreliable_edge(activation_protocol)
180
+ mask = threshold_image(
181
+ std_frame,
182
+ threshold_on_std,
183
+ np.inf,
184
+ foreground_value=1,
185
+ edge_exclusion=edge,
186
+ )
187
+ frame[np.where(mask.astype(int) == 1)] = np.nan
188
+
189
+ elif mode == "tiles":
190
+
191
+ frames = load_frames(
192
+ img_num_channels[0, :], stack_path, normalize_input=False
193
+ ).astype(float)
194
+ frames = np.moveaxis(frames, -1, 0).astype(float)
195
+
196
+ new_frames = []
197
+ for i in range(len(frames)):
198
+
199
+ if np.all(frames[i].flatten() == 0):
200
+ empty_frame = np.zeros_like(frames[i])
201
+ empty_frame[:, :] = np.nan
202
+ new_frames.append(empty_frame)
203
+ continue
204
+
205
+ f = frames[i].copy()
206
+ std_frame = filter_image(f.copy(), filters=activation_protocol)
207
+ edge = estimate_unreliable_edge(activation_protocol)
208
+ mask = threshold_image(
209
+ std_frame,
210
+ threshold_on_std,
211
+ np.inf,
212
+ foreground_value=1,
213
+ edge_exclusion=edge,
214
+ )
215
+ f[np.where(mask.astype(int) == 1)] = np.nan
216
+ new_frames.append(f.copy())
217
+
218
+ frame = np.nanmedian(new_frames, axis=0)
219
+ else:
220
+ print(f"Stack not found for position {pos_path}...")
221
+ frame = []
222
+
223
+ # store
224
+ frame_mean_per_position.append(frame)
225
+
226
+ if progress_callback:
227
+ progress_callback(
228
+ level="position", iter=l, total=len(positions), stage="estimating"
229
+ )
230
+
231
+ try:
232
+ background = np.nanmedian(frame_mean_per_position, axis=0)
233
+ if progress_callback:
234
+ progress_callback(image_preview=background)
235
+
236
+ if offset is not None:
237
+ # print("The offset is applied to background...")
238
+ background -= offset
239
+ if fix_nan:
240
+ background = interpolate_nan(background.copy().astype(float))
241
+ backgrounds.append({"bg": background, "well": well_path})
242
+ logger.info(f"Background successfully computed for well {well_name}...")
243
+ except Exception as e:
244
+ logger.error(e)
245
+ backgrounds.append(None)
246
+
247
+ return backgrounds
248
+
165
249
 
166
250
  def correct_background_model_free(
167
- experiment,
168
- well_option='*',
169
- position_option='*',
170
- target_channel="channel_name",
171
- mode = "timeseries",
172
- threshold_on_std = 1,
173
- frame_range = [0,5],
174
- optimize_option = False,
175
- opt_coef_range = [0.95,1.05],
176
- opt_coef_nbr = 100,
177
- operation = 'divide',
178
- clip = False,
179
- offset = None,
180
- show_progress_per_well = True,
181
- show_progress_per_pos = False,
182
- export = False,
183
- return_stacks = False,
184
- movie_prefix=None,
185
- fix_nan=False,
186
- activation_protocol=[['gauss',2],['std',4]],
187
- export_prefix='Corrected',
188
- **kwargs,
189
- ):
190
-
191
- """
192
- Correct the background of image stacks for a given experiment.
193
-
194
- This function processes image stacks by estimating and correcting the background
195
- for each well and position in the experiment. It supports different modes, such
196
- as timeseries or tiles, and offers options for optimization and exporting the results.
197
-
198
- Parameters
199
- ----------
200
- experiment : str
201
- Path to the experiment configuration.
202
- well_option : str, int, or list of int, optional
203
- Selection of wells to process. '*' indicates all wells. Defaults to '*'.
204
- position_option : str, int, or list of int, optional
205
- Selection of positions to process within each well. '*' indicates all positions. Defaults to '*'.
206
- target_channel : str, optional
207
- The name of the target channel to be corrected. Defaults to "channel_name".
208
- mode : {'timeseries', 'tiles'}, optional
209
- The mode of processing. Defaults to "timeseries".
210
- threshold_on_std : float, optional
211
- The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
212
- frame_range : list of int, optional
213
- The range of frames to consider for background estimation. Defaults to [0, 5].
214
- optimize_option : bool, optional
215
- If True, optimize the correction coefficient. Defaults to False.
216
- opt_coef_range : list of float, optional
217
- The range of coefficients to try for optimization. Defaults to [0.95, 1.05].
218
- opt_coef_nbr : int, optional
219
- The number of coefficients to test within the optimization range. Defaults to 100.
220
- operation : {'divide', 'subtract'}, optional
221
- The operation to apply for background correction. Defaults to 'divide'.
222
- clip : bool, optional
223
- If True, clip the corrected values to be non-negative when using subtraction. Defaults to False.
224
- show_progress_per_well : bool, optional
225
- If True, show progress bar for each well. Defaults to True.
226
- show_progress_per_pos : bool, optional
227
- If True, show progress bar for each position. Defaults to False.
228
- export : bool, optional
229
- If True, export the corrected stacks to files. Defaults to False.
230
- return_stacks : bool, optional
231
- If True, return the corrected stacks as a list of numpy arrays. Defaults to False.
232
-
233
- Returns
234
- -------
235
- list of numpy.ndarray, optional
236
- A list of corrected image stacks if `return_stacks` is True.
237
-
238
- Notes
239
- -----
240
- The function uses several helper functions, including `interpret_wells_and_positions`,
241
- `estimate_background_per_condition`, and `apply_background_to_stack`.
242
-
243
- Examples
244
- --------
245
- >>> experiment = "path/to/experiment/config"
246
- >>> 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)
247
- >>> print(len(corrected_stacks))
248
- 2
249
-
250
- """
251
-
252
- config = get_config(experiment)
253
- wells = get_experiment_wells(experiment)
254
- len_movie = float(config_section_to_dict(config, "MovieSettings")["len_movie"])
255
- if movie_prefix is None:
256
- movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
257
-
258
- well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
259
- channel_indices = _extract_channel_indices_from_config(config, [target_channel])
260
- nbr_channels = _extract_nbr_channels_from_config(config)
261
- img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
262
-
263
- stacks = []
264
-
265
- for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
266
-
267
- well_name, _ = extract_well_name_and_number(well_path)
268
-
269
- try:
270
- 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, offset=offset, fix_nan=fix_nan)
271
- background = background[0]
272
- background = background['bg']
273
- except Exception as e:
274
- print(f'Background could not be estimated due to error "{e}"... Skipping well {well_name}...')
275
- continue
276
-
277
- positions = get_positions_in_well(well_path)
278
- selection = positions[position_indices]
279
- if isinstance(selection[0],np.ndarray):
280
- selection = selection[0]
281
-
282
- for pidx,pos_path in enumerate(tqdm(selection, disable=not show_progress_per_pos)):
283
-
284
- stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
285
- print(f'Applying the correction to position {extract_position_name(pos_path)}...')
286
- if stack_path is not None:
287
- len_movie_auto = auto_load_number_of_frames(stack_path)
288
- if len_movie_auto is not None:
289
- len_movie = len_movie_auto
290
- img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
291
-
292
- corrected_stack = apply_background_to_stack(stack_path,
293
- background,
294
- target_channel_index = channel_indices[0],
295
- nbr_channels = nbr_channels,
296
- stack_length = len_movie,
297
- threshold_on_std = threshold_on_std,
298
- optimize_option = optimize_option,
299
- opt_coef_range = opt_coef_range,
300
- opt_coef_nbr = opt_coef_nbr,
301
- operation = operation,
302
- clip = clip,
303
- offset = offset,
304
- export = export,
305
- fix_nan=fix_nan,
306
- activation_protocol = activation_protocol,
307
- prefix = export_prefix,
308
- )
309
- print('Correction successful.')
310
- if return_stacks:
311
- stacks.append(corrected_stack)
312
- else:
313
- del corrected_stack
314
- collect()
315
- else:
316
- stacks.append(None)
317
-
318
- if return_stacks:
319
- return stacks
320
-
321
-
322
-
323
- def apply_background_to_stack(stack_path, background, target_channel_index=0, nbr_channels=1, stack_length=45, offset = None, 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", fix_nan=False):
324
-
325
- """
326
- Apply background correction to an image stack.
327
-
328
- This function corrects the background of an image stack by applying a specified operation
329
- (either division or subtraction) between the image stack and the background. It also supports
330
- optimization of the correction coefficient through brute-force regression.
331
-
332
- Parameters
333
- ----------
334
- stack_path : str
335
- The path to the image stack file.
336
- background : numpy.ndarray
337
- The background image to be applied for correction.
338
- target_channel_index : int, optional
339
- The index of the target channel to be corrected. Defaults to 0.
340
- nbr_channels : int, optional
341
- The number of channels in the image stack. Defaults to 1.
342
- stack_length : int, optional
343
- The length of the image stack (number of frames). If None, the length is auto-detected. Defaults to 45.
344
- threshold_on_std : float, optional
345
- The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
346
- optimize_option : bool, optional
347
- If True, optimize the correction coefficient using a range of values. Defaults to True.
348
- opt_coef_range : tuple of float, optional
349
- The range of coefficients to try for optimization. Defaults to (0.95, 1.05).
350
- opt_coef_nbr : int, optional
351
- The number of coefficients to test within the optimization range. Defaults to 100.
352
- operation : {'divide', 'subtract'}, optional
353
- The operation to apply for background correction. Defaults to 'divide'.
354
- clip : bool, optional
355
- If True, clip the corrected values to be non-negative when using subtraction. Defaults to False.
356
- export : bool, optional
357
- If True, export the corrected stack to a file. Defaults to False.
358
- prefix : str, optional
359
- The prefix for the exported file name. Defaults to "Corrected".
360
-
361
- Returns
362
- -------
363
- corrected_stack : numpy.ndarray
364
- The background-corrected image stack.
365
-
366
- Examples
367
- --------
368
- >>> stack_path = "path/to/stack.tif"
369
- >>> background = np.zeros((512, 512)) # Example background
370
- >>> 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)
371
- >>> print(corrected_stack.shape)
372
- (44, 512, 512, 3)
373
-
374
- """
375
-
376
- if stack_length is None:
377
- stack_length = auto_load_number_of_frames(stack_path)
378
- if stack_length is None:
379
- print('stack length not provided')
380
- return None
381
-
382
- if optimize_option:
383
- coefficients = np.linspace(opt_coef_range[0], opt_coef_range[1], int(opt_coef_nbr))
384
- coefficients = np.append(coefficients, [1.0])
385
- if export:
386
- path,file = os.path.split(stack_path)
387
- if prefix is None:
388
- newfile = file
389
- else:
390
- newfile = '_'.join([prefix,file])
391
-
392
- corrected_stack = []
393
-
394
- for i in range(0,int(stack_length*nbr_channels),nbr_channels):
395
-
396
- frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
397
- target_img = frames[:,:,target_channel_index].copy()
398
- if offset is not None:
399
- #print(f"The offset is applied to image...")
400
- target_img -= offset
401
-
402
- if optimize_option:
403
-
404
- target_copy = target_img.copy()
405
-
406
- std_frame = filter_image(target_copy.copy(),filters=activation_protocol)
407
- edge = estimate_unreliable_edge(activation_protocol)
408
- mask = threshold_image(std_frame, threshold_on_std, np.inf, foreground_value=1, edge_exclusion=edge)
409
- target_copy[np.where(mask.astype(int)==1)] = np.nan
410
-
411
- loss = []
412
-
413
- # brute-force regression, could do gradient descent instead
414
- for c in coefficients:
415
-
416
- target_crop = unpad(target_copy,edge)
417
- bg_crop = unpad(background, edge)
418
-
419
- roi = np.zeros_like(target_crop).astype(int)
420
- roi[target_crop!=target_crop] = 1
421
- roi[bg_crop!=bg_crop] = 1
422
-
423
- diff = np.subtract(target_crop, c*bg_crop, where=roi==0)
424
- s = np.sum(np.abs(diff, where=roi==0), where=roi==0)
425
- loss.append(s)
426
-
427
- c = coefficients[np.argmin(loss)]
428
- print(f"IFD {i}; optimal coefficient: {c}...")
429
- # if c==min(coefficients) or c==max(coefficients):
430
- # print('Warning... The optimal coefficient is beyond the range provided... Please adjust your coefficient range...')
431
- else:
432
- c=1
433
-
434
- if operation=="divide":
435
- correction = np.divide(target_img, background*c, where=background==background)
436
- correction[background!=background] = np.nan
437
- correction[target_img!=target_img] = np.nan
438
-
439
- elif operation=="subtract":
440
- correction = np.subtract(target_img, background*c, where=background==background)
441
- correction[background!=background] = np.nan
442
- correction[target_img!=target_img] = np.nan
443
- if clip:
444
- correction[correction<=0.] = 0.
445
- else:
446
- print("Operation not supported... Abort.")
447
- return
448
-
449
- correction[~np.isfinite(correction)] = np.nan
450
- if fix_nan:
451
- correction = interpolate_nan(correction.copy())
452
- frames[:,:,target_channel_index] = correction
453
- corrected_stack.append(frames)
454
-
455
- corrected_stack = np.array(corrected_stack)
456
-
457
- if export:
458
- save_tiff_imagej_compatible(os.sep.join([path,newfile]), corrected_stack, axes='TYXC')
459
-
460
- return corrected_stack
251
+ experiment,
252
+ well_option="*",
253
+ position_option="*",
254
+ target_channel="channel_name",
255
+ mode="timeseries",
256
+ threshold_on_std=1,
257
+ frame_range=[0, 5],
258
+ optimize_option=False,
259
+ opt_coef_range=[0.95, 1.05],
260
+ opt_coef_nbr=100,
261
+ operation="divide",
262
+ clip=False,
263
+ offset=None,
264
+ show_progress_per_well=True,
265
+ show_progress_per_pos=False,
266
+ export=False,
267
+ return_stacks=False,
268
+ movie_prefix=None,
269
+ fix_nan=False,
270
+ activation_protocol=[["gauss", 2], ["std", 4]],
271
+ export_prefix="Corrected",
272
+ progress_callback=None,
273
+ **kwargs,
274
+ ):
275
+ """
276
+ Correct the background of image stacks for a given experiment.
277
+
278
+ This function processes image stacks by estimating and correcting the background
279
+ for each well and position in the experiment. It supports different modes, such
280
+ as timeseries or tiles, and offers options for optimization and exporting the results.
281
+
282
+ Parameters
283
+ ----------
284
+ experiment : str
285
+ Path to the experiment configuration.
286
+ well_option : str, int, or list of int, optional
287
+ Selection of wells to process. '*' indicates all wells. Defaults to '*'.
288
+ position_option : str, int, or list of int, optional
289
+ Selection of positions to process within each well. '*' indicates all positions. Defaults to '*'.
290
+ target_channel : str, optional
291
+ The name of the target channel to be corrected. Defaults to "channel_name".
292
+ mode : {'timeseries', 'tiles'}, optional
293
+ The mode of processing. Defaults to "timeseries".
294
+ threshold_on_std : float, optional
295
+ The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
296
+ frame_range : list of int, optional
297
+ The range of frames to consider for background estimation. Defaults to [0, 5].
298
+ optimize_option : bool, optional
299
+ If True, optimize the correction coefficient. Defaults to False.
300
+ opt_coef_range : list of float, optional
301
+ The range of coefficients to try for optimization. Defaults to [0.95, 1.05].
302
+ opt_coef_nbr : int, optional
303
+ The number of coefficients to test within the optimization range. Defaults to 100.
304
+ operation : {'divide', 'subtract'}, optional
305
+ The operation to apply for background correction. Defaults to 'divide'.
306
+ clip : bool, optional
307
+ If True, clip the corrected values to be non-negative when using subtraction. Defaults to False.
308
+ show_progress_per_well : bool, optional
309
+ If True, show progress bar for each well. Defaults to True.
310
+ show_progress_per_pos : bool, optional
311
+ If True, show progress bar for each position. Defaults to False.
312
+ export : bool, optional
313
+ If True, export the corrected stacks to files. Defaults to False.
314
+ return_stacks : bool, optional
315
+ If True, return the corrected stacks as a list of numpy arrays. Defaults to False.
316
+ progress_callback : callable, optional
317
+ A callback function to be called at each step of the process (default is None).
318
+
319
+ Returns
320
+ -------
321
+ list of numpy.ndarray, optional
322
+ A list of corrected image stacks if `return_stacks` is True.
323
+
324
+ Notes
325
+ -----
326
+ The function uses several helper functions, including `interpret_wells_and_positions`,
327
+ `estimate_background_per_condition`, and `apply_background_to_stack`.
328
+
329
+ Examples
330
+ --------
331
+ >>> experiment = "path/to/experiment/config"
332
+ >>> 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)
333
+ >>> print(len(corrected_stacks))
334
+ 2
335
+
336
+ """
337
+
338
+ config = get_config(experiment)
339
+ wells = get_experiment_wells(experiment)
340
+ len_movie = float(config_section_to_dict(config, "MovieSettings")["len_movie"])
341
+ if movie_prefix is None:
342
+ movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
343
+
344
+ well_indices, position_indices = interpret_wells_and_positions(
345
+ experiment, well_option, position_option
346
+ )
347
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
348
+ nbr_channels = _extract_nbr_channels_from_config(config)
349
+ img_num_channels = _get_img_num_per_channel(
350
+ channel_indices, int(len_movie), nbr_channels
351
+ )
352
+
353
+ stacks = []
354
+
355
+ total_wells = len(wells[well_indices])
356
+
357
+ for k, well_path in enumerate(
358
+ tqdm(wells[well_indices], disable=not show_progress_per_well)
359
+ ):
360
+ if progress_callback:
361
+ progress_callback(level="well", iter=k, total=total_wells)
362
+
363
+ well_name, _ = extract_well_name_and_number(well_path)
364
+
365
+ if progress_callback:
366
+ progress_callback(status="Reconstructing background...")
367
+
368
+ try:
369
+ # Estimate background
370
+ background = estimate_background_per_condition(
371
+ experiment,
372
+ threshold_on_std=threshold_on_std,
373
+ well_option=int(well_indices[k]),
374
+ target_channel=target_channel,
375
+ frame_range=frame_range,
376
+ mode=mode,
377
+ show_progress_per_pos=True,
378
+ show_progress_per_well=False,
379
+ activation_protocol=activation_protocol,
380
+ offset=offset,
381
+ fix_nan=fix_nan,
382
+ progress_callback=progress_callback,
383
+ )
384
+ background = background[0]
385
+ background = background["bg"]
386
+ except Exception as e:
387
+ logger.error(
388
+ f'Background could not be estimated due to error "{e}"... Skipping well {well_name}...'
389
+ )
390
+ if progress_callback:
391
+ progress_callback(level="well", iter=k + 1, total=total_wells)
392
+ if progress_callback:
393
+ progress_callback(level="well", iter=k + 1, total=total_wells)
394
+ continue
395
+
396
+ if progress_callback:
397
+ progress_callback(
398
+ level="position", iter=-1, total=1, status="Applying background..."
399
+ )
400
+
401
+ positions = get_positions_in_well(well_path)
402
+ selection = positions[position_indices]
403
+ if isinstance(selection[0], np.ndarray):
404
+ selection = selection[0]
405
+
406
+ total_pos_in_well = len(selection)
407
+
408
+ for pidx, pos_path in enumerate(
409
+ tqdm(selection, disable=not show_progress_per_pos)
410
+ ):
411
+
412
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
413
+ logger.info(
414
+ f"Applying the correction to position {extract_position_name(pos_path)}..."
415
+ )
416
+ if stack_path is not None:
417
+ len_movie_auto = auto_load_number_of_frames(stack_path)
418
+ if len_movie_auto is not None:
419
+ len_movie = len_movie_auto
420
+ img_num_channels = _get_img_num_per_channel(
421
+ channel_indices, int(len_movie), nbr_channels
422
+ )
423
+
424
+ corrected_stack = apply_background_to_stack(
425
+ stack_path,
426
+ background,
427
+ target_channel_index=channel_indices[0],
428
+ nbr_channels=nbr_channels,
429
+ stack_length=len_movie,
430
+ threshold_on_std=threshold_on_std,
431
+ optimize_option=optimize_option,
432
+ opt_coef_range=opt_coef_range,
433
+ opt_coef_nbr=opt_coef_nbr,
434
+ operation=operation,
435
+ clip=clip,
436
+ offset=offset,
437
+ export=export,
438
+ fix_nan=fix_nan,
439
+ activation_protocol=activation_protocol,
440
+ prefix=export_prefix,
441
+ progress_callback=progress_callback,
442
+ )
443
+ logger.info("Correction successful.")
444
+ if return_stacks:
445
+ stacks.append(corrected_stack)
446
+ else:
447
+ del corrected_stack
448
+ collect()
449
+ else:
450
+ stacks.append(None)
451
+
452
+ if progress_callback:
453
+ progress_callback(
454
+ level="position",
455
+ iter=pidx,
456
+ total=total_pos_in_well,
457
+ stage="correcting",
458
+ )
459
+
460
+ if progress_callback:
461
+ progress_callback(level="well", iter=k + 1, total=total_wells)
462
+
463
+ if return_stacks:
464
+ return stacks
465
+
466
+
467
+ def apply_background_to_stack(
468
+ stack_path,
469
+ background,
470
+ target_channel_index=0,
471
+ nbr_channels=1,
472
+ stack_length=45,
473
+ offset=None,
474
+ activation_protocol=[["gauss", 2], ["std", 4]],
475
+ threshold_on_std=1,
476
+ optimize_option=True,
477
+ opt_coef_range=(0.95, 1.05),
478
+ opt_coef_nbr=100,
479
+ operation="divide",
480
+ clip=False,
481
+ export=False,
482
+ prefix="Corrected",
483
+ fix_nan=False,
484
+ progress_callback=None,
485
+ ):
486
+ """
487
+ Apply background correction to an image stack.
488
+
489
+ This function corrects the background of an image stack by applying a specified operation
490
+ (either division or subtraction) between the image stack and the background. It also supports
491
+ optimization of the correction coefficient through brute-force regression.
492
+
493
+ Parameters
494
+ ----------
495
+ stack_path : str
496
+ The path to the image stack file.
497
+ background : numpy.ndarray
498
+ The background image to be applied for correction.
499
+ target_channel_index : int, optional
500
+ The index of the target channel to be corrected. Defaults to 0.
501
+ nbr_channels : int, optional
502
+ The number of channels in the image stack. Defaults to 1.
503
+ stack_length : int, optional
504
+ The length of the image stack (number of frames). If None, the length is auto-detected. Defaults to 45.
505
+ threshold_on_std : float, optional
506
+ The threshold for the standard deviation filter to identify high-variance areas. Defaults to 1.
507
+ optimize_option : bool, optional
508
+ If True, optimize the correction coefficient using a range of values. Defaults to True.
509
+ opt_coef_range : tuple of float, optional
510
+ The range of coefficients to try for optimization. Defaults to (0.95, 1.05).
511
+ opt_coef_nbr : int, optional
512
+ The number of coefficients to test within the optimization range. Defaults to 100.
513
+ operation : {'divide', 'subtract'}, optional
514
+ The operation to apply for background correction. Defaults to 'divide'.
515
+ clip : bool, optional
516
+ If True, clip the corrected values to be non-negative when using subtraction. Defaults to False.
517
+ export : bool, optional
518
+ If True, export the corrected stack to a file. Defaults to False.
519
+ prefix : str, optional
520
+ The prefix for the exported file name. Defaults to "Corrected".
521
+ progress_callback : callable, optional
522
+ A callback function to be called at each step of the process (default is None).
523
+
524
+ Returns
525
+ -------
526
+ corrected_stack : numpy.ndarray
527
+ The background-corrected image stack.
528
+
529
+ Examples
530
+ --------
531
+ >>> stack_path = "path/to/stack.tif"
532
+ >>> background = np.zeros((512, 512)) # Example background
533
+ >>> 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)
534
+ >>> print(corrected_stack.shape)
535
+ (44, 512, 512, 3)
536
+
537
+ """
538
+ import os
539
+ import numpy as np
540
+
541
+ if stack_length is None:
542
+ stack_length = auto_load_number_of_frames(stack_path)
543
+ if stack_length is None:
544
+ logger.error("stack length not provided")
545
+ return None
546
+
547
+ if optimize_option:
548
+ coefficients = np.linspace(
549
+ opt_coef_range[0], opt_coef_range[1], int(opt_coef_nbr)
550
+ )
551
+ coefficients = np.append(coefficients, [1.0])
552
+ if export:
553
+ path, file = os.path.split(stack_path)
554
+ if prefix is None:
555
+ newfile = file
556
+ else:
557
+ newfile = "_".join([prefix, file])
558
+
559
+ corrected_stack = []
560
+
561
+ for i in range(0, int(stack_length * nbr_channels), nbr_channels):
562
+
563
+ frames = load_frames(
564
+ list(np.arange(i, (i + nbr_channels))), stack_path, normalize_input=False
565
+ ).astype(float)
566
+ target_img = frames[:, :, target_channel_index].copy()
567
+ if offset is not None:
568
+ # print(f"The offset is applied to image...")
569
+ target_img -= offset
570
+
571
+ if optimize_option:
572
+
573
+ target_copy = target_img.copy()
574
+
575
+ from celldetective.segmentation import threshold_image
576
+ from celldetective.filters import filter_image
577
+
578
+ std_frame = filter_image(target_copy.copy(), filters=activation_protocol)
579
+ edge = estimate_unreliable_edge(activation_protocol)
580
+ mask = threshold_image(
581
+ std_frame,
582
+ threshold_on_std,
583
+ np.inf,
584
+ foreground_value=1,
585
+ edge_exclusion=edge,
586
+ )
587
+ target_copy[np.where(mask.astype(int) == 1)] = np.nan
588
+
589
+ loss = []
590
+
591
+ # brute-force regression, could do gradient descent instead
592
+ for c in coefficients:
593
+
594
+ target_crop = unpad(target_copy, edge)
595
+ bg_crop = unpad(background, edge)
596
+
597
+ roi = np.zeros_like(target_crop).astype(int)
598
+ roi[target_crop != target_crop] = 1
599
+ roi[bg_crop != bg_crop] = 1
600
+
601
+ diff = np.subtract(target_crop, c * bg_crop, where=roi == 0)
602
+ s = np.sum(np.abs(diff, where=roi == 0), where=roi == 0)
603
+ loss.append(s)
604
+
605
+ c = coefficients[np.argmin(loss)]
606
+ logger.info(f"IFD {i}; optimal coefficient: {c}...")
607
+ # if c==min(coefficients) or c==max(coefficients):
608
+ # print('Warning... The optimal coefficient is beyond the range provided... Please adjust your coefficient range...')
609
+ else:
610
+ c = 1
611
+
612
+ if operation == "divide":
613
+ correction = np.divide(
614
+ target_img, background * c, where=background == background
615
+ )
616
+ correction[background != background] = np.nan
617
+ correction[target_img != target_img] = np.nan
618
+
619
+ elif operation == "subtract":
620
+ correction = np.subtract(
621
+ target_img, background * c, where=background == background
622
+ )
623
+ correction[background != background] = np.nan
624
+ correction[target_img != target_img] = np.nan
625
+ if clip:
626
+ correction[correction <= 0.0] = 0.0
627
+ else:
628
+ logger.error("Operation not supported... Abort.")
629
+ return
630
+
631
+ correction[~np.isfinite(correction)] = np.nan
632
+ if fix_nan:
633
+ correction = interpolate_nan(correction.copy())
634
+ frames[:, :, target_channel_index] = correction
635
+ corrected_stack.append(frames)
636
+
637
+ if progress_callback:
638
+ progress_callback(
639
+ level="frame",
640
+ iter=i,
641
+ total=int(stack_length * nbr_channels),
642
+ stage="correcting",
643
+ )
644
+
645
+ corrected_stack = np.array(corrected_stack)
646
+
647
+ if export:
648
+ from celldetective.utils.io import save_tiff_imagej_compatible
649
+
650
+ save_tiff_imagej_compatible(
651
+ os.sep.join([path, newfile]), corrected_stack, axes="TYXC"
652
+ )
653
+
654
+ return corrected_stack
461
655
 
462
- def paraboloid(x, y, a, b, c, d, e, g):
463
656
 
464
- """
465
- Compute the value of a 2D paraboloid function.
466
-
467
- This function evaluates a paraboloid defined by the equation:
468
- `a * x ** 2 + b * y ** 2 + c * x * y + d * x + e * y + g`.
469
-
470
- Parameters
471
- ----------
472
- x : float or ndarray
473
- The x-coordinate(s) at which to evaluate the paraboloid.
474
- y : float or ndarray
475
- The y-coordinate(s) at which to evaluate the paraboloid.
476
- a : float
477
- The coefficient of the x^2 term.
478
- b : float
479
- The coefficient of the y^2 term.
480
- c : float
481
- The coefficient of the x*y term.
482
- d : float
483
- The coefficient of the x term.
484
- e : float
485
- The coefficient of the y term.
486
- g : float
487
- The constant term.
488
-
489
- Returns
490
- -------
491
- float or ndarray
492
- The value of the paraboloid at the given (x, y) coordinates. If `x` and
493
- `y` are arrays, the result is an array of the same shape.
494
-
495
- Examples
496
- --------
497
- >>> paraboloid(1, 2, 1, 1, 0, 0, 0, 0)
498
- 5
499
- >>> paraboloid(np.array([1, 2]), np.array([3, 4]), 1, 1, 0, 0, 0, 0)
500
- array([10, 20])
501
-
502
- Notes
503
- -----
504
- The paraboloid function is a quadratic function in two variables, commonly used
505
- to model surfaces in three-dimensional space.
506
- """
507
-
508
- return a * x ** 2 + b * y ** 2 + c * x * y + d * x + e * y + g
657
+ def paraboloid(x, y, a, b, c, d, e, g):
658
+ """
659
+ Compute the value of a 2D paraboloid function.
660
+
661
+ This function evaluates a paraboloid defined by the equation:
662
+ `a * x ** 2 + b * y ** 2 + c * x * y + d * x + e * y + g`.
663
+
664
+ Parameters
665
+ ----------
666
+ x : float or ndarray
667
+ The x-coordinate(s) at which to evaluate the paraboloid.
668
+ y : float or ndarray
669
+ The y-coordinate(s) at which to evaluate the paraboloid.
670
+ a : float
671
+ The coefficient of the x^2 term.
672
+ b : float
673
+ The coefficient of the y^2 term.
674
+ c : float
675
+ The coefficient of the x*y term.
676
+ d : float
677
+ The coefficient of the x term.
678
+ e : float
679
+ The coefficient of the y term.
680
+ g : float
681
+ The constant term.
682
+
683
+ Returns
684
+ -------
685
+ float or ndarray
686
+ The value of the paraboloid at the given (x, y) coordinates. If `x` and
687
+ `y` are arrays, the result is an array of the same shape.
688
+
689
+ Examples
690
+ --------
691
+ >>> paraboloid(1, 2, 1, 1, 0, 0, 0, 0)
692
+ 5
693
+ >>> paraboloid(np.array([1, 2]), np.array([3, 4]), 1, 1, 0, 0, 0, 0)
694
+ array([10, 20])
695
+
696
+ Notes
697
+ -----
698
+ The paraboloid function is a quadratic function in two variables, commonly used
699
+ to model surfaces in three-dimensional space.
700
+ """
701
+
702
+ return a * x**2 + b * y**2 + c * x * y + d * x + e * y + g
509
703
 
510
704
 
511
705
  def plane(x, y, a, b, c):
512
-
513
- """
514
- Compute the value of a plane function.
515
-
516
- This function evaluates a plane defined by the equation:
517
- `a * x + b * y + c`.
518
-
519
- Parameters
520
- ----------
521
- x : float or ndarray
522
- The x-coordinate(s) at which to evaluate the plane.
523
- y : float or ndarray
524
- The y-coordinate(s) at which to evaluate the plane.
525
- a : float
526
- The coefficient of the x term.
527
- b : float
528
- The coefficient of the y term.
529
- c : float
530
- The constant term.
531
-
532
- Returns
533
- -------
534
- float or ndarray
535
- The value of the plane at the given (x, y) coordinates. If `x` and
536
- `y` are arrays, the result is an array of the same shape.
537
-
538
- Examples
539
- --------
540
- >>> plane(1, 2, 3, 4, 5)
541
- 16
542
- >>> plane(np.array([1, 2]), np.array([3, 4]), 3, 4, 5)
543
- array([20, 27])
544
-
545
- Notes
546
- -----
547
- The plane function is a linear function in two variables, commonly used
548
- to model flat surfaces in three-dimensional space.
549
- """
550
-
551
- return a * x + b * y + c
706
+ """
707
+ Compute the value of a plane function.
708
+
709
+ This function evaluates a plane defined by the equation:
710
+ `a * x + b * y + c`.
711
+
712
+ Parameters
713
+ ----------
714
+ x : float or ndarray
715
+ The x-coordinate(s) at which to evaluate the plane.
716
+ y : float or ndarray
717
+ The y-coordinate(s) at which to evaluate the plane.
718
+ a : float
719
+ The coefficient of the x term.
720
+ b : float
721
+ The coefficient of the y term.
722
+ c : float
723
+ The constant term.
724
+
725
+ Returns
726
+ -------
727
+ float or ndarray
728
+ The value of the plane at the given (x, y) coordinates. If `x` and
729
+ `y` are arrays, the result is an array of the same shape.
730
+
731
+ Examples
732
+ --------
733
+ >>> plane(1, 2, 3, 4, 5)
734
+ 16
735
+ >>> plane(np.array([1, 2]), np.array([3, 4]), 3, 4, 5)
736
+ array([20, 27])
737
+
738
+ Notes
739
+ -----
740
+ The plane function is a linear function in two variables, commonly used
741
+ to model flat surfaces in three-dimensional space.
742
+ """
743
+
744
+ return a * x + b * y + c
552
745
 
553
746
 
554
747
  def fit_plane(image, cell_masks=None, edge_exclusion=None):
555
-
556
- """
557
- Fit a plane to the given image data.
558
-
559
- This function fits a plane to the provided image data using least squares
560
- regression. It constructs a mesh grid based on the dimensions of the image
561
- and fits a plane model to the data points. If cell masks are provided,
562
- areas covered by cell masks will be excluded from the fitting process.
563
-
564
- Parameters
565
- ----------
566
- image : numpy.ndarray
567
- The input image data.
568
- cell_masks : numpy.ndarray, optional
569
- An array specifying cell masks. If provided, areas covered by cell masks
570
- will be excluded from the fitting process (default is None).
571
- edge_exclusion : int, optional
572
- The size of the edge to exclude from the fitting process (default is None).
573
-
574
- Returns
575
- -------
576
- numpy.ndarray
577
- The fitted plane.
578
-
579
- Notes
580
- -----
581
- - The `cell_masks` parameter allows excluding areas covered by cell masks from
582
- the fitting process.
583
- - The `edge_exclusion` parameter allows excluding edges of the specified size
584
- from the fitting process to avoid boundary effects.
585
-
586
- See Also
587
- --------
588
- plane : The plane function used for fitting.
589
- """
590
-
591
- data = np.empty(image.shape)
592
- x = np.arange(0, image.shape[1])
593
- y = np.arange(0, image.shape[0])
594
- xx, yy = np.meshgrid(x, y)
595
-
596
- params = Parameters()
597
- params.add('a', value=1)
598
- params.add('b', value=1)
599
- params.add('c', value=1)
600
-
601
- model = Model(plane, independent_vars=['x', 'y'])
602
-
603
- weights = np.ones_like(xx, dtype=float)
604
- if cell_masks is not None:
605
- weights[np.where(cell_masks > 0)] = 0.
606
-
607
- if edge_exclusion is not None:
608
- xx = unpad(xx, edge_exclusion)
609
- yy = unpad(yy, edge_exclusion)
610
- weights = unpad(weights, edge_exclusion)
611
- image = unpad(image, edge_exclusion)
612
-
613
- result = model.fit(image,
614
- x=xx,
615
- y=yy,
616
- weights=weights,
617
- params=params, max_nfev=3000)
618
- del model
619
- collect()
620
-
621
- xx, yy = np.meshgrid(x, y)
622
-
623
- return plane(xx, yy, **result.params)
624
-
625
-
626
- def fit_paraboloid(image, cell_masks=None, edge_exclusion=None):
627
-
628
- """
629
- Fit a paraboloid to the given image data.
630
-
631
- This function fits a paraboloid to the provided image data using least squares
632
- regression. It constructs a mesh grid based on the dimensions of the image
633
- and fits a paraboloid model to the data points. If cell masks are provided,
634
- areas covered by cell masks will be excluded from the fitting process.
635
-
636
- Parameters
637
- ----------
638
- image : numpy.ndarray
639
- The input image data.
640
- cell_masks : numpy.ndarray, optional
641
- An array specifying cell masks. If provided, areas covered by cell masks
642
- will be excluded from the fitting process (default is None).
643
- edge_exclusion : int, optional
644
- The size of the edge to exclude from the fitting process (default is None).
645
-
646
- Returns
647
- -------
648
- numpy.ndarray
649
- The fitted paraboloid.
650
-
651
- Notes
652
- -----
653
- - The `cell_masks` parameter allows excluding areas covered by cell masks from
654
- the fitting process.
655
- - The `edge_exclusion` parameter allows excluding edges of the specified size
656
- from the fitting process to avoid boundary effects.
657
-
658
- See Also
659
- --------
660
- paraboloid : The paraboloid function used for fitting.
661
- """
662
-
663
- data = np.empty(image.shape)
664
- x = np.arange(0, image.shape[1])
665
- y = np.arange(0, image.shape[0])
666
- xx, yy = np.meshgrid(x, y)
667
-
668
- params = Parameters()
669
- params.add('a', value=1.0E-05)
670
- params.add('b', value=1.0E-05)
671
- params.add('c', value=1.0E-06)
672
- params.add('d', value=0.01)
673
- params.add('e', value=0.01)
674
- params.add('g', value=100)
675
-
676
- model = Model(paraboloid, independent_vars=['x', 'y'])
677
-
678
- weights = np.ones_like(xx, dtype=float)
679
- if cell_masks is not None:
680
- weights[np.where(cell_masks > 0)] = 0.
681
-
682
- if edge_exclusion is not None:
683
- xx = unpad(xx, edge_exclusion)
684
- yy = unpad(yy, edge_exclusion)
685
- weights = unpad(weights, edge_exclusion)
686
- image = unpad(image, edge_exclusion)
687
-
688
- result = model.fit(image,
689
- x=xx,
690
- y=yy,
691
- weights=weights,
692
- params=params, max_nfev=3000)
693
-
694
- del model
695
- collect()
696
-
697
- xx, yy = np.meshgrid(x, y)
698
-
699
- return paraboloid(xx, yy, **result.params)
748
+ """
749
+ Fit a plane to the given image data.
750
+
751
+ This function fits a plane to the provided image data using least squares
752
+ regression. It constructs a mesh grid based on the dimensions of the image
753
+ and fits a plane model to the data points. If cell masks are provided,
754
+ areas covered by cell masks will be excluded from the fitting process.
755
+
756
+ Parameters
757
+ ----------
758
+ image : numpy.ndarray
759
+ The input image data.
760
+ cell_masks : numpy.ndarray, optional
761
+ An array specifying cell masks. If provided, areas covered by cell masks
762
+ will be excluded from the fitting process (default is None).
763
+ edge_exclusion : int, optional
764
+ The size of the edge to exclude from the fitting process (default is None).
765
+
766
+ Returns
767
+ -------
768
+ numpy.ndarray
769
+ The fitted plane.
770
+
771
+ Notes
772
+ -----
773
+ - The `cell_masks` parameter allows excluding areas covered by cell masks from
774
+ the fitting process.
775
+ - The `edge_exclusion` parameter allows excluding edges of the specified size
776
+ from the fitting process to avoid boundary effects.
777
+
778
+ See Also
779
+ --------
780
+ plane : The plane function used for fitting.
781
+ """
782
+
783
+ data = np.empty(image.shape)
784
+ x = np.arange(0, image.shape[1])
785
+ y = np.arange(0, image.shape[0])
786
+ xx, yy = np.meshgrid(x, y)
787
+
788
+ from lmfit import Parameters, Model
789
+
790
+ params = Parameters()
791
+ params.add("a", value=1)
792
+ params.add("b", value=1)
793
+ params.add("c", value=1)
794
+
795
+ model = Model(plane, independent_vars=["x", "y"])
796
+
797
+ weights = np.ones_like(xx, dtype=float)
798
+ if cell_masks is not None:
799
+ weights[np.where(cell_masks > 0)] = 0.0
800
+
801
+ if edge_exclusion is not None:
802
+ xx = unpad(xx, edge_exclusion)
803
+ yy = unpad(yy, edge_exclusion)
804
+ weights = unpad(weights, edge_exclusion)
805
+ image = unpad(image, edge_exclusion)
806
+
807
+ result = model.fit(image, x=xx, y=yy, weights=weights, params=params, max_nfev=3000)
808
+ del model
809
+ collect()
810
+
811
+ xx, yy = np.meshgrid(x, y)
812
+
813
+ return plane(xx, yy, **result.params)
814
+
815
+
816
+ def fit_paraboloid(image, cell_masks=None, edge_exclusion=None, downsample=10):
817
+ """
818
+ Fit a paraboloid to the given image data.
819
+
820
+ This function fits a paraboloid to the provided image data using least squares
821
+ regression. It constructs a mesh grid based on the dimensions of the image
822
+ and fits a paraboloid model to the data points. If cell masks are provided,
823
+ areas covered by cell masks will be excluded from the fitting process.
824
+
825
+ Parameters
826
+ ----------
827
+ image : numpy.ndarray
828
+ The input image data.
829
+ cell_masks : numpy.ndarray, optional
830
+ An array specifying cell masks. If provided, areas covered by cell masks
831
+ will be excluded from the fitting process (default is None).
832
+ edge_exclusion : int, optional
833
+ The size of the edge to exclude from the fitting process (default is None).
834
+ downsample : int, optional
835
+ The downsampling factor to reduce the number of points used for fitting.
836
+ Default is 10.
837
+
838
+ Returns
839
+ -------
840
+ numpy.ndarray
841
+ The fitted paraboloid.
842
+
843
+ Notes
844
+ -----
845
+ - The `cell_masks` parameter allows excluding areas covered by cell masks from
846
+ the fitting process.
847
+ - The `edge_exclusion` parameter allows excluding edges of the specified size
848
+ from the fitting process to avoid boundary effects.
849
+ - Downsampling significantly speeds up the fitting process for large images
850
+ without compromising the accuracy of the low-frequency background estimate.
851
+
852
+ See Also
853
+ --------
854
+ paraboloid : The paraboloid function used for fitting.
855
+ """
856
+
857
+ data = np.empty(image.shape)
858
+ x = np.arange(0, image.shape[1])
859
+ y = np.arange(0, image.shape[0])
860
+ xx, yy = np.meshgrid(x, y)
861
+
862
+ from lmfit import Parameters, Model
863
+
864
+ params = Parameters()
865
+ params.add("a", value=1.0e-05)
866
+ params.add("b", value=1.0e-05)
867
+ params.add("c", value=1.0e-06)
868
+ params.add("d", value=0.01)
869
+ params.add("e", value=0.01)
870
+ params.add("g", value=100)
871
+
872
+ model = Model(paraboloid, independent_vars=["x", "y"])
873
+
874
+ weights = np.ones_like(xx, dtype=float)
875
+ if cell_masks is not None:
876
+ weights[np.where(cell_masks > 0)] = 0.0
877
+
878
+ if edge_exclusion is not None:
879
+ xx = unpad(xx, edge_exclusion)
880
+ yy = unpad(yy, edge_exclusion)
881
+ weights = unpad(weights, edge_exclusion)
882
+ image = unpad(image, edge_exclusion)
883
+
884
+ # Downsample for faster fitting
885
+ if downsample > 1:
886
+ image_fit = image[::downsample, ::downsample]
887
+ xx_fit = xx[::downsample, ::downsample]
888
+ yy_fit = yy[::downsample, ::downsample]
889
+ weights_fit = weights[::downsample, ::downsample]
890
+ else:
891
+ image_fit = image
892
+ xx_fit = xx
893
+ yy_fit = yy
894
+ weights_fit = weights
895
+
896
+ result = model.fit(
897
+ image_fit, x=xx_fit, y=yy_fit, weights=weights_fit, params=params, max_nfev=3000
898
+ )
899
+
900
+ del model
901
+ collect()
902
+
903
+ xx, yy = np.meshgrid(x, y)
904
+
905
+ return paraboloid(xx, yy, **result.params)
700
906
 
701
907
 
702
908
  def correct_background_model(
703
- experiment,
704
- well_option='*',
705
- position_option='*',
706
- target_channel="channel_name",
707
- threshold_on_std = 1,
708
- model = 'paraboloid',
709
- operation = 'divide',
710
- clip = False,
711
- show_progress_per_well = True,
712
- show_progress_per_pos = False,
713
- export = False,
714
- return_stacks = False,
715
- movie_prefix=None,
716
- activation_protocol=[['gauss',2],['std',4]],
717
- export_prefix='Corrected',
718
- return_stack = True,
719
- **kwargs,
720
- ):
721
-
722
- """
723
- Correct background in image stacks using a specified model.
724
-
725
- This function corrects the background in image stacks obtained from an experiment
726
- using a specified background correction model. It supports various options for
727
- specifying wells, positions, target channel, and background correction parameters.
728
-
729
- Parameters
730
- ----------
731
- experiment : str
732
- The path to the experiment directory.
733
- well_option : str, optional
734
- The option to select specific wells (default is '*').
735
- position_option : str, optional
736
- The option to select specific positions (default is '*').
737
- target_channel : str, optional
738
- The name of the target channel for background correction (default is "channel_name").
739
- threshold_on_std : float, optional
740
- The threshold value on the standard deviation for masking (default is 1).
741
- model : str, optional
742
- The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
743
- operation : str, optional
744
- The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
745
- clip : bool, optional
746
- Whether to clip the corrected image to ensure non-negative values (default is False).
747
- show_progress_per_well : bool, optional
748
- Whether to show progress for each well (default is True).
749
- show_progress_per_pos : bool, optional
750
- Whether to show progress for each position (default is False).
751
- export : bool, optional
752
- Whether to export the corrected stacks (default is False).
753
- return_stacks : bool, optional
754
- Whether to return the corrected stacks (default is False).
755
- movie_prefix : str, optional
756
- The prefix for the movie files (default is None).
757
- activation_protocol : list of list, optional
758
- The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
759
- export_prefix : str, optional
760
- The prefix for exported corrected stacks (default is 'Corrected').
761
- **kwargs : dict
762
- Additional keyword arguments to be passed to the underlying correction function.
763
-
764
- Returns
765
- -------
766
- list of numpy.ndarray
767
- A list of corrected image stacks if `return_stacks` is True, otherwise None.
768
-
769
- Notes
770
- -----
771
- - This function assumes that the experiment directory structure and the configuration
772
- files follow a specific format expected by the helper functions used within.
773
- - Supported background correction models are 'paraboloid' and 'plane'.
774
- - Supported background correction operations are 'divide' and 'subtract'.
775
-
776
- See Also
777
- --------
778
- fit_and_apply_model_background_to_stack : Function to fit and apply background correction to an image stack.
779
- """
780
-
781
- config = get_config(experiment)
782
- wells = get_experiment_wells(experiment)
783
- len_movie = float(config_section_to_dict(config, "MovieSettings")["len_movie"])
784
- if movie_prefix is None:
785
- movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
786
-
787
- well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
788
- channel_indices = _extract_channel_indices_from_config(config, [target_channel])
789
- nbr_channels = _extract_nbr_channels_from_config(config)
790
- img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
791
-
792
- stacks = []
793
-
794
- for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
795
-
796
- well_name, _ = extract_well_name_and_number(well_path)
797
- positions = get_positions_in_well(well_path)
798
- selection = positions[position_indices]
799
- if isinstance(selection[0],np.ndarray):
800
- selection = selection[0]
801
-
802
- for pidx,pos_path in enumerate(tqdm(selection, disable=not show_progress_per_pos)):
803
-
804
- stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
805
- if stack_path is None:
806
- print(f"No stack could be found in {pos_path}... Skip...")
807
- continue
808
-
809
- print(f'Applying the correction to position {extract_position_name(pos_path)}...')
810
- len_movie_auto = auto_load_number_of_frames(stack_path)
811
- if len_movie_auto is not None:
812
- len_movie = len_movie_auto
813
- img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
814
-
815
- corrected_stack = fit_and_apply_model_background_to_stack(stack_path,
816
- target_channel_index=channel_indices[0],
817
- model = model,
818
- nbr_channels=nbr_channels,
819
- stack_length=len_movie,
820
- threshold_on_std=threshold_on_std,
821
- operation=operation,
822
- clip=clip,
823
- export=export,
824
- prefix=export_prefix,
825
- activation_protocol=activation_protocol,
826
- return_stacks = return_stacks,
827
- )
828
- print('Correction successful.')
829
- if return_stacks:
830
- stacks.append(corrected_stack)
831
- else:
832
- del corrected_stack
833
- collect()
834
-
835
- if return_stacks:
836
- return stacks
837
-
838
- def fit_and_apply_model_background_to_stack(stack_path,
839
- target_channel_index=0,
840
- nbr_channels=1,
841
- stack_length=45,
842
- threshold_on_std=1,
843
- operation='divide',
844
- model='paraboloid',
845
- clip=False,
846
- export=False,
847
- activation_protocol=[['gauss',2],['std',4]],
848
- prefix="Corrected",
849
- return_stacks=True,
850
- ):
851
-
852
- """
853
- Fit and apply a background correction model to an image stack.
854
-
855
- This function fits a background correction model to each frame of the image stack
856
- and applies the correction accordingly. It supports various options for specifying
857
- the target channel, number of channels, stack length, threshold on standard deviation,
858
- correction operation, correction model, clipping, and export.
859
-
860
- Parameters
861
- ----------
862
- stack_path : str
863
- The path to the image stack.
864
- target_channel_index : int, optional
865
- The index of the target channel for background correction (default is 0).
866
- nbr_channels : int, optional
867
- The number of channels in the image stack (default is 1).
868
- stack_length : int, optional
869
- The length of the stack (default is 45).
870
- threshold_on_std : float, optional
871
- The threshold value on the standard deviation for masking (default is 1).
872
- operation : str, optional
873
- The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
874
- model : str, optional
875
- The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
876
- clip : bool, optional
877
- Whether to clip the corrected image to ensure non-negative values (default is False).
878
- export : bool, optional
879
- Whether to export the corrected image stack (default is False).
880
- activation_protocol : list of list, optional
881
- The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
882
- prefix : str, optional
883
- The prefix for exported corrected stacks (default is 'Corrected').
884
-
885
- Returns
886
- -------
887
- numpy.ndarray
888
- The corrected image stack.
889
-
890
- Notes
891
- -----
892
- - The function loads frames from the image stack, applies background correction to each frame,
893
- and stores the corrected frames in a new stack.
894
- - Supported background correction models are 'paraboloid' and 'plane'.
895
- - Supported background correction operations are 'divide' and 'subtract'.
896
-
897
- See Also
898
- --------
899
- field_correction : Function to apply background correction to an image.
900
- """
901
-
902
- stack_length_auto = auto_load_number_of_frames(stack_path)
903
- if stack_length_auto is None and stack_length is None:
904
- print('Stack length not provided...')
905
- return None
906
- if stack_length_auto is not None:
907
- stack_length = stack_length_auto
908
-
909
- corrected_stack = []
910
-
911
- if export:
912
- path,file = os.path.split(stack_path)
913
- if prefix is None:
914
- newfile = 'temp_'+file
915
- else:
916
- newfile = '_'.join([prefix,file])
917
-
918
- with tiff.TiffWriter(os.sep.join([path,newfile]), imagej=True, bigtiff=True) as tif:
919
-
920
- for i in tqdm(range(0,int(stack_length*nbr_channels),nbr_channels)):
921
-
922
- frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
923
- target_img = frames[:,:,target_channel_index].copy()
924
-
925
- correction = field_correction(target_img, threshold=threshold_on_std, operation=operation, model=model, clip=clip, activation_protocol=activation_protocol)
926
- frames[:,:,target_channel_index] = correction.copy()
927
-
928
- if return_stacks:
929
- corrected_stack.append(frames)
930
-
931
- if export:
932
- tif.write(np.moveaxis(frames,-1,0).astype(np.dtype('f')), contiguous=True)
933
- del frames
934
- del target_img
935
- del correction
936
- collect()
937
-
938
- if prefix is None:
939
- os.replace(os.sep.join([path,newfile]), os.sep.join([path,file]))
940
- else:
941
- for i in tqdm(range(0,int(stack_length*nbr_channels),nbr_channels)):
942
-
943
- frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
944
- target_img = frames[:,:,target_channel_index].copy()
945
-
946
- correction = field_correction(target_img, threshold=threshold_on_std, operation=operation, model=model, clip=clip, activation_protocol=activation_protocol)
947
- frames[:,:,target_channel_index] = correction.copy()
948
-
949
- corrected_stack.append(frames)
950
-
951
- del frames
952
- del target_img
953
- del correction
954
- collect()
955
-
956
- if return_stacks:
957
- return np.array(corrected_stack)
958
- else:
959
- return None
960
-
961
- def field_correction(img: np.ndarray, threshold: float = 1, operation: str = 'divide', model: str = 'paraboloid', clip: bool = False, return_bg: bool = False, activation_protocol: List[List] = [['gauss',2],['std',4]]):
962
-
963
- """
964
- Apply field correction to an image.
965
-
966
- This function applies field correction to the given image based on the specified parameters
967
- including the threshold on standard deviation, operation, background correction model, clipping,
968
- and activation protocol.
969
-
970
- Parameters
971
- ----------
972
- img : numpy.ndarray
973
- The input image to be corrected.
974
- threshold : float, optional
975
- The threshold value on the image, post activation protocol for masking out cells (default is 1).
976
- operation : str, optional
977
- The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
978
- model : str, optional
979
- The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
980
- clip : bool, optional
981
- Whether to clip the corrected image to ensure non-negative values (default is False).
982
- return_bg : bool, optional
983
- Whether to return the background along with the corrected image (default is False).
984
- activation_protocol : list of list, optional
985
- The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
986
-
987
- Returns
988
- -------
989
- numpy.ndarray or tuple
990
- The corrected image or a tuple containing the corrected image and the background, depending on the value of `return_bg`.
991
-
992
- Notes
993
- -----
994
- - This function first estimates the unreliable edge based on the activation protocol.
995
- - It then applies thresholding to obtain a mask for the background.
996
- - Next, it fits a background model to the image using the specified model.
997
- - Depending on the operation specified, it either divides or subtracts the background from the image.
998
- - If `clip` is True and operation is 'subtract', negative values in the corrected image are clipped to 0.
999
- - If `return_bg` is True, the function returns a tuple containing the corrected image and the background.
1000
-
1001
- See Also
1002
- --------
1003
- fit_background_model : Function to fit a background model to an image.
1004
- threshold_image : Function to apply thresholding to an image.
1005
- """
1006
-
1007
- target_copy = img.copy().astype(float)
1008
- if np.percentile(target_copy.flatten(),99.9)==0.0:
1009
- return target_copy
1010
-
1011
- std_frame = filter_image(target_copy,filters=activation_protocol)
1012
- edge = estimate_unreliable_edge(activation_protocol)
1013
- mask = threshold_image(std_frame, threshold, np.inf, foreground_value=1, edge_exclusion=edge).astype(int)
1014
- background = fit_background_model(img, cell_masks=mask, model=model, edge_exclusion=edge)
1015
-
1016
- if operation=="divide":
1017
- correction = np.divide(img, background, where=background==background)
1018
- correction[background!=background] = np.nan
1019
- correction[img!=img] = np.nan
1020
- fill_val = 1.0
1021
-
1022
- elif operation=="subtract":
1023
- correction = np.subtract(img, background, where=background==background)
1024
- correction[background!=background] = np.nan
1025
- correction[img!=img] = np.nan
1026
- fill_val = 0.0
1027
- if clip:
1028
- correction[correction<=0.] = 0.
1029
-
1030
- if return_bg:
1031
- return correction.copy(), background
1032
- else:
1033
- return correction.copy()
1034
-
1035
- def fit_background_model(img, cell_masks=None, model='paraboloid', edge_exclusion=None):
1036
-
1037
- """
1038
- Fit a background model to the given image.
1039
-
1040
- This function fits a background model to the given image using either a paraboloid or plane model.
1041
- It supports optional cell masks and edge exclusion for fitting.
1042
-
1043
- Parameters
1044
- ----------
1045
- img : numpy.ndarray
1046
- The input image data.
1047
- cell_masks : numpy.ndarray, optional
1048
- An array specifying cell masks. If provided, areas covered by cell masks will be excluded from the fitting process.
1049
- model : str, optional
1050
- The background model to fit, either 'paraboloid' or 'plane' (default is 'paraboloid').
1051
- edge_exclusion : int or None, optional
1052
- The size of the border to exclude from fitting (default is None).
1053
-
1054
- Returns
1055
- -------
1056
- numpy.ndarray or None
1057
- The fitted background model as a numpy array if successful, otherwise None.
1058
-
1059
- Notes
1060
- -----
1061
- - This function fits a background model to the image using either a paraboloid or plane model based on the specified `model`.
1062
- - If `cell_masks` are provided, areas covered by cell masks will be excluded from the fitting process.
1063
- - If `edge_exclusion` is provided, a border of the specified size will be excluded from fitting.
1064
-
1065
- See Also
1066
- --------
1067
- fit_paraboloid : Function to fit a paraboloid model to an image.
1068
- fit_plane : Function to fit a plane model to an image.
1069
- """
1070
-
1071
- if model == "paraboloid":
1072
- bg = fit_paraboloid(img.astype(float), cell_masks=cell_masks, edge_exclusion=edge_exclusion).astype(float)
1073
- elif model == "plane":
1074
- bg = fit_plane(img.astype(float), cell_masks=cell_masks, edge_exclusion=edge_exclusion).astype(float)
1075
-
1076
- if bg is not None:
1077
- bg = np.array(bg)
1078
-
1079
- return bg
909
+ experiment,
910
+ well_option="*",
911
+ position_option="*",
912
+ target_channel="channel_name",
913
+ threshold_on_std=1,
914
+ model="paraboloid",
915
+ operation="divide",
916
+ clip=False,
917
+ show_progress_per_well=True,
918
+ show_progress_per_pos=False,
919
+ export=False,
920
+ return_stacks=False,
921
+ movie_prefix=None,
922
+ activation_protocol=[["gauss", 2], ["std", 4]],
923
+ export_prefix="Corrected",
924
+ return_stack=True,
925
+ progress_callback=None,
926
+ downsample=10,
927
+ **kwargs,
928
+ ):
929
+ """
930
+ Correct background in image stacks using a specified model.
931
+
932
+ This function corrects the background in image stacks obtained from an experiment
933
+ using a specified background correction model. It supports various options for
934
+ specifying wells, positions, target channel, and background correction parameters.
935
+
936
+ Parameters
937
+ ----------
938
+ experiment : str
939
+ The path to the experiment directory.
940
+ well_option : str, optional
941
+ The option to select specific wells (default is '*').
942
+ position_option : str, optional
943
+ The option to select specific positions (default is '*').
944
+ target_channel : str, optional
945
+ The name of the target channel for background correction (default is "channel_name").
946
+ threshold_on_std : float, optional
947
+ The threshold value on the standard deviation for masking (default is 1).
948
+ model : str, optional
949
+ The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
950
+ operation : str, optional
951
+ The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
952
+ clip : bool, optional
953
+ Whether to clip the corrected image to ensure non-negative values (default is False).
954
+ show_progress_per_well : bool, optional
955
+ Whether to show progress for each well (default is True).
956
+ show_progress_per_pos : bool, optional
957
+ Whether to show progress for each position (default is False).
958
+ export : bool, optional
959
+ Whether to export the corrected stacks (default is False).
960
+ return_stacks : bool, optional
961
+ Whether to return the corrected stacks (default is False).
962
+ movie_prefix : str, optional
963
+ The prefix for the movie files (default is None).
964
+ activation_protocol : list of list, optional
965
+ The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
966
+ export_prefix : str, optional
967
+ The prefix for exported corrected stacks (default is 'Corrected').
968
+ **kwargs : dict
969
+ Additional keyword arguments to be passed to the underlying correction function.
970
+
971
+ Returns
972
+ -------
973
+ list of numpy.ndarray
974
+ A list of corrected image stacks if `return_stacks` is True, otherwise None.
975
+
976
+ Notes
977
+ -----
978
+ - This function assumes that the experiment directory structure and the configuration
979
+ files follow a specific format expected by the helper functions used within.
980
+ - Supported background correction models are 'paraboloid' and 'plane'.
981
+ - Supported background correction operations are 'divide' and 'subtract'.
982
+
983
+ See Also
984
+ --------
985
+ fit_and_apply_model_background_to_stack : Function to fit and apply background correction to an image stack.
986
+ """
987
+
988
+ config = get_config(experiment)
989
+ wells = get_experiment_wells(experiment)
990
+ len_movie = float(config_section_to_dict(config, "MovieSettings")["len_movie"])
991
+ if movie_prefix is None:
992
+ movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
993
+
994
+ well_indices, position_indices = interpret_wells_and_positions(
995
+ experiment, well_option, position_option
996
+ )
997
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
998
+ nbr_channels = _extract_nbr_channels_from_config(config)
999
+ img_num_channels = _get_img_num_per_channel(
1000
+ channel_indices, int(len_movie), nbr_channels
1001
+ )
1002
+
1003
+ stacks = []
1004
+
1005
+ total_wells = len(wells[well_indices])
1006
+ for k, well_path in enumerate(
1007
+ tqdm(wells[well_indices], disable=not show_progress_per_well)
1008
+ ):
1009
+ if progress_callback:
1010
+ progress_callback(level="well", iter=k, total=total_wells)
1011
+
1012
+ well_name, _ = extract_well_name_and_number(well_path)
1013
+ positions = get_positions_in_well(well_path)
1014
+ selection = positions[position_indices]
1015
+ if isinstance(selection[0], np.ndarray):
1016
+ selection = selection[0]
1017
+
1018
+ total_pos_in_well = len(selection)
1019
+
1020
+ for pidx, pos_path in enumerate(
1021
+ tqdm(selection, disable=not show_progress_per_pos)
1022
+ ):
1023
+
1024
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
1025
+ if stack_path is None:
1026
+ logger.warning(f"No stack could be found in {pos_path}... Skip...")
1027
+ continue
1028
+
1029
+ logger.info(
1030
+ f"Applying the correction to position {extract_position_name(pos_path)}..."
1031
+ )
1032
+ len_movie_auto = auto_load_number_of_frames(stack_path)
1033
+ if len_movie_auto is not None:
1034
+ len_movie = len_movie_auto
1035
+ img_num_channels = _get_img_num_per_channel(
1036
+ channel_indices, int(len_movie), nbr_channels
1037
+ )
1038
+
1039
+ corrected_stack = fit_and_apply_model_background_to_stack(
1040
+ stack_path,
1041
+ target_channel_index=channel_indices[0],
1042
+ model=model,
1043
+ nbr_channels=nbr_channels,
1044
+ stack_length=len_movie,
1045
+ threshold_on_std=threshold_on_std,
1046
+ operation=operation,
1047
+ clip=clip,
1048
+ export=export,
1049
+ prefix=export_prefix,
1050
+ activation_protocol=activation_protocol,
1051
+ return_stacks=return_stacks,
1052
+ progress_callback=progress_callback,
1053
+ downsample=downsample,
1054
+ subset_indices=kwargs.get("subset_indices", None),
1055
+ )
1056
+ logger.info("Correction successful.")
1057
+ if return_stacks:
1058
+ stacks.append(corrected_stack)
1059
+ else:
1060
+ del corrected_stack
1061
+ collect()
1062
+
1063
+ if progress_callback:
1064
+ progress_callback(
1065
+ level="position",
1066
+ iter=pidx,
1067
+ total=total_pos_in_well,
1068
+ stage="correcting",
1069
+ )
1070
+
1071
+ if progress_callback:
1072
+ progress_callback(level="well", iter=k + 1, total=total_wells)
1073
+
1074
+ if return_stacks:
1075
+ return stacks
1076
+
1077
+
1078
+ def fit_and_apply_model_background_to_stack(
1079
+ stack_path,
1080
+ target_channel_index=0,
1081
+ nbr_channels=1,
1082
+ stack_length=45,
1083
+ threshold_on_std=1,
1084
+ operation="divide",
1085
+ model="paraboloid",
1086
+ clip=False,
1087
+ export=False,
1088
+ activation_protocol=[["gauss", 2], ["std", 4]],
1089
+ prefix="Corrected",
1090
+ return_stacks=True,
1091
+ progress_callback=None,
1092
+ downsample=10,
1093
+ subset_indices=None,
1094
+ ):
1095
+ """
1096
+ Fit and apply a background correction model to an image stack.
1097
+
1098
+ This function fits a background correction model to each frame of the image stack
1099
+ and applies the correction accordingly. It supports various options for specifying
1100
+ the target channel, number of channels, stack length, threshold on standard deviation,
1101
+ correction operation, correction model, clipping, and export.
1102
+
1103
+ Parameters
1104
+ ----------
1105
+ stack_path : str
1106
+ The path to the image stack.
1107
+ target_channel_index : int, optional
1108
+ The index of the target channel for background correction (default is 0).
1109
+ nbr_channels : int, optional
1110
+ The number of channels in the image stack (default is 1).
1111
+ subset_indices : list of int, optional
1112
+ List of absolute frame indices to process (default is None).
1113
+ stack_length : int, optional
1114
+ The length of the stack (default is 45).
1115
+ threshold_on_std : float, optional
1116
+ The threshold value on the standard deviation for masking (default is 1).
1117
+ operation : str, optional
1118
+ The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
1119
+ model : str, optional
1120
+ The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
1121
+ clip : bool, optional
1122
+ Whether to clip the corrected image to ensure non-negative values (default is False).
1123
+ export : bool, optional
1124
+ Whether to export the corrected image stack (default is False).
1125
+ activation_protocol : list of list, optional
1126
+ The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
1127
+ prefix : str, optional
1128
+ The prefix for exported corrected stacks (default is 'Corrected').
1129
+ subset_indices : list of int, optional
1130
+ List of absolute frame indices to process (default is None).
1131
+
1132
+ Returns
1133
+ -------
1134
+ numpy.ndarray
1135
+ The corrected image stack.
1136
+
1137
+ Notes
1138
+ -----
1139
+ - The function loads frames from the image stack, applies background correction to each frame,
1140
+ and stores the corrected frames in a new stack.
1141
+ - Supported background correction models are 'paraboloid' and 'plane'.
1142
+ - Supported background correction operations are 'divide' and 'subtract'.
1143
+
1144
+ See Also
1145
+ --------
1146
+ field_correction : Function to apply background correction to an image.
1147
+ """
1148
+
1149
+ from tqdm import tqdm
1150
+
1151
+ stack_length_auto = auto_load_number_of_frames(stack_path)
1152
+ if stack_length_auto is None and stack_length is None:
1153
+ logger.error("Stack length not provided...")
1154
+ return None
1155
+ if stack_length_auto is not None:
1156
+ stack_length = stack_length_auto
1157
+
1158
+ corrected_stack = []
1159
+
1160
+ if export:
1161
+ path, file = os.path.split(stack_path)
1162
+ if prefix is None:
1163
+ newfile = "temp_" + file
1164
+ else:
1165
+ newfile = "_".join([prefix, file])
1166
+
1167
+ import tifffile.tifffile as tiff
1168
+
1169
+ with tiff.TiffWriter(
1170
+ os.sep.join([path, newfile]), imagej=True, bigtiff=True
1171
+ ) as tif:
1172
+
1173
+ for i in tqdm(range(0, int(stack_length * nbr_channels), nbr_channels)):
1174
+
1175
+ frames = load_frames(
1176
+ list(np.arange(i, (i + nbr_channels))),
1177
+ stack_path,
1178
+ normalize_input=False,
1179
+ ).astype(float)
1180
+ target_img = frames[:, :, target_channel_index].copy()
1181
+
1182
+ correction = field_correction(
1183
+ target_img,
1184
+ threshold=threshold_on_std,
1185
+ operation=operation,
1186
+ model=model,
1187
+ clip=clip,
1188
+ activation_protocol=activation_protocol,
1189
+ downsample=downsample,
1190
+ )
1191
+ frames[:, :, target_channel_index] = correction.copy()
1192
+
1193
+ if return_stacks:
1194
+ corrected_stack.append(frames)
1195
+
1196
+ if export:
1197
+ tif.write(
1198
+ np.moveaxis(frames, -1, 0).astype(np.dtype("f")),
1199
+ contiguous=True,
1200
+ )
1201
+ del frames
1202
+ del target_img
1203
+ del correction
1204
+ collect()
1205
+
1206
+ if progress_callback:
1207
+ progress_callback(
1208
+ level="frame",
1209
+ iter=int(i // nbr_channels),
1210
+ total=stack_length,
1211
+ stage="correcting",
1212
+ )
1213
+
1214
+ if prefix is None:
1215
+ os.replace(os.sep.join([path, newfile]), os.sep.join([path, file]))
1216
+ else:
1217
+
1218
+ if subset_indices is None:
1219
+ iterator = range(0, int(stack_length * nbr_channels), nbr_channels)
1220
+ else:
1221
+ iterator = subset_indices
1222
+
1223
+ for i in tqdm(iterator):
1224
+
1225
+ frames = load_frames(
1226
+ list(np.arange(i, (i + nbr_channels))),
1227
+ stack_path,
1228
+ normalize_input=False,
1229
+ ).astype(float)
1230
+ target_img = frames[:, :, target_channel_index].copy()
1231
+
1232
+ correction = field_correction(
1233
+ target_img,
1234
+ threshold=threshold_on_std,
1235
+ operation=operation,
1236
+ model=model,
1237
+ clip=clip,
1238
+ activation_protocol=activation_protocol,
1239
+ downsample=downsample,
1240
+ )
1241
+ frames[:, :, target_channel_index] = correction.copy()
1242
+
1243
+ corrected_stack.append(frames)
1244
+
1245
+ del frames
1246
+ del target_img
1247
+ del correction
1248
+ collect()
1249
+
1250
+ if progress_callback:
1251
+ progress_callback(
1252
+ level="frame",
1253
+ iter=int(i // nbr_channels),
1254
+ total=stack_length,
1255
+ stage="correcting",
1256
+ )
1257
+
1258
+ if return_stacks:
1259
+ return np.array(corrected_stack)
1260
+ else:
1261
+ return None
1262
+
1263
+
1264
+ def field_correction(
1265
+ img: np.ndarray,
1266
+ threshold: float = 1,
1267
+ operation: str = "divide",
1268
+ model: str = "paraboloid",
1269
+ clip: bool = False,
1270
+ return_bg: bool = False,
1271
+ activation_protocol: List[List] = [["gauss", 2], ["std", 4]],
1272
+ downsample: int = 10,
1273
+ ):
1274
+ """
1275
+ Apply field correction to an image.
1276
+
1277
+ This function applies field correction to the given image based on the specified parameters
1278
+ including the threshold on standard deviation, operation, background correction model, clipping,
1279
+ and activation protocol.
1280
+
1281
+ Parameters
1282
+ ----------
1283
+ img : numpy.ndarray
1284
+ The input image to be corrected.
1285
+ threshold : float, optional
1286
+ The threshold value on the image, post activation protocol for masking out cells (default is 1).
1287
+ operation : str, optional
1288
+ The operation to apply for background correction, either 'divide' or 'subtract' (default is 'divide').
1289
+ model : str, optional
1290
+ The background correction model to use, either 'paraboloid' or 'plane' (default is 'paraboloid').
1291
+ clip : bool, optional
1292
+ Whether to clip the corrected image to ensure non-negative values (default is False).
1293
+ return_bg : bool, optional
1294
+ Whether to return the background along with the corrected image (default is False).
1295
+ activation_protocol : list of list, optional
1296
+ The activation protocol consisting of filters and their respective parameters (default is [['gauss',2],['std',4]]).
1297
+
1298
+ Returns
1299
+ -------
1300
+ numpy.ndarray or tuple
1301
+ The corrected image or a tuple containing the corrected image and the background, depending on the value of `return_bg`.
1302
+
1303
+ Notes
1304
+ -----
1305
+ - This function first estimates the unreliable edge based on the activation protocol.
1306
+ - It then applies thresholding to obtain a mask for the background.
1307
+ - Next, it fits a background model to the image using the specified model.
1308
+ - Depending on the operation specified, it either divides or subtracts the background from the image.
1309
+ - If `clip` is True and operation is 'subtract', negative values in the corrected image are clipped to 0.
1310
+ - If `return_bg` is True, the function returns a tuple containing the corrected image and the background.
1311
+
1312
+ See Also
1313
+ --------
1314
+ fit_background_model : Function to fit a background model to an image.
1315
+ threshold_image : Function to apply thresholding to an image.
1316
+ """
1317
+
1318
+ target_copy = img.copy().astype(float)
1319
+ if np.percentile(target_copy.flatten(), 99.9) == 0.0:
1320
+ return target_copy
1321
+
1322
+ from celldetective.filters import filter_image
1323
+
1324
+ std_frame = filter_image(target_copy, filters=activation_protocol)
1325
+ edge = estimate_unreliable_edge(activation_protocol)
1326
+ mask = threshold_image(
1327
+ std_frame, threshold, np.inf, foreground_value=1, edge_exclusion=edge
1328
+ ).astype(int)
1329
+ background = fit_background_model(
1330
+ img, cell_masks=mask, model=model, edge_exclusion=edge, downsample=downsample
1331
+ )
1332
+
1333
+ if operation == "divide":
1334
+ correction = np.divide(img, background, where=background == background)
1335
+ correction[background != background] = np.nan
1336
+ correction[img != img] = np.nan
1337
+ fill_val = 1.0
1338
+
1339
+ elif operation == "subtract":
1340
+ correction = np.subtract(img, background, where=background == background)
1341
+ correction[background != background] = np.nan
1342
+ correction[img != img] = np.nan
1343
+ fill_val = 0.0
1344
+ if clip:
1345
+ correction[correction <= 0.0] = 0.0
1346
+
1347
+ if return_bg:
1348
+ return correction.copy(), background
1349
+ else:
1350
+ return correction.copy()
1351
+
1352
+ return correction.copy()
1353
+
1354
+
1355
+ def fit_background_model(
1356
+ img, cell_masks=None, model="paraboloid", edge_exclusion=None, downsample=10
1357
+ ):
1358
+ """
1359
+ Fit a background model to the given image.
1360
+
1361
+ This function fits a background model to the given image using either a paraboloid or plane model.
1362
+ It supports optional cell masks and edge exclusion for fitting.
1363
+
1364
+ Parameters
1365
+ ----------
1366
+ img : numpy.ndarray
1367
+ The input image data.
1368
+ cell_masks : numpy.ndarray, optional
1369
+ An array specifying cell masks. If provided, areas covered by cell masks will be excluded from the fitting process.
1370
+ model : str, optional
1371
+ The background model to fit, either 'paraboloid' or 'plane' (default is 'paraboloid').
1372
+ edge_exclusion : int or None, optional
1373
+ The size of the border to exclude from fitting (default is None).
1374
+
1375
+ Returns
1376
+ -------
1377
+ numpy.ndarray or None
1378
+ The fitted background model as a numpy array if successful, otherwise None.
1379
+
1380
+ Notes
1381
+ -----
1382
+ - This function fits a background model to the image using either a paraboloid or plane model based on the specified `model`.
1383
+ - If `cell_masks` are provided, areas covered by cell masks will be excluded from the fitting process.
1384
+ - If `edge_exclusion` is provided, a border of the specified size will be excluded from fitting.
1385
+
1386
+ See Also
1387
+ --------
1388
+ fit_paraboloid : Function to fit a paraboloid model to an image.
1389
+ fit_plane : Function to fit a plane model to an image.
1390
+ """
1391
+
1392
+ if model == "paraboloid":
1393
+ bg = fit_paraboloid(
1394
+ img.astype(float),
1395
+ cell_masks=cell_masks,
1396
+ edge_exclusion=edge_exclusion,
1397
+ downsample=downsample,
1398
+ ).astype(float)
1399
+ elif model == "plane":
1400
+ bg = fit_plane(
1401
+ img.astype(float), cell_masks=cell_masks, edge_exclusion=edge_exclusion
1402
+ ).astype(float)
1403
+
1404
+ if bg is not None:
1405
+ bg = np.array(bg)
1406
+
1407
+ return bg
1080
1408
 
1081
1409
 
1082
1410
  def correct_channel_offset(
1083
- experiment,
1084
- well_option='*',
1085
- position_option='*',
1086
- target_channel="channel_name",
1087
- correction_horizontal = 0,
1088
- correction_vertical = 0,
1089
- show_progress_per_well = True,
1090
- show_progress_per_pos = False,
1091
- export = False,
1092
- return_stacks = False,
1093
- movie_prefix=None,
1094
- export_prefix='Corrected',
1095
- return_stack = True,
1096
- **kwargs,
1097
- ):
1098
-
1099
-
1100
- config = get_config(experiment)
1101
- wells = get_experiment_wells(experiment)
1102
- len_movie = float(config_section_to_dict(config, "MovieSettings")["len_movie"])
1103
- if movie_prefix is None:
1104
- movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
1105
-
1106
- well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
1107
- channel_indices = _extract_channel_indices_from_config(config, [target_channel])
1108
- nbr_channels = _extract_nbr_channels_from_config(config)
1109
- img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
1110
-
1111
- stacks = []
1112
-
1113
- for k, well_path in enumerate(tqdm(wells[well_indices], disable=not show_progress_per_well)):
1114
-
1115
- well_name, _ = extract_well_name_and_number(well_path)
1116
- positions = get_positions_in_well(well_path)
1117
- selection = positions[position_indices]
1118
- if isinstance(selection[0],np.ndarray):
1119
- selection = selection[0]
1120
-
1121
- for pidx,pos_path in enumerate(tqdm(selection, disable=not show_progress_per_pos)):
1122
-
1123
- stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
1124
- print(f'Applying the correction to position {extract_position_name(pos_path)}...')
1125
- len_movie_auto = auto_load_number_of_frames(stack_path)
1126
- if len_movie_auto is not None:
1127
- len_movie = len_movie_auto
1128
- img_num_channels = _get_img_num_per_channel(channel_indices, int(len_movie), nbr_channels)
1129
-
1130
- corrected_stack = correct_channel_offset_single_stack(stack_path,
1131
- target_channel_index=channel_indices[0],
1132
- nbr_channels=nbr_channels,
1133
- stack_length=len_movie,
1134
- correction_vertical=correction_vertical,
1135
- correction_horizontal=correction_horizontal,
1136
- export=export,
1137
- prefix=export_prefix,
1138
- return_stacks = return_stacks,
1139
- )
1140
- print('Correction successful.')
1141
- if return_stacks:
1142
- stacks.append(corrected_stack)
1143
- else:
1144
- del corrected_stack
1145
- collect()
1146
-
1147
- if return_stacks:
1148
- return stacks
1149
-
1150
-
1151
- def correct_channel_offset_single_stack(stack_path,
1152
- target_channel_index=0,
1153
- nbr_channels=1,
1154
- stack_length=45,
1155
- correction_vertical=0,
1156
- correction_horizontal=0,
1157
- export=False,
1158
- prefix="Corrected",
1159
- return_stacks=True,
1160
- ):
1161
-
1162
- assert os.path.exists(stack_path),f"The stack {stack_path} does not exist... Abort."
1163
-
1164
- stack_length_auto = auto_load_number_of_frames(stack_path)
1165
- if stack_length_auto is None and stack_length is None:
1166
- print('Stack length not provided...')
1167
- return None
1168
- if stack_length_auto is not None:
1169
- stack_length = stack_length_auto
1170
-
1171
- corrected_stack = []
1172
-
1173
- if export:
1174
- path,file = os.path.split(stack_path)
1175
- if prefix is None:
1176
- newfile = 'temp_'+file
1177
- else:
1178
- newfile = '_'.join([prefix,file])
1179
-
1180
- with tiff.TiffWriter(os.sep.join([path,newfile]),bigtiff=True,imagej=True) as tif:
1181
-
1182
- for i in tqdm(range(0,int(stack_length*nbr_channels),nbr_channels)):
1183
-
1184
- frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
1185
- target_img = frames[:,:,target_channel_index].copy()
1186
-
1187
- if np.percentile(target_img.flatten(), 99.9)==0.0:
1188
- correction = target_img
1189
- elif np.any(target_img.flatten()!=target_img.flatten()):
1190
- # Routine to interpolate NaN for the spline filter then mask it again
1191
- target_interp = interpolate_nan(target_img)
1192
- correction = shift(target_interp, [correction_vertical, correction_horizontal])
1193
- correction_nan = shift(target_img, [correction_vertical, correction_horizontal], prefilter=False)
1194
- nan_i, nan_j = np.where(correction_nan!=correction_nan)
1195
- correction[nan_i, nan_j] = np.nan
1196
- else:
1197
- correction = shift(target_img, [correction_vertical, correction_horizontal])
1198
-
1199
- frames[:,:,target_channel_index] = correction.copy()
1200
-
1201
- if return_stacks:
1202
- corrected_stack.append(frames)
1203
-
1204
- if export:
1205
- tif.write(np.moveaxis(frames,-1,0).astype(np.dtype('f')), contiguous=True)
1206
- del frames
1207
- del target_img
1208
- del correction
1209
- collect()
1210
-
1211
- if prefix is None:
1212
- os.replace(os.sep.join([path,newfile]), os.sep.join([path,file]))
1213
- else:
1214
- for i in tqdm(range(0,int(stack_length*nbr_channels),nbr_channels)):
1215
-
1216
- frames = load_frames(list(np.arange(i,(i+nbr_channels))), stack_path, normalize_input=False).astype(float)
1217
- target_img = frames[:,:,target_channel_index].copy()
1218
-
1219
- if np.percentile(target_img.flatten(), 99.9)==0.0:
1220
- correction = target_img
1221
- elif np.any(target_img.flatten()!=target_img.flatten()):
1222
- # Routine to interpolate NaN for the spline filter then mask it again
1223
- target_interp = interpolate_nan(target_img)
1224
- correction = shift(target_interp, [correction_vertical, correction_horizontal])
1225
- correction_nan = shift(target_img, [correction_vertical, correction_horizontal], prefilter=False)
1226
- nan_i, nan_j = np.where(correction_nan!=correction_nan)
1227
- correction[nan_i, nan_j] = np.nan
1228
- else:
1229
- correction = shift(target_img, [correction_vertical, correction_horizontal])
1230
-
1231
- frames[:,:,target_channel_index] = correction.copy()
1232
-
1233
- corrected_stack.append(frames)
1234
-
1235
- del frames
1236
- del target_img
1237
- del correction
1238
- collect()
1239
-
1240
- if return_stacks:
1241
- return np.array(corrected_stack)
1242
- else:
1243
- return None
1411
+ experiment,
1412
+ well_option="*",
1413
+ position_option="*",
1414
+ target_channel="channel_name",
1415
+ correction_horizontal=0,
1416
+ correction_vertical=0,
1417
+ show_progress_per_well=True,
1418
+ show_progress_per_pos=True,
1419
+ export=False,
1420
+ return_stacks=False,
1421
+ movie_prefix=None,
1422
+ export_prefix="Corrected",
1423
+ progress_callback=None,
1424
+ **kwargs,
1425
+ ):
1426
+
1427
+ config = get_config(experiment)
1428
+ wells = get_experiment_wells(experiment)
1429
+ len_movie = float(config_section_to_dict(config, "MovieSettings")["len_movie"])
1430
+ if movie_prefix is None:
1431
+ movie_prefix = config_section_to_dict(config, "MovieSettings")["movie_prefix"]
1432
+
1433
+ well_indices, position_indices = interpret_wells_and_positions(
1434
+ experiment, well_option, position_option
1435
+ )
1436
+ channel_indices = _extract_channel_indices_from_config(config, [target_channel])
1437
+ nbr_channels = _extract_nbr_channels_from_config(config)
1438
+ img_num_channels = _get_img_num_per_channel(
1439
+ channel_indices, int(len_movie), nbr_channels
1440
+ )
1441
+
1442
+ stacks = []
1443
+
1444
+ # Well loop with progress reporting
1445
+ total_wells = len(well_indices)
1446
+ for k, well_path in enumerate(wells[well_indices]):
1447
+ if progress_callback:
1448
+ progress_callback(level="well", iter=k, total=total_wells)
1449
+ elif show_progress_per_well:
1450
+ print(f"Processing well {k+1}/{total_wells}...")
1451
+
1452
+ well_name, _ = extract_well_name_and_number(well_path)
1453
+ positions = get_positions_in_well(well_path)
1454
+ selection = positions[position_indices]
1455
+ if isinstance(selection[0], np.ndarray):
1456
+ selection = selection[0]
1457
+
1458
+ total_pos = len(selection)
1459
+ for pidx, pos_path in enumerate(selection):
1460
+ if progress_callback:
1461
+ progress_callback(
1462
+ level="position",
1463
+ iter=pidx,
1464
+ total=total_pos,
1465
+ stage=f"Pos {extract_position_name(pos_path)}",
1466
+ )
1467
+ elif show_progress_per_pos:
1468
+ print(f" Processing position {pidx+1}/{total_pos}...")
1469
+
1470
+ stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
1471
+ logger.info(
1472
+ f"Applying the correction to position {extract_position_name(pos_path)}..."
1473
+ )
1474
+ len_movie_auto = auto_load_number_of_frames(stack_path)
1475
+ if len_movie_auto is not None:
1476
+ len_movie = len_movie_auto
1477
+ img_num_channels = _get_img_num_per_channel(
1478
+ channel_indices, int(len_movie), nbr_channels
1479
+ )
1480
+
1481
+ corrected_stack = correct_channel_offset_single_stack(
1482
+ stack_path,
1483
+ target_channel_index=channel_indices[0],
1484
+ nbr_channels=nbr_channels,
1485
+ stack_length=len_movie,
1486
+ correction_vertical=correction_vertical,
1487
+ correction_horizontal=correction_horizontal,
1488
+ export=export,
1489
+ prefix=export_prefix,
1490
+ return_stacks=return_stacks,
1491
+ progress_callback=progress_callback,
1492
+ )
1493
+
1494
+ logger.info("Correction successful.")
1495
+ if return_stacks:
1496
+ stacks.append(corrected_stack)
1497
+ else:
1498
+ del corrected_stack
1499
+ collect()
1500
+
1501
+ if return_stacks:
1502
+ return stacks
1503
+
1504
+
1505
+ def correct_channel_offset_single_stack(
1506
+ stack_path,
1507
+ target_channel_index=0,
1508
+ nbr_channels=1,
1509
+ stack_length=45,
1510
+ correction_vertical=0,
1511
+ correction_horizontal=0,
1512
+ export=False,
1513
+ prefix="Corrected",
1514
+ return_stacks=True,
1515
+ progress_callback=None,
1516
+ ):
1517
+
1518
+ assert os.path.exists(
1519
+ stack_path
1520
+ ), f"The stack {stack_path} does not exist... Abort."
1521
+
1522
+ from tqdm import tqdm
1523
+ import tifffile.tifffile as tiff
1524
+ from scipy.ndimage import shift
1525
+
1526
+ stack_length_auto = auto_load_number_of_frames(stack_path)
1527
+ if stack_length_auto is None and stack_length is None:
1528
+ logger.error("Stack length not provided...")
1529
+ return None
1530
+ if stack_length_auto is not None:
1531
+ stack_length = stack_length_auto
1532
+
1533
+ corrected_stack = []
1534
+
1535
+ if export:
1536
+ path, file = os.path.split(stack_path)
1537
+ if prefix is None:
1538
+ newfile = "temp_" + file
1539
+ else:
1540
+ newfile = "_".join([prefix, file])
1541
+
1542
+ with tiff.TiffWriter(
1543
+ os.sep.join([path, newfile]), bigtiff=True, imagej=True
1544
+ ) as tif:
1545
+ frames_indices = range(0, int(stack_length * nbr_channels), nbr_channels)
1546
+ total_frames = len(frames_indices)
1547
+ for k, i in enumerate(tqdm(frames_indices)):
1548
+ if progress_callback:
1549
+ progress_callback(level="frame", iter=k, total=total_frames)
1550
+
1551
+ frames = load_frames(
1552
+ list(np.arange(i, (i + nbr_channels))),
1553
+ stack_path,
1554
+ normalize_input=False,
1555
+ ).astype(float)
1556
+ target_img = frames[:, :, target_channel_index].copy()
1557
+
1558
+ if np.percentile(target_img.flatten(), 99.9) == 0.0:
1559
+ correction = target_img
1560
+ elif np.any(target_img.flatten() != target_img.flatten()):
1561
+ # Routine to interpolate NaN for the spline filter then mask it again
1562
+ target_interp = interpolate_nan(target_img)
1563
+ from scipy.ndimage import shift
1564
+
1565
+ correction = shift(
1566
+ target_interp, [correction_vertical, correction_horizontal]
1567
+ )
1568
+ correction_nan = shift(
1569
+ target_img,
1570
+ [correction_vertical, correction_horizontal],
1571
+ prefilter=False,
1572
+ )
1573
+ nan_i, nan_j = np.where(correction_nan != correction_nan)
1574
+ correction[nan_i, nan_j] = np.nan
1575
+ else:
1576
+ correction = shift(
1577
+ target_img, [correction_vertical, correction_horizontal]
1578
+ )
1579
+
1580
+ frames[:, :, target_channel_index] = correction.copy()
1581
+
1582
+ if return_stacks:
1583
+ corrected_stack.append(frames)
1584
+
1585
+ if export:
1586
+ tif.write(
1587
+ np.moveaxis(frames, -1, 0).astype(np.dtype("f")),
1588
+ contiguous=True,
1589
+ )
1590
+ del frames
1591
+ del target_img
1592
+ del correction
1593
+ collect()
1594
+
1595
+ if prefix is None:
1596
+ os.replace(os.sep.join([path, newfile]), os.sep.join([path, file]))
1597
+ else:
1598
+ frames_indices = range(0, int(stack_length * nbr_channels), nbr_channels)
1599
+ total_frames = len(frames_indices)
1600
+ for k, i in enumerate(tqdm(frames_indices)):
1601
+ if progress_callback:
1602
+ progress_callback(level="frame", iter=k, total=total_frames)
1603
+
1604
+ frames = load_frames(
1605
+ list(np.arange(i, (i + nbr_channels))),
1606
+ stack_path,
1607
+ normalize_input=False,
1608
+ ).astype(float)
1609
+ target_img = frames[:, :, target_channel_index].copy()
1610
+
1611
+ if np.percentile(target_img.flatten(), 99.9) == 0.0:
1612
+ correction = target_img
1613
+ elif np.any(target_img.flatten() != target_img.flatten()):
1614
+ # Routine to interpolate NaN for the spline filter then mask it again
1615
+ target_interp = interpolate_nan(target_img)
1616
+ correction = shift(
1617
+ target_interp, [correction_vertical, correction_horizontal]
1618
+ )
1619
+ correction_nan = shift(
1620
+ target_img,
1621
+ [correction_vertical, correction_horizontal],
1622
+ prefilter=False,
1623
+ )
1624
+ nan_i, nan_j = np.where(correction_nan != correction_nan)
1625
+ correction[nan_i, nan_j] = np.nan
1626
+ else:
1627
+ correction = shift(
1628
+ target_img, [correction_vertical, correction_horizontal]
1629
+ )
1630
+
1631
+ frames[:, :, target_channel_index] = correction.copy()
1632
+
1633
+ corrected_stack.append(frames)
1634
+
1635
+ del frames
1636
+ del target_img
1637
+ del correction
1638
+ collect()
1639
+
1640
+ if return_stacks:
1641
+ return np.array(corrected_stack)
1642
+ else:
1643
+ return None