Manipulating raw bitmap data in .NET

The Bitmap class found in the .NET Framework provides a lot of useful functionality.   Unfortunately, it doesn’t have any methods that let you easily manipulate the raw bitmap data.   It provides a SetPixel method which takes x,y coordinates and a color value, and does exactly what you’d think it would.    Unfortunately, the implementation is not the most efficient.   It will lock the entire bitmap before modifying the data.    This is fine if you only need to manipulate a few pixels, but horrible if you need to manipulate the entire image.   Needless to say, the overhead of locking and unlocking the data just to modify one measly little pixel is detrimental for performance.

I found a few articles on the best way to manipulate the raw data.    The simplest and most intuitive way is to create a memory stream, save the bitmap to it, and then manipulate the bytes in the memory stream.     Once you are done, you recreate the bitmap based on the modified memory stream.   For example, this pedeantic snippet sets all the bits in the bitmap purple (based on this tutorial article):

MemoryStream ms = new MemoryStream();

bmp.Save(ms, ImageFormat.Bmp);
byte[] bitmapData = ms.GetBuffer();

const int BITMAP_HEADER_OFFSET = 54;
Color colorValue = Color.Purple;

for (int i = 0; i < bitmapData.Length; i += 4)
{
    bitmapData[BITMAP_HEADER_OFFSET + i] = colorValue.R;
    bitmapData[BITMAP_HEADER_OFFSET + i + 1] = colorValue.G;
    bitmapData[BITMAP_HEADER_OFFSET + i + 2] = colorValue.B;
    bitmapData[BITMAP_HEADER_OFFSET + i + 3] = colorValue.A;
}

bmp = new Bitmap(ms);

Once you have the bytes themselves you can begin manipulating them.      Each pixel is represented by a four bytes, consisting of the Red, Green, Blue, and Alpha (this controls transparency) values, respectively.     Divide the current byte index by four to get the pixel you are on.    From there you can mod by the bitmap width to get the x coordinate, and divide by the bitmap height to get the y coordinate.

One thing to note is that the raw image data does not begin at byte 0.     The first 54 bytes of the data consists of the bitmap header which contains various metadata. The problem with this code of course, is that it hard codes this offset and assumes it will never change.    This ties the code to the internal implementation details of the Bitmap class which violates encapsulation.   Not the best practice.

So another way of accomplishing the same task that does not require knowledge of the internal structure of a windows bitmap is to pin the bitmap object in memory and copy the data over to a local buffer. The data can then be manipulated locally and then copied back to the bitmap, which can then be unpinned from memory.    The following is based on the MSDN example.

// Lock the bitmap's bits.
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
System.Drawing.Imaging.BitmapData bmpData =
bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
bmp.PixelFormat);

// Get the address of the first line.
IntPtr ptr = bmpData.Scan0;

// Declare an array to hold the bytes of the bitmap.
byte[] bitmapData = new byte[Math.Abs(bmpData.Stride) * bmp.Height];

// Copy the RGB values into the array.
System.Runtime.InteropServices.Marshal.Copy(ptr, bitmapData, 0, bitmapData.Length);

Color colorValue = Color.Purple;
for (int i = 0; i < bitmapData.Length; i += 4)
{
    bitmapData[i] = colorValue.R;
    bitmapData[i + 1] = colorValue.G;
    bitmapData[i + 2] = colorValue.B;
    bitmapData[i + 3] = colorValue.A;
}

// Copy the RGB values back to the bitmap
System.Runtime.InteropServices.Marshal.Copy(bitmapData, 0, ptr, bitmapData.Length);

// Unlock the bits.
bmp.UnlockBits(bmpData);

Note the use of the Bitmap data class, particularly the use of Scan0 and Stride.   Scan0  gets the memory location of the actual image data itself, completely bypassing the problem of figuring out the end of the bitmap header.    Stride represents the width in bytes of a bitmap object.

The Lock and UnlockBits are used to pin the bitmap object in memory.     What this means is that this object now has a fixed location in memory and cannot be moved.    Typically, the garbage collector moves objects around during the lifetime of an application.    This is done to avoid memory fragmentation.   For example, the garbage collector has a compaction phase where objects that are still being referenced are packed together in the heap.      This solves the problem of being unable to allocate a block of memory even though there is enough free memory (imagine for example, that there is 3KB of free memory, but a 3KB block cannot be allocated because the 3KB is not contiguous and is spread out in small chunks throughout the heap) Pinning the bitmap allows us to safely copy the data back and forth from the object without having to worry whether or not that memory location is still the correct address.

To test the performance, I wrote a simple program to set all the pixels in a 1000 x 1000 bitmap to purple.      The pinning method is slightly faster, but not by much.   Here is the code:

using System;
using System.Drawing;
using System.IO;
using System.Drawing.Imaging;

namespace BitmapTestFramework
{
    class Program
    {
        static void Main(string[] args)
        {
            Bitmap testBitmap = new Bitmap(1000, 1000);
            DateTime start;
            DateTime end;
            TimeSpan timeSpan;

            Console.WriteLine(string.Format("Method 1:   Calling Get/Set Pixel", testBitmap.Width, testBitmap.Height));
            start = DateTime.UtcNow;
            Method1(testBitmap);
            end = DateTime.UtcNow;
            timeSpan = end - start;
            Console.WriteLine(string.Format("Completed in {0} milliseconds", timeSpan.TotalMilliseconds));

            Console.WriteLine("Method 2:  Create a memory stream of the bitmap and manipulate the buffer");
            start = DateTime.UtcNow;
            Method2(testBitmap);
            end = DateTime.UtcNow;
            timeSpan = end - start;
            Console.WriteLine(string.Format("Completed in {0} milliseconds", timeSpan.TotalMilliseconds));

            Console.WriteLine("Method 3:   Pin the bitmap object in memory and manipulate a local copy of the data");
            start = DateTime.UtcNow;
            Method3(testBitmap);
            end = DateTime.UtcNow;
            timeSpan = end - start;
            Console.WriteLine(string.Format("Completed in {0} milliseconds", timeSpan.TotalMilliseconds));
        }

        static void Method1(Bitmap bmp)
        {
            for (int x = 0; x < bmp.Width; x++)
            {
                for (int y = 0; y < bmp.Height; y++)
                {
                    bmp.SetPixel(x, y, Color.Purple);
                }
            }
        }

        static void Method2(Bitmap bmp)
        {
            MemoryStream ms = new MemoryStream();
            bmp.Save(ms, ImageFormat.Bmp);
            byte[] bitmapData = ms.GetBuffer();

            const int BITMAP_HEADER_OFFSET = 54;

            Color colorValue = Color.Purple;
            for (int i = 0; i < bitmapData.Length* 4; i += 4)
            {
                bitmapData[BITMAP_HEADER_OFFSET + i] = colorValue.R;
                bitmapData[BITMAP_HEADER_OFFSET + i + 1] = colorValue.G;
                bitmapData[BITMAP_HEADER_OFFSET + i + 2] = colorValue.B;
                bitmapData[BITMAP_HEADER_OFFSET + i + 3] = colorValue.A;
            }

            bmp = new Bitmap(ms);
        }

        private static void Method3(Bitmap bmp)
        {
            // Lock the bitmap's bits.
            Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
            System.Drawing.Imaging.BitmapData bmpData =bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, bmp.PixelFormat);

            // Get the address of the first line.
            IntPtr ptr = bmpData.Scan0;

            // Declare an array to hold the bytes of the bitmap.
            byte[] bitmapData = new byte[Math.Abs(bmpData.Stride) * bmp.Height];

            // Copy the RGB values into the array.
            System.Runtime.InteropServices.Marshal.Copy(ptr, bitmapData, 0, bitmapData.Length);

            Color colorValue = Color.Purple;
            for (int i = 0; i < bitmapData.Length; i += 4)
            {
                bitmapData[i] = colorValue.R;
                bitmapData[i + 1] = colorValue.G;
                bitmapData[i + 2] = colorValue.B;
                bitmapData[i + 3] = colorValue.A;
           }

            // Copy the RGB values back to the bitmap
            System.Runtime.InteropServices.Marshal.Copy(bitmapData, 0, ptr, bitmapData.Length);

             // Unlock the bits.
             bmp.UnlockBits(bmpData);
        }
    }
}

Here is the output on my machine:

Method 1:   Calling Get/Set Pixel
Completed in 649.414 milliseconds
Method 2:  Create a memory stream of the bitmap and manipulate the buffer
Completed in 91.7969 milliseconds
Method 3:   Pin the bitmap object in memory and manipulate a local copy of the data
Completed in 82.0312 milliseconds

Leave a Reply

Your email address will not be published. Required fields are marked *