celldetective 1.5.0b3__py3-none-any.whl → 1.5.0b5__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "1.5.0b3"
1
+ __version__ = "1.5.0b5"
@@ -22,19 +22,33 @@ class FigureCanvas(CelldetectiveWidget):
22
22
  from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT
23
23
 
24
24
  self.toolbar = NavigationToolbar2QT(self.canvas)
25
+ self.toolbar.setStyleSheet(
26
+ "QToolButton:hover {background-color: lightgray;} QToolButton {background-color: transparent; border: none;}"
27
+ )
25
28
  self.layout = QVBoxLayout(self)
26
29
  self.layout.addWidget(self.canvas, 90)
27
30
  if interactive:
28
31
  self.layout.addWidget(self.toolbar)
29
32
 
30
- #center_window(self)
33
+ self.manual_layout = False
34
+ # center_window(self)
31
35
  self.setAttribute(Qt.WA_DeleteOnClose)
32
36
 
33
37
  def resizeEvent(self, event):
34
-
38
+ print("DEBUG: resizeEvent called")
35
39
  super().resizeEvent(event)
36
40
  try:
37
- self.fig.tight_layout()
41
+ manual_layout = getattr(self, "manual_layout", False)
42
+
43
+ # Double check for profile axes manually (robust fallback)
44
+ if not manual_layout and hasattr(self.fig, "axes"):
45
+ for ax in self.fig.axes:
46
+ if ax.get_label() == "profile_axes":
47
+ manual_layout = True
48
+ break
49
+
50
+ if not manual_layout:
51
+ self.fig.tight_layout()
38
52
  except:
39
53
  pass
40
54
 
@@ -950,9 +950,9 @@ class MeasureAnnotator(BaseAnnotator):
950
950
  ].to_numpy()
951
951
  )
952
952
  self.colors.append(
953
- self.df_tracks.loc[
954
- self.df_tracks["FRAME"] == t, ["group_color"]
955
- ].to_numpy()
953
+ self.df_tracks.loc[self.df_tracks["FRAME"] == t, ["group_color"]]
954
+ .to_numpy()
955
+ .copy()
956
956
  )
957
957
  if "TRACK_ID" in self.df_tracks.columns:
958
958
  self.tracks.append(
@@ -84,6 +84,12 @@ class SettingsSegmentationModelTraining(CelldetectiveSettingsPanel):
84
84
  self.bg_loader = BackgroundLoader()
85
85
  self.bg_loader.start()
86
86
 
87
+ def closeEvent(self, event):
88
+ if self.bg_loader.isRunning():
89
+ logger.info("Waiting for background loader to finish...")
90
+ self.bg_loader.wait()
91
+ super().closeEvent(event)
92
+
87
93
  def _add_to_layout(self):
88
94
 
89
95
  self._layout.addWidget(self.model_frame)
@@ -111,6 +111,11 @@ class StackLoader(QThread):
111
111
  # If nothing to load, wait
112
112
  self.mutex.lock()
113
113
  self.condition.wait(self.mutex, 500) # Wait 500ms or until new priority
114
+
115
+ if not self.running:
116
+ self.mutex.unlock()
117
+ break
118
+
114
119
  self.mutex.unlock()
115
120
 
116
121
 
@@ -280,6 +285,10 @@ class StackVisualizer(CelldetectiveWidget):
280
285
  self.lock_y_action.setEnabled(True)
281
286
  self.canvas.toolbar.mode = ""
282
287
 
288
+ # Enable manual layout control to prevent tight_layout interference
289
+ if hasattr(self.canvas, "manual_layout"):
290
+ self.canvas.manual_layout = True
291
+
283
292
  # Connect events
284
293
  self.cid_press = self.fig.canvas.mpl_connect(
285
294
  "button_press_event", self.on_line_press
@@ -292,8 +301,8 @@ class StackVisualizer(CelldetectiveWidget):
292
301
  )
293
302
 
294
303
  # Save original position if not saved
295
- if not hasattr(self, "ax_original_pos"):
296
- self.ax_original_pos = self.ax.get_position()
304
+ # if not hasattr(self, "ax_original_pos"):
305
+ # self.ax_original_pos = self.ax.get_position()
297
306
 
298
307
  # Disable tight_layout/layout engine to prevent fighting manual positioning
299
308
  if hasattr(self.fig, "set_layout_engine"):
@@ -329,6 +338,7 @@ class StackVisualizer(CelldetectiveWidget):
329
338
  self.ax_profile.set_position(gs[1].get_position(self.fig))
330
339
 
331
340
  self.ax_profile.set_visible(True)
341
+ self.ax_profile.set_label("profile_axes")
332
342
  self.ax_profile.set_facecolor("none")
333
343
  self.ax_profile.tick_params(axis="y", which="major", labelsize=8)
334
344
  self.ax_profile.set_xticks([])
@@ -341,11 +351,40 @@ class StackVisualizer(CelldetectiveWidget):
341
351
  self.ax_profile.spines["bottom"].set_color("black")
342
352
  self.ax_profile.spines["left"].set_color("black")
343
353
 
354
+ # Update Toolbar Home State to match new layout BUT with full field of view
355
+ # 1. Save current zoom
356
+ current_xlim = self.ax.get_xlim()
357
+ current_ylim = self.ax.get_ylim()
358
+
359
+ # 2. Set limits to full extent (Home State)
360
+ if hasattr(self, "im"):
361
+ extent = self.im.get_extent() # (left, right, bottom, top) or similar
362
+ self.ax.set_xlim(extent[0], extent[1])
363
+ self.ax.set_ylim(extent[2], extent[3])
364
+
365
+ # 3. Reset Stack and save Home
366
+ self.canvas.toolbar._nav_stack.clear()
367
+ self.canvas.toolbar.push_current()
368
+
369
+ # 4. Restore User Zoom
370
+ self.ax.set_xlim(current_xlim)
371
+ self.ax.set_ylim(current_ylim)
372
+
373
+ # 5. Push restored zoom state so "Back"/"Forward" logic works from here?
374
+ # Actually, if we just restore, we are "live" at a new state.
375
+ # If we don't push, "Home" works. "Back" might not exist yet. That's fine.
376
+ self.canvas.toolbar.push_current()
377
+
344
378
  self.canvas.draw()
345
379
  else:
346
380
  self.line_mode = False
347
381
  self.lock_y_action.setChecked(False)
348
382
  self.lock_y_action.setEnabled(False)
383
+
384
+ # Disable manual layout control
385
+ if hasattr(self.canvas, "manual_layout"):
386
+ self.canvas.manual_layout = False
387
+
349
388
  # Disconnect events
350
389
  if hasattr(self, "cid_press"):
351
390
  self.fig.canvas.mpl_disconnect(self.cid_press)
@@ -367,17 +406,24 @@ class StackVisualizer(CelldetectiveWidget):
367
406
  self.ax_profile = None
368
407
 
369
408
  # Restore original layout
370
- if hasattr(self, "ax_original_pos"):
371
- # standard 1x1 GridSpec or manual restore
372
- import matplotlib.gridspec as gridspec
373
-
374
- gs = gridspec.GridSpec(1, 1)
375
- self.ax.set_subplotspec(gs[0])
376
- self.ax.set_position(gs[0].get_position(self.fig))
377
- self.fig.subplots_adjust(
378
- top=1, bottom=0, right=1, left=0, hspace=0, wspace=0
379
- )
380
- # self.ax.set_position(self.ax_original_pos) # tight layout should fix it
409
+ # if hasattr(self, "ax_original_pos"):
410
+ # standard 1x1 GridSpec or manual restore
411
+ import matplotlib.gridspec as gridspec
412
+
413
+ gs = gridspec.GridSpec(1, 1)
414
+ self.ax.set_subplotspec(gs[0])
415
+ # self.ax.set_position(gs[0].get_position(self.fig))
416
+ self.fig.subplots_adjust(
417
+ top=1, bottom=0, right=1, left=0, hspace=0, wspace=0
418
+ )
419
+ # self.ax.set_position(self.ax_original_pos) # tight layout should fix it
420
+
421
+ # Re-enable tight_layout via standard resize event later or explicit call
422
+ self.fig.tight_layout()
423
+
424
+ # Reset Toolbar Stack for Standard View
425
+ self.canvas.toolbar._nav_stack.clear()
426
+ self.canvas.toolbar.push_current()
381
427
 
382
428
  self.canvas.draw()
383
429
  self.info_lbl.setText("")
@@ -548,7 +594,10 @@ class StackVisualizer(CelldetectiveWidget):
548
594
  self.mode = "direct"
549
595
  self.stack_length = len(self.stack)
550
596
  self.mid_time = self.stack_length // 2
551
- self.init_frame = self.stack[self.mid_time, :, :, self.target_channel]
597
+ self.current_time_index = 0
598
+ self.init_frame = self.stack[
599
+ self.current_time_index, :, :, self.target_channel
600
+ ]
552
601
  self.last_frame = self.stack[-1, :, :, self.target_channel]
553
602
  else:
554
603
  self.mode = "virtual"
@@ -561,6 +610,7 @@ class StackVisualizer(CelldetectiveWidget):
561
610
 
562
611
  self.stack_length = auto_load_number_of_frames(self.stack_path)
563
612
  self.mid_time = self.stack_length // 2
613
+ self.current_time_index = 0
564
614
  self.img_num_per_channel = _get_img_num_per_channel(
565
615
  np.arange(self.n_channels), self.stack_length, self.n_channels
566
616
  )
@@ -573,7 +623,7 @@ class StackVisualizer(CelldetectiveWidget):
573
623
  self.loader_thread.start()
574
624
 
575
625
  self.init_frame = load_frames(
576
- self.img_num_per_channel[self.target_channel, self.mid_time],
626
+ self.img_num_per_channel[self.target_channel, self.current_time_index],
577
627
  self.stack_path,
578
628
  normalize_input=False,
579
629
  )[:, :, 0]
@@ -708,7 +758,7 @@ class StackVisualizer(CelldetectiveWidget):
708
758
  layout = QHBoxLayout()
709
759
  self.frame_slider = QLabeledSlider(Qt.Horizontal)
710
760
  self.frame_slider.setRange(0, self.stack_length - 1)
711
- self.frame_slider.setValue(self.mid_time)
761
+ self.frame_slider.setValue(self.current_time_index)
712
762
  self.frame_slider.valueChanged.connect(self.change_frame)
713
763
  layout.addWidget(QLabel("Time: "), 15)
714
764
  layout.addWidget(self.frame_slider, 85)
@@ -716,7 +766,7 @@ class StackVisualizer(CelldetectiveWidget):
716
766
 
717
767
  def set_target_channel(self, value):
718
768
  self.target_channel = value
719
- self.init_frame = self.stack[self.mid_time, :, :, self.target_channel]
769
+ self.init_frame = self.stack[self.current_time_index, :, :, self.target_channel]
720
770
  self.im.set_data(self.init_frame)
721
771
  self.canvas.draw()
722
772
  self.update_profile()
@@ -734,7 +784,9 @@ class StackVisualizer(CelldetectiveWidget):
734
784
  self.change_frame_from_channel_switch(self.frame_slider.value())
735
785
  else:
736
786
  if self.stack is not None and self.stack.ndim == 4:
737
- self.init_frame = self.stack[self.mid_time, :, :, self.target_channel]
787
+ self.init_frame = self.stack[
788
+ self.current_time_index, :, :, self.target_channel
789
+ ]
738
790
  self.im.set_data(self.init_frame)
739
791
  self.canvas.draw()
740
792
  self.update_profile()
@@ -747,7 +799,8 @@ class StackVisualizer(CelldetectiveWidget):
747
799
  p01 = np.nanpercentile(self.init_frame, 0.1)
748
800
  p99 = np.nanpercentile(self.init_frame, 99.9)
749
801
  self.im.set_clim(vmin=p01, vmax=p99)
750
- self.contrast_slider.setValue((p01, p99))
802
+ if self.create_contrast_slider and hasattr(self, "contrast_slider"):
803
+ self.contrast_slider.setValue((p01, p99))
751
804
  self.channel_trigger = False
752
805
  self.canvas.draw()
753
806
 
@@ -9,11 +9,16 @@ from superqt import QLabeledDoubleSlider, QLabeledDoubleRangeSlider
9
9
  from celldetective.gui.base.components import QHSeperationLine
10
10
  from celldetective.gui.gui_utils import QuickSliderLayout, ThresholdLineEdit
11
11
  from celldetective.gui.viewers.base_viewer import StackVisualizer
12
- from celldetective.utils.image_loaders import load_frames, auto_load_number_of_frames, _get_img_num_per_channel
12
+ from celldetective.utils.image_loaders import (
13
+ load_frames,
14
+ auto_load_number_of_frames,
15
+ _get_img_num_per_channel,
16
+ )
13
17
  from celldetective import get_logger
14
18
 
15
19
  logger = get_logger(__name__)
16
20
 
21
+
17
22
  class ChannelOffsetViewer(StackVisualizer):
18
23
 
19
24
  def __init__(self, parent_window=None, *args, **kwargs):
@@ -22,16 +27,32 @@ class ChannelOffsetViewer(StackVisualizer):
22
27
  self.overlay_target_channel = -1
23
28
  self.shift_vertical = 0
24
29
  self.shift_horizontal = 0
30
+ self.overlay_init_contrast = False
25
31
  super().__init__(*args, **kwargs)
26
32
 
27
33
  self.load_stack()
34
+
35
+ if self.mode == "direct":
36
+ # Initialize overlay frames for direct mode
37
+ default_overlay_idx = -1
38
+ if self.stack.ndim == 4:
39
+ self.overlay_init_frame = self.stack[
40
+ self.current_time_index, :, :, default_overlay_idx
41
+ ]
42
+ self.overlay_last_frame = self.stack[-1, :, :, default_overlay_idx]
43
+ else:
44
+ # Should rely on 4D stack assumption from StackVisualizer
45
+ self.overlay_init_frame = self.init_frame
46
+ self.overlay_last_frame = self.last_frame
47
+
28
48
  self.canvas.layout.addWidget(QHSeperationLine())
29
49
 
30
50
  self.generate_overlay_channel_cb()
31
51
  self.generate_overlay_imshow()
32
52
 
33
53
  self.generate_overlay_alpha_slider()
34
- self.generate_overlay_contrast_slider()
54
+ if self.create_contrast_slider:
55
+ self.generate_overlay_contrast_slider()
35
56
 
36
57
  self.generate_overlay_shift()
37
58
  self.generate_add_to_parent_btn()
@@ -190,7 +211,7 @@ class ChannelOffsetViewer(StackVisualizer):
190
211
 
191
212
  self.im_overlay.set_data(self.overlay_init_frame)
192
213
 
193
- if self.overlay_init_contrast:
214
+ if self.overlay_init_contrast and self.create_contrast_slider:
194
215
  self.im_overlay.autoscale()
195
216
  I_min, I_max = self.im_overlay.get_clim()
196
217
  self.overlay_contrast_slider.setRange(
@@ -214,12 +235,13 @@ class ChannelOffsetViewer(StackVisualizer):
214
235
  gc.collect()
215
236
 
216
237
  self.mid_time = self.stack_length // 2
238
+ self.current_time_index = 0
217
239
  self.img_num_per_channel = _get_img_num_per_channel(
218
240
  np.arange(self.n_channels), self.stack_length, self.n_channels
219
241
  )
220
242
 
221
243
  self.init_frame = load_frames(
222
- self.img_num_per_channel[self.target_channel, self.mid_time],
244
+ self.img_num_per_channel[self.target_channel, self.current_time_index],
223
245
  self.stack_path,
224
246
  normalize_input=False,
225
247
  ).astype(float)[:, :, 0]
@@ -229,7 +251,9 @@ class ChannelOffsetViewer(StackVisualizer):
229
251
  normalize_input=False,
230
252
  ).astype(float)[:, :, 0]
231
253
  self.overlay_init_frame = load_frames(
232
- self.img_num_per_channel[self.overlay_target_channel, self.mid_time],
254
+ self.img_num_per_channel[
255
+ self.overlay_target_channel, self.current_time_index
256
+ ],
233
257
  self.stack_path,
234
258
  normalize_input=False,
235
259
  ).astype(float)[:, :, 0]
@@ -306,6 +330,7 @@ class ChannelOffsetViewer(StackVisualizer):
306
330
  )
307
331
  self.im_overlay.set_data(self.shifted_frame)
308
332
  self.fig.canvas.draw_idle()
333
+ self.update_profile()
309
334
 
310
335
  def generate_add_to_parent_btn(self):
311
336
 
@@ -318,6 +343,95 @@ class ChannelOffsetViewer(StackVisualizer):
318
343
  add_hbox.addWidget(QLabel(""), 33)
319
344
  self.canvas.layout.addLayout(add_hbox)
320
345
 
346
+ def update_profile(self):
347
+ if not self.line_mode or not hasattr(self, "line_x") or not self.line_x:
348
+ return
349
+
350
+ # Calculate profile
351
+ x0, y0 = self.line_x[0], self.line_y[0]
352
+ x1, y1 = self.line_x[1], self.line_y[1]
353
+ length_px = np.hypot(x1 - x0, y1 - y0)
354
+ if length_px == 0:
355
+ return
356
+
357
+ num_points = int(length_px)
358
+ if num_points < 2:
359
+ num_points = 2
360
+
361
+ x, y = np.linspace(x0, x1, num_points), np.linspace(y0, y1, num_points)
362
+
363
+ # Use self.init_frame and overlay frame
364
+ profiles = []
365
+ colors = ["black", "tab:blue"]
366
+
367
+ # Main channel profile
368
+ if hasattr(self, "init_frame") and self.init_frame is not None:
369
+ from scipy.ndimage import map_coordinates
370
+
371
+ profile = map_coordinates(
372
+ self.init_frame, np.vstack((y, x)), order=1, mode="nearest"
373
+ )
374
+ profiles.append(profile)
375
+ else:
376
+ profiles.append(None)
377
+
378
+ # Overlay channel profile
379
+ # Use data currently in im_overlay, which accounts for shifts
380
+ overlay_data = self.im_overlay.get_array()
381
+ if overlay_data is not None:
382
+ from scipy.ndimage import map_coordinates
383
+
384
+ profile_overlay = map_coordinates(
385
+ overlay_data, np.vstack((y, x)), order=1, mode="nearest"
386
+ )
387
+ profiles.append(profile_overlay)
388
+ else:
389
+ profiles.append(None)
390
+
391
+ # Basic setup
392
+ self.ax_profile.clear()
393
+ self.ax_profile.set_facecolor("none")
394
+
395
+ # Distance axis
396
+ dist_axis = np.arange(num_points)
397
+ title_str = f"{round(length_px,2)} [px]"
398
+ if self.PxToUm is not None:
399
+ title_str += f" | {round(length_px*self.PxToUm,3)} [µm]"
400
+
401
+ # Handle Y-Axis Locking
402
+ current_ylim = None
403
+ if self.lock_y_action.isChecked():
404
+ current_ylim = self.ax_profile.get_ylim()
405
+
406
+ # Plot profiles
407
+ for i, (profile, color) in enumerate(zip(profiles, colors)):
408
+ if profile is not None:
409
+ if np.all(np.isnan(profile)):
410
+ profile = np.zeros_like(profile)
411
+ profile[:] = np.nan
412
+
413
+ self.ax_profile.plot(
414
+ dist_axis, profile, color=color, linestyle="-", label=f"Ch{i}"
415
+ )
416
+
417
+ self.ax_profile.set_xticks([])
418
+ self.ax_profile.set_ylabel("Intensity", fontsize=8)
419
+ self.ax_profile.set_xlabel(title_str, fontsize=8)
420
+ self.ax_profile.tick_params(axis="y", which="major", labelsize=6)
421
+
422
+ # Hide spines
423
+ self.ax_profile.spines["top"].set_visible(False)
424
+ self.ax_profile.spines["right"].set_visible(False)
425
+ self.ax_profile.spines["bottom"].set_color("black")
426
+ self.ax_profile.spines["left"].set_color("black")
427
+
428
+ self.fig.set_facecolor("none")
429
+
430
+ if current_ylim:
431
+ self.ax_profile.set_ylim(current_ylim)
432
+
433
+ self.fig.canvas.draw_idle()
434
+
321
435
  def set_parent_attributes(self):
322
436
 
323
437
  idx = self.channels_overlay_cb.currentIndex()
@@ -65,8 +65,6 @@ class ThresholdedStackVisualizer(StackVisualizer):
65
65
  self.thresh_min = 0.0
66
66
  self.thresh_max = 30.0
67
67
 
68
- self.thresh_max = 30.0
69
-
70
68
  # Cache for processed images
71
69
  self.processed_cache = OrderedDict()
72
70
  self.processed_image = None
@@ -74,6 +72,15 @@ class ThresholdedStackVisualizer(StackVisualizer):
74
72
 
75
73
  self.generate_threshold_slider()
76
74
 
75
+ # Ensure we start at frame 0 for consistent mask caching and UX
76
+ if self.create_frame_slider and hasattr(self, "frame_slider"):
77
+ self.frame_slider.blockSignals(True)
78
+ self.frame_slider.setValue(0)
79
+ self.frame_slider.blockSignals(False)
80
+ self.change_frame(0)
81
+ elif self.stack_length > 0:
82
+ self.change_frame(0)
83
+
77
84
  if self.thresh is not None:
78
85
  self.compute_mask(self.thresh)
79
86
 
@@ -138,8 +145,8 @@ class ThresholdedStackVisualizer(StackVisualizer):
138
145
  slider_range=(self.thresh_min, np.amax([self.thresh_max, init_value])),
139
146
  decimal_option=True,
140
147
  precision=4,
148
+ layout_ratio=(0.15, 0.85),
141
149
  )
142
- thresh_layout.setContentsMargins(15, 0, 15, 0)
143
150
  self.threshold_slider.valueChanged.connect(self.change_threshold)
144
151
  if self.show_threshold_slider:
145
152
  self.canvas.layout.addLayout(thresh_layout)
@@ -154,8 +161,8 @@ class ThresholdedStackVisualizer(StackVisualizer):
154
161
  slider_range=(0, 1),
155
162
  decimal_option=True,
156
163
  precision=3,
164
+ layout_ratio=(0.15, 0.85),
157
165
  )
158
- opacity_layout.setContentsMargins(15, 0, 15, 0)
159
166
  self.opacity_slider.valueChanged.connect(self.change_mask_opacity)
160
167
  if self.show_opacity_slider:
161
168
  self.canvas.layout.addLayout(opacity_layout)
@@ -190,7 +197,8 @@ class ThresholdedStackVisualizer(StackVisualizer):
190
197
  if self.thresh is not None:
191
198
  self.compute_mask(self.thresh)
192
199
  mask = np.ma.masked_where(self.mask == 0, self.mask)
193
- self.im_mask.set_data(mask)
200
+ if hasattr(self, "im_mask"):
201
+ self.im_mask.set_data(mask)
194
202
  self.canvas.canvas.draw_idle()
195
203
 
196
204
  def change_frame(self, value):
@@ -219,7 +227,7 @@ class ThresholdedStackVisualizer(StackVisualizer):
219
227
  threshold_image,
220
228
  )
221
229
 
222
- edge = estimate_unreliable_edge(self.preprocessing)
230
+ edge = estimate_unreliable_edge(self.preprocessing or [])
223
231
 
224
232
  if isinstance(threshold_value, (list, np.ndarray, tuple)):
225
233
  self.mask = threshold_image(
@@ -113,13 +113,29 @@ def _prep_cellpose_model(
113
113
 
114
114
  from cellpose.models import CellposeModel
115
115
 
116
- model = CellposeModel(
117
- gpu=use_gpu,
118
- device=device,
119
- pretrained_model=path + model_name,
120
- model_type=None,
121
- nchan=n_channels,
122
- ) # diam_mean=30.0,
116
+ try:
117
+ model = CellposeModel(
118
+ gpu=use_gpu,
119
+ device=device,
120
+ pretrained_model=path + model_name,
121
+ model_type=None,
122
+ nchan=n_channels,
123
+ ) # diam_mean=30.0,
124
+ except AssertionError as e:
125
+ if use_gpu:
126
+ print(
127
+ f"[WARNING] Could not load Cellpose model with GPU ({e}). Retrying with CPU..."
128
+ )
129
+ device = torch.device("cpu")
130
+ model = CellposeModel(
131
+ gpu=False,
132
+ device=device,
133
+ pretrained_model=path + model_name,
134
+ model_type=None,
135
+ nchan=n_channels,
136
+ )
137
+ else:
138
+ raise e
123
139
  if scale is None:
124
140
  scale_model = model.diam_mean / model.diam_labels
125
141
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: celldetective
3
- Version: 1.5.0b3
3
+ Version: 1.5.0b5
4
4
  Summary: description
5
5
  Home-page: http://github.com/remyeltorro/celldetective
6
6
  Author: Rémy Torro
@@ -1,6 +1,6 @@
1
1
  celldetective/__init__.py,sha256=LfnOyfUnYPGDc8xcsF_PfYEL7-CqAb7BMBPBIWGv84o,666
2
2
  celldetective/__main__.py,sha256=Rzzu9ArxZSgfBVjV-lyn-3oanQB2MumQR6itK5ZaRpA,2597
3
- celldetective/_version.py,sha256=xyBMhGEXnWbOInuedgW565vK5NILuhG2r2EYCw9E9eU,24
3
+ celldetective/_version.py,sha256=WqWa8H3XCjzmFMLQoflfXoLyQGm-ej1fcgZfMqTF--U,24
4
4
  celldetective/events.py,sha256=n15R53c7QZ2wT8gjb0oeNikQbuRBrVVbyNsRCqXjzXA,8166
5
5
  celldetective/exceptions.py,sha256=f3VmIYOthWTiqMEV5xQCox2rw5c5e7yog88h-CcV4oI,356
6
6
  celldetective/extra_properties.py,sha256=s_2R4_El2p-gQNZ_EpgDxgrN3UnRitN7KDKHhyLuoHc,21681
@@ -30,7 +30,7 @@ celldetective/gui/gui_utils.py,sha256=t6SjEfjcaRH9a0TlbTGEiVRpCgocaCh4lgkIvRgRRw
30
30
  celldetective/gui/interactions_block.py,sha256=34VaHFrdKsq1hYuXrosmpP15JU26dSfbyx4lyt1jxNg,28440
31
31
  celldetective/gui/interactive_timeseries_viewer.py,sha256=u_amAhLdEHRpYSRwPDtVm5v-JZIz0ANTcG4YGksX1Vo,16079
32
32
  celldetective/gui/json_readers.py,sha256=t5rhtIxACj0pdwLrnPs-QjyhQo3P25UGWGgOCIBhQxs,4572
33
- celldetective/gui/measure_annotator.py,sha256=HdhrU3N207evB9s-pqZNfzJC2Jswx3yHdPPLR8CRILc,38354
33
+ celldetective/gui/measure_annotator.py,sha256=ljNbsKmFXQk0R9zEfBZ6XfBHzFmlL7Gt6QyPHyqh08g,38357
34
34
  celldetective/gui/pair_event_annotator.py,sha256=QJHaM-z0K2I36sLf6XCMFMymfw-nFhzfGJ8oxrMfZco,124091
35
35
  celldetective/gui/plot_measurements.py,sha256=a_Mks-5XUTn2QEYih0PlXGp2lX3C34zuhK9ozzE1guM,56593
36
36
  celldetective/gui/plot_signals_ui.py,sha256=9VmA1yaTcNf1jY7drtK41R1q87dhEk7bXBCq_cQ94fs,28133
@@ -45,7 +45,7 @@ celldetective/gui/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
45
45
  celldetective/gui/base/channel_norm_generator.py,sha256=JqNAo87zA3nMKzkGjvoeV-SHI89eIATwQj4jHv3-LpI,13973
46
46
  celldetective/gui/base/components.py,sha256=jNUsCU_QE7QUFR0_xEvEPFEBYolMJt7YXGUKMjF7uOE,8155
47
47
  celldetective/gui/base/feature_choice.py,sha256=n1T2fPoNLiTDS_6fa_GuGReDhBW11HdUrKy2RywotF8,2800
48
- celldetective/gui/base/figure_canvas.py,sha256=dQlmLfB3PUttFmyHZIvSc_9GQ5JRy7jH9w7lyKAGElk,1523
48
+ celldetective/gui/base/figure_canvas.py,sha256=DAAGQAcoR6yubFSsRNRZcpxgdYOWgEHtJqWhv_G7lj0,2195
49
49
  celldetective/gui/base/list_widget.py,sha256=_WU3ZRU7UcJZIxm8qx_5HF7IK7dUu8IU1FY2AaW_qgo,4694
50
50
  celldetective/gui/base/styles.py,sha256=3Kz1eXw6OLr90wtErhK0KPJyJbyhAlqkniqm0JNGwFc,7407
51
51
  celldetective/gui/base/utils.py,sha256=KojauRKGM9XKNhaWn211p6mJNZWIHLH75yeLpDd7pvA,1103
@@ -76,7 +76,7 @@ celldetective/gui/settings/_settings_event_model_training.py,sha256=TVPnnYPmdAF6
76
76
  celldetective/gui/settings/_settings_measurements.py,sha256=nrVR1dG3B7iyJayRql1yierhKvgXZiu6qVtfkxI1ABA,47947
77
77
  celldetective/gui/settings/_settings_neighborhood.py,sha256=ws6H99bKU4NYd2IYyaJj7g9-MScr5W6UB2raP88ytfE,23767
78
78
  celldetective/gui/settings/_settings_segmentation.py,sha256=6DihD1mk-dN4Sstdth1iJ-0HR34rTVlTHP-pXUh_rY0,1901
79
- celldetective/gui/settings/_settings_segmentation_model_training.py,sha256=XxqSCxLHOvn1w8V8ZEEfPAvdwfS0IKz107ox4yll7AA,30229
79
+ celldetective/gui/settings/_settings_segmentation_model_training.py,sha256=6Ftih-_1VaICHBFLs6BPO8kf5ZDohwvDF4dEnhYC19M,30440
80
80
  celldetective/gui/settings/_settings_signal_annotator.py,sha256=Mvvre-yHJvRoR-slbkLNwEemuGgiMCQQ4H1BHjFk3r4,12412
81
81
  celldetective/gui/settings/_settings_tracking.py,sha256=5ZxJp3o3stD2NKdhqZofIgsUNp73oAN_pIi_bDFAd0Y,53293
82
82
  celldetective/gui/settings/_stardist_model_params.py,sha256=dEKhaLcJ4r8VxgBU2DI-hcTaTk5S181O-_CN0j7JSgE,4020
@@ -87,12 +87,12 @@ celldetective/gui/table_ops/_merge_one_hot.py,sha256=gKRgem-u_-JENkVkbjRobsH4TkS
87
87
  celldetective/gui/table_ops/_query_table.py,sha256=K-XHSZ1I4v2wwqWjyQAgyFRZJbem3CmTfHmO0vijh9g,1345
88
88
  celldetective/gui/table_ops/_rename_col.py,sha256=UAgDSpXJo_h4pLJpHaNc2w2VhbaW4D2JZTgJ3cYC4-g,1457
89
89
  celldetective/gui/viewers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
- celldetective/gui/viewers/base_viewer.py,sha256=1U936rVnRqevXtyqlZnu_1o_VbWx3BJVofKh_oKG1jM,29857
91
- celldetective/gui/viewers/channel_offset_viewer.py,sha256=aQpxrJX9qM_xX848XnFrxrhTqE22DIshtD-uKL8e4gU,12423
90
+ celldetective/gui/viewers/base_viewer.py,sha256=ir3J8QyLDAijOMu0Np56lUR0fSeWhz_1xdb_bX6rdRk,31939
91
+ celldetective/gui/viewers/channel_offset_viewer.py,sha256=cywBkxyMPyKIuwZTGA03DBSS4a-H1SAohMJYOPISLEE,16289
92
92
  celldetective/gui/viewers/contour_viewer.py,sha256=riHj03LKXLoa-Ys2o2EhCE5nULfcHMohx9LFoXbI6zU,14720
93
93
  celldetective/gui/viewers/size_viewer.py,sha256=uXITjaxg5dhQ09Q6TNUxwLxi-ZglyGFcxEH1RtGIZWw,6020
94
94
  celldetective/gui/viewers/spot_detection_viewer.py,sha256=JO7kcqATHXR91lLvo8aQ5xVYdtxkMxV-xx36s01VlNQ,12545
95
- celldetective/gui/viewers/threshold_viewer.py,sha256=CBc6Y7cGK5U3ptAMMkmRLBvm5yl7pjGEUJjB4PKWjfM,11627
95
+ celldetective/gui/viewers/threshold_viewer.py,sha256=F-13JF2wFhyvvKfUvgRcSjWL3leAliOXy5yUergndnE,12000
96
96
  celldetective/icons/logo-large.png,sha256=FXSwV3u6zEKcfpuSn4unnqB0oUnN9cHqQ9BCKWytrpg,36631
97
97
  celldetective/icons/logo.png,sha256=wV2OS8_dU5Td5cgdPbCOU3JpMpTwNuYLnfVcnQX0tJA,2437
98
98
  celldetective/icons/signals_icon.png,sha256=vEiKoqWTtN0-uJgVqtAlwCuP-f4QeWYOlO3sdp2tg2w,3969
@@ -161,13 +161,14 @@ celldetective/utils/parsing.py,sha256=1zpIH9tyULCRmO5Kwzy6yA01fqm5uE_mZKYtondy-V
161
161
  celldetective/utils/resources.py,sha256=3Fz_W0NYWl_Ixc2AjEmkOv5f7ejXerCLJ2z1iWhGWUI,1153
162
162
  celldetective/utils/stats.py,sha256=4TVHRqi38Y0sed-izaMI51sMP0fd5tC5M68EYyfJjkE,3636
163
163
  celldetective/utils/types.py,sha256=lRfWSMVzTkxgoctGGp0NqD551akuxu0ygna7zVGonTg,397
164
- celldetective/utils/cellpose_utils/__init__.py,sha256=grsglxR29vj42tlgsBoXnVnjrIJWsBXqbtkjFjzqH7A,4898
164
+ celldetective/utils/cellpose_utils/__init__.py,sha256=MeRDojrAkBSXe-wlLu5KIih5wXP9B2aPdP39JLYpoGE,5417
165
165
  celldetective/utils/event_detection/__init__.py,sha256=KX20PwPTevdbZ-25otDy_QTmealcDx5xNCfH2SOVIZM,323
166
166
  celldetective/utils/plots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
167
167
  celldetective/utils/plots/regression.py,sha256=oUCn29-hp7PxMqC-R0yoL60KMw5ZWpZAIoCDh2ErlcY,1764
168
168
  celldetective/utils/stardist_utils/__init__.py,sha256=e9s3DEaTKCUOGZb5k_DgShBTl4B0U-Jmg3Ioo8D5PyE,3978
169
- celldetective-1.5.0b3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
169
+ celldetective-1.5.0b5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
170
170
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
171
+ tests/test_cellpose_fallback.py,sha256=BJZTDFF8sFR1x7rDbvZQ2RQOB1OP6wuFBRfc8zbl5zw,3513
171
172
  tests/test_events.py,sha256=eLFwwEEJfQAdwhews3-fn1HSvzozcNNFN_Qn0gOvQkE,685
172
173
  tests/test_filters.py,sha256=uj4NVyUnKXa18EpTSiWCetGKI1VFopDyNSJSUxX44wA,1689
173
174
  tests/test_io.py,sha256=gk5FmoI7ANEczUtNXYRxc48KzkfYzemwS_eYaLq4_NI,2093
@@ -181,10 +182,11 @@ tests/test_tracking.py,sha256=_YLjwQ3EChQssoHDfEnUJ7fI1yC5KEcJsFnAVR64L70,18909
181
182
  tests/test_utils.py,sha256=aSB_eMw9fzTsnxxdYoNmdQQRrXkWqBXB7Uv4TGU6kYE,4778
182
183
  tests/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
183
184
  tests/gui/test_enhancements.py,sha256=3x9au_rkQtMZ94DRj3OaEHKPr511RrWqBAUAcNQn1ys,13453
185
+ tests/gui/test_measure_annotator_bugfix.py,sha256=tPfgWNKC0UkvrVssSrUcVDC1qgpzx6l2yCqvKtKYkM4,4544
184
186
  tests/gui/test_new_project.py,sha256=wRjW2vEaZb0LWT-f8G8-Ptk8CW9z8-FDPLpV5uqj6ck,8778
185
187
  tests/gui/test_project.py,sha256=KzAnodIc0Ovta0ARL5Kr5PkOR5euA6qczT_GhEZpyE4,4710
186
- celldetective-1.5.0b3.dist-info/METADATA,sha256=D4LkOtPQrdeuX4zW5_OKXlN1dxkxaZx_zqdILC36-Hk,10947
187
- celldetective-1.5.0b3.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
188
- celldetective-1.5.0b3.dist-info/entry_points.txt,sha256=2NU6_EOByvPxqBbCvjwxlVlvnQreqZ3BKRCVIKEv3dg,62
189
- celldetective-1.5.0b3.dist-info/top_level.txt,sha256=6rsIKKfGMKgud7HPuATcpq6EhdXwcg_yknBVWn9x4C4,20
190
- celldetective-1.5.0b3.dist-info/RECORD,,
188
+ celldetective-1.5.0b5.dist-info/METADATA,sha256=HXfAb1NZp3md807sossVoljx3m1O4VYRaJrgjaq8T0c,10947
189
+ celldetective-1.5.0b5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
190
+ celldetective-1.5.0b5.dist-info/entry_points.txt,sha256=2NU6_EOByvPxqBbCvjwxlVlvnQreqZ3BKRCVIKEv3dg,62
191
+ celldetective-1.5.0b5.dist-info/top_level.txt,sha256=6rsIKKfGMKgud7HPuATcpq6EhdXwcg_yknBVWn9x4C4,20
192
+ celldetective-1.5.0b5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,130 @@
1
+ import pytest
2
+ import os
3
+ import pandas as pd
4
+ import numpy as np
5
+ import logging
6
+ from PyQt5 import QtCore
7
+ from celldetective.gui.InitWindow import AppInitWindow
8
+ from celldetective.gui.measure_annotator import MeasureAnnotator
9
+ from celldetective import get_software_location
10
+ from unittest.mock import patch
11
+ import shutil
12
+ import tifffile
13
+
14
+ software_location = get_software_location()
15
+
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def disable_logging():
19
+ """Disable all logging to avoid Windows OSError with pytest capture."""
20
+ logger = logging.getLogger()
21
+ try:
22
+ logging.disable(logging.CRITICAL)
23
+ yield
24
+ finally:
25
+ logging.disable(logging.NOTSET)
26
+
27
+
28
+ @pytest.fixture
29
+ def app(qtbot):
30
+ test_app = AppInitWindow(software_location=software_location)
31
+ qtbot.addWidget(test_app)
32
+ return test_app
33
+
34
+
35
+ def create_dummy_movie(exp_dir, well="W1", pos="100", prefix="sample", frames=5):
36
+ movie_dir = os.path.join(exp_dir, well, pos, "movie")
37
+ os.makedirs(movie_dir, exist_ok=True)
38
+ # Use a single multi-page TIF as expected by locate_stack
39
+ movie_path = os.path.join(movie_dir, f"{prefix}.tif")
40
+ img = np.zeros((frames, 100, 100), dtype=np.uint16)
41
+ tifffile.imwrite(movie_path, img)
42
+
43
+
44
+ def test_measure_annotator_colors_writable(app, qtbot, tmp_path):
45
+ """
46
+ Test that self.colors in MeasureAnnotator contains writable arrays.
47
+ This verifies the fix for 'ValueError: assignment destination is read-only'.
48
+ """
49
+ exp_dir = str(tmp_path / "ExperimentColors")
50
+ os.makedirs(os.path.join(exp_dir, "W1", "100", "output", "tables"), exist_ok=True)
51
+ os.makedirs(os.path.join(exp_dir, "configs"), exist_ok=True)
52
+
53
+ with open(os.path.join(exp_dir, "config.ini"), "w") as f:
54
+ f.write(
55
+ "[MovieSettings]\nmovie_prefix = sample\nlen_movie = 10\nshape_x = 100\nshape_y = 100\npxtoum = 1.0\nframetomin = 1.0\n"
56
+ )
57
+ f.write(
58
+ "[Labels]\nconcentrations = 0\ncell_types = dummy\nantibodies = none\npharmaceutical_agents = none\n[Channels]\nChannel1 = 0\n"
59
+ )
60
+
61
+ create_dummy_movie(exp_dir, well="W1", pos="100", prefix="sample", frames=10)
62
+
63
+ # DataFrame with tracks
64
+ df = pd.DataFrame(
65
+ {
66
+ "TRACK_ID": [1, 1],
67
+ "FRAME": [0, 1],
68
+ "group_experimental": ["A", "A"],
69
+ "area": [100.0, 110.0],
70
+ "POSITION_X": [10, 12],
71
+ "POSITION_Y": [10, 12],
72
+ "status": [0, 0], # Ensure status column exists
73
+ }
74
+ )
75
+ # The 'group_color' column is usually generated inside MeasureAnnotator,
76
+ # but let's see if we need to let it generate it.
77
+ # MeasureAnnotator calls 'color_from_state', then assigns 'group_color'.
78
+
79
+ traj_path = os.path.join(
80
+ exp_dir, "W1", "100", "output", "tables", "trajectories_effectors.csv"
81
+ )
82
+ df.to_csv(traj_path, index=False)
83
+
84
+ app.experiment_path_selection.setText(exp_dir)
85
+ qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
86
+ qtbot.waitUntil(lambda: hasattr(app, "control_panel"), timeout=30000)
87
+
88
+ cp = app.control_panel
89
+ p0 = cp.ProcessPopulations[0]
90
+
91
+ qtbot.waitUntil(lambda: cp.well_list.count() > 0, timeout=30000)
92
+
93
+ with patch.object(cp.well_list, "getSelectedIndices", return_value=[0]):
94
+ with patch.object(cp.position_list, "getSelectedIndices", return_value=[0]):
95
+
96
+ cp.update_position_options()
97
+ qtbot.wait(500)
98
+
99
+ qtbot.mouseClick(p0.check_measurements_btn, QtCore.Qt.LeftButton)
100
+
101
+ try:
102
+ qtbot.waitUntil(lambda: hasattr(p0, "measure_annotator"), timeout=15000)
103
+ except Exception:
104
+ print("DEBUG: measure_annotator not found on p0.")
105
+ raise
106
+
107
+ annotator = p0.measure_annotator
108
+ qtbot.wait(1000)
109
+ assert annotator is not None
110
+
111
+ # Verify that self.colors arrays are writable
112
+ # extract_scatter_from_trajectories should have been called during init
113
+ assert hasattr(annotator, "colors")
114
+ assert len(annotator.colors) > 0
115
+
116
+ # Check the first frame's colors
117
+ colors_frame_0 = annotator.colors[0]
118
+
119
+ # Check flags
120
+ assert colors_frame_0.flags[
121
+ "WRITEABLE"
122
+ ], "self.colors arrays must be writable"
123
+
124
+ # Try to modify (should not raise ValueError)
125
+ try:
126
+ colors_frame_0[0] = "lime"
127
+ except ValueError as e:
128
+ pytest.fail(f"Could not modify colors array: {e}")
129
+
130
+ annotator.close()
@@ -0,0 +1,101 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+ import sys
4
+
5
+ # Do not import torch here to avoid WinError 1114 if environment is broken.
6
+ # We will mock it in setUp.
7
+
8
+
9
+ class TestCellposeFallback(unittest.TestCase):
10
+
11
+ def setUp(self):
12
+ # Create a mock for torch
13
+ self.mock_torch = MagicMock()
14
+ self.mock_torch.device = MagicMock(return_value="cpu")
15
+ self.mock_torch.cuda = MagicMock()
16
+ self.mock_torch.cuda.is_available.return_value = (
17
+ False # Default to CPU environment simulation
18
+ )
19
+
20
+ # Patch modules so that 'import torch' and 'import cellpose' work with our mocks
21
+ # We need to patch 'torch' in sys.modules BEFORE importing code that uses it.
22
+ self.modules_patcher = patch.dict(
23
+ sys.modules,
24
+ {
25
+ "torch": self.mock_torch,
26
+ "cellpose": MagicMock(),
27
+ "cellpose.models": MagicMock(),
28
+ },
29
+ )
30
+ self.modules_patcher.start()
31
+
32
+ # Define a mock CellposeModel that we can control
33
+ self.MockCellposeModel = MagicMock()
34
+ sys.modules["cellpose.models"].CellposeModel = self.MockCellposeModel
35
+
36
+ def tearDown(self):
37
+ self.modules_patcher.stop()
38
+
39
+ def test_gpu_fallback_on_assertion_error(self):
40
+ """
41
+ Test that _prep_cellpose_model falls back to CPU if GPU init fails with AssertionError.
42
+ """
43
+ # Lazy import inside the test method/patch context
44
+ from celldetective.utils.cellpose_utils import _prep_cellpose_model
45
+
46
+ # Side effect for CellposeModel constructor
47
+ def side_effect(gpu=False, **kwargs):
48
+ if gpu:
49
+ raise AssertionError("Torch not compiled with CUDA enabled")
50
+
51
+ # Return a mock model object
52
+ model = MagicMock()
53
+ model.diam_mean = 30.0
54
+ model.diam_labels = 30.0
55
+ return model
56
+
57
+ self.MockCellposeModel.side_effect = side_effect
58
+
59
+ # Call the function with use_gpu=True
60
+ # We expect it to try with gpu=True, fail, print warning, and retry with gpu=False
61
+ model, scale = _prep_cellpose_model(
62
+ model_name="fake_model", path="fake_path/", use_gpu=True, n_channels=2
63
+ )
64
+
65
+ # Check call history
66
+ self.assertEqual(self.MockCellposeModel.call_count, 2)
67
+
68
+ args1, kwargs1 = self.MockCellposeModel.call_args_list[0]
69
+ self.assertTrue(kwargs1.get("gpu"), "First call should try gpu=True")
70
+
71
+ args2, kwargs2 = self.MockCellposeModel.call_args_list[1]
72
+ self.assertFalse(kwargs2.get("gpu"), "Second call should retry with gpu=False")
73
+
74
+ self.assertIsNotNone(model)
75
+
76
+ def test_gpu_success(self):
77
+ """
78
+ Test that _prep_cellpose_model works normally if GPU init succeeds.
79
+ """
80
+ from celldetective.utils.cellpose_utils import _prep_cellpose_model
81
+
82
+ # Side effect for success
83
+ def side_effect(gpu=False, **kwargs):
84
+ model = MagicMock()
85
+ model.diam_mean = 30.0
86
+ model.diam_labels = 30.0
87
+ return model
88
+
89
+ self.MockCellposeModel.side_effect = side_effect
90
+
91
+ model, scale = _prep_cellpose_model(
92
+ model_name="fake_model", path="fake_path/", use_gpu=True, n_channels=2
93
+ )
94
+
95
+ self.assertEqual(self.MockCellposeModel.call_count, 1)
96
+ args, kwargs = self.MockCellposeModel.call_args
97
+ self.assertTrue(kwargs.get("gpu"))
98
+
99
+
100
+ if __name__ == "__main__":
101
+ unittest.main()