Convierta RGBA PNG a RGB con PIL

97

Estoy usando PIL para convertir una imagen PNG transparente cargada con Django en un archivo JPG. La salida parece rota.

Archivo fuente

archivo fuente transparente

Código

Image.open(object.logo.path).save('/tmp/output.jpg', 'JPEG')

o

Image.open(object.logo.path).convert('RGB').save('/tmp/output.png')

Resultado

En ambos sentidos, la imagen resultante se ve así:

archivo resultante

¿Hay alguna forma de solucionar este problema? Me gustaría tener un fondo blanco donde solía estar el fondo transparente.


Solución

Gracias a las excelentes respuestas, se me ocurrió la siguiente colección de funciones:

import Image
import numpy as np


def alpha_to_color(image, color=(255, 255, 255)):
    """Set all fully transparent pixels of an RGBA image to the specified color.
    This is a very simple solution that might leave over some ugly edges, due
    to semi-transparent areas. You should use alpha_composite_with color instead.

    Source: http://stackoverflow.com/a/9166671/284318

    Keyword Arguments:
    image -- PIL RGBA Image object
    color -- Tuple r, g, b (default 255, 255, 255)

    """ 
    x = np.array(image)
    r, g, b, a = np.rollaxis(x, axis=-1)
    r[a == 0] = color[0]
    g[a == 0] = color[1]
    b[a == 0] = color[2] 
    x = np.dstack([r, g, b, a])
    return Image.fromarray(x, 'RGBA')


def alpha_composite(front, back):
    """Alpha composite two RGBA images.

    Source: http://stackoverflow.com/a/9166671/284318

    Keyword Arguments:
    front -- PIL RGBA Image object
    back -- PIL RGBA Image object

    """
    front = np.asarray(front)
    back = np.asarray(back)
    result = np.empty(front.shape, dtype='float')
    alpha = np.index_exp[:, :, 3:]
    rgb = np.index_exp[:, :, :3]
    falpha = front[alpha] / 255.0
    balpha = back[alpha] / 255.0
    result[alpha] = falpha + balpha * (1 - falpha)
    old_setting = np.seterr(invalid='ignore')
    result[rgb] = (front[rgb] * falpha + back[rgb] * balpha * (1 - falpha)) / result[alpha]
    np.seterr(**old_setting)
    result[alpha] *= 255
    np.clip(result, 0, 255)
    # astype('uint8') maps np.nan and np.inf to 0
    result = result.astype('uint8')
    result = Image.fromarray(result, 'RGBA')
    return result


def alpha_composite_with_color(image, color=(255, 255, 255)):
    """Alpha composite an RGBA image with a single color image of the
    specified color and the same size as the original image.

    Keyword Arguments:
    image -- PIL RGBA Image object
    color -- Tuple r, g, b (default 255, 255, 255)

    """
    back = Image.new('RGBA', size=image.size, color=color + (255,))
    return alpha_composite(image, back)


def pure_pil_alpha_to_color_v1(image, color=(255, 255, 255)):
    """Alpha composite an RGBA Image with a specified color.

    NOTE: This version is much slower than the
    alpha_composite_with_color solution. Use it only if
    numpy is not available.

    Source: http://stackoverflow.com/a/9168169/284318

    Keyword Arguments:
    image -- PIL RGBA Image object
    color -- Tuple r, g, b (default 255, 255, 255)

    """ 
    def blend_value(back, front, a):
        return (front * a + back * (255 - a)) / 255

    def blend_rgba(back, front):
        result = [blend_value(back[i], front[i], front[3]) for i in (0, 1, 2)]
        return tuple(result + [255])

    im = image.copy()  # don't edit the reference directly
    p = im.load()  # load pixel array
    for y in range(im.size[1]):
        for x in range(im.size[0]):
            p[x, y] = blend_rgba(color + (255,), p[x, y])

    return im

def pure_pil_alpha_to_color_v2(image, color=(255, 255, 255)):
    """Alpha composite an RGBA Image with a specified color.

    Simpler, faster version than the solutions above.

    Source: http://stackoverflow.com/a/9459208/284318

    Keyword Arguments:
    image -- PIL RGBA Image object
    color -- Tuple r, g, b (default 255, 255, 255)

    """
    image.load()  # needed for split()
    background = Image.new('RGB', image.size, color)
    background.paste(image, mask=image.split()[3])  # 3 is the alpha channel
    return background

Actuación

La función simple de no composición alpha_to_colores la solución más rápida, pero deja atrás los bordes feos porque no maneja áreas semitransparentes.

Tanto el PIL puro como las soluciones de composición numpy dan excelentes resultados, pero alpha_composite_with_colores mucho más rápido (8,93 mseg) que pure_pil_alpha_to_color(79,6 mseg).Si numpy está disponible en su sistema, ese es el camino a seguir. (Actualización: la nueva versión pura de PIL es la más rápida de todas las soluciones mencionadas).

$ python -m timeit "import Image; from apps.front import utils; i = Image.open(u'logo.png'); i2 = utils.alpha_to_color(i)"
10 loops, best of 3: 4.67 msec per loop
$ python -m timeit "import Image; from apps.front import utils; i = Image.open(u'logo.png'); i2 = utils.alpha_composite_with_color(i)"
10 loops, best of 3: 8.93 msec per loop
$ python -m timeit "import Image; from apps.front import utils; i = Image.open(u'logo.png'); i2 = utils.pure_pil_alpha_to_color(i)"
10 loops, best of 3: 79.6 msec per loop
$ python -m timeit "import Image; from apps.front import utils; i = Image.open(u'logo.png'); i2 = utils.pure_pil_alpha_to_color_v2(i)"
10 loops, best of 3: 1.1 msec per loop
Danilo Bargen
fuente
Para un poco más de velocidad, creo que im = image.copy()se puede eliminar pure_pil_alpha_to_color_v2sin cambiar el resultado. (Después de cambiar las instancias posteriores de ima image, por supuesto.)
unutbu
@unutbu ah, por supuesto :) gracias.
Danilo Bargen

Respuestas:

129

Aquí hay una versión que es mucho más simple, no estoy seguro de su rendimiento. Basada en gran medida en un fragmento de django que encontré mientras RGBA -> JPG + BGcreaba soporte para miniaturas sorl.

from PIL import Image

png = Image.open(object.logo.path)
png.load() # required for png.split()

background = Image.new("RGB", png.size, (255, 255, 255))
background.paste(png, mask=png.split()[3]) # 3 is the alpha channel

background.save('foo.jpg', 'JPEG', quality=80)

Resultado @ 80%

ingrese la descripción de la imagen aquí

Resultado @ 50%
ingrese la descripción de la imagen aquí

Yuji 'Tomita' Tomita
fuente
1
Parece que su versión es la más rápida: pastebin.com/mC4Wgqzv ¡Gracias! Sin embargo, hay dos cosas sobre tu publicación: el comando png.load () parece ser innecesario y la línea 4 debería serlo background = Image.new("RGB", png.size, (255, 255, 255)).
Danilo Bargen
3
Felicitaciones por descubrir cómo hacer pasteuna mezcla adecuada.
Mark Ransom
@DaniloBargen, ¡ah! De hecho, faltaba tamaño, pero el loadmétodo es necesario para el splitmétodo. ¡Y es increíble escuchar que es realmente rápido y simple!
Yuji 'Tomita' Tomita
@YujiTomita: ¡Gracias por esto!
unutbu
12
Este código estaba causando un error de para mí: tuple index out of range. Solucioné esto siguiendo otra pregunta ( stackoverflow.com/questions/1962795/… ). Primero tuve que convertir el PNG a RGBA y luego cortarlo: alpha = img.split()[-1]luego usarlo en la máscara de fondo.
joehand
38

Al usar Image.alpha_composite, la solución de Yuji 'Tomita' Tomita se vuelve más simple. Este código puede evitar un tuple index out of rangeerror si png no tiene canal alfa.

from PIL import Image

png = Image.open(img_path).convert('RGBA')
background = Image.new('RGBA', png.size, (255,255,255))

alpha_composite = Image.alpha_composite(background, png)
alpha_composite.save('foo.jpg', 'JPEG', quality=80)
shuuji3
fuente
Esta es la mejor solución para mí porque todas mis imágenes no tienen canal alfa.
lenhhoxung
2
Cuando uso este código, el modo del objeto png sigue siendo 'RGBA'
logic1976
1
@ logic1976 sólo tirar de una .convert("RGB")antes de guardarla
Josch
13

La mayoría de las partes transparentes tienen un valor RGBA (0,0,0,0). Dado que el JPG no tiene transparencia, el valor de jpeg se establece en (0,0,0), que es negro.

Alrededor del icono circular, hay píxeles con valores RGB distintos de cero donde A = 0. Por lo tanto, se ven transparentes en PNG, pero de colores divertidos en JPG.

Puede configurar todos los píxeles donde A == 0 para tener R = G = B = 255 usando numpy como este:

import Image
import numpy as np

FNAME = 'logo.png'
img = Image.open(FNAME).convert('RGBA')
x = np.array(img)
r, g, b, a = np.rollaxis(x, axis = -1)
r[a == 0] = 255
g[a == 0] = 255
b[a == 0] = 255
x = np.dstack([r, g, b, a])
img = Image.fromarray(x, 'RGBA')
img.save('/tmp/out.jpg')

ingrese la descripción de la imagen aquí


Tenga en cuenta que el logotipo también tiene algunos píxeles semitransparentes que se utilizan para suavizar los bordes alrededor de las palabras y el icono. Guardar en jpeg ignora la semitransparencia, lo que hace que el jpeg resultante parezca bastante irregular.

Se podría obtener un resultado de mejor calidad usando el convertcomando de imagemagick :

convert logo.png -background white -flatten /tmp/out.jpg

ingrese la descripción de la imagen aquí


Para hacer una mezcla de mejor calidad usando numpy, puede usar composición alfa :

import Image
import numpy as np

def alpha_composite(src, dst):
    '''
    Return the alpha composite of src and dst.

    Parameters:
    src -- PIL RGBA Image object
    dst -- PIL RGBA Image object

    The algorithm comes from http://en.wikipedia.org/wiki/Alpha_compositing
    '''
    # http://stackoverflow.com/a/3375291/190597
    # http://stackoverflow.com/a/9166671/190597
    src = np.asarray(src)
    dst = np.asarray(dst)
    out = np.empty(src.shape, dtype = 'float')
    alpha = np.index_exp[:, :, 3:]
    rgb = np.index_exp[:, :, :3]
    src_a = src[alpha]/255.0
    dst_a = dst[alpha]/255.0
    out[alpha] = src_a+dst_a*(1-src_a)
    old_setting = np.seterr(invalid = 'ignore')
    out[rgb] = (src[rgb]*src_a + dst[rgb]*dst_a*(1-src_a))/out[alpha]
    np.seterr(**old_setting)    
    out[alpha] *= 255
    np.clip(out,0,255)
    # astype('uint8') maps np.nan (and np.inf) to 0
    out = out.astype('uint8')
    out = Image.fromarray(out, 'RGBA')
    return out            

FNAME = 'logo.png'
img = Image.open(FNAME).convert('RGBA')
white = Image.new('RGBA', size = img.size, color = (255, 255, 255, 255))
img = alpha_composite(img, white)
img.save('/tmp/out.jpg')

ingrese la descripción de la imagen aquí

unutbu
fuente
Gracias, esa explicación tiene mucho sentido :)
Danilo Bargen
@DaniloBargen, ¿notaste que la calidad de la conversión es mala? Esta solución no tiene en cuenta la transparencia parcial.
Mark Ransom
@MarkRansom: Verdadero. ¿Sabes cómo solucionarlo?
unutbu
Requiere una mezcla completa (con blanco) basada en el valor alfa. He estado buscando en PIL una forma natural de hacerlo y no he encontrado nada.
Mark Ransom
@MarkRansom sí, he notado ese problema. pero en mi caso eso solo afectará a un porcentaje muy pequeño de los datos de entrada, por lo que la calidad es lo suficientemente buena para mí.
Danilo Bargen
4

Aquí hay una solución en PIL puro.

def blend_value(under, over, a):
    return (over*a + under*(255-a)) / 255

def blend_rgba(under, over):
    return tuple([blend_value(under[i], over[i], over[3]) for i in (0,1,2)] + [255])

white = (255, 255, 255, 255)

im = Image.open(object.logo.path)
p = im.load()
for y in range(im.size[1]):
    for x in range(im.size[0]):
        p[x,y] = blend_rgba(white, p[x,y])
im.save('/tmp/output.png')
Mark Ransom
fuente
Gracias, esto funciona bien. Pero la solución numpy parece ser mucho más rápida: pastebin.com/rv4zcpAV (numpy: 8.92ms, pil: 79.7ms)
Danilo Bargen
Parece que hay otra versión más rápida con PIL puro. Ver nueva respuesta.
Danilo Bargen
2
@DaniloBargen, gracias. Aprecio ver la mejor respuesta y no lo habría hecho si no me hubieras llamado la atención.
Mark Ransom
1

No esta roto Está haciendo exactamente lo que le dijiste; esos píxeles son negros con transparencia total. Deberá iterar en todos los píxeles y convertir los que tengan transparencia total en blanco.

Ignacio Vázquez-Abrams
fuente
Gracias. Pero alrededor del círculo azul hay áreas azules. ¿Son esas áreas semitransparentes? ¿Hay alguna manera de que pueda arreglarlos también?
Danilo Bargen
0
import numpy as np
import PIL

def convert_image(image_file):
    image = Image.open(image_file) # this could be a 4D array PNG (RGBA)
    original_width, original_height = image.size

    np_image = np.array(image)
    new_image = np.zeros((np_image.shape[0], np_image.shape[1], 3)) 
    # create 3D array

    for each_channel in range(3):
        new_image[:,:,each_channel] = np_image[:,:,each_channel]  
        # only copy first 3 channels.

    # flushing
    np_image = []
    return new_image
usuario1098761
fuente
-1

importar imagen

def fig2img (fig): "" "@brief Convierta una figura Matplotlib en una imagen PIL en formato RGBA y devuélvala @param fig una figura matplotlib @return una imagen Python Imaging Library (PIL)" "" # coloque el mapa de píxeles de la figura en una matriz numpy buf = fig2data (fig) w, h, d = buf.shape return Image.frombytes ("RGBA", (w, h), buf.tostring ())

def fig2data (fig): "" "@brief Convierta una figura Matplotlib en una matriz 4D numpy con canales RGBA y devuélvala @param fig una figura matplotlib @return una matriz 3D numpy de valores RGBA" "" # dibuje el renderizador fig. canvas.draw ()

# Get the RGBA buffer from the figure
w,h = fig.canvas.get_width_height()
buf = np.fromstring ( fig.canvas.tostring_argb(), dtype=np.uint8 )
buf.shape = ( w, h, 4 )

# canvas.tostring_argb give pixmap in ARGB mode. Roll the ALPHA channel to have it in RGBA mode
buf = np.roll ( buf, 3, axis = 2 )
return buf

def rgba2rgb (img, c = (0, 0, 0), ruta = 'foo.jpg', is_already_saved = False, if_load = True): si no is_already_saved: background = Image.new ("RGB", img.size, c) background.paste (img, mask = img.split () [3]) # 3 es el canal alfa

    background.save(path, 'JPEG', quality=100)   
    is_already_saved = True
if if_load:
    if is_already_saved:
        im = Image.open(path)
        return np.array(im)
    else:
        raise ValueError('No image to load.')
Thomas Chaton
fuente