celldetective 1.5.0b4__py3-none-any.whl → 1.5.0b6__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.0b4"
1
+ __version__ = "1.5.0b6"
@@ -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
 
@@ -285,6 +285,10 @@ class StackVisualizer(CelldetectiveWidget):
285
285
  self.lock_y_action.setEnabled(True)
286
286
  self.canvas.toolbar.mode = ""
287
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
+
288
292
  # Connect events
289
293
  self.cid_press = self.fig.canvas.mpl_connect(
290
294
  "button_press_event", self.on_line_press
@@ -297,8 +301,8 @@ class StackVisualizer(CelldetectiveWidget):
297
301
  )
298
302
 
299
303
  # Save original position if not saved
300
- if not hasattr(self, "ax_original_pos"):
301
- 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()
302
306
 
303
307
  # Disable tight_layout/layout engine to prevent fighting manual positioning
304
308
  if hasattr(self.fig, "set_layout_engine"):
@@ -334,6 +338,7 @@ class StackVisualizer(CelldetectiveWidget):
334
338
  self.ax_profile.set_position(gs[1].get_position(self.fig))
335
339
 
336
340
  self.ax_profile.set_visible(True)
341
+ self.ax_profile.set_label("profile_axes")
337
342
  self.ax_profile.set_facecolor("none")
338
343
  self.ax_profile.tick_params(axis="y", which="major", labelsize=8)
339
344
  self.ax_profile.set_xticks([])
@@ -346,11 +351,40 @@ class StackVisualizer(CelldetectiveWidget):
346
351
  self.ax_profile.spines["bottom"].set_color("black")
347
352
  self.ax_profile.spines["left"].set_color("black")
348
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
+
349
378
  self.canvas.draw()
350
379
  else:
351
380
  self.line_mode = False
352
381
  self.lock_y_action.setChecked(False)
353
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
+
354
388
  # Disconnect events
355
389
  if hasattr(self, "cid_press"):
356
390
  self.fig.canvas.mpl_disconnect(self.cid_press)
@@ -372,17 +406,24 @@ class StackVisualizer(CelldetectiveWidget):
372
406
  self.ax_profile = None
373
407
 
374
408
  # Restore original layout
375
- if hasattr(self, "ax_original_pos"):
376
- # standard 1x1 GridSpec or manual restore
377
- import matplotlib.gridspec as gridspec
378
-
379
- gs = gridspec.GridSpec(1, 1)
380
- self.ax.set_subplotspec(gs[0])
381
- self.ax.set_position(gs[0].get_position(self.fig))
382
- self.fig.subplots_adjust(
383
- top=1, bottom=0, right=1, left=0, hspace=0, wspace=0
384
- )
385
- # 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()
386
427
 
387
428
  self.canvas.draw()
388
429
  self.info_lbl.setText("")
@@ -553,7 +594,10 @@ class StackVisualizer(CelldetectiveWidget):
553
594
  self.mode = "direct"
554
595
  self.stack_length = len(self.stack)
555
596
  self.mid_time = self.stack_length // 2
556
- 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
+ ]
557
601
  self.last_frame = self.stack[-1, :, :, self.target_channel]
558
602
  else:
559
603
  self.mode = "virtual"
@@ -566,6 +610,7 @@ class StackVisualizer(CelldetectiveWidget):
566
610
 
567
611
  self.stack_length = auto_load_number_of_frames(self.stack_path)
568
612
  self.mid_time = self.stack_length // 2
613
+ self.current_time_index = 0
569
614
  self.img_num_per_channel = _get_img_num_per_channel(
570
615
  np.arange(self.n_channels), self.stack_length, self.n_channels
571
616
  )
@@ -578,7 +623,7 @@ class StackVisualizer(CelldetectiveWidget):
578
623
  self.loader_thread.start()
579
624
 
580
625
  self.init_frame = load_frames(
581
- self.img_num_per_channel[self.target_channel, self.mid_time],
626
+ self.img_num_per_channel[self.target_channel, self.current_time_index],
582
627
  self.stack_path,
583
628
  normalize_input=False,
584
629
  )[:, :, 0]
@@ -713,7 +758,7 @@ class StackVisualizer(CelldetectiveWidget):
713
758
  layout = QHBoxLayout()
714
759
  self.frame_slider = QLabeledSlider(Qt.Horizontal)
715
760
  self.frame_slider.setRange(0, self.stack_length - 1)
716
- self.frame_slider.setValue(self.mid_time)
761
+ self.frame_slider.setValue(self.current_time_index)
717
762
  self.frame_slider.valueChanged.connect(self.change_frame)
718
763
  layout.addWidget(QLabel("Time: "), 15)
719
764
  layout.addWidget(self.frame_slider, 85)
@@ -721,7 +766,7 @@ class StackVisualizer(CelldetectiveWidget):
721
766
 
722
767
  def set_target_channel(self, value):
723
768
  self.target_channel = value
724
- self.init_frame = self.stack[self.mid_time, :, :, self.target_channel]
769
+ self.init_frame = self.stack[self.current_time_index, :, :, self.target_channel]
725
770
  self.im.set_data(self.init_frame)
726
771
  self.canvas.draw()
727
772
  self.update_profile()
@@ -739,7 +784,9 @@ class StackVisualizer(CelldetectiveWidget):
739
784
  self.change_frame_from_channel_switch(self.frame_slider.value())
740
785
  else:
741
786
  if self.stack is not None and self.stack.ndim == 4:
742
- 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
+ ]
743
790
  self.im.set_data(self.init_frame)
744
791
  self.canvas.draw()
745
792
  self.update_profile()
@@ -752,7 +799,8 @@ class StackVisualizer(CelldetectiveWidget):
752
799
  p01 = np.nanpercentile(self.init_frame, 0.1)
753
800
  p99 = np.nanpercentile(self.init_frame, 99.9)
754
801
  self.im.set_clim(vmin=p01, vmax=p99)
755
- self.contrast_slider.setValue((p01, p99))
802
+ if self.create_contrast_slider and hasattr(self, "contrast_slider"):
803
+ self.contrast_slider.setValue((p01, p99))
756
804
  self.channel_trigger = False
757
805
  self.canvas.draw()
758
806
 
@@ -831,6 +879,14 @@ class StackVisualizer(CelldetectiveWidget):
831
879
  # Event handler for closing the widget
832
880
  if self.loader_thread:
833
881
  self.loader_thread.stop()
882
+ self.loader_thread = None
834
883
  if hasattr(self, "frame_cache") and isinstance(self.frame_cache, OrderedDict):
835
884
  self.frame_cache.clear()
836
885
  self.canvas.close()
886
+
887
+ def __del__(self):
888
+ try:
889
+ if hasattr(self, "loader_thread") and self.loader_thread:
890
+ self.loader_thread.stop()
891
+ except:
892
+ pass
@@ -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(
@@ -104,14 +104,27 @@ def _prep_cellpose_model(
104
104
  `diam_labels` are attributes of the model.
105
105
  """
106
106
 
107
- import torch
107
+ try:
108
+ import torch
109
+ except ImportError as e:
110
+ raise RuntimeError(
111
+ "Torch is not installed. Please install it to use this feature.\n"
112
+ "You can install the full package with: pip install celldetective[process]\n"
113
+ ) from e
108
114
 
109
115
  if not use_gpu:
110
116
  device = torch.device("cpu")
111
117
  else:
112
118
  device = torch.device("cuda")
113
119
 
114
- from cellpose.models import CellposeModel
120
+ try:
121
+ from cellpose.models import CellposeModel
122
+ except ImportError as e:
123
+ raise RuntimeError(
124
+ "Cellpose is not installed. Please install it to use this feature.\n"
125
+ "You can install the full package with: pip install celldetective[process]\n"
126
+ "Or specifically: pip install celldetective[cellpose]"
127
+ ) from e
115
128
 
116
129
  try:
117
130
  model = CellposeModel(
@@ -77,18 +77,39 @@ def download_url_to_file(url, dst, progress=True):
77
77
  Default: True
78
78
 
79
79
  """
80
- file_size = None
81
80
  import ssl
81
+ import time
82
+ from urllib.error import HTTPError, URLError
82
83
 
84
+ file_size = None
83
85
  ssl._create_default_https_context = ssl._create_unverified_context
84
- u = urlopen(url)
85
- meta = u.info()
86
- if hasattr(meta, "getheaders"):
87
- content_length = meta.getheaders("Content-Length")
88
- else:
89
- content_length = meta.get_all("Content-Length")
90
- if content_length is not None and len(content_length) > 0:
91
- file_size = int(content_length[0])
86
+
87
+ # Retry configuration
88
+ max_retries = 5
89
+ retry_delay = 10 # Initial delay in seconds
90
+
91
+ for attempt in range(max_retries):
92
+ try:
93
+ u = urlopen(url)
94
+ meta = u.info()
95
+ if hasattr(meta, "getheaders"):
96
+ content_length = meta.getheaders("Content-Length")
97
+ else:
98
+ content_length = meta.get_all("Content-Length")
99
+ if content_length is not None and len(content_length) > 0:
100
+ file_size = int(content_length[0])
101
+ break # Success
102
+ except (HTTPError, URLError) as e:
103
+ if attempt < max_retries - 1:
104
+ logger.warning(
105
+ f"Download check failed: {e}. Retrying in {retry_delay}s..."
106
+ )
107
+ time.sleep(retry_delay)
108
+ retry_delay *= 2 # Exponential backoff
109
+ else:
110
+ logger.error(f"Download check failed after {max_retries} attempts: {e}")
111
+ raise e
112
+
92
113
  # We deliberately save it in a temp file and move it after
93
114
  dst = os.path.expanduser(dst)
94
115
  dst_dir = os.path.dirname(dst)
@@ -143,14 +164,27 @@ def download_url_to_file(url, dst, progress=True):
143
164
  unit_divisor=1024,
144
165
  ) as pbar:
145
166
  while True:
146
- buffer = u.read(8192) # 8192
147
- if len(buffer) == 0:
148
- break
149
- f.write(buffer)
150
- pbar.update(len(buffer))
167
+ try:
168
+ buffer = u.read(8192) # 8192
169
+ if len(buffer) == 0:
170
+ break
171
+ f.write(buffer)
172
+ pbar.update(len(buffer))
173
+ except (HTTPError, URLError) as e:
174
+ # Attempt rudimentary resume-like behavior or just fail?
175
+ # Simple retry of read is hard without Range headers on a stream.
176
+ # Best to just fail the whole download and rely on outer retry if we wrapped the whole thing.
177
+ # For now, let's just let it raise, but really we should wrap the whole download block.
178
+ raise e
151
179
 
152
180
  f.close()
153
181
  shutil.move(f.name, dst)
182
+ except Exception as e:
183
+ f.close()
184
+ remove_file_if_exists(f.name)
185
+ # If we failed during download reading (after open), we should probably retry the whole function from start
186
+ # but that requires significant refactoring. Given the error was 504 on open, the retry block above handles it.
187
+ raise e
154
188
  finally:
155
189
  f.close()
156
190
  remove_file_if_exists(f.name)
@@ -43,7 +43,14 @@ def _prep_stardist_model(
43
43
  - GPU support depends on the availability of compatible hardware and software setup.
44
44
  """
45
45
 
46
- from stardist.models import StarDist2D
46
+ try:
47
+ from stardist.models import StarDist2D
48
+ except ImportError as e:
49
+ raise RuntimeError(
50
+ "StarDist is not installed. Please install it to use this feature.\n"
51
+ "You can install the full package with: pip install celldetective[process]\n"
52
+ "Or specifically: pip install celldetective[stardist]"
53
+ ) from e
47
54
 
48
55
  model = StarDist2D(None, name=model_name, basedir=path)
49
56
  model.config.use_gpu = use_gpu
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: celldetective
3
- Version: 1.5.0b4
3
+ Version: 1.5.0b6
4
4
  Summary: description
5
5
  Home-page: http://github.com/remyeltorro/celldetective
6
6
  Author: Rémy Torro
@@ -15,11 +15,8 @@ Requires-Dist: sphinx_rtd_theme
15
15
  Requires-Dist: sphinx
16
16
  Requires-Dist: jinja2
17
17
  Requires-Dist: ipykernel
18
- Requires-Dist: stardist
19
- Requires-Dist: cellpose<3
20
18
  Requires-Dist: scikit-learn
21
19
  Requires-Dist: btrack
22
- Requires-Dist: tensorflow~=2.15.0
23
20
  Requires-Dist: napari<0.6.0
24
21
  Requires-Dist: tqdm
25
22
  Requires-Dist: mahotas
@@ -48,6 +45,19 @@ Requires-Dist: prettyprint
48
45
  Requires-Dist: pandas
49
46
  Requires-Dist: matplotlib
50
47
  Requires-Dist: prettytable
48
+ Requires-Dist: scikit-image
49
+ Requires-Dist: natsort
50
+ Provides-Extra: tensorflow
51
+ Requires-Dist: tensorflow~=2.15.0; extra == "tensorflow"
52
+ Requires-Dist: stardist; extra == "tensorflow"
53
+ Provides-Extra: process
54
+ Requires-Dist: cellpose<3; extra == "process"
55
+ Requires-Dist: stardist; extra == "process"
56
+ Requires-Dist: tensorflow~=2.15.0; extra == "process"
57
+ Provides-Extra: all
58
+ Requires-Dist: cellpose<3; extra == "all"
59
+ Requires-Dist: stardist; extra == "all"
60
+ Requires-Dist: tensorflow~=2.15.0; extra == "all"
51
61
  Dynamic: author
52
62
  Dynamic: author-email
53
63
  Dynamic: description
@@ -55,6 +65,7 @@ Dynamic: description-content-type
55
65
  Dynamic: home-page
56
66
  Dynamic: license
57
67
  Dynamic: license-file
68
+ Dynamic: provides-extra
58
69
  Dynamic: requires-dist
59
70
  Dynamic: summary
60
71
 
@@ -148,10 +159,12 @@ To use the software, you must install python, *e.g.* through
148
159
 
149
160
  Celldetective requires a version of Python between 3.9 and 3.11 (included). If your Python version is older or more recent, consider using `conda` to create an environment as described below.
150
161
 
151
- With the proper Python version, Celldetective can be directly installed with `pip`:
162
+ With the proper Python version, Celldetective can be directly installed with `pip`.
163
+ **Important**: By default, `pip install celldetective` will **not** install deep-learning libraries (`tensorflow`, `cellpose`, `stardist`) to allow users to manage their own GPU environment (e.g. `torch`, `cuda`).
152
164
 
165
+ If you want the standard full installation (recommended for most users), use:
153
166
  ``` bash
154
- pip install celldetective
167
+ pip install celldetective[all]
155
168
  ```
156
169
 
157
170
  We recommend that you create an environment to use Celldetective, to protect your package versions and fix the Python version *e.g.*
@@ -160,7 +173,7 @@ with `conda`:
160
173
  ``` bash
161
174
  conda create -n celldetective python=3.11 pyqt
162
175
  conda activate celldetective
163
- pip install celldetective
176
+ pip install celldetective[all]
164
177
  ```
165
178
 
166
179
  Need an update? Simply type the following in the terminal (in your
@@ -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=i0XQAcqdHCcwV-JF_A_FeVVfTjXROj_WnqM2Ktzzq1Q,24
3
+ celldetective/_version.py,sha256=2t7jfGYhecNEZFNHUmz-D7GUPyvfrkDim4ynpi-TYII,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
@@ -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
@@ -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=gHe9QBX6Lrn567Kc-9T6KoGuLqEP_1q4RROFBd9qLWE,29962
91
- celldetective/gui/viewers/channel_offset_viewer.py,sha256=aQpxrJX9qM_xX848XnFrxrhTqE22DIshtD-uKL8e4gU,12423
90
+ celldetective/gui/viewers/base_viewer.py,sha256=wA2Zrje5nQSUmwnAFCe9hcL1MOWg8uMKwFTWhEYiMXI,32159
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
@@ -143,7 +143,7 @@ celldetective/utils/color_mappings.py,sha256=yarqOTSrTsnOPPiPrrN_vLoPCbgWqF3wjqF
143
143
  celldetective/utils/data_cleaning.py,sha256=K-2gScxLreX7QkrM0h3dZdP0IsmvCzcxNh2-M9PALZY,22025
144
144
  celldetective/utils/data_loaders.py,sha256=6Jg99U93qYjjs2xZErc2cz37tcH5e4vEqDH8PJgoEJs,17077
145
145
  celldetective/utils/dataset_helpers.py,sha256=3ezpHO6nytw2Mx0D3maP_4535V2ohOTQn6Qpfk8gnms,6898
146
- celldetective/utils/downloaders.py,sha256=BIl_8XCeaKvtMVC36WITT2g5-O-n0csoMEQXVoa1B4o,7887
146
+ celldetective/utils/downloaders.py,sha256=o5sogEeYr-LR8mp1D7uNHH1aUJN3V82VVwYZvoSNTkQ,9506
147
147
  celldetective/utils/experiment.py,sha256=bgADS70QuW4KGbzDJbVpVM-tw4qmZKMWtDT1cSxugrY,58342
148
148
  celldetective/utils/image_augmenters.py,sha256=USYd8z6dVn5z1x96IYJ4mG0smN9I_S21QMGU0wyHmjc,11654
149
149
  celldetective/utils/image_cleaning.py,sha256=KliQ3K5hdwPx4eFxJnmg3yi-ZIoimEveunPJkbbA6wA,2388
@@ -161,12 +161,12 @@ 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=MeRDojrAkBSXe-wlLu5KIih5wXP9B2aPdP39JLYpoGE,5417
164
+ celldetective/utils/cellpose_utils/__init__.py,sha256=XUG6T9RFwJ2H2jrz9eO58gYHjI6vxMe93ZeDD35DChg,5999
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
- celldetective/utils/stardist_utils/__init__.py,sha256=e9s3DEaTKCUOGZb5k_DgShBTl4B0U-Jmg3Ioo8D5PyE,3978
169
- celldetective-1.5.0b4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
168
+ celldetective/utils/stardist_utils/__init__.py,sha256=eQiZz7UQbwkFS6zXOJFofoNm2wE7ylt-7hdCOd9uOWc,4304
169
+ celldetective-1.5.0b6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
170
170
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
171
171
  tests/test_cellpose_fallback.py,sha256=BJZTDFF8sFR1x7rDbvZQ2RQOB1OP6wuFBRfc8zbl5zw,3513
172
172
  tests/test_events.py,sha256=eLFwwEEJfQAdwhews3-fn1HSvzozcNNFN_Qn0gOvQkE,685
@@ -175,6 +175,7 @@ tests/test_io.py,sha256=gk5FmoI7ANEczUtNXYRxc48KzkfYzemwS_eYaLq4_NI,2093
175
175
  tests/test_measure.py,sha256=FEUAs1rVHylvIvubCb0bJDNGZLVmkgXNgI3NaGQ1dA8,4542
176
176
  tests/test_neighborhood.py,sha256=gk5FmoI7ANEczUtNXYRxc48KzkfYzemwS_eYaLq4_NI,2093
177
177
  tests/test_notebooks.py,sha256=7HVmYiytsz0QIJ11iRkGGs4_hzNjofXAUs_OZou3Gm0,301
178
+ tests/test_partial_install.py,sha256=G69-GNcJ9YNgs6K2bVTEZO0Jpb14xMRQWTm8A6VuIco,2841
178
179
  tests/test_preprocessing.py,sha256=c0rKS9d5h37uDcV7fVOTnn5GMVbEB84b8ZTCTdRmvFs,1422
179
180
  tests/test_segmentation.py,sha256=k1b_zIZdlytEdJcHjAUQEO3gTBAHtv5WvrwQN2xD4kc,3470
180
181
  tests/test_signals.py,sha256=No4cah6KxplhDcKXnU8RrA7eDla4hWw6ccf7xGnBokU,3599
@@ -185,8 +186,8 @@ tests/gui/test_enhancements.py,sha256=3x9au_rkQtMZ94DRj3OaEHKPr511RrWqBAUAcNQn1y
185
186
  tests/gui/test_measure_annotator_bugfix.py,sha256=tPfgWNKC0UkvrVssSrUcVDC1qgpzx6l2yCqvKtKYkM4,4544
186
187
  tests/gui/test_new_project.py,sha256=wRjW2vEaZb0LWT-f8G8-Ptk8CW9z8-FDPLpV5uqj6ck,8778
187
188
  tests/gui/test_project.py,sha256=KzAnodIc0Ovta0ARL5Kr5PkOR5euA6qczT_GhEZpyE4,4710
188
- celldetective-1.5.0b4.dist-info/METADATA,sha256=VgzQFDS1ExiiQdY4MjqmWfrRTC-MJQR1meL7t0Q8eMY,10947
189
- celldetective-1.5.0b4.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
190
- celldetective-1.5.0b4.dist-info/entry_points.txt,sha256=2NU6_EOByvPxqBbCvjwxlVlvnQreqZ3BKRCVIKEv3dg,62
191
- celldetective-1.5.0b4.dist-info/top_level.txt,sha256=6rsIKKfGMKgud7HPuATcpq6EhdXwcg_yknBVWn9x4C4,20
192
- celldetective-1.5.0b4.dist-info/RECORD,,
189
+ celldetective-1.5.0b6.dist-info/METADATA,sha256=7e7KJTEzTLZx7T0o4ITML6CI6PM8EwXU4ENpB0uymNc,11691
190
+ celldetective-1.5.0b6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
191
+ celldetective-1.5.0b6.dist-info/entry_points.txt,sha256=2NU6_EOByvPxqBbCvjwxlVlvnQreqZ3BKRCVIKEv3dg,62
192
+ celldetective-1.5.0b6.dist-info/top_level.txt,sha256=6rsIKKfGMKgud7HPuATcpq6EhdXwcg_yknBVWn9x4C4,20
193
+ celldetective-1.5.0b6.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,75 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+ import sys
4
+ import importlib
5
+
6
+
7
+ class TestPartialValidation(unittest.TestCase):
8
+
9
+ def test_imports_without_extras(self):
10
+ """Test that main modules can be imported even if optional extras are missing."""
11
+ # This test assumes the environment MIGHT have them, so we must mock them as missing
12
+ # to ensure the code handles it.
13
+
14
+ with patch.dict(
15
+ sys.modules,
16
+ {
17
+ "tensorflow": None,
18
+ "torch": None,
19
+ "stardist": None,
20
+ "cellpose": None,
21
+ "cellpose.models": None,
22
+ "stardist.models": None,
23
+ },
24
+ ):
25
+ # Force reload of celldetective.segmentation to test its imports
26
+ try:
27
+ import celldetective.segmentation
28
+
29
+ importlib.reload(celldetective.segmentation)
30
+ except ImportError as e:
31
+ self.fail(
32
+ f"Could not import celldetective.segmentation without extras: {e}"
33
+ )
34
+ except Exception as e:
35
+ self.fail(f"Unexpected error importing celldetective.segmentation: {e}")
36
+
37
+ def test_graceful_failure_stardist(self):
38
+ """Test that calling stardist functions raises RuntimeError if missing."""
39
+ with patch.dict(sys.modules, {"stardist": None, "stardist.models": None}):
40
+ # We need to reload the util module to pick up the missing module
41
+ import celldetective.utils.stardist_utils
42
+
43
+ importlib.reload(celldetective.utils.stardist_utils)
44
+
45
+ from celldetective.utils.stardist_utils import _prep_stardist_model
46
+
47
+ with self.assertRaises(RuntimeError) as cm:
48
+ _prep_stardist_model("fake_model", "fake_path")
49
+
50
+ self.assertIn("StarDist is not installed", str(cm.exception))
51
+
52
+ def test_graceful_failure_cellpose(self):
53
+ """Test that calling cellpose functions raises RuntimeError if missing."""
54
+ with patch.dict(
55
+ sys.modules, {"cellpose": None, "cellpose.models": None, "torch": None}
56
+ ):
57
+ import celldetective.utils.cellpose_utils
58
+
59
+ importlib.reload(celldetective.utils.cellpose_utils)
60
+
61
+ from celldetective.utils.cellpose_utils import _prep_cellpose_model
62
+
63
+ with self.assertRaises(RuntimeError) as cm:
64
+ _prep_cellpose_model("fake_model", "fake_path")
65
+
66
+ # Message check might correspond to torch or cellpose depending on which import hits first
67
+ # Our code checks torch first.
68
+ self.assertTrue(
69
+ "Torch is not installed" in str(cm.exception)
70
+ or "Cellpose is not installed" in str(cm.exception)
71
+ )
72
+
73
+
74
+ if __name__ == "__main__":
75
+ unittest.main()