## 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′))
下面給個例子,我們的測試圖像如下:

我們的模板如下:

程序中會用到 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();
}
~~~
輸出結果是這樣的。

其實上面的代碼還可以封裝一下。
~~~
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
~~~