1. 程式人生 > >基於opencv的數字識別

基於opencv的數字識別

    最近學習了opencv,然後想通過其對圖片上的數字進行識別,參考了網上幾篇關於opencv數字識別的部落格,我自己也寫了一個程式玩玩。我是在vs2017和opencv3.4.1環境下實現的。

    這裡先說一下我的思路和步驟:

  1. 載入需要識別的圖片,然後將其轉化為二值圖
  2. 尋找數字的外輪廓,切記不可以找全部輪廓,否則一個數字將會有多個輪廓,識別起來就很麻煩了
  3. 對輪廓進行排序,因為使用findcontours函式尋找到的輪廓順序不一定是我們想要的,因此需要對找到的數字輪廓根據輸入的圖片進行排序
  4. 按順序將數字輪廓分割,一個一個提取出來
  5. 進行識別和匹配,將提取出來的數字與模板數字(需要自己建立)進行比較,這裡採用兩幅圖片畫素相減的方法,尋找出差值最小的圖片即為最匹配的數字 

一、圖片預處理

    將輸入的圖片轉化為二值圖以便尋找輪廓

        //輸入要識別的圖片,並顯示
	Mat srcImage = imread("number.jpg");
	imshow("原圖", srcImage);
	//對影象進行處理,轉化為灰度圖然後再轉為二值圖
	Mat grayImage;
	cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);
	Mat binImage;
	//第4個引數為CV_THRESH_BINARY_INV是因為我的輸入原圖為白底黑字
	//若為黑底白字則選擇CV_THRESH_BINARY即可
	threshold(grayImage, binImage, 100, 255, CV_THRESH_BINARY_INV);

二、尋找數字輪廓

     

        //尋找輪廓,必須指定為尋找外部輪廓,不然一個數字可能有多個輪廓組成,比如4,6,8,9等數字
	Mat conImage = Mat::zeros(binImage.size(), binImage.type());
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	//指定CV_RETR_EXTERNAL尋找數字的外輪廓
	findContours(binImage, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
	//繪製輪廓
	drawContours(conImage, contours, -1, 255);

 

 三、對輪廓進行排序

     在排序之前呢,我們需要定義一個類,用來存放輪廓的外接矩陣以及方便後續的排序

class myRect
{
public:
	myRect(){}
	~myRect(){}
	myRect(Rect &temp):myRc(temp){}
	//比較矩形左上角的橫座標,以便排序
	bool operator<(myRect &rect)
	{
		if (this->myRc.x < rect.myRc.x)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	//過載賦值運算子
	myRect operator=(myRect &rect)
	{
		this->myRc = rect.myRc;
		return *this;
	}
	//獲取矩形
	Rect getRect()
	{
		return myRc;
	}
private:
	Rect myRc;//存放矩形
};

 

    有了這個類之後,我們可以將一個一個輪廓外接矩陣保存於該類中。而且類中過載了比較操作符,很容易對輪廓進行排序。

        //將每個數字,分離開,儲存到容器中
	vector<myRect> sort_rect;
	for (int i = 0; i < contours.size(); i++)
	{
		//boundingRect返回輪廓的外接矩陣
		Rect tempRect = boundingRect(contours[i]);
		sort_rect.push_back(tempRect);
	}

    排序演算法的話,採用比較簡單的冒泡法

        //對矩形進行排序,因為輪廓的順序不一定是數字真正的順序
	for (int  i = 0; i < sort_rect.size(); i++)
	{
		for (int j = i + 1; j < sort_rect.size(); j++)
		{
			if (sort_rect[j] < sort_rect[i])
			{
				myRect temp = sort_rect[j];
				sort_rect[j] = sort_rect[i];
				sort_rect[i] = temp;
			}
		}
	}

    這樣,sort_rect容器中的輪廓矩形是按我們輸入的圖片中的數字順序存放的。

四、載入(新建)數字模板

    我們在進行數字匹配時,需要先載入模板進行比較。如果沒有就要先新建一個。新建模板需要輸入的是0-9的數字模板,不用修改程式,只需要新增以下程式碼。

        /*載入模板,若沒有則需自己新建一個*/

	//新建,執行一次就好,不過製作模板的材料為0-9十個數字的影象
	for (int i = 0; i < 10; i++)
	{
		Mat ROI = conImage(sort_rect[i].getRect());
		Mat dstROI;
		resize(ROI, dstROI, Size(40, 50),0, 0, INTER_NEAREST);
		char name[64];
		sprintf(name, "C:/Users/Administrator/Desktop/number_recognition/number_recognition/image/%d.jpg", i);
		//imshow(str, dstROI);
		imwrite(name, dstROI);
	}

    製作模板其實就是將我們需要的ROI區域儲存為圖片,程式碼中的路徑是我的路徑,可以根據自己情況修改。    

    然後是載入模板,也就是從檔案中將各個模板圖片讀入,路徑根據自己的情況修改。

        //載入模板
	vector<Mat> myTemplate;
	for (int i = 0; i < 10; i++)
	{
		char name[64];
		sprintf(name, "C:/Users/Administrator/Desktop/number_recognition/number_recognition/image/%d.jpg", i);
		Mat temp = imread(name, 0);
		myTemplate.push_back(temp);
	}

五、分割數字 

    分割數字比較容易,即通過輪廓外接矩形在二值圖片上尋找我們要使用的ROI,然後分別儲存下來,以供識別。根據排好序的sort_rect可以分割出待識別的數字。

        //按順序取出和分割數字
	vector<Mat> myROI;
	for (int i = 0; i < sort_rect.size(); i++)
	{
		Mat ROI;
		ROI = conImage(sort_rect[i].getRect());
		Mat dstROI = Mat::zeros(myTemplate[0].size(),myTemplate[0].type());
		resize(ROI, dstROI, myTemplate[0].size(), 0, 0, INTER_NEAREST);
		myROI.push_back(dstROI);
	}

 

六、比較和匹配

    我採用的比較和匹配方法是,將absdiff計算模板和待識別數字的差值,然後比較出差值最小的即為最匹配的數字,從而實現匹配。在匹配前我們需要定義一個getPiexSum函式以計算兩幅圖片的差值的畫素之和。

//求圖片的畫素和
int getPiexSum(Mat &image)
{
	int sum = 0;
	for (int i = 0; i < image.cols; i++)
	{
		for (int j = 0; j < image.rows; j++)
		{
			sum += image.at<uchar>(j, i);
		}
	}
	return sum;
}

    下面進行匹配和輸出

        //進行比較,將圖片與模板相減,然後求全部畫素和,和最小表示越相似,進而完成匹配
	vector<int> seq;//順序存放識別結果
	for (int i = 0; i < myROI.size(); i++)
	{
		Mat subImage;
		int sum = 0;
		int min = 100000;
		int min_seq = 0;//記錄最小的和對應的數字
		for (int j = 0; j < 10; j++)
		{
			//計算兩個圖片的差值
			absdiff(myROI[i], myTemplate[j], subImage);
			sum = getPiexSum(subImage);
			if (sum < min)
			{
				min = sum;
				min_seq = j;
			}
			sum = 0;
		}
		seq.push_back(min_seq);
	}

	//輸出結果
	cout << "識別結果為:";
	for (int i = 0; i < seq.size(); i++)
	{
		cout << seq[i];
	}
	cout << endl;

七、總結

        識別的結果圖片:

    存在的不足:這種識別方式有個不好的點就是,模板的數字大小(比如我用的是48號字型大小)不能與待識別的數字的大小相差太多,否則會降低識別準確率。如果相差不是很大的話是可以準確識別的。然後該程式也只能識別單行數字,暫時還不能識別多行。但是可以在本程式的基礎上做點修改就可以了。等以後有時間了再回來解決這兩個問題。

    附上程式原始碼下載:https://download.csdn.net/download/m0_37543178/10662148