【问题标题】:PIL: Generating Vertical Gradient ImagePIL:生成垂直渐变图像
【发布时间】:2015-09-11 18:54:42
【问题描述】:

在 Android 中,我使用以下代码生成了我需要的渐变背景:

<gradient
    android:angle="90"
    android:startColor="#40000000"
    android:endColor="#00000000"
    android:type="linear" />

背景从上到下由浅变暗。我想知道如何在 Python 中使用 PIL 做同样的事情,因为我需要对另一个用 Python 编写的程序产生相同的效果。

【问题讨论】:

  • "#40000000" 会产生什么 RGB 值?
  • @martineau 看起来 (160, 160, 160) 是最接近的。谢谢。
  • 垂直多少像素?你会关心它是用 Numpy 还是 scipy 代替吗?
  • 从我刚刚found 的文档来看,这没有多大意义。它看起来更像是 ARGB 值为 (64, 0, 0, 0) 的东西。另外,你知道需要多大的图片吗?
  • @martineau 它是 640*640。非常感谢!

标签: python python-imaging-library gradient


【解决方案1】:

这里展示了绘制多色矩形水平和垂直渐变的方法。

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

这是它生成并显示的图像:

这会计算RGB color space 中的中间颜色,但也可以使用其他颜色空间——例如比较我对问题Range values to pseudocolor 的回答的结果。

这可以很容易地扩展为生成 RGBA (RGB+Alpha) 模式的图像。

【讨论】:

  • 我刚刚复制并粘贴了您的代码并收到此错误:delta = 1: ^ SyntaxError: invalid syntax(删除:(冒号)解决了错误)。我在 Python 2.7.12 上,但不认为这是原因。顺便说一句,成像很棒!
  • @WinEunuuchs2Unix:感谢您的补充和提醒。当我试图清理其他人对其进行的一些更改以处理 delta 为零时,代码变得混乱 - 已修复。
  • 感谢更新。我刚才在这个网站上用我自己的答案链接到你的答案:*.com/a/63200562/6929343
  • @WinEunuuchs2Unix:很高兴听到 - 虽然对于占位符来说似乎需要付出很多努力,但我该判断谁...;¬)
【解决方案2】:

如果你只需要两种颜色,这可以很简单地完成:

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 应用了该优化。
  • 这可能会在一定程度上加快速度。在我自己的回答中,我使用了ImageDraw.line() 来完全避免创建这些像素值——这将是另一种选择。
【解决方案3】:

这里是详细说明的技术。你需要 2 层相互叠加,每种颜色一个。然后,您使顶层的透明度增加,底层的透明度降低。对于额外的作业,您可以将透明度更改为递增的对数刻度而不是线性。玩得开心。

【讨论】:

    【解决方案4】:

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

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

    【讨论】: