自学内容网 自学内容网

从零开始实现多尺度、多角度的通用模板匹配(深度解析)

前言

  哈哈还在上学的时候,本人的导师就开始使用OpenCV建立自己的图像处理算法库了,之前导师接过一个视觉项目是使用halcon实现的,但是使用halcon需要授权费,该项目的甲方(小公司)无法接受就没成交,后面导师慢慢就转OpenCV这个开源的算法库了,但OpenCV中的很多功能不如halcon中的方便,于是我的导师就参照halcon中的某些功能使用OpenCV一步步的复现出来。
  当然那个时候我也快毕业了,导师只是简单给我介绍了模板匹配的相关实现原理,他的算法库并没有开源给我,虽然我可以看他的电脑(当然是老师允许我用他电脑看代码学习的,我可不是窃密的哈),但也是一知半解的,导师当时跟我说他的匹配速度有些慢,但是可以通过减少匹配次数等提速之类的,说的我一愣一愣的。
  直到后来的某一天我突然悟了,那一日犹如醍醐灌顶,我直接在自己的本地电脑上手搓了个初始版的通用模板匹配(能匹配位置及角度的),把所有的流程跑通了,哈哈那时候我真的觉得自己太秀了,觉得凭这个怎么也能混口饭吃吧,然而现实很残酷,和已经成熟的通用模板匹配效果相比,我这个也只是个小儿科,不过原理都是一样的,后续的相关优化之类的就另说了。

引言

  在传统的工业视觉定位检测任务中,最便捷通用的就是模板匹配算法,通过对目标物体建立模板,使用模板匹配可以快速的定位到该物体在图像中所处的位置及角度,并且通过相机标定确定图像像素与实际物理尺寸的关系后,就能使用机械手等硬件设备在实际物理空间中抓取到该物体了。
  然而有关多角度、多尺度的通用模板匹配的相关资料较少且较难理解,下方会将通用模板匹配各个步骤分解,并配上相关的代码及部分效果图来进行深入的解析。

实现思路

  先截取一个矩形框,然后将其边缘化,对其创建旋转的360个模板,然后对360个模板尺度变换(等比例放大缩小图片),接着对这些个模板采用matchtemplate依次与待匹配图像进行匹配,最终得到相应的结果。

先实现单目标的匹配,再实现多目标的

多目标匹配、非极大值抑制加入等

https://mp.weixin.qq.com/s/LNRtSZP6N8N4mqwPgWVTHA
实战 | OpenCV实现多角度模板匹配(详细步骤 + 代码)

https://mp.weixin.qq.com/s/wK5Z7ut7bGBkv16ixkNKhw
实战 | OpenCV带掩码(mask)的模板匹配使用技巧与演示(附源码)

https://mp.weixin.qq.com/s/5tbsiCQ9FXy8fYmvH9hcSQ
实例应用(二):使用Python和OpenCV进行多尺度模板匹配

https://blog.csdn.net/weixin_39450742/article/details/118600464

仔细阅读,制作边缘、旋转角度、多尺度的模板匹配。

1,创建模板图像

(1)创建单个无损失的旋转、放大图像模板

首先创建模板,创建各个模板要无损失旋转

https://cloud.tencent.com/developer/article/1798209
图像旋转:getRotationMatrix2D详解–无损失旋转图片

https://blog.csdn.net/qq_37781464/article/details/109513518
基于C++版的OpenCV图像旋转无缺失算法实现(必看)

//创建各个模板图像(无损失旋转和根据尺度放大缩小的图像)
//输入原图,输入角度,返回旋转、尺度变换后的图像
Mat createOneTemplateImg(Mat image, double angle, double scale)
{
//放大模板图背景(模板图都是正矩形),根据长宽求其对角线长度,再根据对角线长度制作背景图
//求原始正矩形图像的宽高
float w = image.cols;
float h = image.rows;
//求对角线长度,宽高
float L = sqrt(w*w + h*h);
int WH = (L + 2)*scale;//新的背景图大小应该是整型的,+2使其对角线长度不会有太大损失,同时大小也要按照比例缩放
Mat newImg= Mat::zeros(Size(WH, WH), image.type());//创建一个背景图像,类型和原图像一样,但大小有变化
Point2f pt = Point2f(w/2, h/2);//原图像的中心点
Mat M = getRotationMatrix2D(pt, angle, scale);//获得旋转矩阵,以原图的中心为原点旋转,角度,尺度不变
//对M矩阵再进行平移,平移到新的背景图的中心
//变换矩阵的中心点相当于平移一样 原图像的中心点与新图像的中心点的相对位置
M.at<double>(0, 2) += (WH - w) / 2.;
M.at<double>(1, 2) += (WH - h) / 2.;
warpAffine(image, newImg, M, Size(WH, WH));
return newImg;
}

创建效果如下,
让其创建的背景图像大小都一样都是正方形黑色背景图
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

可以创建进行旋转、缩放的图像模板

(2)创建单个无损失的旋转、放大图像模板及返回对应mask掩膜(更新)

//创建各个模板图像(无损失旋转和根据尺度放大缩小的图像)
//输入原图,输入角度,输出旋转、尺度变换后的图像,及其对应的mask掩膜
void createOneTemplateImg(Mat &image, double angle, double scale,Mat &templateImg,Mat &templateMask)
{
//创建一个初始mask图像(前景为白色,大小和输入的模板图一样)
Mat mask = Mat(image.size(), image.type(),Scalar(255, 255, 255));
//放大模板图背景(模板图都是正矩形),根据长宽求其对角线长度,再根据对角线长度制作背景图
//求原始正矩形图像的宽高
float w = image.cols;
float h = image.rows;
//求对角线长度,宽高
float L = sqrt(w*w + h*h);
int WH = (L + 2)*scale;//新的背景图大小应该是整型的,+2使其对角线长度不会有太大损失,同时大小也要按照比例缩放
templateImg = Mat::zeros(Size(WH, WH), image.type());//创建一个模板背景图像,类型和原图像一样,但大小有变化
templateMask = Mat::zeros(Size(WH, WH), image.type());//创建一个mask背景图像,
Point2f pt = Point2f(w/2, h/2);//原图像的中心点
Mat M = getRotationMatrix2D(pt, angle, scale);//获得旋转矩阵,以原图的中心为原点旋转,角度,尺度不变
//对M矩阵再进行平移,平移到新的背景图的中心
//变换矩阵的中心点相当于平移一样 原图像的中心点与新图像的中心点的相对位置
M.at<double>(0, 2) += (WH - w) / 2.;
M.at<double>(1, 2) += (WH - h) / 2.;
warpAffine(image, templateImg, M, Size(WH, WH));//输出生成的模板图像
warpAffine(mask, templateMask, M, Size(WH, WH));//输出生成的掩膜图像
}

输入图像可以是单通道的,也可以是多通道的,都能得到对应结果的模板图像,其对应的mask也相应的得到了,后面可以根据掩膜进行带掩膜的模板匹配。
在这里插入图片描述

(2)创建单个无损失的旋转、放大图像模板及返回对应mask掩膜(更新2重要)

此时不采用直接创建正方形背景的方式进行,而是使用旋转后的最小外接矩形进行创建,这样计算时既不会有损失也不会过度计算。
结果会更精确些,速度可能更快些

经过测试准确度提升极快,并且在增加金字塔层级后准确度依然在,速度明显提升

//创建各个模板图像(无损失旋转和根据尺度放大缩小的图像)
//输入原始单通道图、输入角度,返回当前模板信息(旋转、尺度变换后的图像,及其mask对应的矩形框、角度、尺度)
//这里的掩膜没有必要是白色矩形(是否后续改进其为其他形状)(不进行更改)
//
Template_Info LzcMatch::createOneTemplateImg(Mat &image, double angle, double scale)
{
/*
对旋转的进行改进,由于图形是一个矩形,旋转后的新图像的形状是一个原图像的外接矩形
因此需要重新计算出旋转后的图形的宽和高,不采用最大正方形模板
*/
//创建一个初始mask图像(前景为白色,大小和输入的模板图一样)
Mat mask = Mat(image.size(), image.type(), Scalar(255, 255, 255));
//求原始正矩形图像的宽高
float width = image.cols;
float height = image.rows;

double radian = angle * CV_PI / 180.;//角度转换为弧度
double width_rotate = fabs(width*cos(radian)) + fabs(height*sin(radian));
double height_rotate = fabs(width*sin(radian)) + fabs(height*cos(radian));

Mat templateImg = Mat::zeros(Size(width_rotate, height_rotate), image.type());//创建一个模板背景图像,类型和原图像一样,但大小有变化
Mat templateMask = Mat::zeros(Size(width_rotate, height_rotate), image.type());//创建一个mask背景图像,
Point2f pt = Point2f(width / 2, height / 2);//原图像的中心点
Mat M = getRotationMatrix2D(pt, angle, scale);//获得旋转矩阵,以原图的中心为原点旋转,角度,尺度不变
//对M矩阵再进行平移,平移到新的背景图的中心
//变换矩阵的中心点相当于平移一样 原图像的中心点与新图像的中心点的相对位置
M.at<double>(0, 2) += (width_rotate - width) / 2.;
M.at<double>(1, 2) += (height_rotate - height) / 2.;
warpAffine(image, templateImg, M, Size(width_rotate, height_rotate));//输出生成的模板图像
warpAffine(mask, templateMask, M, Size(width_rotate, height_rotate));//输出生成的掩膜图像

//寻找生成的掩膜的四个顶点(通过轮廓最小外接矩形得到)
//寻找外侧包装轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(templateMask, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//此处只保留轮廓的拐点信息,此处会改变此im_bin2中的值
RotatedRect minRect = minAreaRect(contours[0]);  //计算当前的轮廓外接矩形(有且只有一个)

//寻找生成的模板图像的点集
//寻找外侧包装轮廓
vector<Point> templatePoints;
findNonZero(templateImg, templatePoints);//将其中的非零点保存

//保存该模板的状态信息
Template_Info template_info;
template_info.templateImg = templateImg; //生成的模板图像
template_info.templateMask = templateMask; //生成的模板掩膜
template_info.minRect = minRect; //模板mask最小外接矩形
template_info.angle = angle;//模板的创建时生成的角度
template_info.scale = scale;//模板的创建时生成的尺度
template_info.templatePoints = templatePoints;//模板图像的点集
return template_info;
}

(3)制作批量模板(根据角度、尺度范围步长,得到相应图像及掩膜)

//创建批量的模板
指定制作模板的角度范围和尺度范围
//vector<float> AngleRange = { 0, 360, 1 };//角度范围 (起始角度,终止角度,角度步长)(min<=angle<=max)
//vector<float> ScaleRange = { 1, 2, 0.5 };//尺度范围 (起始尺度,终止尺度,尺度步长)(min<=scale<=max)

//输入单个模板图像,输出制作好的批量模板
void createTemplateImgs(Mat &image, vector<float> AngleRange, vector<float> ScaleRange, vector<Mat> &templateImgS, vector<Mat> &templateMaskS) {
//读取角度范围及步长
float angleRangeMin = AngleRange[0];
float angleRangeMax = AngleRange[1];
float angleRangeStep = AngleRange[2];
//读取尺度范围及步长
float ScaleRangeMin = ScaleRange[0];
float ScaleRangeMax = ScaleRange[1];
float ScaleRangeStep = ScaleRange[2];

//循环创建旋转角度的图像
//创建旋转角度图像的个数为
int angleNum = (angleRangeMax - angleRangeMin) / angleRangeStep;
//cout << "angleNum:" << angleNum << endl;
//针对旋转后的角度图像再创建尺度变换的图像个数为
int ScaleNum = (ScaleRangeMax - ScaleRangeMin) / ScaleRangeStep;
//cout << "ScaleNum:" << ScaleNum << endl;
for (int i = 0; i <= angleNum; i++) {
//当前第i个旋转图像的旋转角度为
float angleCur = angleRangeMin + i * angleRangeStep;
//cout << "angleCur:" << angleCur << endl;
//循环创建尺度变换的图像
for (int j = 0; j <= ScaleNum;j++) {
//当前第j个尺度变换的尺度为
float ScaleCur = ScaleRangeMin + j * ScaleRangeStep;
//cout << "ScaleCur:" << ScaleCur << endl;
//创建角度、尺度变换的图像
Mat templateImg, templateMask;
createOneTemplateImg(image, angleCur, ScaleCur, templateImg, templateMask);
templateImgS.push_back(templateImg);
templateMaskS.push_back(templateMask);
}
}
}

创建前要进行范围定义如下

//指定制作模板的角度范围和尺度范围
vector<float> AngleRange = { 0, 360, 1 };//角度范围 (起始角度,终止角度,角度步长)(min<=angle<=max)
vector<float> ScaleRange = { 1, 1, 0.5 };//尺度范围 (起始尺度,终止尺度,尺度步长)(min<=scale<=max)

创建0-360个,范围大于等于、小于等于,步长为1时,共(360-0)/1+1个图像

只进行角度变换的效果如下
在这里插入图片描述
在这里插入图片描述

同时角度和尺度的变换也是可以的如下:
在这里插入图片描述
在这里插入图片描述

输入边缘的图像也可以创建边缘的模板
在这里插入图片描述
在这里插入图片描述

(4)更新制作单个模板

//定义一个结构存放每个模板的相关信息(创建时候的信息)
struct Template_Info {
//下方的所有位置都是相对于创建的正方形模板大小为基准的
Mat templateImg; //生成的模板图像
Mat templateMask; //生成的模板掩膜
RotatedRect minRect; //模板mask最小外接矩形
double angle;//模板的创建时生成的角度
double scale;//模板的创建时生成的尺度
};

//创建各个模板图像(无损失旋转和根据尺度放大缩小的图像)
//输入原始单通道图、输入角度,返回当前模板信息(旋转、尺度变换后的图像,及其mask对应的矩形框、角度、尺度)
//这里的掩膜没有必要是白色矩形(是否后续改进其为其他形状)(不进行更改)
//
Template_Info createOneTemplateImg(Mat &image, double angle, double scale)
{
//创建一个初始mask图像(前景为白色,大小和输入的模板图一样)
Mat mask = Mat(image.size(), image.type(),Scalar(255, 255, 255));
//放大模板图背景(模板图都是正矩形),根据长宽求其对角线长度,再根据对角线长度制作背景图
//求原始正矩形图像的宽高
float w = image.cols;
float h = image.rows;
//求对角线长度,宽高
float L = sqrt(w*w + h*h);
int WH = (L + 2)*scale;//新的背景图大小应该是整型的,+2使其对角线长度不会有太大损失,同时大小也要按照比例缩放
Mat templateImg = Mat::zeros(Size(WH, WH), image.type());//创建一个模板背景图像,类型和原图像一样,但大小有变化
Mat templateMask = Mat::zeros(Size(WH, WH), image.type());//创建一个mask背景图像,
Point2f pt = Point2f(w/2, h/2);//原图像的中心点
Mat M = getRotationMatrix2D(pt, angle, scale);//获得旋转矩阵,以原图的中心为原点旋转,角度,尺度不变
//对M矩阵再进行平移,平移到新的背景图的中心
//变换矩阵的中心点相当于平移一样 原图像的中心点与新图像的中心点的相对位置
M.at<double>(0, 2) += (WH - w) / 2.;
M.at<double>(1, 2) += (WH - h) / 2.;
warpAffine(image, templateImg, M, Size(WH, WH));//输出生成的模板图像
warpAffine(mask, templateMask, M, Size(WH, WH));//输出生成的掩膜图像

//寻找生成的掩膜的四个顶点(通过轮廓最小外接矩形得到)
//寻找外侧包装轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(templateMask, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//此处只保留轮廓的拐点信息,此处会改变此im_bin2中的值
RotatedRect minRect = minAreaRect(contours[0]);  //计算当前的轮廓外接矩形(有且只有一个)

//保存该模板的状态信息
Template_Info template_info;
template_info.templateImg = templateImg; //生成的模板图像
template_info.templateMask = templateMask; //生成的模板掩膜
template_info.minRect = minRect; //模板mask最小外接矩形
template_info.angle = angle;//模板的创建时生成的角度
template_info.scale = scale;//模板的创建时生成的尺度
return template_info;
}

(5)更新制作批量模板

//创建批量的模板
指定制作模板的角度范围和尺度范围
//vector<float> AngleRange = { 0, 360, 1 };//角度范围 (起始角度,终止角度,角度步长)(min<=angle<=max)
//vector<float> ScaleRange = { 1, 2, 0.5 };//尺度范围 (起始尺度,终止尺度,尺度步长)(min<=scale<=max)

//输入单个模板图像,输出制作好的批量模板
vector<Template_Info> createTemplateImgs(Mat &image, vector<float> AngleRange, vector<float> ScaleRange) {
//创建大量的模板信息
vector<Template_Info> template_infos;
//读取角度范围及步长
float angleRangeMin = AngleRange[0];
float angleRangeMax = AngleRange[1];
float angleRangeStep = AngleRange[2];
//读取尺度范围及步长
float ScaleRangeMin = ScaleRange[0];
float ScaleRangeMax = ScaleRange[1];
float ScaleRangeStep = ScaleRange[2];

//循环创建旋转角度的图像
//创建旋转角度图像的个数为
int angleNum = (angleRangeMax - angleRangeMin) / angleRangeStep;
//cout << "angleNum:" << angleNum << endl;
//针对旋转后的角度图像再创建尺度变换的图像个数为
int ScaleNum = (ScaleRangeMax - ScaleRangeMin) / ScaleRangeStep;
//cout << "ScaleNum:" << ScaleNum << endl;
for (int i = 0; i <= angleNum; i++) {
//当前第i个旋转图像的旋转角度为
float angleCur = angleRangeMin + i * angleRangeStep;
//cout << "angleCur:" << angleCur << endl;
//循环创建尺度变换的图像
for (int j = 0; j <= ScaleNum;j++) {
//当前第j个尺度变换的尺度为
float ScaleCur = ScaleRangeMin + j * ScaleRangeStep;
//cout << "ScaleCur:" << ScaleCur << endl;
//创建角度、尺度变换的图像
Template_Info template_info = createOneTemplateImg(image, angleCur, ScaleCur);
template_infos.push_back(template_info);
}
}
return template_infos;
}

(6)更新制作单个模板(金字塔层级创建,注意层级需要和匹配时的层级对应,两者必须一致)

单个模板匹配时得到的多个结果信息
输入待检测图像、模板信息、输入分数阈值,返回多个检测结果(旋转矩形、角度、尺度、分数)(坐标相对于待搜索图像)
vector<Match_Result> matchTemplate(Mat search,Template_Info template_info,float score_threshed,int PyramidLevel) {
vector<Match_Result> match_result;

//根据金字塔层级对半缩小图像待匹配图像、模板图像
//对模板图像和待检测图像分别进行图像金字塔下采样缩半(不使用pyrDown有问题,采用resize)
for (int i = 0; i < PyramidLevel; i++)
{
//缩小待匹配的图像
resize(search, search, Size(search.cols / 2, search.rows / 2));
}

Mat templateImg = template_info.templateImg;
//先单个匹配测试103(带掩膜的匹配,掩膜的创建())
int width = search.cols - templateImg.cols + 1;//与公式相同
int height = search.rows - templateImg.rows + 1;
Mat result(width, height, CV_32FC1);// 输出结果,必须是单通道32位浮点数,假设源图像WxH,模板图像wxh,则结果必须为W - w + 1, H - h + 1的大小。
matchTemplate(search, templateImg, result, TM_CCOEFF_NORMED); //归一化相关系数方法,结果-1~1
//下方的normalize不要执行,这样不同模板的匹配分数才可以归到同一标准下
normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());//归一化,将分值变为0-1之间

//循环读取result的结果,将大于某阈值的结果保存
//这里针对的都是单通道
for (int row = 0; row < result.rows; row++) {
for (int col = 0; col < result.cols; col++) {
float score = result.at<float>(row, col);//得到当前位置的分值(读取像素是先row后col,rect是先width后height)
//如果当前的分值大于阈值,返回相关结果
if (score > score_threshed) {

//计算当前阈值下的结果矩形中心(已经进行平移了在新的图像下)
Point result_center(template_info.minRect.center.x + col, template_info.minRect.center.y + row);
//创建一个新的旋转矩形(大小方向一致,只是中心坐标不同)
RotatedRect minRect;
minRect.center = result_center;
minRect.size = template_info.minRect.size;
minRect.angle = template_info.minRect.angle;

Match_Result one_match_result;
one_match_result.minRect = minRect;//先进行矩形框的获取,再更改其矩形中心位置
one_match_result.minRect.center = result_center;
one_match_result.score = score;
one_match_result.angle = template_info.angle;
match_result.push_back(one_match_result);//存当前模板匹配的多个结果
}

}
}

return match_result;
}

(7)保存模板点集(后续绘制)

2,匹配模板

(1)先测试单个模板

先进行角度的匹配测试,在前面的创建的360个角度图像中选取一个和待搜索图像角度差不多的图像,如下选择下方的103模板图像与搜索图像中框选的那个匹配测试。使用OpenCV的带掩膜的NCC匹配方法。
在这里插入图片描述

如上测试失败,原因是此掩膜对应了

不能先创建灰度的模板再边缘化再匹配,或者直接灰度的模板转的掩膜,最后结果不对,先创建灰度的模板在边缘化结果如下,灰度的图像边缘也被边缘化了,此特征是失败的。

在这里插入图片描述
在这里插入图片描述

要先对原始图像边缘化再创建模板
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

发现加入掩膜后匹配的结果是不如不加掩膜匹配的准确的。
不加入矩形的mask直接使用此进行匹配
在这里插入图片描述

匹配的结果是准确的。
在这里插入图片描述

注意匹配完成时需要归一化的,这样分数才在0-1之间,否则分值有负值
在这里插入图片描述

基于灰度的效果比之前加入mask要好一些,但还是不够准确,故决定只进行边缘的匹配。
Mask作用只进行外接矩形的的框选,进行NCC匹配只匹配边缘特征,不加入mask判断。

(2)单个模板匹配得到多个结果框根据匹配分数

这里选取生成的第103个旋转角度的canny图像与canny后的搜索图像进行匹配,得到结果如下:

在这里插入图片描述
在这里插入图片描述

发现有达到1的结果,再测试其他的几个角度看看如45度,每个模板归一化后是否都有个1的分数,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

如上所示确实每个模板匹配后都会得到一个结果,无论该结果是否与真实的结果匹配准确,且对其归一化后必然会有一个1的值。

若先不进行归一化看两个角度103和45匹配的分值是多少?
当45度时,其最大匹配结果如下
在这里插入图片描述
在这里插入图片描述

当103度时,其最大匹配结果如下
在这里插入图片描述
在这里插入图片描述

如上直接归一化相关系数匹配的结果最大的不同模板的是有差异的,而进行normalize后每个模板的结果都有最大值1,直接让两个匹配结果没有差异了,所以此步骤在此处不需要,只需要按照归一化相关系数的大体阈值进行设置就行了。

很明显归一化的位置错误了,或者说,不要在单个归一化相关系数匹配之后立即再进行归一化normalize,normalize的结果只会得到一个1的值,所以去除此行。
在这里插入图片描述

此时发现输出了多个结果,如0.93,1.0的结果,那么这个时候需要进行nms非极大值抑制,非极大值抑制是需要应用到多个模板匹配的多个信息结果上的。

此时不同的模板匹配的结果分数可以在同一标准的进行判断区分了,即可以将多个模板匹配的结果信息放在一起进行处理了,如进行nms根据分数去除冗余框。那么,还需要进行nms,将整个图像的返回框的结果进行冗余的去除

(3)多个模板匹配的结果测试

上方将多个模板匹配的结果信息放在一起进行处理了,如进行nms根据分数去除冗余框。那么,还需要进行nms,将整个图像的返回框的结果进行冗余的去除。下方是0.33匹配分数的结果
在这里插入图片描述

将分数阈值设为0.3时最下面的框也得到了,虽然很少,上方的文字是自己打印的文字。接下来就是对其进行nms处理以此得到一个框。
在这里插入图片描述

//多个模板匹配时得到的多个匹配结果信息
//返回的依然是vector<Match_Result>,这是将所有的模板匹配的结果放在同一个标准下了,后续可以进行nms
//先得到分数大于某个值的所有结果框,再对所有结果框进行nms,输出nms后的结果框
输入待检测图像、模板信息、输入分数阈值,返回多个检测结果(旋转矩形、角度、尺度、分数)(坐标相对于待搜索图像)
vector<Match_Result> matchTemplateS(Mat &search, vector<Template_Info> &template_infos, float score_threshed) {
vector<Match_Result> match_result_s;//存储所有模板匹配得到的所有结果信息

//循环匹配得到结果(即生成的360个模板全部匹配得到结果)
for (int i = 0; i < template_infos.size(); i++) {
vector<Match_Result> match_result = matchTemplate(search, template_infos[i], score_threshed);

//每个结果信息都insert拼接到match_result_s后面
match_result_s.insert(match_result_s.end(), match_result.begin(), match_result.end());
}

return match_result_s;
}

(4)nms(重合度阈值IOU)

Nms用来对多个重合框进行多余的去除,有个阈值进行重合度判断,在此重合度之上且只保留分数最大的框。
Nms的方法有多种,相关原理可以看下方
https://zhuanlan.zhihu.com/p/392740337
NMS(非极大值抑制)总结

https://blog.csdn.net/qq_35054151/article/details/113812581
c++&opencv实现nms

https://mp.weixin.qq.com/s?__biz=MzI5MDUyMDIxNA==&mid=2247488685&idx=1&sn=73c1274ae8801c6cfd21fd8e72a0206e&chksm=ec1ff954db68704227272a6a563d4df7577bd281490d3605a63444f657e126f980c48c82af87&mpshare=1&scene=23&srcid=#rd
倾斜NMS(INMS)

将正外接矩形的nms改成旋转框的nms

int_pts = cv2.rotatedRectangleIntersection(r1, r2)[1] #求两个旋转矩形的交集,并返回相交的点集合
            if int_pts is not None:
                order_pts = cv2.convexHull(int_pts, returnPoints=True) #求点集的凸边形
                int_area = cv2.contourArea(order_pts)  #计算当前点集合组成的凸边形的面积
                inter = int_area * 1.0 / (area_r1 + area_r2 - int_area + 0.0000001)

注意两个旋转矩形的交集返回的点集是无序的不能直接当做轮廓求其面积,要将其求凸包后再求区域面积

//nms筛选分数框(采用传统的nms而非softnms)
//输入输出结果信息(该结果信息被更改),输入IOU阈值百分比
void NMS_Match_Result(vector<Match_Result> &match_result_s, float iou_threshold) {
if (match_result_s.empty()) {
return;
}
// 1.之前需要按照score按照由大到小排序所有结果信息
std::sort(match_result_s.begin(), match_result_s.end(), [&](Match_Result b1, Match_Result b2) {return b1.score > b2.score; });
// 2.先求出所有match_result_s自己旋转矩形框的大小面积
std::vector<float> area(match_result_s.size());
for (int i = 0; i < match_result_s.size(); ++i) {
area[i] = match_result_s[i].minRect.size.area();
}
// 3.循环去除vector中的多余框的结果
for (int i = 0; i < match_result_s.size(); ++i) {
for (int j = i + 1; j < match_result_s.size(); ) {
//求i和j的旋转矩形的重合区域面积
vector<Point> pnts;//返回点集为无序,不能直接按照轮廓求面积,需求其凸包再求面积
rotatedRectangleIntersection(match_result_s[i].minRect, match_result_s[j].minRect, pnts);
//如果交集大于0就计算其交集的面积,否则交集置为0
float iou = 0;//默认无交集
if (pnts.size() > 0) {
vector<Point> contour_hull(pnts.size());//凸包点集
convexHull(Mat(pnts), contour_hull, true);//输入二维点集,输出凸包
float u_area = contourArea(contour_hull);//求凸包面积为IOU面积
iou = (u_area) / (area[i] + area[j] - u_area);//求两个旋转矩形框的重合区域的百分比
}

//若重合区域百分比都在某个阈值之上,只保留其匹配分数最大的那个结果信息。
if (iou >= iou_threshold) {
match_result_s.erase(match_result_s.begin() + j);
area.erase(area.begin() + j);
}
else {
++j;
}
}
}
}
//多个模板匹配时得到的多个匹配结果信息
//返回的依然是vector<Match_Result>,这是将所有的模板匹配的结果放在同一个标准下了,后续可以进行nms
//先得到分数大于某个值的所有结果框,再对所有结果框进行nms,输出nms后的结果框
输入待检测图像、模板信息、输入匹配分数阈值、输入iou阈值,返回多个检测结果(旋转矩形、角度、尺度、分数)(坐标相对于待搜索图像)
vector<Match_Result> matchTemplateS(Mat &search, vector<Template_Info> &template_infos, float score_threshed, float iou_threshold) {
vector<Match_Result> match_result_s;//存储所有模板匹配得到的所有结果信息

//循环匹配得到结果(即生成的360个模板全部匹配得到结果)
for (int i = 0; i < template_infos.size(); i++) {
vector<Match_Result> match_result = matchTemplate(search, template_infos[i], score_threshed);
//每个结果信息都insert拼接到match_result_s后面
match_result_s.insert(match_result_s.end(), match_result.begin(), match_result.end());
}

//对match_result_s进行nms操作,去除多余重合的分数框
NMS_Match_Result(match_result_s,iou_threshold);

return match_result_s;
}

在这里插入图片描述

当加入nms后,设置重合度大于0.5的就将结果去除,部分结果如下,但是最下方的结果明显是有误的,与真实的结果反了,其余几个结果都挺准确的。
在这里插入图片描述

//单独测试166+180=346的角度图片,看其分数,分数才0.30比166度的0.31分数要小,
在这里插入图片描述
在这里插入图片描述

346度的结果和canny的不太一样。

所以提高精准度的方法是获取其主要及准确的特征信息,之前是采用canny的边缘信息包括外侧和内部的一些小圆圈,现在尝试使用一个轮廓看看效果

(5)贪婪度(暂时放弃,理解有误,只通过金字塔提速或者生成的模板个数)

贪婪度实际上是进行匹配时,只匹配百分之多少的模板,而不是生成的所有模板都进行匹配。如生成360度个旋转的模板,若贪婪度为0.5,则只匹配其中的180个模板(该180个模板旋转时是隔着一个选择一个,对称选择的),若贪婪度为1则全部匹配,贪婪度为0.2只匹配360*0.2=72的模板(360/72=5,每5个一组,中间每隔4个模板就要进行匹配)。
所以贪婪度会导致匹配的结果部分准确性可能有差,如贪婪度0.5,旋转角度会差1度的差,而贪婪度0.5时角度会有4度的差。

//多个模板匹配时得到的多个匹配结果信息
//返回的依然是vector<Match_Result>,这是将所有的模板匹配的结果放在同一个标准下了,后续可以进行nms
//先得到分数大于某个值的所有结果框,再对所有结果框进行nms,输出nms后的结果框
//输入待检测图像、模板信息、匹配分数阈值、iou阈值、贪婪度greediness(百分比整数)、金字塔层级int PyramidLevel
//返回多个检测结果(旋转矩形、角度、尺度、分数)(坐标相对于待搜索图像)
vector<Match_Result> matchTemplateS(Mat &search, vector<Template_Info> &template_infos, float score_threshed, float iou_threshold,float greediness) {
vector<Match_Result> match_result_s;//存储所有模板匹配得到的所有结果信息
int num_space = 1 / greediness;//得到要间隔多少个//贪婪度根据贪婪度进行多少个输入模板匹配

//循环匹配得到结果
for (int i = 0; i < template_infos.size(); i++) {
//判断间隔多少个模板再进行匹配操作
if (i%num_space == 0) {
vector<Match_Result> match_result = matchTemplate(search, template_infos[i], score_threshed);
//每个结果信息都insert拼接到match_result_s后面
match_result_s.insert(match_result_s.end(), match_result.begin(), match_result.end());
cout << "angle:" << template_infos[i].angle << endl;
}
}

//对match_result_s进行nms操作,去除多余重合的分数框
NMS_Match_Result(match_result_s,iou_threshold);

return match_result_s;
}

上方贪婪度可以匹配进行间隔匹配,匹配的效果如下
在这里插入图片描述
在这里插入图片描述

再进行匹配时发现多了一个256的结果,该结果多了个256的框,去不掉

当贪婪度为0.6时为下,那个多的框就没了
在这里插入图片描述
在这里插入图片描述

(6)金字塔层级

使用金字塔对模板图像及待匹配的图像进行下采样,先下采样后再进行匹配,此时可以两个图像都缩小了,那么可以增加匹配的速度,然后再对下采样后的匹配结果进行重构(如矩形框位置及大小同时平移放大)。

https://blog.csdn.net/lyc_daniel/article/details/114878102
下采样的方法pyrDown可能会报错,所以采用其他的方法缩小图像,如resize缩小图像后再匹配。
金字塔层级最多0-7个等级,即为0时不缩小,为1缩小一半,为2在1的基础上再缩小1半。

匹配模板(角度或尺度的范围、步长,及金字塔层级)

进行匹配的时候将其先金字塔缩小、再匹配提高匹配速度,金字塔层级最高8层(0-7)即可,匹配分数阈值设定。

先创建
金字塔缩放的效果大致如下,缩放2层后匹配结果,将其绘制原图上如下,还需要等比例将旋转框的结果返还回去
在这里插入图片描述

还原后的匹配结果如下
在这里插入图片描述
在这里插入图片描述

发现多了个框217和38,重合度太高,再低一些看看,低到0.1后结果如下,即只要重合度大于0.1的只保留得分最高的,此时结果没问题了
在这里插入图片描述

金子它层级可否在进行模板创建的时候就进行,而在匹配的时候只是将匹配结果返回原状的操作,这样的话,其每张匹配时的速度就增加了,不会重复创建金字塔模板,重复创建金字塔模板的时间就没了,只有多个匹配的时间。

//故进行运行时,匹配时的金字塔参数更改,创建模板的金字塔参数也必须更改

下两图为创建金字塔模板在匹配算法中的结果对比
左侧图为金字塔层级2(160120),右侧为金字塔层级0(640480)
在这里插入图片描述

再将匹配算法中的模板金字塔创建的部分拿出来对比
时间
在这里插入图片描述

(6-1)更新匹配金字塔层级
// 多个模板匹配时得到的多个匹配结果信息
//返回的依然是vector<Match_Result>,这是将所有的模板匹配的结果放在同一个标准下了,后续可以进行nms
//先得到分数大于某个值的所有结果框,再对所有结果框进行nms,输出nms后的结果框
//输入待检测图像、模板信息、匹配分数阈值、iou阈值、金字塔层级int PyramidLevel(0-7)
//返回多个检测结果(旋转矩形、角度、尺度、分数)(坐标相对于待搜索图像)
vector<Match_Result> matchTemplateS(Mat search, vector<Template_Info> template_infos, float score_threshed, float iou_threshold, int PyramidLevel) {
vector<Match_Result> match_result_s;//存储所有模板匹配得到的所有结果信息
//根据金字塔层级对半缩小图像待匹配图像、模板图像
//对模板图像和待检测图像分别进行图像金字塔下采样缩半(不使用pyrDown有问题,采用resize)
for (int i = 0; i < PyramidLevel; i++)
{
//先缩小待匹配的图像
resize(search, search, Size(search.cols / 2, search.rows / 2));
再循环缩小生成的所有模板图像、mask、及其旋转矩形
//for (int i = 0; i < template_infos.size(); i++) {
//resize(template_infos[i].templateImg, template_infos[i].templateImg, Size(template_infos[i].templateImg.cols / 2, template_infos[i].templateImg.rows / 2));
////resize(template_infos[i].templateMask, template_infos[i].templateMask, Size(template_infos[i].templateMask.cols / 2, template_infos[i].templateMask.rows / 2));
////等比例缩小旋转矩形大小及其坐标
//template_infos[i].minRect.center.x = template_infos[i].minRect.center.x / 2;
//template_infos[i].minRect.center.y = template_infos[i].minRect.center.y / 2;
//template_infos[i].minRect.size.width = template_infos[i].minRect.size.width / 2;
//template_infos[i].minRect.size.height = template_infos[i].minRect.size.height / 2;
//}

}

//循环匹配得到结果(所有生成的模板都进行匹配)
for (int i = 0; i < template_infos.size(); i++) {
vector<Match_Result> match_result = matchTemplate(search, template_infos[i], score_threshed);
//每个结果信息都insert拼接到match_result_s后面
match_result_s.insert(match_result_s.end(), match_result.begin(), match_result.end());
}

//对match_result_s进行nms操作,去除多余重合的分数框
NMS_Match_Result(match_result_s, iou_threshold);
//cout<<" match_result_s.size():"<< match_result_s.size() <<endl;

//根据金字塔层级还原输出结果检测框的位置及大小
for (int i = 0; i < PyramidLevel; i++)
{
//再循环缩小生成的所有模板图像、mask、及其旋转矩形
for (int i = 0; i < match_result_s.size(); i++) {
//等比例缩小旋转矩形大小及其坐标
match_result_s[i].minRect.center.x = match_result_s[i].minRect.center.x * 2;
match_result_s[i].minRect.center.y = match_result_s[i].minRect.center.y * 2;
match_result_s[i].minRect.size.width = match_result_s[i].minRect.size.width * 2;
match_result_s[i].minRect.size.height = match_result_s[i].minRect.size.height * 2;
}

}

return match_result_s;
}

(7)绘制功能

将结果信息绘制出来,主要绘制最小外接矩形、中心点

加入绘制箭头功能(0-360,水平逆时针旋转)

https://blog.csdn.net/MySunshine07/article/details/88936049
已知某一点坐标、线段长度和旋转角度,求另一点坐标

//绘制箭头
//输入图像、起始点、角度、距离
void drawArrow(Mat &image,Point pointStar, float angle, float dis)
{
//根据起点、方向、距离计算终点
float angle_rad = (angle+90) * CV_PI / 180;//计算弧度
float a = dis * sin(angle_rad);
float b = dis * cos(angle_rad);
Point pointEnd(pointStar.x + a, pointStar.y + b);

//绘制箭头函数
arrowedLine(image,pointStar, pointEnd, Scalar(0, 255, 255), 2, 8);
}

//绘制匹配的单个结果信息
//输入图像、单个结果,绘制相关信息
void drawMatchResult(Mat &image, Match_Result match_result) {
//绘制旋转矩形
Point2f vertex[4];//用于存放最小矩形的四个顶点
match_result.minRect.points(vertex);//返回矩形的四个顶点给vertex
for (int j = 0; j < 4; j++)
{
line(image, vertex[j], vertex[(j + 1) % 4], Scalar(0, 255, 0), 1, 8);  //绘制最小外接矩形每条边
}
//绘制结果的中心点,采用红色(注意此为矩形的中心)
circle(image, match_result.minRect.center, 4, Scalar(100, 0, 255), -1);
//求最小外接矩形的正外接矩形(方便使用此绘制文字)
Rect brect = match_result.minRect.boundingRect();
//rectangle(image, brect, Scalar(255, 0, 0), 2);

//绘制箭头表示方向
drawArrow(image, match_result.minRect.center, match_result.angle, 40);

//绘制文字信息,匹配的结果分数,角度
putText(image, format("score:%.2f, angle:%.2f", match_result.score, match_result.angle), Point(brect.x, brect.y), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 255, 0), 1);
cout << "score:" << match_result.score << ", angle:" << match_result.angle << endl;
}

(8)带掩膜的模板匹配

使用带掩膜的模板匹配时,会得到inf的分数,必须将此种的也要去掉

并且使用带矩形的掩膜的精度虽然高了后,但是耗时也变长了,时间长的有点多,所以还是直接不用掩膜进行测试

带掩膜的会更精确些,那么速度增加的话再增加金字塔,不就能更精确些了?经过测试效果不好,精度和之前差不多,但速度增加的几倍

那么创作模板的时候背景扩大时不能用整个正方形扩大,采用刚好扩大即可

3,基于各种特征的匹配

前面的匹配算法基本上完成了,接下来就是对什么样的特征进行匹配,如之前主要进行基于边缘的匹配

(1)基于边缘的模板匹配

基于边缘的匹配主要是模板及待搜索图像在创建模板前就进行相关边缘特征的提取,使其两个图像在同一个处理结果下。

可以使用模板的一部分特征进行匹配,如下只将模板的最外侧轮廓作为特征创建多个模板并进行匹配,也能得到结果
在这里插入图片描述
在这里插入图片描述

将图像通过金字塔缩小到160*120大小后,360个旋转的模板匹配时间仅需要170ms左右。

左侧图为金字塔层级2(160*120)

(1-2)多目标匹配图片及速度测试

在这里插入图片描述
在这里插入图片描述

使用另外的图片测试,测试结果如下,结果都匹配出来了就是速度有些慢
在这里插入图片描述
在这里插入图片描述

采用金字塔等级2时匹配结果

上面的匹配结果是没有问题的,但是匹配的速度确实有些慢,后续提升相关匹配速度。
生成模板时按照2步生成180个模板匹配看其速度,匹配速度明显提升了,如下
在这里插入图片描述
在这里插入图片描述

提高得分阈值应该也会提升速度,如下
在这里插入图片描述
在这里插入图片描述

金字塔等级4时提升的速度最明显,221ms
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(1-3)折叠图片及速度测试

在这里插入图片描述
在这里插入图片描述

提取的模板边缘选择外侧轮廓作为匹配的特征,而待检测图像的边缘就是canny直接的边缘。

当相关参数设置如下,能准确的匹配到结果如下,但耗时有点长
在这里插入图片描述
在这里插入图片描述

使用金字塔,增加层级,设置相关参数如下
在这里插入图片描述

很明显匹配速度提升了,只需要0.079ms即可
在这里插入图片描述

至此所有的相关算法已经完成了,接下来就是对个人算法的封装等操作了。制作一个类文件,以后的匹配采用此。

基于边缘的模板匹配已经满足大部分需求了,关键是前面的模板边缘的提取,可以手动制作去除一些不必要的点之类的。

4,加速方法

加速的方法有多种,在其余条件不变的情况下,0-360模板创建使用步长2就能只匹配180个,提速一倍

当使用金子塔时,每增加一级就能提速一倍。

使用openmp自动分配进程计算时,每多一个核,速度可提升一倍

改进想法

前面创建对应的模板掩膜时直接采用了矩形的掩膜(是否有必要?),后续是否可以将其掩膜创建时为二值化(这样掩膜面积大大缩小,匹配的效果是否更快、或者精确?后续测试)

单个模板背景还是使用其原始的外接正矩形来做,这样匹配时结果会更精确些。

先进行高层金字塔的粗匹配(粗定位),再使用粗定位后的结果进行精确定位?


原文地址:https://blog.csdn.net/qq_44870829/article/details/144248846

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!