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,246 @@
1
+ from multiprocessing import Process
2
+ import os
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from celldetective.log_manager import get_logger
7
+ from celldetective.tracking import clean_trajectories
8
+ from celldetective.utils.color_mappings import (
9
+ color_from_status,
10
+ color_from_class,
11
+ )
12
+ from celldetective.utils.event_detection import _prep_event_detection_model
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class SignalAnalysisProcess(Process):
18
+
19
+ pos = None
20
+ mode = None
21
+ model_name = None
22
+ use_gpu = True
23
+
24
+ def __init__(self, queue=None, process_args=None):
25
+ super().__init__()
26
+ self.queue = queue
27
+ if process_args is not None:
28
+ for key, value in process_args.items():
29
+ setattr(self, key, value)
30
+
31
+ self.column_labels = {
32
+ "track": "TRACK_ID",
33
+ "time": "FRAME",
34
+ "x": "POSITION_X",
35
+ "y": "POSITION_Y",
36
+ }
37
+
38
+ def setup_for_position(self, pos):
39
+ self.pos = pos
40
+ self.pos_path = rf"{pos}"
41
+
42
+ def process_position(self, model=None):
43
+ logger.info(
44
+ f"Analyzing signals for position {self.pos} with model {self.model_name}"
45
+ )
46
+
47
+ try:
48
+ # Determine table name based on mode
49
+ if self.mode.lower() in ["target", "targets"]:
50
+ table_name = "trajectories_targets.csv"
51
+ elif self.mode.lower() in ["effector", "effectors"]:
52
+ table_name = "trajectories_effectors.csv"
53
+ else:
54
+ table_name = f"trajectories_{self.mode}.csv"
55
+
56
+ trajectories_path = os.path.join(self.pos, "output", "tables", table_name)
57
+
58
+ if not os.path.exists(trajectories_path):
59
+ logger.warning(f"No trajectories table found at {trajectories_path}")
60
+ return
61
+
62
+ trajectories = pd.read_csv(trajectories_path)
63
+
64
+ if self.column_labels["track"] not in trajectories.columns:
65
+ logger.warning(
66
+ f"Column {self.column_labels['track']} not found in {trajectories_path}. Skipping position."
67
+ )
68
+ return
69
+
70
+ # --- Logic adapted from analyze_signals to include progress ---
71
+
72
+ # Configuration checks (similar to analyze_signals)
73
+ if model is None:
74
+ # This path handles if model instance wasn't passed (fallback, though unified_process should pass it)
75
+ if hasattr(self, "signal_model_instance"):
76
+ model = self.signal_model_instance
77
+ else:
78
+ # Lazy load if needed
79
+ model = _prep_event_detection_model(
80
+ self.model_name, use_gpu=self.use_gpu
81
+ )
82
+
83
+ config = model.config
84
+ required_signals = config["channels"]
85
+ model_signal_length = config["model_signal_length"]
86
+
87
+ # Channel selection logic
88
+ available_signals = list(trajectories.columns)
89
+ selected_signals = config.get("selected_channels", None)
90
+
91
+ if selected_signals is None:
92
+ selected_signals = []
93
+ for s in required_signals:
94
+ priority_cols = [a for a in available_signals if a == s]
95
+ second_priority_cols = [
96
+ a for a in available_signals if a.startswith(s) and a != s
97
+ ]
98
+ third_priority_cols = [
99
+ a for a in available_signals if s in a and not a.startswith(s)
100
+ ]
101
+ candidates = (
102
+ priority_cols + second_priority_cols + third_priority_cols
103
+ )
104
+
105
+ if len(candidates) > 0:
106
+ selected_signals.append(candidates[0])
107
+ else:
108
+ logger.error(f"No match for signal {s} in {available_signals}")
109
+ raise ValueError(f"Missing required channel: {s}")
110
+
111
+ # Preprocessing
112
+ trajectories_clean = clean_trajectories(
113
+ trajectories,
114
+ interpolate_na=True,
115
+ interpolate_position_gaps=True,
116
+ column_labels=self.column_labels,
117
+ )
118
+
119
+ max_signal_size = (
120
+ int(trajectories_clean[self.column_labels["time"]].max()) + 2
121
+ )
122
+ if max_signal_size > model_signal_length:
123
+ logger.warning(
124
+ f"Signals longer than model input ({max_signal_size} > {model_signal_length}). Truncating may occur."
125
+ )
126
+
127
+ tracks = trajectories_clean[self.column_labels["track"]].unique()
128
+ signals = np.zeros((len(tracks), max_signal_size, len(selected_signals)))
129
+
130
+ # Progress loop for signal extraction
131
+ total_tracks = len(tracks)
132
+
133
+ for i, (tid, group) in enumerate(
134
+ trajectories_clean.groupby(self.column_labels["track"])
135
+ ):
136
+
137
+ # Report progress
138
+ progress = ((i + 1) / total_tracks) * 100
139
+ self.queue.put(
140
+ {
141
+ "frame_progress": progress, # Reusing frame_progress key for UI compatibility
142
+ "frame_time": f"Extracting signals: {i+1}/{total_tracks}",
143
+ }
144
+ )
145
+
146
+ frames = group[self.column_labels["time"]].to_numpy().astype(int)
147
+ for j, col in enumerate(selected_signals):
148
+ signal = group[col].to_numpy()
149
+ signals[i, frames, j] = signal
150
+ signals[i, max(frames) :, j] = signal[-1]
151
+
152
+ # Prediction
153
+ self.queue.put({"frame_time": "Predicting events..."})
154
+ classes = model.predict_class(signals)
155
+ times_recast = model.predict_time_of_interest(signals)
156
+
157
+ # Assign results
158
+ try:
159
+ label = config.get("label", "")
160
+ if label == "":
161
+ label = None
162
+ except:
163
+ label = None
164
+
165
+ if label is None:
166
+ class_col = "class"
167
+ time_col = "t0"
168
+ status_col = "status"
169
+ else:
170
+ class_col = "class_" + label
171
+ time_col = "t_" + label
172
+ status_col = "status_" + label
173
+
174
+ self.queue.put({"frame_time": "Saving results..."})
175
+
176
+ # Vectorized assignment is faster than loop, but let's stick to safe logic
177
+ # We need to map track_id to result index. 'tracks' array indices align with 'signals' indices
178
+ track_to_idx = {t: i for i, t in enumerate(tracks)}
179
+
180
+ # Map predictions to original dataframe
181
+ # Using map is much faster than iterating if possible, but let's do safe iteration for now or efficient mapping
182
+ # Actually, let's use the track ID map
183
+ trajectories[class_col] = trajectories[self.column_labels["track"]].map(
184
+ lambda x: classes[track_to_idx[x]] if x in track_to_idx else 0
185
+ )
186
+ trajectories[time_col] = trajectories[self.column_labels["track"]].map(
187
+ lambda x: times_recast[track_to_idx[x]] if x in track_to_idx else 0
188
+ )
189
+
190
+ # Generate Status/Color columns
191
+ # This is complex to vectorize due to time dependency (t >= t0).
192
+ # We can iterate group-wise again or use vectorized pandas ops
193
+
194
+ # For status generation, we stick to the loop as in original code, but maybe optimize?
195
+ # Original code iterates groupby. Let's do that for safety and correctness.
196
+
197
+ for tid, group in trajectories.groupby(self.column_labels["track"]):
198
+ indices = group.index
199
+ t0 = group[time_col].iloc[0]
200
+ cclass = group[class_col].iloc[0]
201
+ timeline = group[self.column_labels["time"]].to_numpy()
202
+ status = np.zeros_like(timeline)
203
+
204
+ if t0 > 0:
205
+ status[timeline >= t0] = 1.0
206
+ if cclass == 2:
207
+ status[:] = 2
208
+ if cclass > 2:
209
+ status[:] = 42
210
+
211
+ # Color mapping is slow if done element-wise.
212
+ # But color_from_status returns list/string.
213
+ # Let's just assign status first.
214
+ trajectories.loc[indices, status_col] = status
215
+
216
+ # Status colors
217
+ # Optimization: define color map and map values
218
+ # status_color = [color_from_status(s) for s in status]
219
+ # applying function on column is faster
220
+ trajectories["status_color"] = trajectories[status_col].apply(
221
+ color_from_status
222
+ )
223
+ trajectories["class_color"] = trajectories[class_col].apply(
224
+ color_from_class
225
+ )
226
+
227
+ trajectories = trajectories.sort_values(
228
+ by=[self.column_labels["track"], self.column_labels["time"]]
229
+ )
230
+ trajectories.to_csv(trajectories_path, index=False)
231
+
232
+ logger.info(f"Signal analysis completed for {self.pos}")
233
+
234
+ except Exception as e:
235
+ logger.error(f"Error in SignalAnalysisProcess: {e}", exc_info=True)
236
+ raise e
237
+
238
+ def run(self):
239
+ # This run method is for independent execution, but UnifiedBatchProcess calls methods directly.
240
+ # However, keeping it robust.
241
+ self.setup_for_position(self.pos)
242
+ model = _prep_event_detection_model(
243
+ self.model_name, use_gpu=self.use_gpu
244
+ ) # Load local if running standalone
245
+ self.process_position(model)
246
+ self.queue.put("finished")
@@ -0,0 +1,137 @@
1
+ import os
2
+ import ssl
3
+ from tqdm import tqdm
4
+ from multiprocessing import Process
5
+ from glob import glob
6
+ import shutil
7
+ from urllib.request import urlopen
8
+ import zipfile
9
+ import tempfile
10
+ import time
11
+ from pathlib import Path
12
+ import json
13
+
14
+
15
+ class DownloadProcess(Process):
16
+
17
+ def __init__(self, queue=None, process_args=None, *args, **kwargs):
18
+
19
+ super().__init__(*args, **kwargs)
20
+
21
+ if process_args is not None:
22
+ for key, value in process_args.items():
23
+ setattr(self, key, value)
24
+
25
+ self.queue = queue
26
+ self.progress = True
27
+
28
+ # Get celldetective package root
29
+ current_dir = os.path.dirname(os.path.realpath(__file__))
30
+ package_root = os.path.dirname(current_dir)
31
+ zenodo_json = os.path.join(package_root, "links", "zenodo.json")
32
+ # print(f"{zenodo_json=}")
33
+
34
+ with open(zenodo_json, "r") as f:
35
+ zenodo_json = json.load(f)
36
+ all_files = list(zenodo_json["files"]["entries"].keys())
37
+ all_files_short = [f.replace(".zip", "") for f in all_files]
38
+ zenodo_url = zenodo_json["links"]["files"].replace("api/", "")
39
+ full_links = ["/".join([zenodo_url, f]) for f in all_files]
40
+ index = all_files_short.index(self.file)
41
+
42
+ self.zip_url = full_links[index]
43
+ self.path_to_zip_file = os.sep.join([self.output_dir, "temp.zip"])
44
+
45
+ self.sum_done = 0
46
+ self.t0 = time.time()
47
+
48
+ def download_url_to_file(self, url, dst):
49
+ try:
50
+ file_size = None
51
+ ssl._create_default_https_context = ssl._create_unverified_context
52
+ u = urlopen(url)
53
+ meta = u.info()
54
+ if hasattr(meta, "getheaders"):
55
+ content_length = meta.getheaders("Content-Length")
56
+ else:
57
+ content_length = meta.get_all("Content-Length")
58
+ if content_length is not None and len(content_length) > 0:
59
+ file_size = int(content_length[0])
60
+ # We deliberately save it in a temp file and move it after
61
+ dst = os.path.expanduser(dst)
62
+ dst_dir = os.path.dirname(dst)
63
+ f = tempfile.NamedTemporaryFile(delete=False, dir=dst_dir)
64
+
65
+ try:
66
+ with tqdm(
67
+ total=file_size,
68
+ disable=not self.progress,
69
+ unit="B",
70
+ unit_scale=True,
71
+ unit_divisor=1024,
72
+ ) as pbar:
73
+ while True:
74
+ buffer = u.read(8192) # 8192
75
+ if len(buffer) == 0:
76
+ break
77
+ f.write(buffer)
78
+ pbar.update(len(buffer))
79
+ self.sum_done += len(buffer) / file_size * 100
80
+ mean_exec_per_step = (time.time() - self.t0) / (
81
+ self.sum_done * file_size / 100 + 1
82
+ )
83
+ pred_time = (
84
+ file_size - (self.sum_done * file_size / 100 + 1)
85
+ ) * mean_exec_per_step
86
+ self.queue.put([self.sum_done, pred_time])
87
+ f.close()
88
+ shutil.move(f.name, dst)
89
+ finally:
90
+ f.close()
91
+ if os.path.exists(f.name):
92
+ os.remove(f.name)
93
+ except Exception as e:
94
+ print("No internet connection: ", e)
95
+ return None
96
+
97
+ def run(self):
98
+
99
+ self.download_url_to_file(rf"{self.zip_url}", self.path_to_zip_file)
100
+ with zipfile.ZipFile(self.path_to_zip_file, "r") as zip_ref:
101
+ zip_ref.extractall(self.output_dir)
102
+
103
+ file_to_rename = glob(
104
+ os.sep.join(
105
+ [
106
+ self.output_dir,
107
+ self.file,
108
+ "*[!.json][!.png][!.h5][!.csv][!.npy][!.tif][!.ini]",
109
+ ]
110
+ )
111
+ )
112
+ if (
113
+ len(file_to_rename) > 0
114
+ and not file_to_rename[0].endswith(os.sep)
115
+ and not self.file.startswith("demo")
116
+ ):
117
+ os.rename(
118
+ file_to_rename[0], os.sep.join([self.output_dir, self.file, self.file])
119
+ )
120
+
121
+ os.remove(self.path_to_zip_file)
122
+ self.queue.put([100, 0])
123
+ time.sleep(0.5)
124
+
125
+ # Send end signal
126
+ self.queue.put("finished")
127
+ self.queue.close()
128
+
129
+ def end_process(self):
130
+
131
+ self.terminate()
132
+ self.queue.put("finished")
133
+
134
+ def abort_process(self):
135
+
136
+ self.terminate()
137
+ self.queue.put("error")