Click here to Skip to main content
Click here to Skip to main content

.NET Bitmaps with Full 16bit Support

, 18 Mar 2014
Rate this:
Please Sign up or sign in to vote.
Reading and writing Bitmaps with full 16bit per channel (and possibly more)

Introduction

If you have ever tried to read 16bit images with the System.Drawing.Bitmap class, you may have been bitterly disappointed. For some obscure reason, it stores 16bit data only with 13bit (from 65535 values to 8192 is quite the difference) and even that doesn't work properly.

But here is the good news: there is a workaround which I will present here!

Background

To get this code working, you need to reference four .NET DLLs:

  • System
  • PresentationCore
  • WindowsBase
  • System.Xaml

The Code

I made a rather simple class to store and handle the data and provide the basic methods. You can add methods you may need as you wish. At the end, I added the whole class in one piece so you can copy/paste it easier.

First, the variables of this class:

public int Width;           //Width of image in pixel
public int Height;          //Height of image in pixel
public int ChannelCount;    //Number of channels
public int BytesPerChannel; //Number of bytes per channel
public bool HasAlpha;       //Image has alpha channel or not
public int BitDepth;        //Bits per channel
public bool IsBGR;          //Byte order BGR or RGB
public bool IsPinned;       //ImageData pinned or not
public IntPtr Scan0;        //Position of first byte (Set when ImageData is pinned)

private PixelFormat InputPixelFormat;  //Pixel format
private GCHandle pinHandle;            //Handler to pin/unpin ImageData
private byte[] ImageData;              //The image data itself  

Now the constructors, basically we create a BitampSource class in various ways:

/// <summary>
/// Loads an image from a path. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
/// </summary>
/// <param name="path">Path to the image</param>
public BitmapEx(string path)
{
    using (Stream str = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        BitmapDecoder dec = BitmapDecoder.Create(str, 
            BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
        if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
        else throw new FileLoadException("Couldn't load file " + path);
    }
}

/// <summary>
/// Loads an image from an encoded stream. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
/// </summary>
public BitmapEx(Stream encodedStream)
{
    encodedStream.Position = 0;
    BitmapDecoder dec = BitmapDecoder.Create(encodedStream, 
        BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
    if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
    else throw new FileLoadException("Couldn't load file");
}

/// <summary>
/// Loads an image from an encoded byte array. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
/// </summary>
public BitmapEx(byte[] encodedData)
{
    using (MemoryStream str = new MemoryStream(encodedData))
    {
        BitmapDecoder dec = BitmapDecoder.Create(str, 
            BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
        if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
        else throw new FileLoadException("Couldn't load file");
    }
}

Now we have the BitampSource, we can read our information from that and store the image data:

private void SetFromBitmapSource(BitmapSource bmpSrc)
{
    this.Width = bmpSrc.PixelWidth;
    this.Height = bmpSrc.PixelHeight;

    InputPixelFormat = bmpSrc.Format;
    if (bmpSrc.Format == PixelFormats.Bgr24)
    {
        this.ChannelCount = 3;
        this.BytesPerChannel = 1;
        this.HasAlpha = false;
        this.BitDepth = 8;
        this.IsBGR = true;
    }
    else if (bmpSrc.Format == PixelFormats.Bgra32)
    {
        this.ChannelCount = 4;
        this.BytesPerChannel = 1;
        this.HasAlpha = true;
        this.BitDepth = 8;
        this.IsBGR = true;
    }
    else if (bmpSrc.Format == PixelFormats.Rgb24)
    {
        this.ChannelCount = 3;
        this.BytesPerChannel = 1;
        this.HasAlpha = false;
        this.BitDepth = 8;
        this.IsBGR = false;
    }
    else if (bmpSrc.Format == PixelFormats.Rgb48)
    {
        this.ChannelCount = 3;
        this.BytesPerChannel = 2;
        this.HasAlpha = false;
        this.BitDepth = 16;
        this.IsBGR = false;
    }
    else
    {
        //There are some more special cases you might want to handle
        //Also, it's possible that bmpSrc.Format == PixelFormats.Default
        //Then you can only check for BitsPerPixel field 
        //and guess the channel order (I assume it's BGR)
        throw new NotImplementedException();
    }

    int stride = this.Width * this.BytesPerChannel * this.ChannelCount;
    ImageData = new byte[this.Height * stride]; //Init our byte array with the right size
    bmpSrc.CopyPixels(ImageData, stride, 0); //Copy image data to our byte array
} 

Great, we have our image! You probably want to do stuff with it now. To do that, pin the data to work with it and unpin it when you are finished. I added an example on how to manipulate the data at the end.

public void LockBits()
{
    if (!IsPinned)
    {
        pinHandle = GCHandle.Alloc(ImageData, GCHandleType.Pinned); //Pin the image data
        Scan0 = pinHandle.AddrOfPinnedObject();  //Get the pointer to the first byte
        IsPinned = true;
    }
}

public void UnlockBits()
{
    if (IsPinned)
    {
        pinHandle.Free();  //Unpin the image data
        IsPinned = false;
    }
}

Good good. After you are finished with the image, you might want to save it again. So let's do that:

/// <summary>
/// Saves the image to a path. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
/// </summary>
/// <param name="path">Path where the image should be saved to</param>
public void Save(string path)
{
    string ext = Path.GetExtension(path).ToLower();
    using (FileStream str = new FileStream(path, FileMode.Create)) { this.Save(str, ext); }
}

/// <summary>
/// Saves the image into a stream.
/// </summary>
/// <param name="ext">Extension of the desired file format. 
/// Allowed values: ".jpg", ".jpeg", ".png", ".tiff", 
/// ".tif", ".bmp", ".gif", ".wdp"</param>
/// <param name="str">The stream where the image will be saved to.</param>
public void Save(Stream str, string ext)
{
    BitmapEncoder enc;  //Find the right encoder
    switch (ext)
    {
        case ".jpg":
        case ".jpeg": enc = new JpegBitmapEncoder(); 
            ((JpegBitmapEncoder)enc).QualityLevel = 100; break;
        case ".tif":
        case ".tiff": enc = new TiffBitmapEncoder(); 
            ((TiffBitmapEncoder)enc).Compression = TiffCompressOption.Lzw; break;
        case ".png": enc = new PngBitmapEncoder(); break;
        case ".bmp": enc = new BmpBitmapEncoder(); break;
        case ".wdp": enc = new WmpBitmapEncoder(); break;

        default:
            throw new ArgumentException("File format not supported *" + ext);
    }
    //Create a BitmapSource from all the data
    BitmapSource src = BitmapSource.Create((int)this.Width, (int)this.Height, 96, 96, 
        InputPixelFormat, null, ImageData, (int)(this.Width * this.BytesPerChannel * this.ChannelCount));
    enc.Frames.Add(BitmapFrame.Create(src));  //Add the data to the first frame
    enc.Save(str);   //Save the data to the stream
}

Example of Use

Here are examples on how to use the class. Replace the CalcNewRed, CalcNewGreen, CalcNewBlue methods with something useful where you calculate new values.

To ensure the new values are in range, this seems to be the fastest way (where MAXVALUE is the biggest value it can have, e.g. 255 for byte or 65535 for ushort)

 Math.Max(0, Math.Min(value, MAXVALUE)

This is how it would work with an 8-bit image.

BitmapEx bmp = new BitmapEx("Example.jpg");
try
{
    bmp.LockBits();
    unsafe
    {
        int index, x, y;
        int stride = bmp.Width * bmp.ChannelCount;
        byte* pix = (byte*)bmp.Scan0;
        for (y = 0; y < bmp.Height; y++)
        {
            for (x = 0; x < bmp.Width; x++)
            {
                index = y * stride + (x * bmp.ChannelCount);

                float newR = CalcNewRed();
                float newG = CalcNewGreen();
                float newB = CalcNewBlue();

                if (bmp.IsBGR)
                {
                    pix[index] = (byte)Math.Max(0, Math.Min(newB, 255));     //Blue
                    pix[index + 1] = (byte)Math.Max(0, Math.Min(newG, 255)); //Green
                    pix[index + 2] = (byte)Math.Max(0, Math.Min(newR, 255)); //Red
                }
                else
                {
                    pix[index] = (byte)Math.Max(0, Math.Min(newR, 255));     //Red
                    pix[index + 1] = (byte)Math.Max(0, Math.Min(newG, 255)); //Green
                    pix[index + 2] = (byte)Math.Max(0, Math.Min(newB, 255)); //Blue
                }
                if (bmp.HasAlpha) pix[index + 3] = 0;   //Alpha
            }
        }
    }
}
finally { bmp.UnlockBits(); }
bmp.Save("ExampleOut.jpg");

And this is how it would work with a 16-bit image.

BitmapEx bmp = new BitmapEx("Example.tif");
try
{
    bmp.LockBits();
    unsafe
    {
        int index, x, y;
        int stride = bmp.Width * bmp.ChannelCount;
        ushort* pix = (ushort*)bmp.Scan0;
        for (y = 0; y < bmp.Height; y++)
        {
            for (x = 0; x < bmp.Width; x++)
            {
                index = y * stride + (x * bmp.ChannelCount);

                float newR = CalcNewRed();
                float newG = CalcNewGreen();
                float newB = CalcNewBlue();

                if (bmp.IsBGR)
                {
                    pix[index] = (ushort)Math.Max(0, Math.Min(newB, 65535));     //Blue
                    pix[index + 1] = (ushort)Math.Max(0, Math.Min(newG, 65535)); //Green
                    pix[index + 2] = (ushort)Math.Max(0, Math.Min(newR, 65535)); //Red
                }
                else
                {
                    pix[index] = (ushort)Math.Max(0, Math.Min(newR, 65535));     //Red
                    pix[index + 1] = (ushort)Math.Max(0, Math.Min(newG, 65535)); //Green
                    pix[index + 2] = (ushort)Math.Max(0, Math.Min(newB, 65535)); //Blue
                }
                if (bmp.HasAlpha) pix[index + 3] = 0;   //Alpha
            }
        }
    }
}
finally { bmp.UnlockBits(); }
bmp.Save("ExampleOut.tif");

With more bits, it would work the same, just use the correct value type (uint for 32 bit for example) and don't forget to change the max value.

Full Source Code

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Media;
using System.Windows.Media.Imaging;

public class BitmapEx
{
    public int Width;
    public int Height;

    public int ChannelCount;
    public int BytesPerChannel;
    public bool HasAlpha;
    public int BitDepth;
    public bool IsBGR;
    public bool IsPinned;
    public IntPtr Scan0;

    private PixelFormat InputPixelFormat;
    private GCHandle pinHandle;
    private byte[] ImageData;

        
    /// <summary>
    /// Loads an image from a path. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
    /// </summary>
    /// <param name="path">Path to the image</param>
    public BitmapEx(string path)
    {
        using (Stream str = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            BitmapDecoder dec = BitmapDecoder.Create(str, 
                BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
            if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
            else throw new FileLoadException("Couldn't load file " + path);
        }
    }

    /// <summary>
    /// Loads an image from an encoded stream. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
    /// </summary>
    public BitmapEx(Stream encodedStream)
    {
        encodedStream.Position = 0;
        BitmapDecoder dec = BitmapDecoder.Create(encodedStream, 
            BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
        if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
        else throw new FileLoadException("Couldn't load file");
    }

    /// <summary>
    /// Loads an image from an encoded byte array. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
    /// </summary>
    public BitmapEx(byte[] encodedData)
    {
        using (MemoryStream str = new MemoryStream(encodedData))
        {
            BitmapDecoder dec = BitmapDecoder.Create(str, 
                BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
            if (dec.Frames.Count > 0) SetFromBitmapSource(dec.Frames[0]);
            else throw new FileLoadException("Couldn't load file");
        }
    }
        
    private void SetFromBitmapSource(BitmapSource bmpSrc)
    {
        this.Width = bmpSrc.PixelWidth;
        this.Height = bmpSrc.PixelHeight;

        InputPixelFormat = bmpSrc.Format;
        if (bmpSrc.Format == PixelFormats.Bgr24)
        {
            this.ChannelCount = 3;
            this.BytesPerChannel = 1;
            this.HasAlpha = false;
            this.BitDepth = 8;
            this.IsBGR = true;
        }
        else if (bmpSrc.Format == PixelFormats.Bgra32)
        {
            this.ChannelCount = 4;
            this.BytesPerChannel = 1;
            this.HasAlpha = true;
            this.BitDepth = 8;
            this.IsBGR = true;
        }
        else if (bmpSrc.Format == PixelFormats.Rgb24)
        {
            this.ChannelCount = 3;
            this.BytesPerChannel = 1;
            this.HasAlpha = false;
            this.BitDepth = 8;
            this.IsBGR = false;
        }
        else if (bmpSrc.Format == PixelFormats.Rgb48)
        {
            this.ChannelCount = 3;
            this.BytesPerChannel = 2;
            this.HasAlpha = false;
            this.BitDepth = 16;
            this.IsBGR = false;
        }
        else
        {
            //There are some more special cases you might want to handle
            //Also, it's possible that bmpSrc.Format == PixelFormats.Default
            //Then you can only check for BitsPerPixel field and guess the channel order (I assume it's BGR)
            throw new NotImplementedException();
        }

        int stride = this.Width * this.BytesPerChannel * this.ChannelCount;
        ImageData = new byte[this.Height * stride];
        bmpSrc.CopyPixels(ImageData, stride, 0);
    }

    public void LockBits()
    {
        if (!IsPinned)
        {
            pinHandle = GCHandle.Alloc(ImageData, GCHandleType.Pinned);
            Scan0 = pinHandle.AddrOfPinnedObject();
            IsPinned = true;
        }
    }

    public void UnlockBits()
    {
        if (IsPinned)
        {
            pinHandle.Free();
            IsPinned = false;
        }
    }

    /// <summary>
    /// Saves the image to a path. (Jpg, Png, Tiff, Bmp, Gif and Wdp are supported)
    /// </summary>
    /// <param name="path">Path where the image should be saved to</param>
    public void Save(string path)
    {
        string ext = Path.GetExtension(path).ToLower();
        using (FileStream str = new FileStream(path, FileMode.Create)) { this.Save(str, ext); }
    }

    /// <summary>
    /// Saves the image into a stream.
    /// </summary>
    /// <param name="ext">Extension of the desired file format. 
    /// Allowed values: ".jpg", ".jpeg", ".png", 
    /// ".tiff", ".tif", ".bmp", ".gif", ".wdp"</param>
    /// <param name="str">The stream where the image will be saved to.</param>
    public void Save(Stream str, string ext)
    {
        BitmapEncoder enc;
        switch (ext)
        {
            case ".jpg":
            case ".jpeg": enc = new JpegBitmapEncoder(); 
                ((JpegBitmapEncoder)enc).QualityLevel = 100; break;
            case ".tif":
            case ".tiff": enc = new TiffBitmapEncoder(); 
                ((TiffBitmapEncoder)enc).Compression = TiffCompressOption.Lzw; break;
            case ".png": enc = new PngBitmapEncoder(); break;
            case ".bmp": enc = new BmpBitmapEncoder(); break;
            case ".wdp": enc = new WmpBitmapEncoder(); break;

            default:
                throw new ArgumentException("File format not supported *" + ext);
        }

        BitmapSource src = BitmapSource.Create((int)this.Width, (int)this.Height, 96, 96, 
        InputPixelFormat, null, ImageData, (int)(this.Width * this.BytesPerChannel * this.ChannelCount));
        enc.Frames.Add(BitmapFrame.Create(src));
        enc.Save(str);
    }
}

Points of Interest

You may use this code as you wish. See MIT license for more information.

With a little bit more effort, you could also use this class to handle gray images (i.e. one-channel images) or some of the more special pixel formats.

Any questions left? Leave me a comment, I don't bite (maybe Wink | ;) )!

History

  • March 2014 - Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web02 | 2.8.140821.2 | Last Updated 18 Mar 2014
Article Copyright 2014 by Johannes Bildstein
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid