diff --git a/skimage/feature/_canny.py b/skimage/feature/_canny.py index 73566d00..da18508d 100644 --- a/skimage/feature/_canny.py +++ b/skimage/feature/_canny.py @@ -50,7 +50,8 @@ def smooth_with_function_and_mask(image, function, mask): return output_image -def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): +def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None, + use_quantiles=False): """Edge filter an image using the Canny algorithm. Parameters @@ -67,6 +68,10 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): If None, high_threshold is set to 20% of dtype's max. mask : array, dtype=bool, optional Mask to limit the application of Canny to a certain area. + use_quantiles : bool, optional + If True then treat low_threshold and high_threshold as quantiles of the + edge magnitude image, rather than absolute edge magnitude values. If True + then the thresholds must be in the range [0, 1]. Returns ------- @@ -246,6 +251,19 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): c2 = magnitude[1:, :-1][pts[:-1, 1:]] c_minus = c2 * w + c1 * (1 - w) <= m local_maxima[pts] = c_plus & c_minus + + # + #---- If use_quantiles is set then calculate the thresholds to use + # + if use_quantiles: + if high_threshold > 1.0 or low_threshold > 1.0: + raise ValueError("Quantile thresholds must not be > 1.0") + if high_threshold < 0.0 or low_threshold < 0.0: + raise ValueError("Quantile thresholds must not be < 0.0") + + high_threshold = np.percentile(magnitude, 100.0 * high_threshold) + low_threshold = np.percentile(magnitude, 100.0 * low_threshold) + # #---- Create two masks at the two thresholds. # diff --git a/skimage/feature/tests/test_canny.py b/skimage/feature/tests/test_canny.py index 400a7f70..bb0d7825 100644 --- a/skimage/feature/tests/test_canny.py +++ b/skimage/feature/tests/test_canny.py @@ -1,7 +1,10 @@ import unittest import numpy as np +from numpy.testing import assert_equal from scipy.ndimage import binary_dilation, binary_erosion import skimage.feature as F +from skimage import filters, data +from skimage import img_as_float class TestCanny(unittest.TestCase): @@ -66,3 +69,39 @@ class TestCanny(unittest.TestCase): result1 = F.canny(np.zeros((20, 20)), 4, 0, 0, np.ones((20, 20), bool)) result2 = F.canny(np.zeros((20, 20)), 4, 0, 0) self.assertTrue(np.all(result1 == result2)) + + def test_use_quantiles(self): + image = img_as_float(data.camera()[::50,::50]) + + # Correct output produced manually with quantiles + # of 0.8 and 0.6 for high and low respectively + correct_output = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=bool) + + result = F.canny(image, low_threshold=0.6, high_threshold=0.8, use_quantiles=True) + + assert_equal(result, correct_output) + + def test_invalid_use_quantiles(self): + image = img_as_float(data.camera()[::50,::50]) + + self.assertRaises(ValueError, F.canny, image, use_quantiles=True, + low_threshold=0.5, high_threshold=3.6) + + self.assertRaises(ValueError, F.canny, image, use_quantiles=True, + low_threshold=-5, high_threshold=0.5) + + self.assertRaises(ValueError, F.canny, image, use_quantiles=True, + low_threshold=99, high_threshold=0.9) + + self.assertRaises(ValueError, F.canny, image, use_quantiles=True, + low_threshold=0.5, high_threshold=-100)