def binary_dilation(image, selem=None, out=None): """Return fast binary morphological dilation of an image. This function returns the same result as greyscale dilation but performs faster for binary images. Morphological dilation sets a pixel at ``(i,j)`` to the maximum over all pixels in the neighborhood centered at ``(i,j)``. Dilation enlarges bright regions and shrinks dark regions. Parameters ---------- image : ndarray Binary input image. selem : ndarray, optional The neighborhood expressed as a 2-D array of 1's and 0's. If None, use a cross-shaped structuring element (connectivity=1). out : ndarray of bool, optional The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- dilated : ndarray of bool or uint The result of the morphological dilation with values in ``[False, True]``. """ if out is None: out = cp.empty(image.shape, dtype=cp.bool_) ndi.binary_dilation(image, structure=selem, output=out) return out
def test_01_01_circle(self): """Test that the Canny filter finds the outlines of a circle""" i, j = cp.mgrid[-200:200, -200:200].astype(float) / 200 c = cp.abs(cp.sqrt(i * i + j * j) - 0.5) < 0.02 result = feature.canny(c.astype(float), 4, 0, 0, cp.ones(c.shape, bool)) # # erode and dilate the circle to get rings that should contain the # outlines # # TODO: grlee77: only implemented brute_force=True, so added that to # these tests cd = binary_dilation(c, iterations=3, brute_force=True) ce = binary_erosion(c, iterations=3, brute_force=True) cde = cp.logical_and(cd, cp.logical_not(ce)) self.assertTrue(cp.all(cde[result])) # # The circle has a radius of 100. There are two rings here, one # for the inside edge and one for the outside. So that's # 100 * 2 * 2 * 3 for those places where pi is still 3. # The edge contains both pixels if there's a tie, so we # bump the count a little. point_count = cp.sum(result) self.assertTrue(point_count > 1200) self.assertTrue(point_count < 1600)
def inf_sup(u): """IS operator.""" if cnp.ndim(u) == 2: P = _P2 elif cnp.ndim(u) == 3: P = _P3 else: raise ValueError("u has an invalid number of dimensions " "(should be 2 or 3)") dilations = [] for P_i in P: d = ndi.binary_dilation(u, cp.asarray(P_i)).astype(np.int8, copy=False) dilations.append(d) return cp.stack(dilations, axis=0).min(0)
def test_01_02_circle_with_noise(self): """Test that the Canny filter finds the circle outlines in a noisy image""" cp.random.seed(0) i, j = cp.mgrid[-200:200, -200:200].astype(float) / 200 c = cp.abs(cp.sqrt(i * i + j * j) - 0.5) < 0.02 cf = c.astype(float) * 0.5 + cp.random.uniform(size=c.shape) * 0.5 result = F.canny(cf, 4, 0.1, 0.2, cp.ones(c.shape, bool)) # # erode and dilate the circle to get rings that should contain the # outlines # cd = binary_dilation(c, iterations=4, brute_force=True) ce = binary_erosion(c, iterations=4, brute_force=True) cde = cp.logical_and(cd, cp.logical_not(ce)) self.assertTrue(cp.all(cde[result])) point_count = cp.sum(result) self.assertTrue(point_count > 1200) self.assertTrue(point_count < 1600)
def test_mask(): 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 morphological_geodesic_active_contour( gimage, iterations, init_level_set="circle", smoothing=1, threshold="auto", balloon=0, iter_callback=lambda x: None, ): """Morphological Geodesic Active Contours (MorphGAC). Geodesic active contours implemented with morphological operators. It can be used to segment objects with visible but noisy, cluttered, broken borders. Parameters ---------- gimage : (M, N) or (L, M, N) array Preprocessed image or volume to be segmented. This is very rarely the original image. Instead, this is usually a preprocessed version of the original image that enhances and highlights the borders (or other structures) of the object to segment. `morphological_geodesic_active_contour` will try to stop the contour evolution in areas where `gimage` is small. See `morphsnakes.inverse_gaussian_gradient` as an example function to perform this preprocessing. Note that the quality of `morphological_geodesic_active_contour` might greatly depend on this preprocessing. iterations : uint Number of iterations to run. init_level_set : str, (M, N) array, or (L, M, N) array Initial level set. If an array is given, it will be binarized and used as the initial level set. If a string is given, it defines the method to generate a reasonable initial level set with the shape of the `image`. Accepted values are 'checkerboard' and 'circle'. See the documentation of `checkerboard_level_set` and `circle_level_set` respectively for details about how these level sets are created. smoothing : uint, optional Number of times the smoothing operator is applied per iteration. Reasonable values are around 1-4. Larger values lead to smoother segmentations. threshold : float, optional Areas of the image with a value smaller than this threshold will be considered borders. The evolution of the contour will stop in this areas. balloon : float, optional Balloon force to guide the contour in non-informative areas of the image, i.e., areas where the gradient of the image is too small to push the contour towards a border. A negative value will shrink the contour, while a positive value will expand the contour in these areas. Setting this to zero will disable the balloon force. iter_callback : function, optional If given, this function is called once per iteration with the current level set as the only argument. This is useful for debugging or for plotting intermediate results during the evolution. Returns ------- out : (M, N) or (L, M, N) array Final segmentation (i.e., the final level set) See Also -------- inverse_gaussian_gradient, circle_level_set, checkerboard_level_set Notes ----- This is a version of the Geodesic Active Contours (GAC) algorithm that uses morphological operators instead of solving partial differential equations (PDEs) for the evolution of the contour. The set of morphological operators used in this algorithm are proved to be infinitesimally equivalent to the GAC PDEs (see [1]_). However, morphological operators are do not suffer from the numerical stability issues typically found in PDEs (e.g., it is not necessary to find the right time step for the evolution), and are computationally faster. The algorithm and its theoretical derivation are described in [1]_. References ---------- .. [1] A Morphological Approach to Curvature-based Evolution of Curves and Surfaces, Pablo Márquez-Neila, Luis Baumela, Luis Álvarez. In IEEE Transactions on Pattern Analysis and Machine Intelligence (PAMI), 2014, :DOI:`10.1109/TPAMI.2013.106` """ image = gimage init_level_set = _init_level_set(init_level_set, image.shape) _check_input(image, init_level_set) if threshold == "auto": threshold = cp.percentile(image, 40) structure = cp.ones((3, ) * len(image.shape), dtype=cp.int8) dimage = cnp.gradient(image) # threshold_mask = image > threshold if balloon != 0: threshold_mask_balloon = image > threshold / cp.abs(balloon) u = (init_level_set > 0).astype(cp.int8) iter_callback(u) for _ in range(iterations): # Balloon if balloon > 0: aux = ndi.binary_dilation(u, structure) elif balloon < 0: aux = ndi.binary_erosion(u, structure) if balloon != 0: u[threshold_mask_balloon] = aux[threshold_mask_balloon] # Image attachment aux = cp.zeros_like(image) du = cnp.gradient(u) for el1, el2 in zip(dimage, du): aux += el1 * el2 u[aux > 0] = 1 u[aux < 0] = 0 # Smoothing for _ in range(smoothing): u = _curvop(u) iter_callback(u) return u
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. """ 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