以图搜图实现之均值哈希

前言

最近在逛淘宝时发现了淘宝的图片搜索功能,可能是我太Low了这个技术点已经实现很长时间了。想想自己能不能实现这个功能,起初我是这么想的,对两张图片从左上角的第一个像素点一直比较到右下角的最后一个像素点,并在比较时记录它们的相似度,可能是我太天真了(主要还是知识限制了想象),这样做有很多问题,比如说两张图片大小不一致、核心要素点的位置不同等…最终只得借助网络了,找到了一种叫做均值哈希的算法(Average hash algorithm),接下来具体阐述它的基本思路以及适用场景。

均值哈希的基本思路

1、缩小尺寸:

去除图片的高频和细节的最快方法是缩小图片,将图片缩小到8x8的尺寸,总共64个像素。不要保持纵横比,只需将其变成8乘8的正方形。这样就可以比较任意大小的图片,摒弃不同尺寸、比例带来的图片差异。

2、简化色彩:

将8乘8的小图片转换成灰度图像。

3、计算平均值:

计算所有64个像素的灰度平均值。

4、比较像素的灰度:

将每个像素的灰度,与平均值进行比较。大于或等于平均值,记为1;小于平均值,记为0。

5、计算hash值:

将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了。

如果图片放大或缩小,或改变纵横比,结果值也不会改变。增加或减少亮度或对比度,或改变颜色,对hash值都不会太大的影响。最大的优点:计算速度快!

那么完成了以上步骤,一张图片就相当于有了自己的”指纹”了,然后就是计算不同位的个数,也就是汉明距离(例如1010001与1011101的汉明举例就是2,也就是不同的个数)。

如果汉明距离小于5,则表示有些不同,但比较相近,如果汉明距离大于10则表明完全不同的图片。

以上就是均值哈希的基本实现思路,总体来说是比较简单的。

C#实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class ImageHashHelper
{
/// <summary>
/// 获取缩略图
/// </summary>
/// <returns></returns>
private static Bitmap GetThumbImage(Image image, int w, int h)
{
Bitmap bitmap = new Bitmap(w, h);
Graphics g = Graphics.FromImage(bitmap);
g.DrawImage(image,
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
new Rectangle(0, 0, image.Width, image.Height), GraphicsUnit.Pixel);
return bitmap;
}

/// <summary>
/// 将图片转换为灰度图像
/// </summary>
/// <returns></returns>
private static Bitmap ToGray(Bitmap bmp)
{
for (int i = 0; i < bmp.Width; i++)
{
for (int j = 0; j < bmp.Height; j++)
{
//获取该点的像素的RGB的颜色
Color color = bmp.GetPixel(i, j);
//利用公式计算灰度值
int gray = (int)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11);//计算方式1
//int gray1 = (int)((color.R + color.G + color.B) / 3.0M);//计算方式2
Color newColor = Color.FromArgb(gray, gray, gray);
bmp.SetPixel(i, j, newColor);
}
}
return bmp;
}

/// <summary>
/// 获取图片的均值哈希
/// </summary>
/// <returns></returns>
public static int[] GetAvgHash(Bitmap bitmap)
{
Bitmap newBitmap = ToGray(GetThumbImage(bitmap, 8, 8));
int[] code = new int[64];
//计算所有64个像素的灰度平均值。
List<int> allGray = new List<int>();
for (int row = 0; row < bitmap.Width; row++)
{
for (int col = 0; col < bitmap.Height; col++)
{
allGray.Add(newBitmap.GetPixel(row, col).R);
}
}
double avg = allGray.Average(a => a);//拿到平均值
//比较像素的灰度
for (int i = 0; i < allGray.Count; i++)
{
code[i] = allGray[i] >= avg ? 1 : 0;//将比较结果进行组合
}
//返回结果
return code;
}

/// <summary>
/// 对两个AvgHash进行比较
/// </summary>
/// <returns></returns>
public static int Compare(int[] code1, int[] code2)
{
int v = 0;
for (int i = 0; i < 64; i++)
{
if (code1[i] == code2[i])
{
v++;
}
}
return v;
}
}

这里我们在GetAvgHash函数中获取64个像素的灰度值时直接通过了R来获取,因为RGB都是一样的,所以哪一个都可以。

灰度值换算:https://baike.baidu.com/item/%E7%81%B0%E5%BA%A6%E5%80%BC/10259111?fr=aladdin

效果演示:

1、原图查找

2、完全马赛克查找

源码下载:

点击下载源码

最后

均值哈希适合缩略图查找原图,人相匹配等并不适用。