示例#1
0
def overlapping_spheres(shape: List[int], radius: int, porosity: float,
                        iter_max: int = 10, tol: float = 0.01):
    r"""
    Generate a packing of overlapping mono-disperse spheres

    Parameters
    ----------
    shape : list
        The size of the image to generate in [Nx, Ny, Nz] where Ni is the
        number of voxels in the i-th direction.

    radius : scalar
        The radius of spheres in the packing.

    porosity : scalar
        The porosity of the final image, accurate to the given tolerance.

    iter_max : int
        Maximum number of iterations for the iterative algorithm that improves
        the porosity of the final image to match the given value.

    tol : float
        Tolerance for porosity of the final image compared to the given value.

    Returns
    -------
    image : ND-array
        A boolean array with ``True`` values denoting the pore space

    Notes
    -----
    This method can also be used to generate a dispersion of hollows by
    treating ``porosity`` as solid volume fraction and inverting the
    returned image.

    """
    shape = sp.array(shape)
    if sp.size(shape) == 1:
        shape = sp.full((3, ), int(shape))
    ndim = (shape != 1).sum()
    s_vol = ps_disk(radius).sum() if ndim == 2 else ps_ball(radius).sum()

    bulk_vol = sp.prod(shape)
    N = int(sp.ceil((1 - porosity)*bulk_vol/s_vol))
    im = sp.random.random(size=shape)

    # Helper functions for calculating porosity: phi = g(f(N))
    f = lambda N: spim.distance_transform_edt(im > N/bulk_vol) < radius
    g = lambda im: 1 - im.sum() / sp.prod(shape)

    # # Newton's method for getting image porosity match the given
    # w = 1.0                         # Damping factor
    # dN = 5 if ndim == 2 else 25     # Perturbation
    # for i in range(iter_max):
    #     err = g(f(N)) - porosity
    #     d_err = (g(f(N+dN)) - g(f(N))) / dN
    #     if d_err == 0:
    #         break
    #     if abs(err) <= tol:
    #         break
    #     N2 = N - int(err/d_err)   # xnew = xold - f/df
    #     N = w * N2 + (1-w) * N

    # Bisection search: N is always undershoot (bc. of overlaps)
    N_low, N_high = N, 4*N
    for i in range(iter_max):
        N = sp.mean([N_high, N_low], dtype=int)
        err = g(f(N)) - porosity
        if err > 0:
            N_low = N
        else:
            N_high = N
        if abs(err) <= tol:
            break

    return ~f(N)
示例#2
0
def RSA(im: array, radius: int, volume_fraction: int = 1,
        mode: str = 'extended'):
    r"""
    Generates a sphere or disk packing using Random Sequential Addition

    This which ensures that spheres do not overlap but does not guarantee they
    are tightly packed.

    Parameters
    ----------
    im : ND-array
        The image into which the spheres should be inserted.  By accepting an
        image rather than a shape, it allows users to insert spheres into an
        already existing image.  To begin the process, start with an array of
        zero such as ``im = np.zeros([200, 200], dtype=bool)``.
    radius : int
        The radius of the disk or sphere to insert.
    volume_fraction : scalar
        The fraction of the image that should be filled with spheres.  The
        spheres are addeds 1's, so each sphere addition increases the
        ``volume_fraction`` until the specified limit is reach.
    mode : string
        Controls how the edges of the image are handled.  Options are:

        'extended' - Spheres are allowed to extend beyond the edge of the image

        'contained' - Spheres are all completely within the image

        'periodic' - The portion of a sphere that extends beyond the image is
        inserted into the opposite edge of the image (Not Implemented Yet!)

    Returns
    -------
    image : ND-array
        A copy of ``im`` with spheres of specified radius *added* to the
        background.

    Notes
    -----
    Each sphere is filled with 1's, but the center is marked with a 2.  This
    allows easy boolean masking to extract only the centers, which can be
    converted to coordinates using ``scipy.where`` and used for other purposes.
    The obtain only the spheres, use``im = im == 1``.

    This function adds spheres to the background of the received ``im``, which
    allows iteratively adding spheres of different radii to the unfilled space.

    References
    ----------
    [1] Random Heterogeneous Materials, S. Torquato (2001)

    """
    # Note: The 2D vs 3D splitting of this just me being lazy...I can't be
    # bothered to figure it out programmatically right now
    # TODO: Ideally the spheres should be added periodically
    print(78*'―')
    print('RSA: Adding spheres of size ' + str(radius))
    d2 = len(im.shape) == 2
    mrad = 2*radius
    if d2:
        im_strel = ps_disk(radius)
        mask_strel = ps_disk(mrad)
    else:
        im_strel = ps_ball(radius)
        mask_strel = ps_ball(mrad)
    if sp.any(im > 0):
        # Dilate existing objects by im_strel to remove pixels near them
        # from consideration for sphere placement
        mask = ps.tools.fftmorphology(im > 0, im_strel > 0, mode='dilate')
        mask = mask.astype(int)
    else:
        mask = sp.zeros_like(im)
    if mode == 'contained':
        mask = _remove_edge(mask, radius)
    elif mode == 'extended':
        pass
    elif mode == 'periodic':
        raise Exception('Periodic edges are not implemented yet')
    else:
        raise Exception('Unrecognized mode: ' + mode)
    vf = im.sum()/im.size
    free_spots = sp.argwhere(mask == 0)
    i = 0
    while vf <= volume_fraction and len(free_spots) > 0:
        choice = sp.random.randint(0, len(free_spots), size=1)
        if d2:
            [x, y] = free_spots[choice].flatten()
            im = _fit_strel_to_im_2d(im, im_strel, radius, x, y)
            mask = _fit_strel_to_im_2d(mask, mask_strel, mrad, x, y)
            im[x, y] = 2
        else:
            [x, y, z] = free_spots[choice].flatten()
            im = _fit_strel_to_im_3d(im, im_strel, radius, x, y, z)
            mask = _fit_strel_to_im_3d(mask, mask_strel, mrad, x, y, z)
            im[x, y, z] = 2
        free_spots = sp.argwhere(mask == 0)
        vf = im.sum()/im.size
        i += 1
    if vf > volume_fraction:
        print('Volume Fraction', volume_fraction, 'reached')
    if len(free_spots) == 0:
        print('No more free spots', 'Volume Fraction', vf)
    return im
示例#3
0
def RSA(im: array, radius: int, volume_fraction: int = 1, n_max: int = None,
        mode: str = 'contained'):
    r"""
    Generates a sphere or disk packing using Random Sequential Addition

    This algorithm ensures that spheres do not overlap but does not
    guarantee they are tightly packed.

    This function adds spheres to the background of the received ``im``, which
    allows iteratively adding spheres of different radii to the unfilled space,
    be repeatedly passing in the result of previous calls to RSA.

    Parameters
    ----------
    im : ND-array
        The image into which the spheres should be inserted.  By accepting an
        image rather than a shape, it allows users to insert spheres into an
        already existing image.  To begin the process, start with an array of
        zeros such as ``im = np.zeros([200, 200, 200], dtype=bool)``.
    radius : int
        The radius of the disk or sphere to insert.
    volume_fraction : scalar (default is 1.0)
        The fraction of the image that should be filled with spheres.  The
        spheres are added as 1's, so each sphere addition increases the
        ``volume_fraction`` until the specified limit is reach.  Note that if
        ``n_max`` is reached first, then ``volume_fraction`` will not be
        acheived.
    n_max : int (default is 10,000)
        The maximum number of spheres to add.  By default the value of
        ``n_max`` is high so that the addition of spheres will go indefinately
        until ``volume_fraction`` is met, but specifying a smaller value
        will halt addition after the given number of spheres are added.
    mode : string (default is 'contained')
        Controls how the edges of the image are handled.  Options are:

        'contained' - Spheres are all completely within the image

        'extended' - Spheres are allowed to extend beyond the edge of the
        image.  In this mode the volume fraction will be less that requested
        since some spheres extend beyond the image, but their entire volume
        is counted as added for computational efficiency.

    Returns
    -------
    image : ND-array
        A handle to the input ``im`` with spheres of specified radius
        *added* to the background.

    Notes
    -----
    This function uses Numba to speed up the search for valid sphere insertion
    points.  It seems that Numba does not look at the state of the scipy
    random number generator, so setting the seed to a known value has no
    effect on the output of this function. Each call to this function will
    produce a unique image.  If you wish to use the same realization multiple
    times you must save the array (e.g. ``numpy.save``).

    References
    ----------
    [1] Random Heterogeneous Materials, S. Torquato (2001)

    """
    print(80*'-')
    print(f'RSA: Adding spheres of size {radius}')
    im = im.astype(bool)
    if n_max is None:
        n_max = 10000
    vf_final = volume_fraction
    vf_start = im.sum()/im.size
    print('Initial volume fraction:', vf_start)
    if im.ndim == 2:
        template_lg = ps_disk(radius*2)
        template_sm = ps_disk(radius)
    else:
        template_lg = ps_ball(radius*2)
        template_sm = ps_ball(radius)
    vf_template = template_sm.sum()/im.size
    # Pad image by the radius of large template to enable insertion near edges
    im = np.pad(im, pad_width=2*radius, mode='edge')
    # Depending on mode, adjust mask to remove options around edge
    if mode == 'contained':
        border = get_border(im.shape, thickness=2*radius, mode='faces')
    elif mode == 'extended':
        border = get_border(im.shape, thickness=radius+1, mode='faces')
    else:
        raise Exception('Unrecognized mode: ', mode)
    # Remove border pixels
    im[border] = True
    # Dilate existing objects by strel to remove pixels near them
    # from consideration for sphere placement
    print('Dilating foreground features by sphere radius')
    dt = edt(im == 0)
    options_im = (dt >= radius)
    # ------------------------------------------------------------------------
    # Begin inserting the spheres
    vf = vf_start
    free_sites = np.flatnonzero(options_im)
    i = 0
    while (vf <= vf_final) and (i < n_max) and (len(free_sites) > 0):
        c, count = _make_choice(options_im, free_sites=free_sites)
        # The 100 below is arbitrary and may change performance
        if count > 100:
            # Regenerate list of free_sites
            print('Regenerating free_sites after', i, 'iterations')
            free_sites = np.flatnonzero(options_im)
        if all(np.array(c) == -1):
            break
        s_sm = tuple([slice(x - radius, x + radius + 1, None) for x in c])
        s_lg = tuple([slice(x - 2*radius, x + 2*radius + 1, None) for x in c])
        im[s_sm] += template_sm  # Add ball to image
        options_im[s_lg][template_lg] = False  # Update extended region
        vf += vf_template
        i += 1
    print('Number of spheres inserted:', i)
    # ------------------------------------------------------------------------
    # Get slice into returned image to retain original size
    s = tuple([slice(2*radius, d-2*radius, None) for d in im.shape])
    im = im[s]
    vf = im.sum()/im.size
    print('Final volume fraction:', vf)
    return im