前言
本项目是机器视觉课程的大作业,写的并不是很明晰,如有问题欢迎提出。
介绍
本项目为基于OpenCV的火花塞间隙尺寸测量,主要是运用模板匹配技术定位火花塞间隙尺寸,然后寻找火花塞间隙并测量其尺寸。
算法说明
程序主要分成四个部分:
预处理:
在此部分中,将完成模板的创建和待处理图片的路径读取。程序将从文件夹中读取model.png图片作为模板。如果文件夹中没有该图片,则将从第一张待处理图片中截取所需区域作为模板。
匹配:
此函数主要调用OpenCV库中的matchTemplate函数以及相关函数寻找各种角度的图片中与模板的最佳匹配点,并且根据该点找寻区域的中心位置。模板匹配算法根据需要决定,本项目使用平均方差。
旋转图像
为了匹配旋转的图像,所以在匹配时要将图像进行旋转。
旋转使用的是仿射变换,仿射矩阵可用getRotationMatrix2D()得到M1。
由于在旋转图像后,图像的大小也会改变,所以不能直接使用M1进行变换,而是要用getAffineTransform()函数再获得一个大小变换矩阵M2
最后将M1的旋转部分和M2的大小变换部分相结合变成一个新的变换矩阵M,使用warpAffine()函数就能完成图像的仿射变换。
由于代码需要,还需要写坐标变换的函数,公式如下
M就是一个3*2的矩阵
图像金字塔
如果单纯使用模板匹配,时间复杂度将爆炸,但我们可以使用图像金字塔来进行优化
-
建立n层图像金字塔,对于每一层金字塔,将图像向下采样,每 2 n 2^n 2n行 2 n 2^n 2n列取一个像素。由此获得 1 2 n \frac{1}{2^n} 2n1倍的图像和模板。采样完后的图像可用高斯滤波器滤波(本项目没有)
-
从高层开始将该层模板对该层图像进行模板匹配,所有匹配度高于某一阈值的点都将用一个矩形框住,并记录下旋转角度变换范围。
-
下一层在上一层截取的区域和角度中进行模板匹配,依次类推逐渐获得一个比较小的匹配区域和角度范围。
-
对n-1层图像金字塔依次进行匹配后,在源图像中找到得到的区域,并在相应角度范围中进行最终的模板匹配,获得中心点的位置。
尺寸计算:
当找到匹配区域的中心点位置后,垂直向下寻找火花塞间隙。将图片变换成灰度图之后,就可以发现向下直线上的两处灰度值突变的点便是间隙边缘上的点,计算两点间距离就是火花塞间隙的开度。
绘制输出
测量处相关函数之后就可以在图片上绘制测量区域,并且显示开度的数值
代码实现
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace std;
其他函数///
void Min(int &a,int b)
{
if(a>b)a = b;
}
void Max(int &a,int b)
{
if(a<b)a = b;
}
Point GetCenter(int x,int y,int w,int h)
{
return Point(x+w/2,y+h/2);
}
/*
获取仿射矩阵
*/
Mat GetMatrix(int cols,int rows,int newCols,int newRows,int degree)
{
Point2f center(cols / 2, rows/2);//中心
Mat M1 = getRotationMatrix2D(center, degree, 1);//计算旋转的仿射变换矩阵
Point2f srcPoints1[3];
Point2f dstPoints1[3];
srcPoints1[0] = Point2i(0, 0);
srcPoints1[1] = Point2i(0, rows);
srcPoints1[2] = Point2i(cols, 0);
dstPoints1[0] = Point2i((newCols - cols)/2 , (newRows - rows)/2);
dstPoints1[1] = Point2i((newCols - cols)/2 , (newRows + rows)/2);
dstPoints1[2] = Point2i((newCols + cols)/2, (newRows - rows)/2);
Mat M2 = getAffineTransform(srcPoints1, dstPoints1);
M1.at<double>(0, 2) = M1.at<double>(0, 2) + M2.at<double>(0, 2);
M1.at<double>(1, 2) = M1.at<double>(1, 2) + M2.at<double>(1, 2);
return M1;
}
//旋转图像内容不变,尺寸相应变大
Mat Rotate(Mat src, int degree)
{
double angle = degree * CV_PI / 180.;
double a = sin(angle), b = cos(angle);
int rows=src.rows;
int cols=src.cols;
//旋转后的新图尺寸
int width_rotate= int(rows * fabs(a) + cols * fabs(b));
int height_rotate=int(cols * fabs(a) + rows * fabs(b));
Mat M = GetMatrix(cols,rows,width_rotate,height_rotate,degree);
Mat dst= Mat::zeros(width_rotate, height_rotate, src.type());
warpAffine(src, dst, M, Size(width_rotate, height_rotate));//仿射变换
return dst;
}
/*
坐标变换
*/
void TransCoor(Point &point,int degree,int rows,int cols,bool dir = 1)
{
double angle = degree * CV_PI / 180.;
double a = sin(angle), b = cos(angle);
//旋转后的新图尺寸
int width_rotate= ceil(rows * fabs(a) + cols * fabs(b));
int height_rotate=ceil(cols * fabs(a) + rows * fabs(b));
Mat M;
if(dir == 0)//顺时针
{
M = GetMatrix(cols,rows,width_rotate,height_rotate,degree);
}
else //逆时针
{
M = GetMatrix(width_rotate,height_rotate,cols,rows,-degree);
}
point = Point(point.x*M.at<double>(0, 0)+point.y*M.at<double>(0,1)+M.at<double>(0,2),
point.x*M.at<double>(1, 0)+point.y*M.at<double>(1, 1)+M.at<double>(1,2));
}
int level = 3;//金字塔层级
int degreeMin,degreeMax;//旋转范围
int degree;//最终的旋转角度
/*
在ROI图像中匹配模板model
*/
Point Match(Mat src,Mat model)
{
Point temLoc;//最佳匹配点
double Min = 1;
for(int d = degreeMin;d<=degreeMax;d++)//旋转角度
{
Mat img = Rotate(src,d);
int row = img.rows;
int col = img.cols;
Mat result(col, row, CV_32FC1);
if(row < model.rows || col < model.cols)continue;
matchTemplate(img, model, result, TM_SQDIFF_NORMED);//匹配模板
//normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());//归一化
Point minLoc;
Point maxLoc;
double min, max;
//找到最匹配点(函数说明:在一个数组中找到全局最小值和全局最大值)
minMaxLoc(result, &min, &max, &minLoc, &maxLoc, Mat());
if(min<Min)
{
temLoc = minLoc;//TM_SQDIFF_NORMED是最小值
degree = d;//最佳匹配角度
Min = min;
}
}
return GetCenter(temLoc.x, temLoc.y, model.cols, model.rows);
}
/*
获得第level层金字塔图像
*/
Mat GetPyramid(Mat src,int level)
{
int sample = 1<<level;//采样率为2^level
Mat dst = Mat::zeros(Size(src.cols/sample,src.rows/sample),src.type());
for(int i=0;i<dst.rows;i++)
for(int j=0;j<dst.cols;j++)
{
dst.at<Vec3b>(i,j) = src.at<Vec3b>(i*sample,j*sample);
}
return dst;
}
/*
缩小寻找范围
*/
void FindROI(Mat src,Rect &ROI,int level,Mat model)
{
Mat pyramidSrc = GetPyramid(src,level);
Mat pyramidModel = GetPyramid(model,level);
Point topLeftP = Point(pyramidSrc.cols,pyramidSrc.rows);
Point ButtonRightP = Point(0,0);
bool isMatch = false;
int dmin = 359,dmax = 0;
for(int d = degreeMin;d<=degreeMax;d+=10*level)//旋转角度
{
Mat img = Rotate(pyramidSrc,d);
int row = img.rows;
int col = img.cols;
if(row < pyramidModel.rows || col < pyramidModel.cols)continue;
Mat result(col, row, CV_32FC1);
matchTemplate(img, pyramidModel, result, TM_SQDIFF_NORMED);//匹配模板
Point minLoc;
Point maxLoc;
double min, max;
//找到最匹配点(函数说明:在一个数组中找到全局最小值和全局最大值)
minMaxLoc(result, &min, &max, &minLoc, &maxLoc, Mat());
if(min<0.1*level)
{
isMatch = true;
//匹配到的四个点
Point point[4] = {
minLoc,
Point(minLoc.x,minLoc.y+pyramidModel.rows),
Point(minLoc.x+pyramidModel.cols,minLoc.y),
Point(minLoc.x+pyramidModel.cols,minLoc.y+pyramidModel.rows)
};
//寻找左上角和右下角
for(int i = 0;i<4;i++)
{
TransCoor(point[i],d,pyramidSrc.rows,pyramidSrc.cols);
Min(topLeftP.x,point[i].x);
Min(topLeftP.y,point[i].y);
Max(ButtonRightP.x,point[i].x);
Max(ButtonRightP.y,point[i].y);
}
Min(dmin,d);
Max(dmax,d);
}
}
if(isMatch == false)return ;//如果没匹配到则返回
//更新感兴趣区域
topLeftP*=1<<level;
ButtonRightP*=1<<level;
Max(ROI.x,topLeftP.x+ROI.x);
Max(ROI.y,topLeftP.y+ROI.y);
Min(ROI.width,ButtonRightP.x-topLeftP.x);
Min(ROI.height,ButtonRightP.y-topLeftP.y);
Max(ROI.x,0);
Max(ROI.y,0);
Min(ROI.width,src.cols);
Min(ROI.height,src.rows);
degreeMax = dmax;
degreeMin = dmin;
}
Point FindTemplate(Mat src,Mat model)
{
Rect ROI = Rect(0,0,src.cols,src.rows);
degreeMin = 0,degreeMax = 359;
for(int i=level;i>=1;i--)
{
FindROI(src(ROI),ROI,i,model);
}
cout<<degreeMax<<" "<<degreeMin<<endl;
//原始图像中匹配
Point center = Match(src(ROI),model);
//还原中心坐标
TransCoor(center,degree,ROI.height,ROI.width);
center.x+=ROI.x;
center.y+=ROI.y;
TransCoor(center,degree,src.rows,src.cols,false);
return center;
}
/*
寻找直线端点
*/
void FindLinePoint(Mat src,Point startP,Point &firstP,Point &secondP,int &dis)
{
int y = startP.y;
Mat gray;
cvtColor(src,gray,COLOR_RGB2GRAY);
while(y<src.rows)
{
int deri = (int)gray.at<uchar>(y,startP.x)-
(int)gray.at<uchar>(y+1,startP.x);
if( deri < -65)//像素变大且跨度大于阈值则为第一个点
{
firstP = Point(startP.x,y+1);
}
if( deri > 65)//像素变小且跨度大于阈值则为第二个点
{
secondP = Point(startP.x,y+1);
break;
}
y++;
}
dis = abs(firstP.y-secondP.y);//计算尺寸
return ;
}
void Deal(Mat src,Mat model)
{
//寻找匹配到的区域的中心点
Point center = FindTemplate(src,model);
//寻找火花塞间隙的两个端点,并计算尺寸
Point firstP,secondP;
int dis;
FindLinePoint(Rotate(src,degree),center,firstP,secondP,dis);
// //标记并输出图像
Mat dst;
src.copyTo(dst);
// //将坐标变换为原图
TransCoor(center,degree,src.rows,src.cols);
TransCoor(firstP,degree,src.rows,src.cols);
TransCoor(secondP,degree,src.rows,src.cols);
circle(dst,center,3,Scalar(0,0,255));
line(dst,firstP,secondP,Scalar(0,0,255));
putText(dst,"d:"+to_string(dis),(secondP+firstP)/2+Point(10,0),
FONT_HERSHEY_COMPLEX,0.5,Scalar(0,0,255));
imshow("dst",dst);
}
int main()
{
string pattern_jpg;
vector<String> image_files;
pattern_jpg = ".\\img\\*.png";
glob(pattern_jpg, image_files);//读取图片路径
//创建模板
string mode_path = ".\\img\\model.png";
Mat model = imread(mode_path);//模板
if(model.empty())//读取不到图片
{
Mat img=imread(image_files[0]);
model = img(Rect(250,150,120,80));
}
//处理图片
for(int i=0;i<image_files.size();i++)
{
cout << image_files[i] << endl;
if(image_files[i]==".\\img\\model.png")continue;
Mat img=imread(image_files[i]);
Deal(img,model);
waitKey();
}
return 0;
}
实验结果
结论
在这个项目中我们学到了如何基于opencv库中的模板匹配实现待识别器件的定位,以及根据位置信息测量所需的参数。该技术可用于简单环境下的零件尺寸测量。由于需要旋转角度匹配时可以匹配旋转的图片,所以速度非常的慢。但使用图像金字塔进行优化后,速度提升非常之大。