提前说明一下:这是“禾路”老师博客上的一个例子,老师在51cto上有课程,大家如果需要可以去看一下http://edu.51cto.com/lecturer/8887491.html
本博文是参考老师的教程,自己消化理解之后进行了部分代码的改进,发表未经原作者允许,如果有侵犯版权请告知立马删除!
目标:检测以下相机没有拍摄好的答题卡:
第一步:定位点检测
从上图可以看到四个黑圆圈,这个就是定位用的四个角,我们检测这四个角就可以进行答题卡的定位:
方法一:利用霍夫圆变换,进行圆心的查找。
方法二:轮廓区域检测
方法三:模板匹配
方法四:特征检测匹配
本文利用方法三的模板匹配,其它方法完全可行的,如果不知道其它方法可以看看我的其它博文,都有例子。
--------->>>>模板匹配的黑点截取出来(照一张好的图片去测量截取)
上代码:此代码都是原作者发表过的,版权的代码不会发表
1 //--------------------------------注释代码部分为未用掩码操作--------------------------//
2 void FindAnchorPoint(const Mat& src,const Mat& matchMask,vector<Point2f>& anchorPoint)
3 {
4 Mat matchResult;
5 matchResult.create(Size(src.cols - matchMask.cols, src.rows - matchMask.rows), CV_16SC1);
6 //----模板匹配找四个定位点,同时得归一化(初始数据范围太大,自己通过image watch 查看)
7 matchTemplate(src, matchMask, matchResult, TM_CCOEFF, Mat());
8 normalize(matchResult, matchResult, 0, 1, NORM_MINMAX);
9 //----查找匹配的四个点,分成四个区域查找,因为一个区域没办法查找四个值
10 /*Mat topleft = matchResult(Rect(Point(0, 0), Point(matchResult.cols / 2, matchResult.rows / 2)));
11 Mat topright = matchResult(Rect(Point(matchResult.cols / 2, 0), Point(matchResult.cols, matchResult.rows / 2)));
12 Mat botleft = matchResult(Rect(Point(0, matchResult.rows / 2), Point(matchResult.cols / 2, matchResult.rows)));
13 Mat botright = matchResult(Rect(Point(matchResult.cols / 2, matchResult.rows / 2), Point(matchResult.cols , matchResult.rows)));*/
14 double maxValue[4] = { 0 }, minValue[4] = {0};
15 vector<Point2i> maxPoint(4), minPoint(4);
16 Mat topleftMask = Mat::zeros(matchResult.size(), CV_8UC1);
17 Mat toprightMask = Mat::zeros(matchResult.size(), CV_8UC1);
18 Mat botleftMask = Mat::zeros(matchResult.size(), CV_8UC1);
19 Mat botrightMask = Mat::zeros(matchResult.size(), CV_8UC1);
20 topleftMask(Rect(Point(0, 0), Point(matchResult.cols / 2, matchResult.rows / 2))).setTo(255);
21 toprightMask(Rect(Point(matchResult.cols / 2, 0), Point(matchResult.cols, matchResult.rows / 2))).setTo(255);
22 botleftMask(Rect(Point(0, matchResult.rows / 2), Point(matchResult.cols / 2, matchResult.rows))).setTo(255);
23 botrightMask(Rect(Point(matchResult.cols / 2, matchResult.rows / 2), Point(matchResult.cols, matchResult.rows))).setTo(255);
24 vector<Mat> vectorMask;//注意此处如果用vector<Mat> vectorMask(4);对应的下面写法是vectorMask[0]=topleftMask;
25 vectorMask.push_back(topleftMask);
26 vectorMask.push_back(toprightMask);
27 vectorMask.push_back(botleftMask);
28 vectorMask.push_back(botrightMask);
29 for (size_t i = 0; i < vectorMask.size(); i++)
30 {
31 minMaxLoc(matchResult, &minValue[i], &maxValue[i], &minPoint[i], &maxPoint[i], vectorMask[i]);
32 }
33 //minMaxLoc(topleft, &minValue[0], &maxValue[0], &minPoint[0], &maxPoint[0]);
34 //minMaxLoc(topright, &minValue[1], &maxValue[1], &minPoint[1], &maxPoint[1]);
35 //maxPoint[1].x = maxPoint[1].x + matchResult.cols / 2;
36 //minMaxLoc(botleft, &minValue[2], &maxValue[2], &minPoint[2], &maxPoint[2]);
37 //maxPoint[2].y = maxPoint[2].y + matchResult.rows / 2;
38 //minMaxLoc(botright, &minValue[3], &maxValue[3], &minPoint[3], &maxPoint[3]);
39 //maxPoint[3].x = maxPoint[3].x + matchResult.cols / 2;
40 //maxPoint[3].x = maxPoint[3].y + matchResult.rows / 2;
41 }
结果图片:
第二步:定位线检测
定位线:每一个涂卡区域都是由X、Y两个轴共同定位。
由以上的分析可知,我们这一步的操作是找到这些定位线,再由这些定位线去找每个涂卡区的坐标。
------>>>>>裁剪定位上下左右四个区域,利用投影算法找出定位线。
上代码:
1 //------------------------------------图像投影算法-----------------------------------------------//
2 //*************@src------------------输入矩阵为单通道********************************************//
3 //*************@leftUpJumpWave-------上升跳变沿存储**********************************************//
4 //*************@rightDownJumpWave----下降跳变沿存储**********************************************//
5 //*************@maxInterval----------允许高电平(像素)最大间隔,也可以说是允许的最大误差********//
6 //-----------------------------------------------------------------------------------------------//
7 void projectionAlgorithm(Mat src,vector<int>& UpJumpWave,vector<int>& DownJumpWave,bool Axis,int maxInterval)
8 {
9 vector<int> pixNum(src.rows > src.cols ? src.rows : src.cols);
10 //------对X、Y做直方图类似的投影,统计一行或者一列的非零个数--------//
11 if (Axis)
12 {
13 for (size_t i = 0; i < src.cols; i++)
14 {
15 Mat col = src.col(i);//一列数据
16 pixNum[i] = countNonZero(col) > 1 ? countNonZero(col) : 0;
17 }
18 }
19 else
20 {
21
22 for (size_t i = 0; i < src.rows; i++)
23 {
24 Mat row = src.row(i);//一行数据
25 pixNum[i] = countNonZero(row) > 1 ? countNonZero(row) : 0;
26 }
27 }
28 if (pixNum.size() < maxInterval) return;//防止有空洞(实际没见过,如果有的话那程序架构会奔溃了)
29 //-----对上面的数据进行二值化0-1,同时对于不满足maxInterval的数据进行剔除--------//
30 for (int k = 1; k < pixNum.size()-maxInterval; k++)//去除了第一个和最后一个像素
31 {
32 if (pixNum[k] > 0 && pixNum[k + maxInterval] > 0)
33 {
34 for (size_t j = k; j < k + maxInterval; j++)
35 {
36 pixNum[j] = 1;
37 }
38 k = k + maxInterval-1;
39 }
40 else
41 {
42 pixNum[k] = 0;
43 }
44 }
45 //----对跳变的电平进行存储,高->低,低->高,-----//
46 for (size_t i = 1 ; i < pixNum.size()-2; i++)//去除了第一个和最后一个像素
47 {
48 if (pixNum[i] == 0 && pixNum[i + 1] == 1) UpJumpWave.push_back(i);
49 if (pixNum[i] == 1 && pixNum[i + 1] == 0) DownJumpWave.push_back(i);
50 }
51 //----对得到的结果进行处理,定位点被误判----//
52 vector<int>::iterator begin = UpJumpWave.begin();
53 if (UpJumpWave[0] < 15) UpJumpWave.erase(begin);
54 vector<int>::iterator end = UpJumpWave.end()-1;
55 if (UpJumpWave[UpJumpWave.size()-1] > 330) UpJumpWave.erase(end);
56 }
第三步:检测涂卡区域的状态
这一步是我自己写的,没有参考别人程序,如果有错误的地方请不吝指教!
思路:找到检测的点,然后利用非零区域进行判断,想法很简单但是实现完全实现很多小技巧,具体看代码。
上代码:
1 //-----------------------------------------------------------------------------------//
2 //************************************检测涂卡区域函数***********************************//
3 void checkKeypoint(Mat& _src,vector<Point2f>& allPoint,vector<Point2f>& testkeyPoint)
4 {
5 Mat src = _src.clone();
6 Mat show = Mat::zeros(src.size(), CV_8UC3);
7 morphologyEx(src, src, MORPH_DILATE, Mat::ones(3, 3, CV_8UC1));
8 for (size_t i = 0; i < allPoint.size(); i++)
9 {
10 //------判断检测点的正方形涂卡区的非零个数---------//
11 if (allPoint[i].x == 0 || allPoint[i].y == 0)
12 {
13 allPoint[i].x += 1;
14 allPoint[i].y += 1;
15 }
16 Mat rec = src(Rect(static_cast<int>(allPoint[i].x - 1 ), static_cast<int>(allPoint[i].y - 1 ), 12, 5));
17 int count = countNonZero(rec);
18 if (count > 15)
19 {
20 testkeyPoint.push_back(allPoint[i]);
21 rectangle(show, Rect(allPoint[i], Point(allPoint[i].x + 13, allPoint[i].y + 5)), Scalar(0, 0, 255));
22 }
23 }
24 }
整体代码:(再次申明:核心是参考禾路老师的,细节处理和部分代码是自己加的,如有侵权请告知,立马删除)
1 #include <opencv2/opencv.hpp>
2 #include <iostream>
3 #include "math.h"
4 using namespace cv;
5 using namespace std;
6
7 #if 1
8 const bool X_Axis = true;
9 const bool Y_Axis = false;
10
11 void FindAnchorPoint(const Mat& src, const Mat& matchMask, vector<Point2f>& anchorPoint);
12 void projectionAlgorithm(Mat src, vector<int>& UpJumpWave, vector<int>& DownJumpWave, bool Axis, int maxInterval);
13 void checkKeypoint( Mat& _src, vector<Point2f>& allPoint, vector<Point2f>& testkeyPoint);
14 int main(int argc,char** argv)
15 {
16 //变量
17 //读取图片
18 Mat standImage = imread("SheetStand.jpg");
19 Mat perImage = imread("perspective3.bmp");
20 //Mat perImage = imread("perspective.jpg");
21 //Mat matchMask = imread("Circle.jpg");
22 //-----------生成模板图片R = 11
23 Mat matchMask;
24 matchMask.create(Size(24, 24), CV_8UC3);
25 matchMask.setTo(255);
26 circle(matchMask, Point(11, 11), 11, Scalar(0), -1);
27
28 resize(perImage, perImage, Size(600, 600));
29 vector<Point2f> stdAncherPoint(4);
30 vector<Point2f> perAncherPoint(4);
31 FindAnchorPoint(standImage, matchMask, stdAncherPoint);
32 FindAnchorPoint(perImage, matchMask, perAncherPoint);
33 Mat change = getPerspectiveTransform(perAncherPoint, stdAncherPoint);
34 Mat resultPerImage;
35 warpPerspective(perImage, resultPerImage, change, resultPerImage.size());
36 FindAnchorPoint(resultPerImage, matchMask, perAncherPoint);
37
38 Mat grayImage = resultPerImage;//.clone();
39 Mat show = resultPerImage.clone();
40 cvtColor(grayImage, grayImage, CV_BGR2GRAY);
41 threshold(grayImage, grayImage,90,255, THRESH_BINARY_INV);
42 vector<Mat> vectorGrayImage(4);
43 vectorGrayImage[0] = grayImage(Rect(perAncherPoint[0].x+4, 0, 15, standImage.rows));//LEFT
44 vectorGrayImage[1] = grayImage(Rect(perAncherPoint[1].x+4 , 0, 15, standImage.rows));//RIGHT
45 vectorGrayImage[2] = grayImage(Rect(0,perAncherPoint[0].y+4, standImage.cols, 15));//TOP
46 vectorGrayImage[3] = grayImage(Rect(0,perAncherPoint[2].y+4, standImage.cols, 15));//BOTTOM
47 vector<vector<int>> upJumpWave(4);
48 vector<vector<int>> downJumpWave(4);
49 for (size_t i = 0; i < 4; i++)
50 {
51 if (i<2) projectionAlgorithm(vectorGrayImage[i], upJumpWave[i], downJumpWave[i], Y_Axis, 2);
52 else projectionAlgorithm(vectorGrayImage[i], upJumpWave[i], downJumpWave[i], X_Axis, 2);
53 }
54 //-----------------------绘制检测的跳变线-------------------------//
55 for (size_t i = 0; i < upJumpWave[0].size(); i++)
56 {
57 line(grayImage, Point(perAncherPoint[0].x + 11, upJumpWave[0][i]), Point(perAncherPoint[0].x + 22, upJumpWave[0][i]), Scalar(255, 255, 255));
58 }
59 for (size_t i = 0; i < upJumpWave[3].size(); i++)
60 {
61 line(grayImage, Point(upJumpWave[3][i], perAncherPoint[3].y), Point(upJumpWave[3][i], perAncherPoint[3].y + 11), Scalar(255, 255, 255));
62 }
63 for (size_t i = 0; i < upJumpWave[1].size(); i++)
64 {
65 line(grayImage, Point(perAncherPoint[1].x, upJumpWave[1][i]), Point(perAncherPoint[1].x + 11, upJumpWave[1][i]), Scalar(255, 255, 255));
66 }
67 for (size_t i = 0; i < upJumpWave[2].size(); i++)
68 {
69 line(grayImage, Point(upJumpWave[2][i], perAncherPoint[0].y + 11), Point(upJumpWave[2][i], perAncherPoint[0].y + 22), Scalar(255, 255, 255));
70 }
71 //-------------把所有的点存储在容器里,以供下面的函数调用--------------//
72 vector<Point2f> allPoint;
73 for (size_t i = 1; i < upJumpWave[2].size(); i++)//存储上半部分图卡点(准考证号区+旁边那个看不清的区域)
74 {
75 for (size_t j = 0; j < 10; j++)
76 {
77 allPoint.push_back(Point(upJumpWave[2][i], upJumpWave[1][j]));
78 }
79 }
80 for (size_t i = 0; i < upJumpWave[3].size(); i++)//存储下半部分图卡点(答题区)
81 {
82 for (size_t j = 10; j < upJumpWave[1].size(); j++)
83 {
84 allPoint.push_back(Point(upJumpWave[3][i], upJumpWave[1][j]));
85 }
86 }
87 //--------------检测涂上铅笔的区域--------------//
88 vector<Point2f> testKeyPoint;
89 checkKeypoint(grayImage, allPoint, testKeyPoint);
90 for (size_t i = 0; i < testKeyPoint.size(); i++)
91 {
92 rectangle(show, Rect(testKeyPoint.at(i), Point(testKeyPoint.at(i).x + 12, testKeyPoint.at(i).y + 5)), Scalar(0, 0, 255));
93 }
94 waitKey();
95 return 0;
96 }
97 //--------------------------------注释代码部分为未用掩码操作--------------------------//
98 void FindAnchorPoint(const Mat& src,const Mat& matchMask,vector<Point2f>& anchorPoint)
99 {
100 Mat matchResult;
101 matchResult.create(Size(src.cols - matchMask.cols, src.rows - matchMask.rows), CV_16SC1);
102 //----模板匹配找四个定位点,同时得归一化(初始数据范围太大,自己通过image watch 查看)
103 matchTemplate(src, matchMask, matchResult, TM_CCOEFF_NORMED, Mat());
104 normalize(matchResult, matchResult, 0, 1, NORM_MINMAX);
105 //----查找匹配的四个点,分成四个区域查找,因为一个区域没办法查找四个值
106 /*Mat topleft = matchResult(Rect(Point(0, 0), Point(matchResult.cols / 2, matchResult.rows / 2)));
107 Mat topright = matchResult(Rect(Point(matchResult.cols / 2, 0), Point(matchResult.cols, matchResult.rows / 2)));
108 Mat botleft = matchResult(Rect(Point(0, matchResult.rows / 2), Point(matchResult.cols / 2, matchResult.rows)));
109 Mat botright = matchResult(Rect(Point(matchResult.cols / 2, matchResult.rows / 2), Point(matchResult.cols , matchResult.rows)));*/
110 double maxValue[4] = { 0 }, minValue[4] = {0};
111 vector<Point2i> maxPoint(4), minPoint(4);
112 Mat topleftMask = Mat::zeros(matchResult.size(), CV_8UC1);
113 Mat toprightMask = Mat::zeros(matchResult.size(), CV_8UC1);
114 Mat botleftMask = Mat::zeros(matchResult.size(), CV_8UC1);
115 Mat botrightMask = Mat::zeros(matchResult.size(), CV_8UC1);
116 topleftMask(Rect(Point(0, 0), Point(matchResult.cols / 2, matchResult.rows / 2))).setTo(255);
117 toprightMask(Rect(Point(matchResult.cols / 2, 0), Point(matchResult.cols, matchResult.rows / 2))).setTo(255);
118 botleftMask(Rect(Point(0, matchResult.rows / 2), Point(matchResult.cols / 2, matchResult.rows))).setTo(255);
119 botrightMask(Rect(Point(matchResult.cols / 2, matchResult.rows / 2), Point(matchResult.cols, matchResult.rows))).setTo(255);
120 vector<Mat> vectorMask;//注意此处如果用vector<Mat> vectorMask(4);对应的下面写法是vectorMask[0]=topleftMask;
121 vectorMask.push_back(topleftMask);
122 vectorMask.push_back(toprightMask);
123 vectorMask.push_back(botleftMask);
124 vectorMask.push_back(botrightMask);
125 for (size_t i = 0; i < vectorMask.size(); i++)
126 {
127 minMaxLoc(matchResult, &minValue[i], &maxValue[i], &minPoint[i], &maxPoint[i], vectorMask[i]);
128 }
129 anchorPoint.assign(maxPoint.begin(), maxPoint.end());
130 //minMaxLoc(topleft, &minValue[0], &maxValue[0], &minPoint[0], &maxPoint[0]);
131 //minMaxLoc(topright, &minValue[1], &maxValue[1], &minPoint[1], &maxPoint[1]);
132 //maxPoint[1].x = maxPoint[1].x + matchResult.cols / 2;
133 //minMaxLoc(botleft, &minValue[2], &maxValue[2], &minPoint[2], &maxPoint[2]);
134 //maxPoint[2].y = maxPoint[2].y + matchResult.rows / 2;
135 //minMaxLoc(botright, &minValue[3], &maxValue[3], &minPoint[3], &maxPoint[3]);
136 //maxPoint[3].x = maxPoint[3].x + matchResult.cols / 2;
137 //maxPoint[3].x = maxPoint[3].y + matchResult.rows / 2;
138 }
139 //------------------------------------图像投影算法-----------------------------------------------//
140 //*************@src------------------输入矩阵为单通道********************************************//
141 //*************@leftUpJumpWave-------上升跳变沿存储**********************************************//
142 //*************@rightDownJumpWave----下降跳变沿存储**********************************************//
143 //*************@maxInterval----------允许高电平(像素)最大间隔,也可以说是允许的最大误差********//
144 //-----------------------------------------------------------------------------------------------//
145 void projectionAlgorithm(Mat src,vector<int>& UpJumpWave,vector<int>& DownJumpWave,bool Axis,int maxInterval)
146 {
147 vector<int> pixNum(src.rows > src.cols ? src.rows : src.cols);
148 //------对X、Y做直方图类似的投影,统计一行或者一列的非零个数--------//
149 if (Axis)
150 {
151 for (size_t i = 0; i < src.cols; i++)
152 {
153 Mat col = src.col(i);//一列数据
154 pixNum[i] = countNonZero(col) > 1 ? countNonZero(col) : 0;
155 }
156 }
157 else
158 {
159
160 for (size_t i = 0; i < src.rows; i++)
161 {
162 Mat row = src.row(i);//一行数据
163 pixNum[i] = countNonZero(row) > 1 ? countNonZero(row) : 0;
164 }
165 }
166 if (pixNum.size() < maxInterval) return;//防止有空洞(实际没见过,如果有的话那程序架构会奔溃了)
167 //-----对上面的数据进行二值化0-1,同时对于不满足maxInterval的数据进行剔除--------//
168 for (int k = 1; k < pixNum.size()-maxInterval; k++)//去除了第一个和最后一个像素
169 {
170 if (pixNum[k] > 0 && pixNum[k + maxInterval] > 0)
171 {
172 for (size_t j = k; j < k + maxInterval; j++)
173 {
174 pixNum[j] = 1;
175 }
176 k = k + maxInterval-1;
177 }
178 else
179 {
180 pixNum[k] = 0;
181 }
182 }
183 //----对跳变的电平进行存储,高->低,低->高,-----//
184 for (size_t i = 1 ; i < pixNum.size()-2; i++)//去除了第一个和最后一个像素
185 {
186 if (pixNum[i] == 0 && pixNum[i + 1] == 1) UpJumpWave.push_back(i);
187 if (pixNum[i] == 1 && pixNum[i + 1] == 0) DownJumpWave.push_back(i);
188 }
189 //----对得到的结果进行处理,定位点被误判----//
190 vector<int>::iterator begin = UpJumpWave.begin();
191 if (UpJumpWave[0] < 15) UpJumpWave.erase(begin);
192 vector<int>::iterator end = UpJumpWave.end()-1;
193 if (UpJumpWave[UpJumpWave.size()-1] > 330) UpJumpWave.erase(end);
194 }
195 //-----------------------------------------------------------------------------------//
196 //************************************检测涂卡区域函数***********************************//
197 void checkKeypoint(Mat& _src,vector<Point2f>& allPoint,vector<Point2f>& testkeyPoint)
198 {
199 Mat src = _src.clone();
200 Mat show = Mat::zeros(src.size(), CV_8UC3);
201 morphologyEx(src, src, MORPH_DILATE, Mat::ones(3, 3, CV_8UC1));
202 for (size_t i = 0; i < allPoint.size(); i++)
203 {
204 //------判断检测点的正方形涂卡区的非零个数---------//
205 if (allPoint[i].x == 0 || allPoint[i].y == 0)
206 {
207 allPoint[i].x += 1;
208 allPoint[i].y += 1;
209 }
210 Mat rec = src(Rect(static_cast<int>(allPoint[i].x - 1 ), static_cast<int>(allPoint[i].y - 1 ), 12, 5));
211 int count = countNonZero(rec);
212 if (count > 15)
213 {
214 testkeyPoint.push_back(allPoint[i]);
215 rectangle(show, Rect(allPoint[i], Point(allPoint[i].x + 13, allPoint[i].y + 5)), Scalar(0, 0, 255));
216 }
217 }
218 }
219 #endif
补充:
1.此代码无法识别旋转的答题卡,只能识别透视的答题卡。原因是模板匹配不具有旋转和尺度的不变性。
2.随便拿来一张答题卡,主要是改里内部的参数,其中包括:
A. threshold(grayImage, grayImage,90,255, THRESH_BINARY_INV);//阈值更改,一般不用OTSU算法,因为要求得很准确。
B. matchTemplate(src, matchMask, matchResult, TM_CCOEFF_NORMED, Mat());//模板匹配方法更改
C. vectorGrayImage[0] = grayImage(Rect(perAncherPoint[0].x+4, 0, 15, standImage.rows));//区域的大小更改,其它三个等同
D. void projectionAlgorithm(Mat src,vector<int>& UpJumpWave,vector<int>& DownJumpWave,bool Axis,int maxInterval)//maxInterval最大误差更改
E. if (UpJumpWave[0] < 15) UpJumpWave.erase(begin);//对误判定位线进行筛选,另一个等同
F. if (pixNum[i] == 0 && pixNum[i + 1] == 1) UpJumpWave.push_back(i);//跳变沿的两边宽度可以进行调整
G. Mat rec = src(Rect(static_cast<int>(allPoint[i].x - 1 ), static_cast<int>(allPoint[i].y - 1 ), 12, 5));//检测涂卡区域大小更改
H. if (count > 15)//涂卡区域的程度判断更改