Saving 16-bit TIFF images with Pillow in Python

1 minute read

  • Pillow has sparse documentation and poor support for saving 16-bit images.
  • Pillow is, however, a ubiquitous python package and simple to install and use.
  • Presented is a consistent way to save 16-bit TIFF images from data using Pillow.

Introduction

Recently I had a scientific image application that needed the saving of real 16-bit TIFF images to preserve all of the information in the source image. (biorad1sc_reader) While it can be argued that people aren’t sensitive to more than 8-bits of information, quantitative analysis of images can make use of the extra information.

Disappointingly, Pillow, the main image manipulation package for python, doesn’t seem to have great support for 16-bit TIFF images, nor does support seem forthcoming in the near future.  Luckily I found a mostly clean way of using Pillow to save 16-bit image data into a 16-bit TIFF.

Discussion

An issue when trying to save a new TIFF file as 16-bit in Pillow is that there doesn’t seem to be a native 16-bit unsigned integer format in the official “Modes” section of the Pillow documentation.  However, internally there is such a format, which can be found in the Pillow source code.

At first I tried to use Image.fromarray() to send my image list data to the new Image.  This seemed to run into many bugs of casting that resulted between my input numbers and the internal Pillow format.

In the end, the safest and least hack-ful way to send data to the new 16-bit Image turned out to use struct.pack() to create a bytestream and load that into the Pillow Image using Image.frombytes().  My code has versions for use with NumPy (“HAS_NUMPY”) or without NumPy support.  Code is below.

def save_u16_to_tiff(u16in, size, tiff_filename):
    """
    Since Pillow has poor support for 16-bit TIFF, we make our own
    save function to properly save a 16-bit TIFF.
    """
    # write 16-bit TIFF image

    # PIL interprets mode 'I;16' as "uint16, little-endian"
    img_out = Image.new('I;16', size)

    if HAS_NUMPY:
        # make sure u16in little-endian, output bytes
        outpil = u16in.astype(u16in.dtype.newbyteorder("<")).tobytes()
    else:
        # little-endian u16 format
        outpil = struct.pack(
                "<%dH"%(len(u16in)),
                *u16in
                )
    img_out.frombytes(outpil)
    img_out.save(tiff_filename)

Updated: