From 159c9f4e9e0dcee039c002266ebc49cb765f5c22 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 24 Oct 2015 23:34:00 -0500 Subject: [PATCH 1/2] TST/STY: Hough tf regression test & Cython wrappers --- skimage/transform/__init__.py | 6 +- skimage/transform/_hough_transform.pyx | 12 -- skimage/transform/hough_transform.py | 161 +++++++++++++++++- .../transform/tests/test_hough_transform.py | 16 ++ 4 files changed, 177 insertions(+), 18 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 9b40b3a9..3df5863d 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -1,6 +1,6 @@ -from ._hough_transform import (hough_ellipse, hough_line, - probabilistic_hough_line) -from .hough_transform import hough_circle, hough_line_peaks +from .hough_transform import (hough_line, hough_line_peaks, + probabilistic_hough_line, hough_circle, + hough_ellipse) from .radon_transform import radon, iradon, iradon_sart from .finite_radon_transform import frt2, ifrt2 from .integral import integral_image, integrate diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 6ce69d8e..3e4ba254 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -278,16 +278,10 @@ def hough_line(cnp.ndarray img, .. plot:: hough_tf.py """ - if img.ndim != 2: - raise ValueError('The input image must be 2D.') - # Compute the array of angles and their sine and cosine cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] ctheta cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] stheta - if theta is None: - theta = np.linspace(NEG_PI_2, PI_2, 180) - ctheta = np.cos(theta) stheta = np.sin(theta) @@ -354,12 +348,6 @@ def probabilistic_hough_line(cnp.ndarray img, int threshold=10, Hough transform for line detection", in IEEE Computer Society Conference on Computer Vision and Pattern Recognition, 1999. """ - if img.ndim != 2: - raise ValueError('The input image must be 2D.') - - if theta is None: - theta = PI_2 - np.arange(180) / 180.0 * 2 * PI_2 - cdef Py_ssize_t height = img.shape[0] cdef Py_ssize_t width = img.shape[1] diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 92f2c79a..72fcf61e 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -1,7 +1,67 @@ import numpy as np from scipy import ndimage as ndi -from .. import measure, morphology -from ._hough_transform import _hough_circle +from .. import measure +from ._hough_transform import (_hough_circle, + hough_ellipse as _hough_ellipse, + hough_line as _hough_line, + probabilistic_hough_line as _prob_hough_line) + + +# Wrapper for Cython allows function signature introspection +def hough_line(img, theta=None): + """Perform a straight line Hough transform. + + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + theta : 1D ndarray of double + Angles at which to compute the transform, in radians. + Defaults to -pi/2 .. pi/2 + + Returns + ------- + H : 2-D ndarray of uint64 + Hough transform accumulator. + theta : ndarray + Angles at which the transform was computed, in radians. + distances : ndarray + Distance values. + + Notes + ----- + The origin is the top left corner of the original image. + X and Y axis are horizontal and vertical edges respectively. + The distance is the minimal algebraic distance from the origin + to the detected line. + + Examples + -------- + Generate a test image: + + >>> img = np.zeros((100, 150), dtype=bool) + >>> img[30, :] = 1 + >>> img[:, 65] = 1 + >>> img[35:45, 35:50] = 1 + >>> for i in range(90): + ... img[i, i] = 1 + >>> img += np.random.random(img.shape) > 0.95 + + Apply the Hough transform: + + >>> out, angles, d = hough_line(img) + + .. plot:: hough_tf.py + + """ + if img.ndim != 2: + raise ValueError('The input image `img` must be 2D.') + + if theta is None: + # These values are approximations of pi/2 + theta = np.linspace(-1.5707963267948966, 1.5707963267948966, 180) + + return _hough_line(img, theta=theta) def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, @@ -73,7 +133,10 @@ def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, label_hspace = measure.label(hspace_t) props = measure.regionprops(label_hspace, hspace_max) - props = sorted(props, key= lambda x: x.max_intensity)[::-1] + + # Sort the list of peaks by intensity, not left-right, so larger peaks + # in Hough space cannot be arbitrarily suppressed by smaller neighbors + props = sorted(props, key=lambda x: x.max_intensity)[::-1] coords = np.array([np.round(p.centroid) for p in props], dtype=int) hspace_peaks = [] @@ -126,6 +189,48 @@ def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, return hspace_peaks, angle_peaks, dist_peaks +# Wrapper for Cython allows function signature introspection +def probabilistic_hough_line(img, threshold=10, line_length=50, line_gap=10, + theta=None): + """Return lines from a progressive probabilistic line Hough transform. + + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + threshold : int, optional (default 10) + Threshold + line_length : int, optional (default 50) + Minimum accepted length of detected lines. + Increase the parameter to extract longer lines. + line_gap : int, optional, (default 10) + Maximum gap between pixels to still form a line. + Increase the parameter to merge broken lines more aggresively. + theta : 1D ndarray, dtype=double, optional, default (-pi/2 .. pi/2) + Angles at which to compute the transform, in radians. + + Returns + ------- + lines : list + List of lines identified, lines in format ((x0, y0), (x1, y0)), + indicating line start and end. + + References + ---------- + .. [1] C. Galamhos, J. Matas and J. Kittler, "Progressive probabilistic + Hough transform for line detection", in IEEE Computer Society + Conference on Computer Vision and Pattern Recognition, 1999. + """ + if img.ndim != 2: + raise ValueError('The input image `img` must be 2D.') + + if theta is None: + theta = 1.5707963267948966 - np.arange(180) / 180.0 * np.pi + + return _prob_hough_line(img, threshold=threshold, line_length=line_length, + line_gap=line_gap, theta=theta) + + def hough_circle(image, radius, normalize=True, full_output=False): """Perform a circular Hough transform. @@ -169,3 +274,53 @@ def hough_circle(image, radius, normalize=True, full_output=False): radius = np.atleast_1d(np.asarray(radius)) return _hough_circle(image, radius.astype(np.intp), normalize=normalize, full_output=full_output) + + +# Wrapper for Cython allows function signature introspection +def hough_ellipse(img, threshold=4, accuracy=1, min_size=4, max_size=None): + """Perform an elliptical Hough transform. + + Parameters + ---------- + img : (M, N) ndarray + Input image with nonzero values representing edges. + threshold: int, optional (default 4) + Accumulator threshold value. + accuracy : double, optional (default 1) + Bin size on the minor axis used in the accumulator. + min_size : int, optional (default 4) + Minimal major axis length. + max_size : int, optional + Maximal minor axis length. (default None) + If None, the value is set to the half of the smaller + image dimension. + + Returns + ------- + result : ndarray with fields [(accumulator, y0, x0, a, b, orientation)] + Where ``(yc, xc)`` is the center, ``(a, b)`` the major and minor + axes, respectively. The `orientation` value follows + `skimage.draw.ellipse_perimeter` convention. + + Examples + -------- + >>> img = np.zeros((25, 25), dtype=np.uint8) + >>> rr, cc = ellipse_perimeter(10, 10, 6, 8) + >>> img[cc, rr] = 1 + >>> result = hough_ellipse(img, threshold=8) + [(10, 10.0, 8.0, 6.0, 0.0, 10.0)] + + Notes + ----- + The accuracy must be chosen to produce a peak in the accumulator + distribution. In other words, a flat accumulator distribution with low + values may be caused by a too low bin size. + + References + ---------- + .. [1] Xie, Yonghong, and Qiang Ji. "A new efficient ellipse detection + method." Pattern Recognition, 2002. Proceedings. 16th International + Conference on. Vol. 2. IEEE, 2002 + """ + return _hough_ellipse(img, threshold=threshold, accuracy=accuracy, + min_size=min_size, max_size=max_size) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index fce7013c..4877b38b 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -78,6 +78,22 @@ def test_hough_line_peaks(): assert_almost_equal(theta[0], 1.41, 1) +def test_hough_line_peaks_ordered(): + # Regression test per PR #1421 + testim = np.zeros((256, 64), dtype=np.bool) + + testim[50:100, 20] = True + testim[85:200, 25] = True + testim[15:35, 50] = True + testim[1:-1, 58] = True + + hough_space, angles, dists = tf.hough_line(testim) + + with expected_warnings(['`background`']): + hspace, _, _ = tf.hough_line_peaks(hough_space, angles, dists) + assert hspace[0] > hspace[1] + + def test_hough_line_peaks_dist(): img = np.zeros((100, 100), dtype=np.bool_) img[:, 30] = True From a3356194c8fccf2dabe37bb593964ac328d9078f Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 24 Oct 2015 23:58:29 -0500 Subject: [PATCH 2/2] TSTFIX: Fix imports in hough_ellipse doctest --- skimage/transform/_hough_transform.pyx | 3 -- skimage/transform/hough_transform.py | 9 ++++-- .../transform/tests/test_hough_transform.py | 32 +++++++++++++------ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 3e4ba254..a189d7b9 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -12,9 +12,6 @@ from libc.stdlib cimport rand from ..draw import circle_perimeter -cdef double PI_2 = 1.5707963267948966 -cdef double NEG_PI_2 = -PI_2 - from .._shared.interpolation cimport round diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index 72fcf61e..f9a8d2e5 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -59,7 +59,7 @@ def hough_line(img, theta=None): if theta is None: # These values are approximations of pi/2 - theta = np.linspace(-1.5707963267948966, 1.5707963267948966, 180) + theta = np.linspace(-np.pi / 2, np.pi / 2, 180) return _hough_line(img, theta=theta) @@ -225,7 +225,7 @@ def probabilistic_hough_line(img, threshold=10, line_length=50, line_gap=10, raise ValueError('The input image `img` must be 2D.') if theta is None: - theta = 1.5707963267948966 - np.arange(180) / 180.0 * np.pi + theta = np.pi / 2 - np.arange(180) / 180.0 * np.pi return _prob_hough_line(img, threshold=threshold, line_length=line_length, line_gap=line_gap, theta=theta) @@ -304,11 +304,14 @@ def hough_ellipse(img, threshold=4, accuracy=1, min_size=4, max_size=None): Examples -------- + >>> from skimage.transform import hough_ellipse + >>> from skimage.draw import ellipse_perimeter >>> img = np.zeros((25, 25), dtype=np.uint8) >>> rr, cc = ellipse_perimeter(10, 10, 6, 8) >>> img[cc, rr] = 1 >>> result = hough_ellipse(img, threshold=8) - [(10, 10.0, 8.0, 6.0, 0.0, 10.0)] + >>> result.tolist() + [(10, 10.0, 10.0, 8.0, 6.0, 0.0)] Notes ----- diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 4877b38b..0babc769 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import assert_almost_equal, assert_equal +from numpy.testing import assert_almost_equal, assert_equal, assert_raises import skimage.transform as tf from skimage.draw import line, circle_perimeter, ellipse_perimeter @@ -7,15 +7,6 @@ from skimage._shared._warnings import expected_warnings from skimage._shared.testing import test_parallel -def append_desc(func, description): - """Append the test function ``func`` and append - ``description`` to its name. - """ - func.description = func.__module__ + '.' + func.__name__ + description - - return func - - @test_parallel() def test_hough_line(): # Generate a test image @@ -42,12 +33,21 @@ def test_hough_line_angles(): assert_equal(len(angles), 10) +def test_hough_line_bad_input(): + img = np.zeros(100) + img[10] = 1 + + # Expected error, img must be 2D + assert_raises(ValueError, tf.hough_line, img, np.linspace(0, 360, 10)) + + def test_probabilistic_hough(): # Generate a test image img = np.zeros((100, 100), dtype=int) for i in range(25, 75): img[100 - i, i] = 100 img[i, i] = 100 + # decrease default theta sampling because similar orientations may confuse # as mentioned in article of Galambos et al theta = np.linspace(0, np.pi, 45) @@ -59,9 +59,21 @@ def test_probabilistic_hough(): line = list(line) line.sort(key=lambda x: x[0]) sorted_lines.append(line) + assert([(25, 75), (74, 26)] in sorted_lines) assert([(25, 25), (74, 74)] in sorted_lines) + # Execute with default theta + tf.probabilistic_hough_line(img, line_length=10, line_gap=3) + + +def test_probabilistic_hough_bad_input(): + img = np.zeros(100) + img[10] = 1 + + # Expected error, img must be 2D + assert_raises(ValueError, tf.probabilistic_hough_line, img) + def test_hough_line_peaks(): img = np.zeros((100, 150), dtype=int)