mirror of
https://github.com/wassname/scikit-image.git
synced 2026-08-10 12:40:08 +08:00
Merge pull request #29 from amueller/canny_usability
ENH: Add sensible defaults for Canny. Code cleanups.
This commit is contained in:
@@ -14,7 +14,7 @@ Original author: Lee Kamentsky
|
||||
|
||||
import numpy as np
|
||||
import scipy.ndimage as ndi
|
||||
from scipy.ndimage import (gaussian_filter, convolve,
|
||||
from scipy.ndimage import (gaussian_filter,
|
||||
generate_binary_structure, binary_erosion, label)
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ def smooth_with_function_and_mask(image, function, mask):
|
||||
----------
|
||||
image : array
|
||||
The image to smooth
|
||||
|
||||
|
||||
function : callable
|
||||
A function that takes an image and returns a smoothed image
|
||||
|
||||
|
||||
mask : array
|
||||
Mask with 1's for significant pixels, 0 for masked pixels
|
||||
|
||||
@@ -37,11 +37,11 @@ def smooth_with_function_and_mask(image, function, mask):
|
||||
This function calculates 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). We then mask the image
|
||||
and apply the function. The resulting values will be lower by the bleed-over
|
||||
fraction, so you can recalibrate by dividing by the function on the mask
|
||||
to recover the effect of smoothing from just the significant pixels.
|
||||
and apply the function. The resulting values will be lower by the
|
||||
bleed-over fraction, so you can recalibrate by dividing by the function
|
||||
on the mask to recover the effect of smoothing from just the significant
|
||||
pixels.
|
||||
"""
|
||||
not_mask = np.logical_not(mask)
|
||||
bleed_over = function(mask.astype(float))
|
||||
masked_image = np.zeros(image.shape, image.dtype)
|
||||
masked_image[mask] = image[mask]
|
||||
@@ -50,7 +50,7 @@ def smooth_with_function_and_mask(image, function, mask):
|
||||
return output_image
|
||||
|
||||
|
||||
def canny(image, sigma, low_threshold, high_threshold, mask=None):
|
||||
def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None):
|
||||
'''Edge filter an image using the Canny algorithm.
|
||||
|
||||
Parameters
|
||||
@@ -58,10 +58,10 @@ def canny(image, sigma, low_threshold, high_threshold, mask=None):
|
||||
image : array_like, dtype=float
|
||||
The greyscale input image to detect edges on; should be normalized to 0.0
|
||||
to 1.0.
|
||||
|
||||
|
||||
sigma : float
|
||||
The standard deviation of the Gaussian filter
|
||||
|
||||
|
||||
low_threshold : float
|
||||
The lower bound for hysterisis thresholding (linking edges)
|
||||
|
||||
@@ -80,10 +80,11 @@ def canny(image, sigma, low_threshold, high_threshold, mask=None):
|
||||
-----------
|
||||
Canny, J., A Computational Approach To Edge Detection, IEEE Trans.
|
||||
Pattern Analysis and Machine Intelligence, 8:679-714, 1986
|
||||
|
||||
William Green's Canny tutorial
|
||||
http://www.pages.drexel.edu/~weg22/can_tut.html
|
||||
|
||||
William Green' Canny tutorial
|
||||
http://dasl.mem.drexel.edu/alumni/bGreen/www.pages.drexel.edu/_weg22/can_tut.html
|
||||
'''
|
||||
|
||||
#
|
||||
# The steps involved:
|
||||
#
|
||||
@@ -113,6 +114,10 @@ def canny(image, sigma, low_threshold, high_threshold, mask=None):
|
||||
# mask by one and then mask the output. We also mask out the border points
|
||||
# because who knows what lies beyond the edge of the image?
|
||||
#
|
||||
|
||||
if image.ndim != 2:
|
||||
raise TypeError("The input 'image' must be a two dimensional array.")
|
||||
|
||||
if mask is None:
|
||||
mask = np.ones(image.shape, dtype=bool)
|
||||
fsmooth = lambda x: gaussian_filter(x, sigma, mode='constant')
|
||||
@@ -136,7 +141,7 @@ def canny(image, sigma, low_threshold, high_threshold, mask=None):
|
||||
# Assign each point to have a normal of 0-45 degrees, 45-90 degrees,
|
||||
# 90-135 degrees and 135-180 degrees.
|
||||
#
|
||||
local_maxima = np.zeros(image.shape,bool)
|
||||
local_maxima = np.zeros(image.shape, bool)
|
||||
#----- 0 to 45 degrees ------
|
||||
pts_plus = (isobel >= 0) & (jsobel >= 0) & (abs_isobel >= abs_jsobel)
|
||||
pts_minus = (isobel <= 0) & (jsobel <= 0) & (abs_isobel >= abs_jsobel)
|
||||
@@ -185,7 +190,6 @@ def canny(image, sigma, low_threshold, high_threshold, mask=None):
|
||||
c1 = magnitude[:, :-1][pts[:, 1:]]
|
||||
c2 = magnitude[1:, :-1][pts[:-1, 1:]]
|
||||
c_minus = c2 * w + c1 * (1.0 - w) <= m
|
||||
cc = np.logical_and(c_plus,c_minus)
|
||||
local_maxima[pts] = c_plus & c_minus
|
||||
#----- 135 to 180 degrees ------
|
||||
# Mix anti-diagonal and anti-horizontal
|
||||
@@ -200,7 +204,7 @@ def canny(image, sigma, low_threshold, high_threshold, mask=None):
|
||||
w = abs_jsobel[pts] / abs_isobel[pts]
|
||||
c_plus = c2 * w + c1 * (1 - w) <= m
|
||||
c1 = magnitude[1:, :][pts[:-1, :]]
|
||||
c2 = magnitude[1:,:-1][pts[:-1,1:]]
|
||||
c2 = magnitude[1:, :-1][pts[:-1, 1:]]
|
||||
c_minus = c2 * w + c1 * (1 - w) <= m
|
||||
local_maxima[pts] = c_plus & c_minus
|
||||
#
|
||||
@@ -212,14 +216,14 @@ def canny(image, sigma, low_threshold, high_threshold, mask=None):
|
||||
# Segment the low-mask, then only keep low-segments that have
|
||||
# some high_mask component in them
|
||||
#
|
||||
labels,count = label(low_mask, np.ndarray((3, 3),bool))
|
||||
labels, count = label(low_mask, np.ndarray((3, 3), bool))
|
||||
if count == 0:
|
||||
return low_mask
|
||||
|
||||
sums = (np.array(ndi.sum(high_mask,labels,
|
||||
np.arange(count,dtype=np.int32) + 1),
|
||||
|
||||
sums = (np.array(ndi.sum(high_mask, labels,
|
||||
np.arange(count, dtype=np.int32) + 1),
|
||||
copy=False, ndmin=1))
|
||||
good_label = np.zeros((count + 1,),bool)
|
||||
good_label = np.zeros((count + 1,), bool)
|
||||
good_label[1:] = sums > 0
|
||||
output_mask = good_label[labels]
|
||||
return output_mask
|
||||
|
||||
@@ -3,56 +3,61 @@ import numpy as np
|
||||
from scipy.ndimage import binary_dilation, binary_erosion
|
||||
import scikits.image.filter as F
|
||||
|
||||
|
||||
class TestCanny(unittest.TestCase):
|
||||
def test_00_00_zeros(self):
|
||||
'''Test that the Canny filter finds no points for a blank field'''
|
||||
result = F.canny(np.zeros((20,20)), 4, 0, 0, np.ones((20,20),bool))
|
||||
result = F.canny(np.zeros((20, 20)), 4, 0, 0, np.ones((20, 20), bool))
|
||||
self.assertFalse(np.any(result))
|
||||
|
||||
|
||||
def test_00_01_zeros_mask(self):
|
||||
'''Test that the Canny filter finds no points in a masked image'''
|
||||
result = (F.canny(np.random.uniform(size=(20,20)), 4,0,0,
|
||||
np.zeros((20,20),bool)))
|
||||
result = (F.canny(np.random.uniform(size=(20, 20)), 4, 0, 0,
|
||||
np.zeros((20, 20), bool)))
|
||||
self.assertFalse(np.any(result))
|
||||
|
||||
|
||||
def test_01_01_circle(self):
|
||||
'''Test that the Canny filter finds the outlines of a circle'''
|
||||
i,j = np.mgrid[-200:200,-200:200].astype(float) / 200
|
||||
c = np.abs(np.sqrt(i*i+j*j) - .5) < .02
|
||||
result = F.canny(c.astype(float), 4, 0, 0,np.ones(c.shape,bool))
|
||||
i, j = np.mgrid[-200:200, -200:200].astype(float) / 200
|
||||
c = np.abs(np.sqrt(i * i + j * j) - .5) < .02
|
||||
result = F.canny(c.astype(float), 4, 0, 0, np.ones(c.shape, bool))
|
||||
#
|
||||
# erode and dilate the circle to get rings that should contain the
|
||||
# outlines
|
||||
#
|
||||
cd = binary_dilation(c, iterations=3)
|
||||
ce = binary_erosion(c,iterations=3)
|
||||
ce = binary_erosion(c, iterations=3)
|
||||
cde = np.logical_and(cd, np.logical_not(ce))
|
||||
self.assertTrue(np.all(cde[result]))
|
||||
#
|
||||
# The circle has a radius of 100. There are two rings here, one
|
||||
# for the inside edge and one for the outside. So that's 100 * 2 * 2 * 3
|
||||
# for those places where pi is still 3. The edge contains both pixels
|
||||
# if there's a tie, so we bump the count a little.
|
||||
#
|
||||
# for the inside edge and one for the outside. So that's
|
||||
# 100 * 2 * 2 * 3 for those places where pi is still 3.
|
||||
# The edge contains both pixels if there's a tie, so we
|
||||
# bump the count a little.
|
||||
point_count = np.sum(result)
|
||||
self.assertTrue(point_count > 1200)
|
||||
self.assertTrue(point_count < 1600)
|
||||
|
||||
|
||||
def test_01_02_circle_with_noise(self):
|
||||
'''Test that the Canny filter finds the circle outlines in a noisy image'''
|
||||
'''Test that the Canny filter finds the circle outlines
|
||||
in a noisy image'''
|
||||
np.random.seed(0)
|
||||
i,j = np.mgrid[-200:200,-200:200].astype(float) / 200
|
||||
c = np.abs(np.sqrt(i*i+j*j) - .5) < .02
|
||||
cf = c.astype(float) * .5 + np.random.uniform(size=c.shape)*.5
|
||||
result = F.canny(cf, 4, .1, .2,np.ones(c.shape,bool))
|
||||
i, j = np.mgrid[-200:200, -200:200].astype(float) / 200
|
||||
c = np.abs(np.sqrt(i * i + j * j) - .5) < .02
|
||||
cf = c.astype(float) * .5 + np.random.uniform(size=c.shape) * .5
|
||||
result = F.canny(cf, 4, .1, .2, np.ones(c.shape, bool))
|
||||
#
|
||||
# erode and dilate the circle to get rings that should contain the
|
||||
# outlines
|
||||
#
|
||||
cd = binary_dilation(c, iterations=4)
|
||||
ce = binary_erosion(c,iterations=4)
|
||||
ce = binary_erosion(c, iterations=4)
|
||||
cde = np.logical_and(cd, np.logical_not(ce))
|
||||
self.assertTrue(np.all(cde[result]))
|
||||
point_count = np.sum(result)
|
||||
self.assertTrue(point_count > 1200)
|
||||
self.assertTrue(point_count < 1600)
|
||||
|
||||
def test_image_shape(self):
|
||||
self.assertRaises(TypeError, F.canny, np.zeros((20, 20, 20)), 4, 0, 0)
|
||||
|
||||
Reference in New Issue
Block a user