《图像处理实例》 之 答题卡检测
提前说明一下:这是“禾路”老师博客上的一个例子,老师在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)//涂卡区域的程度判断更改
-------------------------------------------
个性签名:衣带渐宽终不悔,为伊消得人憔悴!
如果觉得这篇文章对你有小小的帮助的话,记得关注再下的公众号,同时在右下角点个“推荐”哦,博主在此感谢!