This article describes fast bitmap color depth change. After my previous code sample, I've got some e-mails from people who were interested in my TTF convert solution, but they claimed that my code has poor performance. Ok, guys, (thanks a lot to all of you) you are right. I've developed my utility to run it on bitmaps with a size of 16X16 pixels. When you try to run it on bigger bitmaps, it will take much more time. For example, it will take more than 20 minutes to convert a 1024x768 bitmap (I run Centrino 1.8G).
So let's see the reasons for such bad results. Look at the ConvertTo8bppFormat method. The core of this method is a double loop (width X height), so for big bitmaps (1024x768 and bigger) there are millions and more iterations. During iteration, the code reads pixel's information from the source bitmap, then matches some known color (256 iterations in the worst case) and then copies it to the destination. So I'll perform the following steps to improve performance:
- Decrease the number of iterations, its mean, read all source information at once and copy all destination information at once.
- Decrease the number of property and method invocations of .NET classes. For example, see the double loop of
ConvertTo8bppFormat. There are two invocations of Bitmap width and height properties for each pixel (its mean is 2M invocations). You can check the cost of such an invocation with a profiler - for sure, it is more expensive than reading it only one time to some temp variable before the loop.
- Decrease the number of memory allocations inside the loop. It is one of the principles for time critical code development – do not allocate memory on demand, but perform allocation before.
- Use previous information. For example, during color matching, for each 24bit color we match the appropriate index in the 256 color palette. Almost every image has a lot of pixels that have the same color, so for those pixels, color matching will be executed only once.
Now let's see the solution and test results. All results are relevant for 1024x768 bitmaps.
ConvertTo8bppFormat has no loops; its role is to allocate all buffers and to call methods that do the real job.
public Bitmap ConvertTo8bppFormat(Bitmap bmpSource)
int imageWidth = bmpSource.Width;
int imageHeight = bmpSource.Height;
Bitmap bmpDest = null;
BitmapData bmpDataDest = null;
BitmapData bmpDataSource = null;
bmpDest = new Bitmap(
bmpDataDest = bmpDest.LockBits(
new Rectangle(0, 0, imageWidth, imageHeight),
bmpDataSource = bmpSource.LockBits(
new Rectangle(0, 0, imageWidth, imageHeight),
int pixelSize = GetPixelInfoSize(bmpDataSource.PixelFormat);
byte buffer = new byte[imageWidth * imageHeight * pixelSize];
byte destBuffer = new byte[imageWidth * imageHeight];
ReadBmpData(bmpDataSource, buffer, pixelSize, imageWidth, imageHeight);
MatchColors(buffer, destBuffer, pixelSize, bmpDest.Palette);
WriteBmpData(bmpDataDest, destBuffer, imageWidth, imageHeight);
if (bmpDest != null) bmpDest.UnlockBits(bmpDataDest);
if( bmpSource != null ) bmpSource.UnlockBits( bmpDataSource );
WriteBmpData – These methods just copy unmanaged memory (pixels color information) to managed buffer and vice versa. Pay attention, only 24bit bitmaps are supported now, but you can easily extend it for other bitmap types, just add code to
WriteBmpData are pretty quick, it takes only 0.01s to read and write data.
private void ReadBmpData(
int addrStart = bmpDataSource.Scan0.ToInt32();
for (int i = 0; i < height; i++)
IntPtr realByteAddr = new IntPtr( addrStart +
System.Convert.ToInt32(i * bmpDataSource.Stride)
(int)(i * width * pixelSize),
(int)(width * pixelSize)
private void WriteBmpData(
int addrStart = bmpDataDest.Scan0.ToInt32();
for (int i = 0; i < imageHeight; i++)
IntPtr realByteAddr = new IntPtr(addrStart +
System.Convert.ToInt32(i * bmpDataDest.Stride)
MatchColors – This method matches the index of 256 color palettes for each 24bit color. This method is expensive; color matching calculation takes 99% of run time of this application. So to make it faster, I've used a hash table to store all known colors and that’s why this code will be run really fast on bitmaps with the same colored background and it will be slow on bitmaps with a wide range of colors. I've got from 1 sec to 10 secs of calculation time in my test.
private void MatchColors(
int length = destBuffer.Length;
byte temp = new byte[pixelSize];
int palleteSize = pallete.Entries.Length;
int mult_1 = 256;
int mult_2 = 256 * 256;
int currentKey = 0;
for (int i = 0; i < length; i++)
Array.Copy(buffer, i * pixelSize, temp, 0, pixelSize);
currentKey = temp + temp * mult_1 + temp * mult_2;
destBuffer[i] = GetSimilarColor(pallete, temp, palleteSize);
destBuffer[i] = (byte)m_knownColors[currentKey];
private byte GetSimilarColor(ColorPalette palette, byte color, int palleteSize)
byte minDiff = byte.MaxValue;
byte index = 0;
if (color.Length == 3)
for (int i = 0; i < palleteSize - 1; i++)
byte currentDiff = GetMaxDiff(color, palette.Entries[i]);
if (currentDiff < minDiff)
minDiff = currentDiff;
index = (byte)i;
throw new ApplicationException("Only 24bit colors supported now");
private static byte GetMaxDiff(byte a, Color b)
byte bDiff = a > b.B ? (byte)(a - b.B): (byte)(b.B - a);
byte gDiff = a > b.G ? (byte)(a - b.G) : (byte)(b.G - a);
byte rDiff = a > b.R ? (byte)(a - b.R) : (byte)(b.R - a);
byte max = bDiff > gDiff ? bDiff : gDiff;
max = max > rDiff ? max : rDiff;
As you can see, performance was improved (from 20 minutes to 1-10 seconds for changing the color depth). This means that if .NET code is written properly and carefully, it can be very fast and used in time critical applications!
- 15th January, 2007: Initial post