Search code examples
pythonnumpypython-imaging-library

Can't recover the same numpy array from PIL-saved PNG image


I'm saving a Numpy array as a PNG image using PIL, but when I read it back, I don't get the same array numbers.

I save a PNG image with one pixel with the value 2^16 but when I read it back to a Numpy array, such pixel has a value of 2^16 - 1.

Seems like the bit depth range is clipped to 2^16 -1 bits, but the docs say that saving as PNG, I can use the 'I' mode, which is for 32 bit signed integers.

From the Pillow docs:

The mode of an image is a string which defines the type and depth of a pixel in the image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range of 0-1, an 8-bit pixel has a range of 0-255 and so on. The current release supports the following standard modes:

I (32-bit signed integer pixels)

From the Pillow PNG docs:

Pillow identifies, reads, and writes PNG files containing 1, L, LA, I, P, RGB or RGBA data.

Reproducible example:

import tempfile
from PIL import Image
import numpy as np


im_np = np.array([[1, 1], [1, 2**16]], dtype=np.int32)
im_pil = Image.fromarray(im_np, mode='I')

with tempfile.TemporaryFile() as f:
    im_pil.save(f, 'PNG')
    
    with Image.open(f) as im:
        recovered_im_np = np.array(im)
        
        print(f"Numpy array:\n{im_np}")
        print(f"Numpy array receovered from PNG:\n {recovered_im_np}")

I get this:

Numpy array:
[[    1     1]
 [    1 65536]]
Numpy array receovered from PNG:
 [[    1     1]
 [    1 65535]]

Python version: 3.9.10 (main, Feb 17 2022, 18:15:00) \n[GCC 9.3.0]

PIL version: 9.0.1

Numpy version: 1.22.2


Solution

  • As @mateusreis correctly points out, the .png format only supports 16 bits per pixel in grayscale, so either you have to transform the value into a 3x8 24 bits per pixel RGB value, or you should save in an image format that support 32 bits per pixel, like TIFF:

    import tempfile
    from PIL import Image
    import numpy as np
    
    im_np = np.array([[1, 1], [1, 2 ** 16]], dtype=np.int32)
    im_pil = Image.fromarray(im_np, mode='I')
    
    with tempfile.TemporaryFile() as f:
        im_pil.save(f, 'TIFF')
    
        with Image.open(f) as im:
            recovered_im_np = np.array(im)
    
            print(f"Numpy array:\n{im_np}")
            print(f"Numpy array receovered from PNG:\n {recovered_im_np}")
    

    Result:

    Numpy array:
    [[    1     1]
     [    1 65536]]
    Numpy array receovered from PNG:
     [[    1     1]
     [    1 65536]]
    

    The key thing here is to realise you make 2 conversions (in both directions):

    • 1.) numpy array -> 2.) PIL image -> 3.) image file format

    Your mode='I' covers 1 -> 2, but you need to pick the right format to preserve the data for 2 -> 3.