MxLDevs
MxLDevs

Reputation: 19556

C# faster way to compare pixels between two images and only write out the differences

Just doing a pixel-by-pixel comparison of two images that are very similar (one is an edited version of another) and writing out the differences to a new file.

for (int y = 0; y < height; y++)
{
    for (int x = 0; x < width; x++)
    {
        pix1 = src.GetPixel(x, y);
        pix2 = comp.GetPixel(x, y);
        if (pix1 != pix2)
        {
            dest.SetPixel(x, y, pix1);
        }
    }
}

src and comp are the two images to compare and dest is just a new image. It takes a fairly long time.

What's a faster way to do this?
Maybe it's not necessary to actually get the pixel in order to compare it?

Upvotes: 5

Views: 19057

Answers (3)

Karl-Johan Sj&#246;gren
Karl-Johan Sj&#246;gren

Reputation: 17612

To compare the pixels you need to read them. However, GetPixel() is a very slow method of doing this and not recommended unless you are just checking a very small amount of data.

To get better performance the best way is to go with unsafe-code and use pointers instead. There are lots of samples of this on the internet, below is one that I found that explains the problems a bit and offers two different solutions to this.

http://davidthomasbernal.com/blog/2008/03/13/c-image-processing-performance-unsafe-vs-safe-code-part-i

Be sure to check part two as well where he has some benchmarks and links to the complete source.

Upvotes: 5

Simon Miller
Simon Miller

Reputation: 970

This code is similar to what I use. Absolutely, in this case, unsafe code is the only way to go. Marshalling a bitmap full of pixels around memory isn't necessary, and you'd have to do that twice for the comparison!

However, comparing pixels should be seen as comparing two numbers. As a pixel is 3 bytes for each of the Red, Green and Blue components of the colour, and of course the Alpha - which is often ignored - you might want to compare values instead as UInt32's. This would have the benefit of comparing one number (one pixel) with another, and if they're the same, then you move on.

For this to work, you don't want a byte* but a UInt32*. Secondly, the code above doesn't consider Stride that I can see, which is where each horizontal line of an image may actually use a different number of bytes than the sum of the pixels themselves. As you're likely dealing with 24bits (rgb) and an alpha (a) that means the pixels should already line up, and hence, the above code should work. Yet there's no guarantee it would 100% of the time.

It might sound like I'm being really picky, but I'm guessing performance means something to you - and coming from a gaming background, it does to me too.

(taken from above link to part 1, thanks to Karl-Johan Sjögren)

BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat);

When you hit this line, you really need to be careful - the PixelFormat you're locking with should match one of your source image's PixelFormat. Otherwise things might not behave quite as you expect.

from the LockBits, you get the BitmapData, which you need to dig into.

byte* scan0 = (byte*)bData.Scan0.ToPointer();

this is the line I'm referring to - I'd make this a UInt32* so you're not reading one byte per instruction, but 4 in one go, as a UInt32.

I'd access this memory as an array - and I calculate the offset into the one-dimensional array by recogising the stride is the equivalent of one Y pixel, and so multiplying the Stride by the Y. However, you would fall into the same trap I have so many times at this point!

int stride = bData.Stride / 4;  // the width is expressed in bytes, yet we need it as UInt32's.  As there's 4 bytes per UInt32, this makes sense of the divide by 4.

So reading pixels could be done in this way :

int x = 123;
int y = 321;
UInt32 pixelColour = scan0[(y * stride) + x];

Lastly then, don't forget to unlock your bitmaps when you're done. An easy one to forget. Also, if you're looking to save the changed pixels to another bitmap, you'll want to write the pixels in the same way. I think my example using a pointer as an array to read a pixel should be obvious how to do the same to write pixels then.

One other thought - I hope you're not trying to compare Jpeg or other lossy compressed images - as the changes could be very unpredictable, and not quite what you think. Instead, you need to use loss-less image formats such as BMP and PNG.

Hope this helps someone.

Upvotes: 3

Yasmine GreenApple
Yasmine GreenApple

Reputation: 448

you can take a look at this class : it is an open source code that provide fast methods based on pointers to compare pixels, I could'nt find the link, so I post you the code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace SampleGrabberNET
{
    public unsafe class UnsafeBitmap
    {
        Bitmap bitmap;

  // three elements used for MakeGreyUnsafe
  int width;
  BitmapData bitmapData = null;
    Byte* pBase = null;

  public UnsafeBitmap(Bitmap bitmap)
  {
     this.bitmap = new Bitmap(bitmap);
  }

        public UnsafeBitmap(int width, int height)
    {
        this.bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb);
        }

  public void Dispose()
  {
     bitmap.Dispose();
  }

  public Bitmap Bitmap
  {
 get
     {
    return(bitmap);
     }
  }

  private Point PixelSize
  {
     get
     {
    GraphicsUnit unit = GraphicsUnit.Pixel;
    RectangleF bounds = bitmap.GetBounds(ref unit);

    return new Point((int) bounds.Width, (int) bounds.Height);
     }
  }

  public void LockBitmap()
  {
     GraphicsUnit unit = GraphicsUnit.Pixel;
     RectangleF boundsF = bitmap.GetBounds(ref unit);
     Rectangle bounds = new Rectangle((int) boundsF.X,
    (int) boundsF.Y,
    (int) boundsF.Width,
    (int) boundsF.Height);

     // Figure out the number of bytes in a row
     // This is rounded up to be a multiple of 4
     // bytes, since a scan line in an image must always be a multiple of 4 bytes
     // in length.
     width = (int) boundsF.Width * sizeof(PixelData);
if (width % 4 != 0)
     {
    width = 4 * (width / 4 + 1);
 }
     bitmapData =
    bitmap.LockBits(bounds, ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

     pBase = (Byte*) bitmapData.Scan0.ToPointer();
  }

  public PixelData GetPixel(int x, int y)
  {
            PixelData returnValue = *PixelAt(x, y);
     return returnValue;
  }

        public void SetPixel(int x, int y, PixelData colour)
        {
            PixelData* pixel = PixelAt(x, y);
            *pixel = colour;
        }

  public void UnlockBitmap()
  {
     bitmap.UnlockBits(bitmapData);
     bitmapData = null;
     pBase = null;
  }
        public PixelData* PixelAt(int x, int y)
        {
            return (PixelData*)(pBase + y * width + x * sizeof(PixelData));
        }
    }
    public struct PixelData
    {
        public byte blue;
        public byte green;
        public byte red;
    }

}

Upvotes: 2

Related Questions