Click here to Skip to main content
15,867,308 members
Articles / Multimedia / GDI+

Fast Pixel Operations in .NET (With and Without unsafe)

Rate me:
Please Sign up or sign in to vote.
4.82/5 (47 votes)
8 Jan 2018CPOL4 min read 99.8K   108   27
Using GetPixel or SetPixel for all but tiniest images is a terrible idea. Stop using these methods and make your code over 300 times faster!

Introduction

The Bitmap class has GetPixel and SetPixel methods that let you acquire and change color of chosen pixels. Those methods are very easy to use but are also extremely slow. My previous post gives detailed explanation on the topic, click here if you are interested.

Fortunately, you don’t have to use external libraries (or resign from .NET altogether) to do fast image manipulation. The Framework contains a class called ColorMatrix that lets you apply many changes to images in an efficient manner. Properties such as contrast or saturation can be modified this way. But what about manipulation of individual pixels? It can be done too, with the help from the Bitmap.LockBits method and the BitmapData class…

A good way to test individual pixel manipulation speed is color difference detection. The task is to find portions of an image that have color similar to some chosen color. How to check if colors are similar? Think about color as a point in three dimensional space, where axis are: red, green and blue. Two colors are two points. The difference between colors is described by the distance between two points in RGB space.

Colors as points in 3D space diff = sqrt((C1R-C2R)2+(C1G-C2G)2+(C1B-C2B)2)

This technique is very easy to implement and gives decent results. Color comparison is actually a pretty complex matter though. Different color spaces are better suited for the task than RGB and human color perception should be taken into account (e.g. our eyes are more keen to detect difference in shades of green that in shades of blue). But let’s keep things simple here…

Our test image will be this Ultra HD 8K (7680x4320, 33.1Mpx) picture (on this blog, it’s of course scaled down to save bandwidth):

Color difference detection input image (scaled down for blog)

This is a method that may be used to look for R=255 G=161 B=71 pixels (car number "36"). It sets matching pixels as white (the rest will be black):

C#
static void DetectColorWithGetSetPixel(Bitmap image, 
  byte searchedR, byte searchedG, int searchedB, int tolerance)
{
    int toleranceSquared = tolerance * tolerance;

    for (int x = 0; x < image.Width; x++)
    {
        for (int y = 0; y < image.Height; y++)
        {
            Color pixel = image.GetPixel(x, y);

            int diffR = pixel.R - searchedR;
            int diffG = pixel.G - searchedG;
            int diffB = pixel.B - searchedB;

            int distance = diffR * diffR + diffG * diffG + diffB * diffB;

            image.SetPixel(x, y, distance > toleranceSquared ? Color.Black : Color.White);
        }
    }
}

The above code is our terribly slow Get/SetPixel baseline. If we call it this way (named parameters for clarity):

C#
DetectColorWithGetSetPixel(image, searchedR: 255, searchedG: 161, searchedB: 71, tolerance: 60); 

we will receive the following outcome:

Color difference detection output image (scaled down)

Result may be ok but having to wait over 84300ms* is a complete disaster!

Now check out this method:

C#
static unsafe void DetectColorWithUnsafe(Bitmap image, 
  byte searchedR, byte searchedG, int searchedB, int tolerance)
{
    BitmapData imageData = image.LockBits(new Rectangle(0, 0, image.Width, 
      image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    int bytesPerPixel = 3;

    byte* scan0 = (byte*)imageData.Scan0.ToPointer();
    int stride = imageData.Stride;

    byte unmatchingValue = 0;
    byte matchingValue = 255;
    int toleranceSquared = tolerance * tolerance;

    for (int y = 0; y < imageData.Height; y++)
    {
        byte* row = scan0 + (y * stride);

        for (int x = 0; x < imageData.Width; x++)
        {
            // Watch out for actual order (BGR)!
            int bIndex = x * bytesPerPixel;
            int gIndex = bIndex + 1;
            int rIndex = bIndex + 2;

            byte pixelR = row[rIndex];
            byte pixelG = row[gIndex];
            byte pixelB = row[bIndex];

            int diffR = pixelR - searchedR;
            int diffG = pixelG - searchedG;
            int diffB = pixelB - searchedB;

            int distance = diffR * diffR + diffG * diffG + diffB * diffB;

            row[rIndex] = row[bIndex] = row[gIndex] = distance > 
              toleranceSquared ? unmatchingValue : matchingValue;
        }
    }

    image.UnlockBits(imageData);
} 

It does exactly the same thing but runs for only 230ms - over 360 times faster!

The above code makes use of Bitmap.LockBits method that is a wrapper for native GdipBitmapLockBits (GDI+, gdiplus.dll) function. LockBits creates a temporary buffer that contains pixel information in desired format (in our case RGB, 8 bits per color component). Any changes to this buffer are copied back to the bitmap upon UnlockBits call (therefore, you should always use LockBits and UnlockBits as a pair). Bitmap.LockBits returns BitmapData object (System.Drawing.Imaging namespace) that has two interesting properties: Scan0 and Stride. Scan0 returns an address of the first pixel data. Stride is the width of single row of pixels (scan line) in bytes (with optional padding to make it dividable by 4).

BitmapData layout

Please notice that I don’t use calls to Math.Pow and Math.Sqrt to calculate distance between colors. Writing code like this:

C#
double distance = Math.Sqrt(Math.Pow(pixelR - searchedR, 2) + 
Math.Pow(pixelG - searchedG, 2) + Math.Pow(pixelB - searchedB, 2));

to process millions of pixels is a terrible idea. Such line can make our optimized method about 25 times slower! Using Math.Pow with integer parameters is extremely wasteful and we don’t have to calculate square root to determine if distance is longer than specified tolerance.

Previously presented method uses code marked with unsafe keyword. It allows C# program to take advantage of pointer arithmetic. Unfortunately, unsafe mode has some important restrictions. Code must be compiled with \unsafe option and executed for fully trusted assembly.

Luckily, there is a Marshal.Copy method (from System.Runtime.InteropServices namespace) that can move data between managed and unmanaged memory. We can use it to copy image data into a byte array and manipulate pixels very efficiently. Look at this method:

C#
static void DetectColorWithMarshal(Bitmap image, 
  byte searchedR, byte searchedG, int searchedB, int tolerance)
{        
    BitmapData imageData = image.LockBits(new Rectangle(0, 0, image.Width, 
      image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

    byte[] imageBytes = new byte[Math.Abs(imageData.Stride) * image.Height];
    IntPtr scan0 = imageData.Scan0;

    Marshal.Copy(scan0, imageBytes, 0, imageBytes.Length);
  
    byte unmatchingValue = 0;
    byte matchingValue = 255;
    int toleranceSquared = tolerance * tolerance;

    for (int i = 0; i < imageBytes.Length; i += 3)
    {
        byte pixelB = imageBytes[i];
        byte pixelR = imageBytes[i + 2];
        byte pixelG = imageBytes[i + 1];

        int diffR = pixelR - searchedR;
        int diffG = pixelG - searchedG;
        int diffB = pixelB - searchedB;

        int distance = diffR * diffR + diffG * diffG + diffB * diffB;

        imageBytes[i] = imageBytes[i + 1] = imageBytes[i + 2] = distance > 
          toleranceSquared ? unmatchingValue : matchingValue;
    }

    Marshal.Copy(imageBytes, 0, scan0, imageBytes.Length);

    image.UnlockBits(imageData);
}

It runs for 280ms, so it is only slightly slower than unsafe version. It is CPU efficient but uses more memory than the previous method – almost 100 megabytes for our test Ultra HD 8K image in RGB 24 format.

If you want to make pixel manipulation even faster, you may process different parts of the image in parallel. You need to make some benchmarking first because for small images, the cost of threading may be bigger than gains from concurrent execution. Here’s a quick sample of code that uses 4 threads to process 4 parts of the image simultaneously. It yields 30% time improvement on my machine. Treat is as a quick and dirty hint, this post is already too long…

C#
static unsafe void DetectColorWithUnsafeParallel(Bitmap image, 
    byte searchedR, byte searchedG, int searchedB, int tolerance)
{
    BitmapData imageData = image.LockBits(new Rectangle(0, 0, image.Width, 
      image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    int bytesPerPixel = 3;

    byte* scan0 = (byte*)imageData.Scan0.ToPointer();
    int stride = imageData.Stride;

    byte unmatchingValue = 0;
    byte matchingValue = 255;
    int toleranceSquared = tolerance * tolerance;

    Task[] tasks = new Task[4];
    for (int i = 0; i < tasks.Length; i++)
    {
        int ii = i;
        tasks[i] = Task.Factory.StartNew(() =>
            {
                int minY = ii < 2 ? 0 : imageData.Height / 2;
                int maxY = ii < 2 ? imageData.Height / 2 : imageData.Height;

                int minX = ii % 2 == 0 ? 0 : imageData.Width / 2;
                int maxX = ii % 2 == 0 ? imageData.Width / 2 : imageData.Width;                        
                
                for (int y = minY; y < maxY; y++)
                {
                    byte* row = scan0 + (y * stride);

                    for (int x = minX; x < maxX; x++)
                    {
                        int bIndex = x * bytesPerPixel;
                        int gIndex = bIndex + 1;
                        int rIndex = bIndex + 2;

                        byte pixelR = row[rIndex];
                        byte pixelG = row[gIndex];
                        byte pixelB = row[bIndex];

                        int diffR = pixelR - searchedR;
                        int diffG = pixelG - searchedG;
                        int diffB = pixelB - searchedB;

                        int distance = diffR * diffR + diffG * diffG + diffB * diffB;

                        row[rIndex] = row[bIndex] = row[gIndex] = distance >
                            toleranceSquared ? unmatchingValue : matchingValue;
                    }
                }
            });
    }

    Task.WaitAll(tasks);

    image.UnlockBits(imageData);
}

* .NET 4 console app, executed on MSI GE620 DX laptop: Intel Core i5-2430M 2.40GHz (2 cores, 4 threads), 4GB DDR3 RAM, NVIDIA GT 555M 2GB DDR3, HDD 500GB 7200RPM, Windows 7 Home Premium x64.

Update (2013-07-11): I forgot to precalculate tolerance * tolerance (thanks to Axel Rietschin for noticing this). With toleranceSquared, DetectColorWithUnsafe execution time dropped from 260ms to 230ms.

Update (2018-01-08): If you really want to do some complex and efficient image processing then you should use specialized library like OpenCV. Few months ago I've written "Detecting a Drone - OpenCV in .NET for Beginners (Emgu CV 3.2, Visual Studio 2017)" blog post series that will help you do it...

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Poland Poland

Comments and Discussions

 
SuggestionPixelFormat and bytesPerPixel Pin
Arthurit24-Apr-20 5:28
Arthurit24-Apr-20 5:28 
Questioncomparing 2 images Pin
HardDisk789473-Feb-19 19:51
HardDisk789473-Feb-19 19:51 
QuestionSpan<T> makes it fast and protected Pin
darrellp10-Jan-18 13:34
darrellp10-Jan-18 13:34 
QuestionDistance Pin
Member 100571649-Jan-18 10:16
Member 100571649-Jan-18 10:16 
Why do not use:

d(M,N) = max(|xN-xM|, |yN-yM|, |zN-zM|)

instead:

d(M,N) = sqrt( (xM-xN)*(xM-xN) + (yM-yN)*(yM-yN) + (zM-zN)*(zM-zN))

?

-- modified 9-Jan-18 23:59pm.
AnswerRe: Distance Pin
morzel11-Jan-18 8:56
morzel11-Jan-18 8:56 
QuestionWhat about Parallel.For ? Pin
W. Kleinschmit9-Jan-18 7:32
W. Kleinschmit9-Jan-18 7:32 
AnswerRe: What about Parallel.For ? Pin
morzel11-Jan-18 9:15
morzel11-Jan-18 9:15 
QuestionThe models makes me... Pin
Alex Schunk9-Jan-18 4:04
Alex Schunk9-Jan-18 4:04 
GeneralMy vote of 5 Pin
raddevus8-Jan-18 13:41
mvaraddevus8-Jan-18 13:41 
QuestionThis code but for a CIImage in objective c for ios Pin
Member 108778969-May-16 22:37
Member 108778969-May-16 22:37 
QuestionCan I Run 3 unsafe functions without delays Pin
ajaypelluri55527-Jul-15 8:10
ajaypelluri55527-Jul-15 8:10 
SuggestionSkip Padding Area Pin
LYF61040021013-Jan-15 23:48
LYF61040021013-Jan-15 23:48 
QuestionTransparency Pin
Member 1009315223-Jul-14 10:13
Member 1009315223-Jul-14 10:13 
QuestionWPF BitMapImage Version Pin
kcsass20-May-14 3:54
kcsass20-May-14 3:54 
AnswerRe: WPF BitMapImage Version Pin
morzel20-May-14 6:47
morzel20-May-14 6:47 
QuestionUse Desktop Instead Of Image Pin
the ritzky22-Mar-14 9:38
the ritzky22-Mar-14 9:38 
AnswerRe: Use Desktop Instead Of Image Pin
morzel20-May-14 6:49
morzel20-May-14 6:49 
GeneralMy vote of 5 Pin
Rob Philpott16-Aug-13 22:19
Rob Philpott16-Aug-13 22:19 
NewsNo difference between toleranceSquared and Index. It's the same Pin
Victor Rosenfeld26-Jul-13 1:51
Victor Rosenfeld26-Jul-13 1:51 
GeneralRe: No difference between toleranceSquared and Index. It's the same Pin
morzel29-Jul-13 0:21
morzel29-Jul-13 0:21 
QuestionIt's faster Pin
Victor Rosenfeld24-Jul-13 22:11
Victor Rosenfeld24-Jul-13 22:11 
AnswerRe: It's faster Pin
morzel26-Jul-13 0:20
morzel26-Jul-13 0:20 
Question101 Pin
Axel Rietschin12-Jul-13 12:39
professionalAxel Rietschin12-Jul-13 12:39 
AnswerRe: 101 Pin
morzel12-Jul-13 23:21
morzel12-Jul-13 23:21 
GeneralMy vote of 5 Pin
Tom Clement12-Jul-13 7:20
professionalTom Clement12-Jul-13 7:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.