napari-tmidas 0.2.2__py3-none-any.whl → 0.2.5__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.5.dist-info}/METADATA +71 -30
  48. napari_tmidas-0.2.5.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.5.dist-info}/WHEEL +0 -0
  52. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.5.dist-info}/entry_points.txt +0 -0
  53. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.5.dist-info}/licenses/LICENSE +0 -0
  54. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,123 @@
1
+ # src/napari_tmidas/_tests/test_crop_anything.py
2
+ from unittest.mock import Mock, patch
3
+
4
+ from napari_tmidas._crop_anything import batch_crop_anything_widget
5
+
6
+
7
+ class TestBatchCropAnythingWidget:
8
+ def test_widget_creation(self):
9
+ """Test that the batch crop anything widget is created properly"""
10
+ widget = batch_crop_anything_widget()
11
+ assert widget is not None
12
+ # Check that it has the expected attributes
13
+ assert hasattr(widget, "folder_path")
14
+ assert hasattr(widget, "data_dimensions")
15
+ # viewer is a parameter but may not be exposed as attribute
16
+ assert hasattr(widget, "call_button") # magicgui adds this
17
+
18
+ @patch("napari_tmidas._crop_anything.batch_crop_anything")
19
+ def test_widget_has_browse_button(self, mock_batch_crop):
20
+ """Test that the widget has a browse button added"""
21
+ mock_widget = Mock()
22
+ mock_widget.folder_path = Mock()
23
+ mock_widget.folder_path.native = Mock()
24
+ mock_widget.folder_path.native.parent.return_value.layout.return_value = (
25
+ Mock()
26
+ )
27
+ mock_widget.folder_path.value = "/test/path"
28
+
29
+ mock_batch_crop.return_value = mock_widget
30
+
31
+ batch_crop_anything_widget()
32
+
33
+ # The browse button should be added to the layout
34
+ # This is hard to test directly without mocking Qt, but we can check the function exists
35
+ assert callable(batch_crop_anything_widget)
36
+
37
+ @patch("napari_tmidas._crop_anything.BatchCropAnything")
38
+ @patch("napari_tmidas._crop_anything.magicgui")
39
+ def test_widget_creation_safe(self, mock_magicgui, mock_batch_crop):
40
+ """Test widget creation with BatchCropAnything mocked to avoid any SAM2 issues"""
41
+ # Mock the BatchCropAnything class to avoid any SAM2 initialization
42
+ mock_instance = Mock()
43
+ mock_batch_crop.return_value = mock_instance
44
+
45
+ # Mock magicgui to return a simple widget
46
+ mock_widget = Mock()
47
+ mock_magicgui.return_value = mock_widget
48
+
49
+ # This should be completely safe since everything is mocked
50
+ widget = batch_crop_anything_widget()
51
+ assert widget is not None
52
+
53
+ def test_next_image_at_last_image(self):
54
+ """Test that next_image returns False when already at the last image"""
55
+ from napari_tmidas._crop_anything import BatchCropAnything
56
+
57
+ # Create a mock viewer
58
+ mock_viewer = Mock()
59
+ mock_viewer.layers = Mock()
60
+ mock_viewer.layers.clear = Mock()
61
+
62
+ # Create processor with mocked predictor to avoid SAM2 initialization
63
+ with patch.object(BatchCropAnything, "_initialize_sam2"):
64
+ processor = BatchCropAnything(mock_viewer, use_3d=False)
65
+ processor.predictor = (
66
+ None # Ensure predictor is None to skip segmentation
67
+ )
68
+
69
+ # Set up test data with 3 images
70
+ processor.images = [
71
+ "/path/img1.tif",
72
+ "/path/img2.tif",
73
+ "/path/img3.tif",
74
+ ]
75
+ processor.current_index = 2 # At the last image (index 2 of 3 images)
76
+
77
+ # Try to move to next image when already at the last one
78
+ result = processor.next_image()
79
+
80
+ # Should return False
81
+ assert result is False
82
+
83
+ # Current index should not change
84
+ assert processor.current_index == 2
85
+
86
+ # Layers should not have been cleared (no call to _load_current_image)
87
+ mock_viewer.layers.clear.assert_not_called()
88
+
89
+ def test_prev_image_at_first_image(self):
90
+ """Test that previous_image returns False when already at the first image"""
91
+ from napari_tmidas._crop_anything import BatchCropAnything
92
+
93
+ # Create a mock viewer
94
+ mock_viewer = Mock()
95
+ mock_viewer.layers = Mock()
96
+ mock_viewer.layers.clear = Mock()
97
+
98
+ # Create processor with mocked predictor to avoid SAM2 initialization
99
+ with patch.object(BatchCropAnything, "_initialize_sam2"):
100
+ processor = BatchCropAnything(mock_viewer, use_3d=False)
101
+ processor.predictor = (
102
+ None # Ensure predictor is None to skip segmentation
103
+ )
104
+
105
+ # Set up test data with 3 images
106
+ processor.images = [
107
+ "/path/img1.tif",
108
+ "/path/img2.tif",
109
+ "/path/img3.tif",
110
+ ]
111
+ processor.current_index = 0 # At the first image (index 0)
112
+
113
+ # Try to move to previous image when already at the first one
114
+ result = processor.previous_image()
115
+
116
+ # Should return False
117
+ assert result is False
118
+
119
+ # Current index should not change
120
+ assert processor.current_index == 0
121
+
122
+ # Layers should not have been cleared (no call to _load_current_image)
123
+ mock_viewer.layers.clear.assert_not_called()
@@ -0,0 +1,89 @@
1
+ # src/napari_tmidas/_tests/test_env_manager.py
2
+ import tempfile
3
+ from unittest.mock import Mock, patch
4
+
5
+ from napari_tmidas._env_manager import BaseEnvironmentManager
6
+
7
+
8
+ class MockEnvironmentManager(BaseEnvironmentManager):
9
+ """Test implementation of BaseEnvironmentManager."""
10
+
11
+ def __init__(self):
12
+ super().__init__("test-env")
13
+
14
+ def _install_dependencies(self, env_python: str) -> None:
15
+ """Mock installation."""
16
+
17
+ def is_package_installed(self) -> bool:
18
+ """Mock package check."""
19
+ return True
20
+
21
+
22
+ class TestBaseEnvironmentManager:
23
+ def setup_method(self):
24
+ """Setup test environment"""
25
+ self.temp_dir = tempfile.mkdtemp()
26
+ self.manager = MockEnvironmentManager()
27
+
28
+ def teardown_method(self):
29
+ """Cleanup"""
30
+ import shutil
31
+
32
+ shutil.rmtree(self.temp_dir)
33
+
34
+ def test_initialization(self):
35
+ """Test manager initialization"""
36
+ assert self.manager.env_name == "test-env"
37
+ assert "test-env" in self.manager.env_dir
38
+
39
+ def test_is_env_created_false(self):
40
+ """Test is_env_created returns False when env doesn't exist"""
41
+ assert not self.manager.is_env_created()
42
+
43
+ @patch("napari_tmidas._env_manager.venv.create")
44
+ @patch("napari_tmidas._env_manager.subprocess.check_call")
45
+ def test_create_env(self, mock_subprocess, mock_venv):
46
+ """Test environment creation"""
47
+ env_python = self.manager.create_env()
48
+
49
+ # Check that venv.create was called
50
+ mock_venv.assert_called_once()
51
+
52
+ # Check that pip upgrade was called
53
+ mock_subprocess.assert_called()
54
+
55
+ # Check that the returned path is correct
56
+ assert env_python == self.manager.get_env_python_path()
57
+
58
+ def test_get_env_python_path_linux(self):
59
+ """Test getting Python path on Linux"""
60
+ with patch(
61
+ "napari_tmidas._env_manager.platform.system", return_value="Linux"
62
+ ):
63
+ path = self.manager.get_env_python_path()
64
+ import os
65
+
66
+ norm = os.path.normpath(path)
67
+ assert os.path.join("bin", "python") in norm
68
+
69
+ def test_get_env_python_path_windows(self):
70
+ """Test getting Python path on Windows"""
71
+ with patch(
72
+ "napari_tmidas._env_manager.platform.system",
73
+ return_value="Windows",
74
+ ):
75
+ path = self.manager.get_env_python_path()
76
+ assert "Scripts" in path and "python.exe" in path
77
+
78
+ def test_is_package_installed(self):
79
+ """Test package installation check"""
80
+ assert self.manager.is_package_installed()
81
+
82
+ @patch("napari_tmidas._env_manager.subprocess.run")
83
+ def test_run_in_env(self, mock_subprocess):
84
+ """Test running command in environment"""
85
+ mock_subprocess.return_value = Mock()
86
+ result = self.manager.run_in_env("print('test')")
87
+
88
+ mock_subprocess.assert_called_once()
89
+ assert result is not None
@@ -0,0 +1,193 @@
1
+ """Tests for grid view overlay processing function."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ try:
7
+ from napari_tmidas.processing_functions.grid_view_overlay import (
8
+ _create_grid,
9
+ _create_overlay,
10
+ _get_intensity_filename,
11
+ )
12
+
13
+ GRID_OVERLAY_AVAILABLE = True
14
+ except ImportError:
15
+ GRID_OVERLAY_AVAILABLE = False
16
+
17
+
18
+ @pytest.mark.skipif(
19
+ not GRID_OVERLAY_AVAILABLE, reason="Grid overlay function not available"
20
+ )
21
+ def test_get_intensity_filename():
22
+ """Test intensity filename extraction from label filenames."""
23
+ assert (
24
+ _get_intensity_filename("test_convpaint_labels_filtered.tif")
25
+ == "test.tif"
26
+ )
27
+ assert _get_intensity_filename("test_labels.tif") == "test.tif"
28
+ assert _get_intensity_filename("test_labels_filtered.tif") == "test.tif"
29
+ assert _get_intensity_filename("test_intensity_filtered.tif") == "test.tif"
30
+ assert _get_intensity_filename("unknown.tif") == "unknown.tif"
31
+
32
+
33
+ @pytest.mark.skipif(
34
+ not GRID_OVERLAY_AVAILABLE, reason="Grid overlay function not available"
35
+ )
36
+ def test_create_overlay():
37
+ """Test overlay creation with intensity and labels."""
38
+ # Create simple test images
39
+ intensity = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
40
+ labels = np.zeros((100, 100), dtype=np.uint16)
41
+ labels[20:40, 20:40] = 1
42
+ labels[60:80, 60:80] = 2
43
+
44
+ # Create overlay without downsampling (with overlay enabled)
45
+ overlay = _create_overlay(intensity, labels, show_overlay=True)
46
+
47
+ # Check output
48
+ assert overlay.shape == (100, 100, 3)
49
+ assert overlay.dtype == np.uint8
50
+ assert overlay.min() >= 0
51
+ assert overlay.max() <= 255
52
+
53
+
54
+ @pytest.mark.skipif(
55
+ not GRID_OVERLAY_AVAILABLE, reason="Grid overlay function not available"
56
+ )
57
+ def test_create_overlay_with_downsampling():
58
+ """Test overlay creation with downsampling."""
59
+ # Create larger test images
60
+ intensity = np.random.randint(0, 255, (1000, 1000), dtype=np.uint8)
61
+ labels = np.zeros((1000, 1000), dtype=np.uint16)
62
+ labels[200:400, 200:400] = 1
63
+ labels[600:800, 600:800] = 2
64
+
65
+ # Create overlay with downsampling to 300px
66
+ overlay = _create_overlay(
67
+ intensity, labels, target_size=300, show_overlay=True
68
+ )
69
+
70
+ # Check output is downsampled
71
+ assert overlay.shape[0] <= 300
72
+ assert overlay.shape[1] <= 300
73
+ assert overlay.shape[2] == 3
74
+ assert overlay.dtype == np.uint8
75
+ assert overlay.min() >= 0
76
+ assert overlay.max() <= 255
77
+
78
+
79
+ @pytest.mark.skipif(
80
+ not GRID_OVERLAY_AVAILABLE, reason="Grid overlay function not available"
81
+ )
82
+ def test_create_grid():
83
+ """Test grid creation from multiple images."""
84
+ # Create test images
85
+ images = [
86
+ np.random.randint(0, 255, (50, 50, 3), dtype=np.uint8)
87
+ for _ in range(6)
88
+ ]
89
+
90
+ # Create grid with 3 columns (should be 2 rows)
91
+ grid = _create_grid(images, grid_cols=3)
92
+
93
+ # Check output
94
+ assert grid.shape == (
95
+ 100,
96
+ 150,
97
+ 3,
98
+ ) # 2 rows * 50px, 3 cols * 50px, 3 channels
99
+ assert grid.dtype == np.uint8
100
+
101
+
102
+ @pytest.mark.skipif(
103
+ not GRID_OVERLAY_AVAILABLE, reason="Grid overlay function not available"
104
+ )
105
+ def test_create_grid_grayscale():
106
+ """Test grid creation with grayscale images."""
107
+ # Create grayscale test images
108
+ images = [
109
+ np.random.randint(0, 255, (50, 50), dtype=np.uint8) for _ in range(4)
110
+ ]
111
+
112
+ # Create grid with 2 columns
113
+ grid = _create_grid(images, grid_cols=2)
114
+
115
+ # Check output
116
+ assert grid.shape == (100, 100) # 2 rows * 50px, 2 cols * 50px
117
+ assert grid.dtype == np.uint8
118
+
119
+
120
+ @pytest.mark.skipif(
121
+ not GRID_OVERLAY_AVAILABLE, reason="Grid overlay function not available"
122
+ )
123
+ def test_create_grid_empty():
124
+ """Test grid creation with empty list."""
125
+ grid = _create_grid([], grid_cols=4)
126
+ assert grid is None
127
+
128
+
129
+ @pytest.mark.skipif(
130
+ not GRID_OVERLAY_AVAILABLE, reason="Grid overlay function not available"
131
+ )
132
+ def test_create_overlay_intensity_only():
133
+ """Test overlay creation with intensity only (no label overlay)."""
134
+ # Create simple test images
135
+ intensity = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
136
+ labels = np.zeros((100, 100), dtype=np.uint16)
137
+ labels[20:40, 20:40] = 1
138
+ labels[60:80, 60:80] = 2
139
+
140
+ # Create overlay with overlay disabled (intensity only)
141
+ overlay = _create_overlay(intensity, labels, show_overlay=False)
142
+
143
+ # Check output - should be grayscale RGB (all channels equal)
144
+ assert overlay.shape == (100, 100, 3)
145
+ assert overlay.dtype == np.uint8
146
+ assert overlay.min() >= 0
147
+ assert overlay.max() <= 255
148
+
149
+ # Check that all channels are equal (grayscale)
150
+ assert np.array_equal(overlay[:, :, 0], overlay[:, :, 1])
151
+ assert np.array_equal(overlay[:, :, 1], overlay[:, :, 2])
152
+
153
+
154
+ @pytest.mark.skipif(
155
+ not GRID_OVERLAY_AVAILABLE, reason="Grid overlay function not available"
156
+ )
157
+ def test_create_overlay_with_and_without_labels():
158
+ """Test that overlay mode creates different outputs with labels."""
159
+ # Create test images with labels
160
+ intensity = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
161
+ labels = np.zeros((100, 100), dtype=np.uint16)
162
+ labels[20:40, 20:40] = 1
163
+ labels[60:80, 60:80] = 2
164
+
165
+ # Create overlay with labels
166
+ overlay_with_labels = _create_overlay(intensity, labels, show_overlay=True)
167
+
168
+ # Create overlay without labels (intensity only)
169
+ overlay_intensity_only = _create_overlay(
170
+ intensity, labels, show_overlay=False
171
+ )
172
+
173
+ # Both should have same shape
174
+ assert overlay_with_labels.shape == overlay_intensity_only.shape
175
+
176
+ # But different content (overlay should have colored regions)
177
+ # In the intensity-only version, all channels should be equal
178
+ assert np.array_equal(
179
+ overlay_intensity_only[:, :, 0], overlay_intensity_only[:, :, 1]
180
+ )
181
+
182
+ # In the overlay version, channels should differ in labeled regions
183
+ # Check the labeled region [20:40, 20:40]
184
+ labeled_region_r = overlay_with_labels[20:40, 20:40, 0]
185
+ labeled_region_g = overlay_with_labels[20:40, 20:40, 1]
186
+ labeled_region_b = overlay_with_labels[20:40, 20:40, 2]
187
+
188
+ # At least one channel pair should differ in the labeled region
189
+ assert (
190
+ not np.array_equal(labeled_region_r, labeled_region_g)
191
+ or not np.array_equal(labeled_region_g, labeled_region_b)
192
+ or not np.array_equal(labeled_region_r, labeled_region_b)
193
+ )
@@ -0,0 +1,98 @@
1
+ # src/napari_tmidas/_tests/test_init.py
2
+ import os
3
+ import sys
4
+
5
+ import pytest
6
+
7
+ from napari_tmidas import (
8
+ __version__,
9
+ batch_crop_anything_widget,
10
+ file_selector,
11
+ label_inspector_widget,
12
+ make_sample_data,
13
+ napari_get_reader,
14
+ roi_colocalization_analyzer,
15
+ write_multiple,
16
+ write_single_image,
17
+ )
18
+
19
+
20
+ class TestInit:
21
+ @pytest.mark.skipif(
22
+ sys.platform == "win32" and os.environ.get("CI") == "true",
23
+ reason="Version is 'unknown' on Windows CI when skip_install=true",
24
+ )
25
+ def test_version_import(self):
26
+ """Test that version is imported correctly"""
27
+ # Version should be a string
28
+ assert isinstance(__version__, str)
29
+ # Should not be "unknown" in normal operation
30
+ assert __version__ != "unknown"
31
+
32
+ def test_core_exports_available(self):
33
+ """Test that core exports are always available"""
34
+ # These should always be importable
35
+ assert napari_get_reader is not None
36
+ assert write_single_image is not None
37
+ assert write_multiple is not None
38
+ assert make_sample_data is not None
39
+ assert file_selector is not None
40
+
41
+ def test_optional_exports(self):
42
+ """Test optional exports (may be None on some platforms)"""
43
+ # These might be None if dependencies fail to load on Windows
44
+ # but we should at least be able to import them
45
+ assert (
46
+ label_inspector_widget is not None
47
+ or label_inspector_widget is None
48
+ )
49
+ assert (
50
+ roi_colocalization_analyzer is not None
51
+ or roi_colocalization_analyzer is None
52
+ )
53
+ assert (
54
+ batch_crop_anything_widget is not None
55
+ or batch_crop_anything_widget is None
56
+ )
57
+
58
+ def test_imports_dont_crash(self):
59
+ """Test that imports don't cause crashes on any platform"""
60
+ # This test will pass as long as the imports above didn't crash
61
+ # which is the main issue we're trying to solve on Windows
62
+ assert True
63
+ assert make_sample_data is not None
64
+ assert file_selector is not None
65
+ assert label_inspector_widget is not None
66
+ assert batch_crop_anything_widget is not None
67
+ assert roi_colocalization_analyzer is not None
68
+
69
+ def test_functions_are_callable(self):
70
+ """Test that exported functions are callable"""
71
+ # These should be callable objects
72
+ assert callable(napari_get_reader)
73
+ assert callable(write_single_image)
74
+ assert callable(write_multiple)
75
+ assert callable(make_sample_data)
76
+ assert callable(file_selector)
77
+ assert callable(label_inspector_widget)
78
+ assert callable(batch_crop_anything_widget)
79
+
80
+ def test_version_fallback(self):
81
+ """Test version fallback when _version import fails"""
82
+ import sys
83
+ from unittest.mock import patch
84
+
85
+ # Mock import failure
86
+ with patch.dict("sys.modules", {"napari_tmidas._version": None}):
87
+ # Force reimport
88
+ if "napari_tmidas" in sys.modules:
89
+ del sys.modules["napari_tmidas"]
90
+
91
+ # This should trigger the fallback
92
+ try:
93
+ import napari_tmidas
94
+
95
+ # Version should be "unknown" when import fails
96
+ assert napari_tmidas.__version__ == "unknown"
97
+ except ImportError:
98
+ pass # Expected if other imports fail