heightmapbitmapsource

WPF C# - Loading 16bit (8bit multi-channel) RAW Heightmap into BitmapSource


I'm trying to display a preview of a 16bit RAW image in my WPF application but it's not working as I was hoping it would. I've had a look at countless of examples, questions/answers here on SA and even on CodeProject but that didn't take me far, either. Well, actually it did since I can completely load an 8bit grayscale RAW image and display it on the application without any weird artifacts whatsoever, but when it comes to 16 bit images it get's all messed up.

Here's an example of how it should look: https://i.sstatic.net/kSCX5.jpg

And this is what I get when trying to load a 16bit version of the same raw image (directly exported as 16bit from a third-party program): https://i.sstatic.net/Melmk.jpg

Loading the 16bit image as 8bit gives me half of the correct image.

These are my import functions:

    BinaryReader br = new BinaryReader(File.Open(filename, FileMode.Open));

    private void LoadRAWData8bit()
    {
        int bits = format.BitsPerPixel;
        int stride = ((width * bits + (bits - 1)) & ~(bits - 1)) / 8;

        byte[] pixels = new byte[stride * height];

        for (int i = 0; i < br.BaseStream.Length; ++i)
            pixels[i] = br.ReadByte();

        br.Close();

        BitmapSource bmp = BitmapSource.Create(width, height, 96, 96, format, null, pixels, stride);

        RawFile = new CRAW(new Size(width, height), format, stride, bmp);
    }

    // 16bit load Test
    private void LoadRAWData16bit()
    {
        WriteableBitmap wBmp = new WriteableBitmap(width, height, 96, 96, format, null);
        int bpp = format.BitsPerPixel / 8;

        byte[] pixels = new byte[wBmp.PixelWidth * wBmp.PixelHeight];

        Int32Rect drawRegionRect = new Int32Rect(0, 0, wBmp.PixelWidth, wBmp.PixelHeight);

        for (int i = 0; i < br.BaseStream.Length; ++i)
            pixels[i] = br.ReadByte();

        br.Close();

        int stride = wBmp.PixelWidth * bpp;
        wBmp.WritePixels(drawRegionRect, pixels, stride, 0);

        RawFile = new CRAW(new Size(width, height), format, stride, wBmp);
    }

(Please don't mind the parameters passed to the CRAW class, I'm only experimenting for now until I can get this to work correctly.)

Am I missing a step? Does anyone know how to get this to work? I've tried to make the very same 8bit import function with an ushort array instead of a byte array:

private void LoadRAWData16bit()
    {
        int bits = format.BitsPerPixel;
        int stride = ((width * bits + (bits - 1)) & ~(bits - 1)) / 8;
        int bpp = format.BitsPerPixel / 8;

        ushort[] pixels = new ushort[stride * height * bpp];

        for (int i = 0; i < br.BaseStream.Length; i += sizeof(ushort))
            pixels[i] = br.ReadUInt16();

        br.Close();

        BitmapSource bmp = BitmapSource.Create(width, height, 96, 96, format, null, pixels, stride);

        RawFile = new CRAW(new Size(width, height), format, stride, bmp);
    }

like this, but doing so gives me half the image and it's alot darker than it's supposed to be because there's 1 byte every 2nd byte in the array having a value of 0. Doing pixels[i / 2] gives me again the same image like mentioned before (16bit loading sample).

Loading the 16bit raw image into Photoshop shows the import dialog which suggests a Channel count of 2 as 8bit (setting it to 16 bit with 2 Channels won't let me import it, but 16 bit with 1 Channel works).

This would explain why loading the 16bit image as 8bit into my WPF application only imports half of it.. but that leaves me with a big questionmark as to why it's not loading the 2nd channel too.

This subject is poorly documented and barely discussed anywhere. Can't find anything anywhere on this matter.. Hope to find someone who's able to help me out.

Thanks and happy new year!


Solution

  • Found the solution by myself by analyzing the .RAW image in a Hex editor.

    Basically, a multi-channel 8/16bit RAW image is structured as shown in this screenshot (8bit example): http://puu.sh/t97fY/6920f804af.png

    The first byte belongs to the image itself (also called Alpha 1 according to Photoshop), the second byte is the second channel (called Alpha 2). The bytes of each image layer are alternated as shown in the screenshot. In the screenshot shown above, the 0x00 bytes belong to the second channel while the other bytes (59, 57, 58, 56, etc) belong to the primary image.

    Splitting the 2 layers is pretty straightforward at this point. Since BitmapSource.Create generates a bitmap out of raw bytes, all we have to do is loop through the FileStream and alternate the destination buffer to fit the bytes in their correct buffers which will be fed to the BitmapSource class afterwards.

    In case the RAW image has no alpha channel, but just the plain image itself, you can allocate a buffer as big as the plain image and fill it with 0x00 which will give you a blank alpha layer.

    How to programmatically determine if a RAW image has an alpha channel is easy as well. Given width and height, a multi-channel RAW image will be twice the size of the base image.

    Example:

    Width: 128
    Height: 128
    RAW Image Size: 32768
    128 * 128 = 16384
    

    If

    file_size == width * height

    then we have no alpha channel.

    If

    file_size == width * height * 2

    then we've got an alpha channel.

    Hope this can help someone in the future. There are so many RAW image formats and it can get pretty confusing really quickly especially for those who work with Heightmaps/Photoshop RAW images trying to manipulate these images programmatically.. These formats don't seem to be well documented.. or perhaps I simply didn't dig enough, even though I spent a whole afternoon on this topic.