枕头:如何渐变填充绘制的形状?

Pillow: How to gradient fill drawn shapes?

我已经研究了很多,我发现的只是如何将渐变应用于 Pillow 生成的文本。但是,我想知道如何将渐变而不是常规的单一颜色填充应用于绘制的形状(特别是多边形)。

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

我仍然不确定我是否完全理解你的问题。但听起来你想要一堆具有自己渐变的形状?一种方法是分别生成每个形状的梯度,然后在事后组合这些形状。

借用@HansHirse 已经提到的答案,你可以这样做:

from PIL import Image, ImageDraw


def channel(i, c, size, startFill, stopFill):
    """calculate the value of a single color channel for a single pixel"""
    return startFill[c] + int((i * 1.0 / size) * (stopFill[c] - startFill[c]))


def color(i, size, startFill, stopFill):
    """calculate the RGB value of a single pixel"""
    return tuple([channel(i, c, size, startFill, stopFill) for c in range(3)])


def round_corner(radius):
    """Draw a round corner"""
    corner = Image.new("RGBA", (radius, radius), (0, 0, 0, 0))
    draw = ImageDraw.Draw(corner)
    draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270, fill="blue")
    return corner


def apply_grad_to_corner(corner, gradient, backwards=False, topBottom=False):
    width, height = corner.size
    widthIter = range(width)

    if backwards:
        widthIter = reversed(widthIter)

    for i in range(height):
        gradPos = 0
        for j in widthIter:
            if topBottom:
                pos = (i, j)
            else:
                pos = (j, i)
            pix = corner.getpixel(pos)
            gradPos += 1
            if pix[3] != 0:
                corner.putpixel(pos, gradient[gradPos])

    return corner


def round_rectangle(size, radius, startFill, stopFill, runTopBottom=False):
    """Draw a rounded rectangle"""
    width, height = size
    rectangle = Image.new("RGBA", size)

    if runTopBottom:
        si = height
    else:
        si = width

    gradient = [color(i, width, startFill, stopFill) for i in range(si)]

    if runTopBottom:
        modGrad = []
        for i in range(height):
            modGrad += [gradient[i]] * width
        rectangle.putdata(modGrad)
    else:
        rectangle.putdata(gradient * height)

    origCorner = round_corner(radius)

    # upper left
    corner = origCorner
    apply_grad_to_corner(corner, gradient, False, runTopBottom)
    rectangle.paste(corner, (0, 0))

    # lower left
    if runTopBottom:
        gradient.reverse()
        backwards = True
    else:
        backwards = False

    corner = origCorner.rotate(90)
    apply_grad_to_corner(corner, gradient, backwards, runTopBottom)
    rectangle.paste(corner, (0, height - radius))

    # lower right
    if not runTopBottom:
        gradient.reverse()

    corner = origCorner.rotate(180)
    apply_grad_to_corner(corner, gradient, True, runTopBottom)
    rectangle.paste(corner, (width - radius, height - radius))

    # upper right
    if runTopBottom:
        gradient.reverse()
        backwards = False
    else:
        backwards = True

    corner = origCorner.rotate(270)
    apply_grad_to_corner(corner, gradient, backwards, runTopBottom)
    rectangle.paste(corner, (width - radius, 0))

    return rectangle


def get_concat_h(im1, im2):
    dst = Image.new("RGB", (im1.width + im2.width, im1.height))
    dst.paste(im1, (0, 0))
    dst.paste(im2, (im1.width, 0))
    return dst


def get_concat_v(im1, im2):
    dst = Image.new("RGB", (im1.width, im1.height + im2.height))
    dst.paste(im1, (0, 0))
    dst.paste(im2, (0, im1.height))
    return dst


img1 = round_rectangle((200, 200), 70, (255, 0, 0), (0, 255, 0), True)
img2 = round_rectangle((200, 200), 70, (0, 255, 0), (0, 0, 255), True)


get_concat_h(img1, img2).save("testcombo.png")

结果看起来像这样:

最后出现了唯一的“新”内容:图像只是组合在一起。如果你想变得狂野,你可以旋转单个形状或允许它们重叠(通过调整图像在 get_concat_h 中的位置 + 调整最终图像大小。)

这是我尝试创建的东西,可能适合您的用例。它的功能有限——具体来说,只支持线性和径向渐变——线性渐变本身也有一定的局限性。但是,首先,让我们看一个示例性输出:

基本上有两种方法

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

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

它们都得到一个 Pillow Image 对象 i,一个描述多边形的顶点列表 poly,渐变的开始和结束颜色 c1c2,以及描述线性渐变方向(从一个顶点到第二个顶点)的两个点 p1p2 或描述径向中心的单个点 p渐变.

在这两种方法中,初始多边形都绘制在最终图像大小的空白 canvas 上,仅使用 alpha 通道。

对于线性渐变,计算p1p2之间的角度。绘制的多边形按该角度旋转,并裁剪以获得适当线性渐变所需的尺寸。那个只是 np.linspace 创建的。梯度按已知角度旋转,但方向相反,最后平移以适应实际的多边形。将渐变图像粘贴到中间多边形图像上,得到具有线性渐变的多边形,并将结果粘贴到实际图像上。

线性渐变的限制:您最好选择“相对侧”的多边形的两个顶点,或者更好:这样多边形内的所有点都在由这两个点跨越的虚拟 space 内。否则,当前的实现可能会失败,例如选择两个相邻顶点时。

径向渐变方法略有不同。 p 到所有多边形顶点的最大距离被确定。然后,对于实际图像大小的中间图像中的所有点,计算到 p 的距离,并用计算出的最大距离归一化。对于多边形内的所有点,我们得到 [0.0 ... 1.0] 范围内的值。这些值用于计算范围从 c1c2 的适当颜色。至于线性渐变,就是把渐变图像贴在中间的多边形图像上,结果贴在实际图像上。

希望代码使用注释是不言自明的。但如果有问题,请不要犹豫!

完整代码如下:

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
----------------------------------------