6

This is a bit puzzling here. The following code is part of a little testing application to verify that code changes didn't introduce a regression. To make it fast we used memcmp which appears to be the fastest way of comparing two images of equal size (unsurprisingly).

However, we have a few test images that exhibit a rather surprising problem: memcmp on the bitmap data tells us that they are not equal, however, a pixel-by-pixel comparison doesn't find any difference at all. I was under the impression that when using LockBits on a Bitmap you get the actual raw bytes of the image. For a 24 bpp bitmap it's a bit hard to imagine a condition where the pixels are the same but the underlying pixel data isn't.

A few surprising things:

  1. The differences are always single bytes that are 00 in one image and FF in the other.
  2. If one changes the PixelFormat for LockBits to Format32bppRgb or Format32bppArgb, the comparison succeeds.
  3. If one passes the BitmapData returned by the first LockBits call as 4th argument to the second one, the comparison succeeds.
  4. As noted above, the pixel-by-pixel comparison succeeds as well.

I'm a bit stumped here because frankly I cannot imagine why this happens.

(Reduced) Code below. Just compile with csc /unsafe and pass a 24bpp PNG image as first argument.

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

class Program
{
    public static void Main(string[] args)
    {
        Bitmap title = new Bitmap(args[0]);
        Console.WriteLine(CompareImageResult(title, new Bitmap(title)));
    }

    private static string CompareImageResult(Bitmap bmp, Bitmap expected)
    {
        string retval = "";

        unsafe
        {
            var rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
            var resultData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat);
            var expectedData = expected.LockBits(rect, ImageLockMode.ReadOnly, expected.PixelFormat);

            try
            {
                if (memcmp(resultData.Scan0, expectedData.Scan0, resultData.Stride * resultData.Height) != 0)
                    retval += "Bitmap data did not match\n";
            }
            finally
            {
                bmp.UnlockBits(resultData);
                expected.UnlockBits(expectedData);
            }
        }

        for (var x = 0; x < bmp.Width; x++)
            for (var y = 0; y < bmp.Height; y++)
                if (bmp.GetPixel(x, y) != expected.GetPixel(x, y))
                {
                    Console.WriteLine("Pixel diff at {0}, {1}: {2} - {3}", x, y, bmp.GetPixel(x, y), expected.GetPixel(x, y));
                    retval += "pixel fail";
                }

        return retval != "" ? retval : "success";
    }

    [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    static extern int memcmp(IntPtr b1, IntPtr b2, long count);
}
Community
  • 1
  • 1
Joey
  • 344,408
  • 85
  • 689
  • 683

2 Answers2

7

Take a look at this, which pictorially illustrates a LockBits buffer - it shows the Rows of Strides and where Padding can appear at the end of the Stride (if it's needed).

A stride is probably aligned to the 32bit (i.e. word) boundary (for efficiency purposes)...and the extra unused space at the end of the stride is to make the next Stride be aligned.

So that's what's giving you the random behaviour during the comparison...spurious data in the Padding region.

When you are using Format32bppRgb and Format32bppArgb that's naturally word aligned, so I guess you don't have any extra unused bits on the end, which is why it works.

Andrew Morton
  • 24,203
  • 9
  • 60
  • 84
Colin Smith
  • 12,375
  • 4
  • 39
  • 47
  • 2
    To add to the answer: you have to compare each scan line, *width x height* bytes individually. You can't just do a `memcmp` on the entire array of pixel values, because there can be 1, 2, or 3 unused bytes per scan line. – Jim Mischel Aug 30 '12 at 21:03
4

Just an educated guess:

24 bits (3 bytes) is a little bit awkward on 32/64 bit hardware.

With this format there are bound to be buffers that are flushed out to a multiple of 4 bytes, leaving 1 or more bytes as 'don't care' . They can contain random data and the software doesn't feel obliged to zero them out. This will make memcmp fail.

H H
  • 263,252
  • 30
  • 330
  • 514
  • When I print the length of the data to compare it is equal to *width* × *height* × 3, so I guess this guess doesn't apply. **EDIT**, nevermind. Not always, apparently. A 133×12 image yields a length of 4800. There appears to be a bit padding at the end. Still, the data itself seems to be packed. – Joey Aug 30 '12 at 20:56
  • @Joey To confirm this, try looping through each row and memcmp the Stride. If that does not fail, then Henk is correct. – Tergiver Aug 30 '12 at 20:58
  • @Joey corrected comment: You don't compare the entire Stride, you compare Width * 3. – Tergiver Aug 30 '12 at 20:59
  • @Joey The reason that setting the PixelFormat to a 32-bpp value works is because the stride will then be an even multiple of four. – Tergiver Aug 30 '12 at 21:02
  • Sounds good; my first test image (where the size matched exactly) was 40 px wide, which fits. – Joey Aug 30 '12 at 21:03
  • The docs say: _The stride is the width of a single row of pixels (a scan line), rounded up to a four-byte boundary._ So do test with varying image widths. – H H Aug 30 '12 at 21:07