2

I am looking for a fast way to save a grayscale image as a 4-bit png with python. The images that I have to save are quite big, so saving them takes quite some time.

Suppose my image is stored in a numpy-array (dtype=8-bit). With PyPng I can do:

import png
data = map(lambda x: map(int, x/17), data)
png.from_array(data, 'L;4').save(filename)

This will save a proper 4-bit png. With Pillow, I can do:

import PIL.Image as Image
im = Image.fromarray(data)
im.save(filename)

The second approach (Pillow) is about 10 times as fast as the first one (even without the conversation), however the images are 8-bit pngs. I tried adding the lines

im = im.point(lambda i: i/17) # convert values
im.mode = 'L;4'

but then I get *** SystemError: unknown raw mode, even though the Mode 'L;4' is specified in https://github.com/python-pillow/Pillow/blob/master/PIL/PngImagePlugin.py

Does anyone know how to save 4-bit pngs with Pillow or is there another fast way to do it?

Thomas
  • 1,277
  • 1
  • 12
  • 20
  • PyPNG is always going to be slower. – David Jones Sep 08 '15 at 13:10
  • You should divide by 17 to get from 8-bit to 4-bit (255 == 15 * 17). Unintiuitive. But correct. – David Jones Sep 08 '15 at 13:10
  • 1
    @DavidJones, you are right - of course I have to divide by 17. However, that doesn't solve the problem. Can you explain, **why** PyPNG is always going to be slower? And do you know a way to save 4-bit images in Pillow? – Thomas Sep 09 '15 at 14:52
  • well, PyPNG is written in Python, that's not going to change (that's why it exists), so it will always be slower than a module that is written in C. – David Jones Sep 22 '15 at 19:07

1 Answers1

2

Pillow doesn't support 4-bits grayscale. However, if, like me, you just want to convert the 8-bit image to a 4-bit bytestring, you can. Just dividing by 17 isn't enough, because each pixel will still be output as 1 byte. You need to pair each subsequent nibble with its neighbor nibble to get a full byte.

For that you could use something like this:

def convert_8bit_to_4bit(bytestring):
    fourbit = []
    for i in range(0,len(bytestring),2):
        first_nibble = int(bytestring[i] / 17)
        second_nibble = int(bytestring[i+1] / 17)
        fourbit += [ first_nibble << 4 | second_nibble ]
    fourbit = bytes(fourbit)
    return fourbit

Dependent on how your other application will handle the order of the nibbles you might have to switch 'first_nibble' and 'second_nibble' with each other

Vineeth Sai
  • 3,389
  • 7
  • 23
  • 34
F. Pareto
  • 304
  • 2
  • 10