Konvertieren Sie RGBA PNG mit PIL in RGB

96

Ich verwende PIL, um ein transparentes PNG-Bild, das mit Django hochgeladen wurde, in eine JPG-Datei zu konvertieren. Die Ausgabe sieht kaputt aus.

Quelldatei

transparente Quelldatei

Code

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

oder

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

Ergebnis

In beiden Fällen sieht das resultierende Bild folgendermaßen aus:

resultierende Datei

Gibt es eine Möglichkeit, dies zu beheben? Ich hätte gerne einen weißen Hintergrund, wo früher der transparente Hintergrund war.


Lösung

Dank der tollen Antworten habe ich mir folgende Funktionssammlung ausgedacht:

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

Die einfache Non-Compositing- alpha_to_colorFunktion ist die schnellste Lösung, hinterlässt jedoch hässliche Ränder, da sie keine halbtransparenten Bereiche verarbeitet.

Sowohl die reine PIL- als auch die Numpy-Compositing-Lösung liefern großartige Ergebnisse, sind jedoch alpha_composite_with_colorviel schneller (8,93 ms) als pure_pil_alpha_to_color(79,6 ms).Wenn auf Ihrem System numpy verfügbar ist, ist dies der richtige Weg. (Update: Die neue reine PIL-Version ist die schnellste aller genannten Lösungen.)

$ 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
quelle
Für ein bisschen mehr Geschwindigkeit, glaube ich, im = image.copy()kann entfernt werden, pure_pil_alpha_to_color_v2ohne das Ergebnis zu ändern. (Nach dem Wechsel nachfolgenden Instanzen imauf image, natürlich.)
unutbu
@unutbu ah, natürlich :) danke.
Danilo Bargen

Antworten:

126

Hier ist eine Version, die viel einfacher ist - nicht sicher, wie performant sie ist. Stark basierend auf einem Django-Snippet, das ich beim Aufbau der RGBA -> JPG + BGUnterstützung für Sorl-Thumbnails gefunden habe.

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)

Ergebnis @ 80%

Geben Sie hier die Bildbeschreibung ein

Ergebnis @ 50%
Geben Sie hier die Bildbeschreibung ein

Yuji 'Tomita' Tomita
quelle
1
Ihre Version scheint die schnellste zu sein: pastebin.com/mC4Wgqzv Danke! Zwei Dinge über Ihren Beitrag: Der Befehl png.load () scheint unnötig zu sein, und Zeile 4 sollte es sein background = Image.new("RGB", png.size, (255, 255, 255)).
Danilo Bargen
3
Herzlichen Glückwunsch zum Herausfinden, wie man pasteeine richtige Mischung macht.
Mark Ransom
@ DaniloBargen, ah! Es fehlte zwar die Größe, aber die loadMethode ist für die splitMethode erforderlich . Und das ist großartig zu hören, dass es tatsächlich schnell / und / einfach ist!
Yuji 'Tomita' Tomita
@ YujiTomita: Danke dafür!
Unutbu
12
Dieser Code hat einen Fehler für mich verursacht : tuple index out of range. Ich habe dies behoben, indem ich einer anderen Frage gefolgt bin ( stackoverflow.com/questions/1962795/… ). Ich musste zuerst das PNG in RGBA konvertieren und es dann in Scheiben schneiden: alpha = img.split()[-1]dann das auf der Hintergrundmaske verwenden.
Joehand
37

Durch die Verwendung Image.alpha_compositewird die Lösung von Yuji 'Tomita' Tomita einfacher. Dieser Code kann einen tuple index out of rangeFehler vermeiden , wenn png keinen Alphakanal hat.

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
quelle
Dies ist die beste Lösung für mich, da alle meine Bilder keinen Alphakanal haben.
Lenhhoxung
2
Wenn ich diesen Code verwende, ist der Modus des PNG-Objekts immer noch 'RGBA'
Logik1976
1
@ Logik1976 einfach einwerfen, .convert("RGB")bevor Sie es speichern
Josch
13

Die transparenten Teile haben meist einen RGBA-Wert (0,0,0,0). Da das JPG keine Transparenz hat, wird der JPEG-Wert auf (0,0,0) gesetzt, was schwarz ist.

Um das kreisförmige Symbol herum befinden sich Pixel mit RGB-Werten ungleich Null mit A = 0. Sie sehen im PNG transparent aus, im JPG jedoch witzig.

Sie können alle Pixel mit A == 0 so einstellen, dass R = G = B = 255 ist, indem Sie numpy wie folgt verwenden:

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

Geben Sie hier die Bildbeschreibung ein


Beachten Sie, dass das Logo auch einige halbtransparente Pixel enthält, mit denen die Kanten um die Wörter und das Symbol geglättet werden. Beim Speichern in JPEG wird die Halbtransparenz ignoriert, sodass das resultierende JPEG ziemlich gezackt aussieht.

Ein Ergebnis von besserer Qualität könnte mit dem convertBefehl von imagemagick erzielt werden :

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

Geben Sie hier die Bildbeschreibung ein


Um mit numpy eine Mischung mit besserer Qualität zu erzielen, können Sie Alpha-Compositing verwenden :

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

Geben Sie hier die Bildbeschreibung ein

unutbu
quelle
Vielen Dank, diese Erklärung macht sehr viel Sinn :)
Danilo Bargen
@DaniloBargen, haben Sie bemerkt, dass die Qualität der Konvertierung schlecht ist? Diese Lösung berücksichtigt keine teilweise Transparenz.
Mark Ransom
@ MarkRansom: Richtig. Wissen Sie, wie Sie das beheben können?
Unutbu
Es erfordert eine vollständige Mischung (mit Weiß) basierend auf dem Alpha-Wert. Ich habe PIL nach einem natürlichen Weg durchsucht und bin leer ausgegangen.
Mark Ransom
@ MarkRansom Ja, ich habe dieses Problem bemerkt. In meinem Fall wirkt sich dies jedoch nur auf einen sehr kleinen Prozentsatz der Eingabedaten aus, sodass die Qualität für mich gut genug ist.
Danilo Bargen
4

Hier ist eine Lösung in reinem 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')
Mark Ransom
quelle
Danke, das funktioniert gut. Aber die numpy Lösung scheint viel schneller zu sein: pastebin.com/rv4zcpAV (numpy: 8.92ms, pil: 79.7ms)
Danilo Bargen
Es scheint, als gäbe es eine andere, schnellere Version mit reinem PIL. Siehe neue Antwort.
Danilo Bargen
2
@DaniloBargen, danke - ich freue mich über die bessere Antwort und hätte es nicht getan, wenn Sie mich nicht darauf aufmerksam gemacht hätten.
Mark Ransom
1

Es ist nicht kaputt. Es macht genau das, was Sie ihm gesagt haben. Diese Pixel sind schwarz mit voller Transparenz. Sie müssen alle Pixel durchlaufen und diejenigen mit voller Transparenz in Weiß konvertieren.

Ignacio Vazquez-Abrams
quelle
Vielen Dank. Aber um den blauen Kreis herum gibt es blaue Bereiche. Sind das halbtransparente Bereiche? Gibt es eine Möglichkeit, diese auch zu beheben?
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
quelle
-1

Bild importieren

def fig2img (fig): "" @brief Konvertiert eine Matplotlib-Figur in ein PIL-Bild im RGBA-Format und gibt es zurück @param fig eine Matplotlib-Figur @return a Python Imaging Library (PIL) Bild ein numpy Array buf = fig2data (fig) w, h, d = buf.shape return Image.frombytes ("RGBA", (w, h), buf.tostring ())

def fig2data (fig): "" @brief Konvertiert eine Matplotlib-Figur in ein 4D-Numpy-Array mit RGBA-Kanälen und gibt sie zurück. @param fig eine Matplotlib-Figur @ gibt ein numpy-3D-Array mit RGBA-Werten zurück. 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): wenn nicht is_already_saved: background = Image.new ("RGB", img.size, c) background.paste (img, mask = img.split () [3]) # 3 ist der Alphakanal

    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
quelle