答題卡圖像識別項目


答題卡圖像識別

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

 

 

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

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

當然,如果具備一定基礎,那么閱讀本文就應該能夠掌握足夠的信息了。

 

一、需求分析

一、以接口的方式開發此需求:

1:接收圖片

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

2:識別圖片

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

3:返回數據

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

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

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

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

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

三、其他要求:

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

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

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

四、需求分析:

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

五、需求分析:

普通的答題卡是這樣的:

8691_80題AB型Z1Z2ABCD_B型

用於機器識別的答題卡是這樣的,最明顯的區別在於在邊界處提供了用於標定的黑邊。由於這里的答題卡是可以自己來設計的,就應該設計得最適合識別:

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

二、市場分析

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

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

采用普通攝像頭和特定的支架,銷售情況不好。

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

     形式多樣。值得關注的一點是,這些能夠通過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,  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 |
 
四、小結
   答題卡這種東西很早之前就有了,我想在它第一次被提出的時候,絕對是創造性的、革命的東西,大大地提高了考試的生產率。但是之前的那種設備采用的原理比較精密和復雜,非常依賴設備,一定程度上限制了傳播和發展。今天,隨着機器視覺算法的不斷發展、移動通信設備的不斷發展,解決這種問題有了新的思路和新的市場空間。這也從另一個方向上說明了機器視覺技術的廣闊前景。
   本文從需求分析、市場分析和技術實現3個方面嘗試對這個問題進行剖析。受制於資源和個人能力,很多地方解釋的不是很清楚,最終開發出來的代碼雖然具備了一定的解決問題的能力,但是畢竟不夠魯棒和高效。畢竟機器視覺的項目是由市場和實際需求驅動的,如果有好的想法和需求,歡迎交流。
   感謝閱讀,希望有所幫助。

 






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM