Convertir RGBA PNG en RGB avec PIL

J’utilise PIL pour convertir une image PNG transparente téléchargée avec Django dans un fichier JPG. La sortie semble cassée.

Fichier source

fichier source transparent

Code

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

ou

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

Résultat

Dans les deux cas, l’image résultante ressemble à ceci:

fichier résultant

Y’a t’il un moyen d’arranger cela? J’aimerais avoir un arrière-plan blanc là où se trouvait l’arrière-plan transparent.


Solution

Grâce aux bonnes réponses, j’ai créé la collection de fonctions suivante:

 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 

Performance

La simple fonction alpha_to_color sans composition est la solution la plus rapide, mais laisse des bordures laides car elle ne gère pas les zones semi-transparentes.

Le PIL pur et les solutions de composition numpy donnent d’excellents résultats, mais alpha_composite_with_color est beaucoup plus rapide (8,93 ms) que pure_pil_alpha_to_color (79,6 ms). Si numpy est disponible sur votre système, c’est la voie à suivre. (Mise à jour: la nouvelle version pure PIL est la plus rapide de toutes les solutions mentionnées.)

 $ 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 

Voici une version beaucoup plus simple: vous ne savez pas à quel point elle est performante. Fortement basé sur un extrait de django trouvé lors de la construction du support RGBA -> JPG + BG pour les vignettes de 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) 

Résultat à 80%

entrer la description de l'image ici

Résultat @ 50%
entrer la description de l'image ici

En utilisant Image.alpha_composite , la solution de Yuji ‘Tomita’ Tomita devient plus simple. Ce code peut éviter une erreur d’ tuple index out of range si png n’a pas de canal alpha.

 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) 

Les parties transparentes ont pour la plupart la valeur RGBA (0,0,0,0). Étant donné que le JPG n’a pas de transparence, la valeur jpeg est définie sur (0,0,0), ce qui correspond au noir.

Autour de l’icône circulaire, il y a des pixels avec des valeurs RVB différentes de zéro où A = 0. Ils sont donc transparents dans le format PNG, mais de couleur amusante dans le format JPG.

Vous pouvez définir tous les pixels où A == 0 pour avoir R = G = B = 255 en utilisant numpy comme ceci:

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

entrer la description de l'image ici


Notez que le logo comporte également des pixels semi-transparents utilisés pour lisser les bords autour des mots et des icons. Enregistrer en jpeg ignore la semi-transparence, rendant le résultat JPEG très irrégulier.

Un résultat de meilleure qualité pourrait être obtenu en utilisant la commande convert imagemagick:

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

entrer la description de l'image ici


Pour créer un mélange de qualité plus agréable en utilisant numpy, vous pouvez utiliser la composition alpha :

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

entrer la description de l'image ici

Voici une solution en pure PIL.

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

Ce n’est pas cassé C’est exactement ce que vous lui avez dit. ces pixels sont noirs avec une transparence totale. Vous devrez parcourir tous les pixels et convertir ceux en toute transparence en blanc.

importer l’image

def fig2img (fig): “” “@brief Convertir une image Matplotlib en image PIL au format RGBA et la renvoyer @param Une figure de matplotlib @return une image de la bibliothèque Python Imaging Library (PIL)” un tableau numpy buf = fig2data (fig) w, h, d = buf.shape retourne Image.frombytes (“RGBA”, (w, h), buf.tossortingng ())

def fig2data (fig): “” “Convertit un personnage Matplotlib en tableau 4D numpy avec des canaux RGBA et le retourne @param fig un matplotlib figure @retour un numpy tableau 3D de valeurs RGBA” “” dessine le rendu fig. canvas.draw ()

 # Get the RGBA buffer from the figure w,h = fig.canvas.get_width_height() buf = np.fromssortingng ( fig.canvas.tossortingng_argb(), dtype=np.uint8 ) buf.shape = ( w, h, 4 ) # canvas.tossortingng_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): sinon is_already_saved: background = Image.new (“RGB”, img.size, c) background.paste (img, mask = img.split () [3]) # 3 est le canal alpha

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