skip to Main Content

I want to draw a polygon as an outline and fill it with a specific color. This should be an easy enough task for PIL… Unfortunately the fill argument produces an image that bleeds out of the outline… Demonstration:

Step 1, lets show clearly what my outline should look like:

from PIL import Image, ImageDraw

vertices = [(44, 124),
            (50, 48),
            (74, 46),
            (73, 123),
            (44, 124)]
                  
# draw as an outline
image = Image.new('RGB', (150, 150), 'black')
draw = ImageDraw.Draw(image)
draw.line(vertices, fill='blue', width=1)
image.save(r'outline.png')

This produces the image below:

enter image description here

This is great, and i can confirm in Photoshop that my corners corresponds to the specified coordinates. Now step 2, lets do the same using the polygon function and draw a filled polygon:

# draw as a filled polygon
image = Image.new('RGB', (150, 150), 'black')
draw = ImageDraw.Draw(image)
draw.polygon(vertices, fill='white', outline='blue', width=1)
image.save(r'filled.png')

Notice the two pixels appearing on this shape? These white pixels are at (75, 46) and (43, 124) which are outside the boundary.

a filled polygon with weird pixels

This is not acceptable. The filled polygon should not rasterize beyond its outline. Here is the desired output I would have expected (mocked up in Photoshop):

desired output

QUESTION

At the end of the day, I want to write a function which will take n vertices to draw a clean polygon with a filled boundary the does not exceed the outline. Also, for technical reasons I cannot use matplotlib, which means I cannot use skimage.draw.polygon_perimeter because it needs matplotlib. Any other package (especially if leveraging numpy) would be best.

2

Answers


  1. Chosen as BEST ANSWER

    Here's my current solution: use scipy.ndimage.binary_fill_holes to fill the hole.

    import numpy as np
    from scipy import ndimage
    from PIL import Image, ImageDraw
    
    vertices = [(44, 124),
                (50, 48),
                (74, 46),
                (73, 123),
                (44, 124)]
                
    # draw the outline as a bitmap
    outline = Image.new('1', (150, 150))
    draw = ImageDraw.Draw(outline)
    draw.line(vertices, fill=1, width=1)
    outline = np.array(outline)
    
    # fill with ndimage.binary_fill_holes
    fill = ndimage.binary_fill_holes(outline)
    
    # remove outline from fill using a logical "and"
    fill = fill & ~outline
    
    # create the final image
    canvas = np.zeros((150,150,3), dtype=np.uint8)
    canvas[fill == True] = [255, 255, 255] # fill with white
    canvas[outline == True] = [0, 0, 255] # outline is blue
    
    final = Image.fromarray(canvas)
    final.save('final.png')
    

    Result:

    enter image description here

    This works for now, but I need to compare with ImageDraw for speed as i need to run this millions of times.


  2. The clear answer for speed: opencv-python

    import numpy as np
    import cv2
    
    # create a black canvas
    image = np.zeros((150,150,3), np.uint8)
    
    vertices = np.array([(44, 124),
                         (50, 48),
                         (74, 46),
                         (73, 123),
                         (44, 124)], np.int32)
    vertices = vertices.reshape((-1,1,2))
    
    # fill the polygon with white
    cv2.fillPoly(image, [vertices], (255, 255, 255)) 
    
    # draw the blue outline (opencv uses BGR instead of RGB)
    cv2.polylines(image, [vertices], True, (255,0,0), 1)
    
    cv2.imwrite("opencv.png", image)
    

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search