使用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;
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;
}
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;
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;
}
}
}
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
原文鏈接: http://www.codeproject.com/dotnet/comparingimages.asp