使用GDI+比较图像
介绍
.NET 使用托管的GDI+ 提供了一些重要的方法来处理图像和位图. 尽管如此, 当我想要借助GDI+ 比较两幅图像看它们是否相同时, 我感到有点困惑. 我试着在我们的图表组件 ( SimpleChart ) 上运行一些自动化的测试, 并且在测试说明书中我需要知道那些产生出来的图表是否都相同. 要这样做, 我需要在测试中将由SimpleChart产生的每个图表和一个事先被认为是好的图表进行比较. 如果这两个是相同的就通过测试.
首次尝试
比较两幅图像看它们是否相同的第一步是检查每幅图像的尺寸. 如果它们不匹配, 我们几乎可以立刻知道图像是不相同的. 一旦这个快速的测试完成, 我们就需要看看实际图像的内容是否匹配. 最初, 我决定使用GDI+ 中Bitmap 类里的GetPixel
方法来比较第一幅图像和第二幅图像中每个相对应的像素. 如果在任意一点两个像素不匹配, 我们就可以确实地说这两幅图像是不同的. 然而, 如果我们结束了比较测试而没有出现不匹配现象, 那么我们就能推断出两幅图像真正是相同的.
public static CompareResult Compare(Bitmap bmp1, Bitmap bmp2)
{
CompareResult cr = CompareResult.ciCompareOk;
//测试并观察, 看看我们是否拥有同样尺寸的图像
if (bmp1.Size != bmp2.Size)
{
cr = CompareResult.ciSizeMismatch;
}
else
{
//尺寸相同因此开始比较像素
for (int x = 0; x < bmp1.Width
&& cr == CompareResult.ciCompareOk; x++)
{
for (int y = 0; y < bmp1.Height
&& cr == CompareResult.ciCompareOk; y++)
{
if (bmp1.GetPixel(x, y) != bmp2.GetPixel(x, y))
cr = CompareResult.ciPixelMismatch;
}
}
}
return cr;
}
这个方法工作得很好, 但有一个主要的缺点, 缺乏速度. 使用这个方法来比较两幅2000 × 1500像素的图像将花费超过17秒! 要是有超过200张的图像要比较, 这就意味着我的测试将花费接近1小时的时间来完成, 可是我并没有准备要等那么长的时间.
快速哈希
我所需要的就是一个更快的方法去比较两幅图像, 让测试在某种意义上能及时的完成. 与其在每幅图像上使用GetPixel
方法来比较每个像素, 我决定使用比较每幅图像的哈希值来看看它们是否相同, 那将会变得更快. 正如我们知道的, 哈希作为一个固定长度的大量数据的表现, 在本例中指图像数据, 它是一个唯一的值. 两个图像的哈希值匹配当且仅当相应的图像也匹配. 在哈希中图像的小小改变会导致极大的不可预计的改变.
在.NET的System.Security.Cryptography
名称空间下提供了许多不同的哈希算法, 比如SHA1和MD5, 但是我决定使用SHA256Managed
类. 类中的ComputeHash
方法将数据的字节数组形式作为输入参数, 并且产生出一个256位的哈希值. 通过计算并且比较每幅图像的哈希值, 我将很快能够知道图像是否是相同的.
现在剩下的问题仅仅是怎样将储存在GDI+ 中Bitmap
对象里的图像数据转换为一个合适的形式, 也就是字节数组, 以传递给 ComputeHash
方法. 最初, 我考虑Bitmap
类中的LockBits
方法, 它可以允许我访问每个像素字节, 但这意味着走进一个非托管代码的地带, 那是我最不想访问的地方. 相反的, GDI+好意地提供了一个ImageConvertor
类, 它允许我们将图像对象转换成另一种类型,比如字节数组.
想了解两幅图像是否相同的最后步骤就是比较两个哈希值(或者说是字节数组)看它们是否匹配. 以下是最终的代码.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Security.Cryptography;
namespace Imagio
{
public class ComparingImages
{
public enum CompareResult
{
ciCompareOk,
ciPixelMismatch,
ciSizeMismatch
};
public static CompareResult Compare(Bitmap bmp1, Bitmap bmp2)
{
CompareResult cr = CompareResult.ciCompareOk;
//测试并观察, 看看我们是否拥有同样尺寸的图像
if (bmp1.Size != bmp2.Size)
{
cr = CompareResult.ciSizeMismatch;
}
else
{
//将每幅图像转换成字节数组
System.Drawing.ImageConverter ic =
new System.Drawing.ImageConverter();
byte[] btImage1 = new byte[1];
btImage1 = (byte[])ic.ConvertTo(bmp1, btImage1.GetType());
byte[] btImage2 = new byte[1];
btImage2 = (byte[])ic.ConvertTo(bmp2, btImage2.GetType());
//为每幅图像计算哈希值
SHA256Managed shaM = new SHA256Managed();
byte[] hash1 = shaM.ComputeHash(btImage1);
byte[] hash2 = shaM.ComputeHash(btImage2);
//比较哈希值
for (int i = 0; i < hash1.Length && i < hash2.Length
&& cr == CompareResult.ciCompareOk; i++)
{
if (hash1[i] != hash2[i])
cr = CompareResult.ciPixelMismatch;
}
}
return cr;
}
}
}
结论
在一个2000 x 1500像素的位图上运行这个新的比较方法仅花费0.28秒的比较时间, 这意味着自动测试200个SimpleChart图像现在只要56秒就可以完成了.
哈希算法通常作为一种安全工具来使用看看是否符合资格, 比如密码的匹配. 使用相同的哈希方法, 我们也能很快的比较两幅图像看看它们是否相同.
Comparing Images Using GDI+
Introduction
.NET provides some great methods for working with images and bitmaps using the managed GDI+ methods. However, I found myself a bit stuck even with GDI+ when I wanted to compare two images to see if they were identical. I was trying to run some automated tests on our charting component, SimpleChart, and I needed to know if the charts being produced were identical to those in the test specification. To do this, I needed to compare each image being generated by SimpleChart in the test with a reference image that was known to be good. If the two were identical then the test had passed.
First Attempts
The first step in comparing two images to see if they were identical was to check the size of each. If they don't match then we know almost immediately that the images are not identical. Once that quick test was complete, we needed to look at the actual image content to see if it matched up. Initially, I decided to use GetPixel
method of the GDI+ Bitmap
class to compare each pixel in the first image with the corresponding pixel in the second image. If at any point, the two pixels did not match then we can safely say that the images are different. If, however, we got to the end of the comparison tests without any mismatches then we can conclude that the two images are indeed identical.
public static CompareResult Compare(Bitmap bmp1, Bitmap bmp2)
{
CompareResult cr = CompareResult.ciCompareOk;
//Test to see if we have the same size of image
if (bmp1.Size != bmp2.Size)
{
cr = CompareResult.ciSizeMismatch;
}
else
{
//Sizes are the same so start comparing pixels
for (int x = 0; x < bmp1.Width
&& cr == CompareResult.ciCompareOk; x++)
{
for (int y = 0; y < bmp1.Height
&& cr == CompareResult.ciCompareOk; y++)
{
if (bmp1.GetPixel(x, y) != bmp2.GetPixel(x, y))
cr = CompareResult.ciPixelMismatch;
}
}
}
return cr;
}
This method worked fine but with one major drawback, speed, or rather the lack of it. Comparing two 2000 x 1500 pixel images using this method took over 17 seconds! With over 200 images to compare, this meant that my tests would take nearly an hour to complete and I wasn't prepared to wait that long.
Hash in a Flash
What I needed was a faster method to compare the images to allow the tests to complete in a timely manner. Rather than comparing the individual pixels in each image using GetPixel
, I decided that it would be quicker if I could some how compare a 'hash' of each image to see if they were identical. As we know, a hash is a unique value of a fixed size representing a large amount of data, in this case our image data. Hashes of two images should match if and only if the corresponding images also match. Small changes to the image result in large unpredictable changes in the hash.
There are many different hashing algorithms provided by .NET in the System.Security.Cryptography
namespace such as SHA1 and MD5 but I decided to use the SHA256Managed
class. The ComputeHash
method of this class takes a byte array of data as an input parameter and produces a 256 bit hash of that data. By computing and then comparing the hash of each image, I would be quickly able to tell if the images were identical or not.
The only problem now remaining was how to convert the image data stored in the GDI+ Bitmap
objects to a suitable form for passing to the ComputeHash
method, namely a byte array. Initially, I looked at the LockBits
method of the Bitmap
class which allowed me access to the individual pixel bytes but it would have meant a journey into the land of unmanaged code and that was somewhere I really didn't want to visit. Instead, GDI+ kindly provides an ImageConvertor
class to allow us to convert Image
(or Bitmap
) objects from one data type to another, such as a byte array.
The final step to see if the images are identical is to compare the two hash values (also stored in byte arrays) to see if they match. Here is the final code:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Security.Cryptography;
namespace Imagio
{
public class ComparingImages
{
public enum CompareResult
{
ciCompareOk,
ciPixelMismatch,
ciSizeMismatch
};
public static CompareResult Compare(Bitmap bmp1, Bitmap bmp2)
{
CompareResult cr = CompareResult.ciCompareOk;
//Test to see if we have the same size of image
if (bmp1.Size != bmp2.Size)
{
cr = CompareResult.ciSizeMismatch;
}
else
{
//Convert each image to a byte array
System.Drawing.ImageConverter ic =
new System.Drawing.ImageConverter();
byte[] btImage1 = new byte[1];
btImage1 = (byte[])ic.ConvertTo(bmp1, btImage1.GetType());
byte[] btImage2 = new byte[1];
btImage2 = (byte[])ic.ConvertTo(bmp2, btImage2.GetType());
//Compute a hash for each image
SHA256Managed shaM = new SHA256Managed();
byte[] hash1 = shaM.ComputeHash(btImage1);
byte[] hash2 = shaM.ComputeHash(btImage2);
//Compare the hash values
for (int i = 0; i < hash1.Length && i < hash2.Length
&& cr == CompareResult.ciCompareOk; i++)
{
if (hash1[i] != hash2[i])
cr = CompareResult.ciPixelMismatch;
}
}
return cr;
}
}
}
Conclusion
Running this new compare method on a 2000 x 1500 pixel bitmap resulted in a comparison time of 0.28 seconds which meant that my automated testing of 200 SimpleChart images now takes only 56 seconds to complete.
Hashes are normally used as a security tool to see if credentials such as passwords match. Using the same hashing approach, we can also quickly compare two images to see if they too are identical.
备注: 以上文章选自www.CodeProject.com