車輛檢測和車道檢測
NKU計算機視覺期末大作業
目錄
-
- 車輛檢測和車道檢測
- 軟體要求
- 車輛檢測
- 根據hog特征進行訓練
- 根據haar特征進行訓練
- 最終檢測
- 直線檢測
- 車輛檢測和車道檢測
軟體要求
- opencv3.0+
- opencv-contrib
- cmake
- CLion編譯器(可選)
- opencv python版本
車輛檢測
車輛檢測的整體架構是結合hog-svm分類器和haar-cascade分類器對車輛進行檢測,之後采用非極大值抑制,得出最終的檢測框。
根據hog特征進行訓練
方向梯度直方圖(Histogram of Oriented Gradient, HOG)特征是一種在計算機視覺和圖像進行中用來進行物體檢測的特征描述子。它通過計算和統計圖像局部區域的梯度方向直方圖來構成特征。HOG特征提取方法就是将一個image(要檢測的目标或者掃描視窗):
1)灰階化(将圖像看做一個x,y,z(灰階)的三維圖像);
2)采用Gamma校正法對輸入圖像進行顔色空間的标準化(歸一化);目的是調節圖像的對比度,降低圖像局部的陰影和光照變化所造成的影響,同時可以抑制噪音的幹擾;
3)計算圖像每個像素的梯度(包括大小和方向);主要是為了捕獲輪廓資訊,同時進一步弱化光照的幹擾。
4)将圖像劃分成小cells(例如6*6像素/cell);
5)統計每個cell的梯度直方圖(不同梯度的個數),即可形成每個cell的descriptor;
6)将每幾個cell組成一個block(例如3*3個cell/block),一個block内所有cell的特征descriptor串聯起來便得到該block的HOG特征descriptor。
7)将圖像image内的所有block的HOG特征descriptor串聯起來就可以得到該image(你要檢測的目标)的HOG特征descriptor了。這個就是最終的可供分類使用的特征向量了。
為了樣本的多樣性,我采用的是部分資料集一的負樣本和資料集二部分正樣本和全部負樣本作為訓練資料。最終正樣本與負樣本的比例為1:3,一共16000張圖檔。
正樣本大緻如下:
負樣本大緻如下:
因為汽車大緻呈現正方形,故對每張圖檔resize到64x64大小,然後提取hog特征。在這裡我選擇的相關系數為:
- block大小:16x16
- window大小:64x64
- cell大小:4x4
- block步長:x方向為8,y方向為8
- window步長:x方向為8,y方向為8
根據如下公式可以算出整個hog特征次元為8100
dimension=9∗blockxcellx∗blockycelly∗(1+windowx−blockxblockstridex)∗(1+windowy−blockyblockstridey) d i m e n s i o n = 9 ∗ b l o c k x c e l l x ∗ b l o c k y c e l l y ∗ ( 1 + w i n d o w x − b l o c k x b l o c k s t r i d e x ) ∗ ( 1 + w i n d o w y − b l o c k y b l o c k s t r i d e y )
如果想要采用不同的步長或者塊大小,可以在
config.cpp
可以修改這些全局變量。
在提取特征之前我們要先将訓練集與測試集寫到兩個txt中友善讀取,考慮到c++檔案讀寫以及科學計算方面不是很友善,在這裡我采用python對資料集進行的劃分處理,利用
numpy,cv2,sklearn.model_selection
可以較為友善的完成,具體方法在/python_func/BuildImgList.py檔案中。
接下來可以提取hog特征,這部分代碼在
GetFeature.cpp
中,引入頭檔案
opencv2/xfeature2d.hpp
,我們可以調用提取hog特征的方法:
Mat GetHOGfeature(string imgname){
Mat img = imread(imgname);
resize(img, img, Size(Imgheight, Imgwidth));
Ptr<HOGDescriptor> hog = new HOGDescriptor(Size(Window_y, Window_x),
Size(block_y, block_x),
Size(block_stride_y, block_stride_x),
Size(cell_y, cell_x), );
assert(hog->getDescriptorSize() == dimension);
vector<float> descriptor;
hog->compute(img, descriptor, Size(Window_stride_y, Window_stride_x), Size(, ));
assert(descriptor.size() == dimension);
Mat s(descriptor);
transpose(s, s);
return s;
}
我們對所有圖檔提取特征,接下來的步驟便是送進支援向量機中進行訓練,這一部分的代碼在
train.cpp
中。在對資料集進行處理時,我把正樣本的label标注為1,把負樣本的label标注為-1。由于這是一個二分類問題,是以在選擇SVM的核類型時,選擇線性核即可。為了求出最優的參數,在這裡采用opencv的machine learning子產品的trainAuto函數(可以自動調節超參數)而非train函數。在調用之前必須要對一些超參數賦予初始值,如下為訓練方法:
void HOGSVMtrainAuto(string trainlist){
Mat Data4Train(, dimension, CV_32FC1), labels(, , CV_32SC1);
GetAllImgHOGfeature(Data4Train, labels, trainlist, ImgTrainPath);
struct timeval pre, after;
gettimeofday(&pre, NULL);
Ptr<ml::SVM> model = ml::SVM::create();
model->setKernel(ml::SVM::KernelTypes::LINEAR);
model->setType(ml::SVM::C_SVC);
model->setP();
model->setC();
model->setGamma();
model->setTermCriteria(cvTermCriteria(CV_TERMCRIT_ITER, , ));
if(debug){
cout << "height: "<<Data4Train.rows << ", width: " << Data4Train.cols << endl;
cout << "trainingdata depth: " << Data4Train.depth() << endl;
cout << "label depth: " << labels.depth() << endl;
cout << "trainingdata type " << Data4Train.type() << endl;
cout << "label type " << labels.type() << endl;
}
assert(Data4Train.type() == CV_32FC1);
assert(labels.type() == CV_32SC1);
Ptr<ml::TrainData> data = ml::TrainData::create(Data4Train, ml::ROW_SAMPLE, labels);
cout << "start training ..." << endl;
model->trainAuto(data, );
cout << "finish training ..." << endl;
gettimeofday(&after, NULL);
cout << "training time: " << after.tv_sec - pre.tv_sec << "s"<< endl;
model->save("../model/svm_hog_classifier.xml");
cout << "model saving fininshed ..." << endl;
}
訓練大概花費20~30分鐘,訓練完成後會生成xml檔案,即訓練好的模型,在測試集上測試,準确率可以達到98%,僅從測試集上看,效果還是不錯的。
根據haar特征進行訓練
Haar-like特征點,是一種簡單的特征描述,其理論相當容易了解,就是按照下圖的模式來計算白色視窗的像素總和和黑色視窗像素總和的差,如下圖:
利用提取到的haar特征可以訓練弱分類器,通過若幹個弱分類器可以組建一個強分類器,類似于一種投票的手段,隻不過不同的分類器具有不同的權重,整個訓練過程可以看做是一個不斷調整權重大小的過程,如下:
接下來便是級聯,如下圖,最終的分類器是由多個強分類器級聯而成。當且僅當通過了所有分類器的判定後才能輸出結果。
僅僅參考圖像的hog特征可能會存在漏檢測。由此,我将人臉識别中常見的級聯器檢測方法遷移到車輛檢測中,參考博文一、博文二和opencv官方文檔。以資料集一的全部正樣本和負樣本為資料源,正負樣本比大概為1:3。首先制作兩個标準格式的txt檔案,一個是正樣本txt,另一個時負樣本txt。正樣本txt格式大緻如下(路徑 圖檔中目标個數 xmin ymin xmax ymax):
../../../data/ProData3/jpg
../../../data/ProData3/BMP
../../../data/ProData3/BMP
...
負樣本txt隻需要圖檔路徑即可,如下:
../../../data/ProData4/jpg
../../../data/ProData4/jpg
../../../data/ProData4/jpg
...
為了友善我用python制作了該txt檔案,具體方法在/adaboost/Gen_Imglist.py中。
opencv提供了opencv_createsamples.exe建立訓練所需要的參數清單,在指令行中調用該exe,輸入如下指令:
根據上圖的解釋可知:-vec為最終生成的檔案,-num為要産生的正樣本的數量,-w為輸出的樣本高度,-h為輸出的樣本寬度。
接下來利用opencv提供的opencv_traincascade.exe進行訓練,在指令行中調用該exe,輸入如下指令:
根據上圖的解釋可知:在訓練過程中所有中間模型都會放在model/adaboost這個檔案夾裡,這裡采用2000個正樣本和7000個負樣本。
級聯器的訓練很慢,大概訓練了一天左右,在模型檔案夾中存放着每一級的弱分類器和最終的分類器。
最終檢測
結合訓練好的hog-svm分類器和haar-cascade分類器。便可以檢測出物體。大緻pipline如下:
- 對圖像進行縮放,resize到448x448
- 我們以64x64的滑動視窗在圖像上滑動,用hog-svm分類器和haar-cascade分類器檢測
- 滑動視窗以一定比例放大,對圖像進行多尺度檢測,避免漏檢較大的車輛
- 對所有結果進行非極大值抑制,得出最終檢測結果
部分檢測代碼如下:
void FinalDetect(string filename, string model_cascade, string model_hog, int dataset = , bool IsLine = false) {
setUseOptimized(true);
setNumThreads();
HOGDescriptor my_hog(Size(Window_y, Window_x), Size(block_y, block_x), Size(block_stride_y, block_stride_x),
Size(cell_y, cell_x), );
CascadeClassifier car_classifier;
car_classifier.load(model_cascade);
//get support vector from model
Ptr<ml::SVM> model = ml::StatModel::load<ml::SVM>(model_hog);
Mat sv = model->getSupportVectors();
vector<float> hog_detector;
const int sv_total = sv.cols;
Mat alpha, svidx;
double rho = model->getDecisionFunction(, alpha, svidx);
Mat alpha2;
alpha.convertTo(alpha2, CV_32FC1);
Mat result(, sv_total, CV_32FC1);
result = alpha2 * sv;
for (int i = ; i < sv_total; ++i)
hog_detector.push_back(- * result.at<float>(, i));
hog_detector.push_back((float) rho);
//load vector to hog detector
my_hog.setSVMDetector(hog_detector);
vector<Rect> detections;
vector<double> foundWeights;
vector<int> rejLevel;
vector<bbox_info> dets;
vector<bbox_info> keep;
VideoCapture cap;
cap.open(filename);
while (true) {
Mat img;
cap >> img;
if (!img.data)
break;
resize(img, img, Size(, ));
cout << img.size() << endl;
if (IsLine)
LineDetect2(img, dataset);
detections.clear();
foundWeights.clear();
rejLevel.clear();
dets.clear();
keep.clear();
my_hog.detectMultiScale(img, detections, foundWeights, , Size(, ), Size(), , , true);
cout << "hog detect object: " << detections.size() << endl;
for (size_t i = ; i < detections.size(); i++) {
if (foundWeights[i] > ) {
bbox_info tmp_bbox(detections[i].x, detections[i].y, detections[i].br().x, detections[i].br().y,
foundWeights[i]);
dets.push_back(tmp_bbox);
}
}
car_classifier.detectMultiScale(img, detections, rejLevel, foundWeights, , , , Size(), Size(), true);
cout << "cascade detect object: " << detections.size() << endl;
for (int i = ; i < detections.size(); i++) {
if (rejLevel[i] < || foundWeights[i] < )
continue;
bbox_info tmp(detections[i].x, detections[i].y, detections[i].br().x, detections[i].br().y,
foundWeights[i]);
dets.push_back(tmp);
}
keep = nms(dets);
for (size_t i = ; i < keep.size(); i++) {
Point p1(keep[i].xmin, keep[i].ymin), p2(keep[i].xmax, keep[i].ymax);
Scalar color(, , );
rectangle(img, p1, p2, color, );
}
imshow("detect", img);
waitKey();
}
}
部分結果如下:
直線檢測
參考博文3,博文4,博文5,對于車道檢測,主要采用如下的pipline:
- 對圖像進行透視變換,使其變為鳥瞰圖:
Point2f origin[] = {Point2f(, ), Point2f(, ), Point2f(, ), Point2f(, )};
Point2f dst[] = {Point2f(, ), Point2f(, ), Point2f(, ), Point2f(, )};
trans = getPerspectiveTransform(origin, dst);
warpPerspective(img_o ,img, trans, img.size());
- 對原圖像進行x-sobel濾波,并進行門檻值過濾
void mag_threshold(const Mat img, Mat &out, int sobel_kernel, int min_thres, int max_thres) {
cvtColor(img, out, CV_BGR2GRAY);
Sobel(out, out, CV_8UC1, , , sobel_kernel);
normalize(out, out, , , NORM_MINMAX);
threshold(out, out, min_thres, , THRESH_TOZERO);
threshold(out, out, max_thres, , THRESH_BINARY);
}
- 對原圖像轉換到HLS空間,保留黃色和白色(車道多為黃色和白色)
void yellow_white_threshold(Mat origin, Mat &out1) {
int y_lower[] = {, , };
int y_upper[] = {, , };
int w_lower[] = {, , };
int w_upper[] = {, , };
Mat HLS, y_mask, w_mask, mask;
cvtColor(origin, HLS, CV_BGR2HLS);
vector<int> yellow_lower(y_lower, y_lower + );
vector<int> yellow_upper(y_upper, y_upper + );
vector<int> white_lower(w_lower, w_lower + );
vector<int> white_upper(w_upper, w_upper + );
inRange(HLS, yellow_lower, yellow_upper, y_mask);
inRange(HLS, white_lower, white_upper, w_mask);
bitwise_or(y_mask, w_mask, mask);
bitwise_and(origin, origin, out1, mask);
cvtColor(out1, out1, CV_HLS2BGR);
cvtColor(out1, out1, CV_BGR2GRAY);
threshold(out1, out1, , , THRESH_BINARY);
}
- 根據2,3步得到最終的二值圖
- 利用霍夫變換找出相應的直線端點(根據直線斜率進行一定的限制)
vector<Vec4i> lines;
vector<Point2f> leftlines;
vector<Point2f> rightlines;
HoughLinesP(out1, lines, , CV_PI / , , , );
cout << lines.size() << endl;
for (size_t i = ; i < lines.size(); i++) {
//abandon horizontal line.
if (lines[i][] == lines[i][])
continue;
//get left lines
if (lines[i][] <= && lines[i][] <=){
float k = ;
//if not verticle line
if (lines[i][] != lines[i][])
k = fabs(float(lines[i][]-lines[i][])/float(lines[i][]-lines[i][]));
if (k>=) {
leftlines.push_back(Point2f(lines[i][], lines[i][]));
leftlines.push_back(Point2f(lines[i][], lines[i][]));
}
}
- 對這些點進行線性回歸
Vec4f line_left, line_right;
fitLine(leftlines, line_left, DIST_L1, , , );
fitLine(rightlines, line_right, DIST_L1, , , );
- 畫出直線圍成的區域,并進行高亮,顯示到原圖上
整個流程圖如下:
源代碼詳見:我的github