celldetective 1.5.0b0__py3-none-any.whl → 1.5.0b2__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 (29) hide show
  1. celldetective/_version.py +1 -1
  2. celldetective/gui/InitWindow.py +26 -4
  3. celldetective/gui/base/components.py +20 -1
  4. celldetective/gui/base_annotator.py +11 -7
  5. celldetective/gui/event_annotator.py +51 -1060
  6. celldetective/gui/interactions_block.py +55 -25
  7. celldetective/gui/interactive_timeseries_viewer.py +11 -1
  8. celldetective/gui/measure_annotator.py +968 -0
  9. celldetective/gui/process_block.py +88 -34
  10. celldetective/gui/viewers/base_viewer.py +134 -3
  11. celldetective/gui/viewers/contour_viewer.py +4 -4
  12. celldetective/gui/workers.py +124 -10
  13. celldetective/measure.py +3 -0
  14. celldetective/napari/utils.py +29 -19
  15. celldetective/processes/downloader.py +122 -96
  16. celldetective/processes/load_table.py +55 -0
  17. celldetective/processes/measure_cells.py +107 -81
  18. celldetective/processes/track_cells.py +39 -39
  19. celldetective/segmentation.py +1 -1
  20. celldetective/tracking.py +9 -0
  21. celldetective/utils/data_loaders.py +21 -1
  22. celldetective/utils/downloaders.py +38 -0
  23. celldetective/utils/maths.py +14 -1
  24. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/METADATA +1 -1
  25. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/RECORD +29 -27
  26. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/WHEEL +0 -0
  27. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/entry_points.txt +0 -0
  28. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/licenses/LICENSE +0 -0
  29. {celldetective-1.5.0b0.dist-info → celldetective-1.5.0b2.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ from PyQt5.QtWidgets import (
9
9
  QHBoxLayout,
10
10
  QCheckBox,
11
11
  QMessageBox,
12
+ QApplication,
12
13
  )
13
14
  from PyQt5.QtCore import Qt, QSize, QTimer, QThread, pyqtSignal
14
15
  from superqt.fonticon import icon
@@ -30,13 +31,14 @@ from celldetective.gui.base.components import (
30
31
  CelldetectiveWidget,
31
32
  CelldetectiveProgressDialog,
32
33
  QHSeperationLine,
34
+ HoverButton,
33
35
  )
34
36
 
35
37
  import numpy as np
36
38
  from glob import glob
37
39
  from celldetective import get_logger
38
40
 
39
- logger = get_logger()
41
+ logger = get_logger("celldetective")
40
42
 
41
43
 
42
44
  class NapariLoaderThread(QThread):
@@ -345,11 +347,12 @@ class ProcessPanel(QFrame, Styles):
345
347
  self.refresh_signal_models()
346
348
  # self.to_disable.append(self.cell_models_list)
347
349
 
348
- self.train_signal_model_btn = QPushButton("TRAIN")
350
+ self.train_signal_model_btn = HoverButton(
351
+ "TRAIN", MDI6.redo_variant, "black", "white"
352
+ )
349
353
  self.train_signal_model_btn.setToolTip(
350
354
  "Train or retrain an event detection model\non newly annotated data."
351
355
  )
352
- self.train_signal_model_btn.setIcon(icon(MDI6.redo_variant, color="black"))
353
356
  self.train_signal_model_btn.setIconSize(QSize(20, 20))
354
357
  self.train_signal_model_btn.setStyleSheet(self.button_style_sheet_3)
355
358
  model_zoo_layout.addWidget(self.train_signal_model_btn, 5)
@@ -557,8 +560,7 @@ class ProcessPanel(QFrame, Styles):
557
560
  self.seg_model_list.setGeometry(50, 50, 200, 30)
558
561
  self.init_seg_model_list()
559
562
 
560
- self.upload_model_btn = QPushButton("UPLOAD")
561
- self.upload_model_btn.setIcon(icon(MDI6.upload, color="black"))
563
+ self.upload_model_btn = HoverButton("UPLOAD", MDI6.upload, "black", "white")
562
564
  self.upload_model_btn.setIconSize(QSize(20, 20))
563
565
  self.upload_model_btn.setStyleSheet(self.button_style_sheet_3)
564
566
  self.upload_model_btn.setToolTip(
@@ -568,11 +570,10 @@ class ProcessPanel(QFrame, Styles):
568
570
  self.upload_model_btn.clicked.connect(self.upload_segmentation_model)
569
571
  # self.to_disable.append(self.upload_tc_model)
570
572
 
571
- self.train_btn = QPushButton("TRAIN")
573
+ self.train_btn = HoverButton("TRAIN", MDI6.redo_variant, "black", "white")
572
574
  self.train_btn.setToolTip(
573
575
  "Train or retrain a segmentation model\non newly annotated data."
574
576
  )
575
- self.train_btn.setIcon(icon(MDI6.redo_variant, color="black"))
576
577
  self.train_btn.setIconSize(QSize(20, 20))
577
578
  self.train_btn.setStyleSheet(self.button_style_sheet_3)
578
579
  self.train_btn.clicked.connect(self.open_segmentation_model_config_ui)
@@ -865,7 +866,7 @@ class ProcessPanel(QFrame, Styles):
865
866
  self.signal_loader.start()
866
867
 
867
868
  def check_measurements(self):
868
- from celldetective.gui.event_annotator import MeasureAnnotator
869
+ from celldetective.gui.measure_annotator import MeasureAnnotator
869
870
 
870
871
  test = self.parent_window.locate_selected_position()
871
872
  if test:
@@ -1493,6 +1494,9 @@ class ProcessPanel(QFrame, Styles):
1493
1494
  window_title="Preparing the napari viewer...",
1494
1495
  )
1495
1496
 
1497
+ self.napari_progress.setAutoClose(False)
1498
+ self.napari_progress.setAutoReset(False)
1499
+
1496
1500
  self.napari_progress.setValue(0)
1497
1501
  self.napari_loader.progress.connect(self.napari_progress.setValue)
1498
1502
  self.napari_loader.status.connect(self.napari_progress.setLabelText)
@@ -1502,13 +1506,15 @@ class ProcessPanel(QFrame, Styles):
1502
1506
  from celldetective.napari.utils import launch_napari_viewer
1503
1507
 
1504
1508
  self.napari_progress.blockSignals(True)
1505
- self.napari_progress.close()
1509
+ # self.napari_progress.close()
1506
1510
  if self.napari_loader._is_cancelled:
1507
1511
  logger.info("Task was cancelled...")
1512
+ self.napari_progress.close()
1508
1513
  return
1509
1514
 
1510
1515
  if isinstance(result, Exception):
1511
1516
  logger.error(f"napari loading error: {result}")
1517
+ self.napari_progress.close()
1512
1518
  msgBox = QMessageBox()
1513
1519
  msgBox.setIcon(QMessageBox.Warning)
1514
1520
  msgBox.setText(str(result))
@@ -1519,13 +1525,33 @@ class ProcessPanel(QFrame, Styles):
1519
1525
 
1520
1526
  if result:
1521
1527
  logger.info("Launching the napari viewer with tracks...")
1528
+ self.napari_progress.setLabelText("Initializing Napari viewer...")
1529
+ self.napari_progress.setRange(0, 0)
1530
+ QApplication.processEvents()
1531
+
1532
+ def progress_cb(msg):
1533
+ if isinstance(msg, str):
1534
+ self.napari_progress.setLabelText(msg)
1535
+ QApplication.processEvents()
1536
+
1537
+ if "flush_memory" in result:
1538
+ result.pop("flush_memory")
1539
+
1522
1540
  try:
1523
- launch_napari_viewer(**result)
1541
+ launch_napari_viewer(
1542
+ **result,
1543
+ block=False,
1544
+ flush_memory=False,
1545
+ progress_callback=progress_cb,
1546
+ )
1524
1547
  logger.info("napari viewer was closed...")
1525
1548
  except Exception as e:
1526
1549
  logger.error(f"Failed to launch Napari: {e}")
1527
1550
  QMessageBox.warning(self, "Error", f"Failed to launch Napari: {e}")
1551
+ finally:
1552
+ self.napari_progress.close()
1528
1553
  else:
1554
+ self.napari_progress.close()
1529
1555
  logger.warning(
1530
1556
  "napari loading returned None (likely no trajectories found)."
1531
1557
  )
@@ -1540,33 +1566,61 @@ class ProcessPanel(QFrame, Styles):
1540
1566
 
1541
1567
  def view_table_ui(self):
1542
1568
  from celldetective.gui.tableUI import TableUI
1569
+ from celldetective.gui.workers import ProgressWindow
1570
+ from celldetective.processes.load_table import TableLoaderProcess
1543
1571
 
1544
1572
  logger.info("Load table...")
1545
- self.load_available_tables()
1546
1573
 
1547
- if self.df is not None:
1548
- plot_mode = "plot_track_signals"
1549
- if "TRACK_ID" not in list(self.df.columns):
1550
- plot_mode = "static"
1551
- self.tab_ui = TableUI(
1552
- self.df,
1553
- f"{self.parent_window.well_list.currentText()}; Position {self.parent_window.position_list.currentText()}",
1554
- population=self.mode,
1555
- plot_mode=plot_mode,
1556
- save_inplace_option=True,
1557
- )
1558
- self.tab_ui.show()
1559
- center_window(self.tab_ui)
1560
- else:
1561
- logger.info("Table could not be loaded...")
1562
- msgBox = QMessageBox()
1563
- msgBox.setIcon(QMessageBox.Warning)
1564
- msgBox.setText("No table could be loaded...")
1565
- msgBox.setWindowTitle("Info")
1566
- msgBox.setStandardButtons(QMessageBox.Ok)
1567
- returnValue = msgBox.exec()
1568
- if returnValue == QMessageBox.Ok:
1569
- return None
1574
+ # Prepare args for the process
1575
+ self.well_option = self.parent_window.well_list.getSelectedIndices()
1576
+ self.position_option = self.parent_window.position_list.getSelectedIndices()
1577
+
1578
+ process_args = {
1579
+ "experiment": self.exp_dir,
1580
+ "population": self.mode,
1581
+ "well_option": self.well_option,
1582
+ "position_option": self.position_option,
1583
+ "show_frame_progress": False,
1584
+ }
1585
+
1586
+ self.df = None
1587
+
1588
+ def on_table_loaded(df):
1589
+ self.df = df
1590
+ if self.df is not None:
1591
+ plot_mode = "plot_track_signals"
1592
+ if "TRACK_ID" not in list(self.df.columns):
1593
+ plot_mode = "static"
1594
+ self.tab_ui = TableUI(
1595
+ self.df,
1596
+ f"{self.parent_window.well_list.currentText()}; Position {self.parent_window.position_list.currentText()}",
1597
+ population=self.mode,
1598
+ plot_mode=plot_mode,
1599
+ save_inplace_option=True,
1600
+ )
1601
+ self.tab_ui.show()
1602
+ center_window(self.tab_ui)
1603
+ else:
1604
+ logger.info("Table could not be loaded...")
1605
+ msgBox = QMessageBox()
1606
+ msgBox.setIcon(QMessageBox.Warning)
1607
+ msgBox.setText("No table could be loaded...")
1608
+ msgBox.setWindowTitle("Info")
1609
+ msgBox.setStandardButtons(QMessageBox.Ok)
1610
+ returnValue = msgBox.exec()
1611
+
1612
+ self.job = ProgressWindow(
1613
+ TableLoaderProcess,
1614
+ parent_window=self,
1615
+ title="Loading tables...",
1616
+ process_args=process_args,
1617
+ position_info=False,
1618
+ well_label="Wells loaded:",
1619
+ pos_label="Positions loaded:",
1620
+ )
1621
+ self.job._ProgressWindow__runner.signals.result.connect(on_table_loaded)
1622
+
1623
+ result = self.job.exec_()
1570
1624
 
1571
1625
  def load_available_tables(self):
1572
1626
  """
@@ -1,7 +1,7 @@
1
1
  from collections import OrderedDict
2
2
 
3
3
  import numpy as np
4
- from PyQt5.QtCore import Qt
4
+ from PyQt5.QtCore import Qt, QThread, pyqtSignal, QMutex, QWaitCondition
5
5
  from PyQt5.QtWidgets import QHBoxLayout, QAction, QLabel, QComboBox
6
6
  from fonticon_mdi6 import MDI6
7
7
  from superqt import QLabeledDoubleRangeSlider, QLabeledSlider
@@ -9,11 +9,111 @@ from superqt.fonticon import icon
9
9
 
10
10
  from celldetective.gui.base.components import CelldetectiveWidget
11
11
  from celldetective.gui.base.utils import center_window
12
- from celldetective.utils.image_loaders import auto_load_number_of_frames, _get_img_num_per_channel, load_frames
12
+ from celldetective.utils.image_loaders import (
13
+ auto_load_number_of_frames,
14
+ _get_img_num_per_channel,
15
+ load_frames,
16
+ )
13
17
  from celldetective import get_logger
14
18
 
15
19
  logger = get_logger(__name__)
16
20
 
21
+
22
+ class StackLoader(QThread):
23
+ frame_loaded = pyqtSignal(int, int, np.ndarray) # channel, frame_idx, image
24
+
25
+ def __init__(self, stack_path, img_num_per_channel, n_channels):
26
+ super().__init__()
27
+ self.stack_path = stack_path
28
+ self.img_num_per_channel = img_num_per_channel
29
+ self.n_channels = n_channels
30
+ self.target_channel = 0
31
+ self.priority_frame = 0
32
+ self.cache_keys = set()
33
+ self.running = True
34
+ self.mutex = QMutex()
35
+ self.condition = QWaitCondition()
36
+
37
+ def update_priority(self, channel, frame, current_cache_keys):
38
+ self.mutex.lock()
39
+ self.target_channel = channel
40
+ self.priority_frame = frame
41
+ self.cache_keys = set(current_cache_keys)
42
+ self.condition.wakeAll()
43
+ self.mutex.unlock()
44
+
45
+ def stop(self):
46
+ self.running = False
47
+ self.condition.wakeAll()
48
+ self.wait()
49
+
50
+ def run(self):
51
+ while self.running:
52
+ self.mutex.lock()
53
+ if not self.running:
54
+ self.mutex.unlock()
55
+ break
56
+
57
+ t_ch = self.target_channel
58
+ p_frame = self.priority_frame
59
+ keys_snapshot = list(self.cache_keys)
60
+ self.mutex.unlock()
61
+
62
+ # Determine next frame to load
63
+ # Strategy: look around priority frame
64
+ frame_to_load = -1
65
+
66
+ # Search radius
67
+ radius = 10
68
+ found = False
69
+
70
+ # Check immediate neighbors first
71
+ check_order = [p_frame]
72
+ for r in range(1, radius + 1):
73
+ check_order.append(p_frame + r)
74
+ check_order.append(p_frame - r)
75
+
76
+ # Determine max frames
77
+ max_frames = self.img_num_per_channel.shape[1]
78
+
79
+ for f in check_order:
80
+ if 0 <= f < max_frames:
81
+ if (t_ch, f) not in keys_snapshot:
82
+ frame_to_load = f
83
+ found = True
84
+ break
85
+
86
+ if found:
87
+ try:
88
+ # Load the frame
89
+ from celldetective.utils.image_loaders import load_frames
90
+
91
+ img = load_frames(
92
+ self.img_num_per_channel[t_ch, frame_to_load],
93
+ self.stack_path,
94
+ normalize_input=False,
95
+ )[:, :, 0]
96
+
97
+ self.frame_loaded.emit(t_ch, frame_to_load, img)
98
+
99
+ # Update snapshot locally to avoid reloading immediately in next loop
100
+ self.mutex.lock()
101
+ self.cache_keys.add((t_ch, frame_to_load))
102
+ self.mutex.unlock()
103
+
104
+ except Exception as e:
105
+ pass
106
+ # logger.error(f"Error loading frame {frame_to_load}: {e}")
107
+ # Prepare to wait to avoid spin loop on error
108
+ self.msleep(100)
109
+
110
+ else:
111
+ # If nothing to load, wait
112
+ self.mutex.lock()
113
+ self.condition.wait(self.mutex, 500) # Wait 500ms or until new priority
114
+ self.mutex.unlock()
115
+
116
+
17
117
  class StackVisualizer(CelldetectiveWidget):
18
118
  """
19
119
  A widget for visualizing image stacks with interactive sliders and channel selection.
@@ -96,6 +196,7 @@ class StackVisualizer(CelldetectiveWidget):
96
196
  self._min = 0
97
197
  self._max = 0
98
198
 
199
+ self.loader_thread = None
99
200
  self.load_stack()
100
201
  self.generate_figure_canvas()
101
202
  if self.create_channel_cb:
@@ -113,7 +214,7 @@ class StackVisualizer(CelldetectiveWidget):
113
214
  self.is_drawing_line = False
114
215
  self.generate_custom_tools()
115
216
 
116
- self.canvas.layout.setContentsMargins(15, 15, 15, 30)
217
+ self.canvas.layout.setContentsMargins(15, 15, 15, 15)
117
218
 
118
219
  center_window(self)
119
220
 
@@ -464,6 +565,13 @@ class StackVisualizer(CelldetectiveWidget):
464
565
  np.arange(self.n_channels), self.stack_length, self.n_channels
465
566
  )
466
567
 
568
+ # Initialize background loader
569
+ self.loader_thread = StackLoader(
570
+ self.stack_path, self.img_num_per_channel, self.n_channels
571
+ )
572
+ self.loader_thread.frame_loaded.connect(self.on_frame_loaded)
573
+ self.loader_thread.start()
574
+
467
575
  self.init_frame = load_frames(
468
576
  self.img_num_per_channel[self.target_channel, self.mid_time],
469
577
  self.stack_path,
@@ -647,6 +755,12 @@ class StackVisualizer(CelldetectiveWidget):
647
755
 
648
756
  self.current_time_index = value
649
757
 
758
+ # Update loader priority
759
+ if self.mode == "virtual" and self.loader_thread:
760
+ self.loader_thread.update_priority(
761
+ self.target_channel, value, self.frame_cache.keys()
762
+ )
763
+
650
764
  if self.mode == "direct":
651
765
  self.init_frame = self.stack[value, :, :, self.target_channel]
652
766
 
@@ -693,8 +807,25 @@ class StackVisualizer(CelldetectiveWidget):
693
807
  self.canvas.canvas.draw_idle()
694
808
  self.update_profile()
695
809
 
810
+ def on_frame_loaded(self, channel, frame, image):
811
+ """Callback from loader thread"""
812
+ # Store in cache
813
+ cache_key = (channel, frame)
814
+ if cache_key not in self.frame_cache:
815
+ self.frame_cache[cache_key] = image
816
+ if len(self.frame_cache) > self.max_cache_size:
817
+ self.frame_cache.popitem(last=False)
818
+
819
+ # If this is the current frame (user might have scrolled while loading), update display?
820
+ # Usually change_frame handles display. If we are waiting for this frame, we might want to refresh.
821
+ if channel == self.target_channel and frame == self.current_time_index:
822
+ # Refresh
823
+ self.change_frame(self.current_time_index)
824
+
696
825
  def closeEvent(self, event):
697
826
  # Event handler for closing the widget
827
+ if self.loader_thread:
828
+ self.loader_thread.stop()
698
829
  if hasattr(self, "frame_cache") and isinstance(self.frame_cache, OrderedDict):
699
830
  self.frame_cache.clear()
700
831
  self.canvas.close()
@@ -118,10 +118,10 @@ class CellEdgeVisualizer(StackVisualizer):
118
118
  ), "Wrong dimensions for the provided labels, expect TXY"
119
119
  assert len(self.labels) == self.stack_length
120
120
 
121
- self.mode = "direct"
121
+ self.label_mode = "direct"
122
122
  self.init_label = self.labels[self.mid_time, :, :]
123
123
  else:
124
- self.mode = "virtual"
124
+ self.label_mode = "virtual"
125
125
  assert isinstance(self.stack_path, str)
126
126
  assert self.stack_path.endswith(".tif")
127
127
  self.locate_labels_virtual()
@@ -292,7 +292,7 @@ class CellEdgeVisualizer(StackVisualizer):
292
292
  self.sdf_cache.move_to_end(value)
293
293
  else:
294
294
  # Cache Miss: Load Label
295
- if self.mode == "virtual":
295
+ if self.label_mode == "virtual":
296
296
  if hasattr(self, "label_map") and value in self.label_map:
297
297
  try:
298
298
  self.init_label = imread(self.label_map[value])
@@ -300,7 +300,7 @@ class CellEdgeVisualizer(StackVisualizer):
300
300
  self.init_label = np.zeros_like(self.init_frame)
301
301
  else:
302
302
  self.init_label = np.zeros_like(self.init_frame)
303
- elif self.mode == "direct":
303
+ elif self.label_mode == "direct":
304
304
  if value < len(self.labels):
305
305
  self.init_label = self.labels[value, :, :]
306
306
  else:
@@ -21,6 +21,8 @@ class ProgressWindow(CelldetectiveDialog):
21
21
  title="",
22
22
  position_info=True,
23
23
  process_args=None,
24
+ well_label="Well progress:",
25
+ pos_label="Position progress:",
24
26
  ):
25
27
 
26
28
  super().__init__()
@@ -41,20 +43,26 @@ class ProgressWindow(CelldetectiveDialog):
41
43
  self.__label = QLabel("Idle")
42
44
  self.time_left_lbl = QLabel("")
43
45
 
44
- self.well_time_lbl = QLabel("Well progress:")
46
+ self.well_time_lbl = QLabel(well_label)
45
47
  self.well_progress_bar = QProgressBar()
46
48
  self.well_progress_bar.setValue(0)
47
49
  self.well_progress_bar.setFormat("Total (Wells): %p%")
48
50
 
49
- self.pos_time_lbl = QLabel("Position progress:")
51
+ self.pos_time_lbl = QLabel(pos_label)
50
52
  self.pos_progress_bar = QProgressBar()
51
53
  self.pos_progress_bar.setValue(0)
52
54
  self.pos_progress_bar.setFormat("Current Well (Positions): %p%")
53
55
 
54
- self.frame_time_lbl = QLabel("Frame progress:")
55
- self.frame_progress_bar = QProgressBar()
56
- self.frame_progress_bar.setValue(0)
57
- self.frame_progress_bar.setFormat("Current Position (Frames): %p%")
56
+ if "show_frame_progress" in process_args:
57
+ self.show_frame_progress = process_args["show_frame_progress"]
58
+ else:
59
+ self.show_frame_progress = True
60
+
61
+ if self.show_frame_progress:
62
+ self.frame_time_lbl = QLabel("Frame progress:")
63
+ self.frame_progress_bar = QProgressBar()
64
+ self.frame_progress_bar.setValue(0)
65
+ self.frame_progress_bar.setFormat("Current Position (Frames): %p%")
58
66
 
59
67
  self.__runner = Runner(
60
68
  process=self.__process,
@@ -73,8 +81,10 @@ class ProgressWindow(CelldetectiveDialog):
73
81
  self.__runner.signals.update_pos.connect(self.pos_progress_bar.setValue)
74
82
  self.__runner.signals.update_pos_time.connect(self.pos_time_lbl.setText)
75
83
 
76
- self.__runner.signals.update_frame.connect(self.frame_progress_bar.setValue)
77
- self.__runner.signals.update_frame_time.connect(self.frame_time_lbl.setText)
84
+ if self.show_frame_progress:
85
+ self.__runner.signals.update_frame.connect(self.frame_progress_bar.setValue)
86
+ self.__runner.signals.update_frame_time.connect(self.frame_time_lbl.setText)
87
+
78
88
  self.__runner.signals.update_status.connect(self.__label.setText)
79
89
  self.__runner.signals.update_image.connect(self.update_image)
80
90
 
@@ -96,8 +106,9 @@ class ProgressWindow(CelldetectiveDialog):
96
106
  self.progress_layout.addWidget(self.pos_time_lbl)
97
107
  self.progress_layout.addWidget(self.pos_progress_bar)
98
108
 
99
- self.progress_layout.addWidget(self.frame_time_lbl)
100
- self.progress_layout.addWidget(self.frame_progress_bar)
109
+ if self.show_frame_progress:
110
+ self.progress_layout.addWidget(self.frame_time_lbl)
111
+ self.progress_layout.addWidget(self.frame_progress_bar)
101
112
 
102
113
  self.btn_layout = QHBoxLayout()
103
114
  self.btn_layout.addWidget(self.__btn_stp)
@@ -266,6 +277,9 @@ class Runner(QRunnable):
266
277
  if "training_result" in data:
267
278
  self.signals.training_result.emit(data["training_result"])
268
279
 
280
+ if "result" in data:
281
+ self.signals.result.emit(data["result"])
282
+
269
283
  if "status" in data: # Moved this block out of frame_time check
270
284
  logger.info(
271
285
  f"Runner received status: {data['status']}"
@@ -312,7 +326,107 @@ class RunnerSignal(QObject):
312
326
  update_image = pyqtSignal(object)
313
327
  update_plot = pyqtSignal(dict)
314
328
  training_result = pyqtSignal(dict)
329
+ result = pyqtSignal(object)
315
330
  update_status = pyqtSignal(str)
316
331
 
317
332
  finished = pyqtSignal()
318
333
  error = pyqtSignal(str)
334
+
335
+
336
+ class GenericProgressWindow(CelldetectiveDialog):
337
+
338
+ def __init__(
339
+ self,
340
+ process=None,
341
+ parent_window=None,
342
+ title="",
343
+ process_args=None,
344
+ label_text="Progress:",
345
+ ):
346
+
347
+ super().__init__()
348
+
349
+ self.setWindowTitle(f"{title}")
350
+ self.__process = process
351
+ self.parent_window = parent_window
352
+
353
+ self.__btn_stp = QPushButton("Cancel")
354
+ self.__label = QLabel("Idle")
355
+ self.progress_label = QLabel(label_text)
356
+
357
+ self.progress_bar = QProgressBar()
358
+ self.progress_bar.setValue(0)
359
+ self.progress_bar.setFormat("%p%")
360
+
361
+ self.__runner = Runner(
362
+ process=self.__process,
363
+ process_args=process_args,
364
+ )
365
+ logger.info("Runner initialized...")
366
+ self.pool = QThreadPool.globalInstance()
367
+
368
+ self.__btn_stp.clicked.connect(self.__stp_net)
369
+ self.__runner.signals.finished.connect(self.__on_finished)
370
+ self.__runner.signals.error.connect(self.__on_error)
371
+ self.__runner.signals.update_status.connect(self.__label.setText)
372
+
373
+ # Connect update_pos for generic progress (Runner maps generic list progress to update_pos)
374
+ self.__runner.signals.update_pos.connect(self.progress_bar.setValue)
375
+
376
+ self.__btn_stp.setDisabled(True)
377
+
378
+ self.layout = QVBoxLayout()
379
+ self.layout.addWidget(self.progress_label)
380
+ self.layout.addWidget(self.progress_bar)
381
+
382
+ self.btn_layout = QHBoxLayout()
383
+ self.btn_layout.addWidget(self.__btn_stp)
384
+ self.btn_layout.addWidget(self.__label)
385
+
386
+ self.layout.addLayout(self.btn_layout)
387
+
388
+ self.setLayout(self.layout)
389
+ self.setFixedSize(QSize(400, 150))
390
+ self.show()
391
+ self.raise_()
392
+ self.activateWindow()
393
+ logger.info("GenericProgressWindow initialized and shown.")
394
+ self.__run_net()
395
+ self.setModal(True)
396
+
397
+ def closeEvent(self, evnt):
398
+ evnt.ignore()
399
+ self.setWindowState(Qt.WindowMinimized)
400
+
401
+ def __run_net(self):
402
+ self.__btn_stp.setEnabled(True)
403
+ self.__label.setText("Running...")
404
+ self.pool.start(self.__runner)
405
+
406
+ def __stp_net(self):
407
+ self.__runner.close()
408
+ logger.info("\n Job cancelled... Abort.")
409
+ self.reject()
410
+
411
+ def __on_finished(self):
412
+ self.__btn_stp.setDisabled(True)
413
+ self.__label.setText("\nFinished!")
414
+ self.__runner.close()
415
+ self.accept()
416
+
417
+ def __on_error(self, message="Error"):
418
+ self.__btn_stp.setDisabled(True)
419
+ self.__label.setText("\nError")
420
+ self.__runner.close()
421
+
422
+ # Show error in a message box to ensure it's seen
423
+ from PyQt5.QtWidgets import QMessageBox
424
+
425
+ msg = QMessageBox()
426
+ msg.setIcon(QMessageBox.Critical)
427
+ msg.setText("Process failed")
428
+ msg.setInformativeText(str(message))
429
+ msg.setWindowTitle("Error")
430
+ msg.exec_()
431
+
432
+ self.reject()
celldetective/measure.py CHANGED
@@ -5,6 +5,7 @@ import subprocess
5
5
  from math import ceil
6
6
  from functools import reduce
7
7
  from inspect import getmembers, isfunction
8
+ from celldetective.gui.base.utils import pretty_table
8
9
 
9
10
  from celldetective.exceptions import EmptyQueryError, MissingColumnsError, QueryError
10
11
  from celldetective.utils.masks import (
@@ -1894,6 +1895,8 @@ def measure_radial_distance_to_center(
1894
1895
  ):
1895
1896
 
1896
1897
  try:
1898
+ df[column_labels["x"]] = df[column_labels["x"]].astype(float)
1899
+ df[column_labels["y"]] = df[column_labels["y"]].astype(float)
1897
1900
  df["radial_distance"] = np.sqrt(
1898
1901
  (df[column_labels["x"]] - volume[0] / 2) ** 2
1899
1902
  + (df[column_labels["y"]] - volume[1] / 2) ** 2