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

30 thoughts on “Fast Image Processing in C#

  1. Thank you for your interesting article!

    I test your code on a 2014×1038 jpg file, however, get the following results:
    GetPixel/SetPixel: 12011 (ms)
    LockBits: 12050 (ms)
    LockBits + unsafe: 12077 (ms)
    Lockbits + unsafe + Parallel: 12112 (ms)

    Any idea why the process time even increases?

  2. My bad, I copy and pasted sw.Start(); for each methods.

    Only the first one is sw.Start(), and the rest of them should be sw.Restart();

    The results looks reasonable after the correction with something like:
    GetPixel/SetPixel: 13204 (ms)
    LockBits: 74 (ms)
    LockBits + unsafe: 48 (ms)
    Lockbits + unsafe + Parallel: 73 (ms)

    The image is small, so the parallel method does not improve much. Will try it on a larger one. For image size larger than 5m, may worth a try.

  3. Thank you for this code! In my computer the difference between the the different processing systems was huge
    Now I’ll just modify this code for what i need in my application

  4. I am using usafe code to detect a point in image from one camera(60fps,1280×1024,1.3mp).i am getting 10ms for one frame in 12 core cpu.it is fine and working well.now i am using 3 cameras at a time and i am running 3 unsafe functions at this case am getting inconstant delays in 3 functions.
    can any one help me regarding this issue.

    Thank you.

  5. I am not seeing any benefits of Parallel code while running on my MacBookPro with i7 and VMware Fusion VM running Windows 10 (with 4 cores turned on). Will repeat this on a real PC.

  6. It’s the most comprehensive discussion of this topic that I could find in the interwebs. It really spared me a lot of time in figuring out the details.

    Thanks so much for this code!

  7. Any from doing fill..But this from scanning and create picture /image and this image find in all attribute and this attribute is store database automatically in c#.
    So can you send me any query using this method..

  8. Pingback: C# - Faster Alternatives to SetPixel and GetPixel for Bitmaps for Windows Forms App - QuestionFocus

  9. Thank you for this code example. I have a problem working with it though. The traditional double for loops work fine for me. But the other 3 functions do not return the correct image, if I create a bitmap and try to set the pixels with the presented functions. I just get a black image.
    Any hint, where the problem could be?

    • I found the solution. My problem was, that I’ve created a bitmap of a specific size but did not fill it with any pixel data. So for the faster functions to work properly, you need to read an image from a harddrive or you have to set the pixels at least once after creating a new bitmap container.

  10. Hi to all:
    I’ve been looking into this and other forums for the following.
    I’m not a programmer myself, but I work for an s/w development company as an operations mgr.
    Within the company we have a photo booth side line we use for trade shows and so.
    We use an “out of the box” s/w by: photo booth solutions.
    I would like to implement the virtual background with the RealSense technology without the use of chroma green screen.
    I have experimented with the RealSense technology and put it to work.
    But, my question is if there is a Code routine lightweight enough that I can use and call from this photo booth application for RealSense virtual background?

  11. One more test case that would be useful to add: LockBits + Parallel. Based on the results here, that would probably run about as fast as LockBits + unsafe (without Parallel). If that is so, then its only necessary to go “unsafe”, in the absolutely most time-critical situations, and only after trying Parallel.

  12. Amazing tutorial, thanks!
    I’m trying to process the image in reverse order (so bottom right to op left pixel) but having a rough time with it. Can you help?

  13. Hello,
    I tried to adapt the code to my application, where there is a source bitmap and a destination bitmap.
    I’m getting destination with “holes” in the process when using Parallel.For, and a perfect result when just using a classical for.

    What am I missing ?

    • Hi, It is a byte pointer. Pointers are blocks of memory (8 bytes on 64-bit machines) that reference memory addresses of any data type. It is declared using the * character.

Leave a Reply

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