# Can I save a numpy array as a 16-bit image using “normal” (Enthought) python?

Posted on

### Question :

Can I save a numpy array as a 16-bit image using “normal” (Enthought) python?

Is there any way to save a numpy array as a 16 bit image (tif, png) using any of the commonly available python packages? This is the only way that I could get to work in the past, but I needed to install the FreeImage package, which is a little annoying.

This seems like a pretty basic task, so I would expect that it should be covered by scipy, but scipy.misc.imsave only does 8-bits.

Any ideas?

One alternative is to use pypng. You’ll still have to install another package, but it is pure Python, so that should be easy. (There is actually a Cython file in the pypng source, but its use is optional.)

Here’s an example of using pypng to write numpy arrays to PNG:

``````import png

import numpy as np

# The following import is just for creating an interesting array
# of data.  It is not necessary for writing a PNG file with PyPNG.
from scipy.ndimage import gaussian_filter

# Make an image in a numpy array for this demonstration.
nrows = 240
ncols = 320
np.random.seed(12345)
x = np.random.randn(nrows, ncols, 3)

# y is our floating point demonstration data.
y = gaussian_filter(x, (16, 16, 0))

# Convert y to 16 bit unsigned integers.
z = (65535*((y - y.min())/y.ptp())).astype(np.uint16)

# Use pypng to write z as a color PNG.
with open('foo_color.png', 'wb') as f:
writer = png.Writer(width=z.shape, height=z.shape, bitdepth=16)
# Convert z to the Python list of lists expected by
# the png writer.
z2list = z.reshape(-1, z.shape*z.shape).tolist()
writer.write(f, z2list)

# Here's a grayscale example.
zgray = z[:, :, 0]

# Use pypng to write zgray as a grayscale PNG.
with open('foo_gray.png', 'wb') as f:
writer = png.Writer(width=z.shape, height=z.shape, bitdepth=16, greyscale=True)
zgray2list = zgray.tolist()
writer.write(f, zgray2list)
``````

Here’s the color output: and here’s the grayscale output: Update: I recently created a github repository for a module called `numpngw` that provides a function for writing a numpy array to a PNG file. The repository has a `setup.py` file for installing it as a package, but the essential code is in a single file, `numpngw.py`, that could be copied to any convenient location. The only dependency of `numpngw` is numpy.

Here’s a script that generates the same 16 bit images as those shown above:

``````import numpy as np
import numpngw

# The following import is just for creating an interesting array
# of data.  It is not necessary for writing a PNG file with PyPNG.
from scipy.ndimage import gaussian_filter

# Make an image in a numpy array for this demonstration.
nrows = 240
ncols = 320
np.random.seed(12345)
x = np.random.randn(nrows, ncols, 3)

# y is our floating point demonstration data.
y = gaussian_filter(x, (16, 16, 0))

# Convert y to 16 bit unsigned integers.
z = (65535*((y - y.min())/y.ptp())).astype(np.uint16)

# Use numpngw to write z as a color PNG.
numpngw.write_png('foo_color.png', z)

# Here's a grayscale example.
zgray = z[:, :, 0]

# Use numpngw to write zgray as a grayscale PNG.
numpngw.write_png('foo_gray.png', zgray)
``````

This explanation of png and numpngw is very helpful! But, there is one small “mistake” I thought I should mention. In the conversion to 16 bit unsigned integers, the y.max() should have been y.min(). For the picture of random colors, it didn’t really matter but for a real picture, we need to do it right. Here’s the corrected line of code…

``````z = (65535*((y - y.min())/y.ptp())).astype(np.uint16)
``````

You can convert your 16 bit array to a two channel image (or even 24 bit array to a 3 channel image). Something like this works fine and only numpy is required:

``````import numpy as np
arr = np.random.randint(0, 2 ** 16, (128, 128), dtype=np.uint16)  # 16-bit array
print(arr.min(), arr.max(), arr.dtype)
img_bgr = np.zeros((*arr.shape, 3), np.int)
img_bgr[:, :, 0] = arr // 256
img_bgr[:, :, 1] = arr % 256
cv2.imwrite('arr.png', img_bgr)
# Read image and check if our array is restored without losing precision
B, G, R = np.split(img_bgr_read, [1, 2], 2)
arr_read = (B * 256 + G).astype(np.uint16).squeeze()
``````

Result:

``````0 65523 uint16
True 0
``````

As mentioned, PyPNG is very useful. For Enthought users it can be installed as e.g.:

``````conda install -c eaton-lab pypng
``````

I’d use the `from_array` method of the shelf:

``````import png
import numpy as np

bit_depth = 16

my_array = np.ones((800, 800, 3))

png.from_array(my_array*2**bit_depth-1, 'RGB;%s'%bit_depth).save('foo.png')
``````

Mode uses PIL style format, e.g. ‘L’, ‘LA’, ‘RGB’ or ‘RGBA’, followed by ‘;16’ or ‘;8’ too set bit depth. If bit depth is omitted, the dtype of the array is used.

Created a custom script to do this using just numpy and OpenCV:
(Still feels like a huge overkill though…)

``````import numpy as np
import cv2

def save_gray_deep_bits(filepath, float_array, bitdepth=16):
assert bitdepth in [8,16,24]
arr = np.squeeze(float_array)
assert len(arr.shape) == 2
assert '.png' in filepath

bit_iterations = int(bitdepth/8)
img_bgr = np.zeros((*arr.shape, 3), np.uint8)
encoded = np.zeros(arr.shape, np.uint8)

for i in range(bit_iterations):
residual = float_array - encoded
plane_i = (residual*(256**i)).astype(np.uint8)
img_bgr[:,:,i] = plane_i
encoded += plane_i

cv2.imwrite(filepath, img_bgr)
return img_bgr

def bgr_to_gray_deep_bits(bgr_array, bitdepth=16):
gray = np.zeros((bgr_array.shape, bgr_array.shape), dtype = np.float32)
for i in range(int(bitdepth/8)):
gray += bgr_array[:,:,i] / float(256**i)
return gray