1+ """Tests for emimesh.process_image_data module."""
2+ import numpy as np
3+ from unittest .mock import patch
4+ from emimesh .process_image_data import (
5+ mergecells , ncells , dilate , erode , smooth , removeislands ,
6+ opdict , parse_operations , _parse_to_dict
7+ )
8+
9+ class TestImageOperations :
10+ """Test individual image processing operations."""
11+
12+ def test_mergecells_basic (self ):
13+ """Test basic cell merging."""
14+ img = np .array ([[[1 , 1 , 2 ], [1 , 3 , 2 ], [4 , 4 , 4 ]]], dtype = np .uint32 )
15+ labels = [1 , 2 ]
16+
17+ result = mergecells (img , labels )
18+
19+ # All 1s and 2s should become the first label (1)
20+ non_zero_values = result [result > 0 ]
21+ unique_values = np .unique (non_zero_values )
22+
23+ # Should only have values 1, 3, 4 (1 and 2 merged to 1)
24+ assert set (unique_values ) == {1 , 3 , 4 }
25+ assert 3 in result # 3 should remain unchanged
26+ assert 4 in result # 4 should remain unchanged
27+
28+ def test_ncells_basic (self ):
29+ """Test keeping only N largest cells."""
30+ img = np .array ([[[1 , 1 , 2 ], [1 , 3 , 2 ], [4 , 4 , 4 ]]], dtype = np .uint32 )
31+
32+ result = ncells (img , ncells = 2 )
33+
34+ # Should keep only background (0) and the two largest cells (1 and 4)
35+ assert np .allclose (np .unique (result ), np .array ([0 , 1 ,4 ]))
36+
37+ def test_ncells_with_keep_labels (self ):
38+ """Test keeping specific cells regardless of size."""
39+ img = np .array ([[[1 , 1 , 2 ], [1 , 3 , 2 ], [4 , 4 , 4 ]]], dtype = np .uint32 )
40+ keep_labels = [2 ]
41+
42+ result = ncells (img , ncells = 1 , keep_cell_labels = keep_labels )
43+
44+ # Should only have background (0) and the kept label (2)
45+ assert np .allclose (np .unique (result ), np .array ([0 , 2 ]))
46+
47+ def test_removeislands_basic (self ):
48+ """Test removing small islands."""
49+ # Create an image with small and large connected components
50+ img = np .zeros ((10 , 10 , 10 ), dtype = np .uint32 )
51+ img [2 :4 , 2 :4 , 2 :4 ] = 1 # Small island (8 voxels)
52+ img [6 :9 , 6 :9 , 6 :9 ] = 2 # Large island (27 voxels)
53+
54+ result = removeislands (img , minsize = 10 )
55+
56+ # Small island should be removed, large one should remain
57+ assert 1 not in np .unique (result )
58+ assert 2 in np .unique (result )
59+
60+
61+ class TestOperationDictionary :
62+ """Test the operation dictionary."""
63+
64+ def test_opdict_contains_all_operations (self ):
65+ """Test that opdict contains all expected operations."""
66+ expected_ops = ["merge" , "smooth" , "dilate" , "erode" , "removeislands" , "ncells" ]
67+
68+ for op in expected_ops :
69+ assert op in opdict
70+ assert callable (opdict [op ])
71+
72+
73+ class TestParseOperations :
74+ """Test operation parsing functionality."""
75+
76+ def test_parse_to_dict_basic (self ):
77+ """Test basic dictionary parsing."""
78+ values = ["key1='value1'" , "key2=42" , "key3=True" ]
79+
80+ result = _parse_to_dict (values )
81+
82+ assert result ["key1" ] == "value1"
83+ assert result ["key2" ] == 42
84+ assert result ["key3" ] is True
85+
86+ def test_parse_to_dict_with_lists (self ):
87+ """Test parsing with list values."""
88+ values = ["labels='[1, 2, 3]'" , "radius=5" ]
89+
90+ result = _parse_to_dict (values )
91+
92+ assert result ["labels" ] == [1 , 2 , 3 ]
93+ assert result ["radius" ] == 5
94+
95+ def test_parse_operations_basic (self ):
96+ """Test basic operation parsing."""
97+ ops = [["merge" , "labels='[1, 2]'" , "radius=5" ]]
98+
99+ result = parse_operations (ops )
100+
101+ assert len (result ) == 1
102+ assert result [0 ][0 ] == "merge"
103+ assert result [0 ][1 ]["labels" ] == [1 , 2 ]
104+ assert result [0 ][1 ]["radius" ] == 5
105+
106+ def test_parse_operations_multiple (self ):
107+ """Test parsing multiple operations."""
108+ ops = [
109+ ["merge" , "labels='[1, 2]'" ],
110+ ["removeislands" , "minsize=100" ],
111+ ["dilate" , "radius=3" ]
112+ ]
113+
114+ result = parse_operations (ops )
115+
116+ assert len (result ) == 3
117+ assert result [0 ][0 ] == "merge"
118+ assert result [1 ][0 ] == "removeislands"
119+ assert result [2 ][0 ] == "dilate"
120+
121+
122+ class TestImageProcessingIntegration :
123+ """Integration tests for image processing operations."""
124+
125+ def test_dilate_operation (self ):
126+ """Test dilation operation."""
127+ img = np .zeros ((10 , 10 , 10 ), dtype = np .uint32 )
128+ img [4 :6 , 4 :6 , 4 :6 ] = 1
129+
130+ # Mock nbmorph.dilate_labels_spherical to avoid dependency
131+ with patch ('emimesh.process_image_data.nbmorph' ) as mock_nbmorph :
132+ mock_nbmorph .dilate_labels_spherical .return_value = img # Return same for simplicity
133+
134+ result = dilate (img , radius = 2 )
135+
136+ mock_nbmorph .dilate_labels_spherical .assert_called_once_with (img , radius = 2 )
137+
138+ def test_erode_operation (self ):
139+ """Test erosion operation."""
140+ img = np .ones ((10 , 10 , 10 ), dtype = np .uint32 )
141+
142+ # Mock nbmorph.erode_labels_spherical to avoid dependency
143+ with patch ('emimesh.process_image_data.nbmorph' ) as mock_nbmorph :
144+ mock_nbmorph .erode_labels_spherical .return_value = img # Return same for simplicity
145+
146+ result = erode (img , radius = 2 )
147+
148+ mock_nbmorph .erode_labels_spherical .assert_called_once_with (img , radius = 2 )
149+
150+ def test_smooth_operation (self ):
151+ """Test smoothing operation."""
152+ img = np .ones ((10 , 10 , 10 ), dtype = np .uint32 )
153+
154+ # Mock nbmorph.smooth_labels_spherical to avoid dependency
155+ with patch ('emimesh.process_image_data.nbmorph' ) as mock_nbmorph :
156+ mock_nbmorph .smooth_labels_spherical .return_value = img # Return same for simplicity
157+
158+ result = smooth (img , iterations = 5 , radius = 3 )
159+
160+ mock_nbmorph .smooth_labels_spherical .assert_called_once_with (
161+ img , radius = 3 , iterations = 5 , dilate_radius = 3
162+ )
0 commit comments