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.
- 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.5.dist-info}/METADATA +71 -30
- napari_tmidas-0.2.5.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.5.dist-info}/WHEEL +0 -0
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.5.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {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
|