最近公司要求做一个类似腾讯手机管家的应用,其中有一个功能需要把用户手机中的相似图片查找出来,供用户选择是否删除。
完全毫无头绪,就上网查找有哪些技术可以做的,找来找去,都在说OpenCV,于是乎就看看OpenCV要如何实现
几种方式中,觉得还是感知哈希算法符合项目的要求,这个算法实现也不难,只要遵守如下规则就好
感知哈希算法(perceptual hash algorithm),它的作用是对每张图像生成一个“指纹”(fingerprint)字符串,然后比较不同图像的指纹。结果越接近,就说明图像越相似。
实现步骤:
- 缩小尺寸:将图像缩小到8*8的尺寸,总共64个像素。这一步的作用是去除图像的细节,只保留结构/明暗等基本信息,摒弃不同尺寸/比例带来的图像差异;
- 简化色彩:将缩小后的图像,转为64级灰度,即所有像素点总共只有64种颜色;
- 计算平均值:计算所有64个像素的灰度平均值;
- 比较像素的灰度:将每个像素的灰度,与平均值进行比较,大于或等于平均值记为1,小于平均值记为0;
- 计算哈希值:将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图像的指纹。组合的次序并不重要,只要保证所有图像都采用同样次序就行了;
- 得到指纹以后,就可以对比不同的图像,看看64位中有多少位是不一样的。在理论上,这等同于”汉明距离”(Hamming distance,在信息论中,两个等长字符串之间的汉明距离是两个字符串对应位置的不同字符的个数)。如果不相同的数据位数不超过5,就说明两张图像很相似;如果大于10,就说明这是两张不同的图像。
以上内容摘自:http://www.ruanyifeng.com/blog/2011/07/principle_of_similar_image_search.html
到此,我们知道如何对比图片的相似度
扫描相似图片这个功能,用户可能多次扫描,而我们没有必要每次都去获取图片的灰度值,所以这里我们可以把每次扫描出来的64位灰度值保存起来,一般一张图片的灰度值是不会变的,即只要扫描一次就可以了。把数据保存起来,也可以提高下次的扫描速度。
64位灰度值保存在一个Long类型的整数里,要用的时候,再转换成二进制数。保存在Long类型里面,一方面可以节省一些内存空间,一方面也方便保存,毕竟把一个数组保存到数据库或者shareprefence都比较麻烦
C++代码:
只需要传入图片的地址即可,然后即可返回一个Long类型的数据
include "SimalarPhotoUtils.h"
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/core/core.hpp>
#include <android/log.h>
#define LOG_TAG "JNI"
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
using namespace cv;
JNIEXPORT jlong
JNICALL 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;
}
每张图片的灰度值,我们已经知道如何获取了。现在就是要对比每张图片的灰度值有多少位不一样。上面获取的是一个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);
}
}
能力有限,这还是有很多不足之处,第一次写博客,写得有些乱乱的,请大家多多指教。