1import math
2import unittest
3
4import numpy as np
5import pytest
6from numpy.testing import assert_equal
7from pytest import raises, warns
8
9from skimage._shared.testing import expected_warnings
10from skimage.morphology import extrema
11
12
13eps = 1e-12
14
15
16def diff(a, b):
17    a = np.asarray(a, dtype=np.float64)
18    b = np.asarray(b, dtype=np.float64)
19    t = ((a - b) ** 2).sum()
20    return math.sqrt(t)
21
22
23class TestExtrema():
24
25    def test_saturated_arithmetic(self):
26        """Adding/subtracting a constant and clipping"""
27        # Test for unsigned integer
28        data = np.array([[250, 251, 5, 5],
29                         [100, 200, 253, 252],
30                         [4, 10, 1, 3]],
31                        dtype=np.uint8)
32        # adding the constant
33        img_constant_added = extrema._add_constant_clip(data, 4)
34        expected = np.array([[254, 255, 9, 9],
35                             [104, 204, 255, 255],
36                             [8, 14, 5, 7]],
37                            dtype=np.uint8)
38        error = diff(img_constant_added, expected)
39        assert error < eps
40        img_constant_subtracted = extrema._subtract_constant_clip(data, 4)
41        expected = np.array([[246, 247, 1, 1],
42                             [96, 196, 249, 248],
43                             [0, 6, 0, 0]],
44                            dtype=np.uint8)
45        error = diff(img_constant_subtracted, expected)
46        assert error < eps
47
48        # Test for signed integer
49        data = np.array([[32767, 32766],
50                         [-32768, -32767]],
51                        dtype=np.int16)
52        img_constant_added = extrema._add_constant_clip(data, 1)
53        expected = np.array([[32767, 32767],
54                             [-32767, -32766]],
55                            dtype=np.int16)
56        error = diff(img_constant_added, expected)
57        assert error < eps
58        img_constant_subtracted = extrema._subtract_constant_clip(data, 1)
59        expected = np.array([[32766, 32765],
60                             [-32768, -32768]],
61                            dtype=np.int16)
62        error = diff(img_constant_subtracted, expected)
63        assert error < eps
64
65    def test_h_maxima(self):
66        """h-maxima for various data types"""
67
68        data = np.array([[10, 11, 13, 14, 14, 15, 14, 14, 13, 11],
69                         [11, 13, 15, 16, 16, 16, 16, 16, 15, 13],
70                         [13, 15, 40, 40, 18, 18, 18, 60, 60, 15],
71                         [14, 16, 40, 40, 19, 19, 19, 60, 60, 16],
72                         [14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
73                         [15, 16, 18, 19, 19, 20, 19, 19, 18, 16],
74                         [14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
75                         [14, 16, 80, 80, 19, 19, 19, 100, 100, 16],
76                         [13, 15, 80, 80, 18, 18, 18, 100, 100, 15],
77                         [11, 13, 15, 16, 16, 16, 16, 16, 15, 13]],
78                        dtype=np.uint8)
79
80        expected_result = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
81                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
82                                    [0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
83                                    [0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
84                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
85                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
86                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
87                                    [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
88                                    [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
89                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
90                                   dtype=np.uint8)
91        for dtype in [np.uint8, np.uint64, np.int8, np.int64]:
92            data = data.astype(dtype)
93            out = extrema.h_maxima(data, 40)
94
95            error = diff(expected_result, out)
96            assert error < eps
97
98    def test_h_minima(self):
99        """h-minima for various data types"""
100
101        data = np.array([[10, 11, 13, 14, 14, 15, 14, 14, 13, 11],
102                         [11, 13, 15, 16, 16, 16, 16, 16, 15, 13],
103                         [13, 15, 40, 40, 18, 18, 18, 60, 60, 15],
104                         [14, 16, 40, 40, 19, 19, 19, 60, 60, 16],
105                         [14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
106                         [15, 16, 18, 19, 19, 20, 19, 19, 18, 16],
107                         [14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
108                         [14, 16, 80, 80, 19, 19, 19, 100, 100, 16],
109                         [13, 15, 80, 80, 18, 18, 18, 100, 100, 15],
110                         [11, 13, 15, 16, 16, 16, 16, 16, 15, 13]],
111                        dtype=np.uint8)
112        data = 100 - data
113        expected_result = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
114                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
115                                    [0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
116                                    [0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
117                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
118                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
119                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
120                                    [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
121                                    [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
122                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
123                                   dtype=np.uint8)
124        for dtype in [np.uint8, np.uint64, np.int8, np.int64]:
125            data = data.astype(dtype)
126            out = extrema.h_minima(data, 40)
127
128            error = diff(expected_result, out)
129            assert error < eps
130            assert out.dtype == expected_result.dtype
131
132    def test_extrema_float(self):
133        """specific tests for float type"""
134        data = np.array([[0.10, 0.11, 0.13, 0.14, 0.14, 0.15, 0.14,
135                          0.14, 0.13, 0.11],
136                         [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16,
137                          0.16, 0.15, 0.13],
138                         [0.13, 0.15, 0.40, 0.40, 0.18, 0.18, 0.18,
139                          0.60, 0.60, 0.15],
140                         [0.14, 0.16, 0.40, 0.40, 0.19, 0.19, 0.19,
141                          0.60, 0.60, 0.16],
142                         [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19,
143                          0.19, 0.18, 0.16],
144                         [0.15, 0.182, 0.18, 0.19, 0.204, 0.20, 0.19,
145                          0.19, 0.18, 0.16],
146                         [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19,
147                          0.19, 0.18, 0.16],
148                         [0.14, 0.16, 0.80, 0.80, 0.19, 0.19, 0.19,
149                          1.0, 1.0, 0.16],
150                         [0.13, 0.15, 0.80, 0.80, 0.18, 0.18, 0.18,
151                          1.0, 1.0, 0.15],
152                         [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16,
153                          0.16, 0.15, 0.13]],
154                        dtype=np.float32)
155        inverted_data = 1.0 - data
156
157        out = extrema.h_maxima(data, 0.003)
158        expected_result = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
159                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
160                                    [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
161                                    [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
162                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
163                                    [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
164                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
165                                    [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
166                                    [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
167                                    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
168                                   dtype=np.uint8)
169
170        error = diff(expected_result, out)
171        assert error < eps
172
173        out = extrema.h_minima(inverted_data, 0.003)
174        error = diff(expected_result, out)
175        assert error < eps
176
177    def test_h_maxima_float_image(self):
178        """specific tests for h-maxima float image type"""
179        w = 10
180        x, y = np.mgrid[0:w, 0:w]
181        data = 20 - 0.2 * ((x - w / 2) ** 2 + (y - w / 2) ** 2)
182        data[2:4, 2:4] = 40
183        data[2:4, 7:9] = 60
184        data[7:9, 2:4] = 80
185        data[7:9, 7:9] = 100
186        data = data.astype(np.float32)
187
188        expected_result = np.zeros_like(data)
189        expected_result[(data > 19.9)] = 1.0
190
191        for h in [1.0e-12, 1.0e-6, 1.0e-3, 1.0e-2, 1.0e-1, 0.1]:
192            out = extrema.h_maxima(data, h)
193            error = diff(expected_result, out)
194            assert error < eps
195
196    def test_h_maxima_float_h(self):
197        """specific tests for h-maxima float h parameter"""
198        data = np.array([[0, 0, 0, 0, 0],
199                         [0, 3, 3, 3, 0],
200                         [0, 3, 4, 3, 0],
201                         [0, 3, 3, 3, 0],
202                         [0, 0, 0, 0, 0]], dtype=np.uint8)
203
204        h_vals = np.linspace(1.0, 2.0, 100)
205        failures = 0
206
207        for h in h_vals:
208            if h % 1 != 0:
209                msgs = ['possible precision loss converting image']
210            else:
211                msgs = []
212
213            with expected_warnings(msgs):
214                maxima = extrema.h_maxima(data, h)
215
216            if (maxima[2, 2] == 0):
217                failures += 1
218
219        assert (failures == 0)
220
221    def test_h_maxima_large_h(self):
222        """test that h-maxima works correctly for large h"""
223        data = np.array([[10, 10, 10, 10, 10],
224                         [10, 13, 13, 13, 10],
225                         [10, 13, 14, 13, 10],
226                         [10, 13, 13, 13, 10],
227                         [10, 10, 10, 10, 10]], dtype=np.uint8)
228
229        maxima = extrema.h_maxima(data, 5)
230        assert (np.sum(maxima) == 0)
231
232        data = np.array([[10, 10, 10, 10, 10],
233                         [10, 13, 13, 13, 10],
234                         [10, 13, 14, 13, 10],
235                         [10, 13, 13, 13, 10],
236                         [10, 10, 10, 10, 10]], dtype=np.float32)
237
238        maxima = extrema.h_maxima(data, 5.0)
239        assert (np.sum(maxima) == 0)
240
241    def test_h_minima_float_image(self):
242        """specific tests for h-minima float image type"""
243        w = 10
244        x, y = np.mgrid[0:w, 0:w]
245        data = 180 + 0.2 * ((x - w / 2) ** 2 + (y - w / 2) ** 2)
246        data[2:4, 2:4] = 160
247        data[2:4, 7:9] = 140
248        data[7:9, 2:4] = 120
249        data[7:9, 7:9] = 100
250        data = data.astype(np.float32)
251
252        expected_result = np.zeros_like(data)
253        expected_result[(data < 180.1)] = 1.0
254
255        for h in [1.0e-12, 1.0e-6, 1.0e-3, 1.0e-2, 1.0e-1, 0.1]:
256            out = extrema.h_minima(data, h)
257            error = diff(expected_result, out)
258            assert error < eps
259
260    def test_h_minima_float_h(self):
261        """specific tests for h-minima float h parameter"""
262        data = np.array([[4, 4, 4, 4, 4],
263                         [4, 1, 1, 1, 4],
264                         [4, 1, 0, 1, 4],
265                         [4, 1, 1, 1, 4],
266                         [4, 4, 4, 4, 4]], dtype=np.uint8)
267
268        h_vals = np.linspace(1.0, 2.0, 100)
269        failures = 0
270        for h in h_vals:
271            if h % 1 != 0:
272                msgs = ['possible precision loss converting image']
273            else:
274                msgs = []
275
276            with expected_warnings(msgs):
277                minima = extrema.h_minima(data, h)
278
279            if (minima[2, 2] == 0):
280                failures += 1
281
282        assert (failures == 0)
283
284    def test_h_minima_large_h(self):
285        """test that h-minima works correctly for large h"""
286        data = np.array([[14, 14, 14, 14, 14],
287                         [14, 11, 11, 11, 14],
288                         [14, 11, 10, 11, 14],
289                         [14, 11, 11, 11, 14],
290                         [14, 14, 14, 14, 14]], dtype=np.uint8)
291
292        maxima = extrema.h_minima(data, 5)
293        assert (np.sum(maxima) == 0)
294
295        data = np.array([[14, 14, 14, 14, 14],
296                         [14, 11, 11, 11, 14],
297                         [14, 11, 10, 11, 14],
298                         [14, 11, 11, 11, 14],
299                         [14, 14, 14, 14, 14]], dtype=np.float32)
300
301        maxima = extrema.h_minima(data, 5.0)
302        assert (np.sum(maxima) == 0)
303
304
305class TestLocalMaxima(unittest.TestCase):
306    """Some tests for local_minima are included as well."""
307
308    supported_dtypes = [
309        np.uint8, np.uint16, np.uint32, np.uint64,
310        np.int8, np.int16, np.int32, np.int64,
311        np.float32, np.float64
312    ]
313    image = np.array(
314        [[1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0],
315         [1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0],
316         [0, 0, 0, 2, 0, 0, 3, 3, 0, 0, 4, 0, 2, 0, 0],
317         [0, 1, 0, 0, 0, 0, 0, 0, 4, 4, 0, 3, 0, 0, 0],
318         [0, 2, 0, 1, 0, 2, 1, 0, 0, 0, 0, 3, 0, 0, 0],
319         [0, 0, 2, 0, 2, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0]],
320        dtype=np.uint8
321    )
322    # Connectivity 2, maxima can touch border, returned with default values
323    expected_default = np.array(
324        [[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
325         [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
326         [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
327         [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
328         [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
329         [0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]],
330        dtype=bool
331    )
332    # Connectivity 1 (cross), maxima can touch border
333    expected_cross = np.array(
334        [[1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0],
335         [1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0],
336         [0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0],
337         [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0],
338         [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
339         [0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]],
340        dtype=bool
341    )
342
343    def test_empty(self):
344        """Test result with empty image."""
345        result = extrema.local_maxima(np.array([[]]), indices=False)
346        assert result.size == 0
347        assert result.dtype == bool
348        assert result.shape == (1, 0)
349
350        result = extrema.local_maxima(np.array([]), indices=True)
351        assert isinstance(result, tuple)
352        assert len(result) == 1
353        assert result[0].size == 0
354        assert result[0].dtype == np.intp
355
356        result = extrema.local_maxima(np.array([[]]), indices=True)
357        assert isinstance(result, tuple)
358        assert len(result) == 2
359        assert result[0].size == 0
360        assert result[0].dtype == np.intp
361        assert result[1].size == 0
362        assert result[1].dtype == np.intp
363
364    def test_dtypes(self):
365        """Test results with default configuration for all supported dtypes."""
366        for dtype in self.supported_dtypes:
367            result = extrema.local_maxima(self.image.astype(dtype))
368            assert result.dtype == bool
369            assert_equal(result, self.expected_default)
370
371    def test_dtypes_old(self):
372        """
373        Test results with default configuration and data copied from old unit
374        tests for all supported dtypes.
375        """
376        data = np.array(
377            [[10, 11, 13, 14, 14, 15, 14, 14, 13, 11],
378             [11, 13, 15, 16, 16, 16, 16, 16, 15, 13],
379             [13, 15, 40, 40, 18, 18, 18, 60, 60, 15],
380             [14, 16, 40, 40, 19, 19, 19, 60, 60, 16],
381             [14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
382             [15, 16, 18, 19, 19, 20, 19, 19, 18, 16],
383             [14, 16, 18, 19, 19, 19, 19, 19, 18, 16],
384             [14, 16, 80, 80, 19, 19, 19, 100, 100, 16],
385             [13, 15, 80, 80, 18, 18, 18, 100, 100, 15],
386             [11, 13, 15, 16, 16, 16, 16, 16, 15, 13]],
387            dtype=np.uint8
388        )
389        expected = np.array(
390            [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
391             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
392             [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
393             [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
394             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
395             [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
396             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
397             [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
398             [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
399             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
400            dtype=bool
401        )
402        for dtype in self.supported_dtypes:
403            image = data.astype(dtype)
404            result = extrema.local_maxima(image)
405            assert result.dtype == bool
406            assert_equal(result, expected)
407
408    def test_connectivity(self):
409        """Test results if footprint is a scalar."""
410        # Connectivity 1: generates cross shaped footprint
411        result_conn1 = extrema.local_maxima(self.image, connectivity=1)
412        assert result_conn1.dtype == bool
413        assert_equal(result_conn1, self.expected_cross)
414
415        # Connectivity 2: generates square shaped footprint
416        result_conn2 = extrema.local_maxima(self.image, connectivity=2)
417        assert result_conn2.dtype == bool
418        assert_equal(result_conn2, self.expected_default)
419
420        # Connectivity 3: generates square shaped footprint
421        result_conn3 = extrema.local_maxima(self.image, connectivity=3)
422        assert result_conn3.dtype == bool
423        assert_equal(result_conn3, self.expected_default)
424
425    def test_footprint(self):
426        """Test results if footprint is given."""
427        footprint_cross = np.array(
428            [[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=bool)
429        result_footprint_cross = extrema.local_maxima(
430            self.image, footprint=footprint_cross)
431        assert result_footprint_cross.dtype == bool
432        assert_equal(result_footprint_cross, self.expected_cross)
433
434        for footprint in [
435            ((True,) * 3,) * 3,
436            np.ones((3, 3), dtype=np.float64),
437            np.ones((3, 3), dtype=np.uint8),
438            np.ones((3, 3), dtype=bool),
439        ]:
440            # Test different dtypes for footprint which expects a boolean array
441            # but will accept and convert other types if possible
442            result_footprint_square = extrema.local_maxima(
443                self.image, footprint=footprint
444            )
445            assert result_footprint_square.dtype == bool
446            assert_equal(result_footprint_square, self.expected_default)
447
448        footprint_x = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], dtype=bool)
449        expected_footprint_x = np.array(
450            [[1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0],
451             [1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
452             [0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
453             [0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
454             [0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0],
455             [0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0]],
456            dtype=bool
457        )
458        result_footprint_x = extrema.local_maxima(self.image,
459                                                  footprint=footprint_x)
460        assert result_footprint_x.dtype == bool
461        assert_equal(result_footprint_x, expected_footprint_x)
462
463    def test_indices(self):
464        """Test output if indices of peaks are desired."""
465        # Connectivity 1
466        expected_conn1 = np.nonzero(self.expected_cross)
467        result_conn1 = extrema.local_maxima(self.image, connectivity=1,
468                                            indices=True)
469        assert_equal(result_conn1, expected_conn1)
470
471        # Connectivity 2
472        expected_conn2 = np.nonzero(self.expected_default)
473        result_conn2 = extrema.local_maxima(self.image, connectivity=2,
474                                            indices=True)
475        assert_equal(result_conn2, expected_conn2)
476
477    def test_allow_borders(self):
478        """Test maxima detection at the image border."""
479        # Use connectivity 1 to allow many maxima, only filtering at border is
480        # of interest
481        result_with_boder = extrema.local_maxima(
482            self.image, connectivity=1, allow_borders=True)
483        assert result_with_boder.dtype == bool
484        assert_equal(result_with_boder, self.expected_cross)
485
486        expected_without_border = np.array(
487            [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
488             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
489             [0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0],
490             [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0],
491             [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
492             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
493            dtype=bool
494        )
495        result_without_border = extrema.local_maxima(
496            self.image, connectivity=1, allow_borders=False)
497        assert result_with_boder.dtype == bool
498        assert_equal(result_without_border, expected_without_border)
499
500    def test_nd(self):
501        """Test one- and three-dimensional case."""
502        # One-dimension
503        x_1d = np.array([1, 1, 0, 1, 2, 3, 0, 2, 1, 2, 0])
504        expected_1d = np.array([1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0],
505                               dtype=bool)
506        result_1d = extrema.local_maxima(x_1d)
507        assert result_1d.dtype == bool
508        assert_equal(result_1d, expected_1d)
509
510        # 3-dimensions (adapted from old unit test)
511        x_3d = np.zeros((8, 8, 8), dtype=np.uint8)
512        expected_3d = np.zeros((8, 8, 8), dtype=bool)
513        # first maximum: only one pixel
514        x_3d[1, 1:3, 1:3] = 100
515        x_3d[2, 2, 2] = 200
516        x_3d[3, 1:3, 1:3] = 100
517        expected_3d[2, 2, 2] = 1
518        # second maximum: three pixels in z-direction
519        x_3d[5:8, 1, 1] = 200
520        expected_3d[5:8, 1, 1] = 1
521        # third: two maxima in 0 and 3.
522        x_3d[0, 5:8, 5:8] = 200
523        x_3d[1, 6, 6] = 100
524        x_3d[2, 5:7, 5:7] = 200
525        x_3d[0:3, 5:8, 5:8] += 50
526        expected_3d[0, 5:8, 5:8] = 1
527        expected_3d[2, 5:7, 5:7] = 1
528        # four : one maximum in the corner of the square
529        x_3d[6:8, 6:8, 6:8] = 200
530        x_3d[7, 7, 7] = 255
531        expected_3d[7, 7, 7] = 1
532        result_3d = extrema.local_maxima(x_3d)
533        assert result_3d.dtype == bool
534        assert_equal(result_3d, expected_3d)
535
536    def test_constant(self):
537        """Test behaviour for 'flat' images."""
538        const_image = np.full((7, 6), 42, dtype=np.uint8)
539        expected = np.zeros((7, 6), dtype=np.uint8)
540        for dtype in self.supported_dtypes:
541            const_image = const_image.astype(dtype)
542            # test for local maxima
543            result = extrema.local_maxima(const_image)
544            assert result.dtype == bool
545            assert_equal(result, expected)
546            # test for local minima
547            result = extrema.local_minima(const_image)
548            assert result.dtype == bool
549            assert_equal(result, expected)
550
551    def test_extrema_float(self):
552        """Specific tests for float type."""
553        # Copied from old unit test for local_maxma
554        image = np.array(
555            [[0.10, 0.11, 0.13, 0.14, 0.14, 0.15, 0.14, 0.14, 0.13, 0.11],
556             [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13],
557             [0.13, 0.15, 0.40, 0.40, 0.18, 0.18, 0.18, 0.60, 0.60, 0.15],
558             [0.14, 0.16, 0.40, 0.40, 0.19, 0.19, 0.19, 0.60, 0.60, 0.16],
559             [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16],
560             [0.15, 0.182, 0.18, 0.19, 0.204, 0.20, 0.19, 0.19, 0.18, 0.16],
561             [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16],
562             [0.14, 0.16, 0.80, 0.80, 0.19, 0.19, 0.19, 1.0, 1.0, 0.16],
563             [0.13, 0.15, 0.80, 0.80, 0.18, 0.18, 0.18, 1.0, 1.0, 0.15],
564             [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13]],
565            dtype=np.float32
566        )
567        inverted_image = 1.0 - image
568        expected_result = np.array(
569            [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
570             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
571             [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
572             [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
573             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
574             [0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
575             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
576             [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
577             [0, 0, 1, 1, 0, 0, 0, 1, 1, 0],
578             [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
579            dtype=bool
580        )
581
582        # Test for local maxima with automatic step calculation
583        result = extrema.local_maxima(image)
584        assert result.dtype == bool
585        assert_equal(result, expected_result)
586
587        # Test for local minima with automatic step calculation
588        result = extrema.local_minima(inverted_image)
589        assert result.dtype == bool
590        assert_equal(result, expected_result)
591
592    def test_exceptions(self):
593        """Test if input validation triggers correct exceptions."""
594        # Mismatching number of dimensions
595        with raises(ValueError, match="number of dimensions"):
596            extrema.local_maxima(
597                self.image, footprint=np.ones((3, 3, 3), dtype=bool))
598        with raises(ValueError, match="number of dimensions"):
599            extrema.local_maxima(
600                self.image, footprint=np.ones((3,), dtype=bool))
601
602        # All dimensions in footprint must be of size 3
603        with raises(ValueError, match="dimension size"):
604            extrema.local_maxima(
605                self.image, footprint=np.ones((2, 3), dtype=bool))
606        with raises(ValueError, match="dimension size"):
607            extrema.local_maxima(
608                self.image, footprint=np.ones((5, 5), dtype=bool))
609
610        with raises(TypeError, match="float16 which is not supported"):
611            extrema.local_maxima(np.empty(1, dtype=np.float16))
612
613    def test_small_array(self):
614        """Test output for arrays with dimension smaller 3.
615
616        If any dimension of an array is smaller than 3 and `allow_borders` is
617        false a footprint, which has at least 3 elements in each
618        dimension, can't be applied. This is an implementation detail so
619        `local_maxima` should still return valid output (see gh-3261).
620
621        If `allow_borders` is true the array is padded internally and there is
622        no problem.
623        """
624        warning_msg = "maxima can't exist .* any dimension smaller 3 .*"
625        x = np.array([0, 1])
626        extrema.local_maxima(x, allow_borders=True)  # no warning
627        with warns(UserWarning, match=warning_msg):
628            result = extrema.local_maxima(x, allow_borders=False)
629        assert_equal(result, [0, 0])
630        assert result.dtype == bool
631
632        x = np.array([[1, 2], [2, 2]])
633        extrema.local_maxima(x, allow_borders=True, indices=True)  # no warning
634        with warns(UserWarning, match=warning_msg):
635            result = extrema.local_maxima(x, allow_borders=False, indices=True)
636        assert_equal(result, np.zeros((2, 0), dtype=np.intp))
637        assert result[0].dtype == np.intp
638        assert result[1].dtype == np.intp
639
640
641@pytest.mark.parametrize(
642    'function',
643    ['local_maxima', 'local_minima', 'h_minima', 'h_maxima']
644)
645def test_selem_kwarg_deprecation(function):
646    img = np.zeros((16, 16))
647    args = (20,) if function.startswith('h_') else ()
648    with expected_warnings(["`selem` is a deprecated argument name"]):
649        getattr(extrema, function)(img, *args, selem=np.ones((3, 3)))
650