🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## OpenCV 学习笔记(模板匹配) 模板匹配是在一幅图像中寻找一个特定目标的方法之一。这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否“相似”,当相似度足够高时,就认为找到了我们的目标。 在 OpenCV 中,提供了相应的函数完成这个操作。 matchTemplate 函数:在模板和输入图像之间寻找匹配,获得匹配结果图像 minMaxLoc 函数:在给定的矩阵中寻找最大和最小值,并给出它们的位置 在具体介绍这两个函数之前呢,我们还要介绍一个概念,就是如何来评价两幅图像是否“相似”。 OpenCV 提供了 6 种计算两幅图像相似度的方法。 1. 差值平方和匹配 CV_TM_SQDIFF 1. 标准化差值平方和匹配 CV_TM_SQDIFF_NORMED 1. 相关匹配 CV_TM_CCORR 1. 标准相关匹配 CV_TM_CCORR_NORMED 1. 相关匹配 CV_TM_CCOEFF 1. 标准相关匹配 CV_TM_CCOEFF_NORMED 下面就分别来介绍。首先,先给出几个符号: T(x,y) 用来表示我们的模板。I(x,y) 是我们的目标图像。 R(x,y) 是用来描述相似度的函数。 ### 差值平方和匹配 CV_TM_SQDIFF 这类方法利用图像与模板各个像素差值的平方和来进行匹配,最好匹配为 0。 匹配越差,匹配值越大。 R(x,y)=∑x′,y′(T(x′,y′)−I(x+x′,y+y′))2 ### 标准化差值平方和匹配 CV_TM_SQDIFF_NORMED 这个方法其实和差值平方和算法是类似的。只不过对图像和模板进行了标准化操作。 R(x,y)=∑x′,y′(T(x′,y′)−I(x+x′,y+y′))2∑x′,y′T(x′,y′)2∑x′,y′I(x+x′,y+y′)2−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−√ 这种标准化操作可以保证当模板和图像各个像素的亮度都乘上了同一个系数时,相关度不发生变化。 也就是说当 I(x,y)和T(x,y) 变为k×I(x,y)和k×T(x,y) 时,R(x,y)不发生变化。 ### 相关匹配 CV_TM_CCORR 这类方法采用模板和图像的互相关计算作为相似度的度量方法,所以较大的数表示匹配程度较高,0标识最坏的匹配效果。 R(x,y)=∑x′,y′(T(x′,y′)×I(x+x′,y+y′)) ### 标准化相关匹配 CV_TM_CCORR_NORMED 这个方法和 标准化差值平方和匹配 类似,都是去除了亮度线性变化对相似度计算的影响。可以保证图像和模板同时变亮或变暗k倍时结果不变。 R(x,y)=∑x′,y′(T(x′,y′)×I(x+x′,y+y′))∑x′,y′T(x′,y′)2∑x′,y′I(x+x′,y+y′)2−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−√ ### 相关匹配 CV_TM_CCOEFF 这种方法也叫做相关匹配,但是和上面的 CV_TM_CCORR 匹配方法还是有不通过的。简单的说,这里是把图像和模板都减去了各自的平均值,使得这两幅图像都没有直流分量。 T′(x,y)=T(x,y)−∑x′,y′T(x′,y′)w×hI′(x,y)=I(x,y)−∑x′,y′I(x′,y′)w×hR(x,y)=∑x′,y′(T′(x′,y′)×I′(x+x′,y+y′)) ### 标准相关匹配 CV_TM_CCOEFF_NORMED 这是 OpenCV 支持的最复杂的一种相似度算法。这里的相关运算就是数理统计学科的相关系数计算方法。具体的说,就是在减去了各自的平均值之外,还要各自除以各自的方差。经过减去平均值和除以方差这么两步操作之后,无论是我们的待检图像还是模板都被标准化了,这样可以保证图像和模板分别改变光照亮不影响计算结果。计算出的相关系数被限制在了 -1 到 1 之间,1 表示完全相同,-1 表示两幅图像的亮度正好相反,0 表示两幅图像之间没有线性关系。 T′(x,y)=T(x,y)−1w×h∑x′,y′T(x′,y′)∑x′,y′T(x′,y′)2−−−−−−−−−−−−−√I′(x,y)=I(x,y)−1w×h∑x′,y′I(x′,y′)∑x′,y′I(x′,y′)2−−−−−−−−−−−−−√R(x,y)=∑x′,y′(T′(x′,y′)×I′(x+x′,y+y′)) 下面给个例子,我们的测试图像如下: ![这里写图片描述](https://box.kancloud.cn/2016-04-26_571f1db60b163.jpg "") 我们的模板如下: ![这里写图片描述](https://box.kancloud.cn/2016-04-26_571f1db627c30.jpg "") 程序中会用到 OpenCV 的函数包括: ~~~ void matchTemplate( InputArray image, InputArray templ, OutputArray result, int method ); ~~~ 其中 result 是一个矩阵,返回每一个点匹配的结果。 ~~~ void minMaxLoc(InputArray src, CV_OUT double* minVal, CV_OUT double* maxVal=0, CV_OUT Point* minLoc=0, CV_OUT Point* maxLoc=0, InputArray mask=noArray()); ~~~ 这个函数可以在一个矩阵中寻找最大点或最小点,并将位置返回回来。 下面是完整的测试程序。 ~~~ #include <QCoreApplication> #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" using namespace cv; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); cv::Mat image = imread("D:/test.png", cv::IMREAD_COLOR ); cv::Mat templateImage = imread("D:/template.png", cv::IMREAD_COLOR); int result_cols = image.cols - templateImage.cols + 1; int result_rows = image.rows - templateImage.rows + 1; cv::Mat result = cv::Mat( result_cols, result_rows, CV_32FC1 ); cv::matchTemplate( image, templateImage, result, CV_TM_SQDIFF ); double minVal, maxVal; cv::Point minLoc, maxLoc, matchLoc; cv::minMaxLoc( result, &minVal, &maxVal, &minLoc, &maxLoc, Mat() ); matchLoc = minLoc; cv::rectangle( image, cv::Rect(matchLoc, cv::Size(templateImage.cols, templateImage.rows) ), Scalar(0, 0, 255), 2, 8, 0 ); imshow("", image); return a.exec(); } ~~~ 输出结果是这样的。 ![这里写图片描述](https://box.kancloud.cn/2016-04-26_571f1db63d853.jpg "") 其实上面的代码还可以封装一下。 ~~~ double match(cv::Mat image, cv::Mat tepl, cv::Point &point, int method) { int result_cols = image.cols - tepl.cols + 1; int result_rows = image.rows - tepl.rows + 1; cv::Mat result = cv::Mat( result_cols, result_rows, CV_32FC1 ); cv::matchTemplate( image, tepl, result, method ); double minVal, maxVal; cv::Point minLoc, maxLoc; cv::minMaxLoc( result, &minVal, &maxVal, &minLoc, &maxLoc, Mat() ); switch(method) { case CV_TM_SQDIFF: case CV_TM_SQDIFF_NORMED: point = minLoc; return minVal; break; default: point = maxLoc; return maxVal; break; } } ~~~ 利用这个封装代码,我们可以把所有的匹配方法都实验一下。并且比较一下计算速度。 ~~~ int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); cv::Mat image = imread("D:/test.png", cv::IMREAD_COLOR ); cv::Mat tepl = imread("D:/template.png", cv::IMREAD_COLOR); cv::Point matchLoc; double value; int elapse; QTime t; t.start(); value = match(image, tepl, matchLoc, CV_TM_SQDIFF); elapse = t.elapsed(); qDebug("CV_TM_SQDIFF Time elapsed: %d ms", elapse); qDebug() << value; t.start(); value = match(image, tepl, matchLoc, CV_TM_SQDIFF_NORMED); elapse = t.elapsed(); qDebug("CV_TM_SQDIFF_NORMED Time elapsed: %d ms", elapse); qDebug() << value; t.start(); value = match(image, tepl, matchLoc, CV_TM_CCORR); elapse = t.elapsed(); qDebug("CV_TM_CCORR Time elapsed: %d ms", elapse); qDebug() << value; t.start(); value = match(image, tepl, matchLoc, CV_TM_CCORR_NORMED); elapse = t.elapsed(); qDebug("CV_TM_CCORR_NORMED Time elapsed: %d ms", elapse); qDebug() << value; t.start(); value = match(image, tepl, matchLoc, CV_TM_CCOEFF); elapse = t.elapsed(); qDebug("CV_TM_CCOEFF Time elapsed: %d ms", elapse); qDebug() << value; t.start(); value = match(image, tepl, matchLoc, CV_TM_CCOEFF_NORMED); elapse = t.elapsed(); qDebug("CV_TM_CCOEFF_NORMED Time elapsed: %d ms", elapse); qDebug() << value; cv::rectangle( image, cv::Rect(matchLoc, cv::Size(tepl.cols, tepl.rows) ), Scalar(0, 0, 255), 2, 8, 0 ); imshow("", image); return a.exec(); } ~~~ 输出结果如下: ~~~ CV_TM_SQDIFF Time elapsed: 734 ms 4 CV_TM_SQDIFF_NORMED Time elapsed: 699 ms 1.43391e-08 CV_TM_CCORR Time elapsed: 638 ms 2.78957e+08 CV_TM_CCORR_NORMED Time elapsed: 710 ms 1 CV_TM_CCOEFF Time elapsed: 721 ms 2.30675e+08 CV_TM_CCOEFF_NORMED Time elapsed: 759 ms 1 ~~~ 如果我们先将图像都转换为灰度图,那么计算速度会快很多。 ~~~ CV_TM_SQDIFF Time elapsed: 249 ms 12 CV_TM_SQDIFF_NORMED Time elapsed: 246 ms 1.29052e-07 CV_TM_CCORR Time elapsed: 208 ms 9.29857e+07 CV_TM_CCORR_NORMED Time elapsed: 242 ms 1 CV_TM_CCOEFF Time elapsed: 246 ms 7.68916e+07 CV_TM_CCOEFF_NORMED Time elapsed: 281 ms 1 ~~~ 基本缩短到了 1/3 。所以,如果可以用灰度图来计算,就不要用彩色图。 我们还可以去掉模板大小对匹配度的影响: ~~~ double match(cv::Mat image, cv::Mat tepl, cv::Point &point, int method) { int result_cols = image.cols - tepl.cols + 1; int result_rows = image.rows - tepl.rows + 1; cv::Mat result = cv::Mat( result_cols, result_rows, CV_32FC1 ); cv::matchTemplate( image, tepl, result, method ); double minVal, maxVal; cv::Point minLoc, maxLoc; cv::minMaxLoc( result, &minVal, &maxVal, &minLoc, &maxLoc, Mat() ); switch(method) { case CV_TM_SQDIFF: point = minLoc; return minVal / (tepl.cols * tepl.cols); break; case CV_TM_SQDIFF_NORMED: point = minLoc; return minVal; break; case CV_TM_CCORR: case CV_TM_CCOEFF: point = maxLoc; return maxVal / (tepl.cols * tepl.cols); break; case CV_TM_CCORR_NORMED: case CV_TM_CCOEFF_NORMED: default: point = maxLoc; return maxVal; break; } } ~~~ 这时的结果如下: ~~~ CV_TM_SQDIFF Time elapsed: 705 ms 0.000609663 CV_TM_SQDIFF_NORMED Time elapsed: 682 ms 1.43391e-08 CV_TM_CCORR Time elapsed: 615 ms 42517.5 CV_TM_CCORR_NORMED Time elapsed: 698 ms 1 CV_TM_CCOEFF Time elapsed: 703 ms 35158.5 CV_TM_CCOEFF_NORMED Time elapsed: 757 ms 1 ~~~