Convertir RGBA PNG en RGB avec PIL

97

J'utilise PIL pour convertir une image PNG transparente téléchargée avec Django en 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 fond blanc là où se trouvait l'arrière-plan transparent.


Solution

Grâce aux excellentes 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 non-compositante alpha_to_colorest la solution la plus rapide, mais laisse derrière elle des bordures laides car elle ne gère pas les zones semi-transparentes.

Les solutions de composition PIL pure et numpy donnent tous deux d'excellents résultats, mais alpha_composite_with_colorsont beaucoup plus rapides (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 de 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
Danilo Bargen
la source
Pour un peu plus de vitesse, je pense qu'il im = image.copy()peut être supprimé pure_pil_alpha_to_color_v2sans changer le résultat. (Après avoir changé les instances suivantes de imà image, bien sûr.)
unutbu
@unutbu ah, bien sûr :) merci.
Danilo Bargen

Réponses:

128

Voici une version beaucoup plus simple - je ne sais pas à quel point elle est performante. Fortement basé sur un extrait de django que j'ai trouvé lors de la construction du RGBA -> JPG + BGsupport pour les vignettes 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%

entrez la description de l'image ici

Résultat à 50%
entrez la description de l'image ici

Yuji 'Tomita' Tomita
la source
1
Il semble que votre version soit la plus rapide: pastebin.com/mC4Wgqzv Merci! Cependant, deux choses à propos de votre message: la commande png.load () semble inutile, et la ligne 4 devrait l'être background = Image.new("RGB", png.size, (255, 255, 255)).
Danilo Bargen
3
Félicitations pour déterminer comment faire pastefaire un bon mélange.
Mark Ransom
@DaniloBargen, ah! En effet il manquait de taille, mais la loadméthode est obligatoire pour la splitméthode. Et c'est génial d'entendre que c'est en fait rapide / et / simple!
Yuji 'Tomita' Tomita
@YujiTomita: Merci pour cela!
unutbu
12
Ce code a été à l' origine d' une erreur pour moi: tuple index out of range. J'ai corrigé cela en suivant une autre question ( stackoverflow.com/questions/1962795/… ). J'ai d'abord dû convertir le PNG en RGBA, puis le découper: alpha = img.split()[-1]puis l'utiliser sur le masque d'arrière-plan.
joehand
38

En utilisant Image.alpha_composite, la solution de Yuji 'Tomita' Tomita devient plus simple. Ce code peut éviter une tuple index out of rangeerreur 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)
shuuji3
la source
C'est la meilleure solution pour moi car toutes mes images n'ont pas de canal alpha.
lenhhoxung
2
Lorsque j'utilise ce code, le mode de l'objet png est toujours 'RGBA'
logic1976
1
@ logic1976 il suffit de lancer un .convert("RGB")avant de l'enregistrer
josch
13

Les parties transparentes ont pour la plupart une 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), qui est 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 semblent donc transparents dans le PNG, mais de couleur amusante dans le 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')

entrez 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 de l'icône. L'enregistrement en jpeg ignore la semi-transparence, ce qui donne au jpeg résultant un aspect assez irrégulier.

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

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

entrez la description de l'image ici


Pour créer un mélange de meilleure qualité avec 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')

entrez la description de l'image ici

unutbu
la source
Merci, cette explication a beaucoup de sens :)
Danilo Bargen
@DaniloBargen, avez-vous remarqué que la qualité de la conversion est mauvaise? Cette solution ne tient pas compte d'une transparence partielle.
Mark Ransom le
@MarkRansom: Vrai. Savez-vous comment résoudre ce problème?
unutbu
Il nécessite un mélange complet (avec du blanc) basé sur la valeur alpha. J'ai cherché PIL pour une manière naturelle de le faire et je suis venu vide.
Mark Ransom
@MarkRansom oui, j'ai remarqué ce problème. mais dans mon cas, cela n'affectera qu'un très petit pourcentage des données d'entrée, donc la qualité est assez bonne pour moi.
Danilo Bargen
4

Voici une solution en PIL pur.

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
la source
Merci, cela fonctionne bien. Mais la solution numpy semble être beaucoup plus rapide: pastebin.com/rv4zcpAV (numpy: 8,92ms, pil: 79,7ms)
Danilo Bargen
On dirait qu'il existe une autre version plus rapide avec PIL pur. Voir la nouvelle réponse.
Danilo Bargen
2
@DaniloBargen, merci - j'apprécie de voir la meilleure réponse et je ne l'aurais pas fait si vous ne l'aviez pas portée à mon attention.
Mark Ransom
1

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

Ignacio Vazquez-Abrams
la source
Merci. Mais autour du cercle bleu, il y a des zones bleues. Ces zones sont-elles semi-transparentes? Y a-t-il un moyen de résoudre ces problèmes aussi?
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
user1098761
la source
-1

importer une image

def fig2img (fig): "" "@brief Convertir une figure Matplotlib en une image PIL au format RGBA et la renvoyer @param fig une figure matplotlib @return a Python Imaging Library (PIL) image" "" # mettre la figure pixmap dans un tableau numpy buf = fig2data (fig) w, h, d = buf.shape return Image.frombytes ("RGBA", (w, h), buf.tostring ())

def fig2data (fig): "" "@brief Convertir une figure Matplotlib en un tableau numpy 4D avec des canaux RGBA et le renvoyer @param fig une figure matplotlib @return un tableau 3D numpy de valeurs RGBA" "" # dessiner le moteur de rendu 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): sinon is_already_saved: background = Image.new ("RVB", 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.')
Thomas Chaton
la source