napari-tmidas 0.2.6__py3-none-any.whl → 0.3.1__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.
- napari_tmidas/_tests/test_intensity_label_filter.py +9 -11
- napari_tmidas/_tests/test_registry.py +6 -0
- napari_tmidas/_tests/test_viscy_virtual_staining.py +138 -0
- napari_tmidas/_version.py +2 -2
- napari_tmidas/processing_functions/__init__.py +24 -5
- napari_tmidas/processing_functions/cellpose_env_manager.py +22 -0
- napari_tmidas/processing_functions/intensity_label_filter.py +15 -4
- napari_tmidas/processing_functions/skimage_filters.py +71 -8
- napari_tmidas/processing_functions/viscy_env_manager.py +381 -0
- napari_tmidas/processing_functions/viscy_virtual_staining.py +393 -0
- napari_tmidas-0.3.1.dist-info/METADATA +246 -0
- {napari_tmidas-0.2.6.dist-info → napari_tmidas-0.3.1.dist-info}/RECORD +16 -13
- {napari_tmidas-0.2.6.dist-info → napari_tmidas-0.3.1.dist-info}/WHEEL +1 -1
- napari_tmidas-0.2.6.dist-info/METADATA +0 -278
- {napari_tmidas-0.2.6.dist-info → napari_tmidas-0.3.1.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.2.6.dist-info → napari_tmidas-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {napari_tmidas-0.2.6.dist-info → napari_tmidas-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
except ImportError:
|
|
17
|
-
HAS_KMEDOIDS = False
|
|
7
|
+
# Import the module and check if k-medoids is available
|
|
8
|
+
from napari_tmidas.processing_functions.intensity_label_filter import (
|
|
9
|
+
_HAS_KMEDOIDS,
|
|
10
|
+
_calculate_label_mean_intensities,
|
|
11
|
+
_cluster_intensities,
|
|
12
|
+
_filter_labels_by_threshold,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
HAS_KMEDOIDS = _HAS_KMEDOIDS
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
@pytest.mark.skipif(
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# src/napari_tmidas/_tests/test_registry.py
|
|
2
2
|
from napari_tmidas._registry import BatchProcessingRegistry
|
|
3
|
+
from napari_tmidas.processing_functions import discover_and_load_processing_functions
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class TestBatchProcessingRegistry:
|
|
6
7
|
def setup_method(self):
|
|
7
8
|
"""Clear registry before each test"""
|
|
8
9
|
BatchProcessingRegistry._processing_functions.clear()
|
|
10
|
+
|
|
11
|
+
def teardown_method(self):
|
|
12
|
+
"""Restore registry after each test"""
|
|
13
|
+
BatchProcessingRegistry._processing_functions.clear()
|
|
14
|
+
discover_and_load_processing_functions(reload=True)
|
|
9
15
|
|
|
10
16
|
def test_register_function(self):
|
|
11
17
|
"""Test registering a processing function"""
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Test VisCy virtual staining integration
|
|
2
|
+
"""
|
|
3
|
+
Tests for VisCy virtual staining processing function.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from napari_tmidas._registry import BatchProcessingRegistry
|
|
9
|
+
from napari_tmidas.processing_functions import discover_and_load_processing_functions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_viscy_registered():
|
|
13
|
+
"""Test that VisCy function is registered."""
|
|
14
|
+
# Ensure processing functions are loaded
|
|
15
|
+
discover_and_load_processing_functions()
|
|
16
|
+
|
|
17
|
+
functions = BatchProcessingRegistry.list_functions()
|
|
18
|
+
assert "VisCy Virtual Staining" in functions
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_viscy_function_info():
|
|
22
|
+
"""Test that VisCy function has correct metadata."""
|
|
23
|
+
# Ensure processing functions are loaded
|
|
24
|
+
discover_and_load_processing_functions()
|
|
25
|
+
|
|
26
|
+
info = BatchProcessingRegistry.get_function_info("VisCy Virtual Staining")
|
|
27
|
+
|
|
28
|
+
assert info is not None
|
|
29
|
+
assert "description" in info
|
|
30
|
+
assert "parameters" in info
|
|
31
|
+
assert info["suffix"] == "_virtual_stain"
|
|
32
|
+
|
|
33
|
+
# Check parameters
|
|
34
|
+
params = info["parameters"]
|
|
35
|
+
assert "dim_order" in params
|
|
36
|
+
assert "z_batch_size" in params
|
|
37
|
+
assert "output_channel" in params
|
|
38
|
+
|
|
39
|
+
# Check parameter defaults
|
|
40
|
+
assert params["dim_order"]["default"] == "ZYX"
|
|
41
|
+
assert params["z_batch_size"]["default"] == 15
|
|
42
|
+
assert params["output_channel"]["default"] == "both"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_viscy_import():
|
|
46
|
+
"""Test that VisCy modules can be imported."""
|
|
47
|
+
from napari_tmidas.processing_functions import viscy_env_manager
|
|
48
|
+
from napari_tmidas.processing_functions import viscy_virtual_staining
|
|
49
|
+
|
|
50
|
+
assert hasattr(viscy_env_manager, "ViscyEnvironmentManager")
|
|
51
|
+
assert hasattr(viscy_virtual_staining, "viscy_virtual_staining")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_viscy_dimension_validation():
|
|
55
|
+
"""Test that VisCy validates input dimensions."""
|
|
56
|
+
from napari_tmidas.processing_functions.viscy_virtual_staining import (
|
|
57
|
+
viscy_virtual_staining,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Test with 2D image (should fail)
|
|
61
|
+
image_2d = np.random.rand(512, 512)
|
|
62
|
+
with pytest.raises(ValueError, match="requires 3D images with Z dimension"):
|
|
63
|
+
viscy_virtual_staining(image_2d, dim_order="YX")
|
|
64
|
+
|
|
65
|
+
# Test with insufficient Z slices (should fail)
|
|
66
|
+
image_3d_small = np.random.rand(10, 512, 512) # Only 10 slices
|
|
67
|
+
with pytest.raises(ValueError, match="at least 15 Z slices"):
|
|
68
|
+
viscy_virtual_staining(image_3d_small, dim_order="ZYX")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_viscy_transpose_dimensions():
|
|
72
|
+
"""Test dimension transposition."""
|
|
73
|
+
from napari_tmidas.processing_functions.viscy_virtual_staining import (
|
|
74
|
+
transpose_dimensions,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Test ZYX (no change needed)
|
|
78
|
+
img = np.random.rand(15, 100, 100)
|
|
79
|
+
transposed, new_order, has_time = transpose_dimensions(img, "ZYX")
|
|
80
|
+
assert transposed.shape == (15, 100, 100)
|
|
81
|
+
assert new_order == "ZYX"
|
|
82
|
+
assert has_time is False
|
|
83
|
+
|
|
84
|
+
# Test YXZ (should transpose)
|
|
85
|
+
img = np.random.rand(100, 100, 15)
|
|
86
|
+
transposed, new_order, has_time = transpose_dimensions(img, "YXZ")
|
|
87
|
+
assert transposed.shape == (15, 100, 100)
|
|
88
|
+
assert new_order == "ZYX"
|
|
89
|
+
assert has_time is False
|
|
90
|
+
|
|
91
|
+
# Test TZYX (no change needed)
|
|
92
|
+
img = np.random.rand(5, 15, 100, 100)
|
|
93
|
+
transposed, new_order, has_time = transpose_dimensions(img, "TZYX")
|
|
94
|
+
assert transposed.shape == (5, 15, 100, 100)
|
|
95
|
+
assert new_order == "TZYX"
|
|
96
|
+
assert has_time is True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_viscy_env_manager():
|
|
100
|
+
"""Test VisCy environment manager."""
|
|
101
|
+
from napari_tmidas.processing_functions.viscy_env_manager import (
|
|
102
|
+
ViscyEnvironmentManager,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
manager = ViscyEnvironmentManager()
|
|
106
|
+
assert manager.env_name == "viscy"
|
|
107
|
+
assert "viscy" in manager.env_dir
|
|
108
|
+
assert "models" in manager.model_dir
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_viscy_output_channel_options():
|
|
112
|
+
"""Test that output channel options are correctly defined."""
|
|
113
|
+
# Ensure processing functions are loaded
|
|
114
|
+
discover_and_load_processing_functions()
|
|
115
|
+
|
|
116
|
+
info = BatchProcessingRegistry.get_function_info("VisCy Virtual Staining")
|
|
117
|
+
params = info["parameters"]
|
|
118
|
+
|
|
119
|
+
assert "output_channel" in params
|
|
120
|
+
assert "options" in params["output_channel"]
|
|
121
|
+
|
|
122
|
+
options = params["output_channel"]["options"]
|
|
123
|
+
assert "both" in options
|
|
124
|
+
assert "nuclei" in options
|
|
125
|
+
assert "membrane" in options
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
# Run basic tests
|
|
130
|
+
test_viscy_registered()
|
|
131
|
+
test_viscy_function_info()
|
|
132
|
+
test_viscy_import()
|
|
133
|
+
test_viscy_dimension_validation()
|
|
134
|
+
test_viscy_transpose_dimensions()
|
|
135
|
+
test_viscy_env_manager()
|
|
136
|
+
test_viscy_output_channel_options()
|
|
137
|
+
|
|
138
|
+
print("✓ All tests passed!")
|
napari_tmidas/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.3.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 1)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -5,16 +5,20 @@ Package for processing functions that can be registered with the batch processin
|
|
|
5
5
|
import importlib
|
|
6
6
|
import os
|
|
7
7
|
import pkgutil
|
|
8
|
+
import sys
|
|
8
9
|
from typing import Dict, List
|
|
9
10
|
|
|
10
11
|
# Keep the registry global
|
|
11
12
|
from napari_tmidas._registry import BatchProcessingRegistry
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def discover_and_load_processing_functions() -> List[str]:
|
|
15
|
+
def discover_and_load_processing_functions(reload: bool = False) -> List[str]:
|
|
15
16
|
"""
|
|
16
17
|
Discover and load all processing functions from the processing_functions package.
|
|
17
18
|
|
|
19
|
+
Args:
|
|
20
|
+
reload: If True, force reload of modules even if already imported
|
|
21
|
+
|
|
18
22
|
Returns:
|
|
19
23
|
List of registered function names
|
|
20
24
|
"""
|
|
@@ -27,12 +31,27 @@ def discover_and_load_processing_functions() -> List[str]:
|
|
|
27
31
|
):
|
|
28
32
|
if not is_pkg: # Only load non-package modules
|
|
29
33
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
module_fullname = f"{package}.{module_name}"
|
|
35
|
+
|
|
36
|
+
# If reload requested and module already loaded, reload it
|
|
37
|
+
if reload and module_fullname in sys.modules:
|
|
38
|
+
importlib.reload(sys.modules[module_fullname])
|
|
39
|
+
else:
|
|
40
|
+
# Import the module
|
|
41
|
+
importlib.import_module(module_fullname)
|
|
42
|
+
|
|
32
43
|
print(f"Loaded processing function module: {module_name}")
|
|
33
|
-
except ImportError as e:
|
|
44
|
+
except (ImportError, ValueError) as e:
|
|
34
45
|
# Log the error but continue with other modules
|
|
35
|
-
|
|
46
|
+
# ValueError catches NumPy binary incompatibility issues
|
|
47
|
+
error_msg = str(e)
|
|
48
|
+
if "numpy.dtype size changed" in error_msg:
|
|
49
|
+
print(
|
|
50
|
+
f"Failed to import {module_name}: NumPy binary incompatibility. "
|
|
51
|
+
"Try: pip install --force-reinstall --no-cache-dir scikit-learn-extra"
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
print(f"Failed to import {module_name}: {e}")
|
|
36
55
|
|
|
37
56
|
# Return the list of registered functions
|
|
38
57
|
return BatchProcessingRegistry.list_functions()
|
|
@@ -67,6 +67,28 @@ class CellposeEnvironmentManager(BaseEnvironmentManager):
|
|
|
67
67
|
"Installing Cellpose 4 (Cellpose-SAM) in the dedicated environment..."
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
+
# First, install PyTorch with CUDA 12.x support for sm_120 compatibility
|
|
71
|
+
# RTX PRO 4000 Blackwell has CUDA capability sm_120, which requires newer PyTorch
|
|
72
|
+
print("Installing PyTorch with CUDA 12.x support (for sm_120 compatibility)...")
|
|
73
|
+
try:
|
|
74
|
+
subprocess.check_call(
|
|
75
|
+
[
|
|
76
|
+
env_python,
|
|
77
|
+
"-m",
|
|
78
|
+
"pip",
|
|
79
|
+
"install",
|
|
80
|
+
"torch",
|
|
81
|
+
"torchvision",
|
|
82
|
+
"torchaudio",
|
|
83
|
+
"--index-url",
|
|
84
|
+
"https://download.pytorch.org/whl/cu124",
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
print("✓ PyTorch with CUDA 12.4 installed successfully")
|
|
88
|
+
except subprocess.CalledProcessError as e:
|
|
89
|
+
print(f"✗ Failed to install PyTorch: {e}")
|
|
90
|
+
raise
|
|
91
|
+
|
|
70
92
|
# Install packages one by one with error checking
|
|
71
93
|
packages = ["cellpose", "zarr", "tifffile"]
|
|
72
94
|
for package in packages:
|
|
@@ -47,12 +47,23 @@ try:
|
|
|
47
47
|
from sklearn_extra.cluster import KMedoids
|
|
48
48
|
|
|
49
49
|
_HAS_KMEDOIDS = True
|
|
50
|
-
except ImportError:
|
|
50
|
+
except (ImportError, ValueError) as e:
|
|
51
|
+
# ImportError: package not installed
|
|
52
|
+
# ValueError: binary incompatibility (e.g., numpy version mismatch)
|
|
51
53
|
KMedoids = None
|
|
52
54
|
_HAS_KMEDOIDS = False
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
error_msg = str(e)
|
|
56
|
+
if "numpy.dtype size changed" in error_msg:
|
|
57
|
+
print(
|
|
58
|
+
"scikit-learn-extra has a NumPy binary incompatibility. "
|
|
59
|
+
"This is typically resolved by reinstalling scikit-learn-extra. "
|
|
60
|
+
"Run: pip install --force-reinstall --no-cache-dir scikit-learn-extra"
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
print(
|
|
64
|
+
f"scikit-learn-extra not available ({type(e).__name__}). "
|
|
65
|
+
"Install with: pip install scikit-learn-extra"
|
|
66
|
+
)
|
|
56
67
|
|
|
57
68
|
try:
|
|
58
69
|
import pandas as pd
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Processing functions that depend on scikit-image.
|
|
4
4
|
"""
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
import os
|
|
7
|
+
|
|
5
8
|
import numpy as np
|
|
6
9
|
|
|
7
10
|
try:
|
|
@@ -61,7 +64,7 @@ if SKIMAGE_AVAILABLE:
|
|
|
61
64
|
Parameters
|
|
62
65
|
----------
|
|
63
66
|
image : np.ndarray
|
|
64
|
-
Input image
|
|
67
|
+
Input image (supports 2D, 3D, and 4D arrays like YX, ZYX, TYX, TZYX)
|
|
65
68
|
clip_limit : float
|
|
66
69
|
Clipping limit for contrast limiting (normalized to 0-1 range, e.g., 0.01 = 1%)
|
|
67
70
|
Higher values give more contrast but may amplify noise
|
|
@@ -72,9 +75,18 @@ if SKIMAGE_AVAILABLE:
|
|
|
72
75
|
-------
|
|
73
76
|
np.ndarray
|
|
74
77
|
CLAHE-enhanced image with same dtype as input
|
|
78
|
+
|
|
79
|
+
Notes
|
|
80
|
+
-----
|
|
81
|
+
For large multi-dimensional datasets (TZYX), processing is parallelized across
|
|
82
|
+
the first dimension to utilize multiple CPU cores effectively.
|
|
75
83
|
"""
|
|
76
84
|
# Store original dtype to convert back later
|
|
77
85
|
original_dtype = image.dtype
|
|
86
|
+
|
|
87
|
+
# Print diagnostic info for multi-dimensional data
|
|
88
|
+
if image.ndim > 2:
|
|
89
|
+
print(f"Applying CLAHE to {image.ndim}D image with shape {image.shape}")
|
|
78
90
|
|
|
79
91
|
# Auto-calculate kernel size if not specified
|
|
80
92
|
if kernel_size <= 0:
|
|
@@ -87,13 +99,64 @@ if SKIMAGE_AVAILABLE:
|
|
|
87
99
|
# Ensure kernel_size is odd
|
|
88
100
|
if kernel_size % 2 == 0:
|
|
89
101
|
kernel_size += 1
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
|
|
103
|
+
if image.ndim > 2:
|
|
104
|
+
print(f"Using kernel_size={kernel_size}, clip_limit={clip_limit}")
|
|
105
|
+
|
|
106
|
+
# For 4D data (TZYX), parallelize across first dimension for better performance
|
|
107
|
+
if image.ndim == 4 and image.shape[0] > 1:
|
|
108
|
+
print(f"Parallelizing CLAHE across {image.shape[0]} timepoints/slices using {max(1, os.cpu_count() - 1)} workers...")
|
|
109
|
+
|
|
110
|
+
def process_slice(idx):
|
|
111
|
+
"""Process a single slice along first dimension"""
|
|
112
|
+
result_slice = skimage.exposure.equalize_adapthist(
|
|
113
|
+
image[idx], kernel_size=kernel_size, clip_limit=clip_limit
|
|
114
|
+
)
|
|
115
|
+
if (idx + 1) % max(1, image.shape[0] // 10) == 0:
|
|
116
|
+
print(f" Processed {idx + 1}/{image.shape[0]} slices")
|
|
117
|
+
return result_slice
|
|
118
|
+
|
|
119
|
+
# Process in parallel
|
|
120
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
121
|
+
max_workers=max(1, os.cpu_count() - 1)
|
|
122
|
+
) as executor:
|
|
123
|
+
futures = [executor.submit(process_slice, i) for i in range(image.shape[0])]
|
|
124
|
+
results = [future.result() for future in futures]
|
|
125
|
+
|
|
126
|
+
result = np.stack(results, axis=0)
|
|
127
|
+
print("CLAHE processing complete!")
|
|
128
|
+
|
|
129
|
+
elif image.ndim == 3 and image.shape[0] > 5:
|
|
130
|
+
# For 3D data with many slices, also parallelize
|
|
131
|
+
print(f"Parallelizing CLAHE across {image.shape[0]} slices using {max(1, os.cpu_count() - 1)} workers...")
|
|
132
|
+
|
|
133
|
+
def process_slice(idx):
|
|
134
|
+
"""Process a single 2D slice"""
|
|
135
|
+
result_slice = skimage.exposure.equalize_adapthist(
|
|
136
|
+
image[idx], kernel_size=kernel_size, clip_limit=clip_limit
|
|
137
|
+
)
|
|
138
|
+
if (idx + 1) % max(1, image.shape[0] // 10) == 0:
|
|
139
|
+
print(f" Processed {idx + 1}/{image.shape[0]} slices")
|
|
140
|
+
return result_slice
|
|
141
|
+
|
|
142
|
+
# Process in parallel
|
|
143
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
144
|
+
max_workers=max(1, os.cpu_count() - 1)
|
|
145
|
+
) as executor:
|
|
146
|
+
futures = [executor.submit(process_slice, i) for i in range(image.shape[0])]
|
|
147
|
+
results = [future.result() for future in futures]
|
|
148
|
+
|
|
149
|
+
result = np.stack(results, axis=0)
|
|
150
|
+
print("CLAHE processing complete!")
|
|
151
|
+
else:
|
|
152
|
+
# For 2D or small 3D data, use native implementation
|
|
153
|
+
if image.ndim > 2:
|
|
154
|
+
print("Processing...")
|
|
155
|
+
result = skimage.exposure.equalize_adapthist(
|
|
156
|
+
image, kernel_size=kernel_size, clip_limit=clip_limit
|
|
157
|
+
)
|
|
158
|
+
if image.ndim > 2:
|
|
159
|
+
print("CLAHE processing complete!")
|
|
97
160
|
|
|
98
161
|
# Convert back to original dtype to preserve compatibility
|
|
99
162
|
if np.issubdtype(original_dtype, np.integer):
|