python - Convierta RGBA PNG a RGB con PIL
jpeg python-imaging-library (6)
Estoy usando PIL para convertir una imagen PNG transparente cargada con Django en un archivo JPG. La salida se ve rota.
Archivo fuente
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
De ambas maneras, la imagen resultante se ve así:
¿Hay alguna manera de arreglar esto? Me gustaría tener un fondo blanco donde solía estar el fondo transparente.
Solución
Gracias a las excelentes respuestas, he creado 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 sin composición alpha_to_color
es la solución más rápida, pero deja atrás las fronteras feas porque no maneja áreas semi transparentes.
Tanto las soluciones PIL puras como las soluciones de composición numpy dan grandes resultados, pero alpha_composite_with_color
es mucho más rápido (8,93 pure_pil_alpha_to_color
) que pure_pil_alpha_to_color
(79,6 pure_pil_alpha_to_color
). 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
Al usar Image.alpha_composite
, la solución de Yuji ''Tomita'' Tomita se vuelve más simple. Este código puede evitar un error de tuple index out of range
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)
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'')
Aquí hay una versión que es mucho más simple: no estoy seguro de qué tan eficiente sea. Basada en algunos fragmentos de django que encontré al compilar compatibilidad con RGBA -> JPG + BG
para las 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%
Resultado @ 50%
Las partes transparentes en su mayoría tienen un valor RGBA (0,0,0,0). Como el JPG no tiene transparencia, el valor de jpeg se establece en (0,0,0), que es negro.
Alrededor del ícono circular, hay píxeles con valores RGB distintos de cero, donde A = 0. Se ven transparentes en el PNG, pero tienen un color gracioso en el 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'')
Tenga en cuenta que el logotipo también tiene algunos píxeles semitransparentes utilizados 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 utilizando el comando de convert
de imagemagick:
convert logo.png -background white -flatten /tmp/out.jpg
Para hacer una mezcla de calidad más agradable usando numpy, podrías 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://.com/a/3375291/190597
# http://.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'')
No está roto. Está haciendo exactamente lo que le dijiste; esos píxeles son negros con total transparencia. Deberá iterar en todos los píxeles y convertir los que tengan transparencia total a blanco.
importar imagen
def fig2img (fig): "" "@brief Convierta una figura de Matplotlib en una imagen de PIL en formato RGBA y devuélvala @param fig a matplotlib figure @return una imagen de Python Imaging Library (PIL)" "" # ponga el pixmap 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 a matplotlib figure @return una numpy matriz 3D de valores RGBA" "" # draw the renderer 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), path = ''foo.jpg'', is_already_saved = False, if_load = True): if not 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.'')