python png jpeg python-imaging-library rgba

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