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.
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.4.dist-info}/METADATA +70 -30
  48. napari_tmidas-0.2.4.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.4.dist-info}/WHEEL +0 -0
  52. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
  53. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
  54. {napari_tmidas-0.2.2.dist-info → napari_tmidas-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,500 @@
1
+ # src/napari_tmidas/_tests/test_processing_basic.py
2
+ import numpy as np
3
+ import pytest
4
+
5
+ from napari_tmidas.processing_functions.basic import (
6
+ filter_label_by_id,
7
+ intersect_label_images,
8
+ invert_binary_labels,
9
+ keep_slice_range_by_area,
10
+ labels_to_binary,
11
+ mirror_labels,
12
+ )
13
+
14
+
15
+ class TestBasicProcessing:
16
+ def test_labels_to_binary(self):
17
+ """Test converting labels to binary mask"""
18
+ # Create test label image
19
+ labels = np.array([[0, 1, 2], [1, 2, 0], [2, 0, 1]], dtype=np.uint32)
20
+
21
+ # Process
22
+ result = labels_to_binary(labels)
23
+
24
+ # Check result - now expects 255 instead of 1
25
+ expected = np.array(
26
+ [[0, 255, 255], [255, 255, 0], [255, 0, 255]], dtype=np.uint8
27
+ )
28
+ np.testing.assert_array_equal(result, expected)
29
+ assert result.dtype == np.uint8
30
+
31
+ def test_labels_to_binary_all_zeros(self):
32
+ """Test with all zero labels"""
33
+ labels = np.zeros((3, 3), dtype=np.uint32)
34
+ result = labels_to_binary(labels)
35
+ np.testing.assert_array_equal(result, np.zeros((3, 3), dtype=np.uint8))
36
+
37
+ def test_labels_to_binary_all_nonzero(self):
38
+ """Test with all non-zero labels"""
39
+ labels = np.ones((3, 3), dtype=np.uint32) * 5
40
+ result = labels_to_binary(labels)
41
+ np.testing.assert_array_equal(
42
+ result, np.ones((3, 3), dtype=np.uint8) * 255
43
+ )
44
+
45
+ def test_labels_to_binary_empty_image(self):
46
+ """Test with empty image"""
47
+ labels = np.zeros((0, 0), dtype=np.uint32)
48
+ result = labels_to_binary(labels)
49
+ assert result.shape == (0, 0)
50
+ assert result.dtype == np.uint8
51
+
52
+ def test_labels_to_binary_3d_image(self):
53
+ """Test with 3D image"""
54
+ labels = np.array(
55
+ [[[0, 1], [1, 2]], [[2, 0], [1, 1]]], dtype=np.uint32
56
+ )
57
+ result = labels_to_binary(labels)
58
+ expected = np.array(
59
+ [[[0, 255], [255, 255]], [[255, 0], [255, 255]]], dtype=np.uint8
60
+ )
61
+ np.testing.assert_array_equal(result, expected)
62
+
63
+ def test_labels_to_binary_float_input(self):
64
+ """Test with float input (should still work)"""
65
+ labels = np.array([[0.0, 1.5, 2.7]], dtype=np.float32)
66
+ result = labels_to_binary(labels)
67
+ expected = np.array([[0, 255, 255]], dtype=np.uint8)
68
+ np.testing.assert_array_equal(result, expected)
69
+
70
+ def test_invert_binary_labels_basic(self):
71
+ """Test basic inversion of binary mask"""
72
+ # Create test binary image
73
+ binary = np.array([[0, 1, 1], [1, 0, 0], [1, 1, 0]], dtype=np.uint32)
74
+
75
+ # Process
76
+ result = invert_binary_labels(binary)
77
+
78
+ # Check result - zeros become 255, non-zeros become 0
79
+ expected = np.array(
80
+ [[255, 0, 0], [0, 255, 255], [0, 0, 255]], dtype=np.uint8
81
+ )
82
+ np.testing.assert_array_equal(result, expected)
83
+ assert result.dtype == np.uint8
84
+
85
+ def test_invert_binary_labels_all_zeros(self):
86
+ """Test inversion with all zeros"""
87
+ binary = np.zeros((3, 3), dtype=np.uint32)
88
+ result = invert_binary_labels(binary)
89
+ # All zeros should become 255
90
+ np.testing.assert_array_equal(
91
+ result, np.ones((3, 3), dtype=np.uint8) * 255
92
+ )
93
+
94
+ def test_invert_binary_labels_all_ones(self):
95
+ """Test inversion with all ones"""
96
+ binary = np.ones((3, 3), dtype=np.uint32)
97
+ result = invert_binary_labels(binary)
98
+ # All ones should become zeros
99
+ np.testing.assert_array_equal(result, np.zeros((3, 3), dtype=np.uint8))
100
+
101
+ def test_invert_binary_labels_with_labels(self):
102
+ """Test inversion with multi-label image"""
103
+ # Create label image with different values
104
+ labels = np.array([[0, 1, 2], [3, 0, 5], [7, 8, 0]], dtype=np.uint32)
105
+
106
+ # Process
107
+ result = invert_binary_labels(labels)
108
+
109
+ # Check result - zeros become 255, all non-zero values become 0
110
+ expected = np.array(
111
+ [[255, 0, 0], [0, 255, 0], [0, 0, 255]], dtype=np.uint8
112
+ )
113
+ np.testing.assert_array_equal(result, expected)
114
+
115
+ def test_invert_binary_labels_3d(self):
116
+ """Test inversion with 3D image"""
117
+ binary = np.array(
118
+ [[[0, 1], [1, 0]], [[1, 0], [0, 1]]], dtype=np.uint32
119
+ )
120
+ result = invert_binary_labels(binary)
121
+ expected = np.array(
122
+ [[[255, 0], [0, 255]], [[0, 255], [255, 0]]], dtype=np.uint8
123
+ )
124
+ np.testing.assert_array_equal(result, expected)
125
+
126
+ def test_invert_binary_labels_empty(self):
127
+ """Test with empty image"""
128
+ binary = np.zeros((0, 0), dtype=np.uint32)
129
+ result = invert_binary_labels(binary)
130
+ assert result.shape == (0, 0)
131
+ assert result.dtype == np.uint8
132
+
133
+ def test_filter_label_by_id_basic(self):
134
+ """Test filtering to keep only one label ID"""
135
+ # Create test label image with multiple labels
136
+ labels = np.array([[0, 1, 2], [3, 1, 2], [1, 0, 3]], dtype=np.uint32)
137
+
138
+ # Keep only label 1
139
+ result = filter_label_by_id(labels, label_id=1)
140
+
141
+ # Check result - only label 1 should remain, others become 0
142
+ expected = np.array([[0, 1, 0], [0, 1, 0], [1, 0, 0]], dtype=np.uint32)
143
+ np.testing.assert_array_equal(result, expected)
144
+ assert result.dtype == labels.dtype
145
+
146
+ def test_filter_label_by_id_default_param(self):
147
+ """Test filtering with default parameter (label_id=1)"""
148
+ labels = np.array([[0, 1, 2], [1, 2, 0], [2, 0, 1]], dtype=np.uint32)
149
+ result = filter_label_by_id(labels)
150
+ expected = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]], dtype=np.uint32)
151
+ np.testing.assert_array_equal(result, expected)
152
+
153
+ def test_filter_label_by_id_nonexistent(self):
154
+ """Test filtering with label ID that doesn't exist"""
155
+ labels = np.array([[1, 2, 3], [2, 3, 1], [3, 1, 2]], dtype=np.uint32)
156
+ # Try to keep label 99 which doesn't exist
157
+ result = filter_label_by_id(labels, label_id=99)
158
+ # All should become background
159
+ expected = np.zeros_like(labels)
160
+ np.testing.assert_array_equal(result, expected)
161
+
162
+ def test_filter_label_by_id_3d(self):
163
+ """Test filtering with 3D label image"""
164
+ labels = np.array(
165
+ [[[1, 2], [3, 1]], [[2, 1], [1, 3]]], dtype=np.uint32
166
+ )
167
+ result = filter_label_by_id(labels, label_id=2)
168
+ expected = np.array(
169
+ [[[0, 2], [0, 0]], [[2, 0], [0, 0]]], dtype=np.uint32
170
+ )
171
+ np.testing.assert_array_equal(result, expected)
172
+
173
+ def test_filter_label_by_id_all_same(self):
174
+ """Test filtering when all pixels are the target label"""
175
+ labels = np.ones((3, 3), dtype=np.uint32) * 5
176
+ result = filter_label_by_id(labels, label_id=5)
177
+ # All should remain
178
+ np.testing.assert_array_equal(result, labels)
179
+
180
+ def test_filter_label_by_id_all_background(self):
181
+ """Test filtering with all background"""
182
+ labels = np.zeros((3, 3), dtype=np.uint32)
183
+ result = filter_label_by_id(labels, label_id=1)
184
+ # Should remain all zeros
185
+ np.testing.assert_array_equal(result, labels)
186
+
187
+ def test_mirror_labels_double_size_default_axis(self):
188
+ """Mirroring keeps the same shape and mirrors around largest area slice"""
189
+ image = np.zeros((4, 2, 2), dtype=np.uint16)
190
+ image[0, 0, 0] = 5 # slice 0 has 1 pixel
191
+ image[1, :, :] = 3 # slice 1 has 4 pixels (largest area)
192
+
193
+ result = mirror_labels(image)
194
+
195
+ # Shape should remain the same
196
+ assert result.shape == (4, 2, 2)
197
+ # Mirror around slice 1 (largest area)
198
+ # slice 0 gets from slice 2 (2*1 - 0 = 2), which is empty
199
+ # slice 1 gets from slice 1 (2*1 - 1 = 1), which has value 3
200
+ # slice 2 gets from slice 0 (2*1 - 2 = 0), which has value 5 at [0,0]
201
+ # slice 3 gets from slice -1 (2*1 - 3 = -1, out of bounds)
202
+ expected = np.zeros((4, 2, 2), dtype=np.uint16)
203
+ expected[0] = 0 # mirrored from empty slice 2
204
+ expected[1] = 3 + 5 # mirrored from slice 1 (value 3) with offset
205
+ expected[2, 0, 0] = (
206
+ 5 + 5
207
+ ) # mirrored from slice 0 (value 5 at [0,0]) with offset
208
+ expected[3] = 0 # out of bounds
209
+ np.testing.assert_array_equal(result, expected)
210
+ assert result.dtype == image.dtype
211
+
212
+ def test_mirror_labels_other_axis(self):
213
+ """Mirroring along a non-zero axis keeps shape and mirrors around largest area"""
214
+ image = np.zeros((1, 4, 4), dtype=np.int32)
215
+ image[0, 0, :] = 1 # slice 0: 4 pixels
216
+ image[0, 1, :] = (
217
+ 2 # slice 1: 4 pixels (will be selected as max_area_idx)
218
+ )
219
+ image[0, 2, 0] = 3 # slice 2: 1 pixel
220
+ image[0, 3, 0] = 4 # slice 3: 1 pixel
221
+
222
+ result = mirror_labels(image, axis=1)
223
+
224
+ # Shape should remain the same
225
+ assert result.shape == (1, 4, 4)
226
+ # Mirror around slice 0 (first slice with max area)
227
+ # slice 0 gets from slice 0 (2*0 - 0 = 0), which has value 1
228
+ # slice 1 gets from slice -1 (2*0 - 1 = -1, out of bounds)
229
+ # slice 2 gets from slice -2 (2*0 - 2 = -2, out of bounds)
230
+ # slice 3 gets from slice -3 (2*0 - 3 = -3, out of bounds)
231
+ expected = np.zeros((1, 4, 4), dtype=np.int32)
232
+ expected[0, 0, :] = (
233
+ 1 + 4
234
+ ) # mirrored from slice 0 (value 1) with offset
235
+ expected[0, 1:, :] = 0 # out of bounds
236
+ np.testing.assert_array_equal(result, expected)
237
+
238
+ def test_mirror_labels_prefers_larger_end(self):
239
+ """Mirrors around the slice with the largest area"""
240
+ image = np.zeros((4, 3, 3), dtype=np.uint8)
241
+ image[0, :2, :2] = 1 # slice 0: 4 pixels (largest area)
242
+ image[3, 0, 0] = 1 # slice 3: 1 pixel
243
+
244
+ result = mirror_labels(image)
245
+
246
+ # Shape should remain the same
247
+ assert result.shape == (4, 3, 3)
248
+ # Mirror around slice 0 (largest area)
249
+ # slice 0 mirrors slice 0 (2*0 - 0 = 0)
250
+ # slice 1 mirrors slice -1 (2*0 - 1 = -1, out of bounds)
251
+ # slice 2 mirrors slice -2 (2*0 - 2 = -2, out of bounds)
252
+ # slice 3 mirrors slice -3 (2*0 - 3 = -3, out of bounds)
253
+ expected = np.zeros((4, 3, 3), dtype=np.uint8)
254
+ expected[0, :2, :2] = 1 + 1 # mirrored from slice 0 itself
255
+ expected[1:] = 0 # out of bounds
256
+ np.testing.assert_array_equal(result, expected)
257
+
258
+ def test_mirror_labels_uniform(self):
259
+ """Mirroring uniform labels creates offset mirrored labels"""
260
+ image = np.ones((3, 3, 3), dtype=np.uint8)
261
+
262
+ result = mirror_labels(image)
263
+
264
+ # Shape should remain the same
265
+ assert result.shape == (3, 3, 3)
266
+ # All slices have equal area (9 pixels), so slice 0 is chosen
267
+ # Mirror around slice 0 (first slice with max area)
268
+ # slice 0 mirrors slice 0 (2*0 - 0 = 0)
269
+ # slice 1 mirrors slice -1 (2*0 - 1 = -1, out of bounds)
270
+ # slice 2 mirrors slice -2 (2*0 - 2 = -2, out of bounds)
271
+ expected = np.zeros((3, 3, 3), dtype=np.uint8)
272
+ expected[0] = 2 # mirrored from slice 0 (1 + 1)
273
+ expected[1:] = 0 # out of bounds
274
+ np.testing.assert_array_equal(result, expected)
275
+
276
+ def test_mirror_labels_invalid_axis(self):
277
+ """Invalid axis should raise an error"""
278
+ image = np.zeros((3, 3), dtype=np.uint8)
279
+
280
+ with pytest.raises(ValueError):
281
+ mirror_labels(image, axis=2)
282
+
283
+ def test_keep_slice_range_by_area_basic(self):
284
+ """Keep label content between minimum and maximum area, preserving shape"""
285
+ volume = np.zeros((5, 4, 4), dtype=np.int32)
286
+ volume[0, 0, 0] = 1 # area 1 (min)
287
+ volume[1, :2, :2] = 1 # area 4
288
+ volume[2, :3, :3] = 1 # area 9 (max)
289
+ volume[3, :1, :3] = 1 # area 3
290
+ volume[4, :2, :1] = 1 # area 2
291
+
292
+ result = keep_slice_range_by_area(volume)
293
+
294
+ # Shape should be preserved
295
+ assert result.shape == (5, 4, 4)
296
+ # Content between min (slice 0) and max (slice 2) should be kept
297
+ np.testing.assert_array_equal(result[0:3], volume[0:3])
298
+ # Content after max should be zeroed
299
+ np.testing.assert_array_equal(result[3:], np.zeros((2, 4, 4)))
300
+
301
+ def test_keep_slice_range_by_area_with_axis(self):
302
+ """Axis parameter allows zeroing content along any dimension while preserving shape"""
303
+ # Create volume with different areas along axis 1
304
+ volume = np.zeros((4, 5, 3), dtype=np.uint16)
305
+ volume[:2, 0, :2] = 1 # slice 0: area = 2*2 = 4
306
+ volume[:, 1, :] = 1 # slice 1: area = 4*3 = 12 (max)
307
+ volume[:3, 2, :] = 1 # slice 2: area = 3*3 = 9
308
+ volume[:2, 3, :2] = 1 # slice 3: area = 2*2 = 4
309
+ volume[0, 4, 0] = 1 # slice 4: area = 1 (min)
310
+
311
+ result = keep_slice_range_by_area(volume, axis=1)
312
+
313
+ # Shape should be preserved
314
+ assert result.shape == volume.shape
315
+ # Min area is at slice 4, max area is at slice 1, so range is 1-4 (inclusive)
316
+ # Slice 0 should be zeroed (before the range)
317
+ np.testing.assert_array_equal(
318
+ result[:, 0, :], np.zeros((4, 3), dtype=np.uint16)
319
+ )
320
+ # Slices 1-4 should be kept
321
+ np.testing.assert_array_equal(result[:, 1:5, :], volume[:, 1:5, :])
322
+
323
+ def test_keep_slice_range_by_area_uniform(self):
324
+ """Uniform area returns the original volume"""
325
+ volume = np.ones((3, 4, 4), dtype=np.uint8)
326
+
327
+ result = keep_slice_range_by_area(volume)
328
+
329
+ np.testing.assert_array_equal(result, volume)
330
+
331
+ def test_keep_slice_range_by_area_shape_preserved(self):
332
+ """Verify that output shape matches input shape (critical for image-label alignment)"""
333
+ # Simulate a label volume with 100 z-slices where labels exist in slices 20-80
334
+ volume = np.zeros((100, 50, 50), dtype=np.uint32)
335
+ volume[20, :10, :10] = 1 # Sparse content at slice 20 (min area)
336
+ for i in range(21, 80):
337
+ volume[i, :30, :30] = i # Denser content in middle slices
338
+ volume[79, :, :] = 100 # Maximum content at slice 79 (max area)
339
+ # Slices 0-19 and 80-99 should be empty and get zeroed
340
+
341
+ result = keep_slice_range_by_area(volume, axis=0)
342
+
343
+ # Critical: shape must be preserved to maintain alignment with image data
344
+ assert result.shape == (
345
+ 100,
346
+ 50,
347
+ 50,
348
+ ), "Output shape must match input shape"
349
+
350
+ # Slices before min (0-19) should be zeroed
351
+ assert np.all(
352
+ result[:20] == 0
353
+ ), "Slices before min-area slice should be zeroed"
354
+
355
+ # Slices between min and max (20-79) should be preserved
356
+ np.testing.assert_array_equal(
357
+ result[20:80],
358
+ volume[20:80],
359
+ err_msg="Label content in range should be preserved",
360
+ )
361
+
362
+ # Slices after max (80-99) should be zeroed
363
+ assert np.all(
364
+ result[80:] == 0
365
+ ), "Slices after max-area slice should be zeroed"
366
+
367
+ def test_keep_slice_range_by_area_invalid_dims(self):
368
+ """At least 3 dimensions are required"""
369
+ image = np.ones((4, 4), dtype=np.uint8)
370
+
371
+ with pytest.raises(ValueError):
372
+ keep_slice_range_by_area(image)
373
+
374
+ def test_intersect_label_images_basic(self, tmp_path):
375
+ """Primary file intersects with its paired secondary"""
376
+ label_a = np.array([[0, 5], [2, 0]], dtype=np.uint8)
377
+ label_b = np.array([[1, 5], [0, 0]], dtype=np.uint8)
378
+
379
+ primary_path = tmp_path / "sample_a.npy"
380
+ secondary_path = tmp_path / "sample_b.npy"
381
+ np.save(primary_path, label_a)
382
+ np.save(secondary_path, label_b)
383
+
384
+ def call_primary() -> np.ndarray:
385
+ filepath = str(primary_path)
386
+ assert filepath
387
+ return intersect_label_images(
388
+ label_a,
389
+ primary_suffix="_a.npy",
390
+ secondary_suffix="_b.npy",
391
+ )
392
+
393
+ result = call_primary()
394
+ expected = np.array([[0, 5], [0, 0]], dtype=np.uint8)
395
+ np.testing.assert_array_equal(result, expected)
396
+
397
+ def call_secondary() -> np.ndarray:
398
+ filepath = str(secondary_path)
399
+ assert filepath
400
+ return intersect_label_images(
401
+ label_b,
402
+ primary_suffix="_a.npy",
403
+ secondary_suffix="_b.npy",
404
+ )
405
+
406
+ with pytest.warns(UserWarning, match="Skipping secondary label image"):
407
+ secondary_result = call_secondary()
408
+ assert secondary_result is None
409
+
410
+ def test_intersect_label_images_retains_primary_labels(self, tmp_path):
411
+ label_a = np.zeros((4, 4), dtype=np.uint8)
412
+ label_b = np.zeros((4, 4), dtype=np.uint8)
413
+ label_a[1:3, 1:3] = 1
414
+ label_b[1:2, 1:3] = 2
415
+ label_b[2:3, 1:3] = 3
416
+
417
+ primary_path = tmp_path / "detail_a.npy"
418
+ secondary_path = tmp_path / "detail_b.npy"
419
+ np.save(primary_path, label_a)
420
+ np.save(secondary_path, label_b)
421
+
422
+ def call_primary():
423
+ filepath = str(primary_path)
424
+ assert filepath
425
+ return intersect_label_images(
426
+ label_a,
427
+ primary_suffix="_a.npy",
428
+ secondary_suffix="_b.npy",
429
+ )
430
+
431
+ result = call_primary()
432
+ expected = np.zeros_like(label_a)
433
+ expected[1:3, 1:3] = 1
434
+ np.testing.assert_array_equal(result, expected)
435
+
436
+ def test_intersect_label_images_preserve_primary_detail(self, tmp_path):
437
+ label_a = np.zeros((4, 4), dtype=np.uint8)
438
+ label_b = np.zeros((4, 4), dtype=np.uint8)
439
+ label_a[1:2, 1:3] = 4
440
+ label_a[2:3, 1:3] = 5
441
+ label_b[1:3, 1:3] = 7
442
+
443
+ primary_path = tmp_path / "detail_a.npy"
444
+ secondary_path = tmp_path / "detail_b.npy"
445
+ np.save(primary_path, label_a)
446
+ np.save(secondary_path, label_b)
447
+
448
+ def call_primary():
449
+ filepath = str(primary_path)
450
+ assert filepath
451
+ return intersect_label_images(
452
+ label_a,
453
+ primary_suffix="_a.npy",
454
+ secondary_suffix="_b.npy",
455
+ )
456
+
457
+ result = call_primary()
458
+ expected = np.zeros_like(label_a)
459
+ expected[1:2, 1:3] = 4
460
+ expected[2:3, 1:3] = 5
461
+ np.testing.assert_array_equal(result, expected)
462
+
463
+ def test_intersect_label_images_missing_pair(self, tmp_path):
464
+ label_a = np.ones((2, 2), dtype=np.uint16)
465
+ primary_path = tmp_path / "orphan_a.npy"
466
+ np.save(primary_path, label_a)
467
+
468
+ def call_primary():
469
+ filepath = str(primary_path)
470
+ assert filepath
471
+ return intersect_label_images(
472
+ label_a,
473
+ primary_suffix="_a.npy",
474
+ secondary_suffix="_b.npy",
475
+ )
476
+
477
+ with pytest.raises(FileNotFoundError):
478
+ call_primary()
479
+
480
+ def test_intersect_label_images_shape_mismatch(self, tmp_path):
481
+ label_a = np.ones((2, 2), dtype=np.uint16)
482
+ label_b = np.ones((3, 3), dtype=np.uint16)
483
+
484
+ primary_path = tmp_path / "sample_a.npy"
485
+ secondary_path = tmp_path / "sample_b.npy"
486
+ np.save(primary_path, label_a)
487
+ np.save(secondary_path, label_b)
488
+
489
+ def call_primary():
490
+ filepath = str(primary_path)
491
+ assert filepath
492
+ return intersect_label_images(
493
+ label_a,
494
+ primary_suffix="_a.npy",
495
+ secondary_suffix="_b.npy",
496
+ )
497
+
498
+ result = call_primary()
499
+ expected = np.ones_like(label_a)
500
+ np.testing.assert_array_equal(result, expected)
@@ -0,0 +1,142 @@
1
+ # src/napari_tmidas/_tests/test_processing_worker.py
2
+ import tempfile
3
+ from unittest.mock import Mock, patch
4
+
5
+ import numpy as np
6
+
7
+ from napari_tmidas._processing_worker import ProcessingWorker
8
+
9
+
10
+ class TestProcessingWorker:
11
+ def setup_method(self):
12
+ """Setup test environment"""
13
+ self.temp_dir = tempfile.mkdtemp()
14
+
15
+ def teardown_method(self):
16
+ """Cleanup"""
17
+ import shutil
18
+
19
+ shutil.rmtree(self.temp_dir)
20
+
21
+ def test_worker_initialization(self):
22
+ """Test ProcessingWorker initialization"""
23
+ file_list = ["/path/to/file1.tif", "/path/to/file2.tif"]
24
+ processing_func = Mock()
25
+ param_values = {"param1": "value1"}
26
+ output_folder = "/output"
27
+ input_suffix = ".tif"
28
+ output_suffix = "_processed.tif"
29
+
30
+ worker = ProcessingWorker(
31
+ file_list,
32
+ processing_func,
33
+ param_values,
34
+ output_folder,
35
+ input_suffix,
36
+ output_suffix,
37
+ )
38
+
39
+ assert worker.file_list == file_list
40
+ assert worker.processing_func == processing_func
41
+ assert worker.param_values == param_values
42
+ assert worker.output_folder == output_folder
43
+ assert worker.input_suffix == input_suffix
44
+ assert worker.output_suffix == output_suffix
45
+ assert not worker.stop_requested
46
+ assert worker.thread_count >= 1
47
+
48
+ def test_worker_stop(self):
49
+ """Test stopping the worker"""
50
+ worker = ProcessingWorker([], Mock(), {}, "", "", "")
51
+ assert not worker.stop_requested
52
+ worker.stop()
53
+ assert worker.stop_requested
54
+
55
+ @patch(
56
+ "napari_tmidas._processing_worker.concurrent.futures.ThreadPoolExecutor"
57
+ )
58
+ @patch("napari_tmidas._processing_worker.load_image_file")
59
+ def test_process_file_single_output(self, mock_load, mock_executor):
60
+ """Test processing a file with single output"""
61
+ # Mock the executor and future
62
+ mock_future = Mock()
63
+ mock_future.result.return_value = np.random.rand(100, 100)
64
+ mock_executor.return_value.__enter__.return_value.submit.return_value = (
65
+ mock_future
66
+ )
67
+ mock_executor.return_value.__enter__.return_value.as_completed.return_value = [
68
+ mock_future
69
+ ]
70
+
71
+ # Mock image loading
72
+ mock_load.return_value = np.random.rand(100, 100)
73
+
74
+ # Create worker
75
+ worker = ProcessingWorker(
76
+ ["/test/file.tif"],
77
+ Mock(return_value=np.random.rand(100, 100)),
78
+ {},
79
+ self.temp_dir,
80
+ ".tif",
81
+ "_processed.tif",
82
+ )
83
+
84
+ # Mock the run method to avoid threading issues
85
+ worker.run = Mock()
86
+
87
+ # Test process_file method
88
+ result = worker.process_file("/test/file.tif")
89
+
90
+ assert result is not None
91
+ assert "original_file" in result
92
+ assert "processed_file" in result
93
+
94
+ @patch("napari_tmidas._processing_worker.load_image_file")
95
+ def test_process_file_multiple_outputs(self, mock_load):
96
+ """Test processing a file with multiple outputs"""
97
+ # Mock image loading
98
+ mock_load.return_value = np.random.rand(100, 100)
99
+
100
+ # Create worker with function that returns multiple outputs
101
+ def multi_output_func(image):
102
+ return [image, image * 2, image * 3]
103
+
104
+ worker = ProcessingWorker(
105
+ ["/test/file.tif"],
106
+ multi_output_func,
107
+ {},
108
+ self.temp_dir,
109
+ ".tif",
110
+ "_processed.tif",
111
+ )
112
+
113
+ result = worker.process_file("/test/file.tif")
114
+
115
+ assert result is not None
116
+ assert "original_file" in result
117
+ assert "processed_files" in result
118
+ assert len(result["processed_files"]) == 3
119
+
120
+ @patch("napari_tmidas._processing_worker.load_image_file")
121
+ def test_process_file_folder_function(self, mock_load):
122
+ """Test processing with folder function that returns None"""
123
+ # Mock image loading
124
+ mock_load.return_value = np.random.rand(100, 100)
125
+
126
+ # Create worker with folder function
127
+ def folder_func(image):
128
+ return None # Folder functions don't return processed images
129
+
130
+ worker = ProcessingWorker(
131
+ ["/test/file.tif"],
132
+ folder_func,
133
+ {},
134
+ self.temp_dir,
135
+ ".tif",
136
+ "_processed.tif",
137
+ )
138
+
139
+ result = worker.process_file("/test/file.tif")
140
+
141
+ assert result is not None
142
+ assert result["processed_file"] is None