I researched quite a bit already, and all that I found was how to apply gradients to text generated with Pillow. However, I wanted to know how can I apply a gradient instead of a regular single color fill to a drawn shape (specifically a polygon).

image = Image.new('RGBA', (50, 50)) draw = ImageDraw.Draw(image) draw.polygon([10, 10, 20, 40, 40, 20], fill=(255, 50, 210), outline=None)

## Answer

Here’s my attempt to create something, which might fit your use case. It has limited functionality – specifically, only supports linear and radial gradients – and the linear gradient itself is also kind of limited. But, first of all, let’s see an exemplary output:

Basically, there are two methods

linear_gradient(i, poly, p1, p2, c1, c2)

and

radial_gradient(i, poly, p, c1, c2)

which both get an Pillow `Image`

object `i`

, a list of vertices describing a polygon `poly`

, start and stop colors for the gradient `c1`

and `c2`

, and either two points `p1`

and `p2`

describing the direction of the linear gradient (from one vertex to a second one) or a single point `p`

describing the center of the radial gradient.

In both methods, the initial polygon is drawn on an empty canvas of the final image’s size, solely using the alpha channel.

For the linear gradient, the angle between `p1`

and `p2`

, is calculated. The drawn polygon is rotated by that angle, and cropped to get the needed dimensions for a proper linear gradient. That one is simply created by `np.linspace`

. The gradient is rotated by the known angle, but in the opposite direction, and finally translated to fit the actual polygon. The gradient image is pasted on the intermediate polygon image to get the polygon with the linear gradient, and the result is pasted onto the actual image.

Limitation for the linear gradient: You better pick two vertices of the polygon “on opposite sides”, or better: Such that all points inside the polygon are within the virtual space spanned by that two points. Otherwise, the current implementation might fail, e.g. when choosing two neighbouring vertices.

The radial gradient method works slightly different. The maximum distance from `p`

to all polygon vertices is determined. Then, for all points in an intermediate image of the actual image size, the distance to `p`

is calculated and normalized by the calculated maximum distance. We get values in the range `[0.0 ... 1.0]`

for all points inside the polygon. The values are used to calculate appropriate colors ranging from `c1`

to `c2`

. As for the linear gradient, that gradient image is pasted on the intermediate polygon image, and the result is pasted onto the actual image.

Hopefully, the code is self-explanatory using the comments. But if there are questions, please don’t hesitate to ask!

Here’s the full code:

import matplotlib.pyplot as plt import numpy as np from PIL import Image, ImageDraw # Draw polygon with linear gradient from point 1 to point 2 and ranging # from color 1 to color 2 on given image def linear_gradient(i, poly, p1, p2, c1, c2): # Draw initial polygon, alpha channel only, on an empty canvas of image size ii = Image.new('RGBA', i.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(ii) draw.polygon(poly, fill=(0, 0, 0, 255), outline=None) # Calculate angle between point 1 and 2 p1 = np.array(p1) p2 = np.array(p2) angle = np.arctan2(p2[1] - p1[1], p2[0] - p1[0]) / np.pi * 180 # Rotate and crop shape temp = ii.rotate(angle, expand=True) temp = temp.crop(temp.getbbox()) wt, ht = temp.size # Create gradient from color 1 to 2 of appropriate size gradient = np.linspace(c1, c2, wt, True).astype(np.uint8) gradient = np.tile(gradient, [2 * h, 1, 1]) gradient = Image.fromarray(gradient) # Paste gradient on blank canvas of sufficient size temp = Image.new('RGBA', (max(i.size[0], gradient.size[0]), max(i.size[1], gradient.size[1])), (0, 0, 0, 0)) temp.paste(gradient) gradient = temp # Rotate and translate gradient appropriately x = np.sin(angle * np.pi / 180) * ht y = np.cos(angle * np.pi / 180) * ht gradient = gradient.rotate(-angle, center=(0, 0), translate=(p1[0] + x, p1[1] - y)) # Paste gradient on temporary image ii.paste(gradient.crop((0, 0, ii.size[0], ii.size[1])), mask=ii) # Paste temporary image on actual image i.paste(ii, mask=ii) return i # Draw polygon with radial gradient from point to the polygon border # ranging from color 1 to color 2 on given image def radial_gradient(i, poly, p, c1, c2): # Draw initial polygon, alpha channel only, on an empty canvas of image size ii = Image.new('RGBA', i.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(ii) draw.polygon(poly, fill=(0, 0, 0, 255), outline=None) # Use polygon vertex with highest distance to given point as end of gradient p = np.array(p) max_dist = max([np.linalg.norm(np.array(v) - p) for v in poly]) # Calculate color values (gradient) for the whole canvas x, y = np.meshgrid(np.arange(i.size[0]), np.arange(i.size[1])) c = np.linalg.norm(np.stack((x, y), axis=2) - p, axis=2) / max_dist c = np.tile(np.expand_dims(c, axis=2), [1, 1, 3]) c = (c1 * (1 - c) + c2 * c).astype(np.uint8) c = Image.fromarray(c) # Paste gradient on temporary image ii.paste(c, mask=ii) # Paste temporary image on actual image i.paste(ii, mask=ii) return i # Create blank canvas with zero alpha channel w, h = (800, 600) image = Image.new('RGBA', (w, h), (0, 0, 0, 0)) # Draw first polygon with radial gradient polygon = [(100, 200), (320, 130), (460, 300), (700, 500), (350, 550), (200, 400)] point = (350, 350) color1 = (255, 0, 0) color2 = (0, 255, 0) image = radial_gradient(image, polygon, point, color1, color2) # Draw second polygon with linear gradient polygon = [(500, 50), (650, 250), (775, 150), (700, 25)] point1 = (700, 25) point2 = (650, 250) color1 = (255, 255, 0) color2 = (0, 0, 255) image = linear_gradient(image, polygon, point1, point2, color1, color2) # Draw third polygon with linear gradient polygon = [(50, 550), (200, 575), (200, 500), (100, 300), (25, 450)] point1 = (100, 300) point2 = (200, 575) color1 = (255, 255, 255) color2 = (255, 128, 0) image = linear_gradient(image, polygon, point1, point2, color1, color2) # Save image image.save('image.png')

---------------------------------------- System information ---------------------------------------- Platform: Windows-10-10.0.16299-SP0 Python: 3.9.1 Matplotlib: 3.4.0 NumPy: 1.20.2 Pillow: 8.1.2 ----------------------------------------