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 +1 -1
- celldetective/gui/base/figure_canvas.py +17 -3
- celldetective/gui/measure_annotator.py +3 -3
- celldetective/gui/settings/_settings_segmentation_model_training.py +6 -0
- celldetective/gui/viewers/base_viewer.py +72 -19
- celldetective/gui/viewers/channel_offset_viewer.py +119 -5
- celldetective/gui/viewers/threshold_viewer.py +14 -6
- celldetective/utils/cellpose_utils/__init__.py +23 -7
- {celldetective-1.5.0b3.dist-info → celldetective-1.5.0b5.dist-info}/METADATA +1 -1
- {celldetective-1.5.0b3.dist-info → celldetective-1.5.0b5.dist-info}/RECORD +16 -14
- {celldetective-1.5.0b3.dist-info → celldetective-1.5.0b5.dist-info}/WHEEL +1 -1
- tests/gui/test_measure_annotator_bugfix.py +130 -0
- tests/test_cellpose_fallback.py +101 -0
- {celldetective-1.5.0b3.dist-info → celldetective-1.5.0b5.dist-info}/entry_points.txt +0 -0
- {celldetective-1.5.0b3.dist-info → celldetective-1.5.0b5.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.5.0b3.dist-info → celldetective-1.5.0b5.dist-info}/top_level.txt +0 -0
celldetective/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.5.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
955
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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[
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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[
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
celldetective/__init__.py,sha256=LfnOyfUnYPGDc8xcsF_PfYEL7-CqAb7BMBPBIWGv84o,666
|
|
2
2
|
celldetective/__main__.py,sha256=Rzzu9ArxZSgfBVjV-lyn-3oanQB2MumQR6itK5ZaRpA,2597
|
|
3
|
-
celldetective/_version.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
91
|
-
celldetective/gui/viewers/channel_offset_viewer.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
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.
|
|
187
|
-
celldetective-1.5.
|
|
188
|
-
celldetective-1.5.
|
|
189
|
-
celldetective-1.5.
|
|
190
|
-
celldetective-1.5.
|
|
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,,
|
|
@@ -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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|