1import numpy as np
2import pytest
3from numpy.testing import assert_array_equal
4from scipy.ndimage import correlate
5
6from skimage import draw
7from skimage._shared._warnings import expected_warnings
8from skimage._shared.testing import fetch
9from skimage.io import imread
10from skimage.morphology import medial_axis, skeletonize, thin
11from skimage.morphology._skeletonize import (_generate_thin_luts,
12                                             G123_LUT, G123P_LUT)
13
14
15class TestSkeletonize():
16    def test_skeletonize_no_foreground(self):
17        im = np.zeros((5, 5))
18        result = skeletonize(im)
19        assert_array_equal(result, np.zeros((5, 5)))
20
21    def test_skeletonize_wrong_dim1(self):
22        im = np.zeros((5))
23        with pytest.raises(ValueError):
24            skeletonize(im)
25
26    def test_skeletonize_wrong_dim2(self):
27        im = np.zeros((5, 5, 5))
28        with pytest.raises(ValueError):
29            skeletonize(im, method='zhang')
30
31    def test_skeletonize_not_binary(self):
32        im = np.zeros((5, 5))
33        im[0, 0] = 1
34        im[0, 1] = 2
35        with pytest.raises(ValueError):
36            skeletonize(im)
37
38    def test_skeletonize_unexpected_value(self):
39        im = np.zeros((5, 5))
40        im[0, 0] = 2
41        with pytest.raises(ValueError):
42            skeletonize(im)
43
44    def test_skeletonize_all_foreground(self):
45        im = np.ones((3, 4))
46        skeletonize(im)
47
48    def test_skeletonize_single_point(self):
49        im = np.zeros((5, 5), np.uint8)
50        im[3, 3] = 1
51        result = skeletonize(im)
52        assert_array_equal(result, im)
53
54    def test_skeletonize_already_thinned(self):
55        im = np.zeros((5, 5), np.uint8)
56        im[3, 1:-1] = 1
57        im[2, -1] = 1
58        im[4, 0] = 1
59        result = skeletonize(im)
60        assert_array_equal(result, im)
61
62    def test_skeletonize_output(self):
63        im = imread(fetch("data/bw_text.png"), as_gray=True)
64
65        # make black the foreground
66        im = (im == 0)
67        result = skeletonize(im)
68
69        expected = np.load(fetch("data/bw_text_skeleton.npy"))
70        assert_array_equal(result, expected)
71
72    def test_skeletonize_num_neighbours(self):
73        # an empty image
74        image = np.zeros((300, 300))
75
76        # foreground object 1
77        image[10:-10, 10:100] = 1
78        image[-100:-10, 10:-10] = 1
79        image[10:-10, -100:-10] = 1
80
81        # foreground object 2
82        rs, cs = draw.line(250, 150, 10, 280)
83        for i in range(10):
84            image[rs + i, cs] = 1
85        rs, cs = draw.line(10, 150, 250, 280)
86        for i in range(20):
87            image[rs + i, cs] = 1
88
89        # foreground object 3
90        ir, ic = np.indices(image.shape)
91        circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2
92        circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2
93        image[circle1] = 1
94        image[circle2] = 0
95        result = skeletonize(image)
96
97        # there should never be a 2x2 block of foreground pixels in a skeleton
98        mask = np.array([[1,  1],
99                         [1,  1]], np.uint8)
100        blocks = correlate(result, mask, mode='constant')
101        assert not np.any(blocks == 4)
102
103    def test_lut_fix(self):
104        im = np.zeros((6, 6), np.uint8)
105        im[1, 2] = 1
106        im[2, 2] = 1
107        im[2, 3] = 1
108        im[3, 3] = 1
109        im[3, 4] = 1
110        im[4, 4] = 1
111        im[4, 5] = 1
112        result = skeletonize(im)
113        expected = np.array([[0, 0, 0, 0, 0, 0],
114                             [0, 0, 1, 0, 0, 0],
115                             [0, 0, 0, 1, 0, 0],
116                             [0, 0, 0, 0, 1, 0],
117                             [0, 0, 0, 0, 0, 1],
118                             [0, 0, 0, 0, 0, 0]], dtype=np.uint8)
119        assert np.all(result == expected)
120
121
122class TestThin():
123    @property
124    def input_image(self):
125        """image to test thinning with"""
126        ii = np.array([[0, 0, 0, 0, 0, 0, 0],
127                       [0, 1, 1, 1, 1, 1, 0],
128                       [0, 1, 0, 1, 1, 1, 0],
129                       [0, 1, 1, 1, 1, 1, 0],
130                       [0, 1, 1, 1, 1, 1, 0],
131                       [0, 1, 1, 1, 1, 1, 0],
132                       [0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8)
133        return ii
134
135    def test_zeros(self):
136        assert np.all(thin(np.zeros((10, 10))) == False)
137
138    def test_iter_1(self):
139        result = thin(self.input_image, 1).astype(np.uint8)
140        expected = np.array([[0, 0, 0, 0, 0, 0, 0],
141                             [0, 0, 1, 0, 0, 0, 0],
142                             [0, 1, 0, 1, 1, 0, 0],
143                             [0, 0, 1, 1, 1, 0, 0],
144                             [0, 0, 1, 1, 1, 0, 0],
145                             [0, 0, 0, 0, 0, 0, 0],
146                             [0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8)
147        assert_array_equal(result, expected)
148
149    def test_max_iter_kwarg_deprecation(self):
150        result1 = thin(self.input_image, max_num_iter=1).astype(np.uint8)
151        with expected_warnings(["`max_iter` is a deprecated argument name"]):
152            result2 = thin(self.input_image, max_iter=1).astype(np.uint8)
153        assert_array_equal(result1, result2)
154
155    def test_noiter(self):
156        result = thin(self.input_image).astype(np.uint8)
157        expected = np.array([[0, 0, 0, 0, 0, 0, 0],
158                             [0, 0, 1, 0, 0, 0, 0],
159                             [0, 1, 0, 1, 0, 0, 0],
160                             [0, 0, 1, 0, 0, 0, 0],
161                             [0, 0, 0, 0, 0, 0, 0],
162                             [0, 0, 0, 0, 0, 0, 0],
163                             [0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8)
164        assert_array_equal(result, expected)
165
166    def test_baddim(self):
167        for ii in [np.zeros((3)), np.zeros((3, 3, 3))]:
168            with pytest.raises(ValueError):
169                thin(ii)
170
171    def test_lut_generation(self):
172        g123, g123p = _generate_thin_luts()
173
174        assert_array_equal(g123, G123_LUT)
175        assert_array_equal(g123p, G123P_LUT)
176
177
178class TestMedialAxis():
179    def test_00_00_zeros(self):
180        '''Test skeletonize on an array of all zeros'''
181        result = medial_axis(np.zeros((10, 10), bool))
182        assert np.all(result == False)
183
184    def test_00_01_zeros_masked(self):
185        '''Test skeletonize on an array that is completely masked'''
186        result = medial_axis(np.zeros((10, 10), bool),
187                             np.zeros((10, 10), bool))
188        assert np.all(result == False)
189
190    def test_vertical_line(self):
191        '''Test a thick vertical line, issue #3861'''
192        img = np.zeros((9, 9))
193        img[:, 2] = 1
194        img[:, 3] = 1
195        img[:, 4] = 1
196
197        expected = np.full(img.shape, False)
198        expected[:, 3] = True
199
200        result = medial_axis(img)
201        assert_array_equal(result, expected)
202
203    def test_01_01_rectangle(self):
204        '''Test skeletonize on a rectangle'''
205        image = np.zeros((9, 15), bool)
206        image[1:-1, 1:-1] = True
207        #
208        # The result should be four diagonals from the
209        # corners, meeting in a horizontal line
210        #
211        expected = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
212                             [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
213                             [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
214                             [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
215                             [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
216                             [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
217                             [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
218                             [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
219                             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
220                            dtype=bool)
221        result = medial_axis(image)
222        assert np.all(result == expected)
223        result, distance = medial_axis(image, return_distance=True)
224        assert distance.max() == 4
225
226    def test_01_02_hole(self):
227        '''Test skeletonize on a rectangle with a hole in the middle'''
228        image = np.zeros((9, 15), bool)
229        image[1:-1, 1:-1] = True
230        image[4, 4:-4] = False
231        expected = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
232                             [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
233                             [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
234                             [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
235                             [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
236                             [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
237                             [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
238                             [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
239                             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
240                            dtype=bool)
241        result = medial_axis(image)
242        assert np.all(result == expected)
243
244    def test_narrow_image(self):
245        """Test skeletonize on a 1-pixel thin strip"""
246        image = np.zeros((1, 5), bool)
247        image[:, 1:-1] = True
248        result = medial_axis(image)
249        assert np.all(result == image)
250