1. 程式人生 > >基於OpenCV3.0的車牌識別系統設計(二)--車牌提取

基於OpenCV3.0的車牌識別系統設計(二)--車牌提取

寫在前面的話

上一篇開篇博文寫好之後找女朋友看了一下,希望她提一點建設性建議。結果她很委婉的告訴我,寫的還行就是太表面了,告訴我要注意細節的描述與具體的實現過程與原理等等。其實我只是想騙她看一下增加一下點選量,順便知道我寫的部落格新手能不能看懂而已。結果她告訴我,她那麼聰明當然能看懂,別人就未必能看懂了!!!吼吼吼,信了她的邪,什麼時候都不忘記讚美一下她自己的小妖精。上一篇寫的不好的話請各位見諒,吾理小子文筆有點差,但是我會盡自己的最大努力把自己懂的東西分享出來,希望對你有用。為了講述清楚車牌識別的每一個環節,吾理小子決定把寫好的工程原始碼重新解剖,在每一個環節中貼出該部分完整的程式碼,新手可直接拿去執行觀看實際效果。各位感動嗎!

進入正題,上次重點介紹了車牌識別的流程,本文來詳細聊一下車牌提取的實現過程。

車牌提取方法

車牌提取通常也稱為車牌定位,其目的是從含有車牌的影象中找到車牌區域。車牌定位的重要性不言而喻,作為車牌識別的第一個步驟,車牌區域的提取成功與否是完成車牌識別的基礎也是首要決定因素。車牌提取的方法有很多,不同的分類方法有不同的叫法。通過對常見的幾種方法的歸納總結,見下圖:(編輯一張圖片,噴出一口鮮血)

本文著重講解基於顏色資訊的定位方法。上圖中可以看到基於彩色資訊的定位方法具有準確、快速、精確等優點,對於新手也更加容易理解。通過對質量較高的原始影象進行相應處理之後,定位出車牌區域。現象明顯,一氣呵成。有助與新手繼續研究下去。

到目前為止,關於車牌定位的問題國內外眾多學者提出了很多方法,適用條件不盡相同,各有利弊。弄懂了基本的顏色定位之後,可以嘗試結合其它的方法來提升車牌定位的效能。

車牌特點

說完車牌定位的方法之後,需要對我們提取的物件本身的特點進行說明。車牌的大小固定,字型格式是由國家相關部門統一規定,與一般常見的字型格式都不一樣。據說是由常見的宋體字修改而來,一般民用的車牌包含數字0-9,還有全國26個省市的簡稱,總共是36個字元。車牌的顏色以及字型顏色如下:

藍牌白字:普通小型車(其中包括政府機關專用號段、政法部門警車以外的行政用車)的牌照
           黃牌黑字:大型車輛、摩托車、駕校教練車牌照
           黑牌白字:涉外車輛牌照,式樣和藍牌基本相同
                  白牌:政法部門(公安、法院、檢察院、國安、司法)警車、武警部隊車輛、解放軍軍車的牌照都是白牌
                  警車:公安警車的牌照樣式為[某·A1234警],除“警”為紅字外其他的都是黑字,一共4位數字,含義與普通牌照相同

我們研究的目標是普通民用小型車,也就是藍牌白字車牌。說到這裡,順便說明一下藍牌白字車牌的顏色資訊,對於正常曝光的影象而言,藍色車牌的三個通道值大約為Blue=138,Green=63,Red=23。除了顏色資訊外,車牌形狀為矩形,具有固定的長寬比3:1。知道車牌的這些資訊後,在使用顏色資訊定位時可以做為限制條件,這樣可以提高車牌定位的準確性。

車牌提取的步驟

講完車牌提取的方法和車牌本身的特點之後,接下來仔細說明基於顏色資訊的定位方法各個環節處理效果圖。

第一步:讀取待處理影象

首先,讀取待處理的彩色影象,判斷影象是否讀取成功,成功時顯示原始影象。最後列印影象的長和寬,方便對影象的尺寸有一個瞭解。

Mat OriginalImg ;
OriginalImg = imread("TestPhoto (1).jpg", IMREAD_COLOR);//讀取原始彩色影象
	if (OriginalImg.empty())  //判斷影象對否讀取成功
	{
		cout << "錯誤!讀取影象失敗\n";
		return -1;
	}
	imshow("原圖", OriginalImg); //顯示原始影象
	cout << "Width:" << OriginalImg.rows << "\tHeight:" << OriginalImg.cols << endl;//列印影象長寬

執行效果

第二步:影象尺寸變換

讀取原影象後,可以看到原影象畫素較高,對於車牌識別而言,過高的解析度對識別結果效果太大的幫助,反而會影響識別的速度,也就是系統實時性會變差。所以在這裡對尺寸進行統一變換,在保證輸入影象的長寬比不變的情況下,將影象的長度變成640,相應的寬度可以通過計算得到。

    Mat ResizeImg; 
	if (OriginalImg.cols > 640)
		resize(OriginalImg, ResizeImg, Size(640, 640* OriginalImg.rows / OriginalImg.cols));
	imshow("尺寸變換圖", ResizeImg);

上圖和原圖的區別就是尺寸不一致。後續的處理步驟都是在縮放之後的影象基礎上來做,適當解析度的影象有助於提高系統的響應速度,也就是實時性。

第三步:基於顏色資訊二值化

基於顏色的二值化處理就是通過顏色資訊將影象二值化,上面已經提到了正常曝光的車牌各個通道的顏色資訊大約是Blue=138,Green=63,Red=23。但是顏色資訊有一定的偏差,因此在二值化時放寬顏色條件,然後再通過其他特點來精確尋找車牌區域。本文設定各個通道的偏差值為50。程式如下,注意觀察顏色資訊以及偏差值在程式中的體現。

unsigned char pixelB, pixelG, pixelR;  //記錄各通道值
unsigned char DifMax = 50;             //基於顏色區分的閾值設定
unsigned char B = 138, G = 63, R = 23; //各通道的閾值設定,針對與藍色車牌
Mat BinRGBImg = ResizeImg.clone();  //二值化之後的影象
int i = 0, j = 0;
for (i = 0; i < ResizeImg.rows; i++)   //通過顏色分量將圖片進行二值化處理
{
	for (j = 0; j < ResizeImg.cols; j++)
	{
		pixelB = ResizeImg.at<Vec3b>(i, j)[0]; //獲取圖片各個通道的值
		pixelG = ResizeImg.at<Vec3b>(i, j)[1];
		pixelR = ResizeImg.at<Vec3b>(i, j)[2];

		if (abs(pixelB - B) < DifMax && abs(pixelG - G) < DifMax && abs(pixelR - R) < DifMax)
		{                                           //將各個通道的值和各個通道閾值進行比較
			BinRGBImg.at<Vec3b>(i, j)[0] = 255;     //符合顏色閾值範圍內的設定成白色
			BinRGBImg.at<Vec3b>(i, j)[1] = 255;
			BinRGBImg.at<Vec3b>(i, j)[2] = 255;
		}
		else
		{
			BinRGBImg.at<Vec3b>(i, j)[0] = 0;        //不符合顏色閾值範圍內的設定為黑色
			BinRGBImg.at<Vec3b>(i, j)[1] = 0;
			BinRGBImg.at<Vec3b>(i, j)[2] = 0;
		}
	}
}
imshow("基於顏色資訊二值化", BinRGBImg);        //顯示二值化處理之後的影象

第四步:形態學處理

基於顏色資訊二值化的影象效果還是挺不錯的,可以看到車牌區域基本完整,其他地方有一些細小的干擾,接下來進行形態學處理,消除小區域干擾。形態學閉操作——先膨脹後腐蝕,是影象的基本操作之一,其特點是填充細小空間,連線臨近物體和平滑邊界,不同矩形窗的大小會有不同的結果。(形態學基本知識可參考數字影象處理相關書籍)

Mat BinOriImg;     //形態學處理結果影象
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3)); //設定形態學處理窗的大小
dilate(BinRGBImg, BinOriImg, element);     //進行多次膨脹操作
dilate(BinOriImg, BinOriImg, element);
dilate(BinOriImg, BinOriImg, element);

erode(BinOriImg, BinOriImg, element);      //進行多次腐蝕操作
erode(BinOriImg, BinOriImg, element);
erode(BinOriImg, BinOriImg, element);
imshow("形態學處理後", BinOriImg);        //顯示形態學處理之後的影象

第五步:增加限制條件尋找車牌區域並框選車牌

從上圖中可以看到車牌的基本位置已經能夠確定,接下來通過其他特點來進一步確定車牌區域。處理思路如下:

1.尋找各個空白區域外輪廓並計算面積;

2.為各個空白區域增加外接矩形並計算面積;

3.通過外輪廓面積與外接矩形的比值,判斷區域的矩形度;

4.進一步判斷長寬比;

5.滿足全部條件確定車牌區域

double length, area, rectArea;     //定義輪廓周長、面積、外界矩形面積
double rectDegree = 0.0;           //矩形度=外界矩形面積/輪廓面積
double long2Short = 0.0;           //體態比=長邊/短邊
CvRect rect;           //外界矩形
CvBox2D box, boxTemp;  //外接矩形
CvPoint2D32f pt[4];    //矩形定點變數
double axisLong = 0.0, axisShort = 0.0;        //矩形的長邊和短邊
double axisLongTemp = 0.0, axisShortTemp = 0.0;//矩形的長邊和短邊
double LengthTemp;     //中間變數
float  angle = 0;      //記錄車牌的傾斜角度
float  angleTemp = 0;
bool   TestPlantFlag = 0;  //車牌檢測成功標誌位
cvtColor(BinOriImg, BinOriImg, CV_BGR2GRAY);   //將形態學處理之後的影象轉化為灰度影象
threshold(BinOriImg, BinOriImg, 100, 255, THRESH_BINARY); //灰度影象二值化
CvMemStorage *storage = cvCreateMemStorage(0);
CvSeq * seq = 0;     //建立一個序列,CvSeq本身就是一個可以增長的序列,不是固定的序列
CvSeq * tempSeq = cvCreateSeq(CV_SEQ_ELTYPE_POINT, sizeof(CvSeq), sizeof(CvPoint), storage);
int cnt = cvFindContours(&(IplImage(BinOriImg)), storage, &seq, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
//第一個引數是IplImage指標型別,將MAT強制轉換為IplImage指標型別
//返回輪廓的數目 
//獲取二值影象中輪廓的個數
cout << "number of contours   " << cnt << endl;  //列印輪廓個數
for (tempSeq = seq; tempSeq != NULL; tempSeq = tempSeq->h_next)
{
	length = cvArcLength(tempSeq);       //獲取輪廓周長
	area = cvContourArea(tempSeq);       //獲取輪廓面積
	if (area > 800 && area < 50000)     //矩形區域面積大小判斷
	{
		rect = cvBoundingRect(tempSeq, 1);//計算矩形邊界
		boxTemp = cvMinAreaRect2(tempSeq, 0);  //獲取輪廓的矩形
		cvBoxPoints(boxTemp, pt);              //獲取矩形四個頂點座標
		angleTemp = boxTemp.angle;                 //得到車牌傾斜角度

		axisLongTemp = sqrt(pow(pt[1].x - pt[0].x, 2) + pow(pt[1].y - pt[0].y, 2));  //計算長軸(勾股定理)
		axisShortTemp = sqrt(pow(pt[2].x - pt[1].x, 2) + pow(pt[2].y - pt[1].y, 2)); //計算短軸(勾股定理)

		if (axisShortTemp > axisLongTemp)   //短軸大於長軸,交換資料
		{
			LengthTemp = axisLongTemp;
			axisLongTemp = axisShortTemp;
			axisShortTemp = LengthTemp;
		}
		else
			angleTemp += 90;
		rectArea = axisLongTemp * axisShortTemp;  //計算矩形的面積
		rectDegree = area / rectArea;     //計算矩形度(比值越接近1說明越接近矩形)

		long2Short = axisLongTemp / axisShortTemp; //計算長寬比
		if (long2Short > 2.2 && long2Short < 3.8 && rectDegree > 0.63 && rectDegree < 1.37 && rectArea > 2000 && rectArea < 50000)
		{
			Mat GuiRGBImg = ResizeImg.clone();
			TestPlantFlag = true;             //檢測車牌區域成功
			for (int i = 0; i < 4; ++i)       //劃線框出車牌區域
				cvLine(&(IplImage(GuiRGBImg)), cvPointFrom32f(pt[i]), cvPointFrom32f(pt[((i + 1) % 4) ? (i + 1) : 0]), CV_RGB(255, 0, 0));
			imshow("提取車牌結果圖", GuiRGBImg);    //顯示最終結果圖

			box = boxTemp;
			angle = angleTemp;
			axisLong = axisLongTemp;
			axisShort = axisShortTemp;
			cout << "傾斜角度:" << angle << endl;
		}
	}
}

框選結果如下圖所示。這部分牽扯到的變數較多,主要看思路,不必糾結為什麼設定這麼多變數,因為後面需要用到這些變數。

不知道細心的小夥伴有沒有發現,其中有一個變數是傾斜角度。其實這個變數儲存的是車牌區域的傾斜角度,通過觀察原圖中車牌的位置和姿態,和輸出的傾斜角度的值可以明白這個值表達的含義。後續步驟就是車牌的傾斜矯正。各位首先有一個大致的瞭解。

車牌提取效果展示

下面展示一些提取車牌區域的效果圖。

車牌提取原始碼

說完各個步驟的具體操作,最後貼上設計到的原始碼。各位自行驗證改進!


#include <iostream>
#include <opencv2\opencv.hpp>

using namespace std;
using namespace cv;

int main(int,char *argv[])
{
	Mat OriginalImg;

	OriginalImg = imread("TestPhoto (1).jpg", IMREAD_COLOR);//讀取原始彩色影象
	if (OriginalImg.empty())  //判斷影象對否讀取成功
	{
		cout << "錯誤!讀取影象失敗\n";
		return -1;
	}
//	imshow("原圖", OriginalImg); //顯示原始影象
	cout << "Width:" << OriginalImg.rows << "\tHeight:" << OriginalImg.cols << endl;//列印長寬

	Mat ResizeImg; 
	if (OriginalImg.cols > 640)
		resize(OriginalImg, ResizeImg, Size(640, 640 * OriginalImg.rows / OriginalImg.cols));
	imshow("尺寸變換圖", ResizeImg);

	unsigned char pixelB, pixelG, pixelR;  //記錄各通道值
	unsigned char DifMax = 50;             //基於顏色區分的閾值設定
	unsigned char B = 138, G = 63, R = 23; //各通道的閾值設定,針對與藍色車牌
	Mat BinRGBImg = ResizeImg.clone();  //二值化之後的影象
	int i = 0, j = 0;
	for (i = 0; i < ResizeImg.rows; i++)   //通過顏色分量將圖片進行二值化處理
	{
		for (j = 0; j < ResizeImg.cols; j++)
		{
			pixelB = ResizeImg.at<Vec3b>(i, j)[0]; //獲取圖片各個通道的值
			pixelG = ResizeImg.at<Vec3b>(i, j)[1];
			pixelR = ResizeImg.at<Vec3b>(i, j)[2];

			if (abs(pixelB - B) < DifMax && abs(pixelG - G) < DifMax && abs(pixelR - R) < DifMax)
			{                                           //將各個通道的值和各個通道閾值進行比較
				BinRGBImg.at<Vec3b>(i, j)[0] = 255;     //符合顏色閾值範圍內的設定成白色
				BinRGBImg.at<Vec3b>(i, j)[1] = 255;
				BinRGBImg.at<Vec3b>(i, j)[2] = 255;
			}
			else
			{
				BinRGBImg.at<Vec3b>(i, j)[0] = 0;        //不符合顏色閾值範圍內的設定為黑色
				BinRGBImg.at<Vec3b>(i, j)[1] = 0;
				BinRGBImg.at<Vec3b>(i, j)[2] = 0;
			}
		}
	}
	imshow("基於顏色資訊二值化", BinRGBImg);        //顯示二值化處理之後的影象

	Mat BinOriImg;     //形態學處理結果影象
	Mat element = getStructuringElement(MORPH_RECT, Size(3, 3)); //設定形態學處理窗的大小
	dilate(BinRGBImg, BinOriImg, element);     //進行多次膨脹操作
	dilate(BinOriImg, BinOriImg, element);
	dilate(BinOriImg, BinOriImg, element);

	erode(BinOriImg, BinOriImg, element);      //進行多次腐蝕操作
	erode(BinOriImg, BinOriImg, element);
	erode(BinOriImg, BinOriImg, element);
	imshow("形態學處理後", BinOriImg);        //顯示形態學處理之後的影象

	double length, area, rectArea;     //定義輪廓周長、面積、外界矩形面積
	double rectDegree = 0.0;           //矩形度=外界矩形面積/輪廓面積
	double long2Short = 0.0;           //體態比=長邊/短邊
	CvRect rect;           //外界矩形
	CvBox2D box, boxTemp;  //外接矩形
	CvPoint2D32f pt[4];    //矩形定點變數
	double axisLong = 0.0, axisShort = 0.0;        //矩形的長邊和短邊
	double axisLongTemp = 0.0, axisShortTemp = 0.0;//矩形的長邊和短邊
	double LengthTemp;     //中間變數
	float  angle = 0;      //記錄車牌的傾斜角度
	float  angleTemp = 0;
	bool   TestPlantFlag = 0;  //車牌檢測成功標誌位
	cvtColor(BinOriImg, BinOriImg, CV_BGR2GRAY);   //將形態學處理之後的影象轉化為灰度影象
	threshold(BinOriImg, BinOriImg, 100, 255, THRESH_BINARY); //灰度影象二值化
	CvMemStorage *storage = cvCreateMemStorage(0);
	CvSeq * seq = 0;     //建立一個序列,CvSeq本身就是一個可以增長的序列,不是固定的序列
	CvSeq * tempSeq = cvCreateSeq(CV_SEQ_ELTYPE_POINT, sizeof(CvSeq), sizeof(CvPoint), storage);
	int cnt = cvFindContours(&(IplImage(BinOriImg)), storage, &seq, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
	//第一個引數是IplImage指標型別,將MAT強制轉換為IplImage指標型別
	//返回輪廓的數目 
	//獲取二值影象中輪廓的個數
	cout << "number of contours   " << cnt << endl;  //列印輪廓個數
	for (tempSeq = seq; tempSeq != NULL; tempSeq = tempSeq->h_next)
	{
		length = cvArcLength(tempSeq);       //獲取輪廓周長
		area = cvContourArea(tempSeq);       //獲取輪廓面積
		if (area > 800 && area < 50000)     //矩形區域面積大小判斷
		{
			rect = cvBoundingRect(tempSeq, 1);//計算矩形邊界
			boxTemp = cvMinAreaRect2(tempSeq, 0);  //獲取輪廓的矩形
			cvBoxPoints(boxTemp, pt);              //獲取矩形四個頂點座標
			angleTemp = boxTemp.angle;                 //得到車牌傾斜角度

			axisLongTemp = sqrt(pow(pt[1].x - pt[0].x, 2) + pow(pt[1].y - pt[0].y, 2));  //計算長軸(勾股定理)
			axisShortTemp = sqrt(pow(pt[2].x - pt[1].x, 2) + pow(pt[2].y - pt[1].y, 2)); //計算短軸(勾股定理)

			if (axisShortTemp > axisLongTemp)   //短軸大於長軸,交換資料
			{
				LengthTemp = axisLongTemp;
				axisLongTemp = axisShortTemp;
				axisShortTemp = LengthTemp;
			}
			else
				angleTemp += 90;
			rectArea = axisLongTemp * axisShortTemp;  //計算矩形的面積
			rectDegree = area / rectArea;     //計算矩形度(比值越接近1說明越接近矩形)

			long2Short = axisLongTemp / axisShortTemp; //計算長寬比
			if (long2Short > 2.2 && long2Short < 3.8 && rectDegree > 0.63 && rectDegree < 1.37 && rectArea > 2000 && rectArea < 50000)
			{
				Mat GuiRGBImg = ResizeImg.clone();
				TestPlantFlag = true;             //檢測車牌區域成功
				for (int i = 0; i < 4; ++i)       //劃線框出車牌區域
					cvLine(&(IplImage(GuiRGBImg)), cvPointFrom32f(pt[i]), cvPointFrom32f(pt[((i + 1) % 4) ? (i + 1) : 0]), CV_RGB(255, 0, 0));
				imshow("提取車牌結果圖", GuiRGBImg);    //顯示最終結果圖

				box = boxTemp;
				angle = angleTemp;
				axisLong = axisLongTemp;
				axisShort = axisShortTemp;
				cout << "傾斜角度:" << angle << endl;
			}
		}
	}

	waitKey();
	return 0;

}


好啦,本節就說到這裡。下一篇開始說車牌的傾斜矯正,各位小夥伴期待嗎?