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.
- napari_tmidas/__init__.py +35 -5
- napari_tmidas/_crop_anything.py +1520 -609
- napari_tmidas/_env_manager.py +76 -0
- napari_tmidas/_file_conversion.py +1646 -1131
- napari_tmidas/_file_selector.py +1455 -216
- napari_tmidas/_label_inspection.py +83 -8
- napari_tmidas/_processing_worker.py +309 -0
- napari_tmidas/_reader.py +6 -10
- napari_tmidas/_registry.py +2 -2
- napari_tmidas/_roi_colocalization.py +1221 -84
- napari_tmidas/_tests/test_crop_anything.py +123 -0
- napari_tmidas/_tests/test_env_manager.py +89 -0
- napari_tmidas/_tests/test_grid_view_overlay.py +193 -0
- napari_tmidas/_tests/test_init.py +98 -0
- napari_tmidas/_tests/test_intensity_label_filter.py +222 -0
- napari_tmidas/_tests/test_label_inspection.py +86 -0
- napari_tmidas/_tests/test_processing_basic.py +500 -0
- napari_tmidas/_tests/test_processing_worker.py +142 -0
- napari_tmidas/_tests/test_regionprops_analysis.py +547 -0
- napari_tmidas/_tests/test_registry.py +70 -2
- napari_tmidas/_tests/test_scipy_filters.py +168 -0
- napari_tmidas/_tests/test_skimage_filters.py +259 -0
- napari_tmidas/_tests/test_split_channels.py +217 -0
- napari_tmidas/_tests/test_spotiflow.py +87 -0
- napari_tmidas/_tests/test_tyx_display_fix.py +142 -0
- napari_tmidas/_tests/test_ui_utils.py +68 -0
- napari_tmidas/_tests/test_widget.py +30 -0
- napari_tmidas/_tests/test_windows_basic.py +66 -0
- napari_tmidas/_ui_utils.py +57 -0
- napari_tmidas/_version.py +16 -3
- napari_tmidas/_widget.py +41 -4
- napari_tmidas/processing_functions/basic.py +557 -20
- napari_tmidas/processing_functions/careamics_env_manager.py +72 -99
- napari_tmidas/processing_functions/cellpose_env_manager.py +415 -112
- napari_tmidas/processing_functions/cellpose_segmentation.py +132 -191
- napari_tmidas/processing_functions/colocalization.py +513 -56
- napari_tmidas/processing_functions/grid_view_overlay.py +703 -0
- napari_tmidas/processing_functions/intensity_label_filter.py +422 -0
- napari_tmidas/processing_functions/regionprops_analysis.py +1280 -0
- napari_tmidas/processing_functions/sam2_env_manager.py +53 -69
- napari_tmidas/processing_functions/sam2_mp4.py +274 -195
- napari_tmidas/processing_functions/scipy_filters.py +403 -8
- napari_tmidas/processing_functions/skimage_filters.py +424 -212
- napari_tmidas/processing_functions/spotiflow_detection.py +949 -0
- napari_tmidas/processing_functions/spotiflow_env_manager.py +591 -0
- napari_tmidas/processing_functions/timepoint_merger.py +334 -86
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/METADATA +70 -30
- napari_tmidas-0.2.4.dist-info/RECORD +63 -0
- napari_tmidas/_tests/__init__.py +0 -0
- napari_tmidas-0.2.2.dist-info/RECORD +0 -40
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/WHEEL +0 -0
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# src/napari_tmidas/_tests/test_intensity_label_filter.py
|
|
2
|
+
"""Tests for intensity-based label filtering functions."""
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
# Try importing the functions - they may not be available if sklearn-extra is not installed
|
|
8
|
+
try:
|
|
9
|
+
from napari_tmidas.processing_functions.intensity_label_filter import (
|
|
10
|
+
_calculate_label_mean_intensities,
|
|
11
|
+
_cluster_intensities,
|
|
12
|
+
_filter_labels_by_threshold,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
HAS_KMEDOIDS = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
HAS_KMEDOIDS = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.skipif(
|
|
21
|
+
not HAS_KMEDOIDS, reason="scikit-learn-extra not installed"
|
|
22
|
+
)
|
|
23
|
+
class TestIntensityLabelFilter:
|
|
24
|
+
"""Test suite for intensity-based label filtering."""
|
|
25
|
+
|
|
26
|
+
def test_calculate_label_mean_intensities(self):
|
|
27
|
+
"""Test mean intensity calculation for labels."""
|
|
28
|
+
# Create simple label image with 3 labels
|
|
29
|
+
label_image = np.array(
|
|
30
|
+
[
|
|
31
|
+
[1, 1, 2, 2],
|
|
32
|
+
[1, 1, 2, 2],
|
|
33
|
+
[3, 3, 0, 0],
|
|
34
|
+
[3, 3, 0, 0],
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Create intensity image with different values for each label
|
|
39
|
+
intensity_image = np.array(
|
|
40
|
+
[
|
|
41
|
+
[10, 10, 50, 50],
|
|
42
|
+
[10, 10, 50, 50],
|
|
43
|
+
[100, 100, 0, 0],
|
|
44
|
+
[100, 100, 0, 0],
|
|
45
|
+
],
|
|
46
|
+
dtype=np.float32,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
result = _calculate_label_mean_intensities(
|
|
50
|
+
label_image, intensity_image
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert len(result) == 3
|
|
54
|
+
assert result[1] == pytest.approx(10.0)
|
|
55
|
+
assert result[2] == pytest.approx(50.0)
|
|
56
|
+
assert result[3] == pytest.approx(100.0)
|
|
57
|
+
|
|
58
|
+
def test_cluster_intensities_2medoids(self):
|
|
59
|
+
"""Test 2-medoids clustering."""
|
|
60
|
+
# Create clear separation: low (10, 15, 20) and high (80, 85, 90)
|
|
61
|
+
intensities = np.array([10, 15, 20, 80, 85, 90])
|
|
62
|
+
|
|
63
|
+
labels, medoids, threshold = _cluster_intensities(
|
|
64
|
+
intensities, n_clusters=2
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert len(labels) == 6
|
|
68
|
+
assert len(medoids) == 2
|
|
69
|
+
assert medoids[0] < medoids[1] # Sorted low to high
|
|
70
|
+
assert threshold > medoids[0]
|
|
71
|
+
assert threshold < medoids[1]
|
|
72
|
+
# Check threshold is between the two groups
|
|
73
|
+
assert threshold > 20
|
|
74
|
+
assert threshold < 80
|
|
75
|
+
|
|
76
|
+
def test_cluster_intensities_3medoids(self):
|
|
77
|
+
"""Test 3-medoids clustering."""
|
|
78
|
+
# Create clear separation: low (10, 15), medium (50, 55), high (90, 95)
|
|
79
|
+
intensities = np.array([10, 15, 50, 55, 90, 95])
|
|
80
|
+
|
|
81
|
+
labels, medoids, threshold = _cluster_intensities(
|
|
82
|
+
intensities, n_clusters=3
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
assert len(labels) == 6
|
|
86
|
+
assert len(medoids) == 3
|
|
87
|
+
assert medoids[0] < medoids[1] < medoids[2] # Sorted low to high
|
|
88
|
+
# Threshold should be between lowest and second-lowest
|
|
89
|
+
assert threshold > medoids[0]
|
|
90
|
+
assert threshold < medoids[1]
|
|
91
|
+
|
|
92
|
+
def test_filter_labels_by_threshold(self):
|
|
93
|
+
"""Test label filtering based on threshold."""
|
|
94
|
+
label_image = np.array(
|
|
95
|
+
[
|
|
96
|
+
[1, 1, 2, 2],
|
|
97
|
+
[1, 1, 2, 2],
|
|
98
|
+
[3, 3, 0, 0],
|
|
99
|
+
[3, 3, 0, 0],
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
label_intensities = {1: 10.0, 2: 50.0, 3: 100.0}
|
|
104
|
+
threshold = 40.0 # Should keep labels 2 and 3, remove label 1
|
|
105
|
+
|
|
106
|
+
result = _filter_labels_by_threshold(
|
|
107
|
+
label_image, label_intensities, threshold
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Label 1 should be removed (set to 0)
|
|
111
|
+
assert np.all(result[0:2, 0:2] == 0)
|
|
112
|
+
# Labels 2 and 3 should remain
|
|
113
|
+
assert np.all(result[0:2, 2:4] == 2)
|
|
114
|
+
assert np.all(result[2:4, 0:2] == 3)
|
|
115
|
+
# Background should remain
|
|
116
|
+
assert np.all(result[2:4, 2:4] == 0)
|
|
117
|
+
|
|
118
|
+
def test_filter_labels_2medoids_integration(self, tmp_path):
|
|
119
|
+
"""Integration test for 2-medoids filtering."""
|
|
120
|
+
# Create test label image with 3 labels
|
|
121
|
+
label_image = np.zeros((100, 100), dtype=np.uint16)
|
|
122
|
+
label_image[10:40, 10:40] = 1 # Low intensity
|
|
123
|
+
label_image[50:80, 10:40] = 2 # High intensity
|
|
124
|
+
label_image[10:40, 50:80] = 3 # High intensity
|
|
125
|
+
|
|
126
|
+
# Create intensity image where label 1 has low intensity, 2 and 3 high
|
|
127
|
+
intensity_image = np.zeros((100, 100), dtype=np.float32)
|
|
128
|
+
intensity_image[10:40, 10:40] = 20 # Low
|
|
129
|
+
intensity_image[50:80, 10:40] = 100 # High
|
|
130
|
+
intensity_image[10:40, 50:80] = 110 # High
|
|
131
|
+
|
|
132
|
+
# Save intensity image to temporary file
|
|
133
|
+
intensity_folder = tmp_path / "intensity"
|
|
134
|
+
intensity_folder.mkdir()
|
|
135
|
+
intensity_file = intensity_folder / "test_image.tif"
|
|
136
|
+
|
|
137
|
+
# Use tifffile if available, otherwise numpy
|
|
138
|
+
try:
|
|
139
|
+
import tifffile
|
|
140
|
+
|
|
141
|
+
tifffile.imwrite(intensity_file, intensity_image)
|
|
142
|
+
except ImportError:
|
|
143
|
+
np.save(intensity_file.with_suffix(".npy"), intensity_image)
|
|
144
|
+
intensity_file = intensity_file.with_suffix(".npy")
|
|
145
|
+
|
|
146
|
+
# Create fake label file path
|
|
147
|
+
label_file = tmp_path / "labels" / intensity_file.name
|
|
148
|
+
label_file.parent.mkdir()
|
|
149
|
+
|
|
150
|
+
# Run filter (without actual file, just testing logic)
|
|
151
|
+
# Note: This would require mocking the file reader in a real test
|
|
152
|
+
# For now, we'll test the components separately
|
|
153
|
+
|
|
154
|
+
def test_empty_label_image(self):
|
|
155
|
+
"""Test handling of empty label image."""
|
|
156
|
+
label_image = np.zeros((50, 50), dtype=np.uint16)
|
|
157
|
+
intensity_image = np.random.rand(50, 50).astype(np.float32)
|
|
158
|
+
|
|
159
|
+
result = _calculate_label_mean_intensities(
|
|
160
|
+
label_image, intensity_image
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert len(result) == 0
|
|
164
|
+
|
|
165
|
+
def test_single_label(self):
|
|
166
|
+
"""Test handling of single label."""
|
|
167
|
+
label_image = np.ones((50, 50), dtype=np.uint16)
|
|
168
|
+
intensity_image = np.full((50, 50), 42.0, dtype=np.float32)
|
|
169
|
+
|
|
170
|
+
result = _calculate_label_mean_intensities(
|
|
171
|
+
label_image, intensity_image
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert len(result) == 1
|
|
175
|
+
assert result[1] == pytest.approx(42.0)
|
|
176
|
+
|
|
177
|
+
def test_filter_preserves_dtype(self):
|
|
178
|
+
"""Test that filtering preserves label image dtype."""
|
|
179
|
+
for dtype in [np.uint8, np.uint16, np.uint32, np.int32]:
|
|
180
|
+
label_image = np.array(
|
|
181
|
+
[
|
|
182
|
+
[1, 1, 2, 2],
|
|
183
|
+
[1, 1, 2, 2],
|
|
184
|
+
],
|
|
185
|
+
dtype=dtype,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
label_intensities = {1: 10.0, 2: 50.0}
|
|
189
|
+
threshold = 40.0
|
|
190
|
+
|
|
191
|
+
result = _filter_labels_by_threshold(
|
|
192
|
+
label_image, label_intensities, threshold
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
assert result.dtype == dtype
|
|
196
|
+
|
|
197
|
+
def test_clustering_reproducibility(self):
|
|
198
|
+
"""Test that clustering is reproducible due to random_state."""
|
|
199
|
+
intensities = np.array([10, 15, 20, 25, 80, 85, 90, 95])
|
|
200
|
+
|
|
201
|
+
labels1, medoids1, threshold1 = _cluster_intensities(
|
|
202
|
+
intensities, n_clusters=2
|
|
203
|
+
)
|
|
204
|
+
labels2, medoids2, threshold2 = _cluster_intensities(
|
|
205
|
+
intensities, n_clusters=2
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
np.testing.assert_array_equal(labels1, labels2)
|
|
209
|
+
np.testing.assert_array_almost_equal(medoids1, medoids2)
|
|
210
|
+
assert threshold1 == pytest.approx(threshold2)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@pytest.mark.skipif(HAS_KMEDOIDS, reason="Test for missing dependency")
|
|
214
|
+
def test_import_error_without_sklearn_extra():
|
|
215
|
+
"""Test that appropriate error is raised when sklearn-extra is not installed."""
|
|
216
|
+
# This test only runs when sklearn-extra is NOT installed
|
|
217
|
+
with pytest.raises(ImportError):
|
|
218
|
+
from napari_tmidas.processing_functions.intensity_label_filter import (
|
|
219
|
+
_cluster_intensities,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
_cluster_intensities(np.array([1, 2, 3]), n_clusters=2)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# src/napari_tmidas/_tests/test_label_inspection.py
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from napari_tmidas._label_inspection import (
|
|
9
|
+
LabelInspector,
|
|
10
|
+
label_inspector_widget,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestLabelInspector:
|
|
15
|
+
def setup_method(self):
|
|
16
|
+
"""Setup test environment"""
|
|
17
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
18
|
+
self.viewer = Mock()
|
|
19
|
+
|
|
20
|
+
def teardown_method(self):
|
|
21
|
+
"""Cleanup"""
|
|
22
|
+
import shutil
|
|
23
|
+
|
|
24
|
+
shutil.rmtree(self.temp_dir)
|
|
25
|
+
|
|
26
|
+
def test_label_inspector_initialization(self):
|
|
27
|
+
"""Test LabelInspector initialization"""
|
|
28
|
+
inspector = LabelInspector(self.viewer)
|
|
29
|
+
assert inspector.viewer == self.viewer
|
|
30
|
+
assert inspector.image_label_pairs == []
|
|
31
|
+
assert inspector.current_index == 0
|
|
32
|
+
|
|
33
|
+
def test_load_image_label_pairs_no_folder(self):
|
|
34
|
+
"""Test loading pairs with non-existent folder"""
|
|
35
|
+
inspector = LabelInspector(self.viewer)
|
|
36
|
+
inspector.load_image_label_pairs("/nonexistent/folder", "_labels")
|
|
37
|
+
assert (
|
|
38
|
+
self.viewer.status
|
|
39
|
+
== "Folder path does not exist: /nonexistent/folder"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def test_load_image_label_pairs_no_labels(self):
|
|
43
|
+
"""Test loading pairs with no label files"""
|
|
44
|
+
inspector = LabelInspector(self.viewer)
|
|
45
|
+
|
|
46
|
+
# Create empty folder
|
|
47
|
+
empty_dir = os.path.join(self.temp_dir, "empty")
|
|
48
|
+
os.makedirs(empty_dir)
|
|
49
|
+
|
|
50
|
+
inspector.load_image_label_pairs(empty_dir, "_labels")
|
|
51
|
+
assert self.viewer.status == "No files found with suffix '_labels'"
|
|
52
|
+
|
|
53
|
+
@patch("napari_tmidas._label_inspection.imread")
|
|
54
|
+
def test_load_image_label_pairs_valid(self, mock_imread):
|
|
55
|
+
"""Test loading valid image-label pairs"""
|
|
56
|
+
inspector = LabelInspector(self.viewer)
|
|
57
|
+
|
|
58
|
+
# Create test files
|
|
59
|
+
test_dir = os.path.join(self.temp_dir, "test")
|
|
60
|
+
os.makedirs(test_dir)
|
|
61
|
+
|
|
62
|
+
# Create image and label files
|
|
63
|
+
image_path = os.path.join(test_dir, "test_image.tif")
|
|
64
|
+
label_path = os.path.join(test_dir, "test_image_labels.tif")
|
|
65
|
+
|
|
66
|
+
with open(image_path, "w") as f:
|
|
67
|
+
f.write("dummy")
|
|
68
|
+
with open(label_path, "w") as f:
|
|
69
|
+
f.write("dummy")
|
|
70
|
+
|
|
71
|
+
# Mock imread to return valid label data
|
|
72
|
+
mock_imread.return_value = np.ones((10, 10), dtype=np.uint32)
|
|
73
|
+
|
|
74
|
+
inspector.load_image_label_pairs(test_dir, "_labels")
|
|
75
|
+
|
|
76
|
+
# Check that pairs were loaded
|
|
77
|
+
assert len(inspector.image_label_pairs) == 1
|
|
78
|
+
assert inspector.image_label_pairs[0] == (image_path, label_path)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestLabelInspectorWidget:
|
|
82
|
+
def test_widget_creation(self):
|
|
83
|
+
"""Test that the label inspector widget can be imported and called"""
|
|
84
|
+
# Just test that the function exists and can be called
|
|
85
|
+
# (without actually creating the widget to avoid Qt issues)
|
|
86
|
+
assert callable(label_inspector_widget)
|