最近接触到图像去重算法,有phash、dhash和ahash等基于哈希方法的去重算法。
phash全称是感知哈希算法(Perceptual hash algorithm),使用这玩意儿可以对每个图片生成一个值,然后计算他们的hamming distance,简单的说就是数一数二进制之后有几位不同。整个处理流程有点像对文章去重时先算simhash再算hamming distance,很多东西都可以直接套用过来。
首先介绍ahash算法(全称average hash)是phash算法的一种,是基于图像内容搜索最简单的,因此也有很多的局限性,主要用于图像的缩略图搜原图,对于图像的旋转、平移、对比度和微变形等都无能为力,所以很局限。
其整个算法的步骤:
-
图片缩放—将图片缩放到8*8大小(这一步的作用是去除图片的细节,只保留结构、明暗等基本信息,摒弃不同尺寸、比例带来的图片差异)
-
灰度化—将缩小后的图片,转为64级灰度,也就是说所有的像素点总共有64种颜色
-
计算均值—计算所有像素的灰度平均值
-
得到88图像的ahash—88的像素值中大于均值的则用1表示,小于的用0表示,这样就得到一个64位二进制码最为该图像的ahash值
-
计算两幅图像ahash值的汉明距离,距离越小,表明两幅图像越相似;距离越大,表明两幅图像距离越大(将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了)
得到指纹以后,就可以对比不同的图片,看看64位中有多少位是不一样的。在理论上,这等同于计算"汉明距离"(Hamming distance)。如果不相同的数据位不超过5,就说明两张图片很相似;如果大于10,就说明这是两张不同的图片。这种算法的优点是简单快速,不受图片大小缩放的影响,缺点是图片的内容不能变更。如果在图片上加几个文字,它就认不出来了。所以,它的最佳用途是根据缩略图,找出原图。实际应用中,往往采用更强大的pHash算法和SIFT算法,它们能够识别图片的变形。只要变形程度不超过25%,它们就能匹配原图。这些算法虽然更复杂,但是原理与上面的简便算法是一样的,就是先将图片转化成Hash字符串,然后再进行比较。 具体代码可以参见Wote用python写的代码:
import glob
import os
import sys
from PIL import Image
EXTS = 'jpg', 'jpeg', 'JPG', 'JPEG', 'gif', 'GIF', 'png', 'PNG'
def avhash(im):
if not isinstance(im, Image.Image):
im = Image.open(im)
im = im.resize((8, 8), Image.ANTIALIAS).convert('L')
avg = reduce(lambda x, y: x + y, im.getdata()) / 64.
return reduce(lambda x, (y, z): x | (z << y),
enumerate(map(lambda i: 0 if i < avg else 1, im.getdata())),
0)
def hamming(h1, h2):
h, d = 0, h1 ^ h2
while d:
h += 1
d &= d - 1
return h
if __name__ == '__main__':
if len(sys.argv) <= 1 or len(sys.argv) > 3:
print "Usage: %s image.jpg [dir]" % sys.argv[0]
else:
im, wd = sys.argv[1], '.' if len(sys.argv) < 3 else sys.argv[2]
h = avhash(im)
os.chdir(wd)
images = []
for ext in EXTS:
images.extend(glob.glob('*.%s' % ext))
seq = []
prog = int(len(images) > 50 and sys.stdout.isatty())
for f in images:
seq.append((f, hamming(avhash(f), h)))
if prog:
perc = 100. * prog / len(images)
x = int(2 * perc / 5)
print '\rCalculating... [' + '#' * x + ' ' * (40 - x) + ']',
print '%.2f%%' % perc, '(%d/%d)' % (prog, len(images)),
sys.stdout.flush()
prog += 1
if prog: print
for f, ham in sorted(seq, key=lambda i: i[1]):
print "%d\t%s" % (ham, f)
下面的是根据opencv用c++将其实现
- 图片缩放+灰度化
直接调用opencv的函数就可以了
Mat img = imread("C:\\Users\\zs\\Desktop\\1.jpg", 1);
if(!img.data){
cout << "the image is not exist" << endl;
return 0;
}
int size = 8; // 图片缩放后大小
resize(img, img, Size(size,size)); // 缩放到8*8
cvtColor(img, img, COLOR_BGR2GRAY); // 灰度化
- 计算灰度化的均值
// 计算8*8图像的平均灰度
float calcAverage(Mat_<uchar> image, const int &size){
float sum = 0;
for(int i = 0 ; i < size; i++){
for(int j = 0; j < size; j++){
sum += image(i, j);
}
}
return sum/(size*size);
}
- 得到8*8图像的ahash
/* 计算hash值
image:8*8的灰度图像
size: 图像大小 8*8
ahahs:存放64位hash值
averagePix: 灰度值的平均值
*/
void fingerPrint(Mat_<uchar> image, const int &size, bitset<hashLength> &ahash, const float &averagePix){
for(int i = 0; i < size; i++){
int pos = i * size;
for(int j = 0; j < size; j++){
ahash[pos+j] = image(i, j) >= averagePix ? 1:0;
}
}
}
- 计算汉明距离
/*计算汉明距离*/
int hammingDistance(const bitset<hashLength> &query, const bitset<hashLength> &target){
int distance = 0;
for(int i = 0; i < hashLength; i++){
distance += (query[i] == target[i] ? 0 : 1);
}
return distance;
}
完整的代码:
#include <iostream>
#include <bitset>
#include <string>
#include <iomanip>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\core\core.hpp>
using namespace std;
using namespace cv;
#define hashLength 64
// 计算8*8图像的平均灰度
float calcAverage(Mat_<uchar> image, const int &size) {
float sum = 0;
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
sum += image(i, j);
}
}
return sum / (size*size);
}
/* 计算hash值
image:8*8的灰度图像
size: 图像大小 8*8
ahahs:存放64位hash值
averagePix: 灰度值的平均值
*/
void fingerPrint(Mat_<uchar> image, const int &size, bitset<hashLength> &ahash, const float &averagePix) {
for (int i = 0; i < size; i++) {
int pos = i * size;
for (int j = 0; j < size; j++) {
ahash[pos + j] = image(i, j) >= averagePix ? 1 : 0;
}
}
}
/*计算汉明距离*/
int hammingDistance(const bitset<hashLength> &query, const bitset<hashLength> &target) {
int distance = 0;
for (int i = 0; i < hashLength; i++) {
distance += (query[i] == target[i] ? 0 : 1);
}
return distance;
}
string bitTohex(const bitset<hashLength> &target) {
string str;
for (int i = 0; i < hashLength; i = i + 4) {
int sum = 0;
string s;
sum += target[i] + (target[i + 1] << 1) + (target[i + 2] << 2) + (target[i + 3] << 3);
stringstream ss;
ss << hex << sum; // 以十六进制保存
ss >> s;
str += s;
}
return str;
}
int main() {
Mat img = imread("C:\\Users\\zs\\Desktop\\1.jpg", 1);
if (!img.data) {
cout << "the image is not exist" << endl;
return 0;
}
int size = 8; // 图片缩放后大小
resize(img, img, Size(size, size)); // 缩放到8*8
cvtColor(img, img, COLOR_BGR2GRAY); // 灰度化
float averagePix = calcAverage(img, size); // 计算灰度化的均值
//cout << averagePix << endl;
bitset<hashLength> ahash;
fingerPrint(img, size, ahash, averagePix); // 得到均值hash
//cout << ahash << endl;
cout << bitTohex(ahash) << endl;
//string img_dir = "E:\\algorithmZack\\ImageSearch\\image\\";
for (int i = 1; i <= 8; i++) {
Mat target = imread("C:\\Users\\zs\\Desktop\\2.jpg", 1);
if (!target.data) {
cout << "the target image is not exist" << endl;
continue;
}
resize(target, target, Size(size, size));
cvtColor(target, target, COLOR_BGR2GRAY);
float averagePix2 = calcAverage(target, size);
bitset<hashLength> ahash2;
fingerPrint(target, size, ahash2, averagePix2);
//cout << averagePix2 << endl;
int distance = hammingDistance(ahash, ahash2); // 计算汉明距离
cout << "【" << i << "-" << distance << "】 ";
}
cout << endl;
return 0;
}