简单介绍一下哈希感知算法:

“感知哈希算法”(Perceptual hash algorithm),它的作用是对每张图片生成一个"指纹"(fingerprint)字符串,然后比较不同图片的指纹。结果越接近,就说明图片越相似。

算法步骤:

第一步,缩小尺寸。

最快速的去除高频和细节,只保留结构明暗的方法就是缩小尺寸。

将图片缩小到8x8的尺寸,总共64个像素。摒弃不同尺寸、比例带来的图片差异。

如这张图片:

处理前:

opencv 图片哈希指纹 相似度比较 java_i++


处理后(为方便查看,这里将图片放大了):

opencv 图片哈希指纹 相似度比较 java_#pragma_02

第二步,简化色彩。
将缩小后的图片,转为64级灰度。也就是说,所有像素点总共只有64种颜色。
第三步,计算DCT(离散余弦变换)。
DCT是把图片分解频率聚集和梯状形,虽然JPEG使用8x8的DCT变换,在这里使用32x32的DCT变换。
第四步,缩小DCT。
虽然DCT的结果是32x32大小的矩阵,但我们只要保留左上角的8x8的矩阵,这部分呈现了图片中的最低频率。
第五步,计算平均值。
计算所有64个值的平均值。
第六步,进一步减小DCT。
这是最主要的一步,根据8x8的DCT矩阵,设置0或1的64位的hash值,大于等于DCT均值的设为”1”,小于DCT均值的设为“0”。结果并不能告诉我们真实性的低频率,只能粗略地告诉我们相对于平均值频率的相对比例。只要图片的整体结构保持不变,hash结果值就不变。能够避免伽马校正或颜色直方图被调整带来的影响。
第七步,计算哈希值。
将64bit设置成64位的长整型,组合的次序并不重要,只要保证所有图片都采用同样次序就行了。将32x32的DCT转换成32x32的图像。
将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了(例如,自左到右、自顶向下、big-endian)。
得到指纹以后,就可以对比不同的图片,看看64位中有多少位是不一样的。在理论上,这等同于计算“汉明距离”(Hammingdistance)。如果不相同的数据位不超过5,就说明两张图片很相似;如果大于10,就说明这是两张不同的图片。
下面附上代码:

//
//  main.cpp
//  图像对比
//
//  Created by 王泽一 on 2021/3/29.
//  Copyright © 2021 nan. All rights reserved.


//
//导入所需的库
#include </Users/wangzeyi/Downloads/stdafx.h>//我的自己的文件所在的位置,需要自己自行修改
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <stdio.h>
#include "opencv2/imgcodecs/legacy/constants_c.h"
#pragma comment(lib,"opencv_core2410d.lib")
#pragma comment(lib,"opencv_highgui2410d.lib")
#pragma comment(lib,"opencv_imgproc2410d.lib")


using namespace std;
using namespace cv;

int main()
{

    string strSrcImageName1 = "/Users/wangzeyi/Downloads/IMG_4808.JPG";//读取照片1
    string strSrcImageName2 = "/Users/wangzeyi/Downloads/IMG_4806 2.JPG";//读取照片2
    Mat matSrca, matSrcb, matSrc1, matSrc2;
    //将两张图片读取为彩色
    matSrca = imread(strSrcImageName1, IMREAD_COLOR);
    matSrcb = imread(strSrcImageName2, IMREAD_COLOR);
    
    CV_Assert(matSrca.channels() == 3);
    CV_Assert(matSrcb.channels() == 3);
    cv::resize(matSrca, matSrc1, cv::Size(357, 419), 0, 0, cv::INTER_NEAREST);
    //cv::flip(matSrc1, matSrc1, 1);
    cv::resize(matSrcb, matSrc2, cv::Size(2177, 3233), 0, 0, cv::INTER_LANCZOS4);

    cv::Mat matDst1, matDst2;
    //调整jpg图片大小
    cv::resize(matSrc1, matDst1, cv::Size(8, 8), 0, 0, cv::INTER_CUBIC);
    cv::resize(matSrc2, matDst2, cv::Size(8, 8), 0, 0, cv::INTER_CUBIC);
    
    //update 20181206 for the bug cvtColor
    cv::Mat temp1 = matDst1;
    cv::Mat temp2 = matDst2;
    cv::cvtColor(temp1 , matDst1, COLOR_BGR2GRAY);
    cv::cvtColor(temp2 , matDst2, COLOR_BGR2GRAY);

    int iAvg1 = 0, iAvg2 = 0;
    int arr1[64], arr2[64];

    for (int i = 0; i < 8; i++)
    {
        uchar* data1 = matDst1.ptr<uchar>(i);
        uchar* data2 = matDst2.ptr<uchar>(i);

        int tmp = i * 8;

        for (int j = 0; j < 8; j++)
        {
            int tmp1 = tmp + j;

            arr1[tmp1] = data1[j] / 4 * 4;
            arr2[tmp1] = data2[j] / 4 * 4;

            iAvg1 += arr1[tmp1];
            iAvg2 += arr2[tmp1];
        }
    }

    iAvg1 /= 64;
    iAvg2 /= 64;

    for (int i = 0; i < 64; i++)
    {
        arr1[i] = (arr1[i] >= iAvg1) ? 1 : 0;
        arr2[i] = (arr2[i] >= iAvg2) ? 1 : 0;
    }

    int iDiffNum = 0;
    //等同于计算明汉距离
    for (int i = 0; i < 64; i++)
        if (arr1[i] != arr2[i])
            ++iDiffNum;
  
    cout<<"iDiffNum = "<<iDiffNum<<endl;

    if (iDiffNum <= 5)
        cout<<"two images are very similar!"<<endl;
    else if (iDiffNum > 10)
        cout<<"they are two different images!"<<endl;
    else
        cout<<"two image are somewhat similar!"<<endl;
    imshow("123",matSrca);
    imshow("12",matSrcb);
    waitKey(0);
    getchar();
    return 0;
}

运行结果:

相似:

opencv 图片哈希指纹 相似度比较 java_#include_03


opencv 图片哈希指纹 相似度比较 java_#pragma_04


相同:

opencv 图片哈希指纹 相似度比较 java_#pragma_05


不同

opencv 图片哈希指纹 相似度比较 java_i++_06


参考资料:

相似图片搜索的原理

http://www.ruanyifeng.com/blog/2011/07/principle_of_similar_image_search.html