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://mcmap.net/q/299968/-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
numpy.array(img)
conversion nicely for bivalue images. The top of the array contains 8 small subimages 1 / 8th the expected mask size, with the remaining 7 / 8ths of the array filled with garbage. Perhaps the conversion doesn't unpack the binary data properly? – Travistravus