Search code examples
python-3.xpngtransparencypython-imaging-libraryalpha-transparency

How to convert this indexed PNG to grayscale and keep transparency, using Python and Pillow?


I am trying to convert images to grayscale using Python/Pillow. I had no difficulty in most images, but then, while testing with different images, I found this logo from the BeeWare project, that I know that has been further edited with some image editor and recompressed using ImageOptim.

enter image description here

The image has some kind of transparency (in the whole white area around the bee), but black color gets messed up. Here is the code:

#/usr/bin/env python3

import os
from PIL import Image, ImageFile

src_path = os.path.expanduser("~/Desktop/prob.png")
img = Image.open(src_path)
folder, filename = os.path.split(src_path)
temp_file_path = os.path.join(folder + "/~temp~" + filename)


if 'transparency' in img.info:
    transparency = img.info['transparency']
else:
    transparency = None

if img.mode == "P":
    img = img.convert("LA").convert("P")

try:
    img.save(temp_file_path, optimize=True, format="PNG", transparency=transparency)
except IOError:
    ImageFile.MAXBLOCK = img.size[0] * img.size[1]
    img.save(temp_file_path, optimize=True, format="PNG", transparency=transparency)

I also tried this:

png_info = img.info

if img.mode == "P":
    img = img.convert("LA").convert("P")

try:
    img.save(temp_file_path, optimize=True, format="PNG", **png_info)
except IOError:
    ImageFile.MAXBLOCK = img.size[0] * img.size[1]
    img.save(temp_file_path, optimize=True, format="PNG", **png_info)

Using either approach, all the black in the image becomes transparent.

enter image description here

I am trying to understand what I am missing here, or if this is some bug or limitation in Pillow. Digging a little through the image palette, I would say that transparency is in fact assigned to the black color in the palette. For instance, if I convert it to RGBA mode, the outside becomes black. So there must be something else that makes the outside area transparent.

Any tips?


Solution

  • Digging a little through the image palette, I would say that transparency is in fact assigned to the black color in the palette.

    pngcheck tells me that is not the case:

    ...
    chunk PLTE at offset 0x00025, length 48: 16 palette entries
    chunk tRNS at offset 0x00061, length 1: 1 transparency entry
    

    Each actual color has an index in PLTE, including black, and there is an additional entry that is designated "transparent". The black surroundings are probably an artefact of one of the previous conversions, where alpha=0 got translated to RGBA (0,0,0,0).

    It seems Pillow's immediate conversion to Lab ("L" and "LA") cannot handle indexed color conversions.
    You can solve this by converting the image to RGBA first, then converting each pixel quadruplet of RGBA to gray using the Lab conversion formula from the documentation, and then converting it back to palettized:

    for i in range(img.size[0]): # for every pixel:
        for j in range(img.size[1]):
            g = (pixels[i,j][0]*299 + pixels[i,j][1]*587 + pixels[i,j][2]*114)//1000
            pixels[i,j] = (g,g,g,pixels[i,j][3])
    

    but then I realized that since you start out with a palettized image and want to end up with one again, converting only the palette is much easier ...

    #/usr/bin/env python3
    
    from PIL import Image, ImageFile
    
    img = Image.open('bee.png')
    
    palette = img.getpalette()
    for i in range(len(palette)//3):
        gray = (palette[3*i]*299 + palette[3*i+1]*587 + palette[3*i+2]*114)//1000
        palette[3*i:3*i+3] = [gray,gray,gray]
    
    img.putpalette(palette)
    img.save('bee2a.png', optimize=True, format="PNG")
    
    print ('done')
    

    (Hardcoded to assume your input image is indeed an indexed file. Add checks if you want to make sure.)

    Result, wrapped inside a comment block so you can see the transparency:

    a gray bee