Python - Pillow: ¿Cómo rellenar con degradado las formas dibujadas?

CorePress2024-01-25  10

Ya investigué bastante y todo lo que encontré fue cómo aplicar degradados al texto generado con Pillow. Sin embargo, quería saber cómo puedo aplicar un degradado en lugar de un relleno de un solo color normal a una forma dibujada (específicamente un polígono).

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

2

¿Esto responde a tu pregunta? Dibujo de la biblioteca de imágenes de Python (PIL): rectángulo redondeado con degradado

HansHirse

29 de marzo de 2021 a las 8:50

Bueno, para mi proyecto estoy generando múltiples formas y quiero que cada una de ellas tenga un relleno degradado. En el ejemplo que diste, me parece como si estuviera generando el degradado y poniéndolo en ¿La imagen completa o es incorrecta?

- Okym

1 de abril de 2021 a las 18:15



------------------------------------

Aquí está mi intento de crear algo que pueda adaptarse a su caso de uso. Tiene una funcionalidad limitada (específicamente, sólo admite gradientes lineales y radiales) y el gradiente lineal en sí también es algo limitado.Pero, antes que nada, veamos un resultado ejemplar:

Básicamente, existen dos métodos

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

y

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

que obtienen un objeto Pillow Image i, una lista de vértices que describen un polígono, colores de inicio y fin para el gradiente c1 y c2, y dos puntos p1 y p2 que describen la dirección del gradiente lineal (desde un vértice a un segundo) o un solo punto p que describe el centro del gradiente radial.

En ambos métodos, el polígono inicial se dibuja en un lienzo vacío del tamaño de la imagen final, utilizando únicamente el canal alfa.

Para el gradiente lineal, se calcula el ángulo entre p1 y p2. El polígono dibujado se gira ese ángulo y se recorta para obtener las dimensiones necesarias para un gradiente lineal adecuado. Ese es simplemente creado by np.linspace. El gradiente se gira en el ángulo conocido, pero en la dirección opuesta, y finalmente se traslada para ajustarse al polígono real. La imagen del degradado se pega en la imagen del polígono intermedio para obtener el polígono con el degradado lineal y el resultado se pega en la imagen real.

Limitación del gradiente lineal: es mejor elegir dos vértices del polígono "en lados opuestos", o mejor: de modo que todos los puntos dentro del polígono estén dentro del espacio virtual abarcado por esos dos puntos. De lo contrario, la implementación actual podría fallar, p. al elegir dos vértices vecinos.

El método del gradiente radial funciona de manera ligeramente diferente. Se determina la distancia máxima desde p a todos los vértices del polígono. Entonces, para todos los puntos en una imagen intermedia de la acTamaño real de la imagen, la distancia a p se calcula y se normaliza según la distancia máxima calculada. Obtenemos valores en el rango [0,0 ... 1,0] para todos los puntos dentro del polígono. Los valores se utilizan para calcular los colores apropiados que van de c1 a c2. En cuanto al degradado lineal, esa imagen de degradado se pega en la imagen del polígono intermedio y el resultado se pega en la imagen real.

Con suerte, el código se explica por sí mismo mediante los comentarios. Pero si tienes preguntas, ¡no dudes en preguntar!

Aquí está el código completo:

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

------------------------------------

----

5

¿Por qué tuviste que escribir tu propia función de relleno degradado?

- León de Oro

12 de abril de 2021 a las 20:23

1

@GoldenLion ¿Qué quieres decir con "por qué"? No pude encontrar una manera de usar las funciones incorporadas de Pillow para obtener formas llenas de degradado. Entonces, intenté encontrar una solución relativamente corta para obtener la funcionalidad deseada. Si conoce algún otro método, tal vez incluso el incorporado en Pillow, ¡compártalo!

HansHirse

12 de abril de 2021a las 20:44

Parece que debería haber una biblioteca para hacer un polirelleno degradado. ¿Sigues buscando una solución?

- León de Oro

12 de abril de 2021 a las 21:18

Esto es exactamente lo que estaba buscando sobre cómo hacerlo pero no pude encontrarlo en ninguna parte, ¡realmente lo aprecio!

- Okym

15/04/2021 a las 13:49

Eche un vistazo a generativepy: pythoninformer.com/generative-art/generativepy-tutorial/…

- Rasmus Friis Kjeldsen

7 de diciembre de 2023 a las 16:22



------------------------------------

Aún no estoy seguro de haber entendido completamente tu pregunta. ¿Pero parece que quieres tener un montón de formas con sus propios degradados? Un enfoque sería generar los degradados de cada forma por separado y luego combinar las formas después del hecho.

Aprovechando la respuesta ya referidaRelacionado por @HansHirse, puedes hacer algo como:

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

El resultado se parece a esto:

El único "nuevo" Las cosas aparecen al final: las imágenes simplemente se combinan. Si quieres volverte loco, puedes rotar las formas individuales o permitir que se superpongan (modificando la posición de las imágenes en get_concat_h + jugando con el tamaño final de la imagen).

Su guía para un futuro mejor - libreflare
Su guía para un futuro mejor - libreflare