使用PIL将RGBA格式的PNG转换为RGB

148

我正在使用PIL将在Django上传的透明PNG图像转换为JPG文件。输出结果看起来有问题。

源文件

透明源文件

代码

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

或者

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

结果

两种方法都会得到以下结果:

resulting file

有没有办法解决这个问题?我想在原本透明的背景上加上白色背景。


解决方案

感谢众多回答,我总结了以下函数集合:

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: https://dev59.com/nmox5IYBdhLWcg3wf0bl#9166671

    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: https://dev59.com/nmox5IYBdhLWcg3wf0bl#9166671

    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: https://dev59.com/nmox5IYBdhLWcg3wf0bl#9168169

    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: https://dev59.com/nmox5IYBdhLWcg3wf0bl#9459208

    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

性能

简单的非混合 alpha_to_color 函数是最快的解决方案,但会留下丑陋的边框,因为它无法处理半透明区域。

纯PIL和numpy混合解决方案都可以得到很好的结果,但是alpha_composite_with_colorpure_pil_alpha_to_color 更快(8.93毫秒比79.6毫秒)。如果您的系统上有安装numpy,那么就采用这种方式。(更新:新版本的pure 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

为了更快一些,我相信在不改变结果的情况下可以从pure_pil_alpha_to_color_v2中删除im = image.copy()。(当然,在将后续实例从“im”更改为“image”之后。) - unutbu
8个回答

168

这里有一个更简单的版本 - 不确定它的性能如何。它很大程度上基于我在构建 sorl 缩略图时发现的一些 Django 片段,用于支持 RGBA -> JPG + BG

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)

结果 @80%

输入图片描述

结果 @50%
输入图片描述


1
看起来你的版本是最快的:http://pastebin.com/mC4Wgqzv 谢谢!不过关于你的帖子有两件事:png.load() 命令似乎是不必要的,第四行应该是 background = Image.new("RGB", png.size, (255, 255, 255)) - Danilo Bargen
3
恭喜你成功掌握了如何让“粘贴”实现适当混合的技巧。 - Mark Ransom
@DaniloBargen,啊!确实缺少了大小,但是load方法对于split方法是必需的。听到它实际上又快又简单真是太棒了! - Yuji 'Tomita' Tomita
为什么不使用Image.composite(image1, image2, mask)而不是搞乱paste? - Gonzo
16
这段代码给我带来了错误:tuple index out of range。我通过参考另一个问题的解答(https://dev59.com/NnI-5IYBdhLWcg3wN1lD)进行了修复。我需要先将PNG转换为RGBA格式,然后对其进行切片:`alpha = img.split()[-1]`,然后将其用于背景蒙版。 - joehand
1
background.paste(image, mask=image.getchannel('A')) -- is a bit better with the pixel range issue. And likely would work for some other modes like LA - Tatarize

50
通过使用Image.alpha_composite,Yuji'Tomita' Tomita的解决方案变得更加简单。如果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)

这对我来说是最好的解决方案,因为我的所有图像都没有 alpha 通道。 - lenhhoxung
3
使用这段代码后,png对象的模式仍然是“RGBA”。 - logic1976
5
在保存之前,只需加入 .convert("RGB") 即可。 - josch

15
透明部分的RGBA值大多为(0,0,0,0)。由于JPG没有透明度,所以jpeg值设置为(0,0,0),即黑色。
在圆形图标周围,有一些RGB值不为零且A=0的像素。因此它们在PNG中看起来是透明的,但在JPG中颜色很奇怪。
您可以使用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')

enter image description here


请注意,标志还有一些半透明像素用于平滑单词和图标周围的边缘。保存为jpeg会忽略半透明度,使得生成的jpeg外观非常锯齿。
使用imagemagick的convert命令可以获得更好的质量结果:
convert logo.png -background white -flatten /tmp/out.jpg

enter image description here


为了使用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
    '''
    # https://dev59.com/-HA75IYBdhLWcg3wS29A#3375291
    # https://dev59.com/nmox5IYBdhLWcg3wf0bl#9166671
    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')

enter image description here


@MarkRansom:没错。你知道怎么修复吗? - unutbu
它需要根据 alpha 值进行完全混合(与白色)。我一直在搜索 PIL 中自然的方法来做到这一点,但是我没有找到。 - Mark Ransom
@MarkRansom 是的,我注意到了那个问题。但在我的情况下,那只会影响到输入数据的极小部分,所以对我来说质量已经足够好了。 - Danilo Bargen
1
太棒了,伙计!问题和答案都很棒。非常感谢,我真的学到了很多。 - John Riselvato
你刚刚添加的Alpha合成正是我想要的,尽管实现起来似乎比我预期的更加复杂。我非常惊讶PIL不能自己完成它。 - Mark Ransom
显示剩余7条评论

4

以下是纯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')

谢谢,这个很好用。但是numpy的解决方案似乎要快得多:http://pastebin.com/rv4zcpAV(numpy:8.92毫秒,pil:79.7毫秒) - Danilo Bargen
似乎有另一个更快的版本,使用纯PIL。请查看新答案。 - Danilo Bargen
2
@DaniloBargen,谢谢 - 我很感激能看到更好的答案,如果不是你提醒我,我就看不到了。 - Mark Ransom

1

它没有损坏。它正在按照您告诉它的方式运行;那些像素是带有完全透明度的黑色。您需要遍历所有像素,并将具有完全透明度的像素转换为白色。


谢谢。但是在蓝色圆圈周围有蓝色区域。那些是半透明的区域吗?我能修复它们吗? - 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

0
基于上述示例:
它接收一个RGBA图像,并返回一个将alpha通道转换为白色的RGB图像。
from PIL import Image

def imageAlphaToWhite(image):
    background = Image.new("RGBA", image.size, "WHITE")
    alphaComposite = Image.alpha_composite(background, image)
    alphaComposite.convert("RGB")
    return alphaComposite

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接