枕头:如何渐变填充绘制的形状?
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
,渐变的开始和结束颜色 c1
和 c2
,以及描述线性渐变方向(从一个顶点到第二个顶点)的两个点 p1
和 p2
或描述径向中心的单个点 p
渐变.
在这两种方法中,初始多边形都绘制在最终图像大小的空白 canvas 上,仅使用 alpha 通道。
对于线性渐变,计算p1
和p2
之间的角度。绘制的多边形按该角度旋转,并裁剪以获得适当线性渐变所需的尺寸。那个只是 np.linspace
创建的。梯度按已知角度旋转,但方向相反,最后平移以适应实际的多边形。将渐变图像粘贴到中间多边形图像上,得到具有线性渐变的多边形,并将结果粘贴到实际图像上。
线性渐变的限制:您最好选择“相对侧”的多边形的两个顶点,或者更好:这样多边形内的所有点都在由这两个点跨越的虚拟 space 内。否则,当前的实现可能会失败,例如选择两个相邻顶点时。
径向渐变方法略有不同。 p
到所有多边形顶点的最大距离被确定。然后,对于实际图像大小的中间图像中的所有点,计算到 p
的距离,并用计算出的最大距离归一化。对于多边形内的所有点,我们得到 [0.0 ... 1.0]
范围内的值。这些值用于计算范围从 c1
到 c2
的适当颜色。至于线性渐变,就是把渐变图像贴在中间的多边形图像上,结果贴在实际图像上。
希望代码使用注释是不言自明的。但如果有问题,请不要犹豫!
完整代码如下:
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
----------------------------------------
我已经研究了很多,我发现的只是如何将渐变应用于 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
,渐变的开始和结束颜色 c1
和 c2
,以及描述线性渐变方向(从一个顶点到第二个顶点)的两个点 p1
和 p2
或描述径向中心的单个点 p
渐变.
在这两种方法中,初始多边形都绘制在最终图像大小的空白 canvas 上,仅使用 alpha 通道。
对于线性渐变,计算p1
和p2
之间的角度。绘制的多边形按该角度旋转,并裁剪以获得适当线性渐变所需的尺寸。那个只是 np.linspace
创建的。梯度按已知角度旋转,但方向相反,最后平移以适应实际的多边形。将渐变图像粘贴到中间多边形图像上,得到具有线性渐变的多边形,并将结果粘贴到实际图像上。
线性渐变的限制:您最好选择“相对侧”的多边形的两个顶点,或者更好:这样多边形内的所有点都在由这两个点跨越的虚拟 space 内。否则,当前的实现可能会失败,例如选择两个相邻顶点时。
径向渐变方法略有不同。 p
到所有多边形顶点的最大距离被确定。然后,对于实际图像大小的中间图像中的所有点,计算到 p
的距离,并用计算出的最大距离归一化。对于多边形内的所有点,我们得到 [0.0 ... 1.0]
范围内的值。这些值用于计算范围从 c1
到 c2
的适当颜色。至于线性渐变,就是把渐变图像贴在中间的多边形图像上,结果贴在实际图像上。
希望代码使用注释是不言自明的。但如果有问题,请不要犹豫!
完整代码如下:
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
----------------------------------------