【问题标题】:Pillow: How to gradient fill drawn shapes?枕头:如何渐变填充绘制的形状?
【发布时间】:2021-03-28 00:46:59
【问题描述】:

我已经研究了很多,我发现的只是如何将渐变应用于使用 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)

【问题讨论】:

标签: python python-imaging-library gradient


【解决方案1】:

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

基本上有两种方法

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

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

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

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

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

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

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

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

这是完整的代码:

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

【讨论】:

  • 为什么要自己写渐变填充函数
  • @GoldenLion “为什么”是什么意思?我找不到使用 Pillow 内置函数来获得渐变填充形状的方法。因此,我试图找到一个相对较短的解决方案来获得所需的功能。如果您知道其他任何方法,甚至可能是 Pillow 内置方法,请分享!
  • 似乎应该有一个库来做渐变填充。您还在寻找解决方案吗?
  • 这正是我一直在寻找的方法,但在任何地方都找不到,非常感谢!
【解决方案2】:

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

支持@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 中图像的位置 + 调整最终图像大小。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-06-30
    • 1970-01-01
    • 2016-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多