napari-tmidas 0.2.1__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 (56) hide show
  1. napari_tmidas/__init__.py +35 -5
  2. napari_tmidas/_crop_anything.py +1458 -499
  3. napari_tmidas/_env_manager.py +76 -0
  4. napari_tmidas/_file_conversion.py +1646 -1131
  5. napari_tmidas/_file_selector.py +1464 -223
  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 +15 -14
  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_file_selector.py +90 -0
  14. napari_tmidas/_tests/test_grid_view_overlay.py +193 -0
  15. napari_tmidas/_tests/test_init.py +98 -0
  16. napari_tmidas/_tests/test_intensity_label_filter.py +222 -0
  17. napari_tmidas/_tests/test_label_inspection.py +86 -0
  18. napari_tmidas/_tests/test_processing_basic.py +500 -0
  19. napari_tmidas/_tests/test_processing_worker.py +142 -0
  20. napari_tmidas/_tests/test_regionprops_analysis.py +547 -0
  21. napari_tmidas/_tests/test_registry.py +135 -0
  22. napari_tmidas/_tests/test_scipy_filters.py +168 -0
  23. napari_tmidas/_tests/test_skimage_filters.py +259 -0
  24. napari_tmidas/_tests/test_split_channels.py +217 -0
  25. napari_tmidas/_tests/test_spotiflow.py +87 -0
  26. napari_tmidas/_tests/test_tyx_display_fix.py +142 -0
  27. napari_tmidas/_tests/test_ui_utils.py +68 -0
  28. napari_tmidas/_tests/test_widget.py +30 -0
  29. napari_tmidas/_tests/test_windows_basic.py +66 -0
  30. napari_tmidas/_ui_utils.py +57 -0
  31. napari_tmidas/_version.py +16 -3
  32. napari_tmidas/_widget.py +41 -4
  33. napari_tmidas/processing_functions/basic.py +557 -20
  34. napari_tmidas/processing_functions/careamics_env_manager.py +72 -99
  35. napari_tmidas/processing_functions/cellpose_env_manager.py +415 -112
  36. napari_tmidas/processing_functions/cellpose_segmentation.py +132 -191
  37. napari_tmidas/processing_functions/colocalization.py +513 -56
  38. napari_tmidas/processing_functions/grid_view_overlay.py +703 -0
  39. napari_tmidas/processing_functions/intensity_label_filter.py +422 -0
  40. napari_tmidas/processing_functions/regionprops_analysis.py +1280 -0
  41. napari_tmidas/processing_functions/sam2_env_manager.py +53 -69
  42. napari_tmidas/processing_functions/sam2_mp4.py +274 -195
  43. napari_tmidas/processing_functions/scipy_filters.py +403 -8
  44. napari_tmidas/processing_functions/skimage_filters.py +424 -212
  45. napari_tmidas/processing_functions/spotiflow_detection.py +949 -0
  46. napari_tmidas/processing_functions/spotiflow_env_manager.py +591 -0
  47. napari_tmidas/processing_functions/timepoint_merger.py +334 -86
  48. napari_tmidas/processing_functions/trackastra_tracking.py +24 -5
  49. {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/METADATA +92 -39
  50. napari_tmidas-0.2.4.dist-info/RECORD +63 -0
  51. napari_tmidas/_tests/__init__.py +0 -0
  52. napari_tmidas-0.2.1.dist-info/RECORD +0 -38
  53. {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/WHEEL +0 -0
  54. {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
  55. {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
  56. {napari_tmidas-0.2.1.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)