[機器學習]基於OpenCV實現最簡單的數字識別

http://blog.csdn.net/jinzhuojun/article/details/8579416

本文將基於OpenCV實現簡單的數字識別。這裏以遊戲Angry Birds爲例,通過以下幾個主要步驟對其中右上角的分數部分進行自動識別。



1. 學習分類器

根據訓練樣本,選取模型訓練產生數字分類器。這裏的樣本可以是通用的數字樣本庫(如NIST等),也可以是針對應用場景而製作的專門訓練樣本。前者優在泛化性,後者強在準確率,當然常用做法是將這兩者結合,即在通用數字庫基礎上做修改。另外這裏由於模式並不複雜,計算量也不大,所以不對樣本進行特徵提取,對原始樣本作簡單變換後直接作爲訓練樣本。


具體地,首先是生成訓練樣本矩陣,一般樣本是以二維矩陣的方式存在文件當中,現在要將它們讀出來,進行適當的預處理,然後生成OpenCV能理解的數據結構。

train_X = cvCreateMat(sample_num * class_num, size * size, CV_32FC1);  
train_Y = cvCreateMat(sample_num * class_num, 1, CV_32FC1);  
  
for(i = 0; i < class_num; i++){  
    for(j = 0; j < sample_num; j++){   
        src_image = cvLoadImage(file,0);  
        pimage = preprocessing(src_image, size, size);  
        ...  
        cvGetRow(train_X, &row, i * sample_num + j);  
        row_vec = cvReshape(&data, &mathdr, 0, 1);  
        cvCopy(row_vec, &row, NULL);  
        ...  
        cvGetRow(train_Y, &row, i * sample_num + j);  
        cvSet(&row, cvRealScalar(i));  
    }  
}  

訓練樣本中的數字位置形態各異,因此讀入時需要進行規整化。主要方法是先找到數字的邊界框,然後以寬和高中大的一邊爲基準進行縮放和拉伸,從而使得其可以佔滿整個表示單個樣本的矩陣。

IplImage preprocessing(IplImage* img, int w, int h){  
    ...  
    bb = findBoundingBox(img);  
    cvGetSubRect(img, &data, cvRect(bb.x, bb.y, bb.width, bb.height));  
    size = (bb.width > bb.height) ? bb.width : bb.height;  
      
    res = cvCreateImage(cvSize(size, size), 8, 1);  
    x = floor((float)(size - bb.width) / 2.0f);  
    y = floor((float)(size - bb.height) / 2.0f);  
    cvGetSubRect(res, &subdata, cvRect((int)x, (int)y, bb.width, bb.height));  
    cvCopy(&data, &subdata, NULL);  
  
    ret = cvCreateImage(cvSize(w, h), 8, 1);  
    cvResize(res, ret, CV_INTER_NN);  
    return *ret;  
}  

假設單個樣本可表示爲0/1矩陣,那findBoundingBox()只要從x和y方向分別掃描最大最小的非0值就可以了。 訓練樣本準備好後,在OpenCV中創建相應的分類器非常方便。這裏用的是KNN,當然除了KNN外還有其它很多封裝好的分類器(如NN, SVM等)

knn = new CvKNearest(train_X, train_Y, 0, false, K);  

2. 圖像預處理

前面通過學習產生了分類器,但我們輸入圖像中的數字並不能直接作爲測試輸入。圖像中的數字筆畫有時並不規整,還可能相互重疊。因爲本文例子爲了簡化用的是屏幕截圖,所以位置形變校正,色彩亮度校正等等都省去了,但仍需要一些簡單處理。下面先對輸入圖像進行一把簡單的預處理,主要目的是將數字之間兩兩分開。方法很簡單,首先將圖像轉成二值圖,然後腐蝕一把,數字之間就分離得比較開了,這樣便於我們下一步分割和識別。這樣做還有個好處,就是把其餘的噪聲也順帶去掉了。

cvtColor(input, out_img, CV_BGR2GRAY);  
threshold(out_img, out_img, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);  
...  
erode(out_img, out_img, elem);  

結果:



3. 圖像分割

接下來,就可以對圖像進行分割了。由於我們的分類器只能對數字一個一個地識別,所以首先要把每個數字分割出來。基本思想是先用findContours()函數把基本輪廓找出來,然後通過簡單驗證以確認是否爲數字的輪廓。對於那些通過驗證的輪廓,接下去會用boundingRect()找出它們的包圍盒。

vector< vector< Point> > contours;    
findContours(contour_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);     
    
vector<vector<Point> >::iterator it = contours.begin();    
while (it!=contours.end()) {    
    RotatedRect rect = minAreaRect(Mat(*it));    
    if(verifyRect(rect)){       
        ++it; // A valid rectangle found        
    } else {    
        it= contours.erase(it);    
    }    
}    
    
...    
vector<Rect> boundRect(contours.size());    
for (int i = 0; i < contours.size(); ++i) {    
    Scalar color = Scalar(200, 200, 200);    
    boundRect[i] = boundingRect(Mat(contours[i]));    
    rectangle(out_img, boundRect[i].tl(), boundRect[i].br(), color, 0.2, 8, 0);    
        
    CvRect roi = CvRect(boundRect[i]);    
    IplImage orig = out_img;    
    IplImage *res = cvCreateImage(cvSize(roi.width, roi.height), orig.depth, orig.nChannels);    
    cvSetImageROI(&orig, roi);    
    cvCopy(&orig, res);    
    cvResetImageROI(&orig);    
    
    IplImage *bininv_img;    
    bininv_img = cvCreateImage(cvSize(128, 128), IPL_DEPTH_8U, 1);    
    cvResize(res, bininv_img);    
    cvThreshold(bininv_img, bininv_img, 100, 255, CV_THRESH_BINARY_INV);            
    
    int ret = do_ocr(bininv_img);    
    res_elem elem;    
    elem.num = ret;    
    elem.xpos = boundRect[i].tl().x;    
    res_vec.push_back(elem);    
    ...    
}    

結果:



4. 應用分類器

分割完後就可以應用我們前面訓練好的分類器對分割結果進行識別了。當然,如果感覺結果不滿意,可以將分類錯誤的樣本加上正確的標籤後放入訓練樣本重新生成分類器,使得分類器能夠有更好的識別率。上一步中的do_ocr()函數就是利用先前訓練好的分類器識別單個數字。注意訓練樣本進行過怎麼樣的預處理,這裏也一樣要做。

int do_ocr(IplImage *img)  
{  
    ...  
    pimage = preprocessing(img, size, size);  
      
    ...  
    cvGetSubRect(pimage, &data, cvRect(0, 0, size, size));  
    CvMat mathdr, *vec;  
    vec = cvReshape(&data, &mathdr, 0, 1);  
  
    ret = knn->find_nearest(vec, K, 0, 0, nearest, 0);  
    return (int)ret;  
}  


5. 後期處理

因爲分割圖像時查找數字輪廓並不保證是按順序來的,所以這兒要將識別結果按分割時輸出的包圍盒位置信息進行排序,最後將它們轉換成數字輸出。

sort(res_vec.begin(), res_vec.end(), sort_func);  
int j, num = 0;  
for (j = 0; j < res_vec.size(); ++j) {  
    num = num * 10 + res_vec[j].num;  
}  
char resbuf[256];  
sprintf(resbuf, "%d", num);  
putText(show_img, resbuf, Point(OUTPUT_X, OUTPUT_Y), FONT_HERSHEY_SIMPLEX, 0.8, Scalar(0, 255, 0), 2);  
imshow("show", show_img);  

結果:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章