答題卡圖像識別項目

答題卡圖像識別html

需求分析、市場分析和技術實現算法

 

 

P.S 博客發佈以來,得到多方的關注。相關內容我已經以教程的形式進行了整理髮布(包括算法、硬件搭建和軟件框架),若是須要請移步
網絡

http://edu.51cto.com/course/course_id-8637.html框架

固然,若是具有必定基礎,那麼閱讀本文就應該可以掌握足夠的信息了。函數

 

1、需求分析工具

1、以接口的方式開發此需求:性能

1:接收圖片開發工具

    以上傳的方式把圖片發送到接口。優化

2:識別圖片spa

    接口接收到圖片後,進行圖像識別。

3:返回數據

    返回識別後的JSON格式數據。

2、答題卡圖片識別的具體要求:

圖片是經過手機、相機、掃描儀等設備拍照而來,其中手機、相機拍出的照片會出現像素低、圖像不正、聚焦不清楚等問題;

1:圖片只要是人眼能看清楚的便可完成識別; 
2
800萬像素以上的手機拍的照片能進行識別; 
3
:聚焦不清楚時也能夠進行識別;

4:不符合要求的圖片能夠不識別,一旦識別,正確率必須保證100%

3、其餘要求:

1:此項目驗收須要提供答題卡識別的全部源代碼、接口說明文檔。

2:接口需支持單張圖片上傳識別以及多張圖片的上傳識別。

3:接口使用的開發語言及開發工具不限。

4、需求分析:

    這是一個典型的「機器視覺」應用。其中,答題卡的樣式能夠是由本身來設置的,圖片的獲取方式提到了能夠是「手機拍照、相機拍照」這種比較方便的方式;本例的一個特殊的要求是:你能夠識別不出來,可是你不能識別錯誤,這是項目的特殊要求

5、需求分析:

普通的答題卡是這樣的:

8691_80題AB型Z1Z2ABCD_B型

用於機器識別的答題卡是這樣的,最明顯的區別在於在邊界處提供了用於標定的黑邊。因爲這裏的答題卡是能夠本身來設計的,就應該設計得最適合識別:

通過我修改的答題卡是這樣的,主要是用圓點進行邊界標定,由於在旋轉和縮放的狀況下,圓點都有更好的性能:

2、市場分析

答題卡已經出現好多年了,並且教育機構也是容易出現壁壘的領域。通過簡單調查,制式的答題機應該是這種樣子的,這種答題機採用的應該特殊的成像技術,好比紅外之類的,不然也不須要作成這種樣子:

其價格在數千元到萬元左右,淘寶上也有人作出了機器識別的例子:

採用普通攝像頭和特定的支架,銷售狀況很差。

可是,圖像確是多種多樣的。

     形式多樣。值得關注的一點是,這些可以經過baidu直接搜索獲得的答題卡在設計上和本文提供的答題開有兩點比較大的不一樣,一個是在取消了好比圓點這樣的標定點,二個是在橫版面上採用了「點畫」的方式進行標定

這樣可以獲得的結果仍是使得答題卡更加的簡潔,美觀。

     對於這個市場,我認爲在網絡和即時聊天工具更加發達的今天,答題卡做爲一種很是正式的考試方法,仍是有其市場的(好比高考中考,短期內還不會出現直接採用移動設備進行答卷);可是專門去作一套這樣的設備,市場已經基本飽和,並且教育市場的壁壘應該很高,不是很容易就可以進入的。可是,對於在平常非正式考試中須要答題卡相關設備,而不但願擔負一套昂貴的專業系統的人或單位來所,若是可以以一種比較低廉的價格,而且已一種比較方便操做的方式(好比直接利用手機,或普通相機)進行實現,應該是有必定的市場的。

3、技術實現

     本例的技術難度不是很大,很是關鍵的一點是因爲卡片是能夠由本身來設計的。並且圖像的獲取也比較容易被優化。這裏以最前面的圖片進行設計分析,其餘的例子狀況能夠以此類推;而且公佈核心代碼。

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,  01, 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,  01, 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,  01, 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,  01, 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

 

     Point anchor01,anchor02,anchor03,anchor04;
    Point2f src_vertices[ 4];
    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);
這一步獲得的校訂圖像:
4)對原始圖像進行裁剪
//對原始圖像進行裁剪
    Mat roi01;Mat ro i02;Mat roi03;Mat roi04;
    anchor01  =dst_vertices[ 0] ;
    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);
這一步獲得的結果
 
5)得到區域的投影。這裏的操做其實就是得到圖像的波峯,這樣就能進行定位。
//函數名稱:FetchMaxContour
//函數做用: 對區域進行預處理,返回最大的連續區域
//參    數: 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;
}
 
6)得到投影區域,而且標註出來
 
    vector < int >  vroi02  =  FetchMaxContour(roi02);
    vector <Mat > vmat02;
     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);
 
找到的結果用圓點標註出來
這裏下面一排第一個圓點沒找到,這是原始模板圖像在設計的時候出現的問題,由於這裏只是說明原理,我就沒有修改。
7)架設照相機,獲取實際圖片
作到這一步,下面就是要得到實際的圖片並進行識別了。我採用的方法是將答題卡用打印機打印出來,而後用相機拍攝下來,注意一下光照,效果以下:
照片仍是比較模糊的,識別後達到預期效果。注意模板識別以前首先須要把圖片縮放一下,不然效果不會太好。
Mat matsrc  = imread( "C:/answercard/r4.jpg", 0);
    resize(matsrc,matsrc,Size( 600, 500));
    FetchAnchorPoints(matsrc,mattmp,anchor01,anchor02,anchor03,anchor04);

效果如此。
採用2b鉛筆進行填卡,效果以下
進行閾值分析後,效果不好
能夠發現,採用2b鉛筆,若是採用圖像識別的方法的話,光照的影響仍是很是大的。
接着改用黑色鉛筆(鋼筆也能夠)
則特徵明顯
8)對獲取的結果進行計算。也就是圖片到數據的一個量化的過程。
具體來講,就是將這樣的圖像
量化成爲選擇結果,思路也是很是直接的,就是對比最右側的標尺值和實際得到的值。在編寫具體代碼的時候,可能還要加上必定的修正,而且要儘量保證這個修正是魯棒的。

    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);
        }
    }
 
結果徹底正確:
1 is 0 |
2 is 1 |
3 is 2 |
4 is 4 |
5 is 3 |
6 is 4 |
7 is 4 |
8 is 5 |
9 is 4 |
10 is 4 |
11 is 5 |
12 is 6 |
13 is 3 |
 
4、小結
   答題卡這種東西很早以前就有了,我想在它第一次被提出的時候,絕對是創造性的、革命的東西,大大地提升了考試的生產率。可是以前的那種設備採用的原理比較精密和複雜,很是依賴設備,必定程度上限制了傳播和發展。今天,隨着機器視覺算法的不斷髮展、移動通訊設備的不斷髮展,解決這種問題有了新的思路和新的市場空間。這也從另外一個方向上說明了機器視覺技術的廣闊前景。
   本文從需求分析、市場分析和技術實現3個方面嘗試對這個問題進行剖析。受制於資源和我的能力,不少地方解釋的不是很清楚,最終開發出來的代碼雖然具有了必定的解決問題的能力,可是畢竟不夠魯棒和高效。畢竟機器視覺的項目是由市場和實際需求驅動的,若是有好的想法和需求,歡迎交流。
   感謝閱讀,但願有所幫助。

 



相關文章
相關標籤/搜索