Ubah RGBA PNG ke RGB dengan PIL

97

Saya menggunakan PIL untuk mengubah gambar PNG transparan yang diunggah dengan Django menjadi file JPG. Outputnya terlihat rusak.

Sumber data

file sumber transparan

Kode

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

atau

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

Hasil

Kedua cara tersebut, gambar yang dihasilkan terlihat seperti ini:

file yang dihasilkan

Apakah ada cara untuk memperbaikinya? Saya ingin memiliki latar belakang putih di mana latar belakang transparan dulu.


Larutan

Berkat jawaban yang bagus, saya telah menghasilkan koleksi fungsi berikut:

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

Performa

Fungsi non-pengomposisian sederhana alpha_to_coloradalah solusi tercepat, tetapi meninggalkan batas yang jelek karena tidak menangani area semi transparan.

Baik PIL murni maupun solusi pengomposisian numpy memberikan hasil yang bagus, tetapi alpha_composite_with_colorjauh lebih cepat (8,93 msec) daripada pure_pil_alpha_to_color(79,6 msec).Jika numpy tersedia di sistem Anda, itulah caranya. (Pembaruan: Versi PIL murni yang baru adalah yang tercepat dari semua solusi yang disebutkan.)

$ 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
sumber
Untuk kecepatan yang lebih tinggi, saya yakin im = image.copy()dapat dihapus dari pure_pil_alpha_to_color_v2tanpa mengubah hasilnya. (Setelah mengubah contoh berikutnya immenjadi image, tentu saja.)
unutbu
@unutbu ah, tentu saja :) terima kasih.
Danilo Bargen

Jawaban:

129

Ini adalah versi yang jauh lebih sederhana - tidak yakin seberapa performanya. Sangat berdasarkan pada beberapa potongan django yang saya temukan saat membangun RGBA -> JPG + BGdukungan untuk thumbnail 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)

Hasil @ 80%

masukkan deskripsi gambar di sini

Hasil @ 50%
masukkan deskripsi gambar di sini

Yuji 'Tomita' Tomita
sumber
1
Sepertinya versi Anda adalah yang tercepat: pastebin.com/mC4Wgqzv Terima kasih! Dua hal tentang posting Anda: Perintah png.load () tampaknya tidak diperlukan, dan baris 4 seharusnya background = Image.new("RGB", png.size, (255, 255, 255)).
Danilo Bargen
3
Selamat telah menemukan cara membuat pasteperpaduan yang tepat.
Markus Tebusan
@DaniloBargen, ah! Memang ukuran itu hilang, tetapi loadmetode diperlukan untuk splitmetode tersebut. Dan itu luar biasa mendengarnya sebenarnya cepat / dan / sederhana!
Yuji 'Tomita' Tomita
@YujiTomita: Terima kasih untuk ini!
unutbu
12
Kode ini menyebabkan kesalahan bagi saya: tuple index out of range. Saya memperbaikinya dengan mengikuti pertanyaan lain ( stackoverflow.com/questions/1962795/… ). Saya harus mengonversi PNG ke RGBA terlebih dahulu dan kemudian mengirisnya: alpha = img.split()[-1]kemudian menggunakannya pada background mask.
joehand
38

Dengan menggunakan Image.alpha_composite, solusi dari Yuji 'Tomita' Tomita menjadi lebih sederhana. Kode ini dapat menghindari tuple index out of rangekesalahan jika png tidak memiliki saluran 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
sumber
Ini adalah solusi terbaik bagi saya karena semua gambar saya tidak memiliki saluran alfa.
lenhhoxung
2
Ketika saya menggunakan kode ini mode objek png masih 'RGBA'
logic1976
1
@ logic1976 masukkan saja .convert("RGB")sebelum menyimpannya
josch
13

Bagian transparan sebagian besar memiliki nilai RGBA (0,0,0,0). Karena JPG tidak memiliki transparansi, nilai jpeg diatur ke (0,0,0), yang berwarna hitam.

Di sekitar ikon lingkaran, ada piksel dengan nilai RGB bukan nol di mana A = 0. Jadi, piksel terlihat transparan di PNG, tetapi berwarna lucu di JPG.

Anda dapat mengatur semua piksel di mana A == 0 memiliki R = G = B = 255 menggunakan numpy seperti ini:

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

masukkan deskripsi gambar di sini


Perhatikan bahwa logo juga memiliki beberapa piksel semi transparan yang digunakan untuk menghaluskan tepi di sekitar kata dan ikon. Menyimpan ke jpeg mengabaikan semi-transparansi, membuat jpeg yang dihasilkan terlihat cukup bergerigi.

Hasil kualitas yang lebih baik dapat dibuat dengan menggunakan convertperintah imagemagick :

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

masukkan deskripsi gambar di sini


Untuk membuat perpaduan kualitas yang lebih bagus menggunakan numpy, Anda bisa menggunakan pengomposisian 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')

masukkan deskripsi gambar di sini

unutbu
sumber
Terima kasih, penjelasan itu sangat masuk akal :)
Danilo Bargen
@DaniloBargen, apakah Anda memperhatikan bahwa kualitas konversinya buruk? Solusi ini tidak memperhitungkan transparansi parsial.
Mark Ransom
@MarkRansom: Benar. Apakah Anda tahu cara memperbaikinya?
unutbu
Ini membutuhkan campuran penuh (dengan putih) berdasarkan nilai alpha. Saya telah mencari PIL untuk cara alami untuk melakukannya dan saya merasa hampa.
Markus Tebusan
@MarkRansom ya, saya perhatikan masalah itu. tetapi dalam kasus saya itu hanya akan mempengaruhi sebagian kecil dari data input, jadi kualitasnya cukup baik untuk saya.
Danilo Bargen
4

Berikut solusi dalam PIL murni.

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
sumber
Terima kasih, ini bekerja dengan baik. Tetapi solusi numpy tampaknya jauh lebih cepat: pastebin.com/rv4zcpAV (numpy: 8.92ms, pil: 79.7ms)
Danilo Bargen
Sepertinya ada versi lain yang lebih cepat dengan PIL murni. Lihat jawaban baru.
Danilo Bargen
2
@DaniloBargen, terima kasih - Saya senang melihat jawaban yang lebih baik dan saya tidak akan melakukannya jika Anda tidak memberitahukannya kepada saya.
Mark Ransom
1

Itu tidak rusak. Ia melakukan persis seperti yang Anda perintahkan; piksel tersebut berwarna hitam dengan transparansi penuh. Anda perlu mengulang di semua piksel dan mengonversinya dengan transparansi penuh menjadi putih.

Ignacio Vazquez-Abrams
sumber
Terima kasih. Namun disekitar lingkaran biru tersebut terdapat area biru. Apakah itu area semi transparan? Apakah ada cara untuk memperbaikinya juga?
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
pengguna1098761
sumber
-1

impor Gambar

def fig2img (fig): "" "@brief Mengonversi gambar Matplotlib menjadi Gambar PIL dalam format RGBA dan mengembalikannya @param fig gambar matplotlib @return gambar Python Imaging Library (PIL)" "" # letakkan gambar pixmap ke a numpy array buf = fig2data (fig) w, h, d = buf.shape return Image.frombytes ("RGBA", (w, h), buf.tostring ())

def fig2data (fig): "" "@brief Mengonversi gambar Matplotlib menjadi array numpy 4D dengan saluran RGBA dan mengembalikannya @param fig gambar matplotlib @kembalikan array 3D numpy nilai RGBA" "" # gambar gambar penyaji. 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): jika tidak is_already_saved: background = Image.new ("RGB", img.size, c) background.paste (img, mask = img.split () [3]) # 3 adalah saluran 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
sumber