def _interpolate_image(image, *, multichannel=False): """Replacing each pixel in ``image`` with the average of its neighbors. Parameters ---------- image : ndarray Input data to be interpolated. multichannel : bool, optional Whether the last axis of the image is to be interpreted as multiple channels or another spatial dimension. Returns ------- interp : ndarray Interpolated version of `image`. """ spatialdims = image.ndim if not multichannel else image.ndim - 1 conv_filter = ndi.generate_binary_structure(spatialdims, 1).astype(image.dtype) conv_filter.ravel()[conv_filter.size // 2] = 0 conv_filter /= conv_filter.sum() # CuPy Backend: refactored below to avoid for loop if multichannel: conv_filter = conv_filter[..., np.newaxis] interp = ndi.convolve(image, conv_filter, mode='mirror') return interp
def test_3d_fallback_default_selem(): # 3x3x3 cube inside a 7x7x7 image: image = cp.zeros((7, 7, 7), bool) image[2:-2, 2:-2, 2:-2] = 1 opened = grey.opening(image) # expect a "hyper-cross" centered in the 5x5x5: image_expected = cp.zeros((7, 7, 7), dtype=bool) image_expected[2:5, 2:5, 2:5] = ndi.generate_binary_structure(3, 1) cp.testing.assert_array_equal(opened, image_expected)
def _mask_filter_result(result, mask): """Return result after masking. Input masks are eroded so that mask areas in the original image don't affect values in the result. """ if mask is not None: erosion_selem = ndi.generate_binary_structure(mask.ndim, mask.ndim) mask = ndi.binary_erosion(mask, erosion_selem, border_value=0) result *= mask return result
def test_3d_fallback_black_tophat(): image = cp.ones((7, 7, 7), dtype=bool) image[2, 2:4, 2:4] = 0 image[3, 2:5, 2:5] = 0 image[4, 3:5, 3:5] = 0 with expected_warnings([r'operator.*deprecated|\A\Z']): new_image = grey.black_tophat(image) footprint = ndi.generate_binary_structure(3, 1) with expected_warnings([r'operator.*deprecated|\A\Z']): image_expected = ndi.black_tophat(image.view(dtype=cp.uint8), footprint=footprint) cp.testing.assert_array_equal(new_image, image_expected)
def test_2d_ndimage_equivalence(): image = cp.zeros((9, 9), cp.uint8) image[2:-2, 2:-2] = 128 image[3:-3, 3:-3] = 196 image[4, 4] = 255 opened = grey.opening(image) closed = grey.closing(image) selem = ndi.generate_binary_structure(2, 1) ndimage_opened = ndi.grey_opening(image, footprint=selem) ndimage_closed = ndi.grey_closing(image, footprint=selem) cp.testing.assert_array_equal(opened, ndimage_opened) cp.testing.assert_array_equal(closed, ndimage_closed)
def test_2d_ndimage_equivalence(): image = cp.zeros((9, 9), cp.uint16) image[2:-2, 2:-2] = 2**14 image[3:-3, 3:-3] = 2**15 image[4, 4] = 2**16 - 1 bin_opened = binary.binary_opening(image) bin_closed = binary.binary_closing(image) selem = ndi.generate_binary_structure(2, 1) ndimage_opened = ndi.binary_opening(image, structure=selem) ndimage_closed = ndi.binary_closing(image, structure=selem) testing.assert_array_equal(bin_opened, ndimage_opened) testing.assert_array_equal(bin_closed, ndimage_closed)
def test_mask(): from cudipy.segment.mask import otsu vol = cp.zeros((30, 30, 30)) vol[15, 15, 15] = 1 struct = generate_binary_structure(3, 1) # TODO: remove brute_force=True once non-brute force implemented for CuPy voln = binary_dilation(vol, structure=struct, iterations=4, brute_force=True).astype("f4") initial = cp.sum(voln > 0) mask = voln.copy() thresh = otsu(mask) mask = mask > thresh initial_otsu = cp.sum(mask > 0) assert_array_equal(initial_otsu, initial) mins, maxs = bounding_box(mask) voln_crop = crop(mask, mins, maxs) initial_crop = cp.sum(voln_crop > 0) assert_array_equal(initial_crop, initial) applymask(voln, mask) final = cp.sum(voln > 0) assert_array_equal(final, initial) # Test multi_median. img = cp.arange(25).reshape(5, 5) img_copy = img.copy() medianradius = 2 median_test = multi_median(img, medianradius, 3) assert_array_equal(img, img_copy) medarr = ((medianradius * 2) + 1, ) * img.ndim median_control = median_filter(img, medarr) median_control = median_filter(median_control, medarr) median_control = median_filter(median_control, medarr) assert_array_equal(median_test, median_control)
def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None, use_quantiles=False): """Edge filter an image using the Canny algorithm. Parameters ----------- image : 2D array Grayscale input image to detect edges on; can be of any dtype. sigma : float, optional Standard deviation of the Gaussian filter. low_threshold : float, optional Lower bound for hysteresis thresholding (linking edges). If None, low_threshold is set to 10% of dtype's max. high_threshold : float, optional Upper bound for hysteresis thresholding (linking edges). If None, high_threshold is set to 20% of dtype's max. mask : array, dtype=bool, optional Mask to limit the application of Canny to a certain area. use_quantiles : bool, optional If True then treat low_threshold and high_threshold as quantiles of the edge magnitude image, rather than absolute edge magnitude values. If True, then the thresholds must be in the range [0, 1]. Returns ------- output : 2D array (image) The binary edge map. See also -------- skimage.sobel Notes ----- The steps of the algorithm are as follows: * Smooth the image using a Gaussian with ``sigma`` width. * Apply the horizontal and vertical Sobel operators to get the gradients within the image. The edge strength is the norm of the gradient. * Thin potential edges to 1-pixel wide curves. First, find the normal to the edge at each point. This is done by looking at the signs and the relative magnitude of the X-Sobel and Y-Sobel to sort the points into 4 categories: horizontal, vertical, diagonal and antidiagonal. Then look in the normal and reverse directions to see if the values in either of those directions are greater than the point in question. Use interpolation to get a mix of points instead of picking the one that's the closest to the normal. * Perform a hysteresis thresholding: first label all points above the high threshold as edges. Then recursively label any point above the low threshold that is 8-connected to a labeled point as an edge. References ----------- .. [1] Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8:679-714, 1986 :DOI:`10.1109/TPAMI.1986.4767851` .. [2] William Green's Canny tutorial https://en.wikipedia.org/wiki/Canny_edge_detector Examples -------- >>> import cupy as cp >>> from cucim.skimage import feature >>> # Generate noisy image of a square >>> im = cp.zeros((256, 256)) >>> im[64:-64, 64:-64] = 1 >>> im += 0.2 * cp.random.rand(*im.shape) >>> # First trial with the Canny filter, with the default smoothing >>> edges1 = feature.canny(im) >>> # Increase the smoothing for better results >>> edges2 = feature.canny(im, sigma=3) """ # # The steps involved: # # * Smooth using the Gaussian with sigma above. # # * Apply the horizontal and vertical Sobel operators to get the gradients # within the image. The edge strength is the sum of the magnitudes # of the gradients in each direction. # # * Find the normal to the edge at each point using the arctangent of the # ratio of the Y sobel over the X sobel - pragmatically, we can # look at the signs of X and Y and the relative magnitude of X vs Y # to sort the points into 4 categories: horizontal, vertical, # diagonal and antidiagonal. # # * Look in the normal and reverse directions to see if the values # in either of those directions are greater than the point in question. # Use interpolation to get a mix of points instead of picking the one # that's the closest to the normal. # # * Label all points above the high threshold as edges. # * Recursively label any point above the low threshold that is 8-connected # to a labeled point as an edge. # # Regarding masks, any point touching a masked point will have a gradient # that is "infected" by the masked point, so it's enough to erode the # mask by one and then mask the output. We also mask out the border points # because who knows what lies beyond the edge of the image? # check_nD(image, 2) dtype_max = dtype_limits(image, clip_negative=False)[1] if low_threshold is None: low_threshold = 0.1 elif use_quantiles: if not (0.0 <= low_threshold <= 1.0): raise ValueError("Quantile thresholds must be between 0 and 1.") else: low_threshold = low_threshold / dtype_max if high_threshold is None: high_threshold = 0.2 elif use_quantiles: if not (0.0 <= high_threshold <= 1.0): raise ValueError("Quantile thresholds must be between 0 and 1.") else: high_threshold = high_threshold / dtype_max _gaussian = functools.partial(gaussian, sigma=sigma) def fsmooth(x, mode='constant'): return img_as_float(_gaussian(x, mode=mode)) if mask is None: smoothed = fsmooth(image, mode='reflect') # mask that is ones everywhere except the borders eroded_mask = cp.ones(image.shape, dtype=bool) eroded_mask[:1, :] = 0 eroded_mask[-1:, :] = 0 eroded_mask[:, :1] = 0 eroded_mask[:, -1:] = 0 else: smoothed = smooth_with_function_and_mask(image, fsmooth, mask) # # Make the eroded mask. Setting the border value to zero will wipe # out the image edges for us. # s = generate_binary_structure(2, 2) eroded_mask = binary_erosion(mask, s, border_value=0) jsobel = ndi.sobel(smoothed, axis=1) isobel = ndi.sobel(smoothed, axis=0) abs_isobel = cp.abs(isobel) abs_jsobel = cp.abs(jsobel) magnitude = cp.hypot(isobel, jsobel) eroded_mask = eroded_mask & (magnitude > 0) # TODO: implement custom kernel to compute local maxima # # --------- Find local maxima -------------- # # Assign each point to have a normal of 0-45 degrees, 45-90 degrees, # 90-135 degrees and 135-180 degrees. # local_maxima = cp.zeros(image.shape, bool) isobel_gt_0 = isobel >= 0 jsobel_gt_0 = jsobel >= 0 isobel_lt_0 = isobel <= 0 jsobel_lt_0 = jsobel <= 0 abs_isobel_lt_jsobel = abs_isobel <= abs_jsobel abs_isobel_gt_jsobel = abs_isobel >= abs_jsobel # ----- 0 to 45 degrees ------ pts_plus = isobel_gt_0 & jsobel_gt_0 pts_minus = isobel_lt_0 & jsobel_lt_0 pts_tmp = (pts_plus | pts_minus) & eroded_mask pts = pts_tmp & abs_isobel_gt_jsobel # Get the magnitudes shifted left to make a matrix of the points to the # right of pts. Similarly, shift left and down to get the points to the # top right of pts. c1 = magnitude[1:, :][pts[:-1, :]] c2 = magnitude[1:, 1:][pts[:-1, :-1]] m = magnitude[pts] w = abs_jsobel[pts] / abs_isobel[pts] c_plus = _fused_comparison(w, c1, c2, m) c1 = magnitude[:-1, :][pts[1:, :]] c2 = magnitude[:-1, :-1][pts[1:, 1:]] c_minus = _fused_comparison(w, c1, c2, m) local_maxima[pts] = c_plus & c_minus # ----- 45 to 90 degrees ------ # Mix diagonal and vertical # pts = pts_tmp & abs_isobel_lt_jsobel c1 = magnitude[:, 1:][pts[:, :-1]] c2 = magnitude[1:, 1:][pts[:-1, :-1]] m = magnitude[pts] w = abs_isobel[pts] / abs_jsobel[pts] c_plus = _fused_comparison(w, c1, c2, m) c1 = magnitude[:, :-1][pts[:, 1:]] c2 = magnitude[:-1, :-1][pts[1:, 1:]] c_minus = _fused_comparison(w, c1, c2, m) local_maxima[pts] = c_plus & c_minus # ----- 90 to 135 degrees ------ # Mix anti-diagonal and vertical # pts_plus = isobel_lt_0 & jsobel_gt_0 pts_minus = isobel_gt_0 & jsobel_lt_0 pts_tmp = (pts_plus | pts_minus) & eroded_mask pts = pts_tmp & abs_isobel_lt_jsobel c1a = magnitude[:, 1:][pts[:, :-1]] c2a = magnitude[:-1, 1:][pts[1:, :-1]] m = magnitude[pts] w = abs_isobel[pts] / abs_jsobel[pts] c_plus = _fused_comparison(w, c1a, c2a, m) c1 = magnitude[:, :-1][pts[:, 1:]] c2 = magnitude[1:, :-1][pts[:-1, 1:]] c_minus = _fused_comparison(w, c1, c2, m) local_maxima[pts] = c_plus & c_minus # ----- 135 to 180 degrees ------ # Mix anti-diagonal and anti-horizontal # pts = pts_tmp & abs_isobel_gt_jsobel c1 = magnitude[:-1, :][pts[1:, :]] c2 = magnitude[:-1, 1:][pts[1:, :-1]] m = magnitude[pts] w = abs_jsobel[pts] / abs_isobel[pts] c_plus = _fused_comparison(w, c1, c2, m) c1 = magnitude[1:, :][pts[:-1, :]] c2 = magnitude[1:, :-1][pts[:-1, 1:]] c_minus = _fused_comparison(w, c1, c2, m) local_maxima[pts] = c_plus & c_minus # # ---- If use_quantiles is set then calculate the thresholds to use # if use_quantiles: high_threshold = cp.percentile(magnitude, 100.0 * high_threshold) low_threshold = cp.percentile(magnitude, 100.0 * low_threshold) # # ---- Create two masks at the two thresholds. # high_mask = local_maxima & (magnitude >= high_threshold) low_mask = local_maxima & (magnitude >= low_threshold) # # Segment the low-mask, then only keep low-segments that have # some high_mask component in them # labels, count = ndi.label(low_mask, structure=cp.ones((3, 3), bool)) if count == 0: return low_mask nonzero_sums = cp.unique(labels[high_mask]) good_label = cp.zeros((count + 1, ), bool) good_label[nonzero_sums] = True output_mask = good_label[labels] return output_mask
def find_boundaries(label_img, connectivity=1, mode="thick", background=0): """Return bool array where boundaries between labeled regions are True. Parameters ---------- label_img : array of int or bool An array in which different regions are labeled with either different integers or boolean values. connectivity : int in {1, ..., `label_img.ndim`}, optional A pixel is considered a boundary pixel if any of its neighbors has a different label. `connectivity` controls which pixels are considered neighbors. A connectivity of 1 (default) means pixels sharing an edge (in 2D) or a face (in 3D) will be considered neighbors. A connectivity of `label_img.ndim` means pixels sharing a corner will be considered neighbors. mode : string in {'thick', 'inner', 'outer', 'subpixel'} How to mark the boundaries: - thick: any pixel not completely surrounded by pixels of the same label (defined by `connectivity`) is marked as a boundary. This results in boundaries that are 2 pixels thick. - inner: outline the pixels *just inside* of objects, leaving background pixels untouched. - outer: outline pixels in the background around object boundaries. When two objects touch, their boundary is also marked. - subpixel: return a doubled image, with pixels *between* the original pixels marked as boundary where appropriate. background : int, optional For modes 'inner' and 'outer', a definition of a background label is required. See `mode` for descriptions of these two. Returns ------- boundaries : array of bool, same shape as `label_img` A bool image where ``True`` represents a boundary pixel. For `mode` equal to 'subpixel', ``boundaries.shape[i]`` is equal to ``2 * label_img.shape[i] - 1`` for all ``i`` (a pixel is inserted in between all other pairs of pixels). Examples -------- >>> labels = cp.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0, 5, 5, 5, 0, 0], ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], ... [0, 0, 0, 0, 0, 5, 5, 5, 0, 0], ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=cp.uint8) >>> find_boundaries(labels, mode='thick').astype(cp.uint8) array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 0, 1, 1, 0], [0, 1, 1, 0, 1, 1, 0, 1, 1, 0], [0, 1, 1, 1, 1, 1, 0, 1, 1, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) >>> find_boundaries(labels, mode='inner').astype(cp.uint8) array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 1, 0, 0], [0, 0, 1, 0, 1, 1, 0, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 1, 0, 0], [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) >>> find_boundaries(labels, mode='outer').astype(cp.uint8) array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 0, 1, 0], [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], [0, 0, 1, 1, 1, 1, 0, 0, 1, 0], [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) >>> labels_small = labels[::2, ::3] >>> labels_small array([[0, 0, 0, 0], [0, 0, 5, 0], [0, 1, 5, 0], [0, 0, 5, 0], [0, 0, 0, 0]], dtype=uint8) >>> find_boundaries(labels_small, mode='subpixel').astype(cp.uint8) array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 0], [0, 0, 0, 1, 0, 1, 0], [0, 1, 1, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0], [0, 1, 1, 1, 0, 1, 0], [0, 0, 0, 1, 0, 1, 0], [0, 0, 0, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0]], dtype=uint8) >>> bool_image = cp.array([[False, False, False, False, False], ... [False, False, False, False, False], ... [False, False, True, True, True], ... [False, False, True, True, True], ... [False, False, True, True, True]], ... dtype=cp.bool) >>> find_boundaries(bool_image) array([[False, False, False, False, False], [False, False, True, True, True], [False, True, True, True, True], [False, True, True, False, False], [False, True, True, False, False]]) """ if label_img.dtype == 'bool': label_img = label_img.astype(cp.uint8) ndim = label_img.ndim selem = ndi.generate_binary_structure(ndim, connectivity) if mode != 'subpixel': boundaries = dilation(label_img, selem) != erosion(label_img, selem) if mode == 'inner': foreground_image = label_img != background boundaries &= foreground_image elif mode == 'outer': max_label = cp.iinfo(label_img.dtype).max background_image = label_img == background selem = ndi.generate_binary_structure(ndim, ndim) inverted_background = cp.array(label_img, copy=True) inverted_background[background_image] = max_label adjacent_objects = ((dilation(label_img, selem) != erosion(inverted_background, selem)) & ~background_image) boundaries &= (background_image | adjacent_objects) return boundaries else: boundaries = _find_boundaries_subpixel(label_img) return boundaries
def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): """Remove objects smaller than the specified size. Expects ar to be an array with labeled objects, and removes objects smaller than min_size. If `ar` is bool, the image is first labeled. This leads to potentially different behavior for bool and 0-and-1 arrays. Parameters ---------- ar : ndarray (arbitrary shape, int or bool type) The array containing the objects of interest. If the array type is int, the ints must be non-negative. min_size : int, optional (default: 64) The smallest allowable object size. connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1) The connectivity defining the neighborhood of a pixel. Used during labelling if `ar` is bool. in_place : bool, optional (default: False) If ``True``, remove the objects in the input array itself. Otherwise, make a copy. Raises ------ TypeError If the input array is of an invalid type, such as float or string. ValueError If the input array contains negative values. Returns ------- out : ndarray, same shape and type as input `ar` The input array with small connected components removed. Examples -------- >>> import cupy as cp >>> from cucim.skimage import morphology >>> a = cp.array([[0, 0, 0, 1, 0], ... [1, 1, 1, 0, 0], ... [1, 1, 1, 0, 1]], bool) >>> b = morphology.remove_small_objects(a, 6) >>> b array([[False, False, False, False, False], [ True, True, True, False, False], [ True, True, True, False, False]]) >>> c = morphology.remove_small_objects(a, 7, connectivity=2) >>> c array([[False, False, False, True, False], [ True, True, True, False, False], [ True, True, True, False, False]]) >>> d = morphology.remove_small_objects(a, 6, in_place=True) >>> d is a True """ # Raising type error if not int or bool _check_dtype_supported(ar) if in_place: out = ar else: out = ar.copy() if min_size == 0: # shortcut for efficiency return out if out.dtype == bool: selem = ndi.generate_binary_structure(ar.ndim, connectivity) ccs = cp.zeros_like(ar, dtype=cp.int32) ndi.label(ar, selem, output=ccs) else: ccs = out try: component_sizes = cp.bincount(ccs.ravel()) except ValueError: raise ValueError("Negative value labels are not supported. Try " "relabeling the input with `scipy.ndimage.label` or " "`skimage.morphology.label`.") if len(component_sizes) == 2 and out.dtype != bool: warn("Only one label was provided to `remove_small_objects`. " "Did you mean to use a boolean array?") too_small = component_sizes < min_size too_small_mask = too_small[ccs] out[too_small_mask] = 0 return out
def median(image, selem=None, out=None, mode='nearest', cval=0.0, behavior='ndimage'): """Return local median of an image. Parameters ---------- image : array-like Input image. selem : ndarray, optional If ``behavior=='rank'``, ``selem`` is a 2-D array of 1's and 0's. If ``behavior=='ndimage'``, ``selem`` is a N-D array of 1's and 0's with the same number of dimension than ``image``. If None, ``selem`` will be a N-D array with 3 elements for each dimension (e.g., vector, square, cube, etc.) out : ndarray, (same dtype as image), optional If None, a new array is allocated. mode : {'reflect', 'constant', 'nearest', 'mirror','‘wrap'}, optional The mode parameter determines how the array borders are handled, where ``cval`` is the value when mode is equal to 'constant'. Default is 'nearest'. .. versionadded:: 0.15 ``mode`` is used when ``behavior='ndimage'``. cval : scalar, optional Value to fill past edges of input if mode is 'constant'. Default is 0.0 .. versionadded:: 0.15 ``cval`` was added in 0.15 is used when ``behavior='ndimage'``. behavior : {'ndimage', 'rank'}, optional Either to use the old behavior (i.e., < 0.15) or the new behavior. The old behavior will call the :func:`skimage.filters.rank.median`. The new behavior will call the :func:`scipy.ndimage.median_filter`. Default is 'ndimage'. .. versionadded:: 0.15 ``behavior`` is introduced in 0.15 .. versionchanged:: 0.16 Default ``behavior`` has been changed from 'rank' to 'ndimage' Returns ------- out : 2-D array (same dtype as input image) Output image. See also -------- skimage.filters.rank.median : Rank-based implementation of the median filtering offering more flexibility with additional parameters but dedicated for unsigned integer images. Examples -------- >>> import cupy as cp >>> from skimage import data >>> from cucim.skimage.morphology import disk >>> from cucim.skimage.filters import median >>> img = cp.array(data.camera()) >>> med = median(img, disk(5)) """ if behavior == 'rank': if mode != 'nearest' or not np.isclose(cval, 0.0): warn("Change 'behavior' to 'ndimage' if you want to use the " "parameters 'mode' or 'cval'. They will be discarded " "otherwise.") raise NotImplementedError("rank behavior not currently implemented") # TODO: implement median rank filter # return generic.median(image, selem=selem, out=out) if selem is None: selem = ndi.generate_binary_structure(image.ndim, image.ndim) return ndi.median_filter(image, footprint=selem, output=out, mode=mode, cval=cval)
def median_otsu(input_volume, vol_idx=None, median_radius=4, numpass=4, autocrop=False, dilate=None): """Simple brain extraction tool method for images from DWI data. It uses a median filter smoothing of the input_volumes `vol_idx` and an automatic histogram Otsu thresholding technique, hence the name *median_otsu*. This function is inspired from Mrtrix's bet which has default values ``median_radius=3``, ``numpass=2``. However, from tests on multiple 1.5T and 3T data from GE, Philips, Siemens, the most robust choice is ``median_radius=4``, ``numpass=4``. Parameters ---------- input_volume : ndarray 3D or 4D array of the brain volume. vol_idx : None or array, optional. 1D array representing indices of ``axis=3`` of a 4D `input_volume`. None is only an acceptable input if ``input_volume`` is 3D. median_radius : int Radius (in voxels) of the applied median filter (default: 4). numpass: int Number of pass of the median filter (default: 4). autocrop: bool, optional if True, the masked input_volume will also be cropped using the bounding box defined by the masked data. Should be on if DWI is upsampled to 1x1x1 resolution. (default: False). dilate : None or int, optional number of iterations for binary dilation Returns ------- maskedvolume : ndarray Masked input_volume mask : 3D ndarray The binary brain mask Notes ----- Copyright (C) 2011, the scikit-image team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of skimage nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ if not _otsu_available: raise ImportError("cupyimg is required to use median_otsu") xp = get_array_module(input_volume, vol_idx) if len(input_volume.shape) == 4: if vol_idx is not None: b0vol = xp.mean(input_volume[..., xp.asarray(vol_idx)], axis=3) else: raise ValueError("For 4D images, must provide vol_idx input") else: b0vol = input_volume # Make a mask using a multiple pass median filter and histogram # thresholding. mask = multi_median(b0vol, median_radius, numpass) thresh = otsu(mask) mask = mask > thresh if dilate is not None: cross = generate_binary_structure(3, 1) # only brute_force iterations have been implemented in cupy kwargs = dict(brute_force=True) if xp == cp else {} mask = binary_dilation(mask, cross, iterations=dilate, **kwargs) # Auto crop the volumes using the mask as input_volume for bounding box # computing. if autocrop: mins, maxs = bounding_box(mask) mask = crop(mask, mins, maxs) croppedvolume = crop(input_volume, mins, maxs) maskedvolume = applymask(croppedvolume, mask) else: maskedvolume = applymask(input_volume, mask) return maskedvolume, mask