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,565 @@
1
+ from multiprocessing import Process
2
+ import time
3
+ import datetime
4
+ import os
5
+ import json
6
+ from pathlib import Path, PurePath
7
+
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.experiment import extract_experiment_channels
14
+ from celldetective.utils.parsing import config_section_to_dict
15
+ from celldetective.utils.data_cleaning import (
16
+ _extract_coordinates_from_features,
17
+ remove_trajectory_measurements,
18
+ )
19
+ from glob import glob
20
+ from tqdm import tqdm
21
+ import numpy as np
22
+ import concurrent.futures
23
+ from natsort import natsorted
24
+ from art import tprint
25
+ from typing import Optional, Union
26
+ import gc
27
+ from celldetective.measure import (
28
+ measure_features,
29
+ measure_isotropic_intensity,
30
+ center_of_mass_to_abs_coordinates,
31
+ measure_radial_distance_to_center,
32
+ drop_tonal_features,
33
+ )
34
+ import pandas as pd
35
+ from celldetective.utils.image_loaders import locate_labels
36
+
37
+ from celldetective.log_manager import get_logger
38
+
39
+ logger = get_logger(__name__)
40
+
41
+
42
+ class MeasurementProcess(Process):
43
+
44
+ pos: Optional[Union[str, Path]] = None
45
+ mode: Optional[str] = None
46
+ n_threads: int = 1
47
+
48
+ def __init__(self, queue=None, process_args=None):
49
+
50
+ super().__init__()
51
+
52
+ self.queue = queue
53
+
54
+ if process_args is not None:
55
+ for key, value in process_args.items():
56
+ setattr(self, key, value)
57
+
58
+ self.column_labels = {
59
+ "track": "TRACK_ID",
60
+ "time": "FRAME",
61
+ "x": "POSITION_X",
62
+ "y": "POSITION_Y",
63
+ }
64
+
65
+ self.sum_done = 0
66
+ self.t0 = time.time()
67
+
68
+ def check_possible_measurements(self):
69
+
70
+ if (self.file is None) or (self.intensity_measurement_radii is None):
71
+ self.do_iso_intensities = False
72
+ logger.warning(
73
+ "Either no image, no positions or no radii were provided... Isotropic intensities will not be computed..."
74
+ )
75
+ else:
76
+ self.do_iso_intensities = True
77
+
78
+ if self.label_path is None:
79
+ self.do_features = False
80
+ logger.warning(
81
+ "No labels were provided... Features will not be computed..."
82
+ )
83
+ else:
84
+ self.do_features = True
85
+
86
+ if self.trajectories is None:
87
+ logger.info("Use features as a substitute for the trajectory table.")
88
+ if "label" not in self.features:
89
+ self.features.append("label")
90
+
91
+ def read_measurement_instructions(self):
92
+
93
+ logger.info("Looking for measurement instruction file...")
94
+ instr_path = PurePath(self.exp_dir, Path(f"{self.instruction_file}"))
95
+ if os.path.exists(instr_path):
96
+ with open(instr_path, "r") as f:
97
+ self.instructions = json.load(f)
98
+ logger.info(f"Measurement instruction file successfully loaded...")
99
+ logger.info(f"Instructions: {self.instructions}...")
100
+
101
+ if "background_correction" in self.instructions:
102
+ self.background_correction = self.instructions["background_correction"]
103
+ else:
104
+ self.background_correction = None
105
+
106
+ if "features" in self.instructions:
107
+ self.features = self.instructions["features"]
108
+ else:
109
+ self.features = None
110
+
111
+ if "border_distances" in self.instructions:
112
+ self.border_distances = self.instructions["border_distances"]
113
+ else:
114
+ self.border_distances = None
115
+
116
+ if "spot_detection" in self.instructions:
117
+ self.spot_detection = self.instructions["spot_detection"]
118
+ else:
119
+ self.spot_detection = None
120
+
121
+ if "haralick_options" in self.instructions:
122
+ self.haralick_options = self.instructions["haralick_options"]
123
+ else:
124
+ self.haralick_options = None
125
+
126
+ if "intensity_measurement_radii" in self.instructions:
127
+ self.intensity_measurement_radii = self.instructions[
128
+ "intensity_measurement_radii"
129
+ ]
130
+ else:
131
+ self.intensity_measurement_radii = None
132
+
133
+ if "isotropic_operations" in self.instructions:
134
+ self.isotropic_operations = self.instructions["isotropic_operations"]
135
+ else:
136
+ self.isotropic_operations = None
137
+
138
+ if "clear_previous" in self.instructions:
139
+ self.clear_previous = self.instructions["clear_previous"]
140
+ else:
141
+ self.clear_previous = True
142
+
143
+ else:
144
+ logger.info("No measurement instructions found. Use default measurements.")
145
+ self.features = ["area", "intensity_mean"]
146
+ self.border_distances = None
147
+ self.haralick_options = None
148
+ self.clear_previous = False
149
+ self.background_correction = None
150
+ self.spot_detection = None
151
+ self.intensity_measurement_radii = 10
152
+ self.isotropic_operations = ["mean"]
153
+
154
+ if self.features is None:
155
+ self.features = []
156
+
157
+ def detect_channels(self):
158
+ self.img_num_channels = _get_img_num_per_channel(
159
+ self.channel_indices, self.len_movie, self.nbr_channels
160
+ )
161
+
162
+ def write_log(self):
163
+
164
+ features_log = f"features: {self.features}"
165
+ border_distances_log = f"border_distances: {self.border_distances}"
166
+ haralick_options_log = f"haralick_options: {self.haralick_options}"
167
+ background_correction_log = (
168
+ f"background_correction: {self.background_correction}"
169
+ )
170
+ spot_detection_log = f"spot_detection: {self.spot_detection}"
171
+ intensity_measurement_radii_log = (
172
+ f"intensity_measurement_radii: {self.intensity_measurement_radii}"
173
+ )
174
+ isotropic_options_log = f"isotropic_operations: {self.isotropic_operations} \n"
175
+ log = "\n".join(
176
+ [
177
+ features_log,
178
+ border_distances_log,
179
+ haralick_options_log,
180
+ background_correction_log,
181
+ spot_detection_log,
182
+ intensity_measurement_radii_log,
183
+ isotropic_options_log,
184
+ ]
185
+ )
186
+ with open(self.pos + f"log_{self.mode}.txt", "a") as f:
187
+ f.write(f"{datetime.datetime.now()} MEASURE \n")
188
+ f.write(log + "\n")
189
+
190
+ def prepare_folders(self):
191
+
192
+ if self.mode.lower() == "target" or self.mode.lower() == "targets":
193
+ self.label_folder = "labels_targets"
194
+ self.table_name = "trajectories_targets.csv"
195
+ self.instruction_file = os.sep.join(
196
+ ["configs", "measurement_instructions_targets.json"]
197
+ )
198
+
199
+ elif self.mode.lower() == "effector" or self.mode.lower() == "effectors":
200
+ self.label_folder = "labels_effectors"
201
+ self.table_name = "trajectories_effectors.csv"
202
+ self.instruction_file = os.sep.join(
203
+ ["configs", "measurement_instructions_effectors.json"]
204
+ )
205
+
206
+ else:
207
+ self.label_folder = f"labels_{self.mode}"
208
+ self.table_name = f"trajectories_{self.mode}.csv"
209
+ self.instruction_file = os.sep.join(
210
+ ["configs", f"measurement_instructions_{self.mode}.json"]
211
+ )
212
+
213
+ def extract_experiment_parameters(self):
214
+
215
+ self.movie_prefix = config_section_to_dict(self.config, "MovieSettings")[
216
+ "movie_prefix"
217
+ ]
218
+ self.spatial_calibration = float(
219
+ config_section_to_dict(self.config, "MovieSettings")["pxtoum"]
220
+ )
221
+ self.time_calibration = float(
222
+ config_section_to_dict(self.config, "MovieSettings")["frametomin"]
223
+ )
224
+ self.len_movie = float(
225
+ config_section_to_dict(self.config, "MovieSettings")["len_movie"]
226
+ )
227
+ self.shape_x = int(
228
+ config_section_to_dict(self.config, "MovieSettings")["shape_x"]
229
+ )
230
+ self.shape_y = int(
231
+ config_section_to_dict(self.config, "MovieSettings")["shape_y"]
232
+ )
233
+
234
+ self.channel_names, self.channel_indices = extract_experiment_channels(
235
+ self.exp_dir
236
+ )
237
+ self.nbr_channels = len(self.channel_names)
238
+
239
+ def locate_experiment_config(self):
240
+
241
+ parent1 = Path(self.pos).parent
242
+ self.exp_dir = parent1.parent
243
+ self.config = PurePath(self.exp_dir, Path("config.ini"))
244
+
245
+ if not os.path.exists(self.config):
246
+ logger.error("The configuration file for the experiment was not found...")
247
+ self.abort_process()
248
+
249
+ def detect_tracks(self):
250
+
251
+ # Load trajectories, add centroid if not in trajectory
252
+ self.trajectories = self.pos + os.sep.join(
253
+ ["output", "tables", self.table_name]
254
+ )
255
+ if os.path.exists(self.trajectories):
256
+ logger.info("Previous table detected...")
257
+ self.trajectories = pd.read_csv(self.trajectories)
258
+ if "TRACK_ID" not in list(self.trajectories.columns):
259
+ logger.info("Static measurements detected...")
260
+ self.do_iso_intensities = False
261
+ self.intensity_measurement_radii = None
262
+ if self.clear_previous:
263
+ logger.info("Clear previous measurements...")
264
+ self.trajectories = None # remove_trajectory_measurements(trajectories, column_labels)
265
+ self.do_features = True
266
+ self.features += ["centroid"]
267
+ self.column_labels.update({"track": "ID"})
268
+ else:
269
+ logger.info("Time series detected...")
270
+ if self.clear_previous:
271
+ logger.info("TRACK_ID found... Clear previous measurements...")
272
+ self.trajectories = remove_trajectory_measurements(
273
+ self.trajectories, self.column_labels
274
+ )
275
+ else:
276
+ self.trajectories = None
277
+ self.do_features = True
278
+ self.features += ["centroid"]
279
+ self.do_iso_intensities = False
280
+
281
+ def detect_movie_and_labels(self):
282
+
283
+ self.label_path = natsorted(
284
+ glob(os.sep.join([self.pos, self.label_folder, "*.tif"]))
285
+ )
286
+ if len(self.label_path) > 0:
287
+ logger.info(f"Found {len(self.label_path)} segmented frames...")
288
+ else:
289
+ self.features = None
290
+ self.haralick_options = None
291
+ self.border_distances = None
292
+ self.label_path = None
293
+
294
+ try:
295
+ self.file = glob(
296
+ self.pos + os.sep.join(["movie", f"{self.movie_prefix}*.tif"])
297
+ )[0]
298
+ except IndexError:
299
+ self.file = None
300
+ self.haralick_option = None
301
+ self.features = drop_tonal_features(self.features)
302
+
303
+ len_movie_auto = auto_load_number_of_frames(self.file)
304
+ if len_movie_auto is not None:
305
+ self.len_movie = len_movie_auto
306
+
307
+ def parallel_job(self, indices):
308
+
309
+ measurements = []
310
+
311
+ for t in tqdm(indices, desc="frame"):
312
+
313
+ if self.file is not None:
314
+ img = load_frames(
315
+ self.img_num_channels[:, t],
316
+ self.file,
317
+ scale=None,
318
+ normalize_input=False,
319
+ )
320
+
321
+ if self.label_path is not None:
322
+ lbl = locate_labels(self.pos, population=self.mode, frames=t)
323
+ if lbl is None:
324
+ continue
325
+
326
+ if self.trajectories is not None:
327
+ # Optimized access
328
+ if self.frame_slices is not None:
329
+ # Check if frame t is in our precomputed slices
330
+ if t in self.frame_slices:
331
+ start, end = self.frame_slices[t]
332
+ positions_at_t = self.trajectories.iloc[start:end].copy()
333
+ else:
334
+ # Empty frame for trajectories
335
+ positions_at_t = pd.DataFrame(columns=self.trajectories.columns)
336
+ else:
337
+ # Fallback or original method (should not be reached if optimized)
338
+ positions_at_t = self.trajectories.loc[
339
+ self.trajectories[self.column_labels["time"]] == t
340
+ ].copy()
341
+
342
+ if self.do_features:
343
+ feature_table = measure_features(
344
+ img,
345
+ lbl,
346
+ features=self.features,
347
+ border_dist=self.border_distances,
348
+ channels=self.channel_names,
349
+ haralick_options=self.haralick_options,
350
+ verbose=False,
351
+ normalisation_list=self.background_correction,
352
+ spot_detection=self.spot_detection,
353
+ )
354
+ if self.trajectories is None:
355
+ positions_at_t = _extract_coordinates_from_features(
356
+ feature_table, timepoint=t
357
+ )
358
+ column_labels = {
359
+ "track": "ID",
360
+ "time": self.column_labels["time"],
361
+ "x": self.column_labels["x"],
362
+ "y": self.column_labels["y"],
363
+ }
364
+ feature_table.rename(
365
+ columns={"centroid-1": "POSITION_X", "centroid-0": "POSITION_Y"},
366
+ inplace=True,
367
+ )
368
+
369
+ if self.do_iso_intensities and not self.trajectories is None:
370
+ iso_table = measure_isotropic_intensity(
371
+ positions_at_t,
372
+ img,
373
+ channels=self.channel_names,
374
+ intensity_measurement_radii=self.intensity_measurement_radii,
375
+ column_labels=self.column_labels,
376
+ operations=self.isotropic_operations,
377
+ verbose=False,
378
+ )
379
+
380
+ if (
381
+ self.do_iso_intensities
382
+ and self.do_features
383
+ and not self.trajectories is None
384
+ ):
385
+ measurements_at_t = iso_table.merge(
386
+ feature_table, how="outer", on="class_id", suffixes=("_delme", "")
387
+ )
388
+ measurements_at_t = measurements_at_t[
389
+ [c for c in measurements_at_t.columns if not c.endswith("_delme")]
390
+ ]
391
+ elif (
392
+ self.do_iso_intensities
393
+ * (not self.do_features)
394
+ * (not self.trajectories is None)
395
+ ):
396
+ measurements_at_t = iso_table
397
+ elif self.do_features:
398
+ measurements_at_t = positions_at_t.merge(
399
+ feature_table, how="outer", on="class_id", suffixes=("_delme", "")
400
+ )
401
+ measurements_at_t = measurements_at_t[
402
+ [c for c in measurements_at_t.columns if not c.endswith("_delme")]
403
+ ]
404
+
405
+ measurements_at_t = center_of_mass_to_abs_coordinates(measurements_at_t)
406
+
407
+ measurements_at_t = measure_radial_distance_to_center(
408
+ measurements_at_t, volume=img.shape, column_labels=self.column_labels
409
+ )
410
+
411
+ self.sum_done += 1
412
+ data = {}
413
+
414
+ # Frame Progress
415
+ frame_progress = (self.sum_done / self.len_movie) * 100
416
+ if frame_progress > 100:
417
+ frame_progress = 100
418
+ data["frame_progress"] = frame_progress
419
+
420
+ # Frame Time Estimation
421
+ elapsed = time.time() - getattr(self, "t0_frame", time.time())
422
+ if self.sum_done > 0:
423
+ avg = elapsed / self.sum_done
424
+ rem = self.len_movie - self.sum_done
425
+ rem_t = rem * avg
426
+ mins = int(rem_t // 60)
427
+ secs = int(rem_t % 60)
428
+ data["frame_time"] = f"Measurement: {mins} m {secs} s"
429
+ else:
430
+ data["frame_time"] = "Measuring..."
431
+
432
+ self.queue.put(data)
433
+
434
+ if measurements_at_t is not None:
435
+ measurements_at_t[self.column_labels["time"]] = t
436
+ else:
437
+ measurements_at_t = pd.DataFrame()
438
+
439
+ measurements.append(measurements_at_t)
440
+
441
+ return measurements
442
+
443
+ def setup_for_position(self, pos):
444
+
445
+ self.pos = pos
446
+ # Experiment
447
+ self.prepare_folders()
448
+ self.locate_experiment_config()
449
+ self.extract_experiment_parameters()
450
+ self.read_measurement_instructions()
451
+ self.detect_movie_and_labels()
452
+ self.detect_tracks()
453
+ self.detect_channels()
454
+ self.check_possible_measurements()
455
+ self.write_log()
456
+
457
+ def process_position(self):
458
+ tprint("Measure")
459
+
460
+ self.indices = list(range(self.img_num_channels.shape[1]))
461
+ chunks = np.array_split(self.indices, self.n_threads)
462
+
463
+ self.timestep_dataframes = []
464
+ self.t0_frame = time.time()
465
+ self.sum_done = 0
466
+
467
+ # Optimize: Group trajectories by frame for O(1) access inside the loop
468
+ self.frame_slices = None
469
+ if self.trajectories is not None:
470
+ # Sort by FRAME to enable searchsorted
471
+ self.trajectories = self.trajectories.sort_values(
472
+ self.column_labels["time"]
473
+ )
474
+ frames = self.trajectories[self.column_labels["time"]].values
475
+
476
+ # Find unique frames and their indices
477
+ unique_frames = np.unique(frames)
478
+
479
+ # searchsorted returns the indices where elements should be inserted to maintain order
480
+ # 'left' gives the start index, 'right' gives the end index
481
+ start_indices = np.searchsorted(frames, unique_frames, side="left")
482
+ end_indices = np.searchsorted(frames, unique_frames, side="right")
483
+
484
+ self.frame_slices = {
485
+ frame: (start, end)
486
+ for frame, start, end in zip(unique_frames, start_indices, end_indices)
487
+ }
488
+
489
+ if self.n_threads > 1:
490
+ with concurrent.futures.ThreadPoolExecutor(
491
+ max_workers=self.n_threads
492
+ ) as executor:
493
+ results = executor.map(
494
+ self.parallel_job, chunks
495
+ ) # list(map(lambda x: executor.submit(self.parallel_job, x), chunks))
496
+ try:
497
+ for i, return_value in enumerate(results):
498
+ logger.info(f"Thread {i} completed...")
499
+ self.timestep_dataframes.extend(return_value)
500
+ except Exception as e:
501
+ logger.error("Exception: ", e)
502
+ raise e
503
+ else:
504
+ try:
505
+ # Avoid thread pool overhead for single thread
506
+ results = [self.parallel_job(chunks[0])]
507
+ for i, return_value in enumerate(results):
508
+ logger.info(f"Job {i} completed...")
509
+ self.timestep_dataframes.extend(return_value)
510
+ except Exception as e:
511
+ logger.error("Exception: ", e)
512
+ raise e
513
+
514
+ logger.info("Measurements successfully performed...")
515
+
516
+ if len(self.timestep_dataframes) > 0:
517
+
518
+ df = pd.concat(self.timestep_dataframes)
519
+
520
+ if self.trajectories is not None:
521
+ df = df.sort_values(
522
+ by=[self.column_labels["track"], self.column_labels["time"]]
523
+ )
524
+ df = df.dropna(subset=[self.column_labels["track"]])
525
+ else:
526
+ df["ID"] = np.arange(len(df))
527
+ df = df.sort_values(by=[self.column_labels["time"], "ID"])
528
+
529
+ df = df.reset_index(drop=True)
530
+ # df = _remove_invalid_cols(df)
531
+ logger.info(f"Final columns before export: {df.columns.tolist()}")
532
+ df = df.replace([np.inf, -np.inf], np.nan)
533
+
534
+ df.to_csv(
535
+ self.pos + os.sep.join(["output", "tables", self.table_name]),
536
+ index=False,
537
+ )
538
+ logger.info(
539
+ f'Measurement table successfully exported in {os.sep.join(["output", "tables"])}...'
540
+ )
541
+ logger.info("Done.")
542
+ else:
543
+ logger.error("No measurement could be performed. Check your inputs.")
544
+
545
+ logger.info("Done.")
546
+ gc.collect()
547
+
548
+ def run(self):
549
+
550
+ self.setup_for_position(self.pos)
551
+ self.process_position()
552
+
553
+ # Send end signal
554
+ self.queue.put("finished")
555
+ self.queue.close()
556
+
557
+ def end_process(self):
558
+
559
+ self.terminate()
560
+ self.queue.put("finished")
561
+
562
+ def abort_process(self):
563
+
564
+ self.terminate()
565
+ self.queue.put("error")