Monday, October 05, 2009

Find differences between images C#

Suppose you had two nearly identical images and you wanted to locate and highlight the differences between them.  Here’s a fun snippet of C# code to do just that.

While the Bitmap class includes methods for manipulating individual pixels (GetPixel and SetPixel), they aren’t as efficient as manipulating the data directly.  Fortunately, we can access the low-level bitmap data using the BitmapData class, like so:

Bitmap image = new Bitmap("image1.jpg");

Rectangle rect = new Rectangle(0,0,image.Width,image.Height);
BitmapData data = image.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

Since we’re manipulating memory directly using pointers, we have to mark our method that locks and unlocks the bits with the unsafe keyword, which is not CLS compliant. This is only a problem if you expose this method as part of a public API and you want to share your library between C# and other .NET languages. If you want to maintain CLS compliant code, just make the unsafe method as a private member.

To find the differences between two images, we'll loop through and compare the low-level bytes of the image. Where the pixels match, we'll swap the pixel with a pre-defined colour and then later treat this colour as transparent, much like the green-screen technique used in movies. The end result is an image that contains the differences that can be transparently overlaid over top.

public class ImageTool
{
    public static unsafe Bitmap GetDifferenceImage(Bitmap image1, Bitmap image2, Color matchColor)
    {
        if (image1 == null | image2 == null)
            return null;

        if (image1.Height != image2.Height || image1.Width != image2.Width)
            return null;

        Bitmap diffImage = image2.Clone() as Bitmap;

        int height = image1.Height;
        int width = image1.Width;

        BitmapData data1 = image1.LockBits(new Rectangle(0, 0, width, height), 
                                           ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
        BitmapData data2 = image2.LockBits(new Rectangle(0, 0, width, height), 
                                           ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
        BitmapData diffData = diffImage.LockBits(new Rectangle(0, 0, width, height), 
                                               ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);

        byte* data1Ptr = (byte*)data1.Scan0;
        byte* data2Ptr = (byte*)data2.Scan0;
        byte* diffPtr = (byte*)diffData.Scan0;

        byte[] swapColor = new byte[3];
        swapColor[0] = matchColor.B;
        swapColor[1] = matchColor.G;
        swapColor[2] = matchColor.R;

        int rowPadding = data1.Stride - (image1.Width * 3);

        // iterate over height (rows)
        for (int i = 0; i < height; i++)
        {
            // iterate over width (columns)
            for (int j = 0; j < width; j++)
            {
                int same = 0;

                byte[] tmp = new byte[3];

                // compare pixels and copy new values into temporary array
                for (int x = 0; x < 3; x++)
                {
                    tmp[x] = data2Ptr[0];
                    if (data1Ptr[0] == data2Ptr[0])
                    {
                        same++;
                    }
                    data1Ptr++; // advance image1 ptr
                    data2Ptr++; // advance image2 ptr
                }

                // swap color or add new values
                for (int x = 0; x < 3; x++)
                {
                    diffPtr[0] = (same == 3) ? swapColor[x] : tmp[x];
                    diffPtr++; // advance diff image ptr
                }
            }

            // at the end of each column, skip extra padding
            if (rowPadding > 0)
            {
                data1Ptr += rowPadding;
                data2Ptr += rowPadding;
                diffPtr += rowPadding;
            }
        }

        image1.UnlockBits(data1);
        image2.UnlockBits(data2);
        diffImage.UnlockBits(diffData);

        return diffImage;
    }
}

An example that finds the difference between the images and then converts the matching colour to transparent.

class Program
{
    public static void Main()
    {
        Bitmap image1 = new Bitmap(400, 400);

        using (Graphics g = Graphics.FromImage(image1))
        {
            g.DrawRectangle(Pens.Blue, new Rectangle(0, 0, 50, 50));
            g.DrawRectangle(Pens.Red, new Rectangle(40, 40, 100, 100));
        }
        image1.Save("C:\\test-1.png",ImageFormat.Png);

        Bitmap image2 = (Bitmap)image1.Clone();

        using (Graphics g = Graphics.FromImage(image2))
        {
            g.DrawRectangle(Pens.Purple, new Rectangle(0, 0, 40, 40));
        }
        image2.Save("C:\\test-2.png",ImageFormat.Png);

        Bitmap diff = ImageTool.GetDifferenceImage(image1, image2, Color.Pink);
        diff.MakeTransparent(Color.Pink);
        diff.Save("C:\\test-diff.png",ImageFormat.Png);
    }
}

submit to reddit

3 comments:

chathuranga said...

thanks for your valuable code,,,can you tell me how to run this project,,,i'm trying to run this using visual studio,,,but it says "Unsafe code only appear if compiling with /unsafe "...i can't find way to run and test your project. Please help me.

Deepak said...

Bryan I'm working on Image processing technique to detect smoke in the image captured by our cams. I'll try your code and hope i could bring few enhancements to it. Thanks...

bryan said...

@chathuranga -- because we're dealing with pointers the method signature is unsafe. There's a flag in your project settings to enable this feature (Build -> allow unsafe). Don't worry, it won't blow up your computer, it refers to a feature that isn't available in other programming languages like Visual Basic, so if you were planning on sharing this library with others and they need to use other languages.

The other alternative is to mark the method as private so that it doesn't appear in the method signature outside of the assembly. Eg, your Visual Basic friends wouldn't be able to use the private method.