3

I'm looking for a way to 'draw' a filled polygon into a numpy array based upon a set of polygon vertices. I'd prefer to use as few external libraries as possible.

For example: I have a 20x20 numpy array and I'd like the region bounded by points (3,12), (8,18), (13,14), (11,6) and (4,6) to be filled with 1.0 while the rest of the array contains 0.0

os1
  • 412
  • 1
  • 6
  • 18
  • Simon, see if this is a duplicate of your question (considering that the answer allows for an arbitrary polygon): http://stackoverflow.com/questions/12638790/drawing-a-rectangle-inside-a-2d-numpy-array – Warren Weckesser May 09 '16 at 14:16
  • 1
    @Will - not actually drawing or GUI at all. Just filling an array with ones and zeros. – Christian May 09 '16 at 14:16
  • [Here's one approach that uses matplotlib](http://stackoverflow.com/a/21567205/1461210) (specifically the part where I generate the `mask`) – ali_m May 09 '16 at 19:14
  • 4
    Take a look at [`skimage.draw.polygon()`](http://scikit-image.org/docs/dev/api/skimage.draw.html#skimage.draw.polygon). – Jaime May 09 '16 at 20:12

3 Answers3

8

The following solution requires only numpy. It works for vertices (defined in clockwise order in [Row, Column] coordinate system) for convex polygons. Concave polygons will work, but will end up cutting off the protruding points.

import numpy as np

def check(p1, p2, base_array):
    """
    Uses the line defined by p1 and p2 to check array of 
    input indices against interpolated value

    Returns boolean array, with True inside and False outside of shape
    """
    idxs = np.indices(base_array.shape) # Create 3D array of indices

    p1 = p1.astype(float)
    p2 = p2.astype(float)

    # Calculate max column idx for each row idx based on interpolated line between two points
    max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) +  p1[1]    
    sign = np.sign(p2[0] - p1[0])
    return idxs[1] * sign <= max_col_idx * sign

def create_polygon(shape, vertices):
    """
    Creates np.array with dimensions defined by shape
    Fills polygon defined by vertices with ones, all other values zero"""
    base_array = np.zeros(shape, dtype=float)  # Initialize your array of zeros

    fill = np.ones(base_array.shape) * True  # Initialize boolean array defining shape fill

    # Create check array for each edge segment, combine into fill array
    for k in range(vertices.shape[0]):
        fill = np.all([fill, check(vertices[k-1], vertices[k], base_array)], axis=0)

    # Set all values inside polygon to one
    base_array[fill] = 1

    return base_array


# (Row, Col) Vertices of Polygon (Defined Clockwise)
vertices = np.array([
    [5,12],
    [8,18],
    [13,14],
    [11,6],
    [4,6],
])

polygon_array = create_polygon([20,20], vertices)

# This section prints numbers at each vertex for visual check, just comment out 
# to print an array of only zeros and ones
for n, vertex in enumerate(vertices):
    polygon_array[vertex[0],vertex[1]] = 10*(n+1)

# Simple routine to print the final array
for row in polygon_array.tolist():
    for c in row:
        print '{:4.1f}'.format(c),
    print ''
schoolie
  • 452
  • 3
  • 9
  • Oh, thank you! I was struggling with cv2 polygons and this solution even faster! – Katerina Jun 17 '16 at 15:03
  • @ schoolie i tried this but doesn't work for me. the fill array in the create_polygon is all false. but i check the return value of check function. its not always false – Eshaka Feb 14 '19 at 02:15
4

I found one bug in @schoolie response and stackoverflow do not allow add multi line code snippets in comments the problem is when p1[0] == p2[0]. My suggestion of update in function check:

if p1[0] == p2[0]:
    max_col_idx = (idxs[0] - p1[0]) * idxs.shape[1]
    sign = np.sign(p2[1] - p1[1])
else:
    max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
    sign = np.sign(p2[0] - p1[0])
Grzegorz Bokota
  • 1,736
  • 11
  • 19
0

This example fromscikits-image can be applied to this problem I guess.

vertices = np.array([
    [5,12],
    [8,18],
    [13,14],
    [11,6],
    [4,6],
])

from skimage.draw import polygon

img = np.zeros((20, 20), 'uint8')
rr, cc = polygon(vertices[:,0], vertices[:,1], img.shape)
img[rr,cc] = 1

img.shape
(20, 20)

img.sum()
80
  • Great solution, thanks. Note that `polygon` expects the row coordinates as the first argument and the column coordinates as the second argument. Thus, if thinking about `x` and `y` values, these need to be transposed for this to work as expected. – Andrew Straw Oct 14 '22 at 07:36