无意中看见一篇博客,是讲仿造google搜图的,链接如下:
Google 以图搜图 - 相似图片搜索原理 - Java实现
觉得挺好玩的,博主使用Java实现的,于是我用 OpenCv实现了下。
根据看到的博文,里面说到,Google图像搜索的关键技术是“感知压缩算法”(Perceptual hash algorithm),它的作用是对每张图片生成一个“指纹”(fingerprint)字符串,然后比较不同图片的指纹。结果越接近,就说明图片越相似。看到这里我就突然来了兴趣想自己实现!
接下来,简单介绍以下感知哈希算法:实验室邹晓艺师兄已经总结得挺好了,所以,我摘取部分:
基于低频的均值哈希
一张图片就是一个二维信号,它包含了不同频率的成分。如下图所示,亮度变化小的区域是低频成分,它描述大范围的信息。而亮度变化剧烈的区域(比如物体的边缘)就是高频的成分,它描述具体的细节。或者说高频可以提供图片详细的信息,而低频可以提供一个框架。
而一张大的,详细的图片有很高的频率,而小图片缺乏图像细节,所以都是低频的。所以我们平时的下采样,也就是缩小图片的过程,实际上是损失高频信息的过程。如下图:
上述这些东西,我们在DSP课堂上都可以学到。
均值哈希算法主要是利用图片的低频信息,其工作过程如下:
(1)缩小尺寸:去除高频和细节的最快方法是缩小图片,将图片缩小到8x8的尺寸,总共64个像素。不要保持纵横比,只需将其变成8*8的正方形。这样就可以比较任意大小的图片,摒弃不同尺寸、比例带来的图片差异。
(2)简化色彩:将8*8的小图片转换成灰度图像。
(3)计算平均值:计算所有64个像素的灰度平均值。
(4)比较像素的灰度:将每个像素的灰度,与平均值进行比较。大于或等于平均值,记为1;小于平均值,记为0。
(5)计算hash值:将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了。(我设置的是从左到右,从上到下用二进制保存)。
计算一个图片的 hash 指纹就是这么简单,计算出来的hash指纹相对比原来的图片已经丢失了太多的信息了,以至于我们都怀疑这样的指纹是不是真的能够识别出相似的图片。不过,结果当然是不用怀疑的,数学之美与编程之美的结合!
如果图片放大或缩小,或改变纵横比,结果值也不会改变。增加或减少亮度或对比度,或改变颜色,对hash值都不会太大的影响。这种方法是被图片的最大的优点:计算速度快!
因为就像我们看到的,一幅图片被压缩,被转为灰度图,只采集hash指纹,这个过程计算量并不大,而这些指纹就相当于图片的特征。
比较两个图片的相似性,就是先计算这两张图片的hash指纹,也就是64位0或1值,然后计算不同位的个数(汉明距离)。如果这个值为0,则表示这两张图片非常相似,如果汉明距离小于5,则表示有些不同,但比较相近,如果汉明距离大于10则表明完全不同的图片。
实际情况中,我自己写的时候,发现汉明距离小于5的要求太苛刻了,汉明距离接近20的两张图片的相似度还是挺高的。
其实看到这里,你可以根据这个思路自己去实现以下,不用看下面的代码,一直觉得这样才是学习的好方法,看博客拓展思路,自己去实现。
接下来说代码实现:
首先介绍 getImageFinger 函数,也就是生成图像的指纹,代码如下:
// 寻找图像指纹
// 参数: 输入图像 img ,图像的指纹数组
void getImageFinger(IplImage * img,char *status)
{
int avrpixel= 0;
int i,j;
CvScalar scalar ;
for (i = 0;i<8;i++)
{
for (j = 0;j<8;j++)
{
scalar = cvGet2D(img,i,j);
avrpixel += scalar.val[0];
}
}
avrpixel = avrpixel / 64 ;
int k = 0;
for (i = 0;i<8;i++)
{
for (j = 0;j<8;j++)
{
if (cvGet2D(img,i,j).val[0] > avrpixel)
{
status[k++] = 1;
}else
{
status[k++] = 0;
}
}
}
}
根据上面的原理就可以理解代码。
接下来是计算汉明距离的函数:
// 计算汉明距离
// 参数: 两幅图的指纹数组
// 输出: 汉明距离
int calHammDist(char * src_img,char * dst_img)
{
int dist = 0;
for(int i= 0;i<MAX_PIXEL_NUMBER;i++)
{
if (src_img[i] != dst_img[i])
{
dist ++;
}
}
return dist;
}
搜素图片匹配的过程代码:
// 读取本地某目录下的全部图片进行搜索匹配
int show_number = 0;
for (int i = 0;i<7;i++)
{
sprintf(FilePath,"F://image//%d.JPG",i);
dst_img = cvLoadImage(FilePath);
resz_dst_img = cvCreateImage(cvSize(8,8),IPL_DEPTH_8U,3);
cvResize(dst_img,resz_dst_img,1);
com_dst_img = cvCreateImage(cvSize(8,8),IPL_DEPTH_8U,1);
cvCvtColor(resz_dst_img,com_dst_img,CV_RGB2GRAY);
getImageFinger(com_dst_img,status_dst);
dis = calHammDist(status_src,status_dst);
cout<<" dis :"<<dis<<endl;
if (dis<20)
{
showimgindex[show_number++] = i;
}
}
for 循环中 i 小于7 是因为我只放了7张图片,具体可以自己修改。
效果如下:
这是我本地的要搜索的7张图片
左边第一张为输入图片,第二张和第三张是匹配出来的相似度比较高的图片,第二张其实就是原来的图片,只是改变了大小尺寸,根据下面的命令行打印出来的汉明距离,第一张跟第二张的汉明距离为0,第一张和第三张的汉明距离为10,相似度还是比较高的
这里就不贴出全部代码了,实现起来不是很难,大家可以自己去试试。