Title: Perform image hashing in C#
Image hashing or (perceptual image hashing) attempts to reduce an image to a concise code that represents the image so you can compare it to other images to see if they are the same. This example uses a difference hash algorithm that takes the following steps.
- Reduce the image size - The image is shrunk to 9x9 pixels so all images have a standard size.
- Reduce the color - The 9x9 image is converted to grayscale.
- Calculate the hash - The program scans the image's first 8 rows. For each row, it compares adjacent pairs of pixels. If the right pixel is brighter than the left pixel in a pair, the algorithm adds 1 to the hash code. If the right pixel is darker, the algorithm adds 0 to the hash code. The program then repeats the process for the left 8 columns.
Because there are nine pixels in each row and column, there are eight adjacent pairs of pixels so the hash includes eight zeros or ones for each row and column. The algorithm considers eight rows and eight columns, so the hash code includes a total of 8 * 8 + 8 * 8 = 128 zeros and ones.
Click the Load Image buttons to select two image files to compare. When you click Compare, the program executes the following code to compare the images.
// Compare the images.
private void btnCompare_Click(object sender, EventArgs e)
{
string hash_code1 = ProcessImage(picImage1.Image as Bitmap,
picShrunk1, picMonochrome1, txtHashCode1);
string hash_code2 = ProcessImage(picImage2.Image as Bitmap,
picShrunk2, picMonochrome2, txtHashCode2);
// Display the difference and score.
string difference = "";
int score = 0;
for (int i = 0; i < hash_code1.Length; i++)
{
if (hash_code1[i] == hash_code2[i])
{
difference += " ";
}
else
{
difference += "X";
score++;
}
}
txtDifference.Text = difference;
txtScore.Text = score.ToString();
float percent = (hash_code1.Length - score) / (float)hash_code1.Length;
txtPercent.Text = percent.ToString("P");
}
This code calls the ProcessImage method to do most of the interesting work on the two images. It then compares the two images' hash codes character-by-character. It then displays the difference string.
The code also counts the differences and displays the count as a score indicating how similar to the two images are. A low score indicates few differences so the images are more similar.
The code finishes by displaying the percentage of characters in the hash code that are the same. A value close to 100% indicates that the images are very similar.
The following code shows the ProcessImage method.
private string ProcessImage(Bitmap original_bm,
PictureBox pic_shrunk, PictureBox pic_monochrome,
TextBox txt_hashcode)
{
// Shrink the original image and display the result.
Bitmap shrunk_bm = ScaleTo(original_bm, 9, 9,
InterpolationMode.High);
pic_shrunk.Image = ScaleTo(shrunk_bm, 90, 90,
InterpolationMode.NearestNeighbor);
// Convert to grayscale and display the result.
Bitmap grayscale_bm = ToMonochrome(shrunk_bm);
pic_monochrome.Image = ScaleTo(grayscale_bm, 90, 90,
InterpolationMode.NearestNeighbor);
// Calculate the hash code.
string hash_code = GetHashCode(grayscale_bm);
txt_hashcode.Text = hash_code;
return hash_code;
}
This method calls the ScaleTo method to shrink the image to 9x9 pixels. The first piece of blue code enlarges the image and displays it in the pic_shrunk PictureBox so you can see the shrunk image.
Next, the code calls the ToMonochrome method to convert the small image to monochrome. The second piece of blue code enlarges the monochrome image and displays it in the pic_monochrome PictureBox so you can see it.
Finally, the code calls the GetHashCode method to perform the last step in the image hashing.
The following code shows the ScaleTo helper method.
// Scale an image.
private Bitmap ScaleTo(Bitmap bm, int wid, int hgt,
InterpolationMode interpolation_mode)
{
Bitmap new_bm = new Bitmap(wid, hgt);
using (Graphics gr = Graphics.FromImage(new_bm))
{
RectangleF source_rect = new RectangleF(-0.5f, -0.5f, bm.Width, bm.Height);
Rectangle dest_rect = new Rectangle(0, 0, wid, hgt);
gr.InterpolationMode = interpolation_mode;
gr.DrawImage(bm, dest_rect, source_rect, GraphicsUnit.Pixel);
}
return new_bm;
}
This method creates a bitmap with the desired size and an associated Graphics object. It then draws the original image on the bitmap and returns it.
Note that this method does not try to preserve the image's aspect ratio. For example, if the original image is tall and thin but the desired size is square, as it is for this image hashing algorithm, then the image is stretched to fit.
The following code shows the ToMonochrome helper method.
// Convert an image to monochrome.
private Bitmap ToMonochrome(Image image)
{
// Make the ColorMatrix.
ColorMatrix cm = new ColorMatrix(new float[][]
{
new float[] {0.299f, 0.299f, 0.299f, 0, 0},
new float[] {0.587f, 0.587f, 0.587f, 0, 0},
new float[] {0.114f, 0.114f, 0.114f, 0, 0},
new float[] { 0, 0, 0, 1, 0},
new float[] { 0, 0, 0, 0, 1}
});
ImageAttributes attributes = new ImageAttributes();
attributes.SetColorMatrix(cm);
// Draw the image onto the new bitmap while
// applying the new ColorMatrix.
Point[] points =
{
new Point(0, 0),
new Point(image.Width, 0),
new Point(0, image.Height),
};
Rectangle rect = new Rectangle(0, 0, image.Width, image.Height);
// Make the result bitmap.
Bitmap bm = new Bitmap(image.Width, image.Height);
using (Graphics gr = Graphics.FromImage(bm))
{
gr.DrawImage(image, points, rect,
GraphicsUnit.Pixel, attributes);
}
// Return the result.
return bm;
}
This method creates a ColorMatrix that converts an image to grayscale. The numbers multiply each pixel's red, green, and blue color components by the indicated values to create a monochrome result. The code then creates a new bitmap and copies the image into it, using the ColorMatrix to adjust the new image's colors.
The following code shows the final piece of the program, the GetHashCode method that performs the actual image hashing.
// Return the hashcode for this 9x9 image.
private string GetHashCode(Bitmap bm)
{
string row_hash = "";
for (int r = 0; r < 8; r++)
for (int c = 0; c < 8; c++)
if (bm.GetPixel(c + 1, r).R >= bm.GetPixel(c, r).R)
row_hash += "1";
else
row_hash += "0";
string col_hash = "";
for (int c = 0; c < 8; c++)
for (int r = 0; r < 8; r++)
if (bm.GetPixel(c, r + 1).R >= bm.GetPixel(c, r).R)
col_hash += "1";
else
col_hash += "0";
return row_hash + "," + col_hash;
}
This method loops through the first eight rows, comparing adjacent pixels to add 0s and 1s to the hash string. It then repeats the same steps for the image's first eight columns to create a column hash code. Finally, the method concatenates the two codes separated by a comma and returns the result. (The comma is just to make it easier to tell where the row codes end and the column codes begin.)
You could save space by storing an image's hash codes in two 64-bit integers. I didn't do that because it would make comparing the codes harder and would make them more difficult to visualize.
This image hashing method is good at identifying two images that are roughly the same but that may have different sizes or that have been modified by image compression algorithms.
I've seen at least one other implementation that rotated images by various angles to try to determine if two images are the same but rotated.
Unfortunately, I don't of a reasonable way to determine whether one image is part of another or if two images share some parts. For example, if you crop an image, this method probably won't be able to tell that the original and cropped version are similar.
Download the example to experiment with it and to see additional details.
|