目前有许多算法来衡量两幅图像的相似性,本文主要介绍在工程领域最常用的图像相似性算法评价算法:图像哈希算法(img hash)。图像哈希算法通过获取图像的哈希值并比较两幅图像的哈希值的汉明距离来衡量两幅图像是否相似。两幅图像越相似,其哈希值的汉明距离越小,通过这种方式就能够比较两幅图像是否相似。在实际应用中,图像哈希算法可以用于图片检索,重复图片剔除,以图搜图以及图片相似度比较。
为什么图像哈希算法能够评估两幅图像的相似性,这就需要从哈希值说起,哈希值计算算法的本质就是对原始数据进行有损压缩,有损压缩后的固定字长能够作为唯一标识来标识原始数据,这个唯一标识就是哈希值。通常改变原始数据任意一个部分,哈希值都将不同。关于哈希值的具体介绍见:
通俗地理解哈希函数
但是计算图像哈希值的方法并不唯一,因此有不同的图像哈希计算方法。OpenCV contrib库中的img_hash模块提供计算两种图像的哈希值并比较两张图像相似性的算法。img_hash模块主要移植自PHash库,其官方代码仓库介绍见:
Image Hashing algorithms
img_hash模块提供了多种图像哈希算法,具体介绍如下:
PHash是工程实践最常用的图像哈希算法。本文主要介绍img_hash中的几种哈希算法的使用,关于图像哈希进一步介绍见:
图片哈希概述(image hash)
本文需要OpenCV contrib库,OpenCV contrib库的编译安装见:
OpenCV_contrib库在windows下编译使用指南
本文所有代码见:
OpenCV-Practical-Exercise
图像哈希算法计算过程如下图所示,以pHash为例,先将将图片缩小到8x8的尺寸,总共64个像素,然后通过pHash函数计算hash值,得到一个唯一标识码hash码,hash码包括8个uint8数值,组合在一起,就构成了一个64位的整数。
pHash可以比较不同大小图像的hash值。通过计算两幅图像hash码的汉明距离,得到结果如下表所示:
img1 | img2 | img3 | |
---|---|---|---|
img1 | 0 | ||
img2 | 1.0 | 0 | |
img3 | 29.0 | 30.0 | 0 |
汉明距离越小,表示两幅图像越接近,可以看到img1和img2最相近,img2为img1的灰色版本。
OpenCV img_hash模块各种哈希算法的特点和文献如下所示:
本文提供img_hash模块的C++和Python代码示例。实际调用方法如下:
C++
// 创建AverageHash类
Ptr func= AverageHash::create;
// 计算图a的哈希值
func->compute(a, hashA);
// 计算图b的哈希值
func->compute(b, hashB);
// 比较两张图像哈希值的距离
func->compare(hashA, hashB);
Python
# 创建类
hashFun = cv2.img_hash.AverageHash_create()
# 计算图a的哈希值
hashA = hashFun.compute(a)
# 计算图b的哈希值
hashB = hashFun.compute(b)
# 比较两张图像哈希值的距离
hashFun.compare(hashA, hashB)
C++和Python实现都分别提供,结果如1.1所示,但是C++代码用了类模板。通过ImgHashBase基础类,能够实现代码重复使用。
代码测试的图像已经在1.1部分展示,img1为基准图像,img2为img1的灰色版本,img3是另外一张完全不同的彩色图。
C++
#include
#include
#include
using namespace cv;
using namespace cv::img_hash;
using namespace std;
template
inline void test_one(const std::string &title, const Mat &a, const Mat &b)
{
cout << "=== " << title << " ===" << endl;
TickMeter tick;
Mat hashA, hashB;
// 模板方便重复利用
Ptr func;
func = T::create();
tick.reset();
tick.start();
// 计算图a的哈希值
func->compute(a, hashA);
tick.stop();
cout << "compute1: " << tick.getTimeMilli() << " ms" << endl;
tick.reset();
tick.start();
// 计算图b的哈希值
func->compute(b, hashB);
tick.stop();
cout << "compute2: " << tick.getTimeMilli() << " ms" << endl;
// 比较两张图像哈希值的距离
cout << "compare: " << func->compare(hashA, hashB) << endl << endl;
}
int main()
{
// 打开两张图像进行相似度比较
Mat input = imread("./image/img1.jpg");
Mat target = imread("./image/img2.jpg");
// 通过不同方法比较图像相似性
test_one("AverageHash", input, target);
test_one("PHash", input, target);
test_one("MarrHildrethHash", input, target);
test_one("RadialVarianceHash", input, target);
test_one("BlockMeanHash", input, target);
test_one("ColorMomentHash", input, target);
system("pause");
return 0;
}
Python
# -*- coding: utf-8 -*-
"""
Created on Thu Aug 27 19:03:21 2020
@author: luohenyueji
"""
import cv2
def test_one(title, a, b):
# 创建类
if "AverageHash" == title:
hashFun = cv2.img_hash.AverageHash_create()
elif "PHash" == title:
hashFun = cv2.img_hash.PHash_create()
elif "MarrHildrethHash" == title:
hashFun = cv2.img_hash.MarrHildrethHash_create()
elif "RadialVarianceHash" == title:
hashFun = cv2.img_hash.RadialVarianceHash_create()
elif "BlockMeanHash" == title:
hashFun = cv2.img_hash.BlockMeanHash_create()
elif "ColorMomentHash" == title:
hashFun = cv2.img_hash.ColorMomentHash_create()
tick = cv2.TickMeter()
print("=== " + title + " ===")
tick.reset()
tick.start()
# # 计算图a的哈希值
hashA = hashFun.compute(a)
tick.stop()
print("compute1: " + str(tick.getTimeMilli()) + " ms")
tick.reset()
tick.start()
# 计算图b的哈希值
hashB = hashFun.compute(b)
tick.stop()
print("compute2: " + str(tick.getTimeMilli()) + " ms")
# 比较两张图像哈希值的距离
print("compare: " + str(hashFun.compare(hashA, hashB)))
def main():
inputImg = cv2.imread("./image/img1.jpg")
targetImg = cv2.imread("./image/img2.jpg")
if inputImg is None or targetImg is None:
print("check input image")
return
test_one("AverageHash", inputImg, targetImg)
test_one("PHash", inputImg, targetImg)
test_one("MarrHildrethHash", inputImg, targetImg)
test_one("RadialVarianceHash", inputImg, targetImg)
test_one("BlockMeanHash", inputImg, targetImg)
test_one("ColorMomentHash", inputImg, targetImg)
if __name__ == '__main__':
main()
以img1为基准,img2与img1对比结果和img3与img1对比结果如下表所示。可以看到RadialVarianceHash和ColorMomentHash结果与事实不符,这主要因为RadialVarianceHash和ColorMomentHash通过像素点颜色值信息来计算哈希值,img2是灰色图与img1相差过大。
此外可以看到各种图像哈希算法的计算速度,在实际中PHash是个很不错的选择,快速且效果好。
img1/img2 | img1/img3 | result | speed/ms | |
---|---|---|---|---|
AverageHash | 3 | 31 | TRUE | 0.0565 |
PHash | 1 | 29 | TRUE | 0.072 |
MarrHildrethHash | 28 | 283 | TRUE | 9.8433 |
RadialVarianceHash | 0.989896 | 0.543267 | FALSE | 1.0259 |
BlockMeanHash | 10 | 113 | TRUE | 0.694 |
ColorMomentHash | 45.4928 | 16.7632 | FALSE | 3.39 |
然而,PHash常用,并不代表其他算法没用。比如如果将img1水平翻转得到img4,如下图所示。那么将会得到完全不一样的结果,如下表所示。
img1/img3 | img1/img4 | result | |
---|---|---|---|
AverageHash | 31 | 36 | FALSE |
PHash | 29 | 36 | FALSE |
MarrHildrethHash | 283 | 301 | FALSE |
RadialVarianceHash | 0.543267 | 0.285715 | TRUE |
BlockMeanHash | 113 | 139 | FALSE |
ColorMomentHash | 16.7632 | 0.270448 | TRUE |
导致以上情况的主要原因是,RadialVarianceHash和ColorMomentHash基于全局信息来计算hash值,其他算法基于局部信息来计算hash值。
总之在实际应用中,通过图像哈希值计算图像相似性比较粗糙,但是图像哈希值也是比较常用的图像相似性比较算法。现在图像相似性比较算法效果都很一般,即使用了深度学习如Siamese Network,效果也没有太大提高。因此在计算相似性前,都会进行图像对准和颜色转换,这一点是非常必要的。不过图像哈希算法在实际中计算固定场景效果还是很不错的。
Image Hashing algorithms
OpenCV img_hash
通俗地理解哈希函数
图片哈希概述(image hash)
AverageHash
PHash
MarrHildrethHash
RadialVarianceHash
BlockMeanHash
ColorMomentHash