局部模板匹配 - sumpig/OpenCV GitHub Wiki


通过特征点匹配,可以将一幅图像的点集和另一幅图像(或一批图像)的点集关联起来。

仅凭单个像素就判断两个关键点的相似度显然是不够的,因此要在匹配过程中考虑每个关键点周围的图像块。如果两幅图像块对应着同一个场景元素,那么它们的像素值应该会比较相似。

本节介绍的方案是对图像块中的像素进行逐个比较。这可能是最简单的特征点匹配方法了,但是并不是最可靠的。不过在某些情况下,它也能得到不错的结果。


实现

最常见的图像块是边长为奇数的正方形,关键点的位置就是正方形的中心。可通过比较块内像素的强度值来衡量两个正方形图像块的相似度。

常见的方案是采用简单的差的平方和(Sum of Squared Differences,SSD)算法。

下面是特征匹配策略的具体步骤。

首先检测每幅图像的关键点,这里使用FAST 检测器:

// 定义特征检测器
cv::Ptr<cv::FeatureDetector> ptrDetector; // 泛型检测器指针
ptrDetector = cv::FastFeatureDetector::create(80);

// 检测关键点
ptrDetector->detect(image1, keypoints1);
ptrDetector->detect(image2, keypoints2);

然后定义一个特定大小(例如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());

OpenCV 的 cv::rawMatches 函数,可以显示匹配结果,它把两幅图像拼接起来,然后用线条连接每个对应的点。函数的用法如下所示:

// 画出匹配结果
cv::Mat matchImage;
cv::drawMatches(image1,keypoints1, // 第一幅图像
                image2,keypoints2, // 第二幅图像
                matches, // 匹配项的向量
                cv::Scalar(255,255,255), // 线条颜色
                cv::Scalar(255,255,255)); // 点的颜色

原理

这里用一个简单的标准来比较图像块,即指定 cv::TM_SQDIFF 标志,逐个像素地计算差值的平方和。在比较图像 I1 的像素 (x, y)和图像 I2 的像素 (x', y')时,用下面的公式衡量相似度:

这些(i, j)点的累加值就是以每个点为中心的整个正方形模板的偏移值。如果两个图像块比较相似,它们的相邻像素之间的差距就比较小,因此累加值最小的块就是最匹配的图像块。

只要两幅图像的视角和光照都比较相似,仅用差值平方和来比较两个图像窗口也能得到较好的结果。实际上,只要光照有变化,图像块中所有像素的强度值就会增强或降低,差值平方也会发生很大的变化。为了减少光照对匹配结果的影响,还可采用衡量图像窗口相似度的其他公式。

其中归一化的差值平方和(用 cv::TM_SQDIFF_NORMED 标志)非常实用:

识别出的匹配项存储在 cv::DMatch 类型的向量中。cv::DMatch 数据结构本质上包含两个索引,第一个索引指向第一个关键点向量中的元素,第二个索引指向第二个关键点向量中匹配上的特征点。它还包含一个数值,表示两个已匹配的描述子之间的差距。运算符 < 可用于比较两个cv::DMatch 实例,它的定义中用到了这个差距值。

在绘制匹配项时,我们只显示了差距最小的 25 个匹配项。函数 std::nth_element 将第 N 个元素放在第 N 个位置,将比这个元素小的元素放在它的前面,然后清除向量中的其余元素。


拓展

图像分析中的一个常见任务是检测图像中是否存在特定的图案或物体。

实现方法是把包含该物体的小图像作为模板,然后在指定图像上搜索与模板相似的部分。搜索的范围通常仅限于可能发现该物体的区域。在这个区域上滑动模板,并在每个像素位置计算相似度。执行这个操作的函数是 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);

一定要记住,这个操作是非常耗时的,因此应该限制搜索的区域,并且模板的像素要少。

⚠️ **GitHub.com Fallback** ⚠️