问题来源
有一活动,临摹油画,然后拍照上传。判断与原画的相似度,相似度越高,分数就越高。
作为技术男,看到这个,第一反应当然是思考怎么实现这一功能啦。由于玩过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)?
离散余弦变换在这里其实不做也可以,能正常进行比对,实际上,网上很多人的代码也忽略了这一步。离散余弦变换的直接结果是对图片进行有损压缩,经过离散余弦变换的图片,我们可以将其很容易的恢复原图。做离散余弦变换的目的,按我理解是为了忽略噪点对图片的影响(但是为啥不直接用滤波器?)关于这一问题,数学或者图形学好的童鞋可能比我更清楚,我就不献丑了。也请懂的童鞋留言解惑。
优缺点
优点:
缺点:
补充:
上述缺点可以结合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算法作为一个入门,使我对图像相似度比较和图像检索的基本原理有了基本的理解,更复杂的方法需要后续进一步学习和实践。
显而易见的,这一方法适合用于对图像进行快速索引,适合用于图像检索领域或简单的图像比较,而若我们希望的是更加准确的相似度比较,基于深度学习或基于特征描述的相似度比较显然更合适。
使用汉明距比较与使用编辑距比较的差异还未具体总结,有兴趣的同学可以自行尝试,或等后续我的进一步学习总结。
这篇文章不长,使用的算法也较为简单。写完之后,我自个儿也产生了许多新的以后,后续如果有新的收获,也会拿出来分享,如果有大神之前玩过这东西,还请下方留言点名错漏,感激不尽。