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

2

2

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?

Prosser answered 20/7, 2018 at 15:7 Comment(0)
R
4

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

Ravelment answered 20/7, 2018 at 23:32 Comment(2)
Yes, it works as intended. I have one question, though. Can't we end up with a palette that is bigger that necessary, with duplicate colours?Prosser
@VictorDomingos: when using the palette change? It is efficient because it does not need to touch the original pixel data; if you alter the palette, you must change the pixels as well. My first attempt does alter the RGB data and leaves converting to indexed color to Pillow again. That may optimize the palette as well, so you can use that less efficient method.Ravelment
G
1

Couldn't find a way to get my .png "indexed" or with a palette. What ended up doing is:

  • saving the alpha channel
  • converting to "L" then back "RGB"
  • re-applying the Alpha channel (aka transparency)
    img_colored = Image.open("transparency.png")
    img_colored.load()
    alpha = img_colored.split()[-1]
    img_grey = img_colored.convert("L").convert("RGB")
    img_grey.putalpha(alpha)

May be not as efficient, but it gets the job done

Grimbald answered 6/1, 2021 at 19:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.