最近《旅行青蛙》这款收集类手游十分火爆,大家在手机中养着自己的青蛙“儿子”“女儿”,看着它们去旅行,寄回许多照片,带回各种特产、朋友,这让博主萌生了写这篇博文的想法:
这是一个非常好的探索模板匹配的机会!
现象级的戳中玩家内心的手游
为什么这么说呢?因为《旅行青蛙》中的照片除了少数是整张手绘,大部分是由背景、青蛙形态以及青蛙朋友的形态贴图合成的。从数字图像的角度上看,那些照片都是500*350*3的立体矩阵,而青蛙是糊上去的一团料。这也就意味着以青蛙形态贴图为模板,可以找到寄回的照片中“蛙儿子”的位置,顺便学习OpenCV。
想从寄回的照片中找到“蛙儿子”的位置?你也许需要使用OpenCV3中的模板匹配功能。模板匹配是图像识别中比较常用的一种方法,能从待识别图像中找出与模板图像最相似的部分,相比于经典的特征提取、当下火热的深度学习等方法,模板匹配简单、效率较高,但模板匹配也在旋转不变性、尺度不变性上有所欠缺。因此模板匹配一般用于对与模板完全相同的对象进行识别,并作为一种目标跟踪(Tracking)的方法,与目标识别相结合提高运行效率、识别准确度。博主未来将对基于模板匹配的目标跟踪方法专门写一篇博文做以讨论。
OpenCV3中提供了matchTemplate函数与minMaxLoc函数共同实现模板匹配的功能。
根据OpenCV3.3.1的官方文档中所述:
1、matchTemplate 函数:将模板与匹配图像区域进行比较,函数将遍历匹配图像,根据设定的运算方法对匹配图像矩阵进行运算。
官方定义为
void cv::matchTemplate ( InputArray image,
InputArray templ,
OutputArray result,
int method,
InputArray mask = noArray()
)
其中image为输入的匹配图像(待识别图像),templ为输入的模板图像,result为输出的处理图像,method为设定的运算方法,mask为搜索模板的掩模(仅限TM_SQDIFF与TM_CCORR_NORMED使用,默认为空数组)。设定的运算方法分为六种:TM_SQDIFF、TM_CCORR或TM_CCOEFF及它们的归一化衍生型TM_SQDIFF_NORMED、TM_CCORR_NORMED与TM_CCOEFF_NORMED,在OpenCV中对应六个int型数字,使用时用英文名即可。六种方法的解释如下:
(1)cv::TM_SQDIFF:该方法使用平方差进行匹配,因此最佳的匹配结果在结果为0处,值越大匹配结果越差。
(2)cv::TM_SQDIFF_NORMED:该方法使用归一化的平方差进行匹配,最佳匹配也在结果为0处。
(3)cv::TM_CCORR:相关性匹配方法,该方法使用源图像与模板图像的卷积结果进行匹配,因此,最佳匹配位置在值最大处,值越小匹配结果越差。
(4)cv::TM_CCORR_NORMED:归一化的相关性匹配方法,与相关性匹配方法类似,最佳匹配位置也是在值最大处。
(5)cv::TM_CCOEFF:相关性系数匹配方法,该方法使用源图像与其均值的差、模板与其均值的差二者之间的相关性进行匹配,最佳匹配结果在值等于1处,最差匹配结果在值等于-1处,值等于0直接表示二者不相关。
(6)cv::TM_CCOEFF_NORMED:归一化的相关性系数匹配方法,正值表示匹配的结果较好,负值则表示匹配的效果较差,也是值越大,匹配效果也好。
具体的函数数学形式与解释可见liyuanbhu博主的
OpenCV 学习笔记(模板匹配)
2、minMaxLoc 函数:在数组(或矩阵)中寻找全局最大值与最小值,并给出它们的位置。
官方定义为
void cv::minMaxLoc ( InputArray src,
double * minVal,
double * maxVal = 0,
Point * minLoc = 0,
Point * maxLoc = 0,
InputArray mask = noArray()
)
因为模板匹配的最优结果常在matchTemplate函数的全局最小值(设定方法为TM_SQDIFF时),或全局最大值(设定方法为TM_CCORR或TM_CCOEFF时)处,将matchTemplate函数输出的处理图像作为minMaxLoc的输入即可找到和模板最相似的目标的位置。
此处要注意两点:
(1)当matchTemplate函数中设定方法为TM_SQDIFF时,目标的中心点在Point(minLoc.x + image_template.cols/2, minLoc.y + image_template.rows/2);当设定方法为TM_CCORR或TM_CCOEFF时,目标的中心点在Point(maxLoc.x + image_template.cols/2, maxLoc.y + image_template.rows/2)。
(2)matchTemple中输入的待测图像、模板图像既可是彩色图像,也可是灰度图像。模板匹配并不会单纯把彩色图像转换为灰度图像,而是会对彩色图像的各个通道进行独立平均运算后再对整体进行处理,最终仍是处理成一个单通道的图像。因此将彩色图像转换成灰度图像后再进行模板匹配,和直接输入彩色图像进行模板匹配是不同的。
原理都叙述完了,让我们来看看OpenCV3实现模板匹配的简单实例:
待测图片(匹配图片)为:
1.jpg
模板图片为:
Template1.jpg
本文中的代码都基于OpenCV3.3.1版,使用OpenCV2的小伙伴可能需要微调程序。代码如下:
#include <iostream>
#include <stdio.h>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//加载源图像和模板图像
Mat image_source = imread("1.jpg");
Mat image_template = imread("Template1.jpg");
Mat image_matched;
double minVal, maxVal;
Point minLoc, maxLoc;
//模板匹配
matchTemplate(image_source, image_template, image_matched, TM_CCOEFF_NORMED);
/*
1、cv::TM_SQDIFF:该方法使用平方差进行匹配,因此最佳的匹配结果在结果为0处,值越大匹配结果越差。
2、cv::TM_SQDIFF_NORMED:该方法使用归一化的平方差进行匹配,最佳匹配也在结果为0处。
3、cv::TM_CCORR:相关性匹配方法,该方法使用源图像与模板图像的卷积结果进行匹配,因此,最佳匹配位置在值最大处,值越小匹配结果越差。
4、cv::TM_CCORR_NORMED:归一化的相关性匹配方法,与相关性匹配方法类似,最佳匹配位置也是在值最大处。
5、cv::TM_CCOEFF:相关性系数匹配方法,该方法使用源图像与其均值的差、模板与其均值的差二者之间的相关性进行匹配,最佳匹配结果在值等于1处,最差匹配结果在值等于-1处,值等于0直接表示二者不相关。
6、cv::TM_CCOEFF_NORMED:归一化的相关性系数匹配方法,正值表示匹配的结果较好,负值则表示匹配的效果较差,也是值越大,匹配效果也好。
*/
//寻找最佳匹配位置
minMaxLoc(image_matched, &minVal, &maxVal, &minLoc, &maxLoc);
circle(image_source,
Point(maxLoc.x + image_template.cols/2, maxLoc.y + image_template.rows/2),//使用cv::TM_SQDIFF与cv::TM_SQDIFF_NORMED时maxLoc改为minLoc
20,
Scalar(0, 0, 255),
2,
8,
0);
imshow("match result", image_matched);
imshow("target", image_source);
imwrite("Target1.jpg", image_source);
waitKey(0);
return 0;
}
代码保存的图像如下:
该实例中以TM_CCOEFF_NORMED(归一化相关性系数匹配法)成功地确定出了青蛙在图片中的位置。代码输出图像截图如下:
图中match_result是matchTemplate函数处理生成的中间图像,惊悚地提取出了原图像中的特征。
博主还做了另外几组的模板匹配,分别选择了合适的匹配方法:
1、模板为
匹配方法为TM_CCOEFF_NORMED
2、模板为
匹配方法为TM_CCOEFF
3、模板同2,匹配方法为TM_CCOEFF(这图的匹配方法似乎都不大合适)
4、模板同2,匹配方法为TM_CCOEFF_NORMED
5、模板同2,匹配方法为TM_CCOEFF_NORMED
上述的六张图片主要都用了TM_CCOEFF与TM_CCOEFF_NORMED两种方法。然而另外几种方法运行起来效果如何,方法之间的运行效率又如何呢?博主分别用matchTemplate的六种方法,基于上述5中的模板、图像进行了测试:
对上述模板
Template3.jpg
匹配图像
34.png
用六种方法进行匹配,输出100次matchTemplate、minMaxLoc函数语句循环所花的时间:
程序如下(主要多了个循环,以及计时函数):
#include <iostream>
#include <stdio.h>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
//加载源图像和模板图像
Mat image_source = imread("34.png");
Mat image_template = imread("Template3.jpg");
Mat image_matched;
double maxvalue = 0;
double minVal, maxVal;
Point minLoc, maxLoc;
double startime = cv::getTickCount();
for(int i = 0;i<100;i++)
{
//模板匹配
matchTemplate(image_source, image_template, image_matched, TM_CCOEFF_NORMED);
/*
1、cv::TM_SQDIFF:该方法使用平方差进行匹配,因此最佳的匹配结果在结果为0处,值越大匹配结果越差。
2、cv::TM_SQDIFF_NORMED:该方法使用归一化的平方差进行匹配,最佳匹配也在结果为0处。
3、cv::TM_CCORR:相关性匹配方法,该方法使用源图像与模板图像的卷积结果进行匹配,因此,最佳匹配位置在值最大处,值越小匹配结果越差。
4、cv::TM_CCORR_NORMED:归一化的相关性匹配方法,与相关性匹配方法类似,最佳匹配位置也是在值最大处。
5、cv::TM_CCOEFF:相关性系数匹配方法,该方法使用源图像与其均值的差、模板与其均值的差二者之间的相关性进行匹配,最佳匹配结果在值等于1处,最差匹配结果在值等于-1处,值等于0直接表示二者不相关。
6、cv::TM_CCOEFF_NORMED:归一化的相关性系数匹配方法,正值表示匹配的结果较好,负值则表示匹配的效果较差,也是值越大,匹配效果也好。
*/
//寻找最佳匹配位置
minMaxLoc(image_matched, &minVal, &maxVal, &minLoc, &maxLoc);
}
maxvalue = (cv::getTickCount() - startime)/cv::getTickFrequency();
cout << maxvalue << endl;
circle(image_source,
Point(maxLoc.x + image_template.cols/2, maxLoc.y + image_template.rows/2), //使用cv::TM_SQDIFF与cv::TM_SQDIFF_NORMED时maxLoc改为minLoc
20,
Scalar(0, 0, 255),
2,
8,
0);
imshow("match result", image_matched);
imshow("target", image_source);
imwrite("Target_TM_CCOEFF_NORMED.png", image_source);
waitKey(0);
return 0;
}
六种方法的输出分别如下:
1、cv::TM_SQDIFF:1.86322秒,成功识别
2、cv::TM_SQDIFF_NORMED:1.66382秒,成功识别
3、cv::TM_CCORR:1.06706秒,未能识别
4、cv::TM_CCORR_NORMED:1.64302秒,成功识别(和3差了一个归一化居然差别这么大……)
5、cv::TM_CCOEFF:1.25885秒,成功识别(当日最佳)
6、cv::TM_CCOEFF_NORMED:1.72507秒,成功识别
综上可见,用cv::TM_CCOEFF找青蛙,兼具高效与准确。当然对不同的图像方法各有优势,在实际使用中最好六种方法都进行尝试再做判断。归一化步骤能将十分接近的数据区分开,从而避免特征遗漏(比如上文中的3和4),但同时也增加了程序运行的时间,是否使用要根据具体的图片、视频特征判断。
以上便是模板匹配在OpenCV中的实现方法,模板匹配也可作为图像跟踪的一种方法,与图像识别配合能将目标捕捉的运行效率、捕捉准确度提高一个层级!这些将在之后的博文中详谈。
欢迎交流讨论!