使用 PIL 调整 PNG 图像大小时会导致透明度丢失。

5
我使用这种方法调整PNG图片的大小: 将PNG转换为预乘alpha的方法 但是这张图片仍然失去了透明度:
import Image, numpy

def resize(filename, img,height, width):
    if filename.endswith(".png"):
        img = img.convert('RGBA')
        premult = numpy.fromstring(img.tostring(), dtype=numpy.uint8)
        alphaLayer = premult[3::4] / 255.0
        premult[::4] *= alphaLayer
        premult[1::4] *= alphaLayer
        premult[2::4] *= alphaLayer
        img = Image.fromstring("RGBA", img.size, premult.tostring())
    img = img.resize((height,width), Image.ANTIALIAS)
    return img

http://i.stack.imgur.com/oPV5q.pnghttp://i.stack.imgur.com/jzmT9.png


可能是Scale images with PIL preserving transparency and color?的重复问题。 - Mark Ransom
1
@MarkRansom 但他的图像最初不是RGBA格式,它是一张调色板图像,如果转换为RGBA会使一切变得错误。 - mmgp
@mmgp,我明白了,谢谢你澄清。我在这个位置没有我通常用来评估图像的工具,但我想在PIL中打开它会立即显示给我。 - Mark Ransom
我认为这个特定的PNG文件与PIL完全不兼容。它有一个带透明度的调色板,当PIL读取文件时,透明度被抛弃了。 - Mark Ransom
@MarkRansom确实,我也无法使用PIL正确地读取它。这是我能得到的最好的结果:http://i.imgur.com/8eKkMdh.png。 - mmgp
嗯,我发现问题所在了。如果没有修补PIL,这个图像无法正确读取。读取'tRNS'块时存在错误。 - mmgp
3个回答

4

这是一个更复杂的函数,用于调整大小(保持透明度):它允许您仅使用新的宽度值,同时使用新的宽度和高度值或参考文件来获取新的大小。您还可以更改重新采样方法:

## PngResizeTransparency.py
#
## Resize PNG image by keeping the transparency
## thanks to https://stackoverflow.com/users/1453719/nicolas-barbey
#
## Use:
##   - using a reference file to get new sizes:
#        PNG_ResizeKeepTransparency(SourceFile, ResizedFile, RefFile ='YourRefFile.png')
##   - using only the resized width:
#        PNG_ResizeKeepTransparency(SourceFile, ResizedFile, new_width)
##   - using resized width and hight:
#        PNG_ResizeKeepTransparency(SourceFile, ResizedFile, new_width, new_height)
##   - using resample mode: add param resample="NEAREST"/"BILINEAR"/"BICUBIC"/"ANTIALIAS"

from PIL import Image

def PNG_ResizeKeepTransparency(SourceFile, ResizedFile, new_width=0, new_height=0, resample="ANTIALIAS", RefFile =''):
    # needs PIL
    # Inputs:
    #   - SourceFile  = initial PNG file (including the path)
    #   - ResizedFile = resized PNG file (including the path)
    #   - new_width   = resized width in pixels; if you need % plz include it here: [your%] *initial width
    #   - new_height  = resized hight in pixels ; default = 0 = it will be calculated using new_width
    #   - resample = "NEAREST", "BILINEAR", "BICUBIC" and "ANTIALIAS"; default = "ANTIALIAS"
    #   - RefFile  = reference file to get the size for resize; default = ''

    img = Image.open(SourceFile) # open PNG image path and name
    img = img.convert("RGBA")    # convert to RGBA channels
    width, height = img.size     # get initial size

    # if there is a reference file to get the new size
    if RefFile != '':
        imgRef = Image.open(RefFile)
        new_width, new_height = imgRef.size
    else:
        # if we use only the new_width to resize in proportion the new_height
        # if you want % of resize please use it into new_width (?% * initial width)
        if new_height == 0:
            new_height = new_width*width/height

    # split image by channels (bands) and resize by channels
    img.load()
    bands = img.split()
    # resample mode
    if resample=="NEAREST":
        resample = Image.NEAREST
    else:
        if resample=="BILINEAR":
            resample = Image.BILINEAR
        else:
            if resample=="BICUBIC":
                resample = Image.BICUBIC
            else:
                if resample=="ANTIALIAS":
                    resample = Image.ANTIALIAS
    bands = [b.resize((new_width, new_height), resample) for b in bands]
    # merge the channels after individual resize
    img = Image.merge('RGBA', bands)
    # save the image
    img.save(ResizedFile)
    return

#######################################################

if __name__ == "__main__":
    sFile = './autumn-png-leaf.png'
    # resize using new width value (new height is calculated by keeping image aspect)
    PNG_ResizeKeepTransparency(sFile, sFile[:-4]+'_resized.png', 400)
    # resize using a reference file to get the new image dimension 
    PNG_ResizeKeepTransparency(sFile, sFile[:-4]+'_resized.png', RefFile = 'autumn-png-leaf_starry-night-van-gogh_fchollet_10.png')

太棒了!但是你可以用getattr(Image, resample, Image.ANTIALIAS)代替一堆if/elif语句。 - Lemayzeur

3
问题与链接的问题无关,而是您需要修补PIL,以使其正确读取tRNS PNG块。PIL假设这个块只有一个值,但是这个图片中每个调色板的值都有透明度描述。处理完后,解决问题就很简单了:将图像转换为'LA'模式并调整大小。
import sys
from PIL import Image

img = Image.open(sys.argv[1])
pal = img.getpalette()
width, height = img.size
actual_transp = img.info['actual_transparency'] # XXX This will fail.

result = Image.new('LA', img.size)

im = img.load()
res = result.load()
for x in range(width):
    for y in range(height):
        t = actual_transp[im[x, y]]
        color = pal[im[x, y]]
        res[x, y] = (color, t)

result.resize((64, 64), Image.ANTIALIAS).save(sys.argv[2])

我们从这个 enter image description here,到了这个:enter image description here

针对这种情况的PIL补丁实际上非常简单。打开你的PIL/PngImagePlugin.py,进入函数chunk_tRNS,进入检查im_mode == "P"和随后的检查i >= 0if语句,然后添加一行self.im_info["actual_transparency"] = map(ord, s)


ImagePalette 类已经与一个 mode 相关联,如果 PIL 能使用适当的 'RGBA' 模式,一切都会更好。您的解决方案适用于单色图像的特殊情况,但是对于许多其他有效的图像,它将失败。 - Mark Ransom
@MarkRansom但是这些透明度数据在PLTE块中不可用——PIL在构建png图像的调色板时。因此,如果调色板仅构建为RGBA,那么它无关紧要,它必须正确读取tRNS块并可能将其与调色板合并。因此,“补丁”仅用于正确读取tRNS块,任何依赖它的模式为P的png图像都可以使用此补丁。您想到了哪种png图像会失败? - mmgp
你的回答假设调色板是灰度调色板,但这并不一定是正确的。将转换为“RGBA”而不是“LA”可以解决这个问题,但结果将不再是8位了。 - Mark Ransom
@MarkRansom 但是答案的那部分与问题无关,图像可能包含一个 RGB 调色板,然后通过从 LA 切换到 RGBA 就可以正常工作。 - mmgp

1

你的意思是使用最新版本的 Pillow 对吧?我不确定你在这里所说的“hack”是什么意思,因为你在那段代码中也执行了 self.im_info["transparency_palette"] = s。正确读取块数据怎么会是一种 hack 呢? - mmgp
不,正如你已经注意到的那样,我也在使用那个想法。但是在我的实现中,我没有复制图像并循环遍历它。在加载时,透明度被应用于预期的方式。你解决方案的底部部分是一个“hack”。 你所说的“bleeding”是什么意思? - dss
哦,我称之为“hack”,并不意味着我对它有任何负面评价。Hacks很棒。 - dss
我使用了你关于块的知识,这不是巧合,我也不会声称我发现了缺陷。zuroc在pillow上发布了这个链接问题,我刚刚看了一下,仔细研究了你的解决方案,并直接将其实现到Pillow中,它将被重新编译。MarkRansom也已经提到了你的解决方案中的小缺陷,强制图像转换为灰度或RGB以应用透明度。内部错误修复程序没有这个问题。这只是Mark上面提到的正确解决方案。 - dss
我相信它会被包含在内。颜色的问题是真实存在的,你的解决方案对于 TO 链接的那张图片已经很好地解决了。 - dss
显示剩余2条评论

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