答題卡圖像識別
需求分析、市場分析和技術實現
P.S 博客發布以來,獲得多方的關注。相關內容我已經以教程的形式進行了整理發布(包括算法、硬件搭建和軟件框架),如果需要請移步
http://edu.51cto.com/course/course_id-8637.html
當然,如果具備一定基礎,那么閱讀本文就應該能夠掌握足夠的信息了。
一、需求分析
一、以接口的方式開發此需求:
1:接收圖片
以上傳的方式把圖片發送到接口。
2:識別圖片
接口接收到圖片后,進行圖像識別。
3:返回數據
返回識別后的JSON格式數據。
二、答題卡圖片識別的具體要求:
圖片是通過手機、相機、掃描儀等設備拍照而來,其中手機、相機拍出的照片會出現像素低、圖像不正、聚焦不清楚等問題;
1:圖片只要是人眼能看清楚的即可完成識別;
2:800萬像素以上的手機拍的照片能進行識別;
3:聚焦不清楚時也可以進行識別;
4:不符合要求的圖片可以不識別,一旦識別,正確率必須保證100%。
三、其他要求:
1:此項目驗收需要提供答題卡識別的所有源代碼、接口說明文檔。
2:接口需支持單張圖片上傳識別以及多張圖片的上傳識別。
3:接口使用的開發語言及開發工具不限。
四、需求分析:
這是一個典型的“機器視覺”應用。其中,答題卡的樣式可以是由自己來設置的,圖片的獲取方式提到了可以是“手機拍照、相機拍照”這種比較方便的方式;本例的一個特殊的要求是:你可以識別不出來,但是你不能識別錯誤,這是項目的特殊要求
五、需求分析:
普通的答題卡是這樣的:
用於機器識別的答題卡是這樣的,最明顯的區別在於在邊界處提供了用於標定的黑邊。由於這里的答題卡是可以自己來設計的,就應該設計得最適合識別:
經過我修改的答題卡是這樣的,主要是用圓點進行邊界標定,因為在旋轉和縮放的情況下,圓點都有更好的性能:
二、市場分析
答題卡已經出現好多年了,而且教育機構也是容易出現壁壘的領域。經過簡單調查,制式的答題機應該是這種樣子的,這種答題機采用的應該特殊的成像技術,比如紅外之類的,否則也不需要做成這種樣子:
其價格在數千元到萬元左右,淘寶上也有人做出了機器識別的例子:
采用普通攝像頭和特定的支架,銷售情況不好。
但是,圖像確是多種多樣的。
形式多樣。值得關注的一點是,這些能夠通過baidu直接搜索得到的答題卡在設計上和本文提供的答題開有兩點比較大的不同,一個是在取消了比如圓點這樣的標定點,二個是在橫版面上采用了“點畫”的方式進行標定
這樣能夠得到的結果還是使得答題卡更加的簡潔,美觀。
對於這個市場,我認為在網絡和即時聊天工具更加發達的今天,答題卡作為一種非常正式的考試方法,還是有其市場的(比如高考中考,短時間內還不會出現直接采用移動設備進行答卷);但是專門去做一套這樣的設備,市場已經基本飽和,而且教育市場的壁壘應該很高,不是很容易就能夠進入的。但是,對於在日常非正式考試中需要答題卡相關設備,而不希望擔負一套昂貴的專業系統的人或單位來所,如果能夠以一種比較低廉的價格,並且已一種比較方便操作的方式(比如直接利用手機,或普通相機)進行實現,應該是有一定的市場的。
三、技術實現
本例的技術難度不是很大,非常關鍵的一點是由於卡片是可以由自己來設計的。而且圖像的獲取也比較容易被優化。這里以最前面的圖片進行設計分析,其他的例子情況可以以此類推;並且公布核心代碼。
1)仿照實際的情況,對原始圖片進行相關處理。在實際拍攝的時候,可能會出現“縮放”、“透視變化”等影響最終實際結果的情況:
變小
透視變化
同時透視和縮放
2)編寫獲取錨點(就是圓點)的函數. FetchAnchorPoints函數的主要過程是將輸入的圖片划分為四個部分,並且分別找到其中的圓點。參數中mattmp是模板圖片,也就是哪個小圓的圖片。
void FetchAnchorPoints(Mat src,Mat mattmp,Point &anchor01,Point &anchor02,Point &anchor03,Point &anchor04)
{
Mat imagematch;
Point minLoc;
Point maxLoc01,maxLoc02,maxLoc03,maxLoc04;
//Point anchor01,anchor02,anchor03,anchor04;
double minVal;
double maxVal2;
//Mat src = imread("C:/answercard/1.jpg",0);//讀入黑白原始圖像
int srcRows = src.rows;
int srcCols = src.cols;
Mat src01 = src(Rect( 0, 0,srcCols / 2,srcRows / 2));
Mat src02 = src(Rect(srcCols / 2, 0,srcCols / 2,srcRows / 2));
Mat src03 = src(Rect( 0,srcRows / 2,srcCols / 2,srcRows / 2));
Mat src04 = src(Rect(srcCols / 2,srcRows / 2,srcCols / 2,srcRows / 2));
//imshow("src01",src01);imshow("src02",src02);imshow("src03",src03);imshow("src04",src04);
matchTemplate( mattmp, src01, imagematch, 5 );
normalize( imagematch, imagematch, 0, 1, NORM_MINMAX, - 1, Mat() );
minMaxLoc( imagematch, &minVal, &maxVal2, &minLoc, &maxLoc01, Mat() );
anchor01 = maxLoc01;
//circle(src,maxLoc01,3,Scalar(0),3);
matchTemplate( mattmp, src02, imagematch, 5 );
normalize( imagematch, imagematch, 0, 1, NORM_MINMAX, - 1, Mat() );
minMaxLoc( imagematch, &minVal, &maxVal2, &minLoc, &maxLoc02, Mat() );
anchor02 = Point(maxLoc02.x +srcCols / 2,maxLoc02.y);
//circle(src,anchor02,3,Scalar(0),3);
matchTemplate( mattmp, src03, imagematch, 5 );
normalize( imagematch, imagematch, 0, 1, NORM_MINMAX, - 1, Mat() );
minMaxLoc( imagematch, &minVal, &maxVal2, &minLoc, &maxLoc03, Mat() );
anchor03 = Point(maxLoc03.x,maxLoc03.y +srcRows / 2);
//circle(src,anchor03,3,Scalar(0),3);
matchTemplate( mattmp, src04, imagematch, 5 );
normalize( imagematch, imagematch, 0, 1, NORM_MINMAX, - 1, Mat() );
minMaxLoc( imagematch, &minVal, &maxVal2, &minLoc, &maxLoc04, Mat() );
anchor04 = Point(maxLoc04.x +srcCols / 2,maxLoc04.y +srcRows / 2);
//circle(src,anchor04,3,Scalar(0),3);
}
獲得的結果
3)采用warpPerspective進行透視變換,如果對warpPerspective不是很了解可以查看我前面的blog
Point2f dst_vertices[ 4];
//獲得矯正結果圖像的參數
Mat matstandard = imread( "C:/answercard/1.jpg", 0); //讀入黑白原始圖像
Mat mattmp = imread( "C:/answercard/temp.jpg", 0);
FetchAnchorPoints(matstandard,mattmp,anchor01,anchor02,anchor03,anchor04);
std : :cout << "anchor01" <<anchor01 << " " << "anchor02" <<anchor02 << " " << "anchor03" <<anchor03 << " " << "anchor04" <<anchor04;
dst_vertices[ 0] = anchor01;
dst_vertices[ 1] = anchor02;
dst_vertices[ 2] = anchor03;
dst_vertices[ 3] = anchor04;
//dst_vertices.push_back(anchor01);dst_vertices.push_back(anchor02);dst_vertices.push_back(anchor03);dst_vertices.push_back(anchor04);
//獲得需要矯正圖像參數
Mat matsrc = imread( "C:/answercard/bigroatate.jpg", 0);
FetchAnchorPoints(matsrc,mattmp,anchor01,anchor02,anchor03,anchor04);
cout << "\n";
std : :cout << "anchor01" <<anchor01 << " " << "anchor02" <<anchor02 << " " << "anchor03" <<anchor03 << " " << "anchor04" <<anchor04;
src_vertices[ 0] = anchor01;
src_vertices[ 1] = anchor02;
src_vertices[ 2] = anchor03;
src_vertices[ 3] = anchor04;
//src_vertices.push_back(anchor01);src_vertices.push_back(anchor02);src_vertices.push_back(anchor03);src_vertices.push_back(anchor04);
//透視變化
Mat warpMatrix = getPerspectiveTransform(src_vertices, dst_vertices);
cv : :Mat rotated;
warpPerspective(matsrc, rotated, warpMatrix, rotated.size(), INTER_LINEAR, BORDER_CONSTANT);
imshow( "rotated",rotated);
imshow( "matstandard",matstandard);

Mat roi01;Mat ro i02;Mat roi03;Mat roi04;
anchor02 =dst_vertices[ 1] ;
anchor03 =dst_vertices[ 2] ;
anchor04 =dst_vertices[ 3] ;
//TODO這個地方最終的時候需要改成rotated
roi01 = matstandard(Rect(anchor01.x,anchor01.y +mattmp.rows, 20,anchor03.y -anchor01.y -mattmp.rows));
roi02 = matstandard(Rect(anchor01.x +mattmp.cols,anchor01.y,anchor02.x -anchor01.x -mattmp.cols, 20));
roi03 = matstandard(Rect(anchor02.x + 8,anchor02.y +mattmp.rows, 17,anchor04.y -anchor02.y -mattmp.rows));
roi04 = matstandard(Rect(anchor03.x +mattmp.cols,anchor03.y + 5,anchor04.x -anchor03.x -mattmp.cols, 13));
//roi02 = FetchMaxContour(roi02);
//imshow("roi01",roi01);
//imshow("roi02",roi02);
//imshow("roi03",roi03);
//imshow("roi04",roi04);


//函數作用: 對區域進行預處理,返回最大的連續區域
//參 數: src [in] 輸入mat
//返 回:投影值
vector < int > FetchMaxContour(Mat src)
{
//讀取圖像
Mat testmat = src.clone();
Mat testclone = src.clone();
Mat matcanny;
//用於尋找輪廓
Mat threshold_output;
vector <vector <Point > > contours;
vector <Vec4i > hierarchy;
int imax = 0; int maxsize = 0;
RotatedRect theMinRect;
RotatedRect theMinEllipse;
//imshow("原始圖像",testmat);
//大津法找到敏感區域
threshold(testmat,testmat, 0, 255,cv : :THRESH_OTSU);
//imshow("大津法",testmat);
//為什么要轉換,因為白色是有數據的區域,輪廓是圍繞白色區域的
threshold(testmat,testmat, 0, 255,THRESH_BINARY_INV);
imshow( "二值",testmat);
//計算縱向投影
vector < int > vcol;itmp = 0;
for ( int i = 0;i <testmat.cols;i ++)
{
for ( int j = 0;j <testmat.rows;j ++)
{
if (testmat.at <uchar >(j,i))
{
itmp = itmp + 1;
}
}
vcol.push_back(itmp);
itmp = 0;
}
////對得到的結果進行處理,計算波峰
//int isum = 0;//一共多少個波峰
vector < int > vrise;
for ( int i = 1;i <vcol.size();i ++)
{
if (vcol[i - 1] == 0 && vcol[i] > 0)
{
vrise.push_back(i);
//isum = isum+1;
}
}
return vrise;
}
for ( int i = 1;i <vroi02.size();i ++)
{
Mat roi = rotated(Rect(mattmp.cols +anchor01.x +vroi02[i],roi02.rows + 38, 11, 92));
//imshow("roi",roi);
vmat02.push_back(roi);
circle(rotated,Point(mattmp.cols +anchor01.x +vroi02[i],roi02.rows + 38), 1,Scalar( 0), 1);
}
vector < int > vroi04 = FetchMaxContour(roi04);
vector <Mat > vmat04;
for ( int i = 0;i <vroi04.size();i ++)
{
Mat roi = rotated(Rect(mattmp.cols +anchor03.x +vroi04[i], 153, 11, 198));
//imshow("roi",roi);
//vmat02.push_back(roi);
circle(rotated,Point(mattmp.rows +anchor03.x +vroi04[i], 153), 1,Scalar( 0), 1);
}
imshow( "rotated",rotated);


resize(matsrc,matsrc,Size( 600, 500));
FetchAnchorPoints(matsrc,mattmp,anchor01,anchor02,anchor03,anchor04);

效果如此。




vector < int > vroi02 = FetchMaxContour(roi02);
vector <Mat > vmat02;
vector < int > vroi03 = FetchMaxContour(roi03, 1);
//減去偏移,這里的偏移量可以從roi03第一個值得出
for ( int i = 0;i <vroi03.size();i ++)
{
vroi03[i] = vroi03[i] - 30;
}
int resulttmp = 9;
cout << "vroi02" <<endl;
//這里i = 0的數據是無用數據
for ( int i = 1;i <vroi02.size();i ++)
{
Mat roi = rotated(Rect(mattmp.cols +anchor01.x +vroi02[i],roi02.rows + 38, 11, 92));
//vmat02.push_back(roi);
vector < int > vtmp = FetchMaxContour(roi, 1);
vtmp[ 0] = vtmp[ 0] + 4;
for ( int k = 0;k < 9;k ++)
{
if (vtmp[ 0] > =vroi03[k] && vtmp[ 0] <vroi03[k + 1])
{
resulttmp = k;
break;
}
}
cout <<i << " is " <<resulttmp << " | ";
cout <<endl;resulttmp = 9;
if (IsDebug)
{
char * tmp = new char[ 100];
sprintf(tmp, "C:/answercard/vmat02/%d.jpg",i);
imwrite(tmp,roi);
circle(rotated,Point(mattmp.cols +anchor01.x +vroi02[i],roi02.rows + 38), 1,Scalar( 0), 1);
}
}