celldetective 1.5.0b1__py3-none-any.whl → 1.5.0b3__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 (35) hide show
  1. celldetective/_version.py +1 -1
  2. celldetective/gui/InitWindow.py +51 -12
  3. celldetective/gui/base/components.py +22 -1
  4. celldetective/gui/base_annotator.py +20 -9
  5. celldetective/gui/control_panel.py +21 -16
  6. celldetective/gui/event_annotator.py +51 -1060
  7. celldetective/gui/gui_utils.py +14 -5
  8. celldetective/gui/interactions_block.py +55 -25
  9. celldetective/gui/interactive_timeseries_viewer.py +11 -1
  10. celldetective/gui/measure_annotator.py +1064 -0
  11. celldetective/gui/plot_measurements.py +2 -4
  12. celldetective/gui/plot_signals_ui.py +3 -4
  13. celldetective/gui/process_block.py +298 -72
  14. celldetective/gui/viewers/base_viewer.py +134 -3
  15. celldetective/gui/viewers/contour_viewer.py +4 -4
  16. celldetective/gui/workers.py +25 -10
  17. celldetective/measure.py +3 -0
  18. celldetective/napari/utils.py +29 -19
  19. celldetective/processes/load_table.py +55 -0
  20. celldetective/processes/measure_cells.py +107 -81
  21. celldetective/processes/track_cells.py +39 -39
  22. celldetective/segmentation.py +1 -1
  23. celldetective/tracking.py +9 -0
  24. celldetective/utils/data_loaders.py +21 -1
  25. celldetective/utils/image_loaders.py +3 -0
  26. celldetective/utils/masks.py +1 -1
  27. celldetective/utils/maths.py +14 -1
  28. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/METADATA +1 -1
  29. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/RECORD +35 -32
  30. tests/gui/test_enhancements.py +351 -0
  31. tests/test_notebooks.py +2 -1
  32. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/WHEEL +0 -0
  33. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/entry_points.txt +0 -0
  34. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/licenses/LICENSE +0 -0
  35. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/top_level.txt +0 -0
@@ -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,6 +326,7 @@ 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()
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
@@ -27,6 +27,7 @@ from celldetective.utils.experiment import (
27
27
  )
28
28
  from celldetective.utils.parsing import config_section_to_dict
29
29
  from celldetective import get_logger
30
+ from celldetective.gui.base.styles import Styles
30
31
 
31
32
  logger = get_logger()
32
33
 
@@ -87,10 +88,16 @@ def control_tracks(
87
88
  position += os.sep
88
89
 
89
90
  position = position.replace("\\", "/")
91
+ if progress_callback:
92
+ progress_callback(0)
93
+
90
94
  stack, labels = locate_stack_and_labels(
91
95
  position, prefix=prefix, population=population
92
96
  )
93
97
 
98
+ if progress_callback:
99
+ progress_callback(25)
100
+
94
101
  return view_tracks_in_napari(
95
102
  position,
96
103
  population,
@@ -130,7 +137,13 @@ def view_tracks_in_napari(
130
137
  Updated
131
138
  """
132
139
 
140
+ print(f"DEBUG: view_tracks_in_napari called with pos={position}, pop={population}")
133
141
  df, df_path = get_position_table(position, population=population, return_path=True)
142
+ print(f"DEBUG: get_position_table returned df={df is not None}")
143
+
144
+ if progress_callback:
145
+ progress_callback(50)
146
+
134
147
  if df is None:
135
148
  print("Please compute trajectories first... Abort...")
136
149
  return None
@@ -144,12 +157,18 @@ def view_tracks_in_napari(
144
157
 
145
158
  if (labels is not None) * relabel:
146
159
  print("Replacing the cell mask labels with the track ID...")
160
+
161
+ def wrapped_callback(p):
162
+ if progress_callback:
163
+ return progress_callback(50 + int(p * 0.5))
164
+ return True
165
+
147
166
  labels = relabel_segmentation(
148
167
  labels,
149
168
  df,
150
169
  exclude_nans=True,
151
170
  threads=threads,
152
- progress_callback=progress_callback,
171
+ progress_callback=wrapped_callback,
153
172
  )
154
173
  if labels is None:
155
174
  return None
@@ -186,9 +205,12 @@ def launch_napari_viewer(
186
205
  shared_data,
187
206
  contrast_limits,
188
207
  flush_memory=True,
208
+ block=True,
209
+ progress_callback=None,
189
210
  ):
190
211
 
191
212
  viewer = napari.Viewer()
213
+
192
214
  if stack is not None:
193
215
  viewer.add_image(
194
216
  stack,
@@ -196,6 +218,7 @@ def launch_napari_viewer(
196
218
  colormap=["gray"] * stack.shape[-1],
197
219
  contrast_limits=contrast_limits,
198
220
  )
221
+
199
222
  if labels is not None:
200
223
  labels_layer = viewer.add_labels(
201
224
  labels.astype(int), name="segmentation", opacity=0.4
@@ -274,8 +297,6 @@ def launch_napari_viewer(
274
297
  def export_table_widget():
275
298
  return export_modifications()
276
299
 
277
- from celldetective.gui.base.styles import Styles
278
-
279
300
  export_table_widget.native.setStyleSheet(Styles().button_style_sheet)
280
301
 
281
302
  def label_changed(event):
@@ -426,9 +447,9 @@ def launch_napari_viewer(
426
447
 
427
448
  shared_data["df"] = df
428
449
 
429
- viewer.show(block=True)
450
+ viewer.show(block=block)
430
451
 
431
- if flush_memory:
452
+ if flush_memory and block:
432
453
 
433
454
  # temporary fix for slight napari memory leak
434
455
  for i in range(10000):
@@ -781,8 +802,6 @@ def control_segmentation_napari(
781
802
  def export_widget():
782
803
  return export_annotation()
783
804
 
784
- from celldetective.gui.base.styles import Styles
785
-
786
805
  stack, labels = locate_stack_and_labels(
787
806
  position, prefix=prefix, population=population
788
807
  )
@@ -889,22 +908,13 @@ def correct_annotation(filename):
889
908
  stack,
890
909
  channel_axis=-1,
891
910
  colormap=["gray"] * stack.shape[-1],
892
- constrast_limits=contrast_limits,
911
+ contrast_limits=contrast_limits,
893
912
  )
894
913
  viewer.add_labels(labels, name="segmentation", opacity=0.4)
895
914
  viewer.window.add_dock_widget(save_widget, area="right")
896
- viewer.show(block=True)
915
+ save_widget.native.setStyleSheet(Styles().button_style_sheet)
897
916
 
898
- # temporary fix for slight napari memory leak
899
- for i in range(100):
900
- try:
901
- viewer.layers.pop()
902
- except:
903
- pass
904
- del viewer
905
- del stack
906
- del labels
907
- gc.collect()
917
+ viewer.show(block=False)
908
918
 
909
919
 
910
920
  def _view_on_napari(tracks=None, stack=None, labels=None):
@@ -0,0 +1,55 @@
1
+ import time
2
+ from multiprocessing import Process
3
+ from celldetective.utils.data_loaders import load_experiment_tables
4
+ from celldetective import get_logger
5
+
6
+ logger = get_logger()
7
+
8
+
9
+ class TableLoaderProcess(Process):
10
+
11
+ def __init__(self, queue=None, process_args=None, *args, **kwargs):
12
+
13
+ super().__init__(*args, **kwargs)
14
+
15
+ if process_args is not None:
16
+ for key, value in process_args.items():
17
+ setattr(self, key, value)
18
+
19
+ self.queue = queue
20
+
21
+ def run(self):
22
+
23
+ def progress(well_progress, pos_progress):
24
+ # Check for cancellation if needed?
25
+ # The runner checks queue for instructions? No, runner closes queue.
26
+ # But here we can just push updates.
27
+ self.queue.put(
28
+ {
29
+ "well_progress": well_progress,
30
+ "pos_progress": pos_progress,
31
+ "status": f"Loading tables... Well {well_progress}%, Position {pos_progress}%",
32
+ }
33
+ )
34
+ return True # continue
35
+
36
+ try:
37
+ self.queue.put({"status": "Started loading..."})
38
+
39
+ df = load_experiment_tables(
40
+ experiment=self.experiment,
41
+ population=self.population,
42
+ well_option=self.well_option,
43
+ position_option=self.position_option,
44
+ return_pos_info=False,
45
+ progress_callback=progress,
46
+ )
47
+
48
+ self.queue.put({"status": "finished", "result": df})
49
+
50
+ except Exception as e:
51
+ logger.error(f"Table loading failed: {e}")
52
+ self.queue.put({"status": "error", "message": str(e)})
53
+
54
+ def end_process(self):
55
+ self.terminate()