03 Sep

Fast Image Processing in C#

This example shows how to process the images in C# comparatively. Users often use GetPixel/SetPixel methods in System.Drawing.Bitmap class but these methods have bad performance, especially for big images. It is the simplest approach and it does not require you to know anything about pixel format(number of bits per pixel) of processed picture. If we use the memory access and parallel programming(using threads), image processing time is less than PixelGet and PixelSet methods.

Following samples show how to use comparatively.

Sample Usages :

            Stopwatch sw = new Stopwatch();

            sw.Start();
            ProcessUsingGetPixelSetPixel(bmp);
            sw.Stop();
            Console.WriteLine(string.Format("Processed using ProcessUsingGetPixel method in {0} ms.", sw.ElapsedMilliseconds));

            sw.Restart();
            ProcessUsingLockbits(bmp);
            sw.Stop();
            Console.WriteLine(string.Format("Processed using ProcessUsingLockbits method in {0} ms.", sw.ElapsedMilliseconds));

            sw.Restart();
            ProcessUsingLockbitsAndUnsafe(bmp);
            sw.Stop();
            Console.WriteLine(string.Format("Processed using ProcessUsingLockbitsAndUnsafe method in {0} ms.", sw.ElapsedMilliseconds));

            sw.Restart();
            ProcessUsingLockbitsAndUnsafeAndParallel(bmp);
            sw.Stop();
            Console.WriteLine(string.Format("Processed using ProcessUsingLockbitsAndUnsafeAndParallel method in {0} ms.", sw.ElapsedMilliseconds));

            //Result: (For ~7500x5000 pixel)
            //Processed using ProcessUsingGetPixel method in 87118 ms.
            //Processed using ProcessUsingLockbits method in 333 ms.
            //Processed using ProcessUsingLockbitsAndUnsafe method in 177 ms.
            //Processed using ProcessUsingLockbitsAndUnsafeAndParallel method in 78 ms.

Processing using GetPixel and SetPixel :

        private void ProcessUsingGetPixelSetPixel(Bitmap processedBitmap)
        {
            int width = processedBitmap.Width;
            int height = processedBitmap.Height;
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    Color oldPixel = processedBitmap.GetPixel(x, y);
                    
                    // calculate new pixel value
                    Color newPixel = oldPixel;

                    processedBitmap.SetPixel(x, y, newPixel);
                }
            }
        }

Processing using Bitmap.Lockbits and Marshal.Copy :

        private void ProcessUsingLockbits(Bitmap processedBitmap)
        {
            BitmapData bitmapData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), ImageLockMode.ReadWrite, processedBitmap.PixelFormat);
            
            int bytesPerPixel = Bitmap.GetPixelFormatSize(processedBitmap.PixelFormat) / 8;
            int byteCount = bitmapData.Stride * processedBitmap.Height;
            byte[] pixels = new byte[byteCount];
            IntPtr ptrFirstPixel = bitmapData.Scan0;
            Marshal.Copy(ptrFirstPixel, pixels, 0, pixels.Length);
            int heightInPixels = bitmapData.Height;
            int widthInBytes = bitmapData.Width * bytesPerPixel;

            for (int y = 0; y < heightInPixels; y++)
            {
                int currentLine = y * bitmapData.Stride;
                for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
                {
                    int oldBlue = pixels[currentLine + x];
                    int oldGreen = pixels[currentLine + x + 1];
                    int oldRed = pixels[currentLine + x + 2];
                    
                    // calculate new pixel value
                    pixels[currentLine + x] = (byte)oldBlue;
                    pixels[currentLine + x + 1] = (byte)oldGreen;
                    pixels[currentLine + x + 2] = (byte)oldRed;
                }
            }

            // copy modified bytes back
            Marshal.Copy(pixels, 0, ptrFirstPixel, pixels.Length);
            processedBitmap.UnlockBits(bitmapData);
        }

Processing using Bitmap.LockBits and direct memory access in unsafe context :

        private void ProcessUsingLockbitsAndUnsafe(Bitmap processedBitmap)
        {
            unsafe
            {
                BitmapData bitmapData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), ImageLockMode.ReadWrite, processedBitmap.PixelFormat);
                int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(processedBitmap.PixelFormat) / 8;
                int heightInPixels = bitmapData.Height;
                int widthInBytes = bitmapData.Width * bytesPerPixel;
                byte* ptrFirstPixel = (byte*)bitmapData.Scan0;
                
                for (int y = 0; y < heightInPixels; y++)
                {
                    byte* currentLine = ptrFirstPixel + (y * bitmapData.Stride);
                    for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
                    {
                        int oldBlue = currentLine[x];
                        int oldGreen = currentLine[x + 1];
                        int oldRed = currentLine[x + 2];

                        // calculate new pixel value
                        currentLine[x] = (byte)oldBlue;
                        currentLine[x + 1] = (byte)oldGreen;
                        currentLine[x + 2] = (byte)oldRed;
                    }
                }
                processedBitmap.UnlockBits(bitmapData);
            }
        }

Processing using Bitmap.LockBits and direct memory access in unsafe context with System.Threading.Tasks.Parallel class :

        private void ProcessUsingLockbitsAndUnsafeAndParallel(Bitmap processedBitmap)
        {
            unsafe
            {
                BitmapData bitmapData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), ImageLockMode.ReadWrite, processedBitmap.PixelFormat);

                int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(processedBitmap.PixelFormat) / 8;
                int heightInPixels = bitmapData.Height;
                int widthInBytes = bitmapData.Width * bytesPerPixel;
                byte* PtrFirstPixel = (byte*)bitmapData.Scan0;

                Parallel.For(0, heightInPixels, y =>
                {
                    byte* currentLine = PtrFirstPixel + (y * bitmapData.Stride);
                    for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
                    {
                        int oldBlue = currentLine[x];
                        int oldGreen = currentLine[x + 1];
                        int oldRed = currentLine[x + 2];

                        currentLine[x] = (byte)oldBlue;
                        currentLine[x + 1] = (byte)oldGreen;
                        currentLine[x + 2] = (byte)oldRed;
                    }
                });
                processedBitmap.UnlockBits(bitmapData);
            }
        }
Image Size (MP) GetPixel/SetPixel (ms) LockBits + Marshal.Copy (ms) LockBits + unsafe context (ms) Lockbits + unsafe context + Parallel.For (ms)
~10 22314 82 31 19
~38 87118 333 117 78