napari-tmidas 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. napari_tmidas/__init__.py +35 -5
  2. napari_tmidas/_crop_anything.py +1520 -609
  3. napari_tmidas/_env_manager.py +76 -0
  4. napari_tmidas/_file_conversion.py +1646 -1131
  5. napari_tmidas/_file_selector.py +1455 -216
  6. napari_tmidas/_label_inspection.py +83 -8
  7. napari_tmidas/_processing_worker.py +309 -0
  8. napari_tmidas/_reader.py +6 -10
  9. napari_tmidas/_registry.py +2 -2
  10. napari_tmidas/_roi_colocalization.py +1221 -84
  11. napari_tmidas/_tests/test_crop_anything.py +123 -0
  12. napari_tmidas/_tests/test_env_manager.py +89 -0
  13. napari_tmidas/_tests/test_grid_view_overlay.py +193 -0
  14. napari_tmidas/_tests/test_init.py +98 -0
  15. napari_tmidas/_tests/test_intensity_label_filter.py +222 -0
  16. napari_tmidas/_tests/test_label_inspection.py +86 -0
  17. napari_tmidas/_tests/test_processing_basic.py +500 -0
  18. napari_tmidas/_tests/test_processing_worker.py +142 -0
  19. napari_tmidas/_tests/test_regionprops_analysis.py +547 -0
  20. napari_tmidas/_tests/test_registry.py +70 -2
  21. napari_tmidas/_tests/test_scipy_filters.py +168 -0
  22. napari_tmidas/_tests/test_skimage_filters.py +259 -0
  23. napari_tmidas/_tests/test_split_channels.py +217 -0
  24. napari_tmidas/_tests/test_spotiflow.py +87 -0
  25. napari_tmidas/_tests/test_tyx_display_fix.py +142 -0
  26. napari_tmidas/_tests/test_ui_utils.py +68 -0
  27. napari_tmidas/_tests/test_widget.py +30 -0
  28. napari_tmidas/_tests/test_windows_basic.py +66 -0
  29. napari_tmidas/_ui_utils.py +57 -0
  30. napari_tmidas/_version.py +16 -3
  31. napari_tmidas/_widget.py +41 -4
  32. napari_tmidas/processing_functions/basic.py +557 -20
  33. napari_tmidas/processing_functions/careamics_env_manager.py +72 -99
  34. napari_tmidas/processing_functions/cellpose_env_manager.py +415 -112
  35. napari_tmidas/processing_functions/cellpose_segmentation.py +132 -191
  36. napari_tmidas/processing_functions/colocalization.py +513 -56
  37. napari_tmidas/processing_functions/grid_view_overlay.py +703 -0
  38. napari_tmidas/processing_functions/intensity_label_filter.py +422 -0
  39. napari_tmidas/processing_functions/regionprops_analysis.py +1280 -0
  40. napari_tmidas/processing_functions/sam2_env_manager.py +53 -69
  41. napari_tmidas/processing_functions/sam2_mp4.py +274 -195
  42. napari_tmidas/processing_functions/scipy_filters.py +403 -8
  43. napari_tmidas/processing_functions/skimage_filters.py +424 -212
  44. napari_tmidas/processing_functions/spotiflow_detection.py +949 -0
  45. napari_tmidas/processing_functions/spotiflow_env_manager.py +591 -0
  46. napari_tmidas/processing_functions/timepoint_merger.py +334 -86
  47. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/METADATA +70 -30
  48. napari_tmidas-0.2.4.dist-info/RECORD +63 -0
  49. napari_tmidas/_tests/__init__.py +0 -0
  50. napari_tmidas-0.2.2.dist-info/RECORD +0 -40
  51. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/WHEEL +0 -0
  52. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
  53. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
  54. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/top_level.txt +0 -0
@@ -5,152 +5,439 @@ Updated to support Cellpose 4 (Cellpose-SAM) installation.
5
5
  """
6
6
 
7
7
  import os
8
- import platform
9
- import shutil
10
8
  import subprocess
9
+ import sys
11
10
  import tempfile
12
- import venv
11
+ import threading
12
+ from contextlib import suppress
13
13
 
14
14
  import tifffile
15
15
 
16
- # Define the environment directory in user's home folder
17
- ENV_DIR = os.path.join(
18
- os.path.expanduser("~"), ".napari-tmidas", "envs", "cellpose"
19
- )
16
+ from napari_tmidas._env_manager import BaseEnvironmentManager
17
+
18
+ # Global variable to track running processes for cancellation
19
+ _running_processes = []
20
+ _process_lock = threading.Lock()
21
+
22
+
23
+ def cancel_all_processes():
24
+ """Cancel all running cellpose processes."""
25
+ with _process_lock:
26
+ for process in _running_processes[
27
+ :
28
+ ]: # Copy list to avoid modification during iteration
29
+ try:
30
+ if process.poll() is None: # Process is still running
31
+ process.terminate()
32
+ # Give it a moment to terminate gracefully
33
+ try:
34
+ process.wait(timeout=5)
35
+ except subprocess.TimeoutExpired:
36
+ # Force kill if it doesn't terminate gracefully
37
+ process.kill()
38
+ process.wait()
39
+ _running_processes.remove(process)
40
+ except (OSError, subprocess.SubprocessError) as e:
41
+ print(f"Error terminating process: {e}")
42
+
43
+
44
+ def _add_process(process):
45
+ """Add a process to the tracking list."""
46
+ with _process_lock:
47
+ _running_processes.append(process)
48
+
49
+
50
+ def _remove_process(process):
51
+ """Remove a process from the tracking list."""
52
+ with _process_lock:
53
+ if process in _running_processes:
54
+ _running_processes.remove(process)
55
+
56
+
57
+ class CellposeEnvironmentManager(BaseEnvironmentManager):
58
+ """Environment manager for Cellpose."""
59
+
60
+ def __init__(self):
61
+ super().__init__("cellpose")
62
+
63
+ def _install_dependencies(self, env_python: str) -> None:
64
+ """Install Cellpose-specific dependencies."""
65
+ # Install cellpose 4 and other dependencies
66
+ print(
67
+ "Installing Cellpose 4 (Cellpose-SAM) in the dedicated environment..."
68
+ )
69
+
70
+ # Install packages one by one with error checking
71
+ packages = ["cellpose", "zarr", "tifffile"]
72
+ for package in packages:
73
+ print(f"Installing {package}...")
74
+ try:
75
+ subprocess.check_call(
76
+ [env_python, "-m", "pip", "install", package]
77
+ )
78
+ print(f"✓ {package} installed successfully")
79
+ except subprocess.CalledProcessError as e:
80
+ print(f"✗ Failed to install {package}: {e}")
81
+ raise
82
+
83
+ # Verify installations
84
+ print("Verifying installations...")
85
+
86
+ # Check cellpose
87
+ try:
88
+ result = subprocess.run(
89
+ [
90
+ env_python,
91
+ "-c",
92
+ "from cellpose import core; print(f'GPU available: {core.use_gpu()}')",
93
+ ],
94
+ capture_output=True,
95
+ text=True,
96
+ check=True,
97
+ )
98
+ print("✓ Cellpose installation verified:")
99
+ print(result.stdout)
100
+ except subprocess.CalledProcessError as e:
101
+ print(f"✗ Cellpose verification failed: {e}")
102
+ raise
103
+
104
+ # Check zarr
105
+ try:
106
+ result = subprocess.run(
107
+ [
108
+ env_python,
109
+ "-c",
110
+ "import zarr; print(f'Zarr version: {zarr.__version__}')",
111
+ ],
112
+ capture_output=True,
113
+ text=True,
114
+ check=True,
115
+ )
116
+ print("✓ Zarr installation verified:")
117
+ print(result.stdout)
118
+ except subprocess.CalledProcessError as e:
119
+ print(f"✗ Zarr verification failed: {e}")
120
+ raise
121
+
122
+ # Check tifffile
123
+ try:
124
+ result = subprocess.run(
125
+ [
126
+ env_python,
127
+ "-c",
128
+ "import tifffile; print(f'Tifffile version: {tifffile.__version__}')",
129
+ ],
130
+ capture_output=True,
131
+ text=True,
132
+ check=True,
133
+ )
134
+ print("✓ Tifffile installation verified:")
135
+ print(result.stdout)
136
+ except subprocess.CalledProcessError as e:
137
+ print(f"✗ Tifffile verification failed: {e}")
138
+ raise
139
+
140
+ def is_package_installed(self) -> bool:
141
+ """Check if cellpose is installed in the current environment."""
142
+ try:
143
+ import importlib.util
144
+
145
+ return importlib.util.find_spec("cellpose") is not None
146
+ except ImportError:
147
+ return False
148
+
149
+ def are_all_packages_installed(self) -> bool:
150
+ """Check if all required packages are installed in the dedicated environment."""
151
+ if not self.is_env_created():
152
+ return False
153
+
154
+ env_python = self.get_env_python_path()
155
+ required_packages = ["cellpose", "zarr", "tifffile"]
156
+
157
+ for package in required_packages:
158
+ try:
159
+ subprocess.run(
160
+ [env_python, "-c", f"import {package}"],
161
+ capture_output=True,
162
+ text=True,
163
+ check=True,
164
+ )
165
+ except subprocess.CalledProcessError:
166
+ print(f"Missing package in cellpose environment: {package}")
167
+ return False
168
+
169
+ return True
170
+
171
+ def reinstall_packages(self) -> None:
172
+ """Force reinstall all packages in the dedicated environment."""
173
+ if not self.is_env_created():
174
+ print("Environment not created. Creating new environment...")
175
+ self.create_env()
176
+ return
177
+
178
+ env_python = self.get_env_python_path()
179
+ print("Force reinstalling packages in cellpose environment...")
180
+ self._install_dependencies(env_python)
181
+
182
+
183
+ # Global instance for backward compatibility
184
+ manager = CellposeEnvironmentManager()
20
185
 
21
186
 
22
187
  def is_cellpose_installed():
23
188
  """Check if cellpose is installed in the current environment."""
24
- try:
25
- import importlib.util
26
-
27
- return importlib.util.find_spec("cellpose") is not None
28
- except ImportError:
29
- return False
189
+ return manager.is_package_installed()
30
190
 
31
191
 
32
192
  def is_env_created():
33
193
  """Check if the dedicated environment exists."""
34
- env_python = get_env_python_path()
35
- return os.path.exists(env_python)
194
+ return manager.is_env_created()
36
195
 
37
196
 
38
197
  def get_env_python_path():
39
198
  """Get the path to the Python executable in the environment."""
40
- if platform.system() == "Windows":
41
- return os.path.join(ENV_DIR, "Scripts", "python.exe")
42
- else:
43
- return os.path.join(ENV_DIR, "bin", "python")
199
+ return manager.get_env_python_path()
44
200
 
45
201
 
46
202
  def create_cellpose_env():
47
203
  """Create a dedicated virtual environment for Cellpose."""
48
- # Ensure the environment directory exists
49
- os.makedirs(os.path.dirname(ENV_DIR), exist_ok=True)
50
-
51
- # Remove existing environment if it exists
52
- if os.path.exists(ENV_DIR):
53
- shutil.rmtree(ENV_DIR)
54
-
55
- print(f"Creating Cellpose environment at {ENV_DIR}...")
204
+ return manager.create_env()
56
205
 
57
- # Create a new virtual environment
58
- venv.create(ENV_DIR, with_pip=True)
59
206
 
60
- # Path to the Python executable in the new environment
61
- env_python = get_env_python_path()
62
-
63
- # # Install numpy first to ensure correct version
64
- # print("Installing NumPy...")
65
- # subprocess.check_call(
66
- # [env_python, "-m", "pip", "install", "--upgrade", "pip"]
67
- # )
68
- # subprocess.check_call(
69
- # [env_python, "-m", "pip", "install", "numpy>=1.24,<1.25"]
70
- # )
71
-
72
- # Install cellpose 4 and other dependencies
73
- print(
74
- "Installing Cellpose 4 (Cellpose-SAM) in the dedicated environment..."
75
- )
76
- subprocess.check_call([env_python, "-m", "pip", "install", "cellpose"])
207
+ def check_cellpose_packages():
208
+ """Check if all required packages are installed in the cellpose environment."""
209
+ return manager.are_all_packages_installed()
77
210
 
78
- # # Install PyTorch - needed for GPU acceleration
79
- # print("Installing PyTorch for GPU acceleration...")
80
- # subprocess.check_call([
81
- # env_python, "-m", "pip", "install",
82
- # "torch", "torchvision"
83
- # ])
84
211
 
85
- # # Install tifffile for image handling
86
- # subprocess.check_call([env_python, "-m", "pip", "install", "tifffile"])
212
+ def reinstall_cellpose_packages():
213
+ """Force reinstall all packages in the cellpose environment."""
214
+ return manager.reinstall_packages()
87
215
 
88
- # Check if installation was successful
89
- try:
90
- # Run a command to check if cellpose can be imported and GPU is available
91
- result = subprocess.run(
92
- [
93
- env_python,
94
- "-c",
95
- "from cellpose import core; print(f'GPU available: {core.use_gpu()}')",
96
- ],
97
- capture_output=True,
98
- text=True,
99
- check=True,
100
- )
101
- print(f"Cellpose environment check: {result.stdout.strip()}")
102
- except subprocess.CalledProcessError as e:
103
- print(f"Warning: Cellpose installation check failed: {e.stderr}")
104
216
 
105
- print("Cellpose environment created successfully.")
106
- return env_python
217
+ def cancel_cellpose_processing():
218
+ """Cancel all running cellpose processes."""
219
+ cancel_all_processes()
107
220
 
108
221
 
109
222
  def run_cellpose_in_env(func_name, args_dict):
110
223
  """
111
- Run Cellpose in a dedicated environment with minimal complexity.
112
-
113
- Parameters:
114
- -----------
115
- func_name : str
116
- Name of the Cellpose function to run (currently unused)
117
- args_dict : dict
118
- Dictionary of arguments for Cellpose segmentation
119
-
120
- Returns:
121
- --------
122
- numpy.ndarray
123
- Segmentation masks
224
+ Run Cellpose in a dedicated environment with optimized zarr support.
124
225
  """
125
226
  # Ensure the environment exists
126
227
  if not is_env_created():
127
228
  create_cellpose_env()
128
229
 
129
- # Prepare temporary files
130
- with tempfile.NamedTemporaryFile(
131
- suffix=".tif", delete=False
132
- ) as input_file, tempfile.NamedTemporaryFile(
133
- suffix=".tif", delete=False
134
- ) as output_file, tempfile.NamedTemporaryFile(
135
- mode="w", suffix=".py", delete=False
136
- ) as script_file:
230
+ # Check if all required packages are installed
231
+ if not manager.are_all_packages_installed():
232
+ print("Missing packages detected. Reinstalling...")
233
+ manager.reinstall_packages()
234
+
235
+ # Check for zarr optimization
236
+ use_zarr_direct = "zarr_path" in args_dict
237
+
238
+ if use_zarr_direct:
239
+ zarr_path = args_dict["zarr_path"]
240
+ print(f"Using optimized zarr processing for: {zarr_path}")
241
+ return run_zarr_processing(zarr_path, args_dict)
242
+ else:
243
+ return run_legacy_processing(args_dict)
137
244
 
138
- # Save input image
139
- tifffile.imwrite(input_file.name, args_dict["image"])
140
245
 
141
- # Prepare a temporary script to run Cellpose
142
- # Updated to use Cellpose 4 parameters
246
+ def run_zarr_processing(zarr_path, args_dict):
247
+ """Process zarr files directly without temporary input files."""
248
+
249
+ with (
250
+ tempfile.NamedTemporaryFile(
251
+ suffix=".tif", delete=False
252
+ ) as output_file,
253
+ tempfile.NamedTemporaryFile(
254
+ mode="w", suffix=".py", delete=False
255
+ ) as script_file,
256
+ ):
257
+
258
+ # Create zarr processing script (similar to working TIFF script)
143
259
  script = f"""
144
260
  import numpy as np
261
+ import sys
145
262
  from cellpose import models, core
146
263
  import tifffile
264
+ import zarr
265
+
266
+ # Force output to flush immediately for real-time progress
267
+ import sys
268
+ sys.stdout.flush()
269
+
270
+ print("=== Cellpose Environment Info ===")
271
+ print(f"GPU available in dedicated environment: {{core.use_gpu()}}")
272
+ sys.stdout.flush()
273
+
274
+ def process_volume(image, name=""):
275
+ print(f"\\nProcessing {{name}}: shape={{image.shape}}, range={{np.min(image):.1f}}-{{np.max(image):.1f}}")
276
+ sys.stdout.flush()
277
+
278
+ # GPU detection IN THE DEDICATED ENVIRONMENT (not main environment)
279
+ gpu_available = core.use_gpu()
280
+ use_gpu_requested = {str(args_dict.get('use_gpu', True))} # Convert to string for f-string
281
+ actual_use_gpu = use_gpu_requested and gpu_available
282
+
283
+ print(f"GPU: requested={{use_gpu_requested}}, available={{gpu_available}}, using={{actual_use_gpu}}")
284
+ sys.stdout.flush()
285
+
286
+ # Create model with explicit GPU setting
287
+ print("Creating Cellpose model...")
288
+ sys.stdout.flush()
289
+ model = models.CellposeModel(gpu=actual_use_gpu)
290
+ print(f"Model created (GPU={{model.gpu}})")
291
+ sys.stdout.flush()
292
+
293
+ print("Running segmentation...")
294
+ sys.stdout.flush()
295
+
296
+ # Remove deprecated channels parameter for Cellpose v4.0.1+
297
+ masks, flows, styles = model.eval(
298
+ image,
299
+ # channels parameter removed - deprecated in v4.0.1+
300
+ flow_threshold={args_dict.get('flow_threshold', 0.4)},
301
+ cellprob_threshold={args_dict.get('cellprob_threshold', 0.0)},
302
+ batch_size={args_dict.get('batch_size', 32)},
303
+ normalize={args_dict.get('normalize', {'tile_norm_blocksize': 128})},
304
+ do_3D={args_dict.get('do_3D', False)},
305
+ flow3D_smooth={args_dict.get('flow3D_smooth', 0)},
306
+ anisotropy={args_dict.get('anisotropy', None)},
307
+ z_axis={args_dict.get('z_axis', 0)} if {args_dict.get('do_3D', False)} else None,
308
+ channel_axis={args_dict.get('channel_axis', None)}
309
+ )
147
310
 
311
+ object_count = np.max(masks)
312
+ print(f"Found {{object_count}} objects in {{name}}")
313
+ sys.stdout.flush()
314
+ return masks
315
+
316
+ print("Opening zarr: {zarr_path}")
317
+ sys.stdout.flush()
318
+ zarr_root = zarr.open('{zarr_path}', mode='r')
319
+
320
+ if hasattr(zarr_root, 'shape'):
321
+ image = np.array(zarr_root)
322
+ result = process_volume(image, "zarr")
323
+ else:
324
+ arrays = list(zarr_root.array_keys())
325
+ print(f"Arrays: {{arrays}}")
326
+ sys.stdout.flush()
327
+ zarr_array = zarr_root[arrays[0]]
328
+ print(f"Selected: {{arrays[0]}}, shape={{zarr_array.shape}}")
329
+ sys.stdout.flush()
330
+
331
+ if len(zarr_array.shape) == 5: # TCZYX
332
+ T, C, Z, Y, X = zarr_array.shape
333
+ print(f"5D TCZYX: T={{T}}, C={{C}}, Z={{Z}}, Y={{Y}}, X={{X}}")
334
+ print(f"Will process {{T*C}} T,C combinations")
335
+ sys.stdout.flush()
336
+ result = np.zeros((T, C, Z, Y, X), dtype=np.uint32)
337
+
338
+ for t in range(T):
339
+ for c in range(C):
340
+ print(f"\\n=== T={{t+1}}/{{T}}, C={{c+1}}/{{C}} ===")
341
+ sys.stdout.flush()
342
+ image = np.array(zarr_array[t, c, :, :, :])
343
+ masks = process_volume(image, f"T{{t+1}}C{{c+1}}")
344
+ result[t, c] = masks
345
+
346
+ elif len(zarr_array.shape) == 4: # 4D
347
+ dim1, Z, Y, X = zarr_array.shape
348
+ print(f"4D array: dim1={{dim1}}, Z={{Z}}, Y={{Y}}, X={{X}}")
349
+ sys.stdout.flush()
350
+ result = np.zeros((dim1, Z, Y, X), dtype=np.uint32)
351
+
352
+ for i in range(dim1):
353
+ print(f"\\n=== Volume {{i+1}}/{{dim1}} ===")
354
+ sys.stdout.flush()
355
+ image = np.array(zarr_array[i, :, :, :])
356
+ masks = process_volume(image, f"Vol{{i+1}}")
357
+ result[i] = masks
358
+ else:
359
+ image = np.array(zarr_array)
360
+ result = process_volume(image, "3D")
361
+
362
+ print(f"Saving results: shape={{result.shape}}, total objects={{np.max(result)}}")
363
+ sys.stdout.flush()
364
+ tifffile.imwrite('{output_file.name}', result.astype(np.uint32))
365
+ print("Complete!")
366
+ sys.stdout.flush()
367
+ """
148
368
 
369
+ script_file.write(script)
370
+ script_file.flush()
371
+
372
+ try:
373
+ # Run with REAL-TIME output (don't capture, let it stream)
374
+ env_python = get_env_python_path()
375
+ print("=== Starting Cellpose Processing ===")
376
+
377
+ # Use Popen for real-time progress and cancellation support
378
+ process = subprocess.Popen(
379
+ [env_python, script_file.name],
380
+ cwd=os.getcwd(),
381
+ stdout=sys.stdout,
382
+ stderr=sys.stderr,
383
+ text=True,
384
+ )
385
+
386
+ # Track the process for cancellation
387
+ _add_process(process)
388
+
389
+ try:
390
+ # Wait for completion
391
+ returncode = process.wait()
392
+
393
+ if returncode != 0:
394
+ raise RuntimeError(
395
+ f"Cellpose failed with return code {returncode}"
396
+ )
397
+
398
+ finally:
399
+ # Remove from tracking regardless of outcome
400
+ _remove_process(process)
401
+
402
+ # Check if output file was created
403
+ if not os.path.exists(output_file.name):
404
+ raise RuntimeError("Output file was not created")
405
+
406
+ return tifffile.imread(output_file.name)
407
+
408
+ finally:
409
+ # Cleanup
410
+ for fname in [output_file.name, script_file.name]:
411
+ with suppress(OSError, FileNotFoundError):
412
+ os.unlink(fname)
413
+
414
+
415
+ def run_legacy_processing(args_dict):
416
+ """Legacy processing for numpy arrays (original working TIFF approach)."""
417
+
418
+ with (
419
+ tempfile.NamedTemporaryFile(suffix=".tif", delete=False) as input_file,
420
+ tempfile.NamedTemporaryFile(
421
+ suffix=".tif", delete=False
422
+ ) as output_file,
423
+ tempfile.NamedTemporaryFile(
424
+ mode="w", suffix=".py", delete=False
425
+ ) as script_file,
426
+ ):
427
+
428
+ # Save input image (exactly like original working code)
429
+ tifffile.imwrite(input_file.name, args_dict["image"])
430
+
431
+ # Create script (exactly like original working code)
432
+ script = f"""
433
+ import numpy as np
434
+ from cellpose import models, core
435
+ import tifffile
149
436
 
150
437
  # Load image
151
438
  image = tifffile.imread('{input_file.name}')
152
439
 
153
- # Create and run model
440
+ # Create and run model (exactly like original working code)
154
441
  model = models.CellposeModel(
155
442
  gpu={args_dict.get('use_gpu', True)})
156
443
 
@@ -166,7 +453,10 @@ masks, flows, styles = model.eval(
166
453
  batch_size={args_dict.get('batch_size', 32)},
167
454
  normalize=normalize,
168
455
  do_3D={args_dict.get('do_3D', False)},
169
- z_axis={args_dict.get('z_axis', 0)} if {args_dict.get('do_3D', False)} else None
456
+ flow3D_smooth={args_dict.get('flow3D_smooth', 0)},
457
+ anisotropy={args_dict.get('anisotropy', None)},
458
+ z_axis={args_dict.get('z_axis', 0)} if {args_dict.get('do_3D', False)} else None,
459
+ channel_axis={args_dict.get('channel_axis', None)}
170
460
  )
171
461
 
172
462
  # Save results
@@ -178,18 +468,33 @@ tifffile.imwrite('{output_file.name}', masks)
178
468
  script_file.flush()
179
469
 
180
470
  try:
181
- # Run the script in the dedicated environment
471
+ # Run the script with cancellation support
182
472
  env_python = get_env_python_path()
183
- result = subprocess.run(
184
- [env_python, script_file.name], capture_output=True, text=True
473
+
474
+ # Use Popen for cancellation support
475
+ process = subprocess.Popen(
476
+ [env_python, script_file.name],
477
+ stdout=subprocess.PIPE,
478
+ stderr=subprocess.PIPE,
479
+ text=True,
185
480
  )
186
- print("Stdout:", result.stdout)
187
- # Check for errors
188
- if result.returncode != 0:
189
- print("Stderr:", result.stderr)
190
- raise RuntimeError(
191
- f"Cellpose segmentation failed: {result.stderr}"
192
- )
481
+
482
+ # Track the process for cancellation
483
+ _add_process(process)
484
+
485
+ try:
486
+ # Wait for completion and get output
487
+ stdout, stderr = process.communicate()
488
+ print("Stdout:", stdout)
489
+
490
+ # Check for errors
491
+ if process.returncode != 0:
492
+ print("Stderr:", stderr)
493
+ raise RuntimeError(f"Cellpose segmentation failed: {stderr}")
494
+
495
+ finally:
496
+ # Remove from tracking regardless of outcome
497
+ _remove_process(process)
193
498
 
194
499
  # Read and return the results
195
500
  return tifffile.imread(output_file.name)
@@ -199,9 +504,7 @@ tifffile.imwrite('{output_file.name}', masks)
199
504
  raise
200
505
 
201
506
  finally:
202
- # Clean up temporary files using contextlib.suppress
203
- from contextlib import suppress
204
-
507
+ # Clean up temporary files
205
508
  for fname in [input_file.name, output_file.name, script_file.name]:
206
509
  with suppress(OSError, FileNotFoundError):
207
510
  os.unlink(fname)