Friday, November 12, 2010

Converting 16 bit gray-scale TIFF images for use in WPF

When you try to open a 16 bit gray-scale TIFF image with WPF components you will only see a black screen. The image cannot be displayed by the Bitmap Decoder, because 16 bit gray-scale TIFFs are not supported. But this image format is widely used in scientific visualization (especially microscopy). So, if you want to display such images you first have to normalize them to 8 bit, which is the format, the decoder can read.
To use the image in WPF, we’ll be working with BitmapSource objects. Therefore we create the following method, which takes a BitmapSource representing the 16 bit gray-scale TIFF and returns an 8 bit BitmapSource:

public static BitmapSource NormalizeTiffTo8BitImage(BitmapSource source)

The following high level steps have to be taken to convert the image:
1.       Copy the pixels from the source to a byte array
2.       Get the max values of the first and the second byte per pixel
3.       Normalize each source pixel and copy it to a destination array
4.       Create a new BitmapSource out of the destination array

Let’s take a detailed look at the several steps:

1.    Copy pixels from source to byte array
This step is rather easy. To determine the length of the array, we first need the stride of the image. The stride specifies the number of bytes per image line. In the case of a 16 bit image that value is simply two times the image width (see the code below, were BitsPerPixel is in our case 16), because two bytes are used for each pixel. With the stride, we can initialize a new array and copy the pixels from the BitmapSource:

int rawStride = source.PixelWidth * source.Format.BitsPerPixel / 8;
var rawImage = new byte[rawStride * source.PixelHeight];

source.CopyPixels(rawImage, rawStride, 0);

2. Get the max values of the first and the second byte per pixel
To get our factors for normalizing, we need to determine the max values within the image of both byte values per pixel. In a 16 bit gray-scale TIFF each pixel is represented by two byte values, each between 0 and 255. The first value will always be greater or at least the same as the second one. So we can use an extension method to get the maximum of the first byte value:

ushort maxValue = rawImage.Max();

For the second value, we have to iterate through the array:

int max = 0;
for (int i = 0; i < rawImage.Length; i++)
    if (rawImage[i + 1] > max)
        max = rawImage[i + 1];
if (max == 0) max = 1;

With these two values, we can now define the two needed normalizing factors as follows:

int normFaktor = max == 1 ? 1 : 128 / max;
int faktor = 255 / maxValue;

3. Normalize each source pixel and copy it to a destination array
This step is where the real work is done. Again we need to iterate through the array, take both byte values per pixel, normalize them to one byte value and store the new value in a separate array. What we do here is a projection from the range of 0 to 65536 to a range from 0 to 255:

var buffer8Bit = new byte[rawImage.Length / 2];

for (int src = 0, dst = 0; src < rawImage.Length; dst++)
    int value16 = rawImage[src++];
    int value8 = ((value16 * faktor) / max) - normFaktor;

    if (rawImage[src] > 0)
        int b = rawImage[src] << 8;
        value8 = ((value16 + b) / max) - normFaktor;

    buffer8Bit[dst] = (byte)Math.Max(value8, 0);

4. Create a new BitmapSource out of the destination array
This is again an easy step, because BitmapSource provides a static create method that takes a source array beneath other parameters to create a new BitmapSource:

return BitmapSource.Create(source.PixelWidth, source.PixelHeight,
    source.DpiX, source.DpiY, PixelFormats.Gray8, BitmapPalettes.Gray256,
    buffer8Bit, rawStride / 2);

The newly created BitmapSource can now be displayed within a WPF application.

Some additional words about the sample code:
It took some time and search to find out that algorithm (thanks to all people in forums and web sites who helped me on this) and even after converting lot of sample images I’m not completely sure if it works for all 16 bit gray-scale TIFF images, because TIFF is a very variable and extensible format. But I think it gives a very good starting point.