SciPy Create 2D Polygon Mask

后端 未结 6 875
慢半拍i
慢半拍i 2020-11-29 19:07

I need to create a numpy 2D array which represents a binary mask of a polygon, using standard Python packages.

  • input: polygon vertices, image dimensions
  • <
相关标签:
6条回答
  • 2020-11-29 19:50

    The answer turns out to be quite simple:

    import numpy
    from PIL import Image, ImageDraw
    
    # polygon = [(x1,y1),(x2,y2),...] or [x1,y1,x2,y2,...]
    # width = ?
    # height = ?
    
    img = Image.new('L', (width, height), 0)
    ImageDraw.Draw(img).polygon(polygon, outline=1, fill=1)
    mask = numpy.array(img)
    
    0 讨论(0)
  • 2020-11-29 19:52

    As a slightly more direct alternative to @Anil's answer, matplotlib has matplotlib.nxutils.points_inside_poly that can be used to quickly rasterize an arbitrary polygon. E.g.

    import numpy as np
    from matplotlib.nxutils import points_inside_poly
    
    nx, ny = 10, 10
    poly_verts = [(1,1), (5,1), (5,9),(3,2),(1,1)]
    
    # Create vertex coordinates for each grid cell...
    # (<0,0> is at the top left of the grid in this system)
    x, y = np.meshgrid(np.arange(nx), np.arange(ny))
    x, y = x.flatten(), y.flatten()
    
    points = np.vstack((x,y)).T
    
    grid = points_inside_poly(points, poly_verts)
    grid = grid.reshape((ny,nx))
    
    print grid
    

    Which yields (a boolean numpy array):

    [[False False False False False False False False False False]
     [False  True  True  True  True False False False False False]
     [False False False  True  True False False False False False]
     [False False False False  True False False False False False]
     [False False False False  True False False False False False]
     [False False False False  True False False False False False]
     [False False False False False False False False False False]
     [False False False False False False False False False False]
     [False False False False False False False False False False]
     [False False False False False False False False False False]]
    

    You should be able to pass grid to any of the scipy.ndimage.morphology functions quite nicely.

    0 讨论(0)
  • 2020-11-29 19:54

    Here is a function that implements @IsaacSutherland method (the accepted answer) with some modifications I find useful. Comments are welcome!

    poly_mask() accepts multiple polygons as input so that the output mask can be made of multiple, eventually not connected, polygonal regions. Moreover, because in some cases 0 is not a good value for masking (e.g. if 0 is a valid value of the array to which the maskhas to be applied ) I added a value keyword that sets the actual masking value (e.g. a very small/big number or NAN): to achieve this the mask is converted to array of float.

    def poly_mask(shape, *vertices, value=np.nan):
    """
    Create a mask array filled with 1s inside the polygon and 0s outside.
    The polygon is a list of vertices defined as a sequence of (column, line) number, where the start values (0, 0) are in the
    upper left corner. Multiple polygon lists can be passed in input to have multiple,eventually not connected, ROIs.
        column, line   # x, y
        vertices = [(x0, y0), (x1, y1), ..., (xn, yn), (x0, y0)] or [x0, y0, x1, y1, ..., xn, yn, x0, y0]
    Note: the polygon can be open, that is it doesn't have to have x0,y0 as last element.
    
    adapted from: https://stackoverflow.com/questions/3654289/scipy-create-2d-polygon-mask/64876117#64876117
    :param shape:    (tuple) shape of the output array (height, width)
    :param vertices: (list of tuples of int): sequence of vertices defined as
                                               [(x0, y0), (x1, y1), ..., (xn, yn), (x0, y0)] or
                                               [x0, y0, x1, y1, ..., xn, yn, x0, y0]
                                               Multiple lists (for multiple polygons) can be passed in input
    :param value:    (float or NAN)      The masking value to use (e.g. a very small number). Default: np.nan
    :return:         (ndarray) the mask array
    """
    width, height = shape[::-1]
    # create a binary image
    img = Image.new(mode='L', size=(width, height), color=0)  # mode L = 8-bit pixels, black and white
    draw = ImageDraw.Draw(img)
    # draw polygons
    for polygon in vertices:
        draw.polygon(polygon, outline=1, fill=1)
    # replace 0 with 'value'
    mask = np.array(img).astype('float32')
    mask[np.where(mask == 0)] = value
    return mask
    

    Instead of (width, height) I prefer to have directly shape as input so that I can use it like this:

    polygon_lists = [
        [(x0, y0), (x1, y1), ..., (xn, yn), (x0, y0)],
        [# ... another sequence of coordinates...],
        [# ...yet another sequence of coordinates...],
                    ]
    my_mask = poly_mask(my_array.shape, *polygon_lists)
    

    where my_array is the array to which the mask has to be applied (or another array with the same shape, of course).

    my_array_masked = my_array * my_mask
    
    0 讨论(0)
  • 2020-11-29 20:03

    As a slight alternative to @Yusuke N.'s answer, consider using matplotlib.path, which is just as efficient as the one by from PIL import Image, ImageDraw(no need to install Pillow, no need to consider integer or float. Useful me?)

    Working code is below:

    import pylab as plt
    import numpy as np
    from matplotlib.path import Path
    
    width, height=2000, 2000
    
    polygon=[(0.1*width, 0.1*height), (0.15*width, 0.7*height), (0.8*width, 0.75*height), (0.72*width, 0.15*height)]
    poly_path=Path(polygon)
    
    x, y = np.mgrid[:height, :width]
    coors=np.hstack((x.reshape(-1, 1), y.reshape(-1,1))) # coors.shape is (4000000,2)
    
    mask = poly_path.contains_points(coors)
    plt.imshow(mask.reshape(height, width))
    plt.show()
    

    And the result image is below, where dark area is False, bright area is True.

    0 讨论(0)
  • 2020-11-29 20:07

    You could try to use python's Image Library, PIL. First you initialize the canvas. Then you create a drawing object, and you start making lines. This is assuming that the polygon resides in R^2 and that the vertex list for the input are in the correct order.

    Input = [(x1, y1), (x2, y2), ..., (xn, yn)] , (width, height)

    from PIL import Image, ImageDraw
    
    img = Image.new('L', (width, height), 0)   # The Zero is to Specify Background Color
    draw = ImageDraw.Draw(img)
    
    for vertex in range(len(vertexlist)):
        startpoint = vertexlist[vertex]
        try: endpoint = vertexlist[vertex+1]
        except IndexError: endpoint = vertexlist[0] 
        # The exception means We have reached the end and need to complete the polygon
        draw.line((startpoint[0], startpoint[1], endpoint[0], endpoint[1]), fill=1)
    
    # If you want the result as a single list
    # You can make a two dimensional list or dictionary by iterating over the height and width variable
    list(img.getdata())
    
    # If you want the result as an actual Image
    img.save('polgon.jpg', 'JPEG')
    

    Is this what you were looking for, or were you asking something different?

    0 讨论(0)
  • 2020-11-29 20:08

    An update on Joe's comment. Matplotlib API has changed since the comment was posted, and now you need to use a method provided by a submodule matplotlib.path.

    Working code is below.

    import numpy as np
    from matplotlib.path import Path
    
    nx, ny = 10, 10
    poly_verts = [(1,1), (5,1), (5,9),(3,2),(1,1)]
    
    # Create vertex coordinates for each grid cell...
    # (<0,0> is at the top left of the grid in this system)
    x, y = np.meshgrid(np.arange(nx), np.arange(ny))
    x, y = x.flatten(), y.flatten()
    
    points = np.vstack((x,y)).T
    
    path = Path(poly_verts)
    grid = path.contains_points(points)
    grid = grid.reshape((ny,nx))
    
    print grid
    
    0 讨论(0)
提交回复
热议问题