celldetective 1.5.0b7__py3-none-any.whl → 1.5.0b8__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/_version.py +1 -1
- celldetective/event_detection_models.py +2463 -0
- celldetective/gui/base/channel_norm_generator.py +19 -3
- celldetective/gui/base/figure_canvas.py +1 -1
- celldetective/gui/base_annotator.py +2 -5
- celldetective/gui/event_annotator.py +248 -138
- celldetective/gui/pair_event_annotator.py +146 -20
- celldetective/gui/process_block.py +2 -2
- celldetective/gui/seg_model_loader.py +4 -4
- celldetective/gui/settings/_settings_event_model_training.py +32 -14
- celldetective/gui/settings/_settings_segmentation_model_training.py +5 -5
- celldetective/gui/settings/_settings_signal_annotator.py +0 -19
- celldetective/gui/viewers/base_viewer.py +17 -20
- celldetective/processes/train_signal_model.py +1 -1
- celldetective/scripts/train_signal_model.py +1 -1
- celldetective/signals.py +4 -2426
- celldetective/utils/event_detection/__init__.py +1 -1
- {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b8.dist-info}/METADATA +1 -1
- {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b8.dist-info}/RECORD +24 -23
- tests/test_signals.py +4 -4
- {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b8.dist-info}/WHEEL +0 -0
- {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b8.dist-info}/entry_points.txt +0 -0
- {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b8.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.5.0b7.dist-info → celldetective-1.5.0b8.dist-info}/top_level.txt +0 -0
|
@@ -36,6 +36,22 @@ class ChannelNormGenerator(QVBoxLayout, Styles):
|
|
|
36
36
|
self.generate_widgets()
|
|
37
37
|
self.add_to_layout()
|
|
38
38
|
|
|
39
|
+
def add_items_truncated(self, combo, items):
|
|
40
|
+
"""
|
|
41
|
+
Add items to a combobox with truncated text and full text in tooltip/data.
|
|
42
|
+
"""
|
|
43
|
+
combo.clear()
|
|
44
|
+
for item in items:
|
|
45
|
+
item_text = str(item)
|
|
46
|
+
if len(item_text) > 25:
|
|
47
|
+
display_text = item_text[:25] + "..."
|
|
48
|
+
else:
|
|
49
|
+
display_text = item_text
|
|
50
|
+
combo.addItem(display_text, item_text)
|
|
51
|
+
# Find the index of the added item
|
|
52
|
+
idx = combo.count() - 1
|
|
53
|
+
combo.setItemData(idx, item_text, Qt.ToolTipRole)
|
|
54
|
+
|
|
39
55
|
def generate_widgets(self):
|
|
40
56
|
|
|
41
57
|
self.channel_cbs = [QSearchableComboBox() for i in range(self.init_n_channels)]
|
|
@@ -178,7 +194,7 @@ class ChannelNormGenerator(QVBoxLayout, Styles):
|
|
|
178
194
|
|
|
179
195
|
self.channel_cbs.append(QSearchableComboBox())
|
|
180
196
|
self.channel_labels.append(QLabel())
|
|
181
|
-
self.channel_cbs[-1]
|
|
197
|
+
self.add_items_truncated(self.channel_cbs[-1], self.channel_items)
|
|
182
198
|
self.channel_cbs[-1].currentIndexChanged.connect(self.check_valid_channels)
|
|
183
199
|
self.channel_labels[-1].setText(f"channel {len(self.channel_cbs)-1}: ")
|
|
184
200
|
|
|
@@ -239,7 +255,7 @@ class ChannelNormGenerator(QVBoxLayout, Styles):
|
|
|
239
255
|
ch_layout = QHBoxLayout()
|
|
240
256
|
self.channel_labels[i].setText(f"channel {i}: ")
|
|
241
257
|
ch_layout.addWidget(self.channel_labels[i], 30)
|
|
242
|
-
self.channel_cbs[i]
|
|
258
|
+
self.add_items_truncated(self.channel_cbs[i], self.channel_items)
|
|
243
259
|
self.channel_cbs[i].currentIndexChanged.connect(self.check_valid_channels)
|
|
244
260
|
ch_layout.addWidget(self.channel_cbs[i], 70)
|
|
245
261
|
self.channels_vb.addLayout(ch_layout)
|
|
@@ -323,7 +339,7 @@ class ChannelNormGenerator(QVBoxLayout, Styles):
|
|
|
323
339
|
def check_valid_channels(self):
|
|
324
340
|
|
|
325
341
|
if hasattr(self.parent_window, "submit_btn"):
|
|
326
|
-
if np.all([cb.
|
|
342
|
+
if np.all([cb.currentData() == "--" for cb in self.channel_cbs]):
|
|
327
343
|
self.parent_window.submit_btn.setEnabled(False)
|
|
328
344
|
|
|
329
345
|
if hasattr(self.parent_window, "spatial_calib_le") and hasattr(
|
|
@@ -23,7 +23,7 @@ class FigureCanvas(CelldetectiveWidget):
|
|
|
23
23
|
|
|
24
24
|
self.toolbar = NavigationToolbar2QT(self.canvas)
|
|
25
25
|
self.toolbar.setStyleSheet(
|
|
26
|
-
"QToolButton:hover {background-color: lightgray;} QToolButton {background-color: transparent; border: none;}"
|
|
26
|
+
"QToolButton:checked {background-color: darkgray;} QToolButton:hover {background-color: lightgray;} QToolButton {background-color: transparent; border: none;}"
|
|
27
27
|
)
|
|
28
28
|
self.layout = QVBoxLayout(self)
|
|
29
29
|
self.layout.addWidget(self.canvas, 90)
|
|
@@ -431,10 +431,7 @@ class BaseAnnotator(CelldetectiveMainWindow, Styles):
|
|
|
431
431
|
else:
|
|
432
432
|
self.fraction = 0.25
|
|
433
433
|
|
|
434
|
-
|
|
435
|
-
self.anim_interval = int(instructions["interval"])
|
|
436
|
-
else:
|
|
437
|
-
self.anim_interval = 1
|
|
434
|
+
self.anim_interval = 33
|
|
438
435
|
|
|
439
436
|
if "log" in instructions:
|
|
440
437
|
self.log_option = instructions["log"]
|
|
@@ -446,7 +443,7 @@ class BaseAnnotator(CelldetectiveMainWindow, Styles):
|
|
|
446
443
|
self.percentile_mode = True
|
|
447
444
|
self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
|
|
448
445
|
self.fraction = 0.25
|
|
449
|
-
self.anim_interval =
|
|
446
|
+
self.anim_interval = 33
|
|
450
447
|
|
|
451
448
|
def locate_stack(self):
|
|
452
449
|
"""
|
|
@@ -20,7 +20,12 @@ from PyQt5.QtGui import QKeySequence, QIntValidator
|
|
|
20
20
|
|
|
21
21
|
from celldetective.gui.gui_utils import color_from_state
|
|
22
22
|
from celldetective.gui.base.utils import center_window, pretty_table
|
|
23
|
-
from superqt import
|
|
23
|
+
from superqt import (
|
|
24
|
+
QLabeledDoubleSlider,
|
|
25
|
+
QLabeledDoubleRangeSlider,
|
|
26
|
+
QSearchableComboBox,
|
|
27
|
+
QLabeledSlider,
|
|
28
|
+
)
|
|
24
29
|
from celldetective.utils.image_loaders import (
|
|
25
30
|
locate_labels,
|
|
26
31
|
load_frames,
|
|
@@ -96,14 +101,58 @@ class EventAnnotator(BaseAnnotator):
|
|
|
96
101
|
self.class_name = "class"
|
|
97
102
|
self.time_name = "t0"
|
|
98
103
|
self.status_name = "status"
|
|
104
|
+
self._loader_thread = None
|
|
99
105
|
|
|
100
106
|
# self.locate_stack()
|
|
101
107
|
if not self.proceed:
|
|
102
108
|
self.close()
|
|
103
109
|
else:
|
|
104
110
|
if not lazy_load:
|
|
105
|
-
self.
|
|
106
|
-
|
|
111
|
+
self._start_threaded_loading()
|
|
112
|
+
|
|
113
|
+
def _start_threaded_loading(self):
|
|
114
|
+
"""Start loading the stack in a background thread with progress dialog."""
|
|
115
|
+
from celldetective.gui.base.components import CelldetectiveProgressDialog
|
|
116
|
+
|
|
117
|
+
self._progress_dialog = CelldetectiveProgressDialog(
|
|
118
|
+
"Loading stack...", "Cancel", 0, 100, self
|
|
119
|
+
)
|
|
120
|
+
self._progress_dialog.setWindowTitle("Loading")
|
|
121
|
+
self._progress_dialog.setMinimumDuration(0)
|
|
122
|
+
self._progress_dialog.setValue(0)
|
|
123
|
+
|
|
124
|
+
self._loader_thread = StackLoaderThread(self)
|
|
125
|
+
self._loader_thread.progress.connect(self._on_load_progress)
|
|
126
|
+
self._loader_thread.status_update.connect(self._on_load_status)
|
|
127
|
+
self._loader_thread.finished.connect(self._on_load_finished)
|
|
128
|
+
self._progress_dialog.canceled.connect(self._on_load_canceled)
|
|
129
|
+
|
|
130
|
+
self._loader_thread.start()
|
|
131
|
+
|
|
132
|
+
def _on_load_progress(self, value):
|
|
133
|
+
"""Update progress dialog."""
|
|
134
|
+
if hasattr(self, "_progress_dialog") and self._progress_dialog:
|
|
135
|
+
self._progress_dialog.setValue(value)
|
|
136
|
+
|
|
137
|
+
def _on_load_status(self, status):
|
|
138
|
+
"""Update progress dialog label."""
|
|
139
|
+
if hasattr(self, "_progress_dialog") and self._progress_dialog:
|
|
140
|
+
self._progress_dialog.setLabelText(status)
|
|
141
|
+
|
|
142
|
+
def _on_load_canceled(self):
|
|
143
|
+
"""Handle cancel button click."""
|
|
144
|
+
if self._loader_thread:
|
|
145
|
+
self._loader_thread.stop()
|
|
146
|
+
self._loader_thread.wait()
|
|
147
|
+
self.close()
|
|
148
|
+
|
|
149
|
+
def _on_load_finished(self):
|
|
150
|
+
"""Called when loading completes."""
|
|
151
|
+
if hasattr(self, "_progress_dialog") and self._progress_dialog:
|
|
152
|
+
self._progress_dialog.close()
|
|
153
|
+
self._progress_dialog = None
|
|
154
|
+
self._loader_thread = None
|
|
155
|
+
self.finalize_init()
|
|
107
156
|
|
|
108
157
|
def finalize_init(self):
|
|
109
158
|
self.frame_lbl = QLabel("frame: ")
|
|
@@ -200,7 +249,8 @@ class EventAnnotator(BaseAnnotator):
|
|
|
200
249
|
|
|
201
250
|
self.first_frame_btn = QPushButton()
|
|
202
251
|
self.first_frame_btn.clicked.connect(self.set_first_frame)
|
|
203
|
-
self.
|
|
252
|
+
self.first_short = QShortcut(QKeySequence("f"), self)
|
|
253
|
+
self.first_short.activated.connect(self.set_first_frame)
|
|
204
254
|
self.first_frame_btn.setIcon(icon(MDI6.page_first, color="black"))
|
|
205
255
|
self.first_frame_btn.setStyleSheet(self.button_select_all)
|
|
206
256
|
self.first_frame_btn.setFixedSize(QSize(60, 60))
|
|
@@ -208,7 +258,8 @@ class EventAnnotator(BaseAnnotator):
|
|
|
208
258
|
|
|
209
259
|
self.last_frame_btn = QPushButton()
|
|
210
260
|
self.last_frame_btn.clicked.connect(self.set_last_frame)
|
|
211
|
-
self.
|
|
261
|
+
self.last_short = QShortcut(QKeySequence("l"), self)
|
|
262
|
+
self.last_short.activated.connect(self.set_last_frame)
|
|
212
263
|
self.last_frame_btn.setIcon(icon(MDI6.page_last, color="black"))
|
|
213
264
|
self.last_frame_btn.setStyleSheet(self.button_select_all)
|
|
214
265
|
self.last_frame_btn.setFixedSize(QSize(60, 60))
|
|
@@ -229,11 +280,56 @@ class EventAnnotator(BaseAnnotator):
|
|
|
229
280
|
self.start_btn.setIconSize(QSize(30, 30))
|
|
230
281
|
self.start_btn.hide()
|
|
231
282
|
|
|
283
|
+
self.toggle_short = QShortcut(Qt.Key_Space, self)
|
|
284
|
+
self.toggle_short.activated.connect(self.toggle_animation)
|
|
285
|
+
|
|
286
|
+
self.speed_slider = QLabeledSlider(Qt.Horizontal)
|
|
287
|
+
self.speed_slider.setRange(1, 60)
|
|
288
|
+
# Convert initial interval (ms) to FPS
|
|
289
|
+
initial_fps = int(1000 / max(1, self.anim_interval))
|
|
290
|
+
self.speed_slider.setValue(initial_fps)
|
|
291
|
+
self.speed_slider.valueChanged.connect(self.update_speed)
|
|
292
|
+
self.speed_slider.setFixedWidth(200)
|
|
293
|
+
self.speed_slider.setToolTip("Adjust animation framerate (FPS)")
|
|
294
|
+
|
|
295
|
+
speed_layout = QHBoxLayout()
|
|
296
|
+
speed_layout.addWidget(QLabel("Framerate: "))
|
|
297
|
+
speed_layout.addWidget(self.speed_slider)
|
|
298
|
+
|
|
299
|
+
animation_buttons_box.addLayout(speed_layout, 20)
|
|
300
|
+
|
|
232
301
|
animation_buttons_box.addWidget(
|
|
233
302
|
self.first_frame_btn, 5, alignment=Qt.AlignRight
|
|
234
303
|
)
|
|
304
|
+
|
|
305
|
+
self.prev_frame_btn = QPushButton()
|
|
306
|
+
self.prev_frame_btn.clicked.connect(self.prev_frame)
|
|
307
|
+
self.prev_frame_btn.setIcon(icon(MDI6.chevron_left, color="black"))
|
|
308
|
+
self.prev_frame_btn.setStyleSheet(self.button_select_all)
|
|
309
|
+
self.prev_frame_btn.setFixedSize(QSize(60, 60))
|
|
310
|
+
self.prev_frame_btn.setIconSize(QSize(30, 30))
|
|
311
|
+
self.prev_frame_btn.setEnabled(
|
|
312
|
+
False
|
|
313
|
+
) # Disabled by default (assuming auto-start or initial state)
|
|
314
|
+
# Actually start is hidden initially, and stop shown?
|
|
315
|
+
# In populate_window: start_btn.hide() (line 235), stop_btn defaults?
|
|
316
|
+
# stop_btn is created.
|
|
317
|
+
# So assumed state is Playing? No, usually it starts, looped_animation matches.
|
|
318
|
+
# But if it starts playing, buttons should be disabled.
|
|
319
|
+
animation_buttons_box.addWidget(self.prev_frame_btn, 5, alignment=Qt.AlignRight)
|
|
320
|
+
|
|
235
321
|
animation_buttons_box.addWidget(self.stop_btn, 5, alignment=Qt.AlignRight)
|
|
236
322
|
animation_buttons_box.addWidget(self.start_btn, 5, alignment=Qt.AlignRight)
|
|
323
|
+
|
|
324
|
+
self.next_frame_btn = QPushButton()
|
|
325
|
+
self.next_frame_btn.clicked.connect(self.next_frame)
|
|
326
|
+
self.next_frame_btn.setIcon(icon(MDI6.chevron_right, color="black"))
|
|
327
|
+
self.next_frame_btn.setStyleSheet(self.button_select_all)
|
|
328
|
+
self.next_frame_btn.setFixedSize(QSize(60, 60))
|
|
329
|
+
self.next_frame_btn.setIconSize(QSize(30, 30))
|
|
330
|
+
self.next_frame_btn.setEnabled(False)
|
|
331
|
+
animation_buttons_box.addWidget(self.next_frame_btn, 5, alignment=Qt.AlignRight)
|
|
332
|
+
|
|
237
333
|
animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignRight)
|
|
238
334
|
|
|
239
335
|
self.right_panel.addLayout(animation_buttons_box, 5)
|
|
@@ -246,15 +342,13 @@ class EventAnnotator(BaseAnnotator):
|
|
|
246
342
|
self.contrast_slider.setSingleStep(0.001)
|
|
247
343
|
self.contrast_slider.setTickInterval(0.001)
|
|
248
344
|
self.contrast_slider.setOrientation(Qt.Horizontal)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
self.contrast_slider.setValue(
|
|
256
|
-
[np.nanpercentile(self.stack, 1), np.nanpercentile(self.stack, 99.99)]
|
|
257
|
-
)
|
|
345
|
+
# Cache percentile values to avoid recomputing on the full stack
|
|
346
|
+
self._stack_p_low = np.nanpercentile(self.stack, 0.001)
|
|
347
|
+
self._stack_p_high = np.nanpercentile(self.stack, 99.999)
|
|
348
|
+
self._stack_p1 = np.nanpercentile(self.stack, 1)
|
|
349
|
+
self._stack_p99 = np.nanpercentile(self.stack, 99.99)
|
|
350
|
+
self.contrast_slider.setRange(self._stack_p_low, self._stack_p_high)
|
|
351
|
+
self.contrast_slider.setValue([self._stack_p1, self._stack_p99])
|
|
258
352
|
self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
|
|
259
353
|
contrast_hbox.addWidget(QLabel("contrast: "))
|
|
260
354
|
contrast_hbox.addWidget(self.contrast_slider, 90)
|
|
@@ -263,10 +357,6 @@ class EventAnnotator(BaseAnnotator):
|
|
|
263
357
|
if self.class_choice_cb.currentText() != "":
|
|
264
358
|
self.compute_status_and_colors(0)
|
|
265
359
|
|
|
266
|
-
self.no_event_shortcut = QShortcut(QKeySequence("n"), self) # QKeySequence("s")
|
|
267
|
-
self.no_event_shortcut.activated.connect(self.shortcut_no_event)
|
|
268
|
-
self.no_event_shortcut.setEnabled(False)
|
|
269
|
-
|
|
270
360
|
QApplication.processEvents()
|
|
271
361
|
|
|
272
362
|
# Add Menu for Interactive Plotter
|
|
@@ -429,7 +519,10 @@ class EventAnnotator(BaseAnnotator):
|
|
|
429
519
|
self.enable_time_of_interest()
|
|
430
520
|
self.correct_btn.setText("submit")
|
|
431
521
|
|
|
432
|
-
|
|
522
|
+
try:
|
|
523
|
+
self.correct_btn.clicked.disconnect()
|
|
524
|
+
except TypeError:
|
|
525
|
+
pass # No connections to disconnect
|
|
433
526
|
self.correct_btn.clicked.connect(self.apply_modification)
|
|
434
527
|
|
|
435
528
|
def apply_modification(self):
|
|
@@ -441,8 +534,8 @@ class EventAnnotator(BaseAnnotator):
|
|
|
441
534
|
t0 = float(self.time_of_interest_le.text().replace(",", "."))
|
|
442
535
|
self.line_dt.set_xdata([t0, t0])
|
|
443
536
|
self.cell_fcanvas.canvas.draw_idle()
|
|
444
|
-
except
|
|
445
|
-
|
|
537
|
+
except ValueError:
|
|
538
|
+
# Invalid time value entered
|
|
446
539
|
t0 = -1
|
|
447
540
|
cclass = 2
|
|
448
541
|
elif self.no_event_btn.isChecked():
|
|
@@ -489,9 +582,11 @@ class EventAnnotator(BaseAnnotator):
|
|
|
489
582
|
self.extract_scatter_from_trajectories()
|
|
490
583
|
self.give_cell_information()
|
|
491
584
|
|
|
492
|
-
|
|
585
|
+
try:
|
|
586
|
+
self.correct_btn.clicked.disconnect()
|
|
587
|
+
except TypeError:
|
|
588
|
+
pass # No connections to disconnect
|
|
493
589
|
self.correct_btn.clicked.connect(self.show_annotation_buttons)
|
|
494
|
-
# self.cancel_btn.click()
|
|
495
590
|
|
|
496
591
|
self.hide_annotation_buttons()
|
|
497
592
|
self.correct_btn.setEnabled(False)
|
|
@@ -652,73 +747,25 @@ class EventAnnotator(BaseAnnotator):
|
|
|
652
747
|
self.cell_ax.set_ylim(self.value_magnitude, self.non_log_ymax)
|
|
653
748
|
|
|
654
749
|
def extract_scatter_from_trajectories(self):
|
|
655
|
-
|
|
750
|
+
"""Extract scatter data from trajectories using efficient groupby."""
|
|
656
751
|
self.positions = []
|
|
657
752
|
self.colors = []
|
|
658
753
|
self.tracks = []
|
|
659
754
|
|
|
660
|
-
for
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
self.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
# def load_annotator_config(self):
|
|
676
|
-
#
|
|
677
|
-
# """
|
|
678
|
-
# Load settings from config or set default values.
|
|
679
|
-
# """
|
|
680
|
-
#
|
|
681
|
-
# if os.path.exists(self.instructions_path):
|
|
682
|
-
# with open(self.instructions_path, 'r') as f:
|
|
683
|
-
#
|
|
684
|
-
# instructions = json.load(f)
|
|
685
|
-
#
|
|
686
|
-
# if 'rgb_mode' in instructions:
|
|
687
|
-
# self.rgb_mode = instructions['rgb_mode']
|
|
688
|
-
# else:
|
|
689
|
-
# self.rgb_mode = False
|
|
690
|
-
#
|
|
691
|
-
# if 'percentile_mode' in instructions:
|
|
692
|
-
# self.percentile_mode = instructions['percentile_mode']
|
|
693
|
-
# else:
|
|
694
|
-
# self.percentile_mode = True
|
|
695
|
-
#
|
|
696
|
-
# if 'channels' in instructions:
|
|
697
|
-
# self.target_channels = instructions['channels']
|
|
698
|
-
# else:
|
|
699
|
-
# self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
|
|
700
|
-
#
|
|
701
|
-
# if 'fraction' in instructions:
|
|
702
|
-
# self.fraction = float(instructions['fraction'])
|
|
703
|
-
# else:
|
|
704
|
-
# self.fraction = 0.25
|
|
705
|
-
#
|
|
706
|
-
# if 'interval' in instructions:
|
|
707
|
-
# self.anim_interval = int(instructions['interval'])
|
|
708
|
-
# else:
|
|
709
|
-
# self.anim_interval = 1
|
|
710
|
-
#
|
|
711
|
-
# if 'log' in instructions:
|
|
712
|
-
# self.log_option = instructions['log']
|
|
713
|
-
# else:
|
|
714
|
-
# self.log_option = False
|
|
715
|
-
# else:
|
|
716
|
-
# self.rgb_mode = False
|
|
717
|
-
# self.log_option = False
|
|
718
|
-
# self.percentile_mode = True
|
|
719
|
-
# self.target_channels = [[self.channel_names[0], 0.01, 99.99]]
|
|
720
|
-
# self.fraction = 0.25
|
|
721
|
-
# self.anim_interval = 1
|
|
755
|
+
# Pre-allocate with empty arrays for all frames
|
|
756
|
+
for _ in range(self.len_movie):
|
|
757
|
+
self.positions.append(np.empty((0, 2)))
|
|
758
|
+
self.colors.append(np.empty((0, 2), dtype=object))
|
|
759
|
+
self.tracks.append(np.empty(0))
|
|
760
|
+
|
|
761
|
+
# Use groupby for efficient extraction
|
|
762
|
+
grouped = self.df_tracks.groupby("FRAME")
|
|
763
|
+
for frame, group in grouped:
|
|
764
|
+
frame = int(frame)
|
|
765
|
+
if 0 <= frame < self.len_movie:
|
|
766
|
+
self.positions[frame] = group[["x_anim", "y_anim"]].to_numpy()
|
|
767
|
+
self.colors[frame] = group[["class_color", "status_color"]].to_numpy()
|
|
768
|
+
self.tracks[frame] = group["TRACK_ID"].to_numpy()
|
|
722
769
|
|
|
723
770
|
def prepare_stack(self, progress_callback=None):
|
|
724
771
|
|
|
@@ -808,6 +855,18 @@ class EventAnnotator(BaseAnnotator):
|
|
|
808
855
|
except:
|
|
809
856
|
pass
|
|
810
857
|
|
|
858
|
+
def animation_generator(self):
|
|
859
|
+
"""
|
|
860
|
+
Generator yielding frame indices for the animation,
|
|
861
|
+
starting from the current self.framedata.
|
|
862
|
+
"""
|
|
863
|
+
i = self.framedata
|
|
864
|
+
while True:
|
|
865
|
+
yield i
|
|
866
|
+
i += 1
|
|
867
|
+
if i >= self.len_movie:
|
|
868
|
+
i = 0
|
|
869
|
+
|
|
811
870
|
def looped_animation(self):
|
|
812
871
|
"""
|
|
813
872
|
Load an image.
|
|
@@ -849,12 +908,13 @@ class EventAnnotator(BaseAnnotator):
|
|
|
849
908
|
self.anim = FuncAnimation(
|
|
850
909
|
self.fig,
|
|
851
910
|
self.draw_frame,
|
|
852
|
-
frames=self.
|
|
911
|
+
frames=self.animation_generator, # Use generator to allow seamless restarts
|
|
853
912
|
interval=self.anim_interval, # in ms
|
|
854
913
|
blit=True,
|
|
914
|
+
cache_frame_data=False,
|
|
855
915
|
)
|
|
856
916
|
|
|
857
|
-
self.fig.canvas.mpl_connect("pick_event", self.on_scatter_pick)
|
|
917
|
+
self._pick_cid = self.fig.canvas.mpl_connect("pick_event", self.on_scatter_pick)
|
|
858
918
|
self.fcanvas.canvas.draw()
|
|
859
919
|
|
|
860
920
|
def select_single_cell(self, index, timepoint):
|
|
@@ -957,8 +1017,92 @@ class EventAnnotator(BaseAnnotator):
|
|
|
957
1017
|
self.stop_btn.hide()
|
|
958
1018
|
self.start_btn.show()
|
|
959
1019
|
self.anim.pause()
|
|
1020
|
+
self.prev_frame_btn.setEnabled(True)
|
|
1021
|
+
self.next_frame_btn.setEnabled(True)
|
|
960
1022
|
self.stop_btn.clicked.connect(self.start)
|
|
961
1023
|
|
|
1024
|
+
def start(self):
|
|
1025
|
+
"""
|
|
1026
|
+
Starts interactive animation.
|
|
1027
|
+
"""
|
|
1028
|
+
self.start_btn.hide()
|
|
1029
|
+
self.stop_btn.show()
|
|
1030
|
+
|
|
1031
|
+
self.prev_frame_btn.setEnabled(False)
|
|
1032
|
+
self.next_frame_btn.setEnabled(False)
|
|
1033
|
+
|
|
1034
|
+
self.anim.resume()
|
|
1035
|
+
self.stop_btn.clicked.connect(self.stop)
|
|
1036
|
+
|
|
1037
|
+
def next_frame(self):
|
|
1038
|
+
self.framedata += 1
|
|
1039
|
+
if self.framedata >= self.len_movie:
|
|
1040
|
+
self.framedata = 0
|
|
1041
|
+
self.draw_frame(self.framedata)
|
|
1042
|
+
self.fcanvas.canvas.draw()
|
|
1043
|
+
|
|
1044
|
+
def prev_frame(self):
|
|
1045
|
+
self.framedata -= 1
|
|
1046
|
+
if self.framedata < 0:
|
|
1047
|
+
self.framedata = self.len_movie - 1
|
|
1048
|
+
self.draw_frame(self.framedata)
|
|
1049
|
+
self.fcanvas.canvas.draw()
|
|
1050
|
+
|
|
1051
|
+
def toggle_animation(self):
|
|
1052
|
+
if self.stop_btn.isVisible():
|
|
1053
|
+
self.stop()
|
|
1054
|
+
else:
|
|
1055
|
+
self.start()
|
|
1056
|
+
|
|
1057
|
+
def update_speed(self):
|
|
1058
|
+
fps = self.speed_slider.value()
|
|
1059
|
+
# Convert FPS to interval in ms
|
|
1060
|
+
# FPS = 1000 / interval_ms => interval_ms = 1000 / FPS
|
|
1061
|
+
val = int(1000 / max(1, fps))
|
|
1062
|
+
self.anim_interval = val
|
|
1063
|
+
print(
|
|
1064
|
+
f"DEBUG: Speed slider moved. FPS: {fps} -> Interval: {val} ms. Recreating animation object."
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
# Check if animation is allowed to run (Pause button is visible means we are Playing)
|
|
1068
|
+
should_play = self.stop_btn.isVisible()
|
|
1069
|
+
|
|
1070
|
+
if hasattr(self, "anim") and self.anim:
|
|
1071
|
+
try:
|
|
1072
|
+
self.anim.event_source.stop()
|
|
1073
|
+
except Exception as e:
|
|
1074
|
+
print(f"DEBUG: Error stopping animation: {e}")
|
|
1075
|
+
|
|
1076
|
+
# Recreate animation with new interval
|
|
1077
|
+
try:
|
|
1078
|
+
# Disconnect the old pick event to avoid accumulating connections
|
|
1079
|
+
if hasattr(self, "_pick_cid"):
|
|
1080
|
+
try:
|
|
1081
|
+
self.fig.canvas.mpl_disconnect(self._pick_cid)
|
|
1082
|
+
except Exception:
|
|
1083
|
+
pass
|
|
1084
|
+
|
|
1085
|
+
self.anim = FuncAnimation(
|
|
1086
|
+
self.fig,
|
|
1087
|
+
self.draw_frame,
|
|
1088
|
+
frames=self.animation_generator,
|
|
1089
|
+
interval=self.anim_interval,
|
|
1090
|
+
blit=True,
|
|
1091
|
+
cache_frame_data=False,
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
# Reconnect pick event and store cid
|
|
1095
|
+
self._pick_cid = self.fig.canvas.mpl_connect(
|
|
1096
|
+
"pick_event", self.on_scatter_pick
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
# If we were NOT playing (i.e. Paused), pause the new animation immediately
|
|
1100
|
+
if not should_play:
|
|
1101
|
+
self.anim.event_source.stop()
|
|
1102
|
+
|
|
1103
|
+
except Exception as e:
|
|
1104
|
+
print(f"DEBUG: Error recreating animation: {e}")
|
|
1105
|
+
|
|
962
1106
|
def give_cell_information(self):
|
|
963
1107
|
|
|
964
1108
|
cell_selected = f"cell: {self.track_of_interest}\n"
|
|
@@ -982,56 +1126,22 @@ class EventAnnotator(BaseAnnotator):
|
|
|
982
1126
|
self.compute_status_and_colors(0)
|
|
983
1127
|
self.extract_scatter_from_trajectories()
|
|
984
1128
|
|
|
985
|
-
def set_last_frame(self):
|
|
986
|
-
|
|
987
|
-
self.last_frame_btn.setEnabled(False)
|
|
988
|
-
self.last_frame_btn.disconnect()
|
|
989
|
-
|
|
990
|
-
self.last_key = len(self.stack) - 1
|
|
991
|
-
while len(np.where(self.stack[self.last_key].flatten() == 0)[0]) > 0.99 * len(
|
|
992
|
-
self.stack[self.last_key].flatten()
|
|
993
|
-
):
|
|
994
|
-
self.last_key -= 1
|
|
995
|
-
self.anim._drawn_artists = self.draw_frame(self.last_key)
|
|
996
|
-
self.anim._drawn_artists = sorted(
|
|
997
|
-
self.anim._drawn_artists, key=lambda x: x.get_zorder()
|
|
998
|
-
)
|
|
999
|
-
for a in self.anim._drawn_artists:
|
|
1000
|
-
a.set_visible(True)
|
|
1001
|
-
|
|
1002
|
-
self.fig.canvas.draw()
|
|
1003
|
-
self.anim.event_source.stop()
|
|
1004
|
-
|
|
1005
|
-
# self.cell_plot.draw()
|
|
1006
|
-
self.stop_btn.hide()
|
|
1007
|
-
self.start_btn.show()
|
|
1008
|
-
self.stop_btn.clicked.connect(self.start)
|
|
1009
|
-
self.start_btn.setShortcut(QKeySequence("l"))
|
|
1010
|
-
|
|
1011
1129
|
def set_first_frame(self):
|
|
1130
|
+
self.stop()
|
|
1131
|
+
self.framedata = 0
|
|
1132
|
+
self.draw_frame(self.framedata)
|
|
1133
|
+
self.fcanvas.canvas.draw()
|
|
1012
1134
|
|
|
1013
|
-
|
|
1014
|
-
self.
|
|
1015
|
-
|
|
1016
|
-
self.
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
self.anim._drawn_artists, key=lambda x: x.get_zorder()
|
|
1024
|
-
)
|
|
1025
|
-
for a in self.anim._drawn_artists:
|
|
1026
|
-
a.set_visible(True)
|
|
1027
|
-
|
|
1028
|
-
self.fig.canvas.draw()
|
|
1029
|
-
self.anim.event_source.stop()
|
|
1030
|
-
|
|
1031
|
-
# self.cell_plot.draw()
|
|
1032
|
-
self.stop_btn.hide()
|
|
1033
|
-
self.start_btn.show()
|
|
1034
|
-
self.stop_btn.clicked.connect(self.start)
|
|
1035
|
-
self.start_btn.setShortcut(QKeySequence("f"))
|
|
1036
|
-
|
|
1135
|
+
def set_last_frame(self):
|
|
1136
|
+
self.stop()
|
|
1137
|
+
self.framedata = len(self.stack) - 1
|
|
1138
|
+
while len(np.where(self.stack[self.framedata].flatten() == 0)[0]) > 0.99 * len(
|
|
1139
|
+
self.stack[self.framedata].flatten()
|
|
1140
|
+
):
|
|
1141
|
+
self.framedata -= 1
|
|
1142
|
+
if self.framedata < 0:
|
|
1143
|
+
self.framedata = 0
|
|
1144
|
+
break
|
|
1037
1145
|
|
|
1146
|
+
self.draw_frame(self.framedata)
|
|
1147
|
+
self.fcanvas.canvas.draw()
|