Repository: scikit-image/scikit-image

Issue:
'subpixel' boundaries at the edges of an image
## Description

When using the function `find_boundaries` with `mode='subpixel'` the behavior at the edges of the image seems strange to me.

From the documentation it is not clear to me what the intended behavior should be. Without an option to explicitly specify the behavior at the edges of the image, I would expect a constant input image to have no boundary at all or maybe straight lines at the edges of the image. However, for constant images with a non-zero value the actual image alternates between `True` and `False`. If the image is completely zero, there is no such effect.

## Way to reproduce
```python
import skimage.segmentation
import numpy as np
skimage.segmentation.find_boundaries(np.ones((3,3), dtype=int), mode='subpixel')
```
returns
```python
array([[False,  True, False,  True, False],
       [ True, False, False, False,  True],
       [False, False, False, False, False],
       [ True, False, False, False,  True],
       [False,  True, False,  True, False]])

```

I would expect the same output as `np.zeros((5,5), dtype=bool)`.


## Version information
```python
from __future__ import print_function
import sys; print(sys.version)
import platform; print(platform.platform())
import skimage; print("scikit-image version: {}".format(skimage.__version__))
import numpy; print("numpy version: {}".format(numpy.__version__))
```

```python
3.7.10 | packaged by conda-forge | (default, Feb 19 2021, 16:07:37)
[GCC 9.3.0]
Linux-5.4.0-74-generic-x86_64-with-debian-bullseye-sid
scikit-image version: 0.18.1
numpy version: 1.20.3
```



Discussion:
Hello @firerain42,

Thanks for reporting! I agree with you, the current behaviour is not consistent.

Would you like to try and investigate by yourself? https://github.com/scikit-image/scikit-image/blob/a1c58462b7fe752a9d05d99c997d2d4825bc3644/skimage/segmentation/boundaries.py#L11

You could run this function line by line, starting with `label_img = np.ones((3, 3), dtype=int)` and comparing with `label_img = np.zeros((3, 3), dtype=int)` to see what happens. Let me know, thanks!
Hello @mkcor,

I think the problem is the padding in
https://github.com/scikit-image/scikit-image/blob/23fd40b20a7f6dbd2c78ca85103d42c1c97ae17d/skimage/segmentation/boundaries.py#L37-L39
which introduces 0 as an image value. Replacing it with `np.pad(label_img_expanded, 1, mode='edge')` should do the trick. I would have sent a pull request, but I couldn't get the tests running on my PC (and don't have the time to find the cause).

Resolving patch:
diff --git a/skimage/segmentation/boundaries.py b/skimage/segmentation/boundaries.py
--- a/skimage/segmentation/boundaries.py
+++ b/skimage/segmentation/boundaries.py
@@ -34,8 +34,7 @@ def _find_boundaries_subpixel(label_img):
     edges = np.ones(label_img_expanded.shape, dtype=bool)
     edges[pixels] = False
     label_img_expanded[edges] = max_label
-    windows = view_as_windows(np.pad(label_img_expanded, 1,
-                                     mode='constant', constant_values=0),
+    windows = view_as_windows(np.pad(label_img_expanded, 1, mode='edge'),
                               (3,) * ndim)
 
     boundaries = np.zeros_like(edges)


Test patch:
diff --git a/skimage/segmentation/tests/test_boundaries.py b/skimage/segmentation/tests/test_boundaries.py
--- a/skimage/segmentation/tests/test_boundaries.py
+++ b/skimage/segmentation/tests/test_boundaries.py
@@ -1,9 +1,9 @@
 import numpy as np
-from skimage.segmentation import find_boundaries, mark_boundaries
+import pytest
+from numpy.testing import assert_array_equal, assert_allclose
 
-from skimage._shared import testing
-from skimage._shared.testing import assert_array_equal, assert_allclose
 from skimage._shared.utils import _supported_float_type
+from skimage.segmentation import find_boundaries, mark_boundaries
 
 
 white = (1, 1, 1)
@@ -41,7 +41,9 @@ def test_find_boundaries_bool():
     assert_array_equal(result, ref)
 
 
-@testing.parametrize('dtype', [np.uint8, np.float16, np.float32, np.float64])
+@pytest.mark.parametrize(
+    'dtype', [np.uint8, np.float16, np.float32, np.float64]
+)
 def test_mark_boundaries(dtype):
     image = np.zeros((10, 10), dtype=dtype)
     label_image = np.zeros((10, 10), dtype=np.uint8)
@@ -100,7 +102,7 @@ def test_mark_boundaries_bool():
     assert_array_equal(result, ref)
 
 
-@testing.parametrize('dtype', [np.float16, np.float32, np.float64])
+@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
 def test_mark_boundaries_subpixel(dtype):
     labels = np.array([[0, 0, 0, 0],
                        [0, 0, 5, 0],
@@ -125,3 +127,11 @@ def test_mark_boundaries_subpixel(dtype):
          [ 0.2 ,  0.52,  0.92,  1.  ,  1.  ,  1.  ,  0.54],
          [ 0.02,  0.35,  0.83,  0.9 ,  0.78,  0.81,  0.87]])
     assert_allclose(marked_proj, ref_result, atol=0.01)
+
+
+@pytest.mark.parametrize('mode', ['thick', 'inner', 'outer', 'subpixel'])
+def test_boundaries_constant_image(mode):
+    """A constant-valued image has not boundaries."""
+    ones = np.ones((8, 8), dtype=int)
+    b = find_boundaries(ones, mode=mode)
+    assert np.all(b == 0)