Converti PNG RGBA in RGB con PIL


96

Sto usando PIL per convertire un'immagine PNG trasparente caricata con Django in un file JPG. L'output sembra rotto.

File sorgente

file sorgente trasparente

Codice

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

o

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

Risultato

In entrambi i modi, l'immagine risultante è simile a questa:

file risultante

C'è un modo per risolvere questo problema? Mi piacerebbe avere uno sfondo bianco dove prima c'era lo sfondo trasparente.


Soluzione

Grazie alle ottime risposte, ho creato la seguente raccolta di funzioni:

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

Prestazione

La semplice funzione di non composizione alpha_to_colorè la soluzione più veloce, ma lascia dietro di sé bordi brutti perché non gestisce aree semitrasparenti.

Sia il PIL puro che le soluzioni di compositing numpy danno ottimi risultati, ma alpha_composite_with_colorsono molto più veloci (8,93 msec) che pure_pil_alpha_to_color(79,6 msec).Se numpy è disponibile sul tuo sistema, questa è la strada da percorrere. (Aggiornamento: la nuova versione PIL puro è la più veloce di tutte le soluzioni menzionate.)

$ 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

Per un po 'più di velocità, credo che im = image.copy()possa essere rimosso pure_pil_alpha_to_color_v2senza modificare il risultato. (Dopo aver cambiato istanze successive ima image, naturalmente.)
unutbu

@unutbu ah, ovviamente :) grazie.
Danilo Bargen

Risposte:


127

Ecco una versione molto più semplice, non sicura di quanto sia performante. Fortemente basato su alcuni frammenti di django che ho trovato durante la creazione del RGBA -> JPG + BGsupporto per le miniature di 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)

Risultato @ 80%

inserisci qui la descrizione dell'immagine

Risultato @ 50%
inserisci qui la descrizione dell'immagine


1
Sembra che la tua versione sia la più veloce: pastebin.com/mC4Wgqzv Grazie! Tuttavia, due cose sul tuo post: il comando png.load () sembra non essere necessario e la riga 4 dovrebbe esserlo background = Image.new("RGB", png.size, (255, 255, 255)).
Danilo Bargen

3
Congratulazioni per aver capito come creare pasteuna miscela adeguata.
Mark Ransom

@DaniloBargen, ah! In effetti mancava la dimensione, ma il loadmetodo è richiesto per il splitmetodo. Ed è fantastico sentire che in realtà è veloce / e / semplice!
Yuji 'Tomita' Tomita

@YujiTomita: grazie per questo!
unutbu

12
Questo codice è stato causando un errore per me: tuple index out of range. Ho risolto questo problema seguendo un'altra domanda ( stackoverflow.com/questions/1962795/… ). Ho dovuto prima convertire il PNG in RGBA e poi tagliarlo: alpha = img.split()[-1]poi usarlo sulla maschera di sfondo.
joehand

37

Utilizzando Image.alpha_composite, la soluzione di Yuji "Tomita" Tomita diventa più semplice. Questo codice può evitare un tuple index out of rangeerrore se png non ha un canale 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)

Questa è la soluzione migliore per me perché tutte le mie immagini non hanno il canale alfa.
lenhhoxung

2
Quando uso questo codice la modalità dell'oggetto png è ancora 'RGBA'
logic1976

1
@ logic1976 basta inserire un .convert("RGB")prima di salvarlo
Josch

13

Le parti trasparenti hanno principalmente un valore RGBA (0,0,0,0). Poiché il JPG non ha trasparenza, il valore jpeg è impostato su (0,0,0), che è nero.

Intorno all'icona circolare, ci sono pixel con valori RGB diversi da zero dove A = 0. Quindi sembrano trasparenti nel PNG, ma di colori divertenti nel JPG.

Puoi impostare tutti i pixel dove A == 0 per avere R = G = B = 255 usando numpy in questo modo:

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

inserisci qui la descrizione dell'immagine


Nota che il logo ha anche alcuni pixel semitrasparenti utilizzati per smussare i bordi attorno alle parole e all'icona. Il salvataggio in jpeg ignora la semitrasparenza, rendendo il jpeg risultante piuttosto frastagliato.

Un risultato di qualità migliore potrebbe essere ottenuto utilizzando il convertcomando di imagemagick :

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

inserisci qui la descrizione dell'immagine


Per creare una miscela di qualità migliore usando numpy, puoi usare l' alfa compositing :

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

inserisci qui la descrizione dell'immagine


Grazie, questa spiegazione ha molto senso :)
Danilo Bargen

@DaniloBargen, hai notato che la qualità della conversione è scarsa? Questa soluzione non tiene conto della trasparenza parziale.
Mark Ransom

@MarkRansom: vero. Sai come risolverlo?
unutbu

Richiede una fusione completa (con il bianco) in base al valore alfa. Ho cercato PIL per un modo naturale per farlo e sono uscito vuoto.
Mark Ransom

@ MarkRansom sì, ho notato questo problema. ma nel mio caso ciò influirà solo su una percentuale molto piccola dei dati di input, quindi la qualità è abbastanza buona per me.
Danilo Bargen

4

Ecco una soluzione in puro 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')

Grazie, funziona bene. Ma la soluzione numpy sembra essere molto più veloce: pastebin.com/rv4zcpAV (numpy: 8.92ms, pil: 79.7ms)
Danilo Bargen

Sembra che ci sia un'altra versione più veloce con puro PIL. Vedi nuova risposta.
Danilo Bargen

2
@DaniloBargen, grazie - Apprezzo vedere la risposta migliore e non avrei se non l'avessi portato alla mia attenzione.
Mark Ransom

1

Non è rotto. Sta facendo esattamente quello che gli avevi detto; quei pixel sono neri con piena trasparenza. Dovrai iterare su tutti i pixel e convertire quelli con piena trasparenza in bianco.


Grazie. Ma intorno al cerchio blu ci sono aree blu. Quelle sono aree semitrasparenti? C'è un modo per sistemare anche quelli?
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

-1

importa immagine

def fig2img (fig): "" "@brief Converte una figura Matplotlib in un'immagine PIL in formato RGBA e restituiscila @param fig una figura matplotlib @return un'immagine Python Imaging Library (PIL)" "" # inserisce la pixmap della figura un array numpy buf = fig2data (fig) w, h, d = buf.shape return Image.frombytes ("RGBA", (w, h), buf.tostring ())

def fig2data (fig): "" "@brief Converti una figura Matplotlib in un array numpy 4D con canali RGBA e restituiscilo @param fig una figura matplotlib @return un array 3D numpy di valori RGBA" "" # disegna il 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 è il canale 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.')
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.