車輛檢測和車道檢測
車輛檢測和車道檢測
NKU計算機視覺期末大作業
目錄
軟體要求
- 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
如果想要採用不同的步長或者塊大小,可以在
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), 9);
assert(hog->getDescriptorSize() == dimension);
vector<float> descriptor;
hog->compute(img, descriptor, Size(Window_stride_y, Window_stride_x), Size(0, 0));
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(0, dimension, CV_32FC1), labels(0, 1, 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(1e-2);
model->setC(1);
model->setGamma(1e-2);
model->setTermCriteria(cvTermCriteria(CV_TERMCRIT_ITER, 10000, 0.000001));
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, 10);
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/17622107.jpg 1 0 0 38 38
../../../data/ProData3/112237754.BMP 1 0 0 128 96
../../../data/ProData3/12130486.BMP 1 0 0 128 96
...
負樣本txt只需要圖片路徑即可,如下:
../../../data/ProData4/174157305.jpg
../../../data/ProData4/18635891.jpg
../../../data/ProData4/1356388.jpg
...
為了方便我用python製作了該txt檔案,具體方法在/adaboost/Gen_Imglist.py中。
opencv提供了opencv_createsamples.exe建立訓練所需要的引數列表,在命令列中呼叫該exe,輸入如下命令:
opencv_createsamples -vec pos.vec -info pos_info.dat -bg neg_info.dat -num 2000 -w 24 -b 24
根據上圖的解釋可知:-vec為最終生成的檔案,-num為要產生的正樣本的數量,-w為輸出的樣本高度,-h為輸出的樣本寬度。
接下來利用opencv提供的opencv_traincascade.exe進行訓練,在命令列中呼叫該exe,輸入如下命令:
opencv_traincascade -data ../model/adaboost -vec pos.vec -bg neg_info.dat -numPos 2000 -numNeg 7000
根據上圖的解釋可知:在訓練過程中所有中間模型都會放在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 = 1, bool IsLine = false) {
setUseOptimized(true);
setNumThreads(8);
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), 9);
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(0, alpha, svidx);
Mat alpha2;
alpha.convertTo(alpha2, CV_32FC1);
Mat result(1, sv_total, CV_32FC1);
result = alpha2 * sv;
for (int i = 0; i < sv_total; ++i)
hog_detector.push_back(-1 * result.at<float>(0, 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(448, 448));
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, 0, Size(8, 8), Size(), 1.1, 2., true);
cout << "hog detect object: " << detections.size() << endl;
for (size_t i = 0; i < detections.size(); i++) {
if (foundWeights[i] > 1.3) {
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, 1.1, 3, 0, Size(), Size(), true);
cout << "cascade detect object: " << detections.size() << endl;
for (int i = 0; i < detections.size(); i++) {
if (rejLevel[i] < 20 || foundWeights[i] < 1.)
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 = 0; i < keep.size(); i++) {
Point p1(keep[i].xmin, keep[i].ymin), p2(keep[i].xmax, keep[i].ymax);
Scalar color(0, 255, 0);
rectangle(img, p1, p2, color, 2);
}
imshow("detect", img);
waitKey(0);
}
}
部分結果如下:
直線檢測
參考博文3,博文4,博文5,對於車道檢測,主要採用如下的pipline:
- 對影象進行透視變換,使其變為鳥瞰圖:
Point2f origin[] = {Point2f(204, 286), Point2f(71, 448), Point2f(394, 448), Point2f(243, 286)};
Point2f dst[] = {Point2f(112, 0), Point2f(112, 448), Point2f(336, 448), Point2f(336, 0)};
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, 1, 0, sobel_kernel);
normalize(out, out, 0, 255, NORM_MINMAX);
threshold(out, out, min_thres, 0, THRESH_TOZERO);
threshold(out, out, max_thres, 255, THRESH_BINARY);
}
- 對原影象轉換到HLS空間,保留黃色和白色(車道多為黃色和白色)
void yellow_white_threshold(Mat origin, Mat &out1) {
int y_lower[] = {10, 0, 100};
int y_upper[] = {40, 255, 255};
int w_lower[] = {0, 200, 0};
int w_upper[] = {180, 255, 255};
Mat HLS, y_mask, w_mask, mask;
cvtColor(origin, HLS, CV_BGR2HLS);
vector<int> yellow_lower(y_lower, y_lower + 3);
vector<int> yellow_upper(y_upper, y_upper + 3);
vector<int> white_lower(w_lower, w_lower + 3);
vector<int> white_upper(w_upper, w_upper + 3);
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, 130, 255, THRESH_BINARY);
}
- 根據2,3步得到最終的二值圖
- 利用霍夫變換找出相應的直線端點(根據直線斜率進行一定的限制)
vector<Vec4i> lines;
vector<Point2f> leftlines;
vector<Point2f> rightlines;
HoughLinesP(out1, lines, 1, CV_PI / 180, 50, 30, 10);
cout << lines.size() << endl;
for (size_t i = 0; i < lines.size(); i++) {
//abandon horizontal line.
if (lines[i][1] == lines[i][3])
continue;
//get left lines
if (lines[i][0] <= 224 && lines[i][2] <=224){
float k = 1.5;
//if not verticle line
if (lines[i][0] != lines[i][2])
k = fabs(float(lines[i][3]-lines[i][1])/float(lines[i][2]-lines[i][0]));
if (k>=1.5) {
leftlines.push_back(Point2f(lines[i][0], lines[i][1]));
leftlines.push_back(Point2f(lines[i][2], lines[i][3]));
}
}
- 對這些點進行線性迴歸
Vec4f line_left, line_right;
fitLine(leftlines, line_left, DIST_L1, 0, 0.01, 0.01);
fitLine(rightlines, line_right, DIST_L1, 0, 0.01, 0.01);
- 畫出直線圍成的區域,並進行高亮,顯示到原圖上
整個流程圖如下:
原始碼詳見:我的github