celldetective 1.4.2__py3-none-any.whl → 1.5.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- celldetective/__init__.py +25 -0
- celldetective/__main__.py +62 -43
- celldetective/_version.py +1 -1
- celldetective/extra_properties.py +477 -399
- celldetective/filters.py +192 -97
- celldetective/gui/InitWindow.py +541 -411
- celldetective/gui/__init__.py +0 -15
- celldetective/gui/about.py +44 -39
- celldetective/gui/analyze_block.py +120 -84
- celldetective/gui/base/__init__.py +0 -0
- celldetective/gui/base/channel_norm_generator.py +335 -0
- celldetective/gui/base/components.py +249 -0
- celldetective/gui/base/feature_choice.py +92 -0
- celldetective/gui/base/figure_canvas.py +52 -0
- celldetective/gui/base/list_widget.py +133 -0
- celldetective/gui/{styles.py → base/styles.py} +92 -36
- celldetective/gui/base/utils.py +33 -0
- celldetective/gui/base_annotator.py +900 -767
- celldetective/gui/classifier_widget.py +6 -22
- celldetective/gui/configure_new_exp.py +777 -671
- celldetective/gui/control_panel.py +635 -524
- celldetective/gui/dynamic_progress.py +449 -0
- celldetective/gui/event_annotator.py +2023 -1662
- celldetective/gui/generic_signal_plot.py +1292 -944
- celldetective/gui/gui_utils.py +899 -1289
- celldetective/gui/interactions_block.py +658 -0
- celldetective/gui/interactive_timeseries_viewer.py +447 -0
- celldetective/gui/json_readers.py +48 -15
- celldetective/gui/layouts/__init__.py +5 -0
- celldetective/gui/layouts/background_model_free_layout.py +537 -0
- celldetective/gui/layouts/channel_offset_layout.py +134 -0
- celldetective/gui/layouts/local_correction_layout.py +91 -0
- celldetective/gui/layouts/model_fit_layout.py +372 -0
- celldetective/gui/layouts/operation_layout.py +68 -0
- celldetective/gui/layouts/protocol_designer_layout.py +96 -0
- celldetective/gui/pair_event_annotator.py +3130 -2435
- celldetective/gui/plot_measurements.py +586 -267
- celldetective/gui/plot_signals_ui.py +724 -506
- celldetective/gui/preprocessing_block.py +395 -0
- celldetective/gui/process_block.py +1678 -1831
- celldetective/gui/seg_model_loader.py +580 -473
- celldetective/gui/settings/__init__.py +0 -7
- celldetective/gui/settings/_cellpose_model_params.py +181 -0
- celldetective/gui/settings/_event_detection_model_params.py +95 -0
- celldetective/gui/settings/_segmentation_model_params.py +159 -0
- celldetective/gui/settings/_settings_base.py +77 -65
- celldetective/gui/settings/_settings_event_model_training.py +752 -526
- celldetective/gui/settings/_settings_measurements.py +1133 -964
- celldetective/gui/settings/_settings_neighborhood.py +574 -488
- celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
- celldetective/gui/settings/_settings_signal_annotator.py +329 -305
- celldetective/gui/settings/_settings_tracking.py +1304 -1094
- celldetective/gui/settings/_stardist_model_params.py +98 -0
- celldetective/gui/survival_ui.py +422 -312
- celldetective/gui/tableUI.py +1665 -1701
- celldetective/gui/table_ops/_maths.py +295 -0
- celldetective/gui/table_ops/_merge_groups.py +140 -0
- celldetective/gui/table_ops/_merge_one_hot.py +95 -0
- celldetective/gui/table_ops/_query_table.py +43 -0
- celldetective/gui/table_ops/_rename_col.py +44 -0
- celldetective/gui/thresholds_gui.py +382 -179
- celldetective/gui/viewers/__init__.py +0 -0
- celldetective/gui/viewers/base_viewer.py +700 -0
- celldetective/gui/viewers/channel_offset_viewer.py +331 -0
- celldetective/gui/viewers/contour_viewer.py +394 -0
- celldetective/gui/viewers/size_viewer.py +153 -0
- celldetective/gui/viewers/spot_detection_viewer.py +341 -0
- celldetective/gui/viewers/threshold_viewer.py +309 -0
- celldetective/gui/workers.py +304 -126
- celldetective/log_manager.py +92 -0
- celldetective/measure.py +1895 -1478
- celldetective/napari/__init__.py +0 -0
- celldetective/napari/utils.py +1025 -0
- celldetective/neighborhood.py +1914 -1448
- celldetective/preprocessing.py +1620 -1220
- celldetective/processes/__init__.py +0 -0
- celldetective/processes/background_correction.py +271 -0
- celldetective/processes/compute_neighborhood.py +894 -0
- celldetective/processes/detect_events.py +246 -0
- celldetective/processes/measure_cells.py +565 -0
- celldetective/processes/segment_cells.py +760 -0
- celldetective/processes/track_cells.py +435 -0
- celldetective/processes/train_segmentation_model.py +694 -0
- celldetective/processes/train_signal_model.py +265 -0
- celldetective/processes/unified_process.py +292 -0
- celldetective/regionprops/_regionprops.py +358 -317
- celldetective/relative_measurements.py +987 -710
- celldetective/scripts/measure_cells.py +313 -212
- celldetective/scripts/measure_relative.py +90 -46
- celldetective/scripts/segment_cells.py +165 -104
- celldetective/scripts/segment_cells_thresholds.py +96 -68
- celldetective/scripts/track_cells.py +198 -149
- celldetective/scripts/train_segmentation_model.py +324 -201
- celldetective/scripts/train_signal_model.py +87 -45
- celldetective/segmentation.py +844 -749
- celldetective/signals.py +3514 -2861
- celldetective/tracking.py +30 -15
- celldetective/utils/__init__.py +0 -0
- celldetective/utils/cellpose_utils/__init__.py +133 -0
- celldetective/utils/color_mappings.py +42 -0
- celldetective/utils/data_cleaning.py +630 -0
- celldetective/utils/data_loaders.py +450 -0
- celldetective/utils/dataset_helpers.py +207 -0
- celldetective/utils/downloaders.py +197 -0
- celldetective/utils/event_detection/__init__.py +8 -0
- celldetective/utils/experiment.py +1782 -0
- celldetective/utils/image_augmenters.py +308 -0
- celldetective/utils/image_cleaning.py +74 -0
- celldetective/utils/image_loaders.py +926 -0
- celldetective/utils/image_transforms.py +335 -0
- celldetective/utils/io.py +62 -0
- celldetective/utils/mask_cleaning.py +348 -0
- celldetective/utils/mask_transforms.py +5 -0
- celldetective/utils/masks.py +184 -0
- celldetective/utils/maths.py +351 -0
- celldetective/utils/model_getters.py +325 -0
- celldetective/utils/model_loaders.py +296 -0
- celldetective/utils/normalization.py +380 -0
- celldetective/utils/parsing.py +465 -0
- celldetective/utils/plots/__init__.py +0 -0
- celldetective/utils/plots/regression.py +53 -0
- celldetective/utils/resources.py +34 -0
- celldetective/utils/stardist_utils/__init__.py +104 -0
- celldetective/utils/stats.py +90 -0
- celldetective/utils/types.py +21 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
- celldetective-1.5.0b0.dist-info/RECORD +187 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
- tests/gui/test_new_project.py +129 -117
- tests/gui/test_project.py +127 -79
- tests/test_filters.py +39 -15
- tests/test_notebooks.py +8 -0
- tests/test_tracking.py +232 -13
- tests/test_utils.py +123 -77
- celldetective/gui/base_components.py +0 -23
- celldetective/gui/layouts.py +0 -1602
- celldetective/gui/processes/compute_neighborhood.py +0 -594
- celldetective/gui/processes/measure_cells.py +0 -360
- celldetective/gui/processes/segment_cells.py +0 -499
- celldetective/gui/processes/track_cells.py +0 -303
- celldetective/gui/processes/train_segmentation_model.py +0 -270
- celldetective/gui/processes/train_signal_model.py +0 -108
- celldetective/gui/table_ops/merge_groups.py +0 -118
- celldetective/gui/viewers.py +0 -1354
- celldetective/io.py +0 -3663
- celldetective/utils.py +0 -3108
- celldetective-1.4.2.dist-info/RECORD +0 -123
- /celldetective/{gui/processes → processes}/downloader.py +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
from multiprocessing import Process
|
|
2
|
+
import time
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path, PurePath
|
|
7
|
+
from glob import glob
|
|
8
|
+
from shutil import rmtree
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
import numpy as np
|
|
11
|
+
import gc
|
|
12
|
+
from art import tprint
|
|
13
|
+
import concurrent.futures
|
|
14
|
+
|
|
15
|
+
from celldetective.log_manager import get_logger
|
|
16
|
+
from celldetective.utils.experiment import (
|
|
17
|
+
extract_position_name,
|
|
18
|
+
extract_experiment_channels,
|
|
19
|
+
)
|
|
20
|
+
from celldetective.utils.image_loaders import (
|
|
21
|
+
auto_load_number_of_frames,
|
|
22
|
+
_get_img_num_per_channel,
|
|
23
|
+
_load_frames_to_segment,
|
|
24
|
+
load_frames,
|
|
25
|
+
)
|
|
26
|
+
from celldetective.utils.image_transforms import _estimate_scale_factor
|
|
27
|
+
from celldetective.utils.mask_cleaning import _check_label_dims
|
|
28
|
+
from celldetective.utils.mask_transforms import _rescale_labels
|
|
29
|
+
from celldetective.utils.model_loaders import locate_segmentation_model
|
|
30
|
+
from celldetective.utils.parsing import (
|
|
31
|
+
config_section_to_dict,
|
|
32
|
+
_extract_nbr_channels_from_config,
|
|
33
|
+
_get_normalize_kwargs_from_config,
|
|
34
|
+
_extract_channel_indices_from_config,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _create_preview_overlay(image, mask):
|
|
41
|
+
# If image has channels (C, Y, X), take max projection or first channel
|
|
42
|
+
if image.ndim == 3:
|
|
43
|
+
image = np.max(image, axis=0)
|
|
44
|
+
|
|
45
|
+
# Robust handling for shape mismatch (e.g. transpose or scale issues)
|
|
46
|
+
if image.shape != mask.shape:
|
|
47
|
+
# Try transpose match
|
|
48
|
+
if image.T.shape == mask.shape:
|
|
49
|
+
image = image.T
|
|
50
|
+
else:
|
|
51
|
+
# Resize image to fit mask (mask is the truth for segmentation result)
|
|
52
|
+
from skimage.transform import resize
|
|
53
|
+
|
|
54
|
+
# normalize to float 0-1 before resize to avoid artifacts
|
|
55
|
+
image = image.astype(float)
|
|
56
|
+
image = resize(image, mask.shape, preserve_range=True)
|
|
57
|
+
|
|
58
|
+
# Normalize image to dim range 0-150 (uint8)
|
|
59
|
+
img = image.copy().astype(float)
|
|
60
|
+
img = np.nan_to_num(img)
|
|
61
|
+
min_v, max_v = np.min(img), np.max(img)
|
|
62
|
+
if max_v > min_v:
|
|
63
|
+
img = (img - min_v) / (max_v - min_v) * 150 # Darker context
|
|
64
|
+
else:
|
|
65
|
+
img = np.zeros_like(img)
|
|
66
|
+
img = img.astype(np.uint8)
|
|
67
|
+
|
|
68
|
+
# Overlay: Set mask region to 255 (Bright White)
|
|
69
|
+
img[mask > 0] = 255
|
|
70
|
+
|
|
71
|
+
return img # Returns 2D uint8, handled robustly by workers.py
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BaseSegmentProcess(Process):
|
|
75
|
+
|
|
76
|
+
def __init__(self, queue=None, process_args=None, *args, **kwargs):
|
|
77
|
+
|
|
78
|
+
super().__init__(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
self.queue = queue
|
|
81
|
+
|
|
82
|
+
if process_args is not None:
|
|
83
|
+
for key, value in process_args.items():
|
|
84
|
+
setattr(self, key, value)
|
|
85
|
+
|
|
86
|
+
# Handle batch of positions or single pos
|
|
87
|
+
if hasattr(self, "batch_structure"):
|
|
88
|
+
# Flatten positions from structure for compatibility
|
|
89
|
+
self.positions = []
|
|
90
|
+
for w_idx, data in self.batch_structure.items():
|
|
91
|
+
self.positions.extend(data["positions"])
|
|
92
|
+
elif not hasattr(self, "positions"):
|
|
93
|
+
if hasattr(self, "pos"):
|
|
94
|
+
self.positions = [self.pos]
|
|
95
|
+
else:
|
|
96
|
+
self.positions = []
|
|
97
|
+
logger.error("No positions provided to segmentation process.")
|
|
98
|
+
|
|
99
|
+
# Experiment
|
|
100
|
+
self.locate_experiment_config()
|
|
101
|
+
|
|
102
|
+
logger.info(f"Configuration file: {self.config}")
|
|
103
|
+
logger.info(f"Population: {self.mode}...")
|
|
104
|
+
self.instruction_file = os.sep.join(
|
|
105
|
+
["configs", f"segmentation_instructions_{self.mode}.json"]
|
|
106
|
+
)
|
|
107
|
+
self.read_instructions()
|
|
108
|
+
self.extract_experiment_parameters()
|
|
109
|
+
|
|
110
|
+
def setup_for_position(self, pos_path):
|
|
111
|
+
self.pos = pos_path
|
|
112
|
+
logger.info(f"Position: {extract_position_name(self.pos)}...")
|
|
113
|
+
logger.info(f"Population: {self.mode}...")
|
|
114
|
+
|
|
115
|
+
self.detect_movie_length()
|
|
116
|
+
self.write_folders()
|
|
117
|
+
|
|
118
|
+
def read_instructions(self):
|
|
119
|
+
logger.info("Looking for instruction file...")
|
|
120
|
+
instr_path = PurePath(self.exp_dir, Path(f"{self.instruction_file}"))
|
|
121
|
+
if os.path.exists(instr_path):
|
|
122
|
+
with open(instr_path, "r") as f:
|
|
123
|
+
_instructions = json.load(f)
|
|
124
|
+
logger.info(f"Measurement instruction file successfully loaded...")
|
|
125
|
+
logger.info(f"Instructions: {_instructions}...")
|
|
126
|
+
self.flip = _instructions.get("flip", False)
|
|
127
|
+
else:
|
|
128
|
+
self.flip = False
|
|
129
|
+
|
|
130
|
+
def write_folders(self):
|
|
131
|
+
|
|
132
|
+
self.mode = self.mode.lower()
|
|
133
|
+
self.label_folder = f"labels_{self.mode}"
|
|
134
|
+
|
|
135
|
+
if os.path.exists(self.pos + self.label_folder):
|
|
136
|
+
logger.info("Erasing the previous labels folder...")
|
|
137
|
+
rmtree(self.pos + self.label_folder)
|
|
138
|
+
os.mkdir(self.pos + self.label_folder)
|
|
139
|
+
logger.info(f"Labels folder successfully generated...")
|
|
140
|
+
|
|
141
|
+
def extract_experiment_parameters(self):
|
|
142
|
+
|
|
143
|
+
self.spatial_calibration = float(
|
|
144
|
+
config_section_to_dict(self.config, "MovieSettings")["pxtoum"]
|
|
145
|
+
)
|
|
146
|
+
self.len_movie = float(
|
|
147
|
+
config_section_to_dict(self.config, "MovieSettings")["len_movie"]
|
|
148
|
+
)
|
|
149
|
+
self.movie_prefix = config_section_to_dict(self.config, "MovieSettings")[
|
|
150
|
+
"movie_prefix"
|
|
151
|
+
]
|
|
152
|
+
self.nbr_channels = _extract_nbr_channels_from_config(self.config)
|
|
153
|
+
self.channel_names, self.channel_indices = extract_experiment_channels(
|
|
154
|
+
self.exp_dir
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def locate_experiment_config(self):
|
|
158
|
+
|
|
159
|
+
if hasattr(self, "pos"):
|
|
160
|
+
p = self.pos
|
|
161
|
+
elif hasattr(self, "positions") and len(self.positions) > 0:
|
|
162
|
+
p = self.positions[0]
|
|
163
|
+
else:
|
|
164
|
+
logger.error("No position available to locate experiment config.")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
parent1 = Path(p).parent
|
|
168
|
+
self.exp_dir = parent1.parent
|
|
169
|
+
self.config = PurePath(self.exp_dir, Path("config.ini"))
|
|
170
|
+
|
|
171
|
+
if not os.path.exists(self.config):
|
|
172
|
+
logger.error(
|
|
173
|
+
"The configuration file for the experiment could not be located. Abort."
|
|
174
|
+
)
|
|
175
|
+
self.abort_process()
|
|
176
|
+
|
|
177
|
+
def detect_movie_length(self):
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
self.file = glob(self.pos + f"movie/{self.movie_prefix}*.tif")[0]
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(f"Error {e}.\nMovie could not be found. Check the prefix.")
|
|
183
|
+
self.abort_process()
|
|
184
|
+
|
|
185
|
+
len_movie_auto = auto_load_number_of_frames(self.file)
|
|
186
|
+
if len_movie_auto is not None:
|
|
187
|
+
self.len_movie = len_movie_auto
|
|
188
|
+
|
|
189
|
+
def end_process(self):
|
|
190
|
+
|
|
191
|
+
self.terminate()
|
|
192
|
+
self.queue.put("finished")
|
|
193
|
+
|
|
194
|
+
def abort_process(self):
|
|
195
|
+
|
|
196
|
+
self.terminate()
|
|
197
|
+
self.queue.put("error")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class SegmentCellDLProcess(BaseSegmentProcess):
|
|
201
|
+
|
|
202
|
+
def __init__(self, *args, **kwargs):
|
|
203
|
+
|
|
204
|
+
super().__init__(*args, **kwargs)
|
|
205
|
+
|
|
206
|
+
self.check_gpu()
|
|
207
|
+
|
|
208
|
+
# Model
|
|
209
|
+
self.locate_model_path()
|
|
210
|
+
self.extract_model_input_parameters()
|
|
211
|
+
self.detect_rescaling()
|
|
212
|
+
|
|
213
|
+
self.sum_done = 0
|
|
214
|
+
self.t0 = time.time()
|
|
215
|
+
|
|
216
|
+
def setup_for_position(self, pos_path):
|
|
217
|
+
super().setup_for_position(pos_path)
|
|
218
|
+
self.detect_channels()
|
|
219
|
+
self.write_log()
|
|
220
|
+
|
|
221
|
+
def extract_model_input_parameters(self):
|
|
222
|
+
|
|
223
|
+
self.required_channels = self.input_config["channels"]
|
|
224
|
+
if "selected_channels" in self.input_config:
|
|
225
|
+
self.required_channels = self.input_config["selected_channels"]
|
|
226
|
+
|
|
227
|
+
self.target_cell_size = None
|
|
228
|
+
if (
|
|
229
|
+
"target_cell_size_um" in self.input_config
|
|
230
|
+
and "cell_size_um" in self.input_config
|
|
231
|
+
):
|
|
232
|
+
self.target_cell_size = self.input_config["target_cell_size_um"]
|
|
233
|
+
self.cell_size = self.input_config["cell_size_um"]
|
|
234
|
+
|
|
235
|
+
self.normalize_kwargs = _get_normalize_kwargs_from_config(self.input_config)
|
|
236
|
+
|
|
237
|
+
self.model_type = self.input_config["model_type"]
|
|
238
|
+
self.required_spatial_calibration = self.input_config["spatial_calibration"]
|
|
239
|
+
logger.info(
|
|
240
|
+
f"Spatial calibration expected by the model: {self.required_spatial_calibration}..."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if self.model_type == "cellpose":
|
|
244
|
+
self.diameter = self.input_config["diameter"]
|
|
245
|
+
self.cellprob_threshold = self.input_config["cellprob_threshold"]
|
|
246
|
+
self.flow_threshold = self.input_config["flow_threshold"]
|
|
247
|
+
|
|
248
|
+
def write_log(self):
|
|
249
|
+
|
|
250
|
+
log = f"segmentation model: {self.model_name}\n"
|
|
251
|
+
with open(self.pos + f"log_{self.mode}.txt", "a") as f:
|
|
252
|
+
f.write(f"{datetime.datetime.now()} SEGMENT \n")
|
|
253
|
+
f.write(log)
|
|
254
|
+
|
|
255
|
+
def detect_channels(self):
|
|
256
|
+
|
|
257
|
+
self.channel_indices = _extract_channel_indices_from_config(
|
|
258
|
+
self.config, self.required_channels
|
|
259
|
+
)
|
|
260
|
+
logger.info(
|
|
261
|
+
f"Required channels: {self.required_channels} located at channel indices {self.channel_indices}."
|
|
262
|
+
)
|
|
263
|
+
self.img_num_channels = _get_img_num_per_channel(
|
|
264
|
+
self.channel_indices, int(self.len_movie), self.nbr_channels
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def detect_rescaling(self):
|
|
268
|
+
|
|
269
|
+
self.scale = _estimate_scale_factor(
|
|
270
|
+
self.spatial_calibration, self.required_spatial_calibration
|
|
271
|
+
)
|
|
272
|
+
logger.info(f"Scale: {self.scale} [None = 1]...")
|
|
273
|
+
|
|
274
|
+
if self.target_cell_size is not None and self.scale is not None:
|
|
275
|
+
self.scale *= self.cell_size / self.target_cell_size
|
|
276
|
+
elif self.target_cell_size is not None:
|
|
277
|
+
if self.target_cell_size != self.cell_size:
|
|
278
|
+
self.scale = self.cell_size / self.target_cell_size
|
|
279
|
+
|
|
280
|
+
logger.info(
|
|
281
|
+
f"Scale accounting for expected cell size: {self.scale} [None = 1]..."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def locate_model_path(self):
|
|
285
|
+
|
|
286
|
+
self.model_complete_path = locate_segmentation_model(self.model_name)
|
|
287
|
+
if self.model_complete_path is None:
|
|
288
|
+
logger.error("Model could not be found. Abort.")
|
|
289
|
+
self.abort_process()
|
|
290
|
+
else:
|
|
291
|
+
logger.info(f"Model path: {self.model_complete_path}...")
|
|
292
|
+
|
|
293
|
+
if not os.path.exists(self.model_complete_path + "config_input.json"):
|
|
294
|
+
logger.error(
|
|
295
|
+
"The configuration for the inputs to the model could not be located. Abort."
|
|
296
|
+
)
|
|
297
|
+
self.abort_process()
|
|
298
|
+
|
|
299
|
+
with open(self.model_complete_path + "config_input.json") as config_file:
|
|
300
|
+
self.input_config = json.load(config_file)
|
|
301
|
+
|
|
302
|
+
def check_gpu(self):
|
|
303
|
+
|
|
304
|
+
if not self.use_gpu:
|
|
305
|
+
os.environ["CUDA_VISIBLE_DEVICES"] = "-1"
|
|
306
|
+
|
|
307
|
+
def process_position(self, model=None, scale_model=None):
|
|
308
|
+
|
|
309
|
+
tprint("Segment")
|
|
310
|
+
|
|
311
|
+
list_indices = range(self.len_movie)
|
|
312
|
+
if self.flip:
|
|
313
|
+
list_indices = reversed(list_indices)
|
|
314
|
+
|
|
315
|
+
# Reset counter for this position
|
|
316
|
+
self.loop_count = 0
|
|
317
|
+
self.t0_frame = time.time()
|
|
318
|
+
|
|
319
|
+
for t in tqdm(list_indices, desc="frame"):
|
|
320
|
+
|
|
321
|
+
f = _load_frames_to_segment(
|
|
322
|
+
self.file,
|
|
323
|
+
self.img_num_channels[:, t],
|
|
324
|
+
scale_model=scale_model,
|
|
325
|
+
normalize_kwargs=self.normalize_kwargs,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if self.model_type == "stardist":
|
|
329
|
+
from celldetective.utils.stardist_utils import (
|
|
330
|
+
_segment_image_with_stardist_model,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
Y_pred = _segment_image_with_stardist_model(
|
|
334
|
+
f, model=model, return_details=False
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
elif self.model_type == "cellpose":
|
|
338
|
+
from celldetective.utils.cellpose_utils import (
|
|
339
|
+
_segment_image_with_cellpose_model,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
Y_pred = _segment_image_with_cellpose_model(
|
|
343
|
+
f,
|
|
344
|
+
model=model,
|
|
345
|
+
diameter=self.diameter,
|
|
346
|
+
cellprob_threshold=self.cellprob_threshold,
|
|
347
|
+
flow_threshold=self.flow_threshold,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if self.scale is not None:
|
|
351
|
+
Y_pred = _rescale_labels(Y_pred, scale_model=scale_model)
|
|
352
|
+
|
|
353
|
+
Y_pred = _check_label_dims(Y_pred, file=self.file)
|
|
354
|
+
|
|
355
|
+
from celldetective.utils.io import save_tiff_imagej_compatible
|
|
356
|
+
|
|
357
|
+
save_tiff_imagej_compatible(
|
|
358
|
+
self.pos + os.sep.join([self.label_folder, f"{str(t).zfill(4)}.tif"]),
|
|
359
|
+
Y_pred,
|
|
360
|
+
axes="YX",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# del f
|
|
364
|
+
# del Y_pred
|
|
365
|
+
# gc.collect()
|
|
366
|
+
|
|
367
|
+
# Send signal for progress bar
|
|
368
|
+
# Triple progress bar logic
|
|
369
|
+
|
|
370
|
+
if self.loop_count == 0:
|
|
371
|
+
self.t0_frame = time.time()
|
|
372
|
+
|
|
373
|
+
frame_progress = ((self.loop_count + 1) / self.len_movie) * 100
|
|
374
|
+
if frame_progress > 100:
|
|
375
|
+
frame_progress = 100
|
|
376
|
+
|
|
377
|
+
data = {}
|
|
378
|
+
data["frame_progress"] = frame_progress
|
|
379
|
+
|
|
380
|
+
# Frame time estimation (skip first)
|
|
381
|
+
elapsed = time.time() - getattr(self, "t0_frame", time.time())
|
|
382
|
+
measured_count = self.loop_count
|
|
383
|
+
|
|
384
|
+
if measured_count > 0:
|
|
385
|
+
avg = elapsed / measured_count
|
|
386
|
+
rem = self.len_movie - (self.loop_count + 1)
|
|
387
|
+
rem_t = rem * avg
|
|
388
|
+
mins = int(rem_t // 60)
|
|
389
|
+
secs = int(rem_t % 60)
|
|
390
|
+
data["frame_time"] = f"Segmentation: {mins} m {secs} s"
|
|
391
|
+
else:
|
|
392
|
+
data["frame_time"] = (
|
|
393
|
+
f"Segmentation: {self.loop_count + 1}/{int(self.len_movie)} frames"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Saturate preview: Convert labels to binary (0/1) so all cells are visible
|
|
397
|
+
# data["image_preview"] = Y_pred > 0
|
|
398
|
+
# Saturate preview: Convert labels to binary (0/1) so all cells are visible
|
|
399
|
+
data["image_preview"] = (Y_pred > 0).astype(np.uint8)
|
|
400
|
+
self.queue.put(data)
|
|
401
|
+
self.loop_count += 1
|
|
402
|
+
|
|
403
|
+
del f
|
|
404
|
+
del Y_pred
|
|
405
|
+
gc.collect()
|
|
406
|
+
|
|
407
|
+
def run(self):
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
|
|
411
|
+
if self.model_type == "stardist":
|
|
412
|
+
from celldetective.utils.stardist_utils import _prep_stardist_model
|
|
413
|
+
|
|
414
|
+
model, scale_model = _prep_stardist_model(
|
|
415
|
+
self.model_name,
|
|
416
|
+
Path(self.model_complete_path).parent,
|
|
417
|
+
use_gpu=self.use_gpu,
|
|
418
|
+
scale=self.scale,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
elif self.model_type == "cellpose":
|
|
422
|
+
from celldetective.utils.cellpose_utils import _prep_cellpose_model
|
|
423
|
+
|
|
424
|
+
model, scale_model = _prep_cellpose_model(
|
|
425
|
+
self.model_name,
|
|
426
|
+
self.model_complete_path,
|
|
427
|
+
use_gpu=self.use_gpu,
|
|
428
|
+
n_channels=len(self.required_channels),
|
|
429
|
+
scale=self.scale,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Wrapper for single-position compatibility if batch_structure is missing
|
|
433
|
+
if not hasattr(self, "batch_structure"):
|
|
434
|
+
self.batch_structure = {
|
|
435
|
+
0: {"well_name": "Batch", "positions": self.positions}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
self.t0_well = time.time()
|
|
439
|
+
# Loop over Wells
|
|
440
|
+
for w_i, (w_idx, well_data) in enumerate(self.batch_structure.items()):
|
|
441
|
+
positions = well_data["positions"]
|
|
442
|
+
|
|
443
|
+
# Well Time Estimation
|
|
444
|
+
elapsed = time.time() - self.t0_well
|
|
445
|
+
if w_i > 0:
|
|
446
|
+
avg_well = elapsed / w_i
|
|
447
|
+
rem_well = (len(self.batch_structure) - w_i) * avg_well
|
|
448
|
+
mins_w = int(rem_well // 60)
|
|
449
|
+
secs_w = int(rem_well % 60)
|
|
450
|
+
well_str = f"Well {w_i + 1}/{len(self.batch_structure)} - {mins_w} m {secs_w} s left"
|
|
451
|
+
else:
|
|
452
|
+
well_str = (
|
|
453
|
+
f"Processing well {w_i + 1}/{len(self.batch_structure)}..."
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Update Well Progress
|
|
457
|
+
self.queue.put(
|
|
458
|
+
{
|
|
459
|
+
"well_progress": (w_i / len(self.batch_structure)) * 100,
|
|
460
|
+
"well_time": well_str,
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
self.t0_pos = time.time()
|
|
465
|
+
# Loop over positions in this well
|
|
466
|
+
for pos_idx, pos_path in enumerate(positions):
|
|
467
|
+
|
|
468
|
+
# Setup specific variables for this position (folders, length, etc.)
|
|
469
|
+
self.setup_for_position(pos_path)
|
|
470
|
+
|
|
471
|
+
list_indices = range(self.len_movie)
|
|
472
|
+
if self.flip:
|
|
473
|
+
list_indices = reversed(list_indices)
|
|
474
|
+
|
|
475
|
+
# Position Time Estimation relative to current well
|
|
476
|
+
elapsed_pos = time.time() - self.t0_pos
|
|
477
|
+
if pos_idx > 0:
|
|
478
|
+
avg_pos = elapsed_pos / pos_idx
|
|
479
|
+
rem_pos = (len(positions) - pos_idx) * avg_pos
|
|
480
|
+
mins_p = int(rem_pos // 60)
|
|
481
|
+
secs_p = int(rem_pos % 60)
|
|
482
|
+
pos_str = f"Pos {pos_idx + 1}/{len(positions)} - {mins_p} m {secs_p} s left"
|
|
483
|
+
else:
|
|
484
|
+
pos_str = (
|
|
485
|
+
f"Processing position {pos_idx + 1}/{len(positions)}..."
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
self.process_position(model=model, scale_model=scale_model)
|
|
489
|
+
|
|
490
|
+
# End of position loop
|
|
491
|
+
self.queue.put(
|
|
492
|
+
{"pos_progress": ((pos_idx + 1) / len(positions)) * 100}
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# End of Well loop
|
|
496
|
+
self.queue.put(
|
|
497
|
+
{"well_progress": ((w_i + 1) / len(self.batch_structure)) * 100}
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
except Exception as e:
|
|
501
|
+
logger.error(e)
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
del model
|
|
505
|
+
except:
|
|
506
|
+
pass
|
|
507
|
+
|
|
508
|
+
gc.collect()
|
|
509
|
+
logger.info("Segmentation task is done.")
|
|
510
|
+
|
|
511
|
+
# Send end signal
|
|
512
|
+
self.queue.put("finished")
|
|
513
|
+
self.queue.close()
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class SegmentCellThresholdProcess(BaseSegmentProcess):
|
|
517
|
+
|
|
518
|
+
def __init__(self, *args, **kwargs):
|
|
519
|
+
|
|
520
|
+
super().__init__(*args, **kwargs)
|
|
521
|
+
|
|
522
|
+
self.equalize = False
|
|
523
|
+
|
|
524
|
+
self.sum_done = 0
|
|
525
|
+
self.t0 = time.time()
|
|
526
|
+
|
|
527
|
+
def prepare_equalize(self):
|
|
528
|
+
|
|
529
|
+
for i in range(len(self.instructions)):
|
|
530
|
+
|
|
531
|
+
if self.equalize[i]:
|
|
532
|
+
f_reference = load_frames(
|
|
533
|
+
self.img_num_channels[:, self.equalize_time[i]],
|
|
534
|
+
self.file,
|
|
535
|
+
scale=None,
|
|
536
|
+
normalize_input=False,
|
|
537
|
+
)
|
|
538
|
+
f_reference = f_reference[:, :, self.instructions[i]["target_channel"]]
|
|
539
|
+
else:
|
|
540
|
+
f_reference = None
|
|
541
|
+
|
|
542
|
+
self.instructions[i].update({"equalize_reference": f_reference})
|
|
543
|
+
|
|
544
|
+
def load_threshold_config(self):
|
|
545
|
+
|
|
546
|
+
self.instructions = []
|
|
547
|
+
for inst in self.threshold_instructions:
|
|
548
|
+
if os.path.exists(inst):
|
|
549
|
+
with open(inst, "r") as f:
|
|
550
|
+
self.instructions.append(json.load(f))
|
|
551
|
+
else:
|
|
552
|
+
logger.error("The configuration path is not valid. Abort.")
|
|
553
|
+
self.abort_process()
|
|
554
|
+
|
|
555
|
+
def extract_threshold_parameters(self):
|
|
556
|
+
|
|
557
|
+
self.required_channels = []
|
|
558
|
+
self.equalize = []
|
|
559
|
+
self.equalize_time = []
|
|
560
|
+
|
|
561
|
+
for i in range(len(self.instructions)):
|
|
562
|
+
ch = [self.instructions[i]["target_channel"]]
|
|
563
|
+
self.required_channels.append(ch)
|
|
564
|
+
|
|
565
|
+
if "equalize_reference" in self.instructions[i]:
|
|
566
|
+
equalize, equalize_time = self.instructions[i]["equalize_reference"]
|
|
567
|
+
self.equalize.append(equalize)
|
|
568
|
+
self.equalize_time.append(equalize_time)
|
|
569
|
+
|
|
570
|
+
def write_log(self):
|
|
571
|
+
|
|
572
|
+
log = f"Threshold segmentation: {self.threshold_instructions}\n"
|
|
573
|
+
with open(self.pos + f"log_{self.mode}.txt", "a") as f:
|
|
574
|
+
f.write(f"{datetime.datetime.now()} SEGMENT \n")
|
|
575
|
+
f.write(log)
|
|
576
|
+
|
|
577
|
+
def detect_channels(self):
|
|
578
|
+
|
|
579
|
+
for i in range(len(self.instructions)):
|
|
580
|
+
|
|
581
|
+
self.channel_indices = _extract_channel_indices_from_config(
|
|
582
|
+
self.config, self.required_channels[i]
|
|
583
|
+
)
|
|
584
|
+
logger.info(
|
|
585
|
+
f"Required channels: {self.required_channels[i]} located at channel indices {self.channel_indices}."
|
|
586
|
+
)
|
|
587
|
+
self.instructions[i].update({"target_channel": self.channel_indices[0]})
|
|
588
|
+
self.instructions[i].update({"channel_names": self.channel_names})
|
|
589
|
+
|
|
590
|
+
self.img_num_channels = _get_img_num_per_channel(
|
|
591
|
+
np.arange(self.nbr_channels), self.len_movie, self.nbr_channels
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def parallel_job(self, indices):
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
from celldetective.segmentation import (
|
|
598
|
+
segment_frame_from_thresholds,
|
|
599
|
+
merge_instance_segmentation,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
for t in tqdm(
|
|
603
|
+
indices, desc="frame"
|
|
604
|
+
): # for t in tqdm(range(self.len_movie),desc="frame"):
|
|
605
|
+
|
|
606
|
+
# Load channels at time t
|
|
607
|
+
masks = []
|
|
608
|
+
for i in range(len(self.instructions)):
|
|
609
|
+
f = load_frames(
|
|
610
|
+
self.img_num_channels[:, t],
|
|
611
|
+
self.file,
|
|
612
|
+
scale=None,
|
|
613
|
+
normalize_input=False,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
mask = segment_frame_from_thresholds(f, **self.instructions[i])
|
|
617
|
+
# print(f'Frame {t}; segment with {self.instructions[i]=}...')
|
|
618
|
+
masks.append(mask)
|
|
619
|
+
|
|
620
|
+
if len(self.instructions) > 1:
|
|
621
|
+
mask = merge_instance_segmentation(masks, mode="OR")
|
|
622
|
+
|
|
623
|
+
from celldetective.utils.io import save_tiff_imagej_compatible
|
|
624
|
+
|
|
625
|
+
save_tiff_imagej_compatible(
|
|
626
|
+
os.sep.join(
|
|
627
|
+
[self.pos, self.label_folder, f"{str(t).zfill(4)}.tif"]
|
|
628
|
+
),
|
|
629
|
+
mask.astype(np.uint16),
|
|
630
|
+
axes="YX",
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# del f
|
|
634
|
+
# del mask
|
|
635
|
+
# gc.collect()
|
|
636
|
+
|
|
637
|
+
# Send signal for progress bar
|
|
638
|
+
self.sum_done += 1 / self.len_movie * 100
|
|
639
|
+
|
|
640
|
+
# Triple progress bar logic
|
|
641
|
+
data = {}
|
|
642
|
+
data["frame_progress"] = self.sum_done
|
|
643
|
+
|
|
644
|
+
# Frame time estimation
|
|
645
|
+
elapsed = time.time() - getattr(self, "t0_frame", time.time())
|
|
646
|
+
measured_count = int((self.sum_done / 100) * self.len_movie)
|
|
647
|
+
|
|
648
|
+
if measured_count > 0:
|
|
649
|
+
avg = elapsed / measured_count
|
|
650
|
+
rem = self.len_movie - measured_count
|
|
651
|
+
if rem < 0:
|
|
652
|
+
rem = 0
|
|
653
|
+
rem_t = rem * avg
|
|
654
|
+
mins = int(rem_t // 60)
|
|
655
|
+
secs = int(rem_t % 60)
|
|
656
|
+
data["frame_time"] = f"Segmentation: {mins} m {secs} s"
|
|
657
|
+
else:
|
|
658
|
+
data["frame_time"] = f"Segmentation..."
|
|
659
|
+
|
|
660
|
+
# Saturate preview: Convert labels to binary (0/1)
|
|
661
|
+
# data["image_preview"] = mask > 0
|
|
662
|
+
# Saturate preview: Convert labels to binary (0/1)
|
|
663
|
+
data["image_preview"] = (mask > 0).astype(np.uint8)
|
|
664
|
+
self.queue.put(data)
|
|
665
|
+
|
|
666
|
+
del f
|
|
667
|
+
del mask
|
|
668
|
+
gc.collect()
|
|
669
|
+
|
|
670
|
+
except Exception as e:
|
|
671
|
+
logger.error(e)
|
|
672
|
+
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
def process_position(self):
|
|
676
|
+
|
|
677
|
+
tprint("Segment")
|
|
678
|
+
|
|
679
|
+
# Re-initialize threshold specific stuff (depends on channel indices which depend on metadata)
|
|
680
|
+
self.load_threshold_config()
|
|
681
|
+
self.extract_threshold_parameters()
|
|
682
|
+
self.detect_channels()
|
|
683
|
+
self.prepare_equalize()
|
|
684
|
+
self.write_log() # Log start of segmentation for this pos
|
|
685
|
+
|
|
686
|
+
self.indices = list(range(self.img_num_channels.shape[1]))
|
|
687
|
+
if self.flip:
|
|
688
|
+
self.indices = np.array(list(reversed(self.indices)))
|
|
689
|
+
|
|
690
|
+
chunks = np.array_split(self.indices, self.n_threads)
|
|
691
|
+
|
|
692
|
+
self.t0_frame = time.time() # Reset timer for accurate frame timing
|
|
693
|
+
self.sum_done = 0 # Reset progress for this pos
|
|
694
|
+
|
|
695
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
696
|
+
max_workers=self.n_threads
|
|
697
|
+
) as executor:
|
|
698
|
+
results = results = executor.map(
|
|
699
|
+
self.parallel_job, chunks
|
|
700
|
+
) # list(map(lambda x: executor.submit(self.parallel_job, x), chunks))
|
|
701
|
+
try:
|
|
702
|
+
for i, return_value in enumerate(results):
|
|
703
|
+
pass
|
|
704
|
+
except Exception as e:
|
|
705
|
+
logger.error("Exception: ", e)
|
|
706
|
+
raise e
|
|
707
|
+
|
|
708
|
+
def run(self):
|
|
709
|
+
|
|
710
|
+
# Wrapper for single-position compatibility if batch_structure is missing
|
|
711
|
+
if not hasattr(self, "batch_structure"):
|
|
712
|
+
self.batch_structure = {
|
|
713
|
+
0: {"well_name": "Batch", "positions": self.positions}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
self.t0_well = time.time()
|
|
717
|
+
# Loop over Wells
|
|
718
|
+
for w_i, (w_idx, well_data) in enumerate(self.batch_structure.items()):
|
|
719
|
+
positions = well_data["positions"]
|
|
720
|
+
|
|
721
|
+
# Well Time Estimation
|
|
722
|
+
elapsed = time.time() - self.t0_well
|
|
723
|
+
if w_i > 0:
|
|
724
|
+
avg_well = elapsed / w_i
|
|
725
|
+
rem_well = (len(self.batch_structure) - w_i) * avg_well
|
|
726
|
+
mins_w = int(rem_well // 60)
|
|
727
|
+
secs_w = int(rem_well % 60)
|
|
728
|
+
well_str = f"Well {w_i + 1}/{len(self.batch_structure)} - {mins_w} m {secs_w} s left"
|
|
729
|
+
else:
|
|
730
|
+
well_str = f"Processing well {w_i + 1}/{len(self.batch_structure)}..."
|
|
731
|
+
|
|
732
|
+
# Update Well Progress
|
|
733
|
+
self.queue.put(
|
|
734
|
+
{
|
|
735
|
+
"well_progress": (w_i / len(self.batch_structure)) * 100,
|
|
736
|
+
"well_time": well_str,
|
|
737
|
+
}
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
self.t0_pos = time.time()
|
|
741
|
+
# Loop over positions in this well
|
|
742
|
+
for pos_idx, pos_path in enumerate(positions):
|
|
743
|
+
|
|
744
|
+
# Setup specific variables for this position
|
|
745
|
+
self.setup_for_position(pos_path)
|
|
746
|
+
|
|
747
|
+
self.process_position()
|
|
748
|
+
|
|
749
|
+
# End of position loop
|
|
750
|
+
self.queue.put({"pos_progress": ((pos_idx + 1) / len(positions)) * 100})
|
|
751
|
+
|
|
752
|
+
# End of Well loop
|
|
753
|
+
self.queue.put(
|
|
754
|
+
{"well_progress": ((w_i + 1) / len(self.batch_structure)) * 100}
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
logger.info("Done.")
|
|
758
|
+
# Send end signal
|
|
759
|
+
self.queue.put("finished")
|
|
760
|
+
self.queue.close()
|