以图片识别匹配的案例来分析特征值检测与匹配方法。
目录
一.感知哈希算法(Perceptual Hash Algorithm)
二.特征值检测步骤
1.减小尺寸
2.简化色彩
3.计算像素点均值
4.构造感知哈希位信息
5.构造一维感知哈希值
三.实现程序
1.感知哈希值计算函数
2.计算距离函数
3.计算图像库内所有图像的哈希值
4.比较距离,输出结果
5.完整程序
一.感知哈希算法(Perceptual Hash Algorithm)
哈希值是数据的指纹,是数据的独一无二的特征值。任何微小的差异都会导致两个数据的哈希值完全不同。与传统哈希值的不同之处在于,感知哈希值可以对不同数据对应的哈希值进行比较,进而可以判断两个数据之间的相似性。也就是说,借助感知哈希能够实现对数据的比较。
一般来说,相似的图像即使在尺度、纵横比不同及颜色(对比度、亮度等)存在微小差异的情况下,仍然会具有相似的感知哈希值。这个属性为使用感知哈希值进行图像检索提供了理论基础。
流程图
由图可知,基本流程是先提取所有图像的特征值(感知哈希值),然后比较检索图像和图像库中所有图像的特征值,和检索图像差值最小的图像库中的图像就是检索结果。针对图中的利用检索图像寻找相似图像,图像库第2行第1幅图像的特征值489与检索图像的特征值462的差为27,是所有的差值中最小的,因此该图像就是检索结果。
从上述分析可以看出,检索的关键点在于找到特征值,并计算距离。这涉及的图像处理领域中的三个关键问题:
(1)提取哪些特征:图像有很多特征,要找到有用的特征,这是关键一步。
(2)如何量化特征:简单来说就是用数字来表示特征,让特征变为可计算的。
(3)如何计算距离:有很多种不同的计算距离的方式,从中选择一种合适的即可,本章将使用汉明距离来衡量距离。
二.特征值检测步骤
1.减小尺寸
将图像减小至8像素x8像素大小,总计64像素。减小尺寸的作用在于去除图像内的高频和细节信息,仅保留图像中最重要的结构、亮度等信息。尺寸减小后的图像看起来是非常模糊的。该步骤不需要考虑纵横比等问题,无论原始图像如何,一律处理为8像素x8像素大小。这样的处理,能够让算法适用于各种尺度的图像。
需要注意的是,该方法虽然简单,但是在学习深度学习方法后会发现,该方法与深度学习方法提取图像特征的基本思路是一致的。当然,本章的特征提取相对比较粗糙,而深度学习方法提取的特征更具有抽象性。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
int main() {
cv::Mat img = cv::imread("lena.bmp");
if (img.empty())
{
cerr<<"error"<<endl;
return -1;
}
cv::Size size(8, 8);
cv::Mat rst;
cv::resize(img, rst, size);
std::cout << "img.shape = (" << img.rows << ", " << img.cols << ", " << img.channels() << ")" << std::endl;
std::cout << "rst.shape = (" << rst.rows << ", " << rst.cols << ", " << rst.channels() << ")" << std::endl;
cv::imshow("img", img);
cv::imshow("rst", rst);
cv::waitKey();
cv::destroyAllWindows();
return 0;
}
2.简化色彩
色彩空间转换到灰度空间
cv::Mat gray;
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
3.计算像素点均值
计算图像中64个像素点的均值M。
double m = cv::mean(img)[0];
对于灰度图像,只需使用 [0]
即可。
4.构造感知哈希位信息
依次将每个像素点的像素值与均值M进行比较。像素值大于或等于均值M的像素点记为1;否则,记为0。
cv::Mat r = img > m;
// 打印特征值矩阵
std::cout << "特征值:\n" << r << std::endl;
注意,在python中可以使用astype(int)将逻辑值转换为整数值,
C++中的 cv::Mat
没有直接的 astype(int)
方法,打印布尔矩阵时会显示 0
和 255
。如果需要将布尔矩阵转换为整数矩阵(0 和 1),可以使用以下代码:
cv::Mat r_int;
r.convertTo(r_int, CV_8U, 1.0 / 255.0);
std::cout << "特征值:\n" << r_int << std::endl;
5.构造一维感知哈希值
将上一步得到的64个1或0组合在一起,得到当前图像的一维感知哈希值表。
cv::Mat r1 = r_int.reshape(1, 1);
需要说明的是,64个像素值的组合顺序并不重要,但需针对所有图像采用相同的顺序组合。通常情况采用从左到右从上到下的顺序拼接所有特征值。经过上述处理得到的感知哈希值可以看作图像的指纹。在图像被缩放或纵横比发生变化时,它的感知哈希值不会改变。通常情况下,调整图像的亮度或对比度,甚至改变图像的颜色都不会显著改变感知哈希值。重要的是,这种提取感知哈希值的方法的速度非常快。
综上所述,感知哈希值提取过程示意图如图所示:
按照上述方式分别提取检索图像和图像库内图像的感知哈希值。
依次将检索图像的感知哈希值与图像库内图像的感知哈希值进行比较,距离最小的图像就是检索结果。
比较时,可以采用汉明距离来衡量不同图像之间的距离。具体为,将两幅图像的感知哈希值不同的位个数作为二者的距离。距离为0,表示二者可能是非常相似的图像(或同一幅图像的变体);距离较小,表示二者之间可能有一些不同,但它们十分相似;距离较大,表示二者的差距较大;距离非常大,表示二者可能是完全不同的两幅图像。
三.实现程序
1.感知哈希值计算函数
为了方便,将上述特征值检测步骤封装成函数:注意返回类型为cv::Mat
cv::Mat getHash(cv::Mat& img) {
// 缩放图像到 8x8
cv::Size size(8, 8);
cv::Mat rst;
cv::resize(img, rst, size);
cout << "Resized image:\n" << rst << endl;
// 转换为灰度图像
cv::Mat gray;
cv::cvtColor(rst, gray, cv::COLOR_BGR2GRAY);
cout << "Grayscale image:\n" << gray << endl;
// 计算图像的平均值
cv::Scalar mean_value = cv::mean(gray);
double m = mean_value[0];
cout << "Mean value: " << m << endl;
// 进行比较,并转换为整数矩阵(0和1)
cv::Mat r = (gray > m);
cv::Mat r_int;
r.convertTo(r_int, CV_8U, 1.0 / 255.0);
cout << "Binary image:\n" << r_int << endl;
// 转换为一维数组
cv::Mat r1 = r_int.reshape(1, 1); // 展平为1行
return r1;
}
2.计算距离函数
比较感知哈希值时,可以采用汉明距离来衡量二者的距离,具体为将两个感知哈希值位值不同的位个数作为二者的距离。假设感知哈希值test为“1011”:
感知哈希值x1值为“1010”,test1与x1只有第4位上的值不同,因此二者的距离为1。
可以借助OpenCV中的按位异或运算函数cv::bitwise xor()来计算两个感知哈希值之间的距离在进行按位异或运算时,若运算数相同则返回0,若运算数不同则返回1。
按位异或运算是将两个数值按二进制位逐位进行异或运算。
将上述运算结果逐位相加求和,得到的值就是两个感知哈希值的汉明距离。
例如:cv::bitwise xor(“1011”,“1010”,r)的返回值为“0001”,将“0001”逐位相加求和“0+0+0+1=1”。因此,“1011”和“1010”的距离为1。
int hanming(cv::Mat& h1, cv::Mat& h2) {
cv::Mat r;
cv::bitwise_xor(h1, h2, r);
int h = cv::sum(r)[0];
return h;
}
3.计算图像库内所有图像的哈希值
为了高效读取文件,利用cv::glob函数设计findImages函数来实现读取指定文件夹下所有图像文件。
void findImages(vector<string>& images, const vector<string>& exts, const string& folder) {
for (const auto& ext : exts) {
vector<cv::String> files;
glob(folder + "/*." + ext, files, false);
images.insert(images.end(), files.begin(), files.end());
}
}
在主函数中调用并计算其哈希值:
// 计算检索图像的哈希值
cv::Mat h = getHash(img);
cout << "检索图像的感知哈希值为:\n" << h << endl;
// 获取图像文件列表
vector<string> images;
vector<string> exts = { "jpg", "jpeg", "JPG", "JPEG", "gif", "GIF", "png", "PNG",
"bmp", "BMP" };
findImages(images, exts, "image");
vector<pair<string, cv::Mat>> seq;
// 读取图像并计算哈希值
for (const auto& f : images) {
cv::Mat I = cv::imread(f, cv::IMREAD_COLOR);
if (I.empty()) {
cerr << "无法读取图像文件: " << f << endl;
continue;
}
seq.push_back(make_pair(f, getHash(I)));
}
4.比较距离,输出结果
// 计算检索图像与图像库内所有图像距离,将最小距离作为检索结果
vector<pair<int, string>> distance;
for (const auto& x : seq) {
distance.push_back(make_pair(hamming(h, x.second), x.first)); // 每次添加(距离值,图像名称)
}
// 排序,把距离最小的放在最前面
sort(distance.begin(), distance.end());
// 打印最相似的三个图像
cout << "最相似的图像:\n";
for (int i = 0; i < 3 && i < distance.size(); ++i) {
cout << distance[i].second << " with distance: " << distance[i].first << endl;
}
// 显示最相似的三个图像
cv::Mat r1 = cv::imread(distance[0].second);
cv::Mat r2 = cv::imread(distance[1].second);
cv::Mat r3 = cv::imread(distance[2].second);
// 使用 OpenCV 显示结果图像
cv::imshow("Original", img);
cv::imshow("Most similar 1", r1);
cv::imshow("Most similar 2", r2);
cv::imshow("Most similar 3", r3);
cv::waitKey(0);
cv::destroyAllWindows();
5.完整程序
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
using namespace cv;
// 提取感知哈希值函数
cv::Mat getHash(cv::Mat& img) {
// 缩放图像到 8x8
cv::Size size(8, 8);
cv::Mat rst;
cv::resize(img, rst, size);
// 转换为灰度图像
cv::Mat gray;
cv::cvtColor(rst, gray, cv::COLOR_BGR2GRAY);
// 计算图像的平均值
cv::Scalar mean_value = cv::mean(gray);
double m = mean_value[0];
// 进行比较,并转换为整数矩阵(0和1)
cv::Mat r = (gray > m);
cv::Mat r_int;
r.convertTo(r_int, CV_8U, 1.0 / 255.0);
// 转换为一维数组
cv::Mat r1 = r_int.reshape(1, 1); // 展平为1行
return r1;
}
// 计算汉明距离函数
int hamming(const cv::Mat& h1, const cv::Mat& h2) {
cv::Mat r;
cv::bitwise_xor(h1, h2, r);
int h = cv::sum(r)[0];
return h;
}
// 查找指定文件夹中的图像文件
void findImages(vector<string>& images, const vector<string>& exts, const string& folder) {
for (const auto& ext : exts) {
vector<cv::String> files;
glob(folder + "/*." + ext, files, false);
images.insert(images.end(), files.begin(), files.end());
}
}
int main() {
// 读取检索图像
cv::Mat img = cv::imread("apple.jpg");
if (img.empty()) {
std::cerr << "无法读取图像文件: apple.jpg" << std::endl;
return -1;
}
// 计算检索图像的哈希值
cv::Mat h = getHash(img);
cout << "检索图像的感知哈希值为:\n" << h << endl;
// 获取图像文件列表
vector<string> images;
vector<string> exts = { "jpg", "jpeg", "JPG", "JPEG", "gif", "GIF", "png", "PNG", "bmp", "BMP" };
findImages(images, exts, "image");
vector<pair<string, cv::Mat>> seq;
// 读取图像并计算哈希值
for (const auto& f : images) {
cv::Mat I = cv::imread(f, cv::IMREAD_COLOR);
if (I.empty()) {
cerr << "无法读取图像文件: " << f << endl;
continue;
}
seq.push_back(make_pair(f, getHash(I)));
}
// 计算检索图像与图像库内所有图像距离,将最小距离作为检索结果
vector<pair<int, string>> distance;
for (const auto& x : seq) {
distance.push_back(make_pair(hamming(h, x.second), x.first)); // 每次添加(距离值,图像名称)
}
// 排序,把距离最小的放在最前面
sort(distance.begin(), distance.end());
// 打印最相似的三个图像
cout << "最相似的图像:\n";
for (int i = 0; i < 3 && i < distance.size(); ++i) {
cout << distance[i].second << " with distance: " << distance[i].first << endl;
}
// 显示最相似的三个图像
cv::Mat r1 = cv::imread(distance[0].second);
cv::Mat r2 = cv::imread(distance[1].second);
cv::Mat r3 = cv::imread(distance[2].second);
// 使用 OpenCV 显示结果图像
cv::imshow("Original", img);
cv::imshow("Most similar 1", r1);
cv::imshow("Most similar 2", r2);
cv::imshow("Most similar 3", r3);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}