Repository: scikit-image/scikit-image

Issue:
Canny outputs border artifacts when image is constant with default parameters
I believe the change occurred in 372b901065d5712237105fb1a70c5c6cde530a15

The resulting behavior is that the canny edge detector now returns edge pixels on the border of any image that does not have a black background.

This is a change in behavior from previous versions of skimage.


Discussion:
```python
import numpy as np
import skimage as ski

x = np.ones((10, 10))
print(ski.feature.canny(x))
```

```
[[False False False False False False False False False False]
 [False  True  True  True  True  True  True  True  True False]
 [False  True False False False False False False  True False]
 [False  True False False False False False False  True False]
 [False  True False False False False False False  True False]
 [False  True False False False False False False  True False]
 [False  True False False False False False False  True False]
 [False  True False False False False False False  True False]
 [False  True  True  True  True  True  True  True  True False]
 [False False False False False False False False False False]]
```

Desired behavior is achieved by setting `mode='reflect'`.
I think this should just be `mode='nearest'`.  Definitely agree that constant does not seem desirable for this function.
> I believe the change occurred in [372b901](https://github.com/scikit-image/scikit-image/commit/372b901065d5712237105fb1a70c5c6cde530a15)

For clarification, `constant` was already the default mode before this commit. This commit simply exposes `mode` and `cval` in `canny` API. `constant` mode was kept for backward compatibility.
@stefanv, is this a behaviour change or a bug? 
I know the behavior changed, because a tutorial I had written the year before suddenly started malfunctioning, detecting edges on the border.
> I believe the change occurred in https://github.com/scikit-image/scikit-image/commit/372b901065d5712237105fb1a70c5c6cde530a15

@stefanv, you spotted the right commit. I will investigate :wink: 
More precisely, here is the exact commit https://github.com/scikit-image/scikit-image/pull/4342/commits/4c60efe2d3932c758a4f86e3247b0ae5bd3c2a2a introducing the change!
I will fix this and add a UT :wink:

Resolving patch:
diff --git a/skimage/feature/_canny.py b/skimage/feature/_canny.py
--- a/skimage/feature/_canny.py
+++ b/skimage/feature/_canny.py
@@ -66,24 +66,30 @@ def _preprocess(image, mask, sigma, mode, cval):
     gaussian_kwargs = dict(sigma=sigma, mode=mode, cval=cval,
                            preserve_range=False)
     if mask is None:
-        # Smooth the masked image
-        smoothed_image = gaussian(image, **gaussian_kwargs)
+        mask = np.ones(image.shape)
+        masked_image = image.copy()
+
         eroded_mask = np.ones(image.shape, dtype=bool)
         eroded_mask[:1, :] = 0
         eroded_mask[-1:, :] = 0
         eroded_mask[:, :1] = 0
         eroded_mask[:, -1:] = 0
-        return smoothed_image, eroded_mask
 
-    masked_image = np.zeros_like(image)
-    masked_image[mask] = image[mask]
+    else:
+        mask = mask.astype(bool, copy=False)
+        masked_image = np.zeros_like(image)
+        masked_image[mask] = image[mask]
+
+        # Make the eroded mask. Setting the border value to zero will wipe
+        # out the image edges for us.
+        s = ndi.generate_binary_structure(2, 2)
+        eroded_mask = ndi.binary_erosion(mask, s, border_value=0)
 
     # Compute the fractional contribution of masked pixels by applying
     # the function to the mask (which gets you the fraction of the
     # pixel data that's due to significant points)
-    bleed_over = (
-        gaussian(mask.astype(float), **gaussian_kwargs) + np.finfo(float).eps
-    )
+    bleed_over = gaussian(mask.astype(float, copy=False),
+                          **gaussian_kwargs) + np.finfo(float).eps
 
     # Smooth the masked image
     smoothed_image = gaussian(masked_image, **gaussian_kwargs)
@@ -93,11 +99,6 @@ def _preprocess(image, mask, sigma, mode, cval):
     # the effect of smoothing from just the significant pixels.
     smoothed_image /= bleed_over
 
-    # Make the eroded mask. Setting the border value to zero will wipe
-    # out the image edges for us.
-    s = ndi.generate_binary_structure(2, 2)
-    eroded_mask = ndi.binary_erosion(mask, s, border_value=0)
-
     return smoothed_image, eroded_mask
 
 


Test patch:
diff --git a/skimage/feature/tests/test_canny.py b/skimage/feature/tests/test_canny.py
--- a/skimage/feature/tests/test_canny.py
+++ b/skimage/feature/tests/test_canny.py
@@ -86,10 +86,14 @@ def test_use_quantiles(self):
              [False, False, False, False, False, False]])
 
         result = feature.canny(image, low_threshold=0.6, high_threshold=0.8,
-                               use_quantiles=True, mode='nearest')
+                               use_quantiles=True)
 
         assert_equal(result, correct_output)
 
+    def test_img_all_ones(self):
+        image = np.ones((10, 10))
+        assert np.all(feature.canny(image) == 0)
+
     def test_invalid_use_quantiles(self):
         image = img_as_float(data.camera()[::50, ::50])