From b29ad8ef6d9086e8908c6dced1bf5784aa1b3697 Mon Sep 17 00:00:00 2001 From: Robin Wilson Date: Fri, 28 Aug 2015 22:30:42 +0100 Subject: [PATCH 1/6] Added quantile_threshold option to canny edge detection This allows you to specify the high and low thresholds as quantiles of the edge magnitude image, rather than as absolute edge magnitude values --- skimage/feature/_canny.py | 14 +++++++++++++- skimage/feature/tests/test_canny.py | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/skimage/feature/_canny.py b/skimage/feature/_canny.py index 73566d00..91598ce0 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, + quantile_threshold=False): """Edge filter an image using the Canny algorithm. Parameters @@ -67,6 +68,9 @@ 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. + quantile_threshold : bool, optional + If True then treat low_threshold and high_threshold as quantiles of the + edge magnitude image, rather than absolute edge magnitude values. Returns ------- @@ -246,6 +250,14 @@ 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 quantile_threshold is set then calculate the thresholds to use + # + if quantile_threshold: + 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..0bfb1fcf 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,27 @@ 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_quantile_threshold(): + 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, quantile_threshold=True) + + assert_equal(result, correct_output) + + From 5f0665c99b577888084d93103a5e211da5dfc964 Mon Sep 17 00:00:00 2001 From: Robin Wilson Date: Sat, 29 Aug 2015 13:46:56 +0100 Subject: [PATCH 2/6] Raise ValueError if quantile thresholds outside of [0, 1] - and updated docs --- skimage/feature/_canny.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/skimage/feature/_canny.py b/skimage/feature/_canny.py index 91598ce0..6073c102 100644 --- a/skimage/feature/_canny.py +++ b/skimage/feature/_canny.py @@ -70,7 +70,8 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None, Mask to limit the application of Canny to a certain area. quantile_threshold : bool, optional If True then treat low_threshold and high_threshold as quantiles of the - edge magnitude image, rather than absolute edge magnitude values. + edge magnitude image, rather than absolute edge magnitude values. If True + then the thresholds must be in the range [0, 1]. Returns ------- @@ -255,6 +256,11 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None, #---- If quantile_threshold is set then calculate the thresholds to use # if quantile_threshold: + 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) From f9887e61e5315af78981a4a1e23c5fc9e3497333 Mon Sep 17 00:00:00 2001 From: Robin Wilson Date: Sat, 29 Aug 2015 14:14:13 +0100 Subject: [PATCH 3/6] Added test for invalid quantile inputs --- skimage/feature/tests/test_canny.py | 51 +++++++++++++++++++---------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/skimage/feature/tests/test_canny.py b/skimage/feature/tests/test_canny.py index 0bfb1fcf..17a147f3 100644 --- a/skimage/feature/tests/test_canny.py +++ b/skimage/feature/tests/test_canny.py @@ -70,26 +70,43 @@ class TestCanny(unittest.TestCase): result2 = F.canny(np.zeros((20, 20)), 4, 0, 0) self.assertTrue(np.all(result1 == result2)) + def test_quantile_threshold(self): + image = img_as_float(data.camera()[::50,::50]) -def test_quantile_threshold(): - 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) - # 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, quantile_threshold=True) - result = F.canny(image, low_threshold=0.6, high_threshold=0.8, quantile_threshold=True) + assert_equal(result, correct_output) - assert_equal(result, correct_output) + def test_invalid_quantile_threshold(self): + image = img_as_float(data.camera()[::50,::50]) + with self.assertRaises(ValueError): + result = F.canny(image, quantile_threshold=True, + low_threshold=0.5, high_threshold=3.6) + + with self.assertRaises(ValueError): + result = F.canny(image, quantile_threshold=True, + low_threshold=99, high_threshold=0.9) + + with self.assertRaises(ValueError): + result = F.canny(image, quantile_threshold=True, + low_threshold=-5, high_threshold=0.5) + + with self.assertRaises(ValueError): + result = F.canny(image, quantile_threshold=True, + low_threshold=0.5, high_threshold=-100) From 9f878affbc57205b4f75ed732aeacf41f665732b Mon Sep 17 00:00:00 2001 From: Robin Wilson Date: Sat, 29 Aug 2015 16:25:42 +0100 Subject: [PATCH 4/6] Renamed keyword arg to use_quantiles --- skimage/feature/_canny.py | 8 ++++---- skimage/feature/tests/test_canny.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/skimage/feature/_canny.py b/skimage/feature/_canny.py index 6073c102..43695645 100644 --- a/skimage/feature/_canny.py +++ b/skimage/feature/_canny.py @@ -51,7 +51,7 @@ def smooth_with_function_and_mask(image, function, mask): def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None, - quantile_threshold=False): + use_quantiles=False): """Edge filter an image using the Canny algorithm. Parameters @@ -68,7 +68,7 @@ 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. - quantile_threshold : bool, optional + 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]. @@ -253,9 +253,9 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None, local_maxima[pts] = c_plus & c_minus # - #---- If quantile_threshold is set then calculate the thresholds to use + #---- If use_quantiles is set then calculate the thresholds to use # - if quantile_threshold: + 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: diff --git a/skimage/feature/tests/test_canny.py b/skimage/feature/tests/test_canny.py index 17a147f3..21ffc52f 100644 --- a/skimage/feature/tests/test_canny.py +++ b/skimage/feature/tests/test_canny.py @@ -70,7 +70,7 @@ class TestCanny(unittest.TestCase): result2 = F.canny(np.zeros((20, 20)), 4, 0, 0) self.assertTrue(np.all(result1 == result2)) - def test_quantile_threshold(self): + def test_use_quantiles(self): image = img_as_float(data.camera()[::50,::50]) # Correct output produced manually with quantiles @@ -87,26 +87,26 @@ class TestCanny(unittest.TestCase): [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, quantile_threshold=True) + result = F.canny(image, low_threshold=0.6, high_threshold=0.8, use_quantiles=True) assert_equal(result, correct_output) - def test_invalid_quantile_threshold(self): + def test_invalid_use_quantiles(self): image = img_as_float(data.camera()[::50,::50]) with self.assertRaises(ValueError): - result = F.canny(image, quantile_threshold=True, + result = F.canny(image, use_quantiles=True, low_threshold=0.5, high_threshold=3.6) with self.assertRaises(ValueError): - result = F.canny(image, quantile_threshold=True, + result = F.canny(image, use_quantiles=True, low_threshold=99, high_threshold=0.9) with self.assertRaises(ValueError): - result = F.canny(image, quantile_threshold=True, + result = F.canny(image, use_quantiles=True, low_threshold=-5, high_threshold=0.5) with self.assertRaises(ValueError): - result = F.canny(image, quantile_threshold=True, + result = F.canny(image, use_quantiles=True, low_threshold=0.5, high_threshold=-100) From 98bf48824134eeaf8391d0d6a8c02c0c00bbd712 Mon Sep 17 00:00:00 2001 From: Robin Wilson Date: Sat, 29 Aug 2015 23:26:22 +0100 Subject: [PATCH 5/6] Fixed indentation --- skimage/feature/_canny.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/_canny.py b/skimage/feature/_canny.py index 43695645..da18508d 100644 --- a/skimage/feature/_canny.py +++ b/skimage/feature/_canny.py @@ -51,7 +51,7 @@ def smooth_with_function_and_mask(image, function, mask): def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None, - use_quantiles=False): + use_quantiles=False): """Edge filter an image using the Canny algorithm. Parameters From 5cf7a2013d73f3fb4d73315b389df65a1e5d0667 Mon Sep 17 00:00:00 2001 From: Robin Wilson Date: Sun, 30 Aug 2015 22:49:24 +0100 Subject: [PATCH 6/6] Fixed tests to use assertRaises as a function not a context manager --- skimage/feature/tests/test_canny.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/skimage/feature/tests/test_canny.py b/skimage/feature/tests/test_canny.py index 21ffc52f..bb0d7825 100644 --- a/skimage/feature/tests/test_canny.py +++ b/skimage/feature/tests/test_canny.py @@ -94,19 +94,14 @@ class TestCanny(unittest.TestCase): def test_invalid_use_quantiles(self): image = img_as_float(data.camera()[::50,::50]) - with self.assertRaises(ValueError): - result = 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=0.5, high_threshold=3.6) - with self.assertRaises(ValueError): - result = F.canny(image, use_quantiles=True, - low_threshold=99, high_threshold=0.9) + self.assertRaises(ValueError, F.canny, image, use_quantiles=True, + low_threshold=-5, high_threshold=0.5) - with self.assertRaises(ValueError): - result = F.canny(image, use_quantiles=True, - low_threshold=-5, high_threshold=0.5) - - with self.assertRaises(ValueError): - result = F.canny(image, use_quantiles=True, - low_threshold=0.5, high_threshold=-100) + 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)