diff --git a/doc/source/plots/hough_tf.py b/doc/source/plots/hough_tf.py index ea2fc4bb..df1dcd31 100644 --- a/doc/source/plots/hough_tf.py +++ b/doc/source/plots/hough_tf.py @@ -2,13 +2,14 @@ import numpy as np import matplotlib.pyplot as plt from skimage.transform import hough_line +from skimage.draw import line 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 +rr, cc = line(60, 130, 80, 10) +img[rr, cc] = 1 img += np.random.random(img.shape) > 0.95 out, angles, d = hough_line(img) @@ -20,8 +21,8 @@ plt.title('Input image') plt.subplot(1, 2, 2) plt.imshow(out, cmap=plt.cm.bone, - extent=(np.rad2deg(angles[0]), np.rad2deg(angles[-1]), - d[0], d[-1])) + extent=(np.rad2deg(angles[-1]), np.rad2deg(angles[0]), + d[-1], d[0])) plt.title('Hough transform') plt.xlabel('Angle (degree)') plt.ylabel('Distance (pixel)') diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 089487ee..92858010 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -1,3 +1,6 @@ +from ._hough_transform import (hough_circle, + hough_line, + probabilistic_hough_line) from .hough_transform import * from .radon_transform import * from .finite_radon_transform import * diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 2b1acc7c..b2250ef3 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -20,9 +20,9 @@ cdef inline Py_ssize_t round(double r): return ((r + 0.5) if (r > 0.0) else (r - 0.5)) -def _hough_circle(cnp.ndarray img, - cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, - char normalize=True): +def hough_circle(cnp.ndarray img, + cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, + char normalize=True): """Perform a circular Hough transform. Parameters @@ -88,8 +88,52 @@ def _hough_circle(cnp.ndarray img, return acc -def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): +def hough_line(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] 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 must be 2D.') @@ -98,7 +142,7 @@ def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] stheta if theta is None: - theta = np.linspace(PI_2, NEG_PI_2, 180) + theta = np.linspace(NEG_PI_2, PI_2, 180) ctheta = np.cos(theta) stheta = np.sin(theta) @@ -120,7 +164,7 @@ def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): # finally, run the transform cdef Py_ssize_t nidxs, nthetas, i, j, x, y, accum_idx - nidxs = y_idxs.shape[0] # x and y are the same shape + nidxs = y_idxs.shape[0] # x and y are the same shape nthetas = theta.shape[0] for i in range(nidxs): x = x_idxs[i] @@ -131,10 +175,38 @@ def _hough(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): return accum, theta, bins -def _probabilistic_hough(cnp.ndarray img, int value_threshold, - int line_length, int line_gap, - cnp.ndarray[ndim=1, dtype=cnp.double_t] theta=None): +def probabilistic_hough_line(cnp.ndarray img, int threshold=10, + int line_length=50, int line_gap=10, + cnp.ndarray[ndim=1, dtype=cnp.double_t] 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 must be 2D.') @@ -196,7 +268,7 @@ def _probabilistic_hough(cnp.ndarray img, int value_threshold, continue value = 0 - max_value = value_threshold - 1 + max_value = threshold - 1 max_theta = -1 # apply hough transform on point @@ -207,7 +279,7 @@ def _probabilistic_hough(cnp.ndarray img, int value_threshold, if value > max_value: max_value = value max_theta = j - if max_value < value_threshold: + if max_value < threshold: continue # from the random point walk in opposite directions and find line diff --git a/skimage/transform/hough_transform.py b/skimage/transform/hough_transform.py index f112a025..a3c010cd 100644 --- a/skimage/transform/hough_transform.py +++ b/skimage/transform/hough_transform.py @@ -1,192 +1,49 @@ -__all__ = ['hough', 'hough_line', 'hough_circle', 'hough_peaks', 'probabilistic_hough'] - -from itertools import izip as zip +__all__ = ['hough_line_peaks'] import numpy as np from scipy import ndimage -from ._hough_transform import _probabilistic_hough from skimage import measure, morphology -def _hough(img, theta=None): - if img.ndim != 2: - raise ValueError('The input image must be 2-D') - - if theta is None: - theta = np.linspace(-np.pi / 2, np.pi / 2, 180) - - # compute the vertical bins (the distances) - d = np.ceil(np.hypot(*img.shape)) - nr_bins = 2 * d - bins = np.linspace(-d, d, nr_bins) - - # allocate the output image - out = np.zeros((nr_bins, len(theta)), dtype=np.uint64) - - # precompute the sin and cos of the angles - cos_theta = np.cos(theta) - sin_theta = np.sin(theta) - - # find the indices of the non-zero values in - # the input image - y, x = np.nonzero(img) - - # x and y can be large, so we can't just broadcast to 2D - # arrays as we may run out of memory. Instead we process - # one vertical slice at a time. - for i, (cT, sT) in enumerate(zip(cos_theta, sin_theta)): - - # compute the base distances - distances = x * cT + y * sT - - # round the distances to the nearest integer - # and shift them to a nonzero bin - shifted = np.round(distances) - bins[0] - - # cast the shifted values to ints to use as indices - indices = shifted.astype(np.int) - - # use bin count to accumulate the coefficients - bincount = np.bincount(indices) - - # finally assign the proper values to the out array - out[:len(bincount), i] = bincount - - return out, theta, bins - -_py_hough = _hough - -# try to import and use the faster Cython version if it exists -try: - from ._hough_transform import _hough -except ImportError: - pass - - -def probabilistic_hough(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 - 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. - """ - return _probabilistic_hough(img, threshold, line_length, line_gap, theta) - +from ._hough_transform import hough_line, probabilistic_hough_line from skimage._shared.utils import deprecated + @deprecated('hough_line') def hough(img, theta=None): return hough_line(img, theta) -from ._hough_transform import _hough_circle -def hough_line(img, theta=None): - """Perform a straight line Hough transform. +@deprecated('probabilistic_hough_line') +def probabilistic_hough(img, threshold=10, line_length=50, line_gap=10, + theta=None): + return probabilistic_hough_line(img, threshold=threshold, + line_length=line_length, line_gap=line_gap, + theta=theta) - 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. - distances : ndarray - Distance values. - - Notes - ----- - The origin is the top left corner of the original image. - The angle is counted clockwise from 9 o'clock. - The distance is the minimal algebraic distance from this origin to the 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 - - """ - return _hough(img, theta) - -def hough_circle(img, radius, normalize=True): - """Perform a circular Hough transform. - - Parameters - ---------- - img : (M, N) ndarray - Input image with nonzero values representing edges. - radius : ndarray - Radii at which to compute the Hough transform. - normalize : boolean, optional - Normalize the accumulator with the number - of pixels used to draw the radius - - Returns - ------- - H : 3D ndarray (radius index, (M, N) ndarray) - Hough transform accumulator for each radius - - """ - return _hough_circle(img, radius.astype(np.intp), normalize) +@deprecated('hough_line_peaks') def hough_peaks(hspace, angles, dists, min_distance=10, min_angle=10, threshold=None, num_peaks=np.inf): + return hough_line_peaks(hspace, angles, dists, min_distance, min_angle, + threshold, num_peaks) + + +def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, + threshold=None, num_peaks=np.inf): """Return peaks in hough transform. - Identifies most prominent lines separated by a certain angle and distance in - a hough transform. Non-maximum suppression with different sizes is applied - separately in the first (distances) and second (angles) dimension of the - hough space to identify peaks. + Identifies most prominent lines separated by a certain angle and distance + in a hough transform. Non-maximum suppression with different sizes is + applied separately in the first (distances) and second (angles) dimension + of the hough space to identify peaks. Parameters ---------- hspace : (N, M) array Hough space returned by the `hough_line` function. angles : (M,) array - Angles returned by the `hough_line` function. Assumed to be continuous + Angles returned by the `hough_line` function. Assumed to be continuous. (`angles[-1] - angles[0] == PI`). dists : (N, ) array Distances returned by the `hough_line` function. diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 00427332..256c88a8 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -2,9 +2,7 @@ import numpy as np from numpy.testing import * import skimage.transform as tf -import skimage.transform.hough_transform as ht -from skimage.transform import probabilistic_hough -from skimage.draw import circle_perimeter +from skimage.draw import circle_perimeter, line def append_desc(func, description): @@ -16,11 +14,11 @@ def append_desc(func, description): return func -def test_hough(): +def test_hough_line(): # Generate a test image - img = np.zeros((100, 100), dtype=int) - for i in range(25, 75): - img[100 - i, i] = 1 + img = np.zeros((100, 150), dtype=int) + rr, cc = line(60, 130, 80, 10) + img[rr, cc] = 1 out, angles, d = tf.hough_line(img) @@ -28,11 +26,11 @@ def test_hough(): dist = d[y[0]] theta = angles[x[0]] - assert_equal(dist > 70, dist < 72) - assert_equal(theta > 0.78, theta < 0.79) + assert_almost_equal(dist, 80.723, 1) + assert_almost_equal(theta, 1.41, 1) -def test_hough_angles(): +def test_hough_line_angles(): img = np.zeros((10, 10)) img[0, 0] = 1 @@ -41,15 +39,6 @@ def test_hough_angles(): assert_equal(len(angles), 10) -def test_py_hough(): - ht._hough, fast_hough = ht._py_hough, ht._hough - - yield append_desc(test_hough, '_python') - yield append_desc(test_hough_angles, '_python') - - tf._hough = fast_hough - - def test_probabilistic_hough(): # Generate a test image img = np.zeros((100, 100), dtype=int) @@ -59,8 +48,8 @@ def test_probabilistic_hough(): # decrease default theta sampling because similar orientations may confuse # as mentioned in article of Galambos et al theta = np.linspace(0, np.pi, 45) - lines = probabilistic_hough(img, theta=theta, threshold=10, line_length=10, - line_gap=1) + lines = tf.probabilistic_hough_line(img, threshold=10, line_length=10, + line_gap=1, theta=theta) # sort the lines according to the x-axis sorted_lines = [] for line in lines: @@ -71,45 +60,59 @@ def test_probabilistic_hough(): assert([(25, 25), (74, 74)] in sorted_lines) -def test_hough_peaks_dist(): +def test_hough_line_peaks(): + img = np.zeros((100, 150), dtype=int) + rr, cc = line(60, 130, 80, 10) + img[rr, cc] = 1 + + out, angles, d = tf.hough_line(img) + + out, theta, dist = tf.hough_line_peaks(out, angles, d) + + assert_equal(len(dist), 1) + assert_almost_equal(dist[0], 80.723, 1) + assert_almost_equal(theta[0], 1.41, 1) + + +def test_hough_line_peaks_dist(): img = np.zeros((100, 100), dtype=np.bool_) img[:, 30] = True img[:, 40] = True hspace, angles, dists = tf.hough_line(img) - assert len(tf.hough_peaks(hspace, angles, dists, min_distance=5)[0]) == 2 - assert len(tf.hough_peaks(hspace, angles, dists, min_distance=15)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_distance=5)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_distance=15)[0]) == 1 -def test_hough_peaks_angle(): +def test_hough_line_peaks_angle(): img = np.zeros((100, 100), dtype=np.bool_) img[:, 0] = True img[0, :] = True hspace, angles, dists = tf.hough_line(img) - assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 theta = np.linspace(0, np.pi, 100) hspace, angles, dists = tf.hough_line(img, theta) - assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 theta = np.linspace(np.pi / 3, 4. / 3 * np.pi, 100) hspace, angles, dists = tf.hough_line(img, theta) - assert len(tf.hough_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 -def test_hough_peaks_num(): +def test_hough_line_peaks_num(): img = np.zeros((100, 100), dtype=np.bool_) img[:, 30] = True img[:, 40] = True hspace, angles, dists = tf.hough_line(img) - assert len(tf.hough_peaks(hspace, angles, dists, min_distance=0, - min_angle=0, num_peaks=1)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, min_distance=0, + min_angle=0, num_peaks=1)[0]) == 1 -def test_houghcircle(): +def test_hough_circle(): # Prepare picture img = np.zeros((120, 100), dtype=int) radius = 20