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.
Files changed (151) 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 +642 -554
  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 -1700
  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 +304 -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/measure_cells.py +565 -0
  81. celldetective/processes/segment_cells.py +760 -0
  82. celldetective/processes/track_cells.py +435 -0
  83. celldetective/processes/train_segmentation_model.py +694 -0
  84. celldetective/processes/train_signal_model.py +265 -0
  85. celldetective/processes/unified_process.py +292 -0
  86. celldetective/regionprops/_regionprops.py +358 -317
  87. celldetective/relative_measurements.py +987 -710
  88. celldetective/scripts/measure_cells.py +313 -212
  89. celldetective/scripts/measure_relative.py +90 -46
  90. celldetective/scripts/segment_cells.py +165 -104
  91. celldetective/scripts/segment_cells_thresholds.py +96 -68
  92. celldetective/scripts/track_cells.py +198 -149
  93. celldetective/scripts/train_segmentation_model.py +324 -201
  94. celldetective/scripts/train_signal_model.py +87 -45
  95. celldetective/segmentation.py +844 -749
  96. celldetective/signals.py +3514 -2861
  97. celldetective/tracking.py +1332 -1011
  98. celldetective/utils/__init__.py +0 -0
  99. celldetective/utils/cellpose_utils/__init__.py +133 -0
  100. celldetective/utils/color_mappings.py +42 -0
  101. celldetective/utils/data_cleaning.py +630 -0
  102. celldetective/utils/data_loaders.py +450 -0
  103. celldetective/utils/dataset_helpers.py +207 -0
  104. celldetective/utils/downloaders.py +197 -0
  105. celldetective/utils/event_detection/__init__.py +8 -0
  106. celldetective/utils/experiment.py +1782 -0
  107. celldetective/utils/image_augmenters.py +308 -0
  108. celldetective/utils/image_cleaning.py +74 -0
  109. celldetective/utils/image_loaders.py +926 -0
  110. celldetective/utils/image_transforms.py +335 -0
  111. celldetective/utils/io.py +62 -0
  112. celldetective/utils/mask_cleaning.py +348 -0
  113. celldetective/utils/mask_transforms.py +5 -0
  114. celldetective/utils/masks.py +184 -0
  115. celldetective/utils/maths.py +351 -0
  116. celldetective/utils/model_getters.py +325 -0
  117. celldetective/utils/model_loaders.py +296 -0
  118. celldetective/utils/normalization.py +380 -0
  119. celldetective/utils/parsing.py +465 -0
  120. celldetective/utils/plots/__init__.py +0 -0
  121. celldetective/utils/plots/regression.py +53 -0
  122. celldetective/utils/resources.py +34 -0
  123. celldetective/utils/stardist_utils/__init__.py +104 -0
  124. celldetective/utils/stats.py +90 -0
  125. celldetective/utils/types.py +21 -0
  126. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
  127. celldetective-1.5.0b0.dist-info/RECORD +187 -0
  128. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
  129. tests/gui/test_new_project.py +129 -117
  130. tests/gui/test_project.py +127 -79
  131. tests/test_filters.py +39 -15
  132. tests/test_notebooks.py +8 -0
  133. tests/test_tracking.py +425 -144
  134. tests/test_utils.py +123 -77
  135. celldetective/gui/base_components.py +0 -23
  136. celldetective/gui/layouts.py +0 -1602
  137. celldetective/gui/processes/compute_neighborhood.py +0 -594
  138. celldetective/gui/processes/measure_cells.py +0 -360
  139. celldetective/gui/processes/segment_cells.py +0 -499
  140. celldetective/gui/processes/track_cells.py +0 -303
  141. celldetective/gui/processes/train_segmentation_model.py +0 -270
  142. celldetective/gui/processes/train_signal_model.py +0 -108
  143. celldetective/gui/table_ops/merge_groups.py +0 -118
  144. celldetective/gui/viewers.py +0 -1354
  145. celldetective/io.py +0 -3663
  146. celldetective/utils.py +0 -3108
  147. celldetective-1.4.1.post1.dist-info/RECORD +0 -123
  148. /celldetective/{gui/processes → processes}/downloader.py +0 -0
  149. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
  150. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
  151. {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
- from PyQt5.QtWidgets import QVBoxLayout, QScrollArea, QLabel, QHBoxLayout, QLineEdit, QPushButton
3
- from PyQt5.QtCore import Qt
4
- import configparser
5
- from celldetective.gui import CelldetectiveWidget
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('[{}]'.format(section))
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('Save')
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, 'w') as f:
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