Module conduction.tools.flood_fill

Copyright 2017 Ben Mather

This file is part of Conduction https://git.dias.ie/itherc/conduction/

Conduction is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.

Conduction is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with Conduction. If not, see http://www.gnu.org/licenses/.

Expand source code
"""
Copyright 2017 Ben Mather

This file is part of Conduction <https://git.dias.ie/itherc/conduction/>

Conduction is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or any later version.

Conduction is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Conduction.  If not, see <http://www.gnu.org/licenses/>.
"""

import numpy as np
try: range = xrange
except: pass

def flood_fill(grid, spts):
    """
    Poisson disc sampler in two dimensions.
    This is a flood-fill algorithm for generating points that are
    separated by a minimum radius.

    Arguments
    ---------
     minX, maxX, minY, maxY : float
        coordinates of the domain
     spacing : float
        constant radius to sample across the domain
        every point is guaranteed to be no less than this distance
        from each other
     k : int (default: k=30)
        number of random samples to generate around a single point
        30 generally gives good results
     r_grid : array of floats, optional, shape (height,width)
        support for variable radii
        radius is ignored if an array is given here
     cpts : array of floats, optional, shape (n,2)
        points that must be sampled; useful for irregular boundaries
     spts : array of floats, optional, shape (s,2)
        points used to seed the flood-fill algorithm,
        samples are generated outwards from these seed points

    Returns
    -------
     pts : array of floats, shape (N,2)
        x, y coordinates for each sample point
     cpts_mask : array of bools, shape (N,2)
        boolean array where new points are True and
        cpts are False

    Notes
    -----
     One should aim to sample around 10,000 points, much more than that
     and the algorithm slows rapidly.
    """


    def random_point_around(point, k=7):
        """ Generate neighbouring points """
        P[:] = point # fill with point

        closure = [-1, 0, 0, 1, 0, 1, 1]
        for i in range(k):
            r = closure[i]
            c = closure[-1+i]
            d = closure[-2+i]

            P[i] += [r, c, d]

        return P

    def in_neighbourhood(point):
        """ Checks if point is in the neighbourhood """
        i, j, k = point
        if M[i,j,k]:
            return True
        return False

    def in_limits(point):
        """ Returns True if point is within box """
        return 0 <= point[0] < width and 0 <= point[1] < height and 0 <= point[2] < depth

    def add_point(point, index):
        """ Append point to the points list """
        points.append(point)

        i, j, k = point
        M[i,j,k] = True
        grid[i,j,k] = index


    # Size of the domain
    dim = len(grid.shape)
    n = np.prod(grid.shape)
    width, height, depth = grid.shape


    # Position cells
    P = np.empty((7, dim), dtype=np.int32)
    M = np.zeros((width, height, depth), dtype=bool)
    i, j, k = np.nonzero(grid)
    M[i,j,k] = True


    # Add seed points
    points = []
    if spts is not None:
        spts = spts.reshape(-1,dim)
        for index, pt in enumerate(spts):
            add_point(tuple(pt), index+1)
    else:
        # add a random initial point
        add_point((np.random.uniform(0, width),\
                   np.random.uniform(0, height),\
                   np.random.uniform(0, depth)),\
                   index=1)


    length = len(points)
    while length:
        i = np.random.randint(0,length)
        pt = points.pop(i)
        i, j, k = pt
        index = grid[i,j,k]

        qt = random_point_around(pt)
        for q in qt:
            if in_limits(q) and not in_neighbourhood(q):
                add_point(q, index)

        # re-evaluate length
        length = len(points)

    return grid

Functions

def flood_fill(grid, spts)

Poisson disc sampler in two dimensions. This is a flood-fill algorithm for generating points that are separated by a minimum radius.

Arguments

minX, maxX, minY, maxY : float coordinates of the domain spacing : float constant radius to sample across the domain every point is guaranteed to be no less than this distance from each other k : int (default: k=30) number of random samples to generate around a single point 30 generally gives good results r_grid : array of floats, optional, shape (height,width) support for variable radii radius is ignored if an array is given here cpts : array of floats, optional, shape (n,2) points that must be sampled; useful for irregular boundaries spts : array of floats, optional, shape (s,2) points used to seed the flood-fill algorithm, samples are generated outwards from these seed points

Returns

pts : array of floats, shape (N,2) x, y coordinates for each sample point cpts_mask : array of bools, shape (N,2) boolean array where new points are True and cpts are False

Notes

One should aim to sample around 10,000 points, much more than that and the algorithm slows rapidly.

Expand source code
def flood_fill(grid, spts):
    """
    Poisson disc sampler in two dimensions.
    This is a flood-fill algorithm for generating points that are
    separated by a minimum radius.

    Arguments
    ---------
     minX, maxX, minY, maxY : float
        coordinates of the domain
     spacing : float
        constant radius to sample across the domain
        every point is guaranteed to be no less than this distance
        from each other
     k : int (default: k=30)
        number of random samples to generate around a single point
        30 generally gives good results
     r_grid : array of floats, optional, shape (height,width)
        support for variable radii
        radius is ignored if an array is given here
     cpts : array of floats, optional, shape (n,2)
        points that must be sampled; useful for irregular boundaries
     spts : array of floats, optional, shape (s,2)
        points used to seed the flood-fill algorithm,
        samples are generated outwards from these seed points

    Returns
    -------
     pts : array of floats, shape (N,2)
        x, y coordinates for each sample point
     cpts_mask : array of bools, shape (N,2)
        boolean array where new points are True and
        cpts are False

    Notes
    -----
     One should aim to sample around 10,000 points, much more than that
     and the algorithm slows rapidly.
    """


    def random_point_around(point, k=7):
        """ Generate neighbouring points """
        P[:] = point # fill with point

        closure = [-1, 0, 0, 1, 0, 1, 1]
        for i in range(k):
            r = closure[i]
            c = closure[-1+i]
            d = closure[-2+i]

            P[i] += [r, c, d]

        return P

    def in_neighbourhood(point):
        """ Checks if point is in the neighbourhood """
        i, j, k = point
        if M[i,j,k]:
            return True
        return False

    def in_limits(point):
        """ Returns True if point is within box """
        return 0 <= point[0] < width and 0 <= point[1] < height and 0 <= point[2] < depth

    def add_point(point, index):
        """ Append point to the points list """
        points.append(point)

        i, j, k = point
        M[i,j,k] = True
        grid[i,j,k] = index


    # Size of the domain
    dim = len(grid.shape)
    n = np.prod(grid.shape)
    width, height, depth = grid.shape


    # Position cells
    P = np.empty((7, dim), dtype=np.int32)
    M = np.zeros((width, height, depth), dtype=bool)
    i, j, k = np.nonzero(grid)
    M[i,j,k] = True


    # Add seed points
    points = []
    if spts is not None:
        spts = spts.reshape(-1,dim)
        for index, pt in enumerate(spts):
            add_point(tuple(pt), index+1)
    else:
        # add a random initial point
        add_point((np.random.uniform(0, width),\
                   np.random.uniform(0, height),\
                   np.random.uniform(0, depth)),\
                   index=1)


    length = len(points)
    while length:
        i = np.random.randint(0,length)
        pt = points.pop(i)
        i, j, k = pt
        index = grid[i,j,k]

        qt = random_point_around(pt)
        for q in qt:
            if in_limits(q) and not in_neighbourhood(q):
                add_point(q, index)

        # re-evaluate length
        length = len(points)

    return grid