celldetective 1.5.0b9__py3-none-any.whl → 1.5.0b11__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.
@@ -0,0 +1,299 @@
1
+ """
2
+ Unit tests for contour measurement format parsing and interpretation.
3
+
4
+ Tests the complete pipeline:
5
+ 1. CellEdgeVisualizer output format: "(min,max)"
6
+ 2. ListWidget.getItems() parsing
7
+ 3. contour_of_instance_segmentation() interpretation
8
+ 4. measure_features() with border_distances
9
+ """
10
+
11
+ import unittest
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+
16
+ class TestContourFormatParsing(unittest.TestCase):
17
+ """
18
+ Test that the contour format "(min,max)" is correctly parsed by list_widget.getItems()
19
+ and contour_of_instance_segmentation().
20
+ """
21
+
22
+ def test_tuple_format_parsing_positive(self):
23
+ """Test parsing of positive range like (0,5)"""
24
+ from celldetective.utils.masks import contour_of_instance_segmentation
25
+
26
+ # Create a simple label with a square object
27
+ label = np.zeros((50, 50), dtype=int)
28
+ label[15:35, 15:35] = 1 # 20x20 square
29
+
30
+ # Test with tuple format string "(0,5)" - inner contour 0-5px
31
+ result = contour_of_instance_segmentation(label, "(0,5)")
32
+
33
+ # Should have non-zero pixels (edge region exists)
34
+ self.assertGreater(np.sum(result > 0), 0, "Contour should have pixels")
35
+
36
+ # The result should be smaller than the original object
37
+ self.assertLess(
38
+ np.sum(result > 0), np.sum(label > 0), "Edge should be smaller than object"
39
+ )
40
+
41
+ def test_tuple_format_parsing_negative(self):
42
+ """Test parsing of negative range like (-5,0) - outer contour"""
43
+ from celldetective.utils.masks import contour_of_instance_segmentation
44
+
45
+ label = np.zeros((50, 50), dtype=int)
46
+ label[15:35, 15:35] = 1
47
+
48
+ # Test with tuple format string "(-5,0)" - outer contour
49
+ result = contour_of_instance_segmentation(label, "(-5,0)")
50
+
51
+ # Should have non-zero pixels in the region outside the original object
52
+ self.assertGreater(np.sum(result > 0), 0, "Outer contour should have pixels")
53
+
54
+ def test_tuple_format_parsing_mixed(self):
55
+ """Test parsing of mixed range like (-3,3) - crossing boundary"""
56
+ from celldetective.utils.masks import contour_of_instance_segmentation
57
+
58
+ label = np.zeros((50, 50), dtype=int)
59
+ label[15:35, 15:35] = 1
60
+
61
+ # Test with tuple format string "(-3,3)" - straddles boundary
62
+ result = contour_of_instance_segmentation(label, "(-3,3)")
63
+
64
+ # Should have non-zero pixels
65
+ self.assertGreater(np.sum(result > 0), 0, "Mixed contour should have pixels")
66
+
67
+ def test_list_format_direct(self):
68
+ """Test that list format [min, max] works correctly"""
69
+ from celldetective.utils.masks import contour_of_instance_segmentation
70
+
71
+ label = np.zeros((50, 50), dtype=int)
72
+ label[15:35, 15:35] = 1
73
+
74
+ # Test with list format [-5, 5]
75
+ result = contour_of_instance_segmentation(label, [-5, 5])
76
+
77
+ self.assertGreater(np.sum(result > 0), 0, "List format should work")
78
+
79
+ def test_tuple_format_direct(self):
80
+ """Test that tuple format (min, max) works correctly"""
81
+ from celldetective.utils.masks import contour_of_instance_segmentation
82
+
83
+ label = np.zeros((50, 50), dtype=int)
84
+ label[15:35, 15:35] = 1
85
+
86
+ # Test with tuple format (-5, 5)
87
+ result = contour_of_instance_segmentation(label, (-5, 5))
88
+
89
+ self.assertGreater(np.sum(result > 0), 0, "Tuple format should work")
90
+
91
+
92
+ class TestListWidgetParsing(unittest.TestCase):
93
+ """
94
+ Test that ListWidget.getItems() correctly parses the "(min,max)" format.
95
+ """
96
+
97
+ def setUp(self):
98
+ """Set up a mock list widget for testing."""
99
+ from unittest.mock import MagicMock
100
+
101
+ # Create a mock item that returns text
102
+ self.mock_item = MagicMock()
103
+ self.mock_list_widget = MagicMock()
104
+
105
+ def test_parse_tuple_format(self):
106
+ """Test that getItems parses (min,max) format correctly."""
107
+ import re
108
+
109
+ # Simulate the parsing logic from getItems
110
+ tuple_pattern = re.compile(r"^\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)$")
111
+
112
+ test_cases = [
113
+ ("(0,5)", [0, 5]),
114
+ ("(-5,0)", [-5, 0]),
115
+ ("(-10,10)", [-10, 10]),
116
+ ("(-3,3)", [-3, 3]),
117
+ ("(0, 10)", [0, 10]), # with space
118
+ ]
119
+
120
+ for text, expected in test_cases:
121
+ match = tuple_pattern.match(text.strip())
122
+ self.assertIsNotNone(match, f"Should match pattern: {text}")
123
+ minn = int(float(match.group(1)))
124
+ maxx = int(float(match.group(2)))
125
+ self.assertEqual([minn, maxx], expected, f"Failed for: {text}")
126
+
127
+ def test_parse_single_value(self):
128
+ """Test that single values are parsed correctly."""
129
+ import re
130
+
131
+ tuple_pattern = re.compile(r"^\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)$")
132
+
133
+ test_cases = ["5", "10", "-5", "0"]
134
+
135
+ for text in test_cases:
136
+ match = tuple_pattern.match(text.strip())
137
+ self.assertIsNone(match, f"Should NOT match tuple pattern: {text}")
138
+ # Should be parseable as int
139
+ val = int(text)
140
+ self.assertIsInstance(val, int)
141
+
142
+
143
+ class TestContourMeasurementIntegration(unittest.TestCase):
144
+ """
145
+ Integration test for the complete contour measurement pipeline.
146
+ Tests that measure_features correctly uses border_distances with the new format.
147
+ """
148
+
149
+ @classmethod
150
+ def setUpClass(cls):
151
+ """Create test data for all tests."""
152
+ # Create a larger image with clear intensity gradient
153
+ cls.frame = np.ones((100, 100, 1), dtype=float)
154
+ # Create a gradient - center is brighter
155
+ for i in range(100):
156
+ for j in range(100):
157
+ dist_from_center = np.sqrt((i - 50) ** 2 + (j - 50) ** 2)
158
+ cls.frame[i, j, 0] = max(0, 1 - dist_from_center / 50)
159
+
160
+ # Create a single centered object
161
+ cls.labels = np.zeros((100, 100), dtype=int)
162
+ cls.labels[35:65, 35:65] = 1 # 30x30 square centered at (50, 50)
163
+
164
+ def test_measure_with_list_border_distance(self):
165
+ """Test that measure_features works with list [min, max] border distance."""
166
+ from celldetective.measure import measure_features
167
+
168
+ result = measure_features(
169
+ self.frame,
170
+ self.labels,
171
+ features=["intensity_mean"],
172
+ channels=["test"],
173
+ border_dist=[[-5, 5]], # Edge region from -5 to +5 around boundary
174
+ )
175
+
176
+ self.assertIsInstance(result, pd.DataFrame)
177
+ self.assertEqual(len(result), 1)
178
+ # Check that edge intensity column exists
179
+ edge_cols = [c for c in result.columns if "edge" in c or "slice" in c]
180
+ self.assertGreater(len(edge_cols), 0, "Should have edge measurement columns")
181
+
182
+ def test_measure_with_positive_only_border(self):
183
+ """Test inner contour measurement [0, 5]."""
184
+ from celldetective.measure import measure_features
185
+
186
+ result = measure_features(
187
+ self.frame,
188
+ self.labels,
189
+ features=["intensity_mean"],
190
+ channels=["test"],
191
+ border_dist=[[0, 5]], # Inner edge only
192
+ )
193
+
194
+ self.assertIsInstance(result, pd.DataFrame)
195
+ self.assertEqual(len(result), 1)
196
+
197
+ def test_measure_with_negative_only_border(self):
198
+ """Test outer contour measurement [-5, 0]."""
199
+ from celldetective.measure import measure_features
200
+
201
+ result = measure_features(
202
+ self.frame,
203
+ self.labels,
204
+ features=["intensity_mean"],
205
+ channels=["test"],
206
+ border_dist=[[-5, 0]], # Outer edge only
207
+ )
208
+
209
+ self.assertIsInstance(result, pd.DataFrame)
210
+ self.assertEqual(len(result), 1)
211
+
212
+ def test_measure_with_scalar_border_distance(self):
213
+ """Test that scalar border_dist still works (backwards compatibility)."""
214
+ from celldetective.measure import measure_features
215
+
216
+ result = measure_features(
217
+ self.frame,
218
+ self.labels,
219
+ features=["intensity_mean"],
220
+ channels=["test"],
221
+ border_dist=[5], # Scalar - should be interpreted as [0, 5]
222
+ )
223
+
224
+ self.assertIsInstance(result, pd.DataFrame)
225
+ self.assertEqual(len(result), 1)
226
+
227
+
228
+ class TestContourWithEdgeCases(unittest.TestCase):
229
+ """Test edge cases and potential issues with contour computation."""
230
+
231
+ def test_small_object_with_large_contour(self):
232
+ """Test that small objects with large contour distance don't crash."""
233
+ from celldetective.utils.masks import contour_of_instance_segmentation
234
+
235
+ # Very small object
236
+ label = np.zeros((50, 50), dtype=int)
237
+ label[24:26, 24:26] = 1 # 2x2 pixel object
238
+
239
+ # Large contour request - may result in empty contour
240
+ result = contour_of_instance_segmentation(label, [0, 10])
241
+
242
+ # Should not crash, may be empty or small
243
+ self.assertEqual(result.shape, label.shape)
244
+
245
+ def test_empty_label_returns_zeros(self):
246
+ """Test that empty labels return zero array."""
247
+ from celldetective.utils.masks import contour_of_instance_segmentation
248
+
249
+ label = np.zeros((50, 50), dtype=int)
250
+
251
+ result = contour_of_instance_segmentation(label, [0, 5])
252
+
253
+ self.assertTrue(np.all(result == 0), "Empty label should return zeros")
254
+
255
+ def test_multiple_objects(self):
256
+ """Test contour with multiple distinct objects."""
257
+ from celldetective.utils.masks import contour_of_instance_segmentation
258
+
259
+ label = np.zeros((100, 100), dtype=int)
260
+ label[10:30, 10:30] = 1
261
+ label[60:80, 60:80] = 2
262
+
263
+ result = contour_of_instance_segmentation(label, [0, 3])
264
+
265
+ # Should have both object IDs in result
266
+ unique_ids = np.unique(result[result > 0])
267
+ self.assertIn(1, unique_ids, "Object 1 should be in result")
268
+ self.assertIn(2, unique_ids, "Object 2 should be in result")
269
+
270
+
271
+ class TestSuffixFormatting(unittest.TestCase):
272
+ """Test that measurement column names are formatted correctly."""
273
+
274
+ def test_get_suffix_function(self):
275
+ """Test the get_suffix helper function logic."""
276
+
277
+ # Simulate the get_suffix function from measure.py
278
+ def get_suffix(d):
279
+ d_str = str(d)
280
+ d_clean = (
281
+ d_str.replace("(", "")
282
+ .replace(")", "")
283
+ .replace(", ", "_")
284
+ .replace(",", "_")
285
+ )
286
+ if "-" in d_str or "," in d_str:
287
+ return f"_slice_{d_clean.replace('-', 'm')}px"
288
+ else:
289
+ return f"_edge_{d_clean}px"
290
+
291
+ # Test cases
292
+ self.assertEqual(get_suffix(5), "_edge_5px")
293
+ self.assertEqual(get_suffix(-5), "_slice_m5px")
294
+ self.assertEqual(get_suffix([0, 5]), "_slice_[0_5]px")
295
+ self.assertEqual(get_suffix([-5, 5]), "_slice_[m5_5]px")
296
+
297
+
298
+ if __name__ == "__main__":
299
+ unittest.main()