前言
最近在逛淘宝时发现了淘宝的图片搜索功能,可能是我太 Low 了这个技术点已经实现很长时间了。想想自己能不能实现这个功能,起初我是这么想的,对两张图片从左上角的第一个像素点一直比较到右下角的最后一个像素点,并在比较时记录它们的相似度,可能是我太天真了 (主要还是知识限制了想象),这样做有很多问题,比如说两张图片大小不一致、核心要素点的位置不同等 … 最终只得借助网络了,找到了一种叫做均值哈希的算法 (Average hash algorithm),接下来具体阐述它的基本思路以及适用场景。
均值哈希的基本思路
1、缩小尺寸:
去除图片的高频和细节的最快方法是缩小图片,将图片缩小到 8 ×8 的尺寸,总共 64 个像素。不要保持纵横比,只需将其变成 8 乘 8 的正方形。这样就可以比较任意大小的图片,摒弃不同尺寸、比例带来的图片差异。
2、简化色彩:
将 8 乘 8 的小图片转换成灰度图像。
3、计算平均值:
计算所有 64 个像素的灰度平均值。
4、比较像素的灰度:
将每个像素的灰度,与平均值进行比较。大于或等于平均值,记为 1;小于平均值,记为 0。
5、计算 hash 值:
将上一步的比较结果,组合在一起,就构成了一个 64 位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了。
如果图片放大或缩小,或改变纵横比,结果值也不会改变。增加或减少亮度或对比度,或改变颜色,对 hash 值都不会太大的影响。最大的优点:计算速度快!
那么完成了以上步骤, 一张图片就相当于有了自己的 ” 指纹 ” 了, 然后就是计算不同位的个数,也就是汉明距离(例如 1010001 与 1011101 的汉明举例就是 2,也就是不同的个数)。
如果汉明距离小于 5,则表示有些不同,但比较相近,如果汉明距离大于 10 则表明完全不同的图片。
以上就是均值哈希的基本实现思路,总体来说是比较简单的。
C# 实现
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/…
效果演示:
1、原图查找
2、完全马赛克查找
源码下载:
点击下载源码
最后
均值哈希适合缩略图查找原图,人相匹配等并不适用。