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,447 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from PyQt5.QtCore import Qt
|
|
3
|
+
from PyQt5.QtWidgets import QComboBox, QDialog, QHBoxLayout, QLabel, QMessageBox, QPushButton, QVBoxLayout
|
|
4
|
+
from matplotlib import pyplot as plt
|
|
5
|
+
from matplotlib.widgets import RectangleSelector
|
|
6
|
+
|
|
7
|
+
from celldetective.gui.base.styles import Styles
|
|
8
|
+
from celldetective.gui.base.figure_canvas import FigureCanvas
|
|
9
|
+
from celldetective import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InteractiveEventViewer(QDialog, Styles):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
table_path,
|
|
18
|
+
signal_name=None,
|
|
19
|
+
event_label=None,
|
|
20
|
+
df=None,
|
|
21
|
+
callback=None,
|
|
22
|
+
parent=None,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(parent)
|
|
25
|
+
self.table_path = table_path
|
|
26
|
+
|
|
27
|
+
if df is not None:
|
|
28
|
+
self.df = df
|
|
29
|
+
else:
|
|
30
|
+
self.df = pd.read_csv(table_path)
|
|
31
|
+
|
|
32
|
+
self.signal_name = signal_name
|
|
33
|
+
self.event_label = event_label
|
|
34
|
+
self.callback = callback
|
|
35
|
+
self.selected_tracks = set()
|
|
36
|
+
self.setWindowTitle("Interactive Event Viewer")
|
|
37
|
+
self.resize(800, 600)
|
|
38
|
+
|
|
39
|
+
# Analyze columns to identify signal, class, time columns
|
|
40
|
+
self.detect_columns()
|
|
41
|
+
|
|
42
|
+
self.init_ui()
|
|
43
|
+
self.plot_signals()
|
|
44
|
+
|
|
45
|
+
def notify_update(self):
|
|
46
|
+
if self.callback:
|
|
47
|
+
self.callback()
|
|
48
|
+
|
|
49
|
+
def detect_columns(self):
|
|
50
|
+
self.event_types = {}
|
|
51
|
+
cols = self.df.columns
|
|
52
|
+
|
|
53
|
+
# If explicit label is provided, prioritize it
|
|
54
|
+
if self.event_label is not None:
|
|
55
|
+
label = self.event_label
|
|
56
|
+
if label == "": # No label
|
|
57
|
+
c_col, t_col, s_col = "class", "t0", "status"
|
|
58
|
+
else:
|
|
59
|
+
c_col, t_col, s_col = f"class_{label}", f"t_{label}", f"status_{label}"
|
|
60
|
+
|
|
61
|
+
if c_col in cols and t_col in cols:
|
|
62
|
+
self.event_types[label if label else "Default"] = {
|
|
63
|
+
"class": c_col,
|
|
64
|
+
"time": t_col,
|
|
65
|
+
"status": s_col if s_col in cols else None,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# If no label provided or columns not found (safety), fall back to scan
|
|
69
|
+
if not self.event_types:
|
|
70
|
+
# Check for default
|
|
71
|
+
if "class" in cols and "t0" in cols:
|
|
72
|
+
self.event_types["Default"] = {
|
|
73
|
+
"class": "class",
|
|
74
|
+
"time": "t0",
|
|
75
|
+
"status": "status" if "status" in cols else None,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Check for labeled events
|
|
79
|
+
# Find all columns starting with class_
|
|
80
|
+
for c in cols:
|
|
81
|
+
if c.startswith("class_") and c not in ["class_id", "class_color"]:
|
|
82
|
+
suffix = c[len("class_") :]
|
|
83
|
+
# Avoid duplication if label was provided but somehow not matched above
|
|
84
|
+
if suffix == self.event_label:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
t_col = f"t_{suffix}"
|
|
88
|
+
if t_col in cols:
|
|
89
|
+
status_col = f"status_{suffix}"
|
|
90
|
+
self.event_types[suffix] = {
|
|
91
|
+
"class": c,
|
|
92
|
+
"time": t_col,
|
|
93
|
+
"status": status_col if status_col in cols else None,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if not self.event_types:
|
|
97
|
+
# Fallback if no pairs found (maybe just class exists?)
|
|
98
|
+
# Use heuristics from before but valid only if one exists
|
|
99
|
+
self.event_types["Unknown"] = {
|
|
100
|
+
"class": next(
|
|
101
|
+
(
|
|
102
|
+
c
|
|
103
|
+
for c in cols
|
|
104
|
+
if c.startswith("class")
|
|
105
|
+
and c not in ["class_id", "class_color"]
|
|
106
|
+
),
|
|
107
|
+
"class",
|
|
108
|
+
),
|
|
109
|
+
"time": next(
|
|
110
|
+
(c for c in cols if c.startswith("t_") or c == "t0"), "t0"
|
|
111
|
+
),
|
|
112
|
+
"status": next((c for c in cols if c.startswith("status")), "status"),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Set current active columns to first found
|
|
116
|
+
self.set_active_event_type(next(iter(self.event_types)))
|
|
117
|
+
|
|
118
|
+
self.time_axis_col = next((c for c in cols if c in ["FRAME", "time"]), "FRAME")
|
|
119
|
+
self.track_col = next(
|
|
120
|
+
(c for c in cols if c in ["TRACK_ID", "track"]), "TRACK_ID"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Signal name detection
|
|
124
|
+
if self.signal_name and self.signal_name not in cols:
|
|
125
|
+
# Try to find a match (e.g. if config has 'dead_nuclei_channel' but table has 'dead_nuclei_channel_mean')
|
|
126
|
+
potential = [c for c in cols if c.startswith(self.signal_name)]
|
|
127
|
+
if potential:
|
|
128
|
+
logger.info(
|
|
129
|
+
f"Signal '{self.signal_name}' not found. Using '{potential[0]}' instead."
|
|
130
|
+
)
|
|
131
|
+
self.signal_name = potential[0]
|
|
132
|
+
else:
|
|
133
|
+
logger.info(
|
|
134
|
+
f"Signal '{self.signal_name}' not found and no partial match. Falling back to auto-detection."
|
|
135
|
+
)
|
|
136
|
+
self.signal_name = None
|
|
137
|
+
|
|
138
|
+
if self.signal_name is None:
|
|
139
|
+
excluded = {
|
|
140
|
+
"class_id",
|
|
141
|
+
"class_color",
|
|
142
|
+
"None",
|
|
143
|
+
self.track_col,
|
|
144
|
+
self.time_axis_col,
|
|
145
|
+
}
|
|
146
|
+
for info in self.event_types.values():
|
|
147
|
+
excluded.update(info.values())
|
|
148
|
+
|
|
149
|
+
candidates = [
|
|
150
|
+
c
|
|
151
|
+
for c in cols
|
|
152
|
+
if c not in excluded
|
|
153
|
+
and pd.api.types.is_numeric_dtype(self.df[c])
|
|
154
|
+
and not c.startswith("class")
|
|
155
|
+
and not c.startswith("t_")
|
|
156
|
+
and not c.startswith("status")
|
|
157
|
+
]
|
|
158
|
+
if candidates:
|
|
159
|
+
self.signal_name = candidates[0]
|
|
160
|
+
else:
|
|
161
|
+
self.signal_name = cols[0]
|
|
162
|
+
|
|
163
|
+
def set_active_event_type(self, type_name):
|
|
164
|
+
self.current_event_type = type_name
|
|
165
|
+
info = self.event_types[type_name]
|
|
166
|
+
self.class_col = info["class"]
|
|
167
|
+
self.time_col = info["time"]
|
|
168
|
+
self.status_col = info["status"]
|
|
169
|
+
|
|
170
|
+
def init_ui(self):
|
|
171
|
+
layout = QVBoxLayout(self)
|
|
172
|
+
|
|
173
|
+
# Top controls
|
|
174
|
+
top_layout = QHBoxLayout()
|
|
175
|
+
|
|
176
|
+
# Event Type Selector
|
|
177
|
+
if len(self.event_types) > 1:
|
|
178
|
+
top_layout.addWidget(QLabel("Event Type:"))
|
|
179
|
+
self.event_combo = QComboBox()
|
|
180
|
+
self.event_combo.addItems(list(self.event_types.keys()))
|
|
181
|
+
self.event_combo.currentTextChanged.connect(self.change_event_type)
|
|
182
|
+
top_layout.addWidget(self.event_combo)
|
|
183
|
+
|
|
184
|
+
top_layout.addWidget(QLabel("Signal:"))
|
|
185
|
+
self.signal_combo = QComboBox()
|
|
186
|
+
|
|
187
|
+
# Populate signal combo
|
|
188
|
+
excluded = {
|
|
189
|
+
"class_id",
|
|
190
|
+
"class_color",
|
|
191
|
+
"None",
|
|
192
|
+
self.track_col,
|
|
193
|
+
self.time_axis_col,
|
|
194
|
+
}
|
|
195
|
+
for info in self.event_types.values():
|
|
196
|
+
excluded.update({v for k, v in info.items() if v})
|
|
197
|
+
|
|
198
|
+
candidates = [
|
|
199
|
+
c
|
|
200
|
+
for c in self.df.columns
|
|
201
|
+
if c not in excluded and pd.api.types.is_numeric_dtype(self.df[c])
|
|
202
|
+
]
|
|
203
|
+
self.signal_combo.addItems(candidates)
|
|
204
|
+
if self.signal_name in candidates:
|
|
205
|
+
self.signal_combo.setCurrentText(self.signal_name)
|
|
206
|
+
self.signal_combo.currentTextChanged.connect(self.change_signal)
|
|
207
|
+
top_layout.addWidget(self.signal_combo)
|
|
208
|
+
|
|
209
|
+
top_layout.addWidget(QLabel("Filter:"))
|
|
210
|
+
self.event_filter_combo = QComboBox()
|
|
211
|
+
self.event_filter_combo.addItems(
|
|
212
|
+
["All", "Events (0)", "No Events (1)", "Else (2)"]
|
|
213
|
+
)
|
|
214
|
+
self.event_filter_combo.currentTextChanged.connect(self.plot_signals)
|
|
215
|
+
top_layout.addWidget(self.event_filter_combo)
|
|
216
|
+
|
|
217
|
+
self.event_btn = QPushButton("Event")
|
|
218
|
+
self.event_btn.clicked.connect(lambda: self.set_class(0))
|
|
219
|
+
top_layout.addWidget(self.event_btn)
|
|
220
|
+
|
|
221
|
+
self.reject_btn = QPushButton("No Event")
|
|
222
|
+
self.reject_btn.clicked.connect(lambda: self.set_class(1))
|
|
223
|
+
top_layout.addWidget(self.reject_btn)
|
|
224
|
+
|
|
225
|
+
self.else_btn = QPushButton("Left-censored/Else")
|
|
226
|
+
self.else_btn.clicked.connect(lambda: self.set_class(2))
|
|
227
|
+
top_layout.addWidget(self.else_btn)
|
|
228
|
+
|
|
229
|
+
self.delete_btn = QPushButton("Delete")
|
|
230
|
+
self.delete_btn.clicked.connect(lambda: self.set_class(3))
|
|
231
|
+
top_layout.addWidget(self.delete_btn)
|
|
232
|
+
|
|
233
|
+
self.save_btn = QPushButton("Save Changes")
|
|
234
|
+
self.save_btn.clicked.connect(self.save_changes)
|
|
235
|
+
top_layout.addWidget(self.save_btn)
|
|
236
|
+
|
|
237
|
+
for btn in [self.event_btn, self.reject_btn, self.else_btn, self.delete_btn]:
|
|
238
|
+
btn.setStyleSheet(self.button_style_sheet_2)
|
|
239
|
+
for btn in [self.save_btn]:
|
|
240
|
+
btn.setStyleSheet(self.button_style_sheet)
|
|
241
|
+
|
|
242
|
+
layout.addLayout(top_layout)
|
|
243
|
+
|
|
244
|
+
# Plot
|
|
245
|
+
self.fig = plt.figure(figsize=(8, 6))
|
|
246
|
+
self.canvas = FigureCanvas(self.fig, interactive=True)
|
|
247
|
+
layout.addWidget(self.canvas)
|
|
248
|
+
|
|
249
|
+
# Tooltip/Info
|
|
250
|
+
self.info_label = QLabel(
|
|
251
|
+
"Select (Box): Drag mouse. | Shift Time: Left/Right Arrows. | Set Class: Buttons above."
|
|
252
|
+
)
|
|
253
|
+
layout.addWidget(self.info_label)
|
|
254
|
+
|
|
255
|
+
def change_event_type(self, text):
|
|
256
|
+
self.set_active_event_type(text)
|
|
257
|
+
self.plot_signals()
|
|
258
|
+
|
|
259
|
+
def change_signal(self, text):
|
|
260
|
+
self.signal_name = text
|
|
261
|
+
self.plot_signals()
|
|
262
|
+
|
|
263
|
+
def keyPressEvent(self, event):
|
|
264
|
+
if not self.selected_tracks:
|
|
265
|
+
super().keyPressEvent(event)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
step = 0.5
|
|
269
|
+
mask = self.df[self.track_col].isin(self.selected_tracks)
|
|
270
|
+
|
|
271
|
+
if event.key() == Qt.Key_Left:
|
|
272
|
+
# Shift curve LEFT: Increase t0 -> x decreases
|
|
273
|
+
self.df.loc[mask, self.time_col] += step
|
|
274
|
+
|
|
275
|
+
# Recompute status if column exists
|
|
276
|
+
if self.status_col and self.status_col in self.df.columns:
|
|
277
|
+
# status is 1 if time >= t0, else 0
|
|
278
|
+
self.df.loc[mask, self.status_col] = (
|
|
279
|
+
self.df.loc[mask, self.time_axis_col]
|
|
280
|
+
>= self.df.loc[mask, self.time_col]
|
|
281
|
+
).astype(int)
|
|
282
|
+
|
|
283
|
+
self.plot_signals()
|
|
284
|
+
self.notify_update()
|
|
285
|
+
elif event.key() == Qt.Key_Right:
|
|
286
|
+
# Shift curve RIGHT: Decrease t0 -> x increases
|
|
287
|
+
self.df.loc[mask, self.time_col] -= step
|
|
288
|
+
|
|
289
|
+
# Recompute status if column exists
|
|
290
|
+
if self.status_col and self.status_col in self.df.columns:
|
|
291
|
+
# status is 1 if time >= t0, else 0
|
|
292
|
+
self.df.loc[mask, self.status_col] = (
|
|
293
|
+
self.df.loc[mask, self.time_axis_col]
|
|
294
|
+
>= self.df.loc[mask, self.time_col]
|
|
295
|
+
).astype(int)
|
|
296
|
+
|
|
297
|
+
self.plot_signals()
|
|
298
|
+
self.notify_update()
|
|
299
|
+
else:
|
|
300
|
+
super().keyPressEvent(event)
|
|
301
|
+
|
|
302
|
+
def plot_signals(self):
|
|
303
|
+
self.fig.clf()
|
|
304
|
+
self.ax = self.fig.add_subplot(111)
|
|
305
|
+
self.lines = {} # map line -> track_id
|
|
306
|
+
|
|
307
|
+
# Filter based on combo box
|
|
308
|
+
filter_choice = self.event_filter_combo.currentText()
|
|
309
|
+
if "All" in filter_choice:
|
|
310
|
+
# Exclude deleted (3) usually? Or allow all? Only exclude 3 if it means strictly delete.
|
|
311
|
+
valid_mask = self.df[self.class_col] != 3
|
|
312
|
+
elif "Events (0)" in filter_choice:
|
|
313
|
+
valid_mask = self.df[self.class_col] == 0
|
|
314
|
+
elif "No Events (1)" in filter_choice:
|
|
315
|
+
valid_mask = self.df[self.class_col] == 1
|
|
316
|
+
elif "Else (2)" in filter_choice:
|
|
317
|
+
valid_mask = self.df[self.class_col] == 2
|
|
318
|
+
else:
|
|
319
|
+
valid_mask = ~self.df[self.class_col].isin([1, 3])
|
|
320
|
+
|
|
321
|
+
if not valid_mask.any():
|
|
322
|
+
# If nothing left, show empty or message?
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
tracks = self.df[valid_mask][self.track_col].unique()
|
|
326
|
+
|
|
327
|
+
for tid in tracks:
|
|
328
|
+
group = self.df[self.df[self.track_col] == tid]
|
|
329
|
+
t0 = group[self.time_col].iloc[0]
|
|
330
|
+
# Handle NaN t0 if necessary
|
|
331
|
+
if pd.isna(t0):
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
time = group[self.time_axis_col].values
|
|
335
|
+
signal = group[self.signal_name].values
|
|
336
|
+
|
|
337
|
+
# Center time
|
|
338
|
+
x = time - t0
|
|
339
|
+
|
|
340
|
+
# Color coding
|
|
341
|
+
# Class 0: Blue, Class 1: Gray, Class 2: Orange
|
|
342
|
+
c_val = group[self.class_col].iloc[0]
|
|
343
|
+
color = "tab:red"
|
|
344
|
+
if c_val == 1:
|
|
345
|
+
color = "tab:blue"
|
|
346
|
+
elif c_val == 2:
|
|
347
|
+
color = "yellow"
|
|
348
|
+
|
|
349
|
+
(line,) = self.ax.plot(x, signal, picker=True, alpha=0.95, color=color)
|
|
350
|
+
self.lines[line] = tid
|
|
351
|
+
|
|
352
|
+
# Highlight if selected (persist selection)
|
|
353
|
+
if tid in self.selected_tracks:
|
|
354
|
+
line.set_color("red")
|
|
355
|
+
line.set_alpha(1.0)
|
|
356
|
+
|
|
357
|
+
self.ax.set_title(f"Centered Signals: {self.signal_name}")
|
|
358
|
+
self.ax.set_xlabel("Time from Event (t - t0)")
|
|
359
|
+
self.ax.set_ylabel("Signal Intensity")
|
|
360
|
+
|
|
361
|
+
# Setup selector
|
|
362
|
+
self.selector = RectangleSelector(
|
|
363
|
+
self.ax,
|
|
364
|
+
self.on_select_rect,
|
|
365
|
+
useblit=True,
|
|
366
|
+
button=[1], # Left mouse button
|
|
367
|
+
minspanx=5,
|
|
368
|
+
minspany=5,
|
|
369
|
+
spancoords="pixels",
|
|
370
|
+
interactive=True,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
self.ax.grid(True)
|
|
374
|
+
|
|
375
|
+
self.canvas.draw()
|
|
376
|
+
|
|
377
|
+
def on_select_rect(self, eclick, erelease):
|
|
378
|
+
# Find lines intersecting the rectangle
|
|
379
|
+
x1, y1 = eclick.xdata, eclick.ydata
|
|
380
|
+
x2, y2 = erelease.xdata, erelease.ydata
|
|
381
|
+
|
|
382
|
+
xmin, xmax = sorted([x1, x2])
|
|
383
|
+
ymin, ymax = sorted([y1, y2])
|
|
384
|
+
|
|
385
|
+
self.selected_tracks.clear()
|
|
386
|
+
|
|
387
|
+
for line, tid in self.lines.items():
|
|
388
|
+
xdata = line.get_xdata()
|
|
389
|
+
ydata = line.get_ydata()
|
|
390
|
+
|
|
391
|
+
# Check if any point is in rect
|
|
392
|
+
mask = (xdata >= xmin) & (xdata <= xmax) & (ydata >= ymin) & (ydata <= ymax)
|
|
393
|
+
if mask.any():
|
|
394
|
+
self.selected_tracks.add(tid)
|
|
395
|
+
line.set_color("red")
|
|
396
|
+
line.set_alpha(1.0)
|
|
397
|
+
else:
|
|
398
|
+
# Reset color based on class
|
|
399
|
+
# Need to look up class again or store it in line metadata?
|
|
400
|
+
# Just redraw is safer/easier or lookup df
|
|
401
|
+
# Optimization: store class in lines map? self.lines[line] = (tid, class)
|
|
402
|
+
# For now just set to blue/orange heuristic
|
|
403
|
+
c_val = self.df.loc[
|
|
404
|
+
self.df[self.track_col] == tid, self.class_col
|
|
405
|
+
].iloc[0]
|
|
406
|
+
color = "tab:red"
|
|
407
|
+
if c_val == 1:
|
|
408
|
+
color = "tab:blue"
|
|
409
|
+
elif c_val == 2:
|
|
410
|
+
color = "yellow"
|
|
411
|
+
line.set_color(color)
|
|
412
|
+
line.set_alpha(0.5)
|
|
413
|
+
|
|
414
|
+
self.canvas.draw()
|
|
415
|
+
self.info_label.setText(f"Selected {len(self.selected_tracks)} tracks.")
|
|
416
|
+
|
|
417
|
+
def set_class(self, class_val):
|
|
418
|
+
"""Set class for selected tracks."""
|
|
419
|
+
if not self.selected_tracks:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
count = len(self.selected_tracks)
|
|
423
|
+
# direct update without confirmation for speed, or maybe optional?
|
|
424
|
+
# User wants interactive flow.
|
|
425
|
+
|
|
426
|
+
mask = self.df[self.track_col].isin(self.selected_tracks)
|
|
427
|
+
self.df.loc[mask, self.class_col] = class_val
|
|
428
|
+
|
|
429
|
+
# Clear selection after action? Or keep it?
|
|
430
|
+
# Usually better to clear or refresh.
|
|
431
|
+
# Since we filter out Class 1/3, the lines will disappear.
|
|
432
|
+
|
|
433
|
+
self.selected_tracks.clear()
|
|
434
|
+
self.plot_signals()
|
|
435
|
+
self.info_label.setText(f"Set {count} tracks to Class {class_val}.")
|
|
436
|
+
|
|
437
|
+
self.notify_update()
|
|
438
|
+
|
|
439
|
+
def reject_selection(self):
|
|
440
|
+
self.set_class(1)
|
|
441
|
+
|
|
442
|
+
def save_changes(self):
|
|
443
|
+
try:
|
|
444
|
+
self.df.to_csv(self.table_path, index=False)
|
|
445
|
+
QMessageBox.information(self, "Saved", "Table saved successfully.")
|
|
446
|
+
except Exception as e:
|
|
447
|
+
QMessageBox.critical(self, "Error", f"Could not save table: {e}")
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from subprocess import Popen
|
|
3
|
+
|
|
4
|
+
from PyQt5.QtWidgets import (
|
|
5
|
+
QVBoxLayout,
|
|
6
|
+
QScrollArea,
|
|
7
|
+
QLabel,
|
|
8
|
+
QHBoxLayout,
|
|
9
|
+
QLineEdit,
|
|
10
|
+
QPushButton,
|
|
11
|
+
)
|
|
12
|
+
from PyQt5.QtCore import Qt, QSize
|
|
1
13
|
import configparser
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
import
|
|
5
|
-
|
|
14
|
+
|
|
15
|
+
from fonticon_mdi6 import MDI6
|
|
16
|
+
from superqt.fonticon import icon
|
|
17
|
+
|
|
18
|
+
from celldetective.gui.base.components import CelldetectiveWidget
|
|
6
19
|
|
|
7
20
|
|
|
8
21
|
class ConfigEditor(CelldetectiveWidget):
|
|
9
|
-
|
|
10
|
-
def __init__(self, parent_window):
|
|
11
22
|
|
|
23
|
+
def __init__(self, parent_window):
|
|
12
24
|
"""
|
|
13
25
|
Load and edit the experiment config.
|
|
14
26
|
"""
|
|
@@ -18,7 +30,7 @@ class ConfigEditor(CelldetectiveWidget):
|
|
|
18
30
|
self.parent_window = parent_window
|
|
19
31
|
self.config_path = self.parent_window.exp_config
|
|
20
32
|
|
|
21
|
-
self.setGeometry(500,200,400,700)
|
|
33
|
+
self.setGeometry(500, 200, 400, 700)
|
|
22
34
|
|
|
23
35
|
self.setWindowTitle("Configuration")
|
|
24
36
|
|
|
@@ -26,6 +38,14 @@ class ConfigEditor(CelldetectiveWidget):
|
|
|
26
38
|
self.layout = QVBoxLayout()
|
|
27
39
|
|
|
28
40
|
# Create a scroll area to contain the main layout
|
|
41
|
+
self.edit_config_btn = QPushButton("")
|
|
42
|
+
self.edit_config_btn.setStyleSheet(self.button_select_all)
|
|
43
|
+
self.edit_config_btn.setIcon(icon(MDI6.file_cog, color="black"))
|
|
44
|
+
self.edit_config_btn.setToolTip("Advanced edition.")
|
|
45
|
+
self.edit_config_btn.setIconSize(QSize(20, 20))
|
|
46
|
+
|
|
47
|
+
self.layout.addWidget(self.edit_config_btn, alignment=Qt.AlignRight)
|
|
48
|
+
|
|
29
49
|
scroll = QScrollArea()
|
|
30
50
|
scroll.setWidgetResizable(True)
|
|
31
51
|
scroll_content = CelldetectiveWidget()
|
|
@@ -46,9 +66,22 @@ class ConfigEditor(CelldetectiveWidget):
|
|
|
46
66
|
|
|
47
67
|
self.load_config()
|
|
48
68
|
|
|
69
|
+
self.edit_config_btn.clicked.connect(self.edit_in_text_editor)
|
|
70
|
+
|
|
71
|
+
def edit_in_text_editor(self):
|
|
72
|
+
path = self.config_path
|
|
73
|
+
try:
|
|
74
|
+
Popen(f"explorer {os.path.realpath(path)}")
|
|
75
|
+
except:
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
os.system('xdg-open "%s"' % path)
|
|
79
|
+
except:
|
|
80
|
+
return None
|
|
81
|
+
|
|
49
82
|
def load_config(self):
|
|
50
83
|
file_name = self.config_path
|
|
51
|
-
#self.file_edit.setText(file_name)
|
|
84
|
+
# self.file_edit.setText(file_name)
|
|
52
85
|
|
|
53
86
|
config = configparser.ConfigParser(interpolation=None)
|
|
54
87
|
config.read(file_name)
|
|
@@ -56,7 +89,7 @@ class ConfigEditor(CelldetectiveWidget):
|
|
|
56
89
|
# Create a layout for each section of the config file
|
|
57
90
|
for section in config.sections():
|
|
58
91
|
section_layout = QVBoxLayout()
|
|
59
|
-
section_label = QLabel(
|
|
92
|
+
section_label = QLabel("[{}]".format(section))
|
|
60
93
|
self.labels.append(section_label)
|
|
61
94
|
|
|
62
95
|
# Create an editor box for each parameter in the section
|
|
@@ -77,13 +110,13 @@ class ConfigEditor(CelldetectiveWidget):
|
|
|
77
110
|
|
|
78
111
|
# Add a save button
|
|
79
112
|
save_layout = QHBoxLayout()
|
|
80
|
-
save_button = QPushButton(
|
|
113
|
+
save_button = QPushButton("Save")
|
|
81
114
|
save_button.setStyleSheet(self.button_style_sheet)
|
|
82
115
|
save_button.clicked.connect(self.save_config)
|
|
83
116
|
save_button.setShortcut("Return")
|
|
84
|
-
#save_button.setIcon(QIcon_from_svg(self.parent.abs_path+f"/icons/save.svg", color='white'))
|
|
117
|
+
# save_button.setIcon(QIcon_from_svg(self.parent.abs_path+f"/icons/save.svg", color='white'))
|
|
85
118
|
|
|
86
|
-
#save_layout.addStretch()
|
|
119
|
+
# save_layout.addStretch()
|
|
87
120
|
save_layout.addWidget(save_button, alignment=Qt.AlignTop)
|
|
88
121
|
|
|
89
122
|
# Add the save button to the main layout
|
|
@@ -93,7 +126,7 @@ class ConfigEditor(CelldetectiveWidget):
|
|
|
93
126
|
def save_config(self):
|
|
94
127
|
# Save the configuration to the file
|
|
95
128
|
file_name = self.config_path
|
|
96
|
-
|
|
129
|
+
|
|
97
130
|
config = configparser.ConfigParser(interpolation=None)
|
|
98
131
|
|
|
99
132
|
# Update the values in the config object
|
|
@@ -104,8 +137,8 @@ class ConfigEditor(CelldetectiveWidget):
|
|
|
104
137
|
config.set(section, key, edit_box.text())
|
|
105
138
|
|
|
106
139
|
# Write the config object to the file
|
|
107
|
-
with open(file_name,
|
|
140
|
+
with open(file_name, "w") as f:
|
|
108
141
|
config.write(f)
|
|
109
142
|
|
|
110
143
|
self.parent_window.load_configuration()
|
|
111
|
-
self.close()
|
|
144
|
+
self.close()
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from .background_model_free_layout import BackgroundModelFreeCorrectionLayout
|
|
2
|
+
from .model_fit_layout import BackgroundFitCorrectionLayout
|
|
3
|
+
from .channel_offset_layout import ChannelOffsetOptionsLayout
|
|
4
|
+
from .protocol_designer_layout import ProtocolDesignerLayout
|
|
5
|
+
from .local_correction_layout import LocalCorrectionLayout
|