20
\$\begingroup\$

This is my program code for comparing if 2 images are the same. It basically uses the open dialog and opens 1 image at a time and puts it in 1 picture box.

I heard that GetPixel() method may be slow. Is there a more efficient and faster way to compare 2 if 2 images are the same?

Bitmap image1 = null;
Bitmap image2 = null;

public Form1()
{
    InitializeComponent();
}

private void button1_Click(object sender, EventArgs e) //first image open
{
    OpenFileDialog openDialog = new OpenFileDialog();
    if (openDialog.ShowDialog() == DialogResult.OK)
    {
        image1 = new Bitmap(openDialog.FileName);
        pictureBox1.Image = image1;
    }   
}

private void button2_Click(object sender, EventArgs e) //second image open
{

    OpenFileDialog openDialog = new OpenFileDialog();
    if (openDialog.ShowDialog() == DialogResult.OK)
    {
        image2 = new Bitmap(openDialog.FileName);
        pictureBox2.Image = image2;
    }  
}

private void button3_Click(object sender, EventArgs e) //compare button
{
    if (compare(image1, image2))
    {
        MessageBox.Show("Same Image.");
    }

    else
    {
        MessageBox.Show("Different Image.");
    }
}

private bool compare(Bitmap bmp1, Bitmap bmp2) 
{
    bool equals = true;
    bool flag = true;  //Inner loop isn't broken

    //Test to see if we have the same size of image
    if (bmp1.Size == bmp2.Size)
    {
        for (int x = 0; x < bmp1.Width; ++x)
        {
            for (int y = 0; y < bmp1.Height; ++y)
            {
                if (bmp1.GetPixel(x, y) != bmp2.GetPixel(x, y))
                {
                    equals = false;
                    flag = false;
                    break;
                }
            }
            if (!flag)
            {
                break;
            }
        }
    }
    else
    {
        equals = false;
    }
    return equals;
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Note that Bitmap inherits from Image, which implements IDisposable. This means you very much better call Dispose() on those objects are you are done with them (way many paths here with UI event handlers), or put them into using blocks. \$\endgroup\$ – Jesse C. Slicer Jan 24 '14 at 18:45
  • 1
    \$\begingroup\$ Oh, and same goes for OpenFileDialog. I'll probably post an answer to show some of this. \$\endgroup\$ – Jesse C. Slicer Jan 24 '14 at 18:48
  • 1
    \$\begingroup\$ See ImageComparer.Compare method. Has overloads to specify tolerance. It is available since VS2102. msdn.microsoft.com/en-us/library/… \$\endgroup\$ – user3285954 Jul 2 '15 at 20:06
14
\$\begingroup\$

You can use the LockBits method and pointers to access the image data directly.

Example for 24 bpp images:

bool equals = true;
Rectangle rect = new Rectangle(0, 0, bmp1.Width, bmp1.Height);
BitmapData bmpData1 = bmp1.LockBits(rect, ImageLockMode.ReadOnly, bmp1.PixelFormat);
BitmapData bmpData2 = bmp2.LockBits(rect, ImageLockMode.ReadOnly, bmp2.PixelFormat);
unsafe {
  byte* ptr1 = (byte*)bmpData1.Scan0.ToPointer();
  byte* ptr2 = (byte*)bmpData2.Scan0.ToPointer();
  int width = rect.Width * 3; // for 24bpp pixel data
  for (int y = 0; equals && y < rect.Height; y++) {
    for (int x = 0; x < width; x++) {
      if (*ptr1 != *ptr2) {
        equals = false;
        break;
      }
      ptr1++;
      ptr2++;
    }
    ptr1 += bmpData1.Stride - width;
    ptr2 += bmpData2.Stride - width;
  }
}
bmp1.UnlockBits(bmpData1);
bmp2.UnlockBits(bmpData2);
\$\endgroup\$
  • \$\begingroup\$ I assume I drag this in my compare() method? \$\endgroup\$ – puretppc Jan 24 '14 at 18:57
  • \$\begingroup\$ @puretppc: Yes, that is correct. After checking that the images have the same size and the correct pixel format. \$\endgroup\$ – Guffa Jan 24 '14 at 19:13
  • \$\begingroup\$ BitmapData doesn't exist for me. Also Unsafe code may only appear if compiling with /unsafe. \$\endgroup\$ – puretppc Jan 24 '14 at 19:16
  • \$\begingroup\$ @puretppc: Add using System.Drawing.Imaging; at the top, or use System.Drawing.Imaging.BitmapData. (If you right click on BitmapData both options can be done automatically under Resolve.) To use unsafe code you need to enable it in the project properties. \$\endgroup\$ – Guffa Jan 24 '14 at 19:24
  • \$\begingroup\$ Oh ok. But now I'm getting an error saying 'System.Drawing.Imaging.ImageLockMode' does not contain a definition for 'Read' At the very top of the program I put using System.Drawing.Imaging; \$\endgroup\$ – puretppc Jan 24 '14 at 19:31
11
\$\begingroup\$

One thing I have done in the past is to convert the images to 64-bit encoded strings and just string compare. It won't be as fast as using pointers, naturally. However, it can be done entirely in managed code, it doesn't require you to know bits per pixel, and it works for the Image base class.

byte[] image1Bytes;
byte[] image2Bytes;

using(var mstream = new MemoryStream())
{
    image1.Save(mstream, image1.RawFormat);
    image1Bytes = mstream.ToArray();
}

using(var mstream = new MemoryStream())
{
    image2.Save(mstream, image2.RawFormat);
    image2Bytes = mstream.ToArray();
}

var image164 = Convert.ToBase64String(image1Bytes);
var image264 = Convert.ToBase64String(image2Bytes);

return string.Equals(image164, image264);
\$\endgroup\$
  • \$\begingroup\$ Thanks this worked :) So my old code was about comparing BPP (bits per pixel)? I don't really know this since I got this off the another site \$\endgroup\$ – puretppc Jan 24 '14 at 18:58
  • 3
    \$\begingroup\$ Note that this will also compare any meta data in the images. If they for example are identical but with different resolution settings, this will return false. \$\endgroup\$ – Guffa Jan 24 '14 at 19:14
  • 4
    \$\begingroup\$ What is the purpose of converting to a base64 string? Why not simple compare the elements of both arrays (or streams)? \$\endgroup\$ – NPSF3000 May 5 '14 at 8:30
  • \$\begingroup\$ It took a bit more work for me than I thought to get this method working; I read something about hashing the image data and thought Bitmap.GetHashCode() would suffice. I have several subimages I'm pulling from spritesheets and the last few spritesheets have blank subimages which I want to skip. It was either this or looping through each pixel of all subimages and checking to see if they were colorless (black with no alpha), which of course is processor-intensive. Thanks for sharing this! \$\endgroup\$ – Artorias2718 May 20 at 4:59
9
\$\begingroup\$

I'll focus on problems in compare().

You're using the wrong equality comparison for the bitmap size. You need to compare the contents of the Size objects, not whether they are the same reference.

A variable named flag should be a… red flag! Not only is it vaguely named, its presence suggests that your code is ineffective. Avoid using variables for flow control; find more active ways to get to where you need to go.

In this case, the solution is an early return. As soon as you find a single difference between the two images, you're done! You don't even need the equals variable.

I would also rename compare() for clarity, and make it static because it is a pure function of its two parameters.

private static bool Equals(Bitmap bmp1, Bitmap bmp2) 
{
    if (!bmp1.Size.Equals(bmp2.Size))
    {
        return false;
    }
    for (int x = 0; x < bmp1.Width; ++x)
    {
        for (int y = 0; y < bmp1.Height; ++y)
        {
            if (bmp1.GetPixel(x, y) != bmp2.GetPixel(x, y))
            {
                return false;
            }
        }
    }
    return true;
}
\$\endgroup\$
6
\$\begingroup\$

Note that Bitmap inherits from Image, which implements IDisposable. This means you very much better call Dispose() on those objects are you are done with them (way many paths here with UI event handlers), or put them into using blocks. Same goes for OpenFileDialog.

As per my comments, some using usage (and try..finally usage, incorporating Guffa's answer):

private Bitmap image1;

private Bitmap image2;

public Form1()
{
    this.InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
    using (var openDialog = new OpenFileDialog())
    {
        if (openDialog.ShowDialog() != DialogResult.OK)
        {
            return;
        }

        this.DisposeImage1();
        this.image1 = new Bitmap(openDialog.FileName);
    }

    this.pictureBox1.Image = this.image1;
}

private void button2_Click(object sender, EventArgs e)
{
    using (var openDialog = new OpenFileDialog())
    {
        if (openDialog.ShowDialog() != DialogResult.OK)
        {
            return;
        }

        this.DisposeImage2();
        this.image2 = new Bitmap(openDialog.FileName);
    }

    this.pictureBox2.Image = this.image2;
}

private void button3_Click(object sender, EventArgs e)
{
    MessageBox.Show(Compare(this.image1, this.image2) ? "Same Image." : "Different Image.");
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    this.DisposeImage2();
    this.DisposeImage1();
}

private static bool Compare(Bitmap bmp1, Bitmap bmp2)
{
    // Test to see if we have the same size of image
    if (bmp1.Size != bmp2.Size)
    {
        return false;
    }

    var rect = new Rectangle(0, 0, bmp1.Width, bmp1.Height);
    var bmpData1 = bmp1.LockBits(rect, ImageLockMode.ReadOnly, bmp1.PixelFormat);

    try
    {
        var bmpData2 = bmp2.LockBits(rect, ImageLockMode.ReadOnly, bmp1.PixelFormat);

        try
        {
            unsafe
            {
                var ptr1 = (byte*)bmpData1.Scan0.ToPointer();
                var ptr2 = (byte*)bmpData2.Scan0.ToPointer();
                var width = 3 * rect.Width; // for 24bpp pixel data

                for (var y = 0; y < rect.Height; y++)
                {
                    for (var x = 0; x < width; x++)
                    {
                        if (*ptr1 != *ptr2)
                        {
                            return false;
                        }

                        ptr1++;
                        ptr2++;
                    }

                    ptr1 += bmpData1.Stride - width;
                    ptr2 += bmpData2.Stride - width;
                }
            }
        }
        finally
        {
            bmp2.UnlockBits(bmpData2);
        }
    }
    finally
    {
        bmp1.UnlockBits(bmpData1);
    }

    return true;
}

private void DisposeImage1()
{
    if (this.image1 == null)
    {
        return;
    }

    this.pictureBox1.Image = null;
    this.image1.Dispose();
    this.image1 = null;
}

private void DisposeImage2()
{
    if (this.image2 == null)
    {
        return;
    }

    this.pictureBox2.Image = null;
    this.image2.Dispose();
    this.image2 = null;
}
\$\endgroup\$
  • \$\begingroup\$ ImageLockMode doesn't exist? Also, the unsafe I'm getting an error \$\endgroup\$ – puretppc Jan 24 '14 at 19:17
  • 1
    \$\begingroup\$ You'll need to check enable unsafe code on the Build page of your project's options. Also, add using System.Drawing.Imaging to your using directives at the top of the file to get access to ImageLockMode. \$\endgroup\$ – Jesse C. Slicer Jan 24 '14 at 20:53
  • \$\begingroup\$ in "var bmpData2 = bmp2.LockBits(rect, ImageLockMode.ReadOnly, bmp1.PixelFormat)" last parameter bmp1.PixelFormat or bmp2.PixelFormat? \$\endgroup\$ – Sajitha Rathnayake Aug 15 '14 at 5:26
  • \$\begingroup\$ I got error "Bitmap region is already locked." in code "var bmpData2 = bmp2.LockBits(rect, ImageLockMode.ReadOnly, bmp1.PixelFormat)" \$\endgroup\$ – Sajitha Rathnayake Aug 15 '14 at 5:30
-1
\$\begingroup\$

Have a look at this SO question.

https://stackoverflow.com/q/35151067/4062881

It decreases the size of image, turns it B&W, and then uses GetPixel() to generate hash.

It is much faster, efficient, and works! It is able to find equal images with:

  • different file formats (e.g. jpg, png, bmp)
  • rotation (90, 180, 270) - by changing the iteration order of i and j
  • different dimensions (same aspect is required)
  • different compression (tolerance is required in case of quality loss like jpeg artifacts) - you can accept a 99% equality to be the same image and 50% to be a different one.

Cheers..!! ;)

\$\endgroup\$
-1
\$\begingroup\$

Why convert in base64 or compare every pixel if there's hash comparison? Do something like this:

byte[] fileData = File.ReadAllBytes(filePath);
byte[] hash = MD5.Create().ComputeHash(fileData);

And simply compare the hashes.

You'll need MD5CryptoServiceProvider for that.

\$\endgroup\$
  • \$\begingroup\$ Identical images may have differences in how they are encoded (progressive or interlaced, for example, or different levels of error-correction code), so needn't be bit-identical. Also, when there's a match, you'd still have to also compare the actual data as well, given that hash collisions are possible. \$\endgroup\$ – Toby Speight Nov 21 '18 at 8:38
  • \$\begingroup\$ First of all, if that would be the case base64 and pixel comparison wouldn't work either. Second, OP hasn't mentioned anything in that regard. If the plan is to compare slightly different images you could use the library OpenCV and a method like SIFT or SURF \$\endgroup\$ – F.H. Nov 21 '18 at 14:19
  • \$\begingroup\$ That makes no sense - there's no base-64 in the code that I can see, and I've just explained where pixel comparison shows identical content from different image files. \$\endgroup\$ – Toby Speight Nov 21 '18 at 14:29
  • \$\begingroup\$ Second most upvoted answer is suggesting base64 string comparison. Nice that you take the time to check before you randomly downvote others answers. \$\endgroup\$ – F.H. Nov 21 '18 at 14:45
  • \$\begingroup\$ You have no idea which answers I have voted on (up or down), nor whether any of those votes were random, so please stop hurling accusations. \$\endgroup\$ – Toby Speight Nov 21 '18 at 15:16

Not the answer you're looking for? Browse other questions tagged or ask your own question.