import numpy as np import scipy.ndimage as ndi from .. import measure from ..filters import rank_order def _get_high_intensity_peaks(image, mask, num_peaks): """ Return the highest intensity peak coordinates. """ # get coordinates of peaks coord = np.nonzero(mask) intensities = image[coord] # Highest peak first idx_maxsort = np.argsort(-intensities) coord = np.transpose(coord)[idx_maxsort] # select num_peaks peaks if len(coord) > num_peaks: coord = coord[:num_peaks] return coord def _get_peak_mask(image, min_distance, footprint, threshold_abs, threshold_rel): """ Return the mask containing all peak candidates above thresholds. """ if footprint is not None: image_max = ndi.maximum_filter(image, footprint=footprint, mode='constant') else: size = 2 * min_distance + 1 image_max = ndi.maximum_filter(image, size=size, mode='constant') mask = image == image_max if threshold_rel is not None: threshold = max(threshold_abs, threshold_rel * image.max()) else: threshold = threshold_abs mask &= image > threshold return mask def _exclude_border(mask, exclude_border): """ Remove peaks near the borders """ # zero out the image borders for i, excluded in enumerate(exclude_border): if excluded == 0: continue mask[(slice(None),) * i + (slice(None, excluded),)] = False mask[(slice(None),) * i + (slice(-excluded, None),)] = False return mask def peak_local_max(image, min_distance=1, threshold_abs=None, threshold_rel=None, exclude_border=True, indices=True, num_peaks=np.inf, footprint=None, labels=None, num_peaks_per_label=np.inf): """Find peaks in an image as coordinate list or boolean mask. Peaks are the local maxima in a region of `2 * min_distance + 1` (i.e. peaks are separated by at least `min_distance`). If there are multiple local maxima with identical pixel intensities inside the region defined with `min_distance`, the coordinates of all such pixels are returned. If both `threshold_abs` and `threshold_rel` are provided, the maximum of the two is chosen as the minimum intensity threshold of peaks. Parameters ---------- image : ndarray Input image. min_distance : int, optional Minimum number of pixels separating peaks in a region of `2 * min_distance + 1` (i.e. peaks are separated by at least `min_distance`). To find the maximum number of peaks, use `min_distance=1`. threshold_abs : float, optional Minimum intensity of peaks. By default, the absolute threshold is the minimum intensity of the image. threshold_rel : float, optional Minimum intensity of peaks, calculated as `max(image) * threshold_rel`. exclude_border : int, tuple of ints, or bool, optional If positive integer, `exclude_border` excludes peaks from within `exclude_border`-pixels of the border of the image. If tuple of non-negative ints, the length of the tuple must match the input array's dimensionality. Each element of the tuple will exclude peaks from within `exclude_border`-pixels of the border of the image along that dimension. If True, takes the `min_distance` parameter as value. If zero or False, peaks are identified regardless of their distance from the border. indices : bool, optional If True, the output will be an array representing peak coordinates. The coordinates are sorted according to peaks values (Larger first). If False, the output will be a boolean array shaped as `image.shape` with peaks present at True elements. num_peaks : int, optional Maximum number of peaks. When the number of peaks exceeds `num_peaks`, return `num_peaks` peaks based on highest peak intensity. footprint : ndarray of bools, optional If provided, `footprint == 1` represents the local region within which to search for peaks at every point in `image`. Overrides `min_distance`. labels : ndarray of ints, optional If provided, each unique region `labels == value` represents a unique region to search for peaks. Zero is reserved for background. num_peaks_per_label : int, optional Maximum number of peaks for each label. Returns ------- output : ndarray or ndarray of bools * If `indices = True` : (row, column, ...) coordinates of peaks. * If `indices = False` : Boolean array shaped like `image`, with peaks represented by True values. Notes ----- The peak local maximum function returns the coordinates of local peaks (maxima) in an image. A maximum filter is used for finding local maxima. This operation dilates the original image. After comparison of the dilated and original image, this function returns the coordinates or a mask of the peaks where the dilated image equals the original image. See also -------- skimage.feature.corner_peaks Examples -------- >>> img1 = np.zeros((7, 7)) >>> img1[3, 4] = 1 >>> img1[3, 2] = 1.5 >>> img1 array([[0. , 0. , 0. , 0. , 0. , 0. , 0. ], [0. , 0. , 0. , 0. , 0. , 0. , 0. ], [0. , 0. , 0. , 0. , 0. , 0. , 0. ], [0. , 0. , 1.5, 0. , 1. , 0. , 0. ], [0. , 0. , 0. , 0. , 0. , 0. , 0. ], [0. , 0. , 0. , 0. , 0. , 0. , 0. ], [0. , 0. , 0. , 0. , 0. , 0. , 0. ]]) >>> peak_local_max(img1, min_distance=1) array([[3, 2], [3, 4]]) >>> peak_local_max(img1, min_distance=2) array([[3, 2]]) >>> img2 = np.zeros((20, 20, 20)) >>> img2[10, 10, 10] = 1 >>> peak_local_max(img2, exclude_border=0) array([[10, 10, 10]]) """ out = np.zeros_like(image, dtype=np.bool) threshold_abs = threshold_abs if threshold_abs is not None else image.min() if isinstance(exclude_border, bool): exclude_border = (min_distance if exclude_border else 0,) * image.ndim elif isinstance(exclude_border, int): if exclude_border < 0: raise ValueError("`exclude_border` cannot be a negative value") exclude_border = (exclude_border,) * image.ndim elif isinstance(exclude_border, tuple): if len(exclude_border) != image.ndim: raise ValueError( "`exclude_border` should have the same length as the " "dimensionality of the image.") for exclude in exclude_border: if not isinstance(exclude, int): raise ValueError( "`exclude_border`, when expressed as a tuple, must only " "contain ints." ) if exclude < 0: raise ValueError( "`exclude_border` cannot contain a negative value") else: raise TypeError( "`exclude_border` must be bool, int, or tuple with the same " "length as the dimensionality of the image.") # no peak for a trivial image if np.all(image == image.flat[0]): if indices is True: return np.empty((0, image.ndim), np.int) else: return out # In the case of labels, call ndi on each label if labels is not None: label_values = np.unique(labels) # Reorder label values to have consecutive integers (no gaps) if np.any(np.diff(label_values) != 1): mask = labels >= 1 labels[mask] = 1 + rank_order(labels[mask])[0].astype(labels.dtype) labels = labels.astype(np.int32) # create a mask for the non-exclude region inner_mask = _exclude_border(np.ones_like(labels, dtype=bool), exclude_border) # For each label, extract a smaller image enclosing the object of # interest, identify num_peaks_per_label peaks and mark them in # variable out. for label_idx, obj in enumerate(ndi.find_objects(labels)): img_object = image[obj] * (labels[obj] == label_idx + 1) mask = _get_peak_mask(img_object, min_distance, footprint, threshold_abs, threshold_rel) if exclude_border: # remove peaks fall in the exclude region mask &= inner_mask[obj] coordinates = _get_high_intensity_peaks(img_object, mask, num_peaks_per_label) nd_indices = tuple(coordinates.T) mask.fill(False) mask[nd_indices] = True out[obj] += mask if not indices and np.isinf(num_peaks): return out coordinates = _get_high_intensity_peaks(image, out, num_peaks) if indices: return coordinates else: out.fill(False) nd_indices = tuple(coordinates.T) out[nd_indices] = True return out # Non maximum filter mask = _get_peak_mask(image, min_distance, footprint, threshold_abs, threshold_rel) mask = _exclude_border(mask, exclude_border) # Select highest intensities (num_peaks) coordinates = _get_high_intensity_peaks(image, mask, num_peaks) if indices is True: return coordinates else: nd_indices = tuple(coordinates.T) out[nd_indices] = True return out def _prominent_peaks(image, min_xdistance=1, min_ydistance=1, threshold=None, num_peaks=np.inf): """Return peaks with non-maximum suppression. Identifies most prominent features separated by certain distances. Non-maximum suppression with different sizes is applied separately in the first and second dimension of the image to identify peaks. Parameters ---------- image : (M, N) ndarray Input image. min_xdistance : int Minimum distance separating features in the x dimension. min_ydistance : int Minimum distance separating features in the y dimension. threshold : float Minimum intensity of peaks. Default is `0.5 * max(image)`. num_peaks : int Maximum number of peaks. When the number of peaks exceeds `num_peaks`, return `num_peaks` coordinates based on peak intensity. Returns ------- intensity, xcoords, ycoords : tuple of array Peak intensity values, x and y indices. """ img = image.copy() rows, cols = img.shape if threshold is None: threshold = 0.5 * np.max(img) ycoords_size = 2 * min_ydistance + 1 xcoords_size = 2 * min_xdistance + 1 img_max = ndi.maximum_filter1d(img, size=ycoords_size, axis=0, mode='constant', cval=0) img_max = ndi.maximum_filter1d(img_max, size=xcoords_size, axis=1, mode='constant', cval=0) mask = (img == img_max) img *= mask img_t = img > threshold label_img = measure.label(img_t) props = measure.regionprops(label_img, img_max) # 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) img_peaks = [] ycoords_peaks = [] xcoords_peaks = [] # relative coordinate grid for local neighbourhood suppression ycoords_ext, xcoords_ext = np.mgrid[-min_ydistance:min_ydistance + 1, -min_xdistance:min_xdistance + 1] for ycoords_idx, xcoords_idx in coords: accum = img_max[ycoords_idx, xcoords_idx] if accum > threshold: # absolute coordinate grid for local neighbourhood suppression ycoords_nh = ycoords_idx + ycoords_ext xcoords_nh = xcoords_idx + xcoords_ext # no reflection for distance neighbourhood ycoords_in = np.logical_and(ycoords_nh > 0, ycoords_nh < rows) ycoords_nh = ycoords_nh[ycoords_in] xcoords_nh = xcoords_nh[ycoords_in] # reflect xcoords and assume xcoords are continuous, # e.g. for angles: # (..., 88, 89, -90, -89, ..., 89, -90, -89, ...) xcoords_low = xcoords_nh < 0 ycoords_nh[xcoords_low] = rows - ycoords_nh[xcoords_low] xcoords_nh[xcoords_low] += cols xcoords_high = xcoords_nh >= cols ycoords_nh[xcoords_high] = rows - ycoords_nh[xcoords_high] xcoords_nh[xcoords_high] -= cols # suppress neighbourhood img_max[ycoords_nh, xcoords_nh] = 0 # add current feature to peaks img_peaks.append(accum) ycoords_peaks.append(ycoords_idx) xcoords_peaks.append(xcoords_idx) img_peaks = np.array(img_peaks) ycoords_peaks = np.array(ycoords_peaks) xcoords_peaks = np.array(xcoords_peaks) if num_peaks < len(img_peaks): idx_maxsort = np.argsort(img_peaks)[::-1][:num_peaks] img_peaks = img_peaks[idx_maxsort] ycoords_peaks = ycoords_peaks[idx_maxsort] xcoords_peaks = xcoords_peaks[idx_maxsort] return img_peaks, xcoords_peaks, ycoords_peaks