Konwertuj RGBA PNG na RGB za pomocą PIL

101

Używam PIL do konwersji przezroczystego obrazu PNG przesłanego za pomocą Django do pliku JPG. Wynik wygląda na uszkodzony.

Plik źródłowy

przezroczysty plik źródłowy

Kod

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

lub

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

Wynik

W obu przypadkach wynikowy obraz wygląda następująco:

plik wynikowy

Czy jest sposób, aby to naprawić? Chciałbym mieć białe tło tam, gdzie kiedyś było przezroczyste.


Rozwiązanie

Dzięki świetnym odpowiedziom opracowałem następujący zbiór funkcji:

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

Występ

Prosta alpha_to_colorfunkcja niekomponująca jest najszybszym rozwiązaniem, ale pozostawia brzydkie granice, ponieważ nie obsługuje obszarów półprzezroczystych.

Zarówno czysty PIL, jak i rozwiązanie numpy compositing dają świetne wyniki, ale alpha_composite_with_colorsą znacznie szybsze (8,93 ms) niż pure_pil_alpha_to_color(79,6 ms).Jeśli numpy jest dostępny w twoim systemie, to jest droga. (Aktualizacja: nowa czysta wersja PIL jest najszybszym ze wszystkich wymienionych rozwiązań.)

$ 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
źródło
Myślę, że im = image.copy()aby uzyskać nieco większą prędkość, można ją usunąć pure_pil_alpha_to_color_v2bez zmiany wyniku. ( Oczywiście po zmianie kolejnych instancji imna image.)
unutbu
@unutbu ah, oczywiście :) dzięki.
Danilo Bargen

Odpowiedzi:

133

Oto wersja, która jest znacznie prostsza - nie jestem pewien, jak wydajna jest. W dużej mierze oparte na fragmencie django, który znalazłem podczas tworzenia RGBA -> JPG + BGobsługi miniatur 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)

Wynik przy 80%

wprowadź opis obrazu tutaj

Wynik @ 50%
wprowadź opis obrazu tutaj

Yuji „Tomita” Tomita
źródło
1
Wygląda na to, że twoja wersja jest najszybsza: pastebin.com/mC4Wgqzv Dzięki! Dwie rzeczy dotyczące twojego postu: Wydaje się, że polecenie png.load () jest niepotrzebne, a linia 4 powinna background = Image.new("RGB", png.size, (255, 255, 255)).
Danilo Bargen
3
Gratulujemy zastanowienia się, jak pastezrobić odpowiednią mieszankę.
Mark Ransom
@DaniloBargen, ah! Rzeczywiście brakowało rozmiaru, ale loadmetoda jest wymagana dla tej splitmetody. I wspaniale jest słyszeć, że jest naprawdę szybki / i / prosty!
Yuji 'Tomita' Tomita
@YujiTomita: Dziękuję za to!
unutbu
12
Kod ten był przyczyną błędu dla mnie tuple index out of range. Naprawiłem to, wykonując inne pytanie ( stackoverflow.com/questions/1962795/ ... ). Musiałem najpierw przekonwertować PNG na RGBA, a następnie go pokroić: alpha = img.split()[-1]następnie użyć tego na masce tła.
joehand
39

Używając Image.alpha_composite, rozwiązanie autorstwa Yuji „Tomita” Tomita stało się prostsze. Ten kod pozwala uniknąć tuple index out of rangebłędu, jeśli png nie ma kanału 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)
shuuji3
źródło
To dla mnie najlepsze rozwiązanie, ponieważ wszystkie moje obrazy nie mają kanału alfa.
lenhhoxung
2
Kiedy używam tego kodu, tryb obiektu png to nadal
``
1
@ logic1976 po prostu wrzuć a .convert("RGB")przed zapisaniem
josch
13

Części przezroczyste mają przeważnie wartość RGBA (0,0,0,0). Ponieważ JPG nie ma przezroczystości, wartość jpeg jest ustawiona na (0,0,0), czyli czarny.

Wokół okrągłej ikony znajdują się piksele o niezerowych wartościach RGB, gdzie A = 0. Wyglądają więc na przezroczyste w PNG, ale mają zabawne kolory w JPG.

Możesz ustawić wszystkie piksele, w których A == 0, aby R = G = B = 255, używając numpy w następujący sposób:

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

wprowadź opis obrazu tutaj


Zwróć uwagę, że logo zawiera również półprzezroczyste piksele używane do wygładzania krawędzi wokół słów i ikony. Zapisywanie do formatu jpeg ignoruje półprzezroczystość, przez co wynikowy plik jpeg wygląda na dość postrzępiony.

Lepszą jakość można uzyskać za pomocą convertpolecenia imagemagick :

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

wprowadź opis obrazu tutaj


Aby uzyskać ładniejszą mieszankę jakości za pomocą numpy, możesz użyć kompozycji 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://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')

wprowadź opis obrazu tutaj

unutbu
źródło
Dziękuję, to wyjaśnienie ma dużo sensu :)
Danilo Bargen,
@DaniloBargen, czy zauważyłeś, że jakość konwersji jest słaba? To rozwiązanie nie zapewnia częściowej przejrzystości.
Mark Ransom
@MarkRansom: True. Czy wiesz, jak to naprawić?
unutbu
Wymaga pełnego zmieszania (z bielą) w oparciu o wartość alfa. Szukałem w PIL-u naturalnego sposobu na zrobienie tego i wyszedłem pusty.
Mark Ransom
@MarkRansom tak, zauważyłem ten problem. ale w moim przypadku wpłynie to tylko na bardzo mały procent danych wejściowych, więc jakość jest dla mnie wystarczająco dobra.
Danilo Bargen
4

Oto rozwiązanie w czystym 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 Okup
źródło
Dzięki, to działa dobrze. Ale rozwiązanie numpy wydaje się być znacznie szybsze: pastebin.com/rv4zcpAV (numpy: 8,92 ms, pil: 79,7 ms)
Danilo Bargen
Wygląda na to, że istnieje inna, szybsza wersja z czystym PIL. Zobacz nową odpowiedź.
Danilo Bargen
2
@DaniloBargen, dziękuję - doceniam lepszą odpowiedź i nie zrobiłbym tego, gdybyś nie zwrócił mi na to uwagi.
Mark Ransom
1

To nie jest zepsute. Robi dokładnie to, co mu kazałeś; te piksele są czarne z pełną przezroczystością. Będziesz musiał powtórzyć wszystkie piksele i przekonwertować te z pełną przezroczystością na białe.

Ignacio Vazquez-Abrams
źródło
Dzięki. Ale wokół niebieskiego koła są niebieskie obszary. Czy to półprzezroczyste obszary? Czy jest sposób, żebym je też naprawił?
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
źródło
-1

importuj obraz

def fig2img (fig): "" "@brief Konwertuj figurę Matplotlib do obrazu PIL w formacie RGBA i zwróć ją @param fig a matplotlib figure @return a Python Imaging Library (PIL) obraz" "" # umieść obrazek w a numpy tablica buf = fig2data (fig) w, h, d = buf.shape return Image.frombytes ("RGBA", (w, h), buf.tostring ())

def fig2data (fig): "" "@brief Konwertuj figurę Matplotlib na tablicę numpy 4D z kanałami RGBA i zwracaj ją @param fig a matplotlib figure @return tablicę numpy 3D wartości RGBA" "" # narysuj renderer rys. 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 to kanał 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.')
Thomas Chaton
źródło