本章包括以下内容:
- 局部模板匹配;
- 描述并匹配局部强度值模式;
- 用二值描述子匹配关键点。
9.2 局部模板匹配
本节介绍的方案是对图像块中的像素进行逐个比较。这可能是最简单的特征点匹配方法了,但是并不是最可靠的。
最常见的图像块是边长为奇数的正方形,关键点的位置就是正方形的中心。可通过比较块内像素的强度值来衡量两个正方形图像块的相似度。
常见的方案是采用简单的差的平方和(Sum of Squared Differences,SSD)算法。下面是特征匹配策略的具体步骤。
首先检测每幅图像的关键点,这里使用FAST 检测器:
// 定义特征检测器
cv::Ptr<cv::FeatureDetector> ptrDetector; // 泛型检测器指针
ptrDetector = cv::FastFeatureDetector::create(80); // 这里选用FAST 检测器
// 检测关键点
ptrDetector->detect(image1, keypoints1);
ptrDetector->detect(image2, keypoints2);
这里采用了可以指向任何特征检测器的泛型指针类型cv::Ptr<cv::FeatureDetector>。上述代码可用于各种兴趣点检测器,只需在调用函数时更换检测器即可。
然后定义一个特定大小(例如11×11)的矩形,用于表示每个关键点周围的图像块:
// 定义正方形的邻域
const int nsize(11); // 邻域的尺寸
cv::Rect neighborhood(0, 0, nsize, nsize); // 11×11
cv::Mat patch1;
cv::Mat patch2;
将一幅图像的关键点与另一幅图像的全部关键点进行比较。在第二幅图像中找出与第一幅图像中的每个关键点最相似的图像块。这个过程用两个嵌套循环实现,代码如下所示:
// 在第二幅图像中找出与第一幅图像中的每个关键点最匹配的
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;
// 第一幅图像的块
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;
// 第二幅图像的块
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 对象来表示。这个工具类存储了两个被匹配关键点的序号和它们的相似度。
两个图像块越相似,它们对应着同一个场景点的可能性就越大。因此需要根据相似度对匹配结果进行排序:
// 提取25 个最佳匹配项
std::nth_element(matches.begin(), matches.begin() + 25, matches.end());
matches.erase(matches.begin() + 25, matches.end());
你可以用一个相似度阈值筛选这些匹配项,并得出筛选结果。这里保留相似度最高的N 个匹配项(为了方便显示结果,选用N = 25)
OpenCV 本身就带有一个能显示匹配结果的函数——它把两幅图像拼接起来,然后用线条连接每个对应的点。函数的用法如下所示:
// 画出匹配结果
cv::Mat matchImage;
cv::drawMatches(image1, keypoints1, // 第一幅图像
image2, keypoints2, // 第二幅图像
matches, // 匹配项的向量
cv::Scalar(255, 255, 255), // 线条颜色
cv::Scalar(255, 255, 255)); // 点的颜色
得到的结果如下所示。
模板匹配
图像分析中的一个常见任务是检测图像中是否存在特定的图案或物体。实现方法是把包含该物体的小图像作为模板,然后在指定图像上搜索与模板相似的部分。搜索的范围通常仅限于可能发现该物体的区域。在这个区域上滑动模板,并在每个像素位置计算相似度。执行这个操作的函数是cv::matchTemplate,函数的输入对象是一个小图像模板和一个被搜索的图像。
结果是一个浮点数型的cv::mat 函数,表示每个像素位置上的相似度。假设模板尺寸为M × N,图像尺寸为W × H,那么结果矩阵的尺寸就是(W - M + 1) × (H - N + 1)。我们通常只关注相似度最高的位置。
典型的模板匹配代码如下所示(假设目标变量就是这个模板):
// 定义搜索区域
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);
// 在相似度最高的位置绘制矩形
// 本例中为minPt
cv::rectangle(roi, cv::Rect(minPt.x, minPt.y,
target.cols, target.rows), 255);
这个操作是非常耗时的,因此应该限制搜索的区域,并且模板的像素要少。