celldetective 1.4.1.post1__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 +642 -554
- 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 -1700
- 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 +1332 -1011
- 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.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
- celldetective-1.5.0b0.dist-info/RECORD +187 -0
- {celldetective-1.4.1.post1.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 +425 -144
- 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.1.post1.dist-info/RECORD +0 -123
- /celldetective/{gui/processes → processes}/downloader.py +0 -0
- {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
- {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.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")
|