This code shows how to convert a bitmap into monochrome (1bpp) or palettized (8bpp) from C#. The technique is fast because it calls gdi32 (the windows graphics system) directly.
- Download source code 1bpp.zip (82k, C#)
About it
Some functionality that's present in GDI (the windows Graphics Driver Interface) is simply absent from the standard .NET framework. One example is the ability to draw onto PixelFormat.1bpp images -- the Graphics class throws an exception when you try to create a Graphics object out of such an image. (Another example is the ability to create metafiles in memory.) To use this kind of functionality, we have to interop with GDI.
This page shows how to use the Windows GDI from C# to achieve faster conversions to 1bpp/8bpp. How fast? Well, let's compare it to normal C# code which doesn't use the Windows GDI. (taken from Bob Powell's GDI+ faq).
GDI+ faq: 8.5 seconds for a 4000x5000 image This code: 2.2 seconds Speedup: 4x speedup!
Code
The rest of this page is publically editable. If you want to add comments, or fix bugs, or ask questions, then click the "Edit" button at the bottom left.
This shows how to copy a Bitmap into a 1bpp copy, using the functions below:
static void Main(string[] args) { System.Drawing.Bitmap b = new System.Drawing.Bitmap("c:\\test.jpg"); System.Drawing.Bitmap b0 = CopyToBpp(b,1); // below is just a function I wrote to easily display the result onscreen SplashImage(b0,0,0); System.Threading.Thread.Sleep(1000); }
To convert to an 8bpp (palettized) image with a greyscale palette, do
System.Drawing.Bitmap b0 = CopyToBpp(b,8);
If you want to convert to an image with a different palette, look at the comments in the source code of CopyToBpp for suggestions. Note that, when you convert to a 1bpp or 8bpp palettized copy, Windows will look at each pixel one by one, and will chose the palette entry that's closest to that pixel. Depending on your source image and choice of palette, you may very well end up with a resulting image that uses only half of the colours available in the palette.
To convert a 1bpp/8bpp image back into a normal 24bpp bitmap,
System.Drawing.Bitmap b1 = new System.Drawing.Bitmap(b0);
How it works
First, some terminology:
| GDI+,.NET | GDI equivalent | |
| System.Drawing.Bitmap | HBITMAP | stores the bitmap data |
| System.Drawing.Graphics | HDC, DisplayContext | via this you draw onto bitmaps &c. |
| Graphics.DrawImage | BitBlt | copies from one bitmap onto another |
| note: C# uses IntPtr for HBitmaps and HDCs | ||
And here is the main code.
/// <summary> /// Copies a bitmap into a 1bpp/8bpp bitmap of the same dimensions, fast /// </summary> /// <param name="b">original bitmap</param> /// <param name="bpp">1 or 8, target bpp</param> /// <returns>a 1bpp copy of the bitmap</returns> static System.Drawing.Bitmap CopyToBpp(System.Drawing.Bitmap b, int bpp) { if (bpp!=1 && bpp!=8) throw new System.ArgumentException("1 or 8","bpp"); // Plan: built into Windows GDI is the ability to convert // bitmaps from one format to another. Most of the time, this // job is actually done by the graphics hardware accelerator card // and so is extremely fast. The rest of the time, the job is done by // very fast native code. // We will call into this GDI functionality from C#. Our plan: // (1) Convert our Bitmap into a GDI hbitmap (ie. copy unmanaged->managed) // (2) Create a GDI monochrome hbitmap // (3) Use GDI "BitBlt" function to copy from hbitmap into monochrome (as above) // (4) Convert the monochrone hbitmap into a Bitmap (ie. copy unmanaged->managed) int w=b.Width, h=b.Height; IntPtr hbm = b.GetHbitmap(); // this is step (1) // // Step (2): create the monochrome bitmap. // "BITMAPINFO" is an interop-struct which we define below. // In GDI terms, it's a BITMAPHEADERINFO followed by an array of two RGBQUADs BITMAPINFO bmi = new BITMAPINFO(); bmi.biSize=40; // the size of the BITMAPHEADERINFO struct bmi.biWidth=w; bmi.biHeight=h; bmi.biPlanes=1; // "planes" are confusing. We always use just 1. Read MSDN for more info. bmi.biBitCount=(short)bpp; // ie. 1bpp or 8bpp bmi.biCompression=BI_RGB; // ie. the pixels in our RGBQUAD table are stored as RGBs, not palette indexes bmi.biSizeImage = (uint)(((w+7)&0xFFFFFFF8)*h/8); bmi.biXPelsPerMeter=1000000; // not really important bmi.biYPelsPerMeter=1000000; // not really important // Now for the colour table. uint ncols = (uint)1<<bpp; // 2 colours for 1bpp; 256 colours for 8bpp bmi.biClrUsed=ncols; bmi.biClrImportant=ncols; bmi.cols=new uint[256]; // The structure always has fixed size 256, even if we end up using fewer colours if (bpp==1) {bmi.cols[0]=MAKERGB(0,0,0); bmi.cols[1]=MAKERGB(255,255,255);} else {for (int i=0; i<ncols; i++) bmi.cols[i]=MAKERGB(i,i,i);} // For 8bpp we've created an palette with just greyscale colours. // You can set up any palette you want here. Here are some possibilities: // greyscale: for (int i=0; i<256; i++) bmi.cols[i]=MAKERGB(i,i,i); // rainbow: bmi.biClrUsed=216; bmi.biClrImportant=216; int[] colv=new int[6]{0,51,102,153,204,255}; // for (int i=0; i<216; i++) bmi.cols[i]=MAKERGB(colv[i/36],colv[(i/6)%6],colv[i%6]); // optimal: a difficult topic: http://en.wikipedia.org/wiki/Color_quantization // // Now create the indexed bitmap "hbm0" IntPtr bits0; // not used for our purposes. It returns a pointer to the raw bits that make up the bitmap. IntPtr hbm0 = CreateDIBSection(IntPtr.Zero,ref bmi,DIB_RGB_COLORS,out bits0,IntPtr.Zero,0); // // Step (3): use GDI's BitBlt function to copy from original hbitmap into monocrhome bitmap // GDI programming is kind of confusing... nb. The GDI equivalent of "Graphics" is called a "DC". IntPtr sdc = GetDC(IntPtr.Zero); // First we obtain the DC for the screen // Next, create a DC for the original hbitmap IntPtr hdc = CreateCompatibleDC(sdc); SelectObject(hdc,hbm); // and create a DC for the monochrome hbitmap IntPtr hdc0 = CreateCompatibleDC(sdc); SelectObject(hdc0,hbm0); // Now we can do the BitBlt: BitBlt(hdc0,0,0,w,h,hdc,0,0,SRCCOPY); // Step (4): convert this monochrome hbitmap back into a Bitmap: System.Drawing.Bitmap b0 = System.Drawing.Bitmap.FromHbitmap(hbm0); // // Finally some cleanup. DeleteDC(hdc); DeleteDC(hdc0); ReleaseDC(IntPtr.Zero,sdc); DeleteObject(hbm); DeleteObject(hbm0); // return b0; }
To understand the code, here are links to the MSDN documentation for each GDI function and structure that it uses: BitBlt, BITMAPINFO, CreateDIBSection, GetDC, CreateCompatibleDC, SelectObject, SRCCOPY, DeleteDC, ReleaseDC, DeleteObject
Also here's my routine to splash an image onto the screen. I use this just as a quick convenient way to see the contents of a Bitmap. It's easier to call this than to create a proper WinForms project. But note: all this does is splash it onto the screen. Any window on the screen will overpaint it.
/// <summary> /// Draws a bitmap onto the screen. /// </summary> /// <param name="b">the bitmap to draw on the screen</param> /// <param name="x">x screen coordinate</param> /// <param name="y">y screen coordinate</param> static void SplashImage(System.Drawing.Bitmap b, int x, int y) { // Drawing onto the screen is supported by GDI, but not by the Bitmap/Graphics class. // So we use interop: // (1) Copy the Bitmap into a GDI hbitmap IntPtr hbm = b.GetHbitmap(); // (2) obtain the GDI equivalent of a "Graphics" for the screen IntPtr sdc = GetDC(IntPtr.Zero); // (3) obtain the GDI equivalent of a "Graphics" for the hbitmap IntPtr hdc = CreateCompatibleDC(sdc); SelectObject(hdc,hbm); // (4) Draw from the hbitmap's "Graphics" onto the screen's "Graphics" BitBlt(sdc,x,y,b.Width,b.Height,hdc,0,0,SRCCOPY); // and do boring GDI cleanup: DeleteDC(hdc); ReleaseDC(IntPtr.Zero,sdc); DeleteObject(hbm); }
Finally, here are the interop functions we use
[System.Runtime.InteropServices.DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr hObject); [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr hwnd); [System.Runtime.InteropServices.DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleDC(IntPtr hdc); [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); [System.Runtime.InteropServices.DllImport("gdi32.dll")] public static extern int DeleteDC(IntPtr hdc); [System.Runtime.InteropServices.DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj); [System.Runtime.InteropServices.DllImport("gdi32.dll")] public static extern int BitBlt(IntPtr hdcDst, int xDst, int yDst, int w, int h, IntPtr hdcSrc, int xSrc, int ySrc, int rop); static int SRCCOPY = 0x00CC0020; [System.Runtime.InteropServices.DllImport("gdi32.dll")] static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFO bmi, uint Usage, out IntPtr bits, IntPtr hSection, uint dwOffset); static uint BI_RGB = 0; static uint DIB_RGB_COLORS=0; [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] public struct BITMAPINFO { public uint biSize; public int biWidth, biHeight; public short biPlanes, biBitCount; public uint biCompression, biSizeImage; public int biXPelsPerMeter, biYPelsPerMeter; public uint biClrUsed, biClrImportant; [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst=256)] public uint[] cols; } static uint MAKERGB(int r,int g,int b) { return ((uint)(b&255)) | ((uint)((r&255)<<8)) | ((uint)((g&255)<<16)); }
it's so fast
thanks
The only strange thing I encountered was color quantization when converting to grayscale (even with the palette you used). I created a 256x256 bitmap and filled it with horizontal lines starting from (0,0,0) to (255,255,255) and after conversion to 8bpp the resulting image included only something around 20 gray levels.
I checked the color table selected into the DC and everything looks ok, but still quantization occurred.
For now I added another method for recalculating the gray image after it's created by the CopyToBpp method (see below). I'm not sure what causes the problem or if I'm doing something wrong.
private static void Calc8BPPGrayscale(Bitmap bmpSrc, Bitmap bmpDst8BPP)
{
if (bmpSrc.Size != bmpDst8BPP.Size)
{
throw new System.ArgumentException("Destination size mismatch", "bmpDst8BPP.Size");
}
if (bmpDst8BPP.PixelFormat != System.Drawing.Imaging.PixelFormat.Format8bppIndexed)
{
throw new System.ArgumentException("Format8bppIndexed", "bmpDst8BPP.PixelFormat");
}
if (bmpSrc.PixelFormat != System.Drawing.Imaging.PixelFormat.Format24bppRgb &&
bmpSrc.PixelFormat != System.Drawing.Imaging.PixelFormat.Format32bppArgb &&
bmpSrc.PixelFormat != System.Drawing.Imaging.PixelFormat.Format32bppRgb)
{
throw new System.ArgumentException("Format24bppRgb or Format32bppArgb or Format32bppRgb", "bmpSrc.PixelFormat");
}
int width = bmpSrc.Size.Width;
int height = bmpSrc.Size.Height;
Rectangle rect = new Rectangle(0, 0, width, height);
BitmapData bmpData = bmpSrc.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, bmpSrc.PixelFormat);
BitmapData bmpData8 = bmpDst8BPP.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, bmpDst8BPP.PixelFormat);
int iPixelWidth = bmpSrc.PixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb ? 3 : 4;
unsafe
{
byte* pScan = (byte*)bmpData.Scan0.ToPointer();
byte* pScan8 = (byte*)bmpData8.Scan0.ToPointer();
for (int y = 0; y < height; y++)
{
byte* pPixel = pScan;
byte* pPixel8 = pScan8;
for (int x = 0; x < width; x++)
{
byte gray = (byte)((*pPixel + *(pPixel + 1) + *(pPixel + 2)) / 3);
*pPixel8 = gray;
pPixel += iPixelWidth;
pPixel8++;
}
pScan += bmpData.Stride;
pScan8 += bmpData8.Stride;
}
}
bmpSrc.UnlockBits(bmpData);
bmpDst8BPP.UnlockBits(bmpData8);
}
This way, we can set the original resolution:
// Set Resolution.
b0.SetResolution(b.HorizontalResolution, b.VerticalResolution);
This code comes before CopyToBpp function returns b0 bitmap.
Thanks