#cython: cdivision=True #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False import numpy as np cimport numpy as cnp from cpython.mem cimport PyMem_Malloc, PyMem_Free from libc.stdlib cimport abs from libc.math cimport fabs, sqrt, ceil, atan2, M_PI from ..draw import circle_perimeter from .._shared.interpolation cimport round cnp.import_array() def _hough_circle(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.intp_t] radius, char normalize=True, char full_output=False): """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 (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. """ if img.ndim != 2: raise ValueError('The input image must be 2D.') cdef Py_ssize_t xmax = img.shape[0] cdef Py_ssize_t ymax = img.shape[1] # compute the nonzero indexes cdef cnp.ndarray[ndim=1, dtype=cnp.intp_t] x, y x, y = np.nonzero(img) cdef Py_ssize_t num_pixels = x.size cdef Py_ssize_t offset = 0 if full_output: # Offset the image offset = radius.max() x = x + offset y = y + offset cdef Py_ssize_t i, p, c, num_circle_pixels, tx, ty cdef double incr cdef cnp.ndarray[ndim=1, dtype=cnp.intp_t] circle_x, circle_y cdef cnp.ndarray[ndim=3, dtype=cnp.double_t] acc = \ np.zeros((radius.size, img.shape[0] + 2 * offset, img.shape[1] + 2 * offset), dtype=np.double) for i, rad in enumerate(radius): # Store in memory the circle of given radius # centered at (0,0) circle_x, circle_y = circle_perimeter(0, 0, rad) num_circle_pixels = circle_x.size with nogil: if normalize: incr = 1.0 / num_circle_pixels else: incr = 1 # For each non zero pixel for p in range(num_pixels): # Plug the circle at (px, py), # its coordinates are (tx, ty) for c in range(num_circle_pixels): tx = circle_x[c] + x[p] ty = circle_y[c] + y[p] if offset: acc[i, tx, ty] += incr elif 0 <= tx < xmax and 0 <= ty < ymax: acc[i, tx, ty] += incr return acc def _hough_ellipse(cnp.ndarray img, Py_ssize_t threshold=4, double accuracy=1, Py_ssize_t 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, 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 -------- >>> 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 """ if img.ndim != 2: raise ValueError('The input image must be 2D.') # The creation of the array `pixels` results in a rather nasty error # when the image is empty. # As discussed in GitHub #2820 and #2996, we opt to return an empty array. if not np.any(img): return np.zeros((0, 6)) cdef Py_ssize_t[:, ::1] pixels = np.row_stack(np.nonzero(img)) cdef Py_ssize_t num_pixels = pixels.shape[1] cdef list acc = list() cdef list results = list() cdef double bin_size = accuracy * accuracy cdef double max_b_squared if max_size is None: if img.shape[0] < img.shape[1]: max_b_squared = np.round(0.5 * img.shape[0]) else: max_b_squared = np.round(0.5 * img.shape[1]) max_b_squared *= max_b_squared else: max_b_squared = max_size * max_size cdef Py_ssize_t p1, p2, p3, p1x, p1y, p2x, p2y, p3x, p3y cdef double xc, yc, a, b, d, k, dx, dy cdef double cos_tau_squared, b_squared, orientation for p1 in range(num_pixels): p1x = pixels[1, p1] p1y = pixels[0, p1] for p2 in range(p1): p2x = pixels[1, p2] p2y = pixels[0, p2] # Candidate: center (xc, yc) and main axis a dx = p1x - p2x dy = p1y - p2y a = 0.5 * sqrt(dx * dx + dy * dy) if a > 0.5 * min_size: xc = 0.5 * (p1x + p2x) yc = 0.5 * (p1y + p2y) for p3 in range(num_pixels): p3x = pixels[1, p3] p3y = pixels[0, p3] dx = p3x - xc dy = p3y - yc d = sqrt(dx * dx + dy * dy) if d > min_size: dx = p3x - p1x dy = p3y - p1y cos_tau_squared = ((a*a + d*d - dx*dx - dy*dy) / (2 * a * d)) cos_tau_squared *= cos_tau_squared # Consider b2 > 0 and avoid division by zero k = a*a - d*d * cos_tau_squared if k > 0 and cos_tau_squared < 1: b_squared = a*a * d*d * (1 - cos_tau_squared) / k # b2 range is limited to avoid histogram memory # overflow if b_squared <= max_b_squared: acc.append(b_squared) if len(acc) > 0: bins = np.arange(0, np.max(acc) + bin_size, bin_size) hist, bin_edges = np.histogram(acc, bins=bins) hist_max = np.max(hist) if hist_max > threshold: orientation = atan2(p1x - p2x, p1y - p2y) b = sqrt(bin_edges[hist.argmax()]) # to keep ellipse_perimeter() convention if orientation != 0: orientation = M_PI - orientation # When orientation is not in [-pi:pi] # it would mean in ellipse_perimeter() # that a < b. But we keep a > b. if orientation > M_PI: orientation = orientation - M_PI / 2. a, b = b, a results.append((hist_max, # Accumulator yc, xc, a, b, orientation)) acc = [] return np.array(results, dtype=[('accumulator', np.intp), ('yc', np.double), ('xc', np.double), ('a', np.double), ('b', np.double), ('orientation', np.double)]) def _hough_line(cnp.ndarray img, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta): """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. 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 """ # 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 ctheta = np.cos(theta) stheta = np.sin(theta) # compute the bins and allocate the accumulator array cdef cnp.ndarray[ndim=2, dtype=cnp.uint64_t] accum cdef cnp.ndarray[ndim=1, dtype=cnp.double_t] bins cdef Py_ssize_t max_distance, offset max_distance = 2 * ceil(sqrt(img.shape[0] * img.shape[0] + img.shape[1] * img.shape[1])) accum = np.zeros((max_distance, theta.shape[0]), dtype=np.uint64) bins = np.linspace(-max_distance / 2.0, max_distance / 2.0, max_distance) offset = max_distance / 2 # compute the nonzero indexes cdef cnp.ndarray[ndim=1, dtype=cnp.npy_intp] x_idxs, y_idxs y_idxs, x_idxs = np.nonzero(img) # 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 nthetas = theta.shape[0] with nogil: for i in range(nidxs): x = x_idxs[i] y = y_idxs[i] for j in range(nthetas): accum_idx = round((ctheta[j] * x + stheta[j] * y)) + offset accum[accum_idx, j] += 1 return accum, theta, bins def _probabilistic_hough_line(cnp.ndarray img, Py_ssize_t threshold, Py_ssize_t line_length, Py_ssize_t line_gap, cnp.ndarray[ndim=1, dtype=cnp.double_t] theta, seed=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 Minimum accepted length of detected lines. Increase the parameter to extract longer lines. line_gap : int Maximum gap between pixels to still form a line. Increase the parameter to merge broken lines more aggressively. theta : 1D ndarray, dtype=double Angles at which to compute the transform, in radians. seed : int, optional Seed to initialize the random number generator. 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. """ cdef Py_ssize_t height = img.shape[0] cdef Py_ssize_t width = img.shape[1] # compute the bins and allocate the accumulator array cdef cnp.ndarray[ndim=2, dtype=cnp.uint8_t] mask = \ np.zeros((height, width), dtype=np.uint8) cdef Py_ssize_t *line_end = \ PyMem_Malloc(4 * sizeof(Py_ssize_t)) if not line_end: raise MemoryError('could not allocate line_end') cdef Py_ssize_t max_distance, offset, num_indexes, index cdef double a, b cdef Py_ssize_t nidxs, i, j, k, x, y, px, py, accum_idx, max_theta cdef Py_ssize_t xflag, x0, y0, dx0, dy0, dx, dy, gap, x1, y1, count cdef cnp.int64_t value, max_value, cdef int shift = 16 cdef int good_line cdef Py_ssize_t nlines = 0 cdef Py_ssize_t lines_max = 2 ** 15 # maximum line number cutoff cdef cnp.intp_t[:, :, ::1] lines = np.zeros((lines_max, 2, 2), dtype=np.intp) max_distance = 2 * ceil((sqrt(img.shape[0] * img.shape[0] + img.shape[1] * img.shape[1]))) cdef cnp.int64_t[:, ::1] accum = np.zeros((max_distance, theta.shape[0]), dtype=np.int64) offset = max_distance / 2 cdef Py_ssize_t nthetas = theta.shape[0] # compute sine and cosine of angles cdef cnp.double_t[::1] ctheta = np.cos(theta) cdef cnp.double_t[::1] stheta = np.sin(theta) # find the nonzero indexes cdef cnp.intp_t[:] y_idxs, x_idxs y_idxs, x_idxs = np.nonzero(img) # mask all non-zero indexes mask[y_idxs, x_idxs] = 1 count = len(x_idxs) random_state = np.random.RandomState(seed) random_ = np.arange(count, dtype=np.intp) random_state.shuffle(random_) cdef cnp.intp_t[::1] random = random_ with nogil: while count > 0: count -= 1 # select random non-zero point index = random[count] x = x_idxs[index] y = y_idxs[index] # if previously eliminated, skip if not mask[y, x]: continue value = 0 max_value = threshold - 1 max_theta = -1 # apply hough transform on point for j in range(nthetas): accum_idx = round((ctheta[j] * x + stheta[j] * y)) + offset accum[accum_idx, j] += 1 value = accum[accum_idx, j] if value > max_value: max_value = value max_theta = j if max_value < threshold: continue # from the random point walk in opposite directions and find line # beginning and end a = -stheta[max_theta] b = ctheta[max_theta] x0 = x y0 = y # calculate gradient of walks using fixed point math xflag = fabs(a) > fabs(b) if xflag: if a > 0: dx0 = 1 else: dx0 = -1 dy0 = round(b * (1 << shift) / fabs(a)) y0 = (y0 << shift) + (1 << (shift - 1)) else: if b > 0: dy0 = 1 else: dy0 = -1 dx0 = round(a * (1 << shift) / fabs(b)) x0 = (x0 << shift) + (1 << (shift - 1)) # pass 1: walk the line, merging lines less than specified gap # length for k in range(2): gap = 0 px = x0 py = y0 dx = dx0 dy = dy0 if k > 0: dx = -dx dy = -dy while 1: if xflag: x1 = px y1 = py >> shift else: x1 = px >> shift y1 = py # check when line exits image boundary if x1 < 0 or x1 >= width or y1 < 0 or y1 >= height: break gap += 1 # if non-zero point found, continue the line if mask[y1, x1]: gap = 0 line_end[2*k] = x1 line_end[2*k + 1] = y1 # if gap to this point was too large, end the line elif gap > line_gap: break px += dx py += dy # confirm line length is sufficient good_line = (abs(line_end[3] - line_end[1]) >= line_length or abs(line_end[2] - line_end[0]) >= line_length) # pass 2: walk the line again and reset accumulator and mask for k in range(2): px = x0 py = y0 dx = dx0 dy = dy0 if k > 0: dx = -dx dy = -dy while 1: if xflag: x1 = px y1 = py >> shift else: x1 = px >> shift y1 = py # if non-zero point found, continue the line if mask[y1, x1]: if good_line: accum_idx = round( (ctheta[j] * x1 + stheta[j] * y1)) + offset accum[accum_idx, max_theta] -= 1 mask[y1, x1] = 0 # exit when the point is the line end if x1 == line_end[2*k] and y1 == line_end[2*k + 1]: break px += dx py += dy # add line to the result if good_line: lines[nlines, 0, 0] = line_end[0] lines[nlines, 0, 1] = line_end[1] lines[nlines, 1, 0] = line_end[2] lines[nlines, 1, 1] = line_end[3] nlines += 1 if nlines >= lines_max: break PyMem_Free(line_end) return [((line[0, 0], line[0, 1]), (line[1, 0], line[1, 1])) for line in lines[:nlines]]