提前說明一下:這是“禾路”老師博客上的一個例子,老師在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)//塗卡區域的程度判斷更改
