- 概述
Opencv提供的可用于模板匹配的方式有三种类型
(1) matchTemplate
(2) matchShapes
(3) keyPointMatch
网上也上这些方式的实现代码,但在各种环境下使用的效果并不怎么好,特别是匹配角度。网上的代码要么不完整,要么在特定的环境下表现还行,但换一种应用环境情况很糟糕。
这里的目标就是构建一种通用的模板匹配,能在大多数环境下适应。
- 构建思路
构思的思路是要找出matchTemplate或matchShapes的在常规应用中的问题,改善这些问题。
直接使用matchTemplate,在简单环境中(图片中轮廓和形体比较简单)位置匹配还不错,但角度结果很糟糕,根本不能使用。复杂环境中位置匹配(轮廓、形体比较复杂,灰度阶差较小)经常出错不够准确。
matchShapes在复杂轮廓环境中情况比matchTemplate的表现还差。但若只找出模版中最大的一个轮廓,使用这个轮廓作为模版匹配,情况能改善很多,但也只有90%左右的准确率。
分析matchTemplete位置匹配失败的原因大多是因为使用整个模版匹配导致的特征权重不够引起的,所以使用mask图片只选择目标特种能改善不小。matchShapes使用了多种方式试图改善匹配结果,但收效甚微。
对于角度匹配,实现思路也很简单。对每个角度做一个模板,进行匹配找到最优的角度。
- 代码实现
3.1. 创建模板
创建模板的代码核心是使用轮廓处理cv::findContour函数找到ROI的轮廓,过滤出参数设定的面积作为有效轮廓,依此轮廓区域做为模版。然后对此模板选择360度创建各个角度的模板并保存
void QGrayMatch::createTemp(const cv::Mat &img)
{
m_countourMap.clear();
m_maskMap.clear();
m_templateMap.clear();
IVisionWidget * pWnd = ::getCurVisionWnd();
if(pWnd)pWnd->clear();
double thres = this->propValue(1).toDouble();
QString binType = this->propValue(2).toString();
double minArea = this->propValue(3).toDouble();
double maxArea = this->propValue(4).toDouble();
int core = this->propValue(6).toInt();
double gan = this->propValue(7).toDouble();
double startAngle = this->propValue(8).toDouble();
double endAngle = this->propValue(9).toDouble();
double angleStep = this->propValue(10).toDouble();
cv::Point2f roiPt;
QROIVal *pRoi = (QROIVal*)prop(0);
if(pRoi)
{
int n = pRoi->getSubItemCount();
for(int k =0; k<n; k++)
{
const QROIVal * subRoi = pRoi->getSubItem(k);
if(subRoi->getRoiType() == QROIVal::RECTROI)
{
double r0,c0,r1,c1;
subRoi->getItemRect(r0,c0,r1,c1);
cv::Rect modelROI(c0,r0,c1-c0,r1-r0);
cv::Mat modelImg = img(modelROI);
debugDispImage(modelImg,"roi");
roiPt = cv::Point2f(c0,r0);
cv::Mat grayImage;
if(modelImg.channels() > 1)cv::cvtColor(modelImg, grayImage, cv::COLOR_BGR2GRAY);
else grayImage = modelImg;
debugDispImage(grayImage,"gray");
cv::Mat gaussImage;
cv::GaussianBlur(grayImage, gaussImage, cv::Size(core, core), gan, 0);
debugDispImage(gaussImage,"gaussImage");
cv::Mat binary;
if("THRESH_BINARY" == binType)cv::threshold(gaussImage, binary, thres, 255, cv::THRESH_BINARY);
else cv::threshold(gaussImage, binary, thres, 255, cv::THRESH_BINARY_INV);
debugDispImage(binary,"threshold");
m_roiSize = binary.size();
int n = (endAngle - startAngle)/angleStep;
for(int i=0; i<n; i++)
{
double angle = startAngle + i*angleStep;
cv::Mat rotImage;
rotateImage(binary,rotImage,angle);
debugDispImage(rotImage,QString("angle %0").arg(angle));
QCountourList countours;
cv::findContours(rotImage, countours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);
//选择符合条件的区域
QList<int> ids = select(countours,minArea,maxArea);
if(ids.size() > 0)
{
//创建一个全黑的掩码
cv::Mat mask = cv::Mat::zeros(rotImage.size(), CV_8UC1);
for(int k=0; k<ids.size(); k++)
{
cv::drawContours(mask,countours,ids[k],cv::Scalar(255),-1);
//保存要显示的模版轮廓
if(0 == angle)m_countourMap.insert(std::make_pair(ids[k],countours[ids[k]]));
}
m_maskMap.insert(std::make_pair(angle,mask));
debugDispImage(mask,"mask");
cv::Mat mm;
rotateImage(modelImg,mm,angle);
cv::Mat tmpImg;
mm.copyTo(tmpImg,mask);
m_templateMap.insert(std::make_pair(angle,tmpImg));
m_templateSize = tmpImg.size();
debugDispImage(tmpImg,"temp");
}
}
//没有0度是的模版信息,创建0度时的模版信息
if(m_countourMap.empty())
{
double angle = 0;
cv::Mat rotImage = binary;
debugDispImage(rotImage,QString("angle %0").arg(angle));
QCountourList countours;
cv::findContours(rotImage, countours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE);
//选择符合条件的区域
QList<int> ids = select(countours,minArea,maxArea);
if(ids.size() > 0)
{
//创建一个全黑的掩码
cv::Mat mask = cv::Mat::zeros(rotImage.size(), CV_8UC1);
for(int k=0; k<ids.size(); k++)
{
cv::drawContours(mask,countours,ids[k],cv::Scalar(255),-1);
//保存要显示的模版轮廓
m_countourMap.insert(std::make_pair(ids[k],countours[ids[k]]));
}
m_maskMap.insert(std::make_pair(angle,mask));
debugDispImage(mask,"mask");
cv::Mat mm;
rotateImage(modelImg,mm,angle);
cv::Mat tmpImg;
mm.copyTo(tmpImg,mask);
m_templateMap.insert(std::make_pair(angle,tmpImg));
m_templateSize = tmpImg.size();
debugDispImage(tmpImg,"temp");
}
}
}
}
}
if(pWnd)
{
pWnd->clear();
//显示示教的模版轮廓
QCountourMap::iterator it = m_countourMap.begin();
for(; it != m_countourMap.end(); ++it)
{
this->dispCountour(it->second,roiPt);
}
}
if(pWnd)pWnd->updataView();
}
3.2. 匹配
实现的核心是先使用带mask的matchTemplate方法先获得配置的位置,然后截取ROI大小的区域与各个角度的模版匹配获得准确的角度,但角度的匹配不能使用matchTemplate或matchShapes,这两个函数角度匹配的准确性太差,我使用图像差值后求取差值的图片的平均值作为参考,找差值平均值最小的模版即为当前的角度,角度准确率比较高接近100%
void QGrayMatch::match(const cv::Mat &im)
{
IVisionWidget * pWnd = ::getCurVisionWnd();
if(pWnd)pWnd->clear();
//double thres = this->propValue(1).toDouble();
//QString binType = this->propValue(2).toString();
//int core = this->propValue(6).toInt();
double minSocre = this->propValue(5).toDouble();
//double gan = this->propValue(7).toDouble();
double startAngle = this->propValue(8).toDouble();
//double endAngle = this->propValue(9).toDouble();
double angleStep = this->propValue(10).toDouble();
cv::Mat grayImage;
if(im.channels() > 1)cv::cvtColor(im, grayImage, cv::COLOR_BGR2GRAY);
else grayImage = im;
debugDispImage(grayImage,"gray");
//匹配位置
cv::Mat mask = m_maskMap[0];
cv::Mat tmpp = m_templateMap[0];
debugDispImage(mask,"mask");
debugDispImage(tmpp,"tempp");
cv::Rect rect;
cv::Point center;
double score = this->templateMatch(grayImage,tmpp,mask,rect,center);
if(score > 0.2)return;
//根据位置匹配结果获取区域图片
cv::Mat imm = grayImage(rect);
debugDispImage(imm,"imm");
//匹配角度
double angle = 0;
double angleScore = 100;
for(int i=0; i<m_templateMap.size(); i++)
{
double ang = startAngle + i*angleStep;
cv::Mat mask = m_maskMap[ang];
cv::Mat tmpp = m_templateMap[ang];
debugDispImage(mask,"mask");
debugDispImage(tmpp,"tempp");
cv::Mat rrs;
cv::absdiff(imm,tmpp,rrs);
cv::Mat rrt;
rrs.copyTo(rrt,mask);
cv::Scalar s = cv::mean(rrt);
double d = s[0];
if(d < angleScore)
{
angleScore = d;
angle = ang;
}
debugDispImage(rrt,QString("t-m %0 %1").arg(d).arg(ang));
}
if(score < minSocre && angleScore < 10)
{
QString msg = QString("match res : %0, angle : %1").arg(score).arg(angle);
if(pWnd)
{
pWnd->dispText(msg);
pWnd->dispCross(center.y,center.x,20,45);
pWnd->dispRect(rect.y,rect.x,rect.y+rect.height,rect.x+rect.width);
}
}
if(pWnd)pWnd->updataView();
}
3.3. 归档和载入
档的目标是模版信息保存在.dat文件中,其中包含了各个角度的模版信息和mask信息。
void QGrayMatch::saveTemplate()
{
QString fileName = QSystem::getCurrentProductDir() +"/" + id() + "_grayMatch.dat";
QFile file(fileName);
if(file.open(QIODevice::WriteOnly))
{
QDataStream stream(&file);
//for(int i=0; i<100; i++)
// stream << "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
int nAngle = m_templateMap.size();
stream << 0xffff0000 << 0xffff0000;
stream << m_nTemplateMethod << m_templateSize << m_roiSize << nAngle; //16个byte
QTemplateMap::iterator it = m_templateMap.begin();
for(; it != m_templateMap.end(); ++it)
{
stream << (unsigned int)0xffff0000 << (unsigned int)0xffff0000 << it->first;
cv::Mat &im = it->second;
stream << im;
}
QMaskMap::iterator it2 = m_maskMap.begin();
for(; it2 != m_maskMap.end(); ++it2)
{
stream << (unsigned int)0xffff0000 << (unsigned int)0xffff0000 << it2->first;
cv::Mat &im = it2->second;
stream << im;
}
file.flush();
}
}
void QGrayMatch::loadTemplate()
{
QString fileName = QSystem::getCurrentProductDir() +"/" + id() + "_grayMatch.dat";
QFile file(fileName);
if(file.open(QIODevice::ReadOnly))
{
QDataStream stream(&file);
unsigned int fl1,fl2;
stream >> fl1 >> fl2;
if(fl1 == fl2 && fl2== 0xffff0000)
{
if(stream.atEnd())return;
int nAngle = 0;
stream >> m_nTemplateMethod >> m_templateSize >> m_roiSize >> nAngle;
if(stream.atEnd())return;
for(int i=0; i<nAngle; i++)
{
if(stream.atEnd())return;
unsigned int f1,f2;
double angle = 0;
stream >> f1 >> f2 >> angle;
if(stream.atEnd())return;
if(f1 == 0xffff0000 && f2 == 0xffff0000)
{
cv::Mat im;
stream >> im;
m_templateMap.insert(std::make_pair(angle,im));
//QString path = QString("F:\\test\\template_%0.png").arg(i);
//cv::imwrite(path.toStdString(),im);
}
}
for(int i=0; i<nAngle; i++)
{
if(stream.atEnd())return;
unsigned int f1,f2;
double angle = 0;
stream >> f1 >> f2 >> angle;
if(stream.atEnd())return;
if(f1 == 0xffff0000 && f2 == 0xffff0000)
{
cv::Mat im;
stream >> im;
m_maskMap.insert(std::make_pair(angle,im));
//QString path = QString("F:\\test\\mask_%0.png").arg(i);
//cv::imwrite(path.toStdString(),im);
}
}
}
}
}
- 后记
opencv模版匹配最大的为题是位置匹配的准确度和角度匹配的准确度。使用mask后模板匹配的准确度提升了不少,截取配置匹配后的区域,使用图像差值法匹配角度基本上能100%准确匹配角度。
但受限于matchTemplate函数的能力,目前还无法做到与halcon的模版匹配同样的准确率。