使用PIL缩放图像时如何保留透明度和颜色?

11
说你想要缩放一个透明图像,但目前还不知道你将在以后将其组合到的背景颜色(s)。不幸的是,PIL似乎会合并完全透明像素的颜色值,从而导致糟糕的结果。有没有办法告诉PIL-resize忽略完全透明的像素?
import PIL.Image

filename = "trans.png"
size = (25,25)

im = PIL.Image.open(filename)
print im.mode  # RGBA

im = im.resize(size, PIL.Image.LINEAR)  # the same with CUBIC, ANTIALIAS, transform
# im.show()  # does not use alpha
im.save("resizelinear_"+filename)


# PIL scaled image has dark border

original image with (0,0,0,0) black but fully transparent background output image with black halo proper output scaled with gimp

使用(0,0,0,0)(黑色但完全透明)背景的原始图像(左)

带有黑色光晕的输出图像(中)

使用GIMP进行适当缩放的输出(右)

看起来要实现我想要的效果,我需要修改调整大小函数本身的采样方式,使其忽略完全透明的像素。

我找到了一个非常丑陋的解决方案。它将完全透明像素的颜色值设置为周围非完全透明像素的平均值,以最小化在调整大小时完全透明像素颜色的影响。简单形式下速度较慢,但如果没有其他解决方案,我会发表出来。可能可以通过使用膨胀操作只处理必要的像素来加快速度。


当您将文件加载到图像中时,它处于哪种模式?也许 alpha 通道已经被删除了? - Ber
它说RGBA,缩放后的图像在Gimp中看起来还不错,除了边框。 - Gonzo
你尝试将缩放后的图像与其他东西混合,比如白色背景了吗?如果缩放没问题,暗边就会消失。 - Ber
在Gimp中,带边框的输出图像被压平到白色背景上仍然存在问题。 - Gonzo
1
透明PNG调整大小:Python图像库和光晕效应 - Gonzo
4个回答

6
似乎PIL在调整大小之前没有进行alpha预乘,这是获得正确结果所必需的。幸运的是,通过暴力方法很容易实现。然后您必须对调整大小的结果进行反向操作。
def premultiply(im):
    pixels = im.load()
    for y in range(im.size[1]):
        for x in range(im.size[0]):
            r, g, b, a = pixels[x, y]
            if a != 255:
                r = r * a // 255
                g = g * a // 255
                b = b * a // 255
                pixels[x, y] = (r, g, b, a)

def unmultiply(im):
    pixels = im.load()
    for y in range(im.size[1]):
        for x in range(im.size[0]):
            r, g, b, a = pixels[x, y]
            if a != 255 and a != 0:
                r = 255 if r >= a else 255 * r // a
                g = 255 if g >= a else 255 * g // a
                b = 255 if b >= a else 255 * b // a
                pixels[x, y] = (r, g, b, a)

结果:premultiply、resize、unmultiply的结果

比我的解决方案好多了。我想知道信息损失有多严重。使用numpy数组应该可以很好地加速这些过程。 - Gonzo
在预乘 Alpha 丢失方面:http://www.quasimondo.com/archives/000665.php 结论:最好只在最终输出时使用。 - Gonzo
1
@Phelix 只有在你因某种原因降低透明度时,精度损失才是一个问题。大多数人不会这样做。 - Mark Ransom

3

您可以单独重新采样每个带:

im.load()
bands = im.split()
bands = [b.resize(size, Image.LINEAR) for b in bands]
im = Image.merge('RGBA', bands)

编辑

也许可以通过避免使用高透明度值来实现(需要numpy)。

import numpy as np

# ...

im.load()
bands = list(im.split())
a = np.asarray(bands[-1])
a.flags.writeable = True
a[a != 0] = 1
bands[-1] = Image.fromarray(a)
bands = [b.resize(size, Image.LINEAR) for b in bands]
a = np.asarray(bands[-1])
a.flags.writeable = True
a[a != 0] = 255
bands[-1] = Image.fromarray(a)
im = Image.merge('RGBA', bands)

第二个建议会去掉所有的透明度。但是我想保留平滑的边框。 - Gonzo
我看到第二个版本中有一些透明度,尽管它被制成了一个二进制掩码。 - Nicolas Barbey
你说得对,存在二进制透明度。我的意思是将半透明的alpha值更改为0/255。 - Gonzo

0
抱歉自问自答,但这是我所知道的唯一有效的解决方案。它将完全透明像素的颜色值设置为周围非完全透明像素的平均值,以最小化调整大小时完全透明像素颜色的影响。在某些特殊情况下,可能无法得到正确的结果。
这种方法非常丑陋且缓慢。如果您能提出更好的解决方案,我将非常乐意接受您的答案。
# might be possible to speed this up by only processing necessary pixels
#  using scipy dilate, numpy where

import PIL.Image

filename = "trans.png"
size = (25,25)

import numpy as np
   
im = PIL.Image.open(filename)

npImRgba = np.asarray(im, dtype=np.uint8)
npImRgba2 = np.asarray(im, dtype=np.uint8)
npImRgba2.flags.writeable = True
lenY = npImRgba.shape[0]
lenX = npImRgba.shape[1]
for y in range(npImRgba.shape[0]):
    for x in range(npImRgba.shape[1]):
        if npImRgba[y, x, 3] != 0:  # only change completely transparent pixels
            continue        
        colSum = np.zeros((3), dtype=np.uint16)
        i = 0
        for oy in [-1, 0, 1]:
            for ox in [-1, 0, 1]:
                if not oy and not ox:
                    continue
                iy = y + oy
                if iy < 0:
                    continue
                if iy >= lenY:
                    continue
                ix = x + ox
                if ix < 0:
                    continue
                if ix >= lenX:
                    continue
                col = npImRgba[iy, ix]
                if not col[3]:
                    continue
                colSum += col[:3]
                i += 1
        npImRgba2[y, x, :3] = colSum / i
                                
im = PIL.Image.fromarray(npImRgba2)
im = im.transform(size, PIL.Image.EXTENT, (0,0) + im.size, PIL.Image.LINEAR)
im.save("slime_"+filename)

结果: 输入图像描述

0
也许你可以用想要的颜色填充整个图像,然后只在 alpha 通道中创建形状?

如果我理解正确的话,这将会使边界失去抗锯齿效果。我希望粘贴到背景图像上的图像仍然具有平滑的过渡效果。 - Gonzo
AA 可以在 alpha 通道中完成(除非它已经存在于那里)。 - Ber
这就是问题所在,我想尽可能地保留原始图像的抗锯齿效果。 - Gonzo

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