时间为友,记录点滴。
现在都9012年了,飞飞大神都能随意识别出来小狗小猫了,可我们还是要从传统的图像识别的角度来理解一下模式识别中的模板匹配。
所谓“虽不明,但觉厉”,我们要一步一步摸着石头走向以前未知恐惧的领域,想想还有些小兴奋。
什么是模式识别?
要解释这个概念,还真的不容易(因为我理解也不深)。但是简单的理解,就是老爸经常说我的大脑“机械化”。我们之所以可以很快辨别猫是猫、O不是0,就是因为在我们大脑中已经给猫的做了一个抽象,给O和0做了区分,这样我们才不用每次都重新靠思考和计算理解这到底是不是猫。
这个在大脑中的抽象就是模式识别。
什么是模板匹配?
机器学习炙手可热的今天,貌似好多人都信手拈来“K-NN”、“Bayes Classifier”、“PCA”这种主流的模式识别算法。但是我们今天要聊的是传统、最近单的模板匹配。
模板匹配是一种最原始、最基本的模式识别方法,研究某一特定对象物的图案位于图像的什么地方,进而识别对象物,这就是一个匹配问题。它是图像处理中最基本、最常用的匹配方法。模板匹配具有自身的局限性,主要表现在它只能进行平行移动,若原图像中的匹配目标发生旋转或大小变化,该算法无效。
简单来说,模板匹配就是在整个图像区域发现与给定子图像匹配的小块区域。
怎么实现模板匹配?
如下图,如果我们要在一堆9中找到一个8,最保险的做法是什么?
当然是对这个图从左到右、从上到下依次扫一遍,看看那个数组跟我们大脑中的8的模式匹配度最高,那个就是8喽。
恭喜你,计算机也是这么想的。
在带检测图像上,从左到右,从上向下一个像素一个像素地移动模板,计算模板图像与重叠子图像的匹配度,匹配程度越大,两者相同的可能性越大。
怎么计算匹配度?
其他的我们按下不表,我们看一看OpenCV中提供的集中模板识别的方法。
1.利用平方差来进行匹配,最好匹配为0.匹配越差,匹配值越大。
- TM_SQDIFF:平方差匹配
- TM_SQDIFF_NORMED:标准平方差匹配
2.采用模板和图像间的乘法操作,数越大表示匹配程度较高, 0表示最坏的匹配效果。
- TM_CCORR:相关性匹配
- TM_CCORR_NORMED:标准相关性匹配
3. 将模版对其均值的相对值与图像对其均值的相关值进行匹配,1表示完美匹配,-1表示糟糕的匹配,0表示没有任何相关性(随机序列)。
- TM_CCOEFF:相关性系数匹配
- TM_CCOEFF_NORMED:标准相关性系数匹配
总结:随着从简单的测量(平方差)到更复杂的测量(相关系数),我们可获得越来越准确的匹配(同时也意味着越来越大的计算代价)。
有了这些方法,就好办多了,套用公式就好了。还有更好的。OpenCv提供了现成的API
void cv::matchTemplate ( InputArray image,
InputArray templ,
OutputArray result,
int method,
InputArray mask = noArray()
)
- image参数表示待搜索源图像,必须是8位整数或32位浮点。
- templ参数表示模板图像,必须不大于源图像并具有相同的数据类型。
- method参数表示计算匹配程度的方法。
- result参数表示匹配结果图像,必须是单通道32位浮点。如果image的尺寸为W x H,templ的尺寸为w x h,则result的尺寸为(W-w+1)x(H-h+1)。
还有一个API可以配合使用:
void cv::minMaxLoc ( InputArray src,
double * minVal,
double * maxVal = 0,
Point * minLoc = 0,
Point * maxLoc = 0,
InputArray mask = noArray()
)
- src参数表示输入单通道图像。
- mask参数表示用于选择子数组的可选掩码。
- minVal参数表示返回的最小值,如果不需要,则使用NULL。
- maxVal参数表示返回的最大值,如果不需要,则使用NULL。
- minLoc参数表示返回的最小位置的指针(在2D情况下); 如果不需要,则使用NULL。
- maxLoc参数表示返回的最大位置的指针(在2D情况下); 如果不需要,则使用NULL。
纸上得来终觉浅,绝知此事要躬行。
毛主席说:写代码是检验真理的唯一标准。
C++
注意点:
1. matchTemplate出来的target,是单通道的浮点型。
2. 可以通过minMaxLoc函数找到最大/最小点的位置,然后标定出模板在哪。
#include <iostream>
#include <string>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
class MatchTemp
{
public:
bool matchTest();
void showImgPara(Mat img);
bool showImg();
bool loadOriImg(string fileName);
bool loadTarImg(string fileName);
public:
Mat imgOri;
Mat imgTar;
Mat imgOut;
};
void MatchTemp::showImgPara(Mat img)
{
cout << "sizeof(img) is: " << sizeof(img) << ", img size is: " << img.size << endl;
cout << "rows x cols: (" << img.rows << " x " << img.cols << ")" << endl;
cout << "dims: " << img.dims << endl;
cout << "channels: " << img.channels() << endl;
cout << "type: " << img.type() << endl;
cout << "depth:" << img.depth() << endl;
cout << "elemSize:" << img.elemSize() << " (Bytes per element)" << endl;
cout << "elemSize1:" << img.elemSize1() << "(Bytes per channel)" << endl;
cout << "step[0]: " << img.step[0] << " (Bytes per cows only when 2 dims)" << endl;
cout << "step[1]: " << img.step[1] << " (Bytes per element only when 2 dims)" << endl;
cout << "step1(0): " << img.step1(0) << ", step1(1): " << img.step1(1) << " (step / elemSize1)" << endl;
cout << endl;
}
bool MatchTemp::matchTest()
{
double minVal = 0, maxVal = 0;
Point minLoc, maxLoc, tarLoc;
if (imgOri.empty() || imgTar.empty())
{
cout << "Please input correct image!" << endl;
return false;
}
matchTemplate(imgOri, imgTar, imgOut, TM_SQDIFF);
minMaxLoc(imgOut, &minVal, &maxVal, &minLoc, &maxLoc);
showImgPara(imgOri);
showImgPara(imgTar);
showImgPara(imgOut);
tarLoc = minLoc;
rectangle(imgOri, tarLoc, Point(tarLoc.x + imgTar.cols, tarLoc.y + imgTar.rows), Scalar(0, 0, 255));
imshow("imgMatch", imgOri);
return true;
}
bool MatchTemp::showImg()
{
if (!imgOri.empty())
{
imshow("ImgOri", imgOri);
}
if (!imgTar.empty())
{
imshow("imgTar", imgTar);
}
if (!imgOut.empty())
{
imshow("imgOut", imgOut);
}
return true;
}
bool MatchTemp::loadOriImg(string fileName)
{
imgOri = imread(fileName);
if (imgOri.empty())
{
cout << "loadOriImg Fail!" << endl;
return false;
}
return true;
}
bool MatchTemp::loadTarImg(string fileName)
{
imgTar = imread(fileName);
if (imgTar.empty())
{
cout << "loadOriImg Fail!" << endl;
return false;
}
return true;
}
int main()
{
MatchTemp M;
M.loadOriImg("Gril.jpg");
M.loadTarImg("eye.png");
M.matchTest();
M.showImg();
waitKey(0);
return true;
}
Python:
注意事项:
1. 我们使用了numpy.where来查找目标值,并且使用了npWhereTest来测试where的返回值到底是什么东西?返回值如下图:其实是个tuple(就是一个zip过的坐标值)
2. 我们使用了zip来解压np.where的返回值,并且通过小例子看了zip的压缩和解压。需要注意的是zip出来的类型无法直接打印,需要list()转一下。
3. 如果不用接口minMaxLoc找最大最小值,那么可能找出来的是个范围,这样可以找多个匹配。但是t的赋值需要注意,不同的算法会不同。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author:lowkeyway time:9/2/2019
import sys
import cv2 as cv
import numpy as np
def npWhereTest():
temp = np.arange(9).reshape(3, 3)
print(type(temp), temp)
index = np.where(temp > 5)
print(type(index), index)
print(temp[index])
print(list(zip(*index)))
print(list(zip(*index[::-1])))
def zipTest():
a = np.array([1, 1, 2, 2, 2, 2, 3, 3, 3, 3])
b = np.array([2, 3, 0, 1, 2, 3, 0, 1, 2, 3])
print(a)
print(b)
c = zip(a, b)
print(list(c))
d = [1, 2], [3, 4], [5, 6]
print(type(d), d)
e = zip(*d)
print(type(e), list(e))
def matchTest(imgOri, imgTemp):
cv.imshow("imgOri", imgOri)
cv.imshow("imgTemp", imgTemp)
th, tw = imgTemp.shape[:2]
methods = [cv.TM_SQDIFF_NORMED, cv.TM_CCORR_NORMED, cv.TM_CCOEFF_NORMED]
for md in methods:
imgTarget = cv.matchTemplate(imgOri, imgTemp, md)
if cv.TM_SQDIFF_NORMED == md:
t = 1 - 0.98
loc = np.where(imgTarget < t)
else:
t = 0.98
loc = np.where(imgTarget > t)
for pt in zip(*loc[::-1]):
cv.rectangle(imgOri, pt, (pt[0]+tw, pt[1]+th), (0, 0, 255))
cv.imshow("imgTarget_" + str(md), imgTarget)
cv.imshow("imgOriEye_" + str(md), imgOri)
def main_func(argv):
imgOri = cv.imread("Gril.jpg")
imgTemp = cv.imread("eye.png")
matchTest(imgOri, imgTemp)
# npWhereTest()
# zipTest()
cv.waitKey(0)
if __name__ == '__main__':
main_func(sys.argv)