前兩篇文章分別介紹了級聯分類器的原理和源碼解析,下面我們給出一個具體的應用實例。
下面我們以車牌識別爲例,具體講解OpenCV的級聯分類器的用法。在這裏我們只對藍底白字的普通車牌進行識別判斷,對於其他車牌不在考慮範圍內。而且車牌是正面照,略微傾斜可以,傾斜程度太大也是不在識別範圍內的。
我們通過不同渠道共收集了1545幅符合要求的帶有車牌圖像的照片(很遺憾,我只能得到這麼多車牌照片,如果能再多一些就更好了!),通過ACDSee軟件手工把車牌圖像從照片中剪切出來,並統一保存爲jpg格式。爲便於後續處理,我們把文件名按照數字順序命名,如圖8所示。然後我們把這些車牌圖像保存到pos文件夾內。
圖8 藍底白字車牌圖像
需要注意的是,在這裏我們沒有必要把車牌圖像縮放成統一的尺寸(即正樣本圖像的大小),更沒有必要把它們轉換成灰度圖像,這些工作完全可以由系統完成。我們只需要告訴系統車牌圖像文件、車牌的位置,以及車牌的尺寸大小即可。
爲了高效的完成上述工作,我們編寫了以下代碼:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <fstream>
#include <string>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
ofstream postxt("pos.txt",ios::out); //創建pox.txt文件
if ( !postxt.is_open() )
{
cout<<"can not creat pos txt file!";
return false;
}
//N表示車牌圖像的總數,c表示最終可以利用的車牌樣本圖像的數量
int N = 1545, c = 0;
int width, height, i;
String filename;
Mat posimage;
for(i=0;i<N;i++) //遍歷所有車牌圖像
{
filename = to_string(i) + ".jpg"; //得到當前車牌圖像的文件名
posimage = imread("pos\\" + filename); //打開當前車牌圖像
if ( posimage.empty() )
{
cout<<"can not open "+ filename +" file!"<<endl;
continue;
}
width = posimage.size().width; //當前車牌圖像的寬
height = posimage.size().height; //當前車牌圖像的高
//如果當前車牌圖像的寬小於60,或高小於20,則剔除該車牌圖像
if(width < 60 || height < 20)
{
cout<<filename +" too small!"<<endl;
continue;
}
//把當前車牌圖像的信息寫入pos.txt文件內
postxt<<"pos/" + filename + " 1 0 0 " + to_string(width) + " " + to_string(height)<<endl;
c++; //累計
}
cout<<c; //終端輸出c值
postxt.close(); //關閉pos.txt文件
return 0;
}
執行完該程序後,在終端輸出得到的c值爲1390,這說明有155(1545-1390)個車牌圖像由於尺寸過小而被剔除。另外,在當前目錄下我們還得到了pos.txt文件,該文件正是系統所需要的,它的文件內容如圖9所示。
圖9 pos.txt文件
在pos.txt文件中,每一行代表一個圖像文件。我們以第一行爲例,它表示pos文件夾內的0.jpg文件,後面的“1”表示該文件只有一個樣本圖像(即車牌),再後面的“0 0”表示該樣本圖像的左上角座標,由於我們已經對圖像進行了剪切,每個jpg文件就是一幅完成的車牌,所以所有行的這三個變量都是“1 0 0”。最後的“450 140”表示0.jpg文件的寬和高。
我們收集了10589幅大小不同的不含車牌圖像的無水印、無logo、無日期的照片。這些照片統一轉換爲jpg格式,並且也是按照數字的順序命名,如圖10所示。然後我們把這些照片放入neg文件夾內。
圖10 不含車牌圖像的照片
這些照片的尺寸沒有要求,只要大於正樣本圖像的尺寸即可,因爲系統是對這些照片進行剪切,從而得到與正樣本圖像尺寸相同的負樣本圖像,所以一幅照片可以得到若干個負樣本圖像。這些照片儘量保證多樣性,並且每幅照片的內容儘可能的豐富,當然最重要的一點是不能含有車牌信息。
我們還需要爲系統提供一個保存有這些照片信息的文本文件。同樣的,我們也寫了一段簡單的程序來完成這個工作:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <fstream>
#include <string>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
ofstream negtxt("neg.txt",ios::out); //創建neg.txt文件
if ( !negtxt.is_open() )
{
cout<<"can not creat neg txt file!";
return false;
}
//N表示照片的總數,c表示最終得到的照片的數量
int N = 10589, c=0;
int i;
String filename;
Mat posimage;
for(i=0;i<N;i++)
{
filename = to_string(i) + ".jpg"; //照片文件名
posimage = imread("neg\\" + filename); //打開當前照片
if ( posimage.empty() )
{
cout<<"can not open "+ filename +" file!"<<endl;
continue;
}
negtxt<<"neg/" + filename<<endl; //向neg.txt文件寫入照片文件名
c++; //累加
}
cout<<c; //終端輸出c值
negtxt.close(); //關閉neg.txt文件
return 0;
}
執行完該程序後,在當前目錄下得到了neg.txt文件,它的文件內容如圖11所示。
圖11 neg.txt文件
以上內容準備好後,我們就可以利用Opencv提供的相關程序得到能夠識別車牌的級聯分類器了。
首先在D盤下新建plate文件夾,我們把前面提到的保存有大量照片圖像的pos和neg這兩個文件夾、以及pos.txt和neg.txt這兩個文本文件複製到plate文件夾內,再在plate文件夾內新建data文件夾(後面需要)。由於本人的電腦是64位win7系統,編譯器使用的是Microsoft Visual Studio 2012,因此需要從opencv/build/x64/vc11/bin文件夾內複製opencv_createsamples.exe和opencv_traincascade.exe這兩個文件到plate文件夾內。opencv_createsamples.exe用於創建系統所需的正樣本vec文件,opencv_traincascade.exe用於訓練級聯分類器。這兩個文件都需要在命令行下運行。
opencv_createsamples.exe所需的參數較多,這裏我們只把要用到的參數進行講解:
-info:用於表示含有車牌照片的文本文件,即pos.txt
-bg:用於表示不含車牌照片的文本文件,即neg.txt
-vec:輸出的正樣本vec文件名,我們把這個文件命名爲pos.vec
-num:車牌照片圖像的數量,即1390
-w:正樣本圖像的寬(像素)
-h:正樣本圖像的高(像素)
後兩個參數需要我們根據實際情況填寫,由於我們只對藍底白字的車牌進行識別,這類車牌的實際尺寸爲440mm×140mm,我們必須要保持正樣本圖像的寬和高也是這個比例,而且寬和高不能過大,更不能過小。綜合考慮,我們選擇:-w爲58,-h爲18。
在前面我們準備車牌照片時,並沒有把車牌縮放成58×18這個尺寸,這是因爲opencv_createsamples.exe會根據-w和-h這兩個參數對圖像進行統一縮放處理的,所以前面就沒有處理。
最終的opencv_createsamples.exe命令爲:
opencv_createsamples.exe -info pos.txt -bg neg.txt -vec pos.vec -num 1390 -w 58 -h 18
爲方便起見,我們把這個命令保存到createsamples.bat批處理文件中,這樣只要執行該文件即可。執行的結果如圖12所示,並且在plate文件夾內會生成pos.vec文件。
圖12 opencv_createsamples.exe執行結果
下面就要執行opencv_traincascade.exe來訓練級聯分類器,該命令所需要的參數也較多,但都很重要,它們的含義如下:
-data:文件夾名,用於保存訓練生成的各種xml文件,該文件夾一定要事先創建好,否則系統會報錯,在這裏,我們定義該文件夾名爲data,它已在前面創建好
-vec:由opencv_createsamples.exe程序生成的正樣本vec文件,即pos.vec
-bg:用於表示不含車牌照片的文本文件,即neg.txt
-numPos:訓練級聯分類器的每一級分類器(即強分類器)時所用的正樣本數目
-numNeg:訓練級聯分類器的每一級分類器(即強分類器)時所用的負樣本數目
-numStages:最終得到的級聯分類器的級數,我們設置爲12
-precalcValBufSize:用於存儲預先計算特徵值的內存空間大小,單位爲MB
-precalcIdxBufSize:用於存儲預先計算特徵索引的內存空間大小,單位爲MB
-stageType:強分類器的類型,目前只實現了AdaBoost,因此唯一的值(缺省值)爲BOOST
-featureType:特徵類型,HAAR(缺省值),LBP或HOG
-w:正樣本圖像的寬,必須與opencv_createsamples.exe命令的參數一致,即58
-h:正樣本圖像的高,必須與opencv_createsamples.exe命令的參數一致,即18
-bt:AdaBoost的類型,DAB,RAB,LB或GAB(缺省值)
-minHitRate:原理部分提到的每級分類器的最小識別率
-maxFalseAlarmRate:原理部分提到的每級分類器的最大錯誤率
-weightTrimRate:用於決策樹的剪枝,缺省值爲0.95
-maxDepth:決策樹的最大深度,缺省值爲1,即該決策樹爲二叉樹(樹墩形)
-maxWeakCount:強分類器所包含的最大決策樹的數量,該值也與最大錯誤率有關,我們定義該值爲150
-mode:如果特徵爲HAAR,則該參數決定了使用哪種HAAR狀特徵(見圖1),BASIC(缺省值)、CORE或ALL
下面我們就重點介紹幾個重要參數的選取。由於本人的計算機的內存爲16G,爲了最大化的利用該內存,我們把-precalcValBufSize和-precalcIdxBufSize這兩個參數值都定義爲5000,即5G。最小識別率和最大錯誤率決定了訓練時間的長短和識別的質量,我們定義這兩個值分別爲0.999和0.25。-numPos指的是訓練強分類器時所用的正樣本數量,它並不是全體正樣本的數量,原則上該值越大,分類器的質量越好,但還要考慮識別率,如果識別率設置得不高,會有一些正樣本被識別爲負樣本,因此要有一定的冗餘,當然系統也考慮到了這點,即如果正樣本都用完了,並且還沒有達到numPos所指定的數量,則系統會調整該值爲實際的數量(詳細內容見前面的源碼分析部分)。我們設置該值爲1300。-numNeg設置爲多大似乎還沒有定論,但通過閱讀Viola & Jones算法的原文發現,他們使用9832個正樣本(4916個人臉圖像,再加上它們的垂直鏡像圖像)和10000個負樣本,正、負樣本的數量接近於1:1,因此我們設置numNeg爲1350。
最終的opencv_traincascade.exe命令爲:
opencv_traincascade.exe -data data -vec pos.vec -bg neg.txt -numPos 1300 -numNeg 1350 -numStages 12 -precalcValBufSize 5000 -precalcIdxBufSize 5000 -w 58 -h 18 -maxWeakCount 150 -mode ALL -minHitRate 0.999 -maxFalseAlarmRate 0.25
同理,我們也把這個命令保存到批處理文件train.bat中。這裏還需要注意一點的是:參數的大小寫一定要分區,否則系統出錯。
圖13 opencv_traincascade.exe執行過程中輸出的參數信息
圖14 opencv_traincascade.exe執行過程中輸出的第3級強分類器的信息
在執行該命令時,終端首先輸出一些參數信息,如圖13所示。然後是輸出級聯分類器的每級強分類器的訓練信息,因爲我們設置了numStages爲12,所以一共有12個強分類器:0-stage至11-stage。圖14所示爲第3級強分類器的信息。下面我們逐條分析這些信息的含義:
===== TRAINING 3-stage =====
<BEGIN
表示開始訓練第3級強分類器。
POS count : consumed 1300 : 1302
在訓練本級強分類器時,能夠使用1300個正樣本圖像,而這1300個正樣本圖像是從1302個正樣本圖像集中選取出來的,也就是說此時有兩個正樣本沒有被識別出來。前面的1300正是opencv_traincascade.exe命令中參數numPos所指定的數量,有時這個值會小於numPos,說明numPos設置過大,並且最小識別率設置的較小,從而導致正樣本圖像數量不足。後面的1302可以用來表示當前級聯分類器的識別率,即由0-stage、1-stage、2-stage組成的級聯分類器的識別率。此時的識別率爲99.846%,因爲1300÷1302=0.99846。
NEG count : acceptanceRatio 1350 : 0.00620359
在訓練本級強分類器時,能夠使用1350個負樣本圖像,這個數正是opencv_traincascade.exe命令中參數numNeg所指定的數量,當然這個數也有可能小於numNeg,這是因爲前面信息中POS count的數值不等於numPos所致,具體數值的大小見源碼分析。後面的0.00620359表示負樣本的接受率,也就是當前強分類器之前的所有強分類器(0-stage、1-stage、2-stage)構成的級聯分類器的錯誤率,即經過當前級聯分類器預測後,這些被預測爲正樣本而實際爲負樣本的1350幅圖像是從多少個負樣本圖像中得到的。級聯分類器的特點是後一級的強分類器只接收那些前面分類器認爲是正樣本的數據,把負樣本預測爲正樣本,這種情況會隨着訓練級數的增加,困難程度也在增加,當然這種困難程度還與opencv_traincascade.exe命令中所設置的最大錯誤率maxFalseAlarmRate有關,錯誤率設置的越低,困難程度會越大。以本級爲例,這1350個負樣本是從二十多萬個負樣本中選擇出來的,計算公式爲:1350÷0.00620359≈217615。在本例的最後一級強分類器的訓練中,這個數值甚至會高達十億。所以訓練過程中的時間消耗主要就在這裏。在沒有顯示該行信息之前,終端輸出的是下列信息:NEG current samples: XXXX。XXXX代表着當前時刻得到的負樣本數量,這個數值會逐漸增加,當增加到1350時,則會正常顯示上面的信息。當此時得到的級聯分類器的錯誤率小於我們所設置的錯誤率時(以此時爲例,當前已得到了3個強分類器:0-stage、1-stage、2-stage,現在要訓練第4個強分類器3-stage,當這個強分類器訓練好後,這4個強分類器構成的級聯分類器應該滿足的最大錯誤率爲:0.25×0.25×0.25×0.25=0.00390625),則系統會停止訓練,因爲當前得到的級聯分類器已經滿足了要求,無需再訓練下去了。
Precalculation time: 52.337
表示預先計算特徵值所消耗的時間,即在沒有構建強分類器之前,我們就把一部分特徵值計算好了,該值與opencv_traincascade.exe命令中的參數precalcValBufSize和precalcIdxBufSize有關,也就是我們事先爲此開闢的內存越大,所保存的特徵值就越多,因此計算這些特徵值所花費的時間就越長。由於在構建強分類器之前,要用到的特徵值都已計算好,所以構建強分類器的時間就大大縮短了。
+------+-------------+-------------+
| N| HR | FA |
+------+-------------+-------------+
| 1| 1| 1|
+------+-------------+-------------+
| 2| 1| 1|
+------+-------------+-------------+
…… ……
+------+-------------+-------------+
| 10|0.999231|0.336296|
+------+-------------+-------------+
| 11|0.999231|0.228148|
+------+-------------+-------------+
N表示當前強分類器的弱分類器(即決策樹)的訓練得到的數量,HR表示當前強分類器的識別率,FA表示當前強分類器的錯誤率。我們從倒數第2行開始,此時訓練得到了10棵決策樹,識別率爲99.9231%,錯誤率爲33.6296%,識別率滿足了要求,即大於最小識別率99.9%,但錯誤率不滿足要求,即它大於最大錯誤率25%,所以還需要繼續訓練,當又得到了一棵決策樹時(即此時有11棵決策樹),識別率和錯誤率都滿足了要求(99.9231%>99.9%,22.8148%<25%)。
END>
表示此時該級的強分類器已經得到,因爲識別率和錯誤率都滿足了要求,所以此級強分類器的訓練結束。
Training until now has taken 0 days 0 hours27 minutes 2 seconds.
表示到目前爲止,訓練級聯分類器共用時27分2秒。
圖15 opencv_traincascade.exe命令執行結束
圖15顯示了整個級聯分類器訓練完成後的界面,可以看出一共訓練了10多個小時。我的計算機的CPU是Intel Core i5-4690K。如果我們把識別率和錯誤率分別改爲0.9995和0.2,則需要一天多的時間,如果再把級數調整爲13級,則需要6天。
當訓練結束後,在data文件夾內會得到cascade.xml文件,這正是我們需要的級聯分類器數據,我們利用它就可以識別出車牌。
下面的程序是一個簡單的應用:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/objdetect/objdetect.hpp"
#include <iostream>
#include <fstream>
#include <string>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
CascadeClassifier classifier("cascade.xml"); //實例化級聯分類器
Mat img = imread("car.jpg"); //讀取照片
vector<Rect> plates; //代表車牌區域
//車牌識別,默認識別的最小車牌爲正樣本的面積(這裏就是58×18),最大爲整幅照片的面積,即只能識別面積爲58×18以上的車牌
classifier.detectMultiScale(img, plates);
for(int I = 0; i < plates.size(); i++) //畫出車牌區域
rectangle(img, plates[i], Scalar(255, 0, 255), 2);
imshow("plates", img);
waitKey(0);
return 0;
}
圖16 識別結果
圖16爲運行的效果。由於手上的車牌照片不多,無法對識別效果做全面的衡量,但從不多的實驗結果來看,雖然有錯檢的情況,檢測到的車牌也有不完整的現象,但基本上能夠滿足要求。我通過一些實驗發現,單純的提高識別率或降低錯誤率、以及增加級數似乎都不能改善上述問題,我認爲只有增大正樣本的數量纔是提高識別質量的有效方法。
下面是對視頻文件進行車牌識別:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/objdetect/objdetect.hpp"
#include <iostream>
#include <fstream>
#include <string>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
VideoCapture vedio("cars.avi"); //讀取視頻
if(!vedio.isOpened())
{
cout<<"視頻打開失敗!"<<endl;
return 1;
}
double rate= vedio.get(CV_CAP_PROP_FPS); //得到幀頻
int delay= int(1000/rate); //定義一個延時時間
Mat frame;
Size size = Size(int(vedio.get( CV_CAP_PROP_FRAME_WIDTH )), int(vedio.get( CV_CAP_PROP_FRAME_HEIGHT ))); //視頻圖像的尺寸
//定義一個寫入視頻文件
VideoWriter writer("plates.avi", CV_FOURCC('M','J','P','G'), rate, size, true);
if (!writer.isOpened())
{
cout << "初始化VideoWriter失敗!" << endl;
return 1;
}
CascadeClassifier classifier("cascade.xml");
while(true)
{
if (!vedio.read(frame))
break;
vector<Rect> plates;
//車牌檢測,這裏設定車牌的最大尺寸爲190×60
classifier.detectMultiScale(frame, plates, 1.1, 3, 0, Size(), Size(190, 60));
for(int i = 0; i < plates.size(); i++)
rectangle(frame, plates[i], Scalar(255, 0, 255), 2);
//加上文字
putText(frame,"http://blog.csdn.net/zhaocj",Point(50,60),CV_FONT_HERSHEY_COMPLEX,0.7,Scalar(255,0,0), 2);
writer.write(frame); //寫視頻
if (cv::waitKey(delay)>=0)
break;
}
vedio.release();
return 0;
}
我把視頻的結果上傳到了下列網址。該視頻爲3分鐘,可以看出,在車牌的可識別尺寸範圍內,能夠準確識別車牌,當然,也有錯檢和車牌識別不完整的現象:
http://v.youku.com/v_show/id_XMjI4ODM3Mjk1Ng==.html
另外,我把cascade.xml文件也上傳到了下列網址,大家可以下載檢驗:
http://download.csdn.net/detail/zhaocj/9737259