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
@@ -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()