PIL:生成垂直渐变图像

6
在Android中,我使用以下代码生成所需的渐变背景:
<gradient
    android:angle="90"
    android:startColor="#40000000"
    android:endColor="#00000000"
    android:type="linear" />

背景从上到下逐渐变暗。我想知道如何在Python中使用PIL实现相同的效果,因为我需要在另一个用Python编写的程序中使用相同的效果。


"#40000000" 会得出什么 RGB 值? - martineau
@martineau 看起来 (160, 160, 160) 是最接近的。谢谢。 - J Freebird
垂直方向有多少个像素?如果使用Numpy或scipy,你会介意吗? - Back2Basics
从我刚刚找到的文档来看,这并没有太多意义。它看起来更像是一个ARGB值为(64,0,0,0)的东西。另外,你知道需要多大的图像吗? - martineau
@martineau 它是640*640。非常感谢! - J Freebird
@Back2Basics 对我来说,Numpy或Scipy都可以。谢谢。 - J Freebird
5个回答

12

这里有一些展示如何绘制多色矩形水平和垂直渐变的方法。

rom PIL import Image, ImageDraw

BLACK, DARKGRAY, GRAY = ((0,0,0), (63,63,63), (127,127,127))
LIGHTGRAY, WHITE = ((191,191,191), (255,255,255))
BLUE, GREEN, RED = ((0, 0, 255), (0, 255, 0), (255, 0, 0))


class Point(object):
    def __init__(self, x, y):
        self.x, self.y = x, y

class Rect(object):
    def __init__(self, x1, y1, x2, y2):
        minx, maxx = (x1,x2) if x1 < x2 else (x2,x1)
        miny, maxy = (y1,y2) if y1 < y2 else (y2,y1)
        self.min = Point(minx, miny)
        self.max = Point(maxx, maxy)

    width  = property(lambda self: self.max.x - self.min.x)
    height = property(lambda self: self.max.y - self.min.y)


def gradient_color(minval, maxval, val, color_palette):
    """ Computes intermediate RGB color of a value in the range of minval
        to maxval (inclusive) based on a color_palette representing the range.
    """
    max_index = len(color_palette)-1
    delta = maxval - minval
    if delta == 0:
        delta = 1
    v = float(val-minval) / delta * max_index
    i1, i2 = int(v), min(int(v)+1, max_index)
    (r1, g1, b1), (r2, g2, b2) = color_palette[i1], color_palette[i2]
    f = v - i1
    return int(r1 + f*(r2-r1)), int(g1 + f*(g2-g1)), int(b1 + f*(b2-b1))

def horz_gradient(draw, rect, color_func, color_palette):
    minval, maxval = 1, len(color_palette)
    delta = maxval - minval
    width = float(rect.width)  # Cache.
    for x in range(rect.min.x, rect.max.x+1):
        f = (x - rect.min.x) / width
        val = minval + f * delta
        color = color_func(minval, maxval, val, color_palette)
        draw.line([(x, rect.min.y), (x, rect.max.y)], fill=color)

def vert_gradient(draw, rect, color_func, color_palette):
    minval, maxval = 1, len(color_palette)
    delta = maxval - minval
    height = float(rect.height)  # Cache.
    for y in range(rect.min.y, rect.max.y+1):
        f = (y - rect.min.y) / height
        val = minval + f * delta
        color = color_func(minval, maxval, val, color_palette)
        draw.line([(rect.min.x, y), (rect.max.x, y)], fill=color)


if __name__ == '__main__':
    # Draw a three color vertical gradient.
    color_palette = [BLUE, GREEN, RED]
    region = Rect(0, 0, 730, 350)
    width, height = region.max.x+1, region.max.y+1
    image = Image.new("RGB", (width, height), WHITE)
    draw = ImageDraw.Draw(image)
    vert_gradient(draw, region, gradient_color, color_palette)
    image.show()
    #image.save("vert_gradient.png", "PNG")
    #print('image saved')

以下是它生成和显示的图片:

screenshot of gradient image created

此代码在RGB颜色空间中计算出中间颜色,但也可以使用其他颜色空间——例如,将结果与我在问题Range values to pseudocolor中得到的答案进行比较。

这可以轻松扩展以生成RGBA(RGB + Alpha)模式图像。


我刚刚复制并粘贴了您的代码,但收到了以下错误:delta = 1: ^ SyntaxError: invalid syntax (去掉冒号即可解决该错误)。 我使用的是Python 2.7.12,但不认为这是原因。另外,您的图像处理真的很棒! - WinEunuuchs2Unix
@WinEunuuchs2Unix:感谢你的赞美和提醒。当我试图清理其他人对代码所做的一些更改以处理“delta”为零时,代码出现了混乱 - 已经修复。 - martineau
感谢您的更新。我刚刚在这个网站上将您的答案链接到了我的答案中:https://dev59.com/JVYN5IYBdhLWcg3wRmah#63200562 - WinEunuuchs2Unix

9
如果你只需要两种颜色,那么这可以非常简单地完成:
def generate_gradient(
        colour1: str, colour2: str, width: int, height: int) -> Image:
    """Generate a vertical gradient."""
    base = Image.new('RGB', (width, height), colour1)
    top = Image.new('RGB', (width, height), colour2)
    mask = Image.new('L', (width, height))
    mask_data = []
    for y in range(height):
        mask_data.extend([int(255 * (y / height))] * width)
    mask.putdata(mask_data)
    base.paste(top, (0, 0), mask)
    return base

这段代码会为每种颜色创建一层,并根据y的位置创建带有不同透明度的蒙版。你可以在第10行中将y / height 替换为 x / width 来创建水平渐变,或者用任何关于xy的函数来生成其他类型的渐变。


不需要为每个“width”像素计算表达式int(255 * (y / height)) - 您可以使用该行替换整个for x in range(width):循环:mask_data.extend(width * [int(255 * (y / height))])。您建议的整体方法可以通过在连续两种颜色之间多次应用它并组合图像来处理多于两种颜色。 - martineau
@martineau 应用了该优化。 - Artemis
这可能会在某种程度上加快速度。在我的答案中,我使用了ImageDraw.line()来避免完全创建那些像素值 - 这将是另一种选择。 - martineau

0

这里是技术的详细说明。您需要在彼此上面放置两个图层,每个颜色一个图层。然后,您使得顶部图层的透明度增加,底部图层的透明度减少。如果您想要额外的练习,可以将透明度的速率更改为升序对数刻度,而不是线性刻度。祝您玩得愉快。


0

基于 Artemis 的代码,这是从右上角到左下角的渐变代码。

def generate_gradient(
        colour1: str, colour2: str, width: int, height: int) -> Image:
    """Generate a vertical gradient."""
    base = Image.new('RGB', (width, height), colour1)
    top = Image.new('RGB', (width, height), colour2)
    mask = Image.new('L', (width, height))
    mask_data = []
    for y in range(height):
        for x in range(width):
            mask_data.append(int(255 * ( (x*(height-y)) / (width*height) )))
    mask.putdata(mask_data)
    base.paste(top, (0, 0), mask)
    return base

0
通过对@martineau的代码进行一些修改,这个函数可以处理梯度方向(不仅限于垂直或水平)的角度:
from PIL import Image
import math


BLACK, DARKGRAY, GRAY = ((0,0,0), (63,63,63), (127,127,127))
LIGHTGRAY, WHITE = ((191,191,191), (255,255,255))
BLUE, GREEN, RED = ((0, 0, 255), (0, 255, 0), (255, 0, 0))


class Point(object):
    def __init__(self, x, y):
        self.x, self.y = x, y

    def rot_x(self, degrees):
        radians = math.radians(degrees)
        return self.x * math.cos(radians) + self.y * math.sin(radians)


class Rect(object):
    def __init__(self, x1, y1, x2, y2):
        minx, maxx = (x1,x2) if x1 < x2 else (x2,x1)
        miny, maxy = (y1,y2) if y1 < y2 else (y2,y1)
        self.min = Point(minx, miny)
        self.max = Point(maxx, maxy)

    def min_max_rot_x(self, degrees):
        first = True
        for x in [self.min.x, self.max.x]:
            for y in [self.min.y, self.max.y]:
                p = Point(x, y)
                rot_d = p.rot_x(degrees)
                if first:
                    min_d = rot_d
                    max_d = rot_d
                else:
                    min_d = min(min_d, rot_d)
                    max_d = max(max_d, rot_d)
                first = False
        return min_d, max_d

    width  = property(lambda self: self.max.x - self.min.x)
    height = property(lambda self: self.max.y - self.min.y)


def gradient_color(minval, maxval, val, color_palette):
    """ Computes intermediate RGB color of a value in the range of minval
        to maxval (inclusive) based on a color_palette representing the range.
    """
    max_index = len(color_palette)-1
    delta = maxval - minval
    if delta == 0:
        delta = 1
    v = float(val-minval) / delta * max_index
    i1, i2 = int(v), min(int(v)+1, max_index)
    (r1, g1, b1), (r2, g2, b2) = color_palette[i1], color_palette[i2]
    f = v - i1
    return int(r1 + f*(r2-r1)), int(g1 + f*(g2-g1)), int(b1 + f*(b2-b1))


def degrees_gradient(im, rect, color_func, color_palette, degrees):
    minval, maxval = 1, len(color_palette)
    delta = maxval - minval
    min_d, max_d = rect.min_max_rot_x(degrees)
    range_d = max_d - min_d
    for x in range(rect.min.x, rect.max.x + 1):
        for y in range(rect.min.y, rect.max.y+1):
            p = Point(x, y)
            f = (p.rot_x(degrees) - min_d) / range_d
            val = minval + f * delta
            color = color_func(minval, maxval, val, color_palette)
            im.putpixel((x, y), color)


def gradient_image(color_palette, degrees):
    region = Rect(0, 0, 600, 400)
    width, height = region.max.x+1, region.max.y+1
    image = Image.new("RGB", (width, height), WHITE)
    degrees_gradient(image, region, gradient_color, color_palette, -degrees)
    return image

这种灵活性的代价是必须逐像素设置颜色,而不能使用线条。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接