import numpy as np from scipy.spatial import cKDTree from ._hough_transform import (_hough_circle, _hough_ellipse, _hough_line, _probabilistic_hough_line as _prob_hough_line) def hough_line_peaks(hspace, angles, dists, min_distance=9, min_angle=10, threshold=None, num_peaks=np.inf): """Return peaks in a straight line 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. 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[-1] - angles[0] == PI`). dists : (N, ) array Distances returned by the `hough_line` function. min_distance : int, optional Minimum distance separating lines (maximum filter size for first dimension of hough space). min_angle : int, optional Minimum angle separating lines (maximum filter size for second dimension of hough space). threshold : float, optional Minimum intensity of peaks. Default is `0.5 * max(hspace)`. num_peaks : int, optional Maximum number of peaks. When the number of peaks exceeds `num_peaks`, return `num_peaks` coordinates based on peak intensity. Returns ------- accum, angles, dists : tuple of array Peak values in Hough space, angles and distances. Examples -------- >>> from skimage.transform import hough_line, hough_line_peaks >>> from skimage.draw import line >>> img = np.zeros((15, 15), dtype=np.bool_) >>> rr, cc = line(0, 0, 14, 14) >>> img[rr, cc] = 1 >>> rr, cc = line(0, 14, 14, 0) >>> img[cc, rr] = 1 >>> hspace, angles, dists = hough_line(img) >>> hspace, angles, dists = hough_line_peaks(hspace, angles, dists) >>> len(angles) 2 """ from ..feature.peak import _prominent_peaks h, a, d = _prominent_peaks(hspace, min_xdistance=min_angle, min_ydistance=min_distance, threshold=threshold, num_peaks=num_peaks) if a.any(): return (h, angles[a], dists[d]) else: return (h, np.array([]), np.array([])) def hough_circle(image, radius, normalize=True, full_output=False): """Perform a circular Hough transform. Parameters ---------- image : (M, N) ndarray Input image with nonzero values representing edges. radius : scalar or sequence of scalars Radii at which to compute the Hough transform. Floats are converted to integers. normalize : boolean, optional (default True) Normalize the accumulator with the number of pixels used to draw the radius. full_output : boolean, optional (default False) Extend the output size by twice the largest radius in order to detect centers outside the input picture. Returns ------- H : 3D ndarray (radius index, (M + 2R, N + 2R) ndarray) Hough transform accumulator for each radius. R designates the larger radius if full_output is True. Otherwise, R = 0. Examples -------- >>> from skimage.transform import hough_circle >>> from skimage.draw import circle_perimeter >>> img = np.zeros((100, 100), dtype=np.bool_) >>> rr, cc = circle_perimeter(25, 35, 23) >>> img[rr, cc] = 1 >>> try_radii = np.arange(5, 50) >>> res = hough_circle(img, try_radii) >>> ridx, r, c = np.unravel_index(np.argmax(res), res.shape) >>> r, c, try_radii[ridx] (25, 35, 23) """ radius = np.atleast_1d(np.asarray(radius)) return _hough_circle(image, radius.astype(np.intp), normalize=normalize, full_output=full_output) def hough_ellipse(image, threshold=4, accuracy=1, min_size=4, max_size=None): """Perform an elliptical Hough transform. Parameters ---------- image : (M, N) ndarray Input image with nonzero values representing edges. threshold : int, optional Accumulator threshold value. accuracy : double, optional Bin size on the minor axis used in the accumulator. min_size : int, optional Minimal major axis length. max_size : int, optional Maximal minor axis length. If None, the value is set to the half of the smaller image dimension. Returns ------- result : ndarray with fields [(accumulator, yc, xc, 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 -------- >>> 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) >>> result.tolist() [(10, 10.0, 10.0, 8.0, 6.0, 0.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(image, threshold=threshold, accuracy=accuracy, min_size=min_size, max_size=max_size) def hough_line(image, theta=None): """Perform a straight line Hough transform. Parameters ---------- image : (M, N) ndarray Input image with nonzero values representing edges. theta : 1D ndarray of double, optional Angles at which to compute the transform, in radians. Defaults to a vector of 180 angles evenly spaced from -pi/2 to pi/2. Returns ------- hspace : 2-D ndarray of uint64 Hough transform accumulator. angles : ndarray Angles at which the transform is 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. The angle accuracy can be improved by decreasing the step size in the `theta` array. 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 image.ndim != 2: raise ValueError('The input image `image` must be 2D.') if theta is None: # These values are approximations of pi/2 theta = np.linspace(-np.pi / 2, np.pi / 2, 180) return _hough_line(image, theta=theta) def probabilistic_hough_line(image, threshold=10, line_length=50, line_gap=10, theta=None, seed=None): """Return lines from a progressive probabilistic line Hough transform. Parameters ---------- image : (M, N) ndarray Input image with nonzero values representing edges. threshold : int, optional Threshold line_length : int, optional Minimum accepted length of detected lines. Increase the parameter to extract longer lines. line_gap : int, optional Maximum gap between pixels to still form a line. Increase the parameter to merge broken lines more aggressively. theta : 1D ndarray, dtype=double, optional Angles at which to compute the transform, in radians. If None, use a range from -pi/2 to pi/2. seed : int, optional Seed to initialize the random number generator. Returns ------- lines : list List of lines identified, lines in format ((x0, y0), (x1, y1)), 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 image.ndim != 2: raise ValueError('The input image `image` must be 2D.') if theta is None: theta = np.pi / 2 - np.arange(180) / 180.0 * np.pi return _prob_hough_line(image, threshold=threshold, line_length=line_length, line_gap=line_gap, theta=theta, seed=seed) def hough_circle_peaks(hspaces, radii, min_xdistance=1, min_ydistance=1, threshold=None, num_peaks=np.inf, total_num_peaks=np.inf, normalize=False): """Return peaks in a circle Hough transform. Identifies most prominent circles separated by certain distances in given Hough spaces. Non-maximum suppression with different sizes is applied separately in the first and second dimension of the Hough space to identify peaks. For circles with different radius but close in distance, only the one with highest peak is kept. Parameters ---------- hspaces : (N, M) array Hough spaces returned by the `hough_circle` function. radii : (M,) array Radii corresponding to Hough spaces. min_xdistance : int, optional Minimum distance separating centers in the x dimension. min_ydistance : int, optional Minimum distance separating centers in the y dimension. threshold : float, optional Minimum intensity of peaks in each Hough space. Default is `0.5 * max(hspace)`. num_peaks : int, optional Maximum number of peaks in each Hough space. When the number of peaks exceeds `num_peaks`, only `num_peaks` coordinates based on peak intensity are considered for the corresponding radius. total_num_peaks : int, optional Maximum number of peaks. When the number of peaks exceeds `num_peaks`, return `num_peaks` coordinates based on peak intensity. normalize : bool, optional If True, normalize the accumulator by the radius to sort the prominent peaks. Returns ------- accum, cx, cy, rad : tuple of array Peak values in Hough space, x and y center coordinates and radii. Examples -------- >>> from skimage import transform, draw >>> img = np.zeros((120, 100), dtype=int) >>> radius, x_0, y_0 = (20, 99, 50) >>> y, x = draw.circle_perimeter(y_0, x_0, radius) >>> img[x, y] = 1 >>> hspaces = transform.hough_circle(img, radius) >>> accum, cx, cy, rad = hough_circle_peaks(hspaces, [radius,]) Notes ----- Circles with bigger radius have higher peaks in Hough space. If larger circles are preferred over smaller ones, `normalize` should be False. Otherwise, circles will be returned in the order of decreasing voting number. """ from ..feature.peak import _prominent_peaks r = [] cx = [] cy = [] accum = [] for rad, hp in zip(radii, hspaces): h_p, x_p, y_p = _prominent_peaks(hp, min_xdistance=min_xdistance, min_ydistance=min_ydistance, threshold=threshold, num_peaks=num_peaks) r.extend((rad,)*len(h_p)) cx.extend(x_p) cy.extend(y_p) accum.extend(h_p) r = np.array(r) cx = np.array(cx) cy = np.array(cy) accum = np.array(accum) if normalize: s = np.argsort(accum / r) else: s = np.argsort(accum) accum_sorted, cx_sorted, cy_sorted, r_sorted = \ accum[s][::-1], cx[s][::-1], cy[s][::-1], r[s][::-1] tnp = len(accum_sorted) if total_num_peaks == np.inf else total_num_peaks # Skip searching for neighboring circles # if default min_xdistance and min_ydistance are used # or if no peak was detected if (min_xdistance == 1 and min_ydistance == 1) or len(accum_sorted) == 0: return (accum_sorted[:tnp], cx_sorted[:tnp], cy_sorted[:tnp], r_sorted[:tnp]) # For circles with centers too close, only keep the one with # the highest peak should_keep = label_distant_points( cx_sorted, cy_sorted, min_xdistance, min_ydistance, tnp ) return (accum_sorted[should_keep], cx_sorted[should_keep], cy_sorted[should_keep], r_sorted[should_keep]) def label_distant_points(xs, ys, min_xdistance, min_ydistance, max_points): """Keep points that are separated by certain distance in each dimension. The first point is always accpeted and all subsequent points are selected so that they are distant from all their preceding ones. Parameters ---------- xs : array X coordinates of points. ys : array Y coordinates of points. min_xdistance : int Minimum distance separating points in the x dimension. min_ydistance : int Minimum distance separating points in the y dimension. max_points : int Max number of distant points to keep. Returns ------- should_keep : array of bool A mask array for distant points to keep. """ is_neighbor = np.zeros(len(xs), dtype=bool) coordinates = np.stack([xs, ys], axis=1) # Use a KDTree to search for neighboring points effectively kd_tree = cKDTree(coordinates) n_pts = 0 for i in range(len(xs)): if n_pts >= max_points: # Ignore the point if points to keep reaches maximum is_neighbor[i] = True elif not is_neighbor[i]: # Find a short list of candidates to remove # by searching within a circle neighbors_i = kd_tree.query_ball_point( (xs[i], ys[i]), np.hypot(min_xdistance, min_ydistance) ) # Check distance in both dimensions and mark if close for ni in neighbors_i: x_close = abs(xs[ni] - xs[i]) <= min_xdistance y_close = abs(ys[ni] - ys[i]) <= min_ydistance if x_close and y_close and ni > i: is_neighbor[ni] = True n_pts += 1 should_keep = ~is_neighbor return should_keep