12
votes

I'm trying to read a PNG image file written in 16-bit data type. The data should be converted to a NumPy array. But I have no idea how to read the file in '16-bit'. I tried with PIL and SciPy, but they converted the 16-bit data to 8-bit when they load it. Could anyone please let me know how to read data from a 16-bit PNG file and convert it to NumPy array without changing the datatype?

The following is the script that I used.

from scipy import misc
import numpy as np
from PIL import Image
#make a png file    
a = np.zeros((1304,960), dtype=np.uint16)
a[:] = np.arange(960)
misc.imsave('16bit.png',a)

#read the png file using scipy
b = misc.imread('16bit.png')
print "scipy:" ,b.dtype

#read the png file using PIL
c = Image.open('16bit.png')   
d = np.array(c)
print "PIL:", d.dtype
               
9

9 Answers

3
votes

I'd recommend using opencv:

pip install opencv-python

and

import cv2
image = cv2.imread('16bit.png', cv2.IMREAD_UNCHANGED)

  • in contrast to OpenImageIO, opencv could be installed from pip
  • The time, required to read a single 4000x4000 png is about the same as PIL, but PIL uses more CPU and requires additional time to convert data back to uint16.
6
votes

I have the same problem here. I tested it even with 16 bit images i created by my own. All of them were opened correctly when i loaded them with the png package. Also the output of 'file ' looked okay.

Opening them with PIL always led to 8-bit numpy-arrays.

Working with Python 2.7.6 on Linux btw.

Like this it works for me:

import png
import numpy as np

reader = png.Reader( path-to-16bit-png )
pngdata = reader.read()
px_array = np.array( map( np.uint16, pngdata[2] ) 
print( px_array.dtype )

Maybe someone can give more information under which circumstances the former approach worked? (as this one is pretty slow)

Thanks in advance.

4
votes

The simplest solution I've founded:

When I open a 16 bit monochrome PNG Pillow doesn't open it correctly as I;16 mode. Image.mode is opened as I (32 bits)

So, the best way to convert to numpy Array. It is dtype="int32" so we will convert it to dtype="uint16".

import numpy as np
from PIL import Image

im = Image.fromarray(np.array(Image.open(name)).astype("uint16"))
print("Image mode: ",im.mode))

Tested in Python 3.6.8 with Pillow 6.1.0

3
votes

This happens because PIL does not support 16-bit data, explained here: http://effbot.org/imagingbook/concepts.htm

I use a work around using the osgeo gdal package (which can read PNG).

#Import
import numpy as np
from osgeo import gdal

#Read in PNG file as 16-bit numpy array
lon_offset_px=0
lat_offset_px=0
fn = 'filepath'
gdo = gdal.Open(fn)
band = gdo.GetRasterBand(1)
xsize = band.XSize
ysize = band.YSize
png_array = gdo.ReadAsArray(lon_offset_px, lat_offset_px, xsize, ysize)
png_array = np.array(png_array)

This will return

png_array.dtype
dtype('uint16')

A cleaner way I found is using the skimage package.

from skimage import io
im = io.imread(jpg)

Where 'im' will be a numpy array. Note: I haven't tested this with PNG but it works with TIFF files

2
votes

I'm using png module: At first install png by:

>pip install pypng

Then

import png
import numpy as np
reader = png.Reader('16bit.png')
data = reader.asDirect()
pixels = data[2]
image = []
for row in pixels:
  row = np.asarray(row)
  row = np.reshape(row, [-1, 3])
  image.append(row)
image = np.stack(image, 1)
print(image.dtype)
print(image.shape)
0
votes

I've been playing with this image using PIL version 5.3.0:

enter image description here

it reads the data just fine:

>>> image = Image.open('/home/jcomeau/Downloads/grayscale_example.png')
>>> image.mode
'I'
>>> image.getextrema()
(5140, 62708)
>>> image.save('/tmp/test.png')

and it saves in the right mode, however the contents are not identical:

jcomeau@aspire:~$ diff /tmp/test.png ~/Downloads/grayscale_example.png 
Binary files /tmp/test.png and /home/jcomeau/Downloads/grayscale_example.png differ
jcomeau@aspire:~$ identify /tmp/test.png ~/Downloads/grayscale_example.png 
/tmp/test.png PNG 85x63 85x63+0+0 16-bit sRGB 6.12KB 0.010u 0:00.000
/home/jcomeau/Downloads/grayscale_example.png PNG 85x63 85x63+0+0 16-bit sRGB 6.14KB 0.000u 0:00.000

however, image.show() always converts to 8-bit grayscale, clamped at 0 and 255. so it's useless for seeing what you've got at any stage of the transformation. while I could write a routine to do so, and perhaps even monkeypatch .show(), I just run the display command in another xterm.

>>> image.putdata([n - 32768 for n in image.getdata()])
>>> image.getextrema()
(-27628, 29940)
>>> image.save('/tmp/test2.png')

darkened grayscale image

note that converting to mode I;16 doesn't help:

>>> image.convert('I;16').save('/tmp/test3.png')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jcomeau/.local/lib/python2.7/site-packages/PIL/Image.py", line 1969, in save
    save_handler(self, fp, filename)
  File "/home/jcomeau/.local/lib/python2.7/site-packages/PIL/PngImagePlugin.py", line 729, in _save
    raise IOError("cannot write mode %s as PNG" % mode)
IOError: cannot write mode I;16 as PNG
0
votes

You can also use the excellent OpenImageIO library's Python API.

import OpenImageIO as oiio
img_input = oiio.ImageInput.open("test.png")    # Only reads the image header
pix = img_input.read_image(format="uint16")     # Reads the pixels into a Numpy array

OpneImageIO is used extensively in the VFX industry, so most Linux distros come with a native package for it. Unfortunately the otherwise excellent documentation is in PDF format (I personally prefer HTML), look for it in /usr/share/doc/OpenImageIO.

0
votes

Another option to consider, based on Mr. Fridy's answer, is to load it using pypng like this:

import png
pngdata = png.Reader("path/to/16bit.png").read_flat()
img = np.array(pngdata[2]).reshape((pngdata[1], pngdata[0], -1))

You can install pypng using pip:

pip install pypng

The dtype from png.Reader.read_flat() is correctly uint16 and the reshaping of the np.ndarray puts it into (height, width, channels) format.

-1
votes

I suspect your "16 bit" PNG is not 16-bit. (if you're on Linux or Mac you could run file 16bit.png and see what it says)

When I use PIL and numpy I get a 32-bit array with 16-bit values in it:

import PIL.Image
import numpy

image = PIL.Image.open('16bit.png')   
pixel = numpy.array(image)

print "PIL:", pixel.dtype

print max(max(row) for row in pixel)

the output is:

PIL: int32
65535