利用OpenCV感知哈希算法进行图片相似度对比

--- by Liuhua.chen


笔者日前在项目中需要处理相似图片对比的问题,经过网络搜寻资料加上一些个人研究,顺利完成功能开发。特分享如下;

首先采用了主流框架OpenCV,该框架同时提供了以下几种对比图片方法。

1、PSNR峰值信噪比

PSNR是最普遍和使用最为广泛的一种图像客观评价指标,然而它是基于对应像素点间的误差,即基于误差敏感的图像质量评价。由于并未考虑到人眼的视觉特性(人眼对空间频率较低的对比差异敏感度较高,人眼对亮度对比差异的敏感度较色度高,人眼对一个区域的感知结果会受到其周围邻近区域的影响等),因而经常出现评价结果与人的主观感觉不一致的情况。

2、直方图方法

方法描述:有两幅图像patch(当然也可是整幅图像),分别计算两幅图像的直方图,并将直方图进行归一化,然后按照某种距离度量的标准进行相似度的测量。

方法的思想:基于简单的向量相似度来对图像相似度进行度量。

优点:直方图能够很好的归一化,比如256个bin条,那么即使是不同分辨率的图像都可以直接通过其直方图来计算相似度,计算量适中。比较适合描述难以自动分割的图像。

缺点:直方图反应的是图像灰度值得概率分布,并没有图像的空间位置信息在里面,因此,常常出现误判;从信息论来讲,通过直方图转换,信息丢失量较大,因此单一的通过直方图进行匹配显得有点力不从心。

3、图像模板匹配

一般而言,源图像与模板图像patch尺寸一样的话,可以直接使用上面介绍的图像相似度测量的方法;如果源图像与模板图像尺寸不一样,通常需要进行滑动匹配窗口,扫面个整幅图像获得最好的匹配patch。

在OpenCV中对应的函数为:matchTemplate():函数功能是在输入图像中滑动窗口寻找各个位置与模板图像patch的相似度。

4、SSIM(structural similarity)结构相似性

也是一种全参考的图像质量评价指标,它分别从亮度、对比度、结构三方面度量图像相似性。

SSIM取值范围[0,1],值越大,表示图像失真越小.

在实际应用中,可以利用滑动窗将图像分块,令分块总数为N,考虑到窗口形状对分块的影响,采用高斯加权计算每一窗口的均值、方差以及协方差,然后计算对应块的结构相似度SSIM,最后将平均值作为两图像的结构相似性度量,即平均结构相似性MSSIM:

5、感知哈希算法

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

因为手机中的相似图片对比可能涉及数量庞大,需要考虑到对比的速度及准确率。综合以上的描述和技术的可行性,最终选择了感知哈希算法。该算法相对来说在对比图片的开发中实施难度不大,且速度快,准确率也满足项目要求。

图片对比实现步骤:

  1. 缩小尺寸:将图像缩小到8*8的尺寸,总共64个像素。这一步的作用是去除图像的细节,只保留结构/明暗等基本信息,摒弃不同尺寸/比例带来的图像差异;
  2. 简化色彩:将缩小后的图像,转为64级灰度,即所有像素点总共只有64种颜色;
  3. 计算平均值:计算所有64个像素的灰度平均值;
  4. 比较像素的灰度:将每个像素的灰度,与平均值进行比较,大于或等于平均值记为1,小于平均值记为0;
  5. 计算哈希值:将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图像的指纹。组合的次序并不重要,只要保证所有图像都采用同样次序就行了;
  6. 得到指纹以后,就可以对比不同的图像,看看64位中有多少位是不一样的。在理论上,这等同于”汉明距离”(Hamming distance,在信息论中,两个等长字符串之间的汉明距离是两个字符串对应位置的不同字符的个数)。如果不相同的数据位数不超过5,就说明两张图像很相似;如果大于10,就说明这是两张不同的图像。

扫描相似图片这个功能,用户可能多次扫描,而我们没有必要每次都去获取图片的灰度值,所以这里我们可以把每次扫描出来的64位灰度值保存起来,一般一张图片的灰度值是不会变的,即只要扫描一次就可以了。把数据保存起来,也可以提高下次的扫描速度。

64位灰度值保存在一个Long类型的整数里,要用的时候,再转换成二进制数。保存在Long类型里面,一方面可以节省一些内存空间,一方面也方便保存,毕竟把一个数组保存到数据库或者shareprefence都比较麻烦

C++代码:

只需要传入图片的地址即可,然后即可返回一个Long类型的数据 

 Java_com_tpv_phonemanager_utils_SimalarPhotoUtils_getPhotoArray(
    JNIEnv *env, jclass cls, jstring strSrcImageName) {
//this all code are copy form internet
char *imagPath = NULL;
jclass clsstring = env->FindClass("java/lang/String");
jstring strencode = env->NewStringUTF("utf-8");
jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray) env->CallObjectMethod(strSrcImageName, mid, strencode);
jsize alen = env->GetArrayLength(barr);
jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
if (alen > 0) {
    imagPath = (char *) malloc(alen + 1);

    memcpy(imagPath, ba, alen);
    imagPath[alen] = 0;
}
env->ReleaseByteArrayElements(barr, ba, 0);

Mat matSrc, matDst1;

matSrc = cv::imread(imagPath, CV_LOAD_IMAGE_COLOR);

int iAvg1 = 0;

int arr1[64];

if (!matSrc.data) {
    return 0j;
}

cv::resize(matSrc, matDst1, cv::Size(8, 8), 0, 0, cv::INTER_CUBIC);

cv::cvtColor(matDst1, matDst1, CV_BGR2GRAY);

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

    int tmp = i * 8;

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

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

        iAvg1 += arr1[tmp1];
    }
}

iAvg1 /= 64;

char *result;
int p = 1;
jlong value = 0;
for (int i = 0; i < 64; i++) {
    p *= 2;
    if (arr1[i] >= iAvg1) {
        value += p;
    }
}
return value;}

获取图片灰度值是一个耗时操作,若是放在主线程操作会出现无响应现象,所以把操作放在子线程里操作,java中获取灰度值,代码如下:

class GetPhotoArrayThread extends Thread {

    private File file;
    private GetPhotoArrayListen listen;

    public GetPhotoArrayThread(File file, GetPhotoArrayListen listen) {
        this.file = file;
        this.listen = listen;
    }

    @Override
    public void run() {
        super.run();
        long ints = SimalarPhotoUtils.getPhotoArray(file.getPath());

        publishProgress(file.getPath(), "0");
        PictureItem appInfo = generateBean(file, ints);
        listen.onGetPhotoArrayListen(appInfo);
    }
}

获取数据后,把数据保存到数据库,key就是图片的地址。下次获取灰度值时,先查询是否已存在数据库里,考虑用户可能会下载一些三方图片编辑软件对图片进行编辑操作,在查询数据库中是否存在该图片时,需要2个条件,一个是图片的地址,一个是图片的修改时间。

private void setPhotoArray(ArrayList<File> photoFiles, final CountDownLatch downLatch) {
    if (photoFiles.size() == 0) {
        return;
    }
    if (mExecutor == null) {
        mExecutor = new ExecutorProcessPool();
    }

    for (int i = 0; i < photoFiles.size(); i++) {
        if (isCancelled()) {
            return;
        }
        File file = photoFiles.get(i);
        publishProgress(file.getPath(), "0");
        if (mPhotoMaps.containsKey(file.getPath())) {
            PictureItem pictureItem = mPhotoMaps.get(file.getPath());
            if (pictureItem.getLastModifyTime() == file.lastModified()) {
                KLog.i("liuhua ==== data is in sql ");
                LogSaveUtils.getInstance().saveLog(pictureItem.getPath() + " is in sql ");
                pictureItem.setChecked(false);
                pictureItem.setSize(file.length());
                synchronized (downLatch) {
                    downLatch.countDown();
                }
                continue;
            }
        }
        LogSaveUtils.getInstance().saveLog(file.getPath() + " is not in sql ");
        GetPhotoArrayThread getPhotoArrayThread = new GetPhotoArrayThread(file, new GetPhotoArrayListen() {
            @Override
            public void onGetPhotoArrayListen(PictureItem pictureItem) {
                if (isCancelled()) {
                    return;
                }
                synchronized (mPhotoMaps) {
                    mPhotoMaps.put(pictureItem.getPath(), pictureItem);
                    DBManager.getInstance(App.getApp()).insertOrUpdate(pictureItem);
                    synchronized (downLatch) {
                        downLatch.countDown();
                    }
                }
            }
        });
        mExecutor.submit(getPhotoArrayThread);
    }
}


现在就是要对比每张图片的灰度值有多少位不一样。上面获取的是一个Long类型的数据,我们还需要把数据转换成二进制数,而一个Long类型转换成二进制可能会不足64位,所以此时得自己补0至64位数

 private int comparePhoto(PictureItem picture1, PictureItem picture2) {
    int iDiffNum = 0;

    StringBuffer arr1 = new StringBuffer();
    StringBuffer arr2 = new StringBuffer();

    genarateByte(picture1, arr1);
    genarateByte(picture2, arr2);

    for (int i = 0; i < 64; i++) {
        if (arr1.charAt(i) != arr2.charAt(i))
            ++iDiffNum;
    }

    return iDiffNum;
}

private void genarateByte(PictureItem info, StringBuffer arr) {
    String str = Long.toBinaryString(info.getPhotoArray());
    if (str.length() < 64) {
        int len = 64 - str.length();
        for (int i = 0; i < len; i++) {
            arr.append(0);
        }
        arr.append(str);
    } else {
        arr.append(str);
    }
}

通过返回的iDiffNum数值来判断两张图片是否相似,值越低,说明图片越相似



版权声明:本文为maetelibom原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。