图形相似度比较(图形检索)学习笔记——pHash算法(图像感知算法)

问题来源

        有一活动,临摹油画,然后拍照上传。判断与原画的相似度,相似度越高,分数就越高。

        作为技术男,看到这个,第一反应当然是思考怎么实现这一功能啦。由于玩过opencv,所以第一反应是利用opencv的sift获取特征点集合,然后比较特征点集合,但是如何比较特征点集合一致就成了问题,一致找不到比较好的方法,后来在网上查了一下,发现比较图案的方法还不少。

        了解一下,发现最简单易实现的就是pHash算法了,所以决定写一下,作为图像相似度比较的入门学习。

 

算法思路

基本思路就是灰度化->压缩至同一尺寸(32*32)->离散变换->计算相似hash(指纹)->计算汉明距,具体逻辑如下:

获取图形矩阵(每个rgb图片的像素由三个维度指明:横坐标、纵坐标、rgb值,因此该矩阵实际是三维矩阵)

压缩至统一尺寸(减少计算量、忽略尺度变换带来的影响,插值方法使用邻域再取样方法)

计算灰度均值

遍历矩阵,大于等于灰度均值为1,小于灰度均值为0

转为对应字符串,获得hash值

计算汉明距

 

问题与思考

问:为什么要转灰度图?

忽略轻微色差带来的影响,也便于计算指纹和降低计算量。

 

问:这里的图形矩阵是什么?

对于rgb图像,每个像素由三个维度指明:横坐标、纵坐标、色值(有三个元素,分别表示rgb值),因此矩阵实际是三维矩阵,如img[0][0][]=[0xFF, 0xFF, 0xFF]表示的就是第一行第一个像素的rgb值(这里是白色),当然你也可以把rgb值直接组合成一个3字节整型数,这样他就是一个二维矩阵了。同时,灰度矩阵则是一张图的每个像素点的灰度值,对应的是一个二维矩阵,该矩阵每个元素的值就是对应坐标的像素点的灰度值。

 

问:会不会有相似的两张图片,压缩后差异反而变大的情况?

这点可能取决于压缩图片所采用的具体算法(或者说插值方式?),对于opencv就支持多种插值方式(具体可以参考文档https://www.docs.opencv.org/3.4.3/da/d54/group__imgproc__transform.html#gga5bb5a1fea74ea38e1a5445ca803ff121acf959dca2480cc694ca016b81b442ceb ),包括:最邻近插值、双线性插值、双三次插值、8x8像素邻域内的Lanczos插值。按照opencv官网介绍,最适合图像抽取的是基于像素区域关系再取样(INTER_AREA)。进过我的实际验证,一个图(4K)和它对应的缩小图(2K)用这个插值方式,缩放得到的矩阵是一样的。而别的插值方式对于同样比例的缩放图,得到的矩阵都存在不同程度的差异(差异会随着程序运算被放大,而指定生成的指纹长度不同,也会导致这个差异放大程度不同)。

 

问:为什么要经过离散余弦变换(dct)?

离散余弦变换在这里其实不做也可以,能正常进行比对,实际上,网上很多人的代码也忽略了这一步。离散余弦变换的直接结果是对图片进行有损压缩,经过离散余弦变换的图片,我们可以将其很容易的恢复原图。做离散余弦变换的目的,按我理解是为了忽略噪点对图片的影响(但是为啥不直接用滤波器?)关于这一问题,数学或者图形学好的童鞋可能比我更清楚,我就不献丑了。也请懂的童鞋留言解惑。

 

优缺点

优点:

  1. 压缩后计算图片指纹再使用汉明距或编辑距计算相似度,快速易实现
  2. 能较好的实现尺度不变性
  3. 由于使用灰度图,且指纹使用的是灰度值与均值比较得到的,因此能容忍一定的色差

缺点:

  1. 不能智能排除不需要比较的部分
  2. 不能实现旋转不变性
  3. 不能比较准确的结合颜色比较相似度

补充:

上述缺点可以结合opencv的特征采集方法弥补,但是会导致性能急剧降低。

 

代码

import cv2
import numpy


# pHash算法 快速简单易实现 缺点是无法支持旋转不变性
def p_hash(src):
    hash_len = 32

    # 灰度
    gray_img = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

    # 压缩
    # resize_gray_img = cv2.resize(gray_img, (hash_len, hash_len))
    # resize_gray_img = cv2.resize(gray_img, (hash_len, hash_len), interpolation=cv2.INTER_LANCZOS4)
    resize_gray_img = cv2.resize(gray_img, (hash_len, hash_len), interpolation=cv2.INTER_AREA)  # 似乎最准确
    # resize_gray_img = cv2.resize(gray_img, (hash_len, hash_len), interpolation=cv2.INTER_LINEAR)
    # resize_gray_img = cv2.resize(gray_img, (hash_len, hash_len), interpolation=cv2.INTER_NEAREST)
    # resize_gray_img = cv2.resize(gray_img, (hash_len, hash_len), interpolation=cv2.INTER_CUBIC)

    # 图像内的整型转为浮点数便于dct
    h, w = resize_gray_img.shape[:2]  # 获取图像宽高
    vis0 = numpy.zeros((h, w), numpy.float32)  # 初始化
    # vis0 = numpy.zeros((h, w), numpy.int8)  # 初始化
    vis0[:h, :w] = resize_gray_img  # 填充数据

    # 离散余弦变换
    vis1 = cv2.dct(cv2.dct(vis0))
    vis1.resize(hash_len, hash_len)
    img_list = vis1.flatten()
    # img_list = vis0.flatten()

    # 计算均值
    avg = sum(img_list) * 1. / len(img_list)
    avg_list = []
    for i in img_list:
        if i < avg:
            tmp = '0'
        else:
            tmp = '1'
        avg_list.append(tmp)

    # 计算哈希值
    p_hash_str = ''
    for x in range(0, hash_len * hash_len, 4):
        p_hash_str += '%x' % int(''.join(avg_list[x:x + 4]), 2)
    return p_hash_str


def ham_dist(x, y):
    # return bin(x ^ y).count('1')
    assert len(x) == len(y)
    return sum([ch1 != ch2 for ch1, ch2 in zip(x, y)])


if __name__ == '__main__':
    # print('running...')

    # 读取图像
    img_src = cv2.imread('test_src.jpeg')
    img_small = cv2.imread('test_small.jpeg')
    img_tiny = cv2.imread('test_tiny.jpeg')
    img_diff = cv2.imread('test_diff.jpeg')
    img_rotate = cv2.imread('test_rotate.jpeg')
    hash_src = p_hash(img_src)
    hash_small = p_hash(img_small)
    hash_tiny = p_hash(img_tiny)
    hash_diff = p_hash(img_diff)
    hash_rotate = p_hash(img_rotate)
    print('src:\t%s' % hash_src)
    print('small:\t%s' % hash_small)
    print('tiny:\t%s' % hash_tiny)
    print('diff:\t%s' % hash_diff)
    print('ham-src-small:\t%s' % ham_dist(hash_src, hash_small))  # 一般缩放能保证汉明距小于10
    print('ham-src-tiny:\t%s' % ham_dist(hash_src, hash_tiny))
    print('ham-src-diff:\t%s' % ham_dist(hash_src, hash_diff))  # 完全不同的图像 汉明距大于10
    print('ham-src-rotate:\t%s' % ham_dist(hash_src, hash_rotate))  # 无法识别旋转情况

 这里给出我的运行结果,距离越短,表示两张图片越相似

 

总结

        算法比较有很多方式:基于相似Hash计算汉明距或编辑距、基于图像特征SIFT或者SURF的特征相似度比较、基于深度学习的相似性检索。。。但实际选择哪中,需要考虑到业务需求(比如响应时间和)、开发难度、开发成本等多个方面考虑(Ps,深度学习的不太懂,但是如果是基于图像特征的话,我只知道opencv的sift和surf真的很慢,不排除是我缺少优化的原因)。pHash算法作为一个入门,使我对图像相似度比较和图像检索的基本原理有了基本的理解,更复杂的方法需要后续进一步学习和实践。

        显而易见的,这一方法适合用于对图像进行快速索引,适合用于图像检索领域或简单的图像比较,而若我们希望的是更加准确的相似度比较,基于深度学习或基于特征描述的相似度比较显然更合适。

        使用汉明距比较与使用编辑距比较的差异还未具体总结,有兴趣的同学可以自行尝试,或等后续我的进一步学习总结。

        这篇文章不长,使用的算法也较为简单。写完之后,我自个儿也产生了许多新的以后,后续如果有新的收获,也会拿出来分享,如果有大神之前玩过这东西,还请下方留言点名错漏,感激不尽。

你可能感兴趣的:(phash,图像比较,汉明距,编辑距,python,算法赛题,python)