基于OpenCV和图像金字塔的模板匹配——火花塞间隙尺寸测量

介绍

本项目为基于OpenCV的火花塞间隙尺寸测量,主要是运用模板匹配技术定位火花塞间隙尺寸,然后寻找火花塞间隙并测量其尺寸。

算法说明

流程图
程序主要分成四个部分:

预处理:

在此部分中,将完成模板的创建和待处理图片的路径读取。程序将从文件夹中读取model.png图片作为模板。如果文件夹中没有该图片,则将从第一张待处理图片中截取所需区域作为模板。

匹配:

此函数主要调用OpenCV库中的matchTemplate函数以及相关函数寻找各种角度的图片中与模板的最佳匹配点,并且根据该点找寻区域的中心位置。模板匹配算法根据需要决定,本项目使用平均方差。

旋转图像

为了匹配旋转的图像,所以在匹配时要将图像进行旋转。
旋转使用的是仿射变换,仿射矩阵可用getRotationMatrix2D()得到M1。
由于在旋转图像后,图像的大小也会改变,所以不能直接使用M1进行变换,而是要用getAffineTransform()函数再获得一个大小变换矩阵M2
最后将M1的旋转部分和M2的大小变换部分相结合变成一个新的变换矩阵M,使用warpAffine()函数就能完成图像的仿射变换。
由于代码需要,还需要写坐标变换的函数,公式如下
仿射变换
M就是一个3*2的矩阵

图像金字塔

如果单纯使用模板匹配,时间复杂度将爆炸,但我们可以使用图像金字塔来进行优化

  1. 建立n层图像金字塔,对于每一层金字塔,将图像向下采样,每$2^n$行$2^n$列取一个像素。由此获得$\frac{1}{2^n}$倍的图像和模板。采样完后的图像可用高斯滤波器滤波(本项目没有)

  2. 从高层开始将该层模板对该层图像进行模板匹配,所有匹配度高于某一阈值的点都将用一个矩形框住,并记录下旋转角度变换范围。

  3. 下一层在上一层截取的区域和角度中进行模板匹配,依次类推逐渐获得一个比较小的匹配区域和角度范围。

  4. 对n-1层图像金字塔依次进行匹配后,在源图像中找到得到的区域,并在相应角度范围中进行最终的模板匹配,获得中心点的位置。

尺寸计算:

当找到匹配区域的中心点位置后,垂直向下寻找火花塞间隙。将图片变换成灰度图之后,就可以发现向下直线上的两处灰度值突变的点便是间隙边缘上的点,计算两点间距离就是火花塞间隙的开度。

绘制输出

测量处相关函数之后就可以在图片上绘制测量区域,并且显示开度的数值

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
#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;
}

实验结果

匹配结果1
匹配结果2
匹配结果3

结论

在这个项目中我们学到了如何基于opencv库中的模板匹配实现待识别器件的定位,以及根据位置信息测量所需的参数。该技术可用于简单环境下的零件尺寸测量。由于需要旋转角度匹配时可以匹配旋转的图片,所以速度非常的慢。但使用图像金字塔进行优化后,速度提升非常之大。