简体   繁体   English

使用 PIL 将 RGBA PNG 转换为 RGB

[英]Convert RGBA PNG to RGB with PIL

I'm using PIL to convert a transparent PNG image uploaded with Django to a JPG file.我正在使用 PIL 将使用 Django 上传的透明 PNG 图像转换为 JPG 文件。 The output looks broken.输出看起来坏了。

Source file源文件

透明源文件

Code代码

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

or或者

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

Result结果

Both ways, the resulting image looks like this:两种方式,生成的图像如下所示:

结果文件

Is there a way to fix this?有没有办法来解决这个问题? I'd like to have white background where the transparent background used to be.我想要在透明背景曾经是白色背景的地方。


Solution解决方案

Thanks to the great answers, I've come up with the following function collection:感谢出色的答案,我想出了以下函数集合:

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表现

The simple non-compositing alpha_to_color function is the fastest solution, but leaves behind ugly borders because it does not handle semi transparent areas.简单的非合成alpha_to_color函数是最快的解决方案,但会留下难看的边框,因为它不处理半透明区域。

Both the pure PIL and the numpy compositing solutions give great results, but alpha_composite_with_color is much faster (8.93 msec) than pure_pil_alpha_to_color (79.6 msec).纯 PIL 和 numpy 合成解决方案都给出了很好的结果,但alpha_composite_with_colorpure_pil_alpha_to_color (79.6 毫秒) 快得多 (8.93 毫秒)。 If numpy is available on your system, that's the way to go.如果 numpy 在您的系统上可用,那就是要走的路。 (Update: The new pure PIL version is the fastest of all mentioned solutions.) (更新:新的纯 PIL 版本是所有提到的解决方案中最快的。)

$ 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

Here's a version that's much simpler - not sure how performant it is.这是一个更简单的版本 - 不确定它的性能如何。 Heavily based on some django snippet I found while building RGBA -> JPG + BG support for sorl thumbnails.很大程度上基于我在构建RGBA -> JPG + BG支持 sorl 缩略图时发现的一些 django 片段。

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)

Result @80%结果@80%

在此处输入图像描述

Result @ 50%结果@ 50%
在此处输入图像描述

By using Image.alpha_composite , the solution by Yuji 'Tomita' Tomita become simpler.通过使用Image.alpha_composite ,Yuji 'Tomita' Tomita 的解决方案变得更简单。 This code can avoid a tuple index out of range error if png has no alpha channel.如果 png 没有 alpha 通道,此代码可以避免tuple index out of range错误。

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)

The transparent parts mostly have RGBA value (0,0,0,0).透明部分大多具有 RGBA 值 (0,0,0,0)。 Since the JPG has no transparency, the jpeg value is set to (0,0,0), which is black.由于 JPG 没有透明度,所以 jpeg 值设置为 (0,0,0),即黑色。

Around the circular icon, there are pixels with nonzero RGB values where A = 0. So they look transparent in the PNG, but funny-colored in the JPG.在圆形图标周围,有非零 RGB 值的像素,其中 A = 0。因此它们在 PNG 中看起来是透明的,但在 JPG 中看起来很有趣。

You can set all pixels where A == 0 to have R = G = B = 255 using numpy like this:您可以使用 numpy 将 A == 0 的所有像素设置为 R = G = B = 255,如下所示:

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

在此处输入图像描述


Note that the logo also has some semi-transparent pixels used to smooth the edges around the words and icon.请注意,徽标还有一些半透明像素,用于平滑文字和图标周围的边缘。 Saving to jpeg ignores the semi-transparency, making the resultant jpeg look quite jagged.保存到 jpeg 会忽略半透明度,使生成的 jpeg 看起来很锯齿。

A better quality result could be made using imagemagick's convert command:使用 imagemagick 的convert命令可以获得更好的质量结果:

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

在此处输入图像描述


To make a nicer quality blend using numpy, you could use alpha compositing :要使用 numpy 进行质量更好的混合,您可以使用alpha 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')

在此处输入图像描述

Here's a solution in pure PIL.这是纯 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')

It's not broken.它没有坏。 It's doing exactly what you told it to;它完全按照您的指示行事; those pixels are black with full transparency.这些像素是完全透明的黑色。 You will need to iterate across all pixels and convert ones with full transparency to white.您将需要遍历所有像素并将完全透明的像素转换为白色。

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
from PIL import Image
 
def fig2img ( fig ):
    """
    @brief Convert a Matplotlib figure to a PIL Image in RGBA format and return it
    @param fig a matplotlib figure
    @return a Python Imaging Library ( PIL ) image
    """
    # put the figure pixmap into a numpy array
    buf = fig2data ( fig )
    w, h, d = buf.shape
    return Image.frombytes( "RGBA", ( w ,h ), buf.tostring( ) )

def fig2data ( fig ):
    """
    @brief Convert a Matplotlib figure to a 4D numpy array with RGBA channels and return it
    @param fig a matplotlib figure
    @return a numpy 3D array of RGBA values
    """
    # draw the 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 is the alpha channel

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM