在关键点检测一节中,我们学习了如何检测图像中的关键点,其目的是用于执行局部图像分析。这些关键点需要足够独特,以便在具有相同对象的不同图像中能够检测到相同的点。
基于关键点执行图像分析需要构建丰富的表示来唯一地描述这些关键点,本节将重点介绍如何从关键点中提取描述符,描述符通常是描述关键点及其邻域的二进制、整数或浮点数的一维或二维向量。一个好的描述符应该足够独特,以便唯一地表示图像中的每个关键点;同时其应该具有足够的鲁棒性,以应对可能的光照或视点变化,能够以相似的方式表示相同的点。并且描述符的结构也应该是紧凑的,以便进行操作。
关键点最常见的用途之一是图像匹配,例如,执行图像匹配用于关联同一场景中的两张不同图像或检测图像中的目标对象。在本节中,我们将学习基本的匹配算法。
特征点匹配操作可以将一个图像中的点与另一个图像(或图像集)中的点进行对应,当图像点对应于现实世界中相同场景元素(或对象点)时,表示图像点相互匹配。
仅仅单个像素并不足以决定两个关键点的相似性,因此必须在匹配过程中考虑每个关键点周围的图像块。如果两个图像块对应于相同的场景元素,那么它们的像素将具有相似的值。最简单的特征点匹配方法是像素块的直接逐像素比较,虽然在某些情况下,它可以取得良好的结果,但这并不是最可靠的方法。
大多数情况下,图像块定义为以关键点位置为中心的正方形(边长为奇数),可以通过比较图像块内相应的像素强度值来测量两个正方形图像块之间的相似性,一种流行的解决方案是使用平方差和 (Sum of Squared
, SSD
)。特征匹配策略首先需要在每个图像中检测关键点,本节中,我们使用 FAST
检测器检测图像关键点。
(1) 定义两个关键点向量来存储每个图像检测到的关键点:
std::vector<cv::KeyPoint> keypoints1;
std::vector<cv::KeyPoint> keypoints2;
(2) 创建阈值为 80
的 FAST
检测器:
cv::Ptr<cv::FeatureDetector> ptrDetector;
ptrDetector = cv::FastFeatureDetector::create(80);
(3) 应用创建的 FAST
检测器检测每个图像中的所有关键点:
ptrDetector->detect(image1, keypoints1);
ptrDetector->detect(image2, keypoints2);
(4) 定义一个大小为 11x11
的矩形,用于定义每个关键点周围的图像块:
const int nsize(11);
cv::Rect neighborhood(0, 0, nsize, nsize);
cv::Mat patch1;
cv::Mat patch2;
(5) 将一幅图像中的关键点与另一幅图像中的关键点进行比较。针对第一幅图像的每个关键点,识别出第二幅图像中最相似的图像块,以上过程可以使用两个嵌套循环实现:
cv::Mat result;
std::vector<cv::DMatch> matches;
for (int i=0; i<keypoints1.size(); i++) {
// 定义图像块
neighborhood.x = keypoints1[i].pt.x-nsize/2;
neighborhood.y = keypoints1[i].pt.y-nsize/2;
if (neighborhood.x<0 || neighborhood.y<0 || neighborhood.x+nsize>=image1.cols || neighborhood.y+nsize>=image1.rows) {
continue;
}
// image1 中图像块
patch1 = image1(neighborhood);
// 重置最佳相关性值
cv::DMatch bestMatch;
for (int j=0; j<keypoints2.size(); j++) {
// 定义图像块
neighborhood.x = keypoints2[j].pt.x-nsize/2;
neighborhood.y = keypoints2[j].pt.y-nsize/2;
if (neighborhood.x<0 || neighborhood.y<0 || neighborhood.x+nsize>=image2.cols || neighborhood.y+nsize>=image2.rows) {
continue;
}
// image2 中图像块
patch2 = image2(neighborhood);
// 匹配两个图像块
cv::matchTemplate(patch1, patch2, result, cv::TM_SQDIFF);
// 检查是否是最佳匹配
if (result.at<float>(0, 0) < bestMatch.distance) {
bestMatch.distance = result.at<float>(0, 0);
bestMatch.queryIdx = i;
bestMatch.trainIdx = j;
}
}
// 添加最佳匹配
matches.push_back(bestMatch);
}
在以上代码中,cv::matchTemplate
函数用于计算图像块的相似性得分。当识别出一个潜在的匹配时,可以通过使用 cv::DMatch
对象来表示这个匹配,该对象可以存储两个匹配关键点的索引以及相似度得分。 两个图像块越相似,它们对应于相同场景点的概率就越高,因此,我们可以按相似度分数对匹配点的结果进行排序。
(6) 要对相似度分数排序,可以使用 std
库中的 nth_element
排序算法:
std::nth_element(matches.begin(), matches.begin()+50, matches.end());
matches.erase(matches.begin()+50, matches.end());
(6) 通过给定相似度阈值保留的最大的若干匹配项。我们选择只保留 N
个最佳匹配点,本节,我们使用 N=25
来使匹配结果的可视化更清晰。OpenCV
中可以使用函数 cv::drawMatches
将两个图像中的每对对应点用一条线连接起来显示匹配结果:
cv::Mat matchImage;
cv::drawMatches(image1, keypoints1, // 第一张图像
image2, keypoints2, // 第二张图像
matches, // 匹配向量
matchImage, // 生成的图像
cv::Scalar(255, 255, 255), // 线颜色
cv::Scalar(255, 255, 255)); // 点颜色
绘制匹配项时,我们希望限制匹配数以使结果更具可读性,因此,我们使用 std::nth_element
函数只显示距离最小的 25
个匹配项。std::nth_element
函数将第 N
个元素按排序顺序放置在第 N
个位置(所有较小的元素置于此元素之前),完成此操作后,清除向量中剩余的元素。
匹配结果如下图所示:
以上获得的结果并不完美,但可以看到上图中显示出了许多成功的匹配。还可以观察到,皮肤的相似纹理结构造成了一些混乱匹配。此外,由于我们尝试将左图像中的所有关键点与右图像中的点进行匹配,因此右图像中的点可能会与左图像中的多个关键点匹配,这称为不对称匹配,可以通过仅保留图像中每个关键点的最佳分数的匹配来纠正这种不对称匹配。
cv::matchTemplate
函数是以上特征匹配方法的核心,本节中,我们将其用于比较两个图像块,但此函数还具有更加通用的用途。
为了比较每个图像的图像块,我们使用 TM_SQDIFF
标志指定逐像素平方差之和作为比较标准。如果我们将图像 I 1 I_1 I1 的 ( x , y ) (x,y) (x,y) 点与图像 I 2 I_2 I2 中 ( x ′ , y ′ ) (x',y') (x′,y′) 处的图像块进行比较,则相似性度量如下:
∑ i , j ( I 1 ( x + i , y + i ) − I 2 ( x ′ + i , y ′ + j ) ) 2 \sum_{i,j}(I_1(x+i,y+i)-I_2(x'+i,y'+j))^2 i,j∑(I1(x+i,y+i)−I2(x′+i,y′+j))2
使用上式可以计算以每个点 ( i , j ) (i,j) (i,j) 为中心的正方形模板的像素值差。由于相似图像块中相邻像素之间的差异应该很小,因此最佳匹配的图像块应该是总和最小的图像块。这可以在匹配函数的主循环中完成的;也就是说,对于一幅图像中的每个关键点,我们匹配另一幅图像上的关键点,该关键点具有最小平方差之和,我们也可以拒绝总和超过某个阈值的匹配。在示例代码中,我们按照相似性分数从大到小对匹配进行排序。
在本节中,我们使用大小为 11x11
的方形图像块进行匹配,更大的图像邻域会创建更独特的图像块,但也会使它们对局部场景变化更加敏感。
只要两个图像从相似的视角和相似的条件显示场景,就可以通过简单的平方差之和来比较两个图像窗口。但在实际场景中,简单的光照变化就会增加或减少一个图像块的所有像素强度,从而产生较大的平方差。为了使匹配对光照变化更加鲁棒,我们可以使用其他相似性度量测量两个图像窗口之间的相似性。在 OpenCV
中可以使用归一化平方差之和 (TM_SQDIFF_NORMED
标志)计算相似性:
∑ i , j ( I 1 ( x + i , y + i ) − I 2 ( x ′ + i , y ′ + j ) ) 2 ∑ i , j I 1 ( x + i , y + i ) 2 ∑ i , j ( I 2 ( x ′ + i , y ′ + j ) 2 \frac {\sum_{i,j}(I_1(x+i,y+i)-I_2(x'+i,y'+j))^2} {\sqrt{\sum_{i,j}I_1(x+i,y+i)^2}\sqrt{\sum_{i,j}(I_2(x'+i,y'+j)^2}} ∑i,jI1(x+i,y+i)2∑i,j(I2(x′+i,y′+j)2∑i,j(I1(x+i,y+i)−I2(x′+i,y′+j))2
OpenCV
中也存在基于相关性的其他相似性度量,例如,TM_CCORR
标志定义的相似性度量如下:
∑ i , j I 1 ( x + i , y + i ) I 2 ( x ′ + i , y ′ + j ) \sum_{i,j}I_1(x+i,y+i)I_2(x'+i,y'+j) i,j∑I1(x+i,y+i)I2(x′+i,y′+j)
当两个图像块相似时,该值达到最大。
识别出的匹配项存储在 cv::DMatch
实例向量中。本质上,cv::DMatch
数据结构包含引用第一个关键点向量中的元素的索引和引用第二个关键点向量中的匹配特征的个索引;它还包含一个实数值,表示两个匹配的描述符之间的距离。当比较两个 cv::DMatch
实例时,使用操作符 <
比较距离值。
检测图像中特定模式或对象是图像分析中的一项常见任务,可以通过定义对象的缩略图、模板并在给定图像中搜索相似的区域来完成。通常,搜索仅限于我们认为可以在其中找到对象的感兴趣区域,然后将模板滑过该区域,并在每个像素位置使用 cv::matchTemplate
函数计算相似性度量。cv::matchTemplate
函数的输入是小尺寸的模板图像和要执行搜索的图像,输出结果是浮点类型 cv::Mat
数据,对应于每个像素位置的相似度得分。如果模板的大小为 MxN
而图像的大小为 WxH
,则生成的 cv::Mat
矩阵的大小为 (W-N+1)x(H-N+1)
。一般来说,我们仅对相似度最高的位置感兴趣;所以,典型的模板匹配代码如下:
// 匹配模板
// 定义一个模板
cv::Mat target(image1, cv::Rect(250, 150, 50, 50));
cv::namedWindow("Template");
cv::imshow("Template", target);
// 定义搜索区域
cv::Mat roi(image2, cv::Rect(0, 0, image2.cols, image2.rows/2));
// 执行模板匹配
cv::matchTemplate(roi, // 搜索区域
target, // 模板
result, // 结果
cv::TM_SQDIFF); // 相似性度量
// 最相似区域
double minVal, maxVal;
cv::Point minPt, maxPt;
cv::minMaxLoc(result, &minVal, &maxVal, &minPt, &maxPt);
cv::rectangle(roi, cv::Rect(minPt.x, minPt.y, target.cols, target.rows), 255);
需要注意的是,以上操作的的计算代价较高,因此我们应该限制搜索区域并使用尺寸较小的模板。
完整代码 patches.cpp
如下所示:
#include
#include
#include
#include
#include
#include
#include
int main() {
// 图像匹配
// 1. 读取图像
cv::Mat image1 = cv::imread("1.png", cv::IMREAD_GRAYSCALE);
cv::Mat image2 = cv::imread("2.png", cv::IMREAD_GRAYSCALE);
// 2. 定义关键点向量
std::vector<cv::KeyPoint> keypoints1;
std::vector<cv::KeyPoint> keypoints2;
// 3. 定义特征检测器
cv::Ptr<cv::FeatureDetector> ptrDetector;
ptrDetector = cv::FastFeatureDetector::create(80);
// 4. 关键点检测
ptrDetector->detect(image1, keypoints1);
ptrDetector->detect(image2, keypoints2);
std::cout << "Number of keypoints (image 1): " << keypoints1.size() << std::endl;
std::cout << "Number of keypoints (image 2): " << keypoints2.size() << std::endl;
// 5. 定义正方形邻域
const int nsize(11);
cv::Rect neighborhood(0, 0, nsize, nsize);
cv::Mat patch1;
cv::Mat patch2;
// 6. 对于第一张图像中的每个关键点在第二张图像中检测最佳匹配
cv::Mat result;
std::vector<cv::DMatch> matches;
for (int i=0; i<keypoints1.size(); i++) {
// 定义图像块
neighborhood.x = keypoints1[i].pt.x-nsize/2;
neighborhood.y = keypoints1[i].pt.y-nsize/2;
if (neighborhood.x<0 || neighborhood.y<0 || neighborhood.x+nsize>=image1.cols || neighborhood.y+nsize>=image1.rows) {
continue;
}
// image1 中图像块
patch1 = image1(neighborhood);
// 重置最佳相关性值
cv::DMatch bestMatch;
for (int j=0; j<keypoints2.size(); j++) {
// 定义图像块
neighborhood.x = keypoints2[j].pt.x-nsize/2;
neighborhood.y = keypoints2[j].pt.y-nsize/2;
if (neighborhood.x<0 || neighborhood.y<0 || neighborhood.x+nsize>=image2.cols || neighborhood.y+nsize>=image2.rows) {
continue;
}
// image2 中图像块
patch2 = image2(neighborhood);
// 匹配两个图像块
cv::matchTemplate(patch1, patch2, result, cv::TM_SQDIFF);
// 检查是否是最佳匹配
if (result.at<float>(0, 0) < bestMatch.distance) {
bestMatch.distance = result.at<float>(0, 0);
bestMatch.queryIdx = i;
bestMatch.trainIdx = j;
}
}
// 添加最佳匹配
matches.push_back(bestMatch);
}
std::cout << "Number of matches: " << matches.size() << std::endl;
// 提取最佳的 50 个匹配
std::nth_element(matches.begin(), matches.begin()+50, matches.end());
matches.erase(matches.begin()+50, matches.end());
std::cout << "Number of matches (after): " << matches.size() << std::endl;
// 绘制匹配结果
cv::Mat matchImage;
cv::drawMatches(image1, keypoints1, // 第一张图像
image2, keypoints2, // 第二张图像
matches, // 匹配向量
matchImage, // 生成的图像
cv::Scalar(255, 255, 255), // 线颜色
cv::Scalar(255, 255, 255)); // 点颜色
cv::namedWindow("Matches");
cv::imshow("Matches",matchImage);
// 匹配模板
// 定义一个模板
cv::Mat target(image1, cv::Rect(250, 150, 50, 50));
cv::namedWindow("Template");
cv::imshow("Template", target);
// 定义搜索区域
cv::Mat roi(image2, cv::Rect(0, 0, image2.cols, image2.rows/2));
// 执行模板匹配
cv::matchTemplate(roi, // 搜索区域
target, // 模板
result, // 结果
cv::TM_SQDIFF); // 相似性度量
// 最相似区域
double minVal, maxVal;
cv::Point minPt, maxPt;
cv::minMaxLoc(result, &minVal, &maxVal, &minPt, &maxPt);
cv::rectangle(roi, cv::Rect(minPt.x, minPt.y, target.cols, target.rows), 255);
cv::namedWindow("Best");
cv::imshow("Best",image2);
cv::waitKey();
return 0;
}
基于关键点执行图像分析需要构建丰富的表示来唯一地描述这些关键点,本节重点介绍了如何从关键点中提取描述符,然后基于特征描述符执行图像匹配,关联同一场景中的两张不同图像或检测图像中的目标对象。
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测