Sunday, November 27, 2011

Rapid Bitmap Access using "Unsafe" Code (Pointers) in C#

Today we discuss efficient programmatic writing of Bitmaps in C#.

I've spent a fair amount of time developing computer-generated fractal art (link and link).  For example, this is one of my Newton fractals (which is derived from the time it takes for Newton's method to converge to a zero from various starting locations in the complex plane given a specified polynomial):
The Eye
One prerequisite is the ability to efficiently set pixels in a C# Bitmap object.  Today, I'll show a trick that I've used - that I'm sure I learned elsewhere, but have long since forgotten where - to speed up generating Bitmaps compared to a naive method.

A naive method to setting every pixel in the Bitmap is:

public static Bitmap drawNaive(Color[,] data)
{            
    int height = data.GetLength(0);
    int width = data.GetLength(1);
    
    Bitmap result = new Bitmap(width, height);
    for(int row = 0; row < height; row++)
    {
        for(int col = 0; col < width; col++)
        {
            Color color = data[row, col];
            
            //Set the pixel using the exposed method by the 
            // Bitmap class (slow!)
            result.SetPixel(col, row, color);
        }
    }
    
    return result;
}

However, this solution is relatively slow because it requires an expensive operation to lock and unlock the bitmap every time a pixel is set.  Must better performance can be had by using pointers (which is permitted when "unsafe" code is used).  Bitmaps are very simple objects that hold the intensity of Red, Green, Blue and the Alpha (transparency) channel at each pixel in four bytes (1 byte each) from the top-left corner, across each row, and then down the image to the bottom right corner.  Our strategy will be:
  1. Declare a struct representing a pixel
  2. Lock the bitmap once at the start
  3. Use pointers to scan through the image, updating pixels
  4. Unlock the bitmap once at the end
First we declare the struct for the pixel.  Note that the order of the fields matters and are chosen to match the way the Bitmap object's pixels are laid out in memory.

public struct PixelData
{
    public byte B;
    public byte G;
    public byte R;
    public byte A;
    
    public PixelData(byte r, byte g, byte b, byte a)
    {
        this.R = r;
        this.G = g;
        this.B = b;
        this.A = a;
    }
}

This can be used then to sweep along the Bitmap.

public static Bitmap drawPointer(Color[,] data)
{
    int height = data.GetLength(0);
    int width = data.GetLength(1);
    
    Bitmap result = new Bitmap(width, height);
    
    //Lock the entire bitmap (the rectangle argument) once
    BitmapData locked = result.LockBits(new Rectangle(0, 0,
                                        result.Width, result.Height),
                                        ImageLockMode.ReadWrite,
                                        PixelFormat.Format32bppArgb);
    
    //Enter unsafe mode so that we can use pointers
    unsafe
    {
        //Get a pointer to the beginning of the pixel data region
        //The upper-left corner
        PixelData* pixelPtr = (PixelData*)(void*)locked.Scan0;
        
        //Iterate through rows and columns
        for(int row = 0; row < height; row++)
        {
            for(int col = 0; col < width; col++)
            {
                Color color = data[row, col];
                
                //Set the pixel (fast!)
                *pixelPtr = new PixelData(color.R,
                                          color.G,
                                          color.B,
                                          color.A);
                
                //Update the pointer
                pixelPtr++;
            }
        }
    }
    
    //Unlock the bitmp
    result.UnlockBits(locked);
    
    return result;
}

This results in a significant speed-up.

1 comment: