2

I did some image-processing on multi-frame TIFF images from a 12-bit camera and would like to save the output. However, the PIL documentation does not list a 12-bit mode for fromarray(). How does PIL handle bit depth and how can I ensure that the saved TIFF images will have the same dynamic range as the original ones?

Example code:

import os

import numpy as np
import matplotlib.pyplot as plt

from PIL import Image


# Read image file names
pathname = '/home/user/images/'
filenameList = [filename for filename in os.listdir(pathname)
                if filename.endswith(('.tif', '.TIF', '.tiff', '.TIFF'))]

# Open image files, average over all frames, save averaged image files
for filename in filenameList:
    img = Image.open(pathname + filename)
    X, Y = img.size
    NFrames = img.n_frames

    imgArray = np.zeros((Y, X))
    for i in range(NFrames):
        img.seek(i)
        imgArray += np.array(img)
        i += 1
    imgArrayAverage = imgArray/NFrames

    imgAverage = Image.fromarray(imgArrayAverage)    # <=== THIS!!!
    imgAverage.save(pathname + filename.rsplit('.')[0] + '.tif')

    img.close()
david
  • 201
  • 2
  • 10
  • You could use the second answer in this [link] (https://stackoverflow.com/questions/53776506/how-to-save-an-array-representing-an-image-with-40-band-to-a-tif-file) Cheers! – Aravind Aug 08 '20 at 06:34
  • @Aravind: Hi, thanks for your help. Which answer do you mean? I couldn't find any info on how to deal with the dynamic range/bit depth issue :o) – david Aug 09 '20 at 18:06

1 Answers1

0

In my experience, 12-bit images get opened as 16-bit images with the first four MSB as all zeroes. My solution has been to convert the images to numpy arrays using

arr = np.array(img).astype(np.uint16)

the astype() directive is probably not strictly necessary, but it seems like it's a good idea. Then to convert to 16-bit, shift your binary digits four to the left:

arr = np.multiply(arr,2**4)

If you want to work with 8-bit instead,

arr = np.floor(np.divide(arr,2**4)).astype(np.uint8)

where here the astype() is necessary to force conversion to 8-bit integers. I think that the 8-bit truncation implicitly performs the floor() function but I left it in just in case.

Finally, convert back to PIL Image object and you're good to go:

img = Image.fromarray(arr)

For your specific use-case, this would have the same effect:

imgAverage = Image.fromarray(imgarrayAverage.astype(np.uint16) * 2**4)

The type conversion again may not be necessary but it will probably save you time since dividing imgArray by NFrames should implicity result in an array of floats. If you're worried about precision, it could be omitted.