问题内容:路面箭头检测与识别(该部分主要介绍基于SVM的分类,也就是识别,检测问题暂不谈)
问题背景:路面箭头识别在无人车中有着重要的作用:
1.车道线等重要信息的检测,需要排除路面箭头的干扰;
2.需要根据路面箭头的指示,完成下一步动作
问题描述:为了简化这个问题,重点描述基于OpenCV的SVM路面箭头分类,我们将预先检测到的待分类 路面箭头给出如下,样本均从俯视图(逆透视)中检测获得:
图片1-11中包括:1-3、10为虚线的误检、4-5为直右箭头、6-9为误检、11为倒着的直左箭头
问题难点(本文中未解决):为何找到一个合适的特征,来解决箭头的旋转不变、尺度不变问题
作者在有限的时间中尝试过Hu矩特征、SIFT特征,效果都不是很理想,具体情况表现为:
Hu矩特征:虽然有旋转不变特性,但是将信息压缩太严重,最后得到的仅是一个七维特征,放入SVM中训练效果很差;
SIFT特征:其也具有尺度和旋转不变性,且对光照不均等现象具有抑制作用,但是作者尝试发现,样本和测试集的特征点检测较少,无法聚类得到定长的特征算子,故无法进行SVM分类
本文采用方法:考虑到路面箭头的形式、形状甚至大小比较单一,仅仅是位置、角度不同,故可以放入较多的模板来反映同一标志不同角度的形态特征,最后将整图信息压缩到固定大小作为特征算子输入SVM中,仍可以取得不错的效果。
样本准备:
不同样式、相同样式不同角度以及负样本,本文采用374个样本。
代码操作:
首先在工程的路径下放入样本集以及测试集,如下图所示sample和test文件夹
上代码:
- #include <opencv2/core/core.hpp>
- #include <opencv2/highgui/highgui.hpp>
- #include <opencv2/ml/ml.hpp>
- #include <opencv2\opencv.hpp>
- #include "opencv2/core/core.hpp"
- #include "highgui.h"
- #include "opencv2/imgproc/imgproc.hpp"
- #include "opencv2/features2d/features2d.hpp"
- #include "opencv2/nonfree/nonfree.hpp"
- #include <iostream>
- #include <time.h>
- #include "MomentFeature.h"
- #include <iostream>
- #include <fstream>
- #include <iterator>
- #include <vector>
- using namespace cv;
- using namespace std;
- #define trainnum 374//374
- #define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
- /*
- //标签值,角度未合并
- float label[trainnum] =
- {
- 1,1,1,1,1,1,1,1,1,1,
- 2,2,2,2,2,2,2,2,2,2,
- 3,3,3,3,3,3,3,3,3,3,
- 4,4,4,4,4,4,4,4,4,4,
- 5,5,5,5,5,5,5,5,5,5,
- 6,6,6,6,6,6,6,6,6,6,
- 7,7,7,7,7,7,7,7,7,7,
- 8,8,8,8,8,8,8,8,8,8,
- 9,9,9,9,9,9,9,9,9,9,
- 10,10,10,10,10,10,10,10,10,10,
- 11,11,11,11,11,11,11,11,11,11,
- 12,12,12,12,12,12,12,12,12,12,
- 13,13,13,13,13,13,13,13,13,13,
- 14,14,14,14,14,14,14,14,14,14,
- 15,15,15,15,15,15,15,15,15,15,
- 16,16,16,16,16,16,16,16,16,16,
- 17,17,17,17,17,17,17,17,17,17,
- 18,18,18,18,18,18,18,18,18,18,
- 1,1,1,1,1,1,1,1,1,1,
- 9,9,9,9,9,9,9,9,9,9,
- 4,4,4,4,4,4,4,4,4,4,
- 6,6,6,6,6,6,6,6,6,6,
- 8,8,8,8,8,8,8,8,8,8,
- 3,3,3,3,3,3,3,3,3,3,
- 6,6,6,6,6,6,6,6,6,6,
- 6,6,6,6,6,6,6,6,6,6,
- 11,11,11,11,11,11,11,11,11,11,
- 16,16,16,16,16,16,16,16,16,16,
- 12,12,12,12,12,12,12,12,12,12,
- 12,12,12,12,12,12,12,12,12,12,
- 3,3,3,3,3,3,3,3,3,3,
- 3,3,3,3,3,3,3,3,3,3,
- 19,19,19,19,19,19,19,19,19,19,
- 19,19,19,19,19,19,19,19,19,19,
- 19,19,19,19,19,19,19,19,19,19,
- 8,8,8,8,8,8,8,8,8,8,
- 8,8,8,8,8,8,8,8,8,8,
- 10,10,10,10
- };
- */
- //标签值,角度合并了!
- float label[trainnum] =
- {
- 1,1,1,1,1,1,1,1,1,1,
- 2,2,2,2,2,2,2,2,2,2,
- 3,3,3,3,3,3,3,3,3,3,
- 1,1,1,1,1,1,1,1,1,1,
- 5,5,5,5,5,5,5,5,5,5,
- 5,5,5,5,5,5,5,5,5,5,
- 5,5,5,5,5,5,5,5,5,5,
- 8,8,8,8,8,8,8,8,8,8,
- 9,9,9,9,9,9,9,9,9,9,
- 10,10,10,10,10,10,10,10,10,10,
- 1,1,1,1,1,1,1,1,1,1,
- 3,3,3,3,3,3,3,3,3,3,
- 5,5,5,5,5,5,5,5,5,5,
- 14,14,14,14,14,14,14,14,14,14,
- 15,15,15,15,15,15,15,15,15,15,
- 2,2,2,2,2,2,2,2,2,2,
- 17,17,17,17,17,17,17,17,17,17,
- 18,18,18,18,18,18,18,18,18,18,
- 1,1,1,1,1,1,1,1,1,1,
- 9,9,9,9,9,9,9,9,9,9,
- 1,1,1,1,1,1,1,1,1,1,
- 5,5,5,5,5,5,5,5,5,5,
- 8,8,8,8,8,8,8,8,8,8,
- 3,3,3,3,3,3,3,3,3,3,
- 5,5,5,5,5,5,5,5,5,5,
- 5,5,5,5,5,5,5,5,5,5,
- 1,1,1,1,1,1,1,1,1,1,
- 2,2,2,2,2,2,2,2,2,2,
- 3,3,3,3,3,3,3,3,3,3,
- 3,3,3,3,3,3,3,3,3,3,
- 3,3,3,3,3,3,3,3,3,3,
- 3,3,3,3,3,3,3,3,3,3,
- 17,17,17,17,17,17,17,17,17,17,
- 17,17,17,17,17,17,17,17,17,17,
- 17,17,17,17,17,17,17,17,17,17,
- 8,8,8,8,8,8,8,8,8,8,
- 8,8,8,8,8,8,8,8,8,8,
- 10,10,10,10
- };
- string labelname[500] = {"左1",//1,4,11
- "右1",//2,16
- "直右",//3,12
- "左2",//4
- "直1",//5,6,7,13
- "直2",//6
- "直3",//7
- "虚线",//8
- "斑马线1",//9
- "斑马线2",//10
- "倒左",//11
- "倒直右",//12
- "倒直",//13
- "误检",//14
- "误检2",//15
- "倒右",//16
- "倒直左",//17
- "误检3",//18
- "直左"//19,17
- };
- /*----------------------------
- * 功能 : 将 cv::Mat 数据写入到 .txt 文件
- *----------------------------
- * 函数 : WriteData
- * 访问 : public
- * 返回 : -1:打开文件失败;0:写入数据成功;1:矩阵为空
- *
- * 参数 : fileName [in] 文件名
- * 参数 : matData [in] 矩阵数据
- */
- int WriteData(string fileName, cv::Mat& matData)
- {
- int retVal = 0;
- // 打开文件
- ofstream outFile(fileName.c_str(), ios_base::out); //按新建或覆盖方式写入
- if (!outFile.is_open())
- {
- cout << "打开文件失败" << endl;
- retVal = -1;
- return (retVal);
- }
- // 检查矩阵是否为空
- if (matData.empty())
- {
- cout << "矩阵为空" << endl;
- retVal = 1;
- return (retVal);
- }
- // 写入数据
- for (int r = 0; r < matData.rows; r++)
- {
- for (int c = 0; c < matData.cols; c++)
- {
- if(c != 0)
- outFile << ",";
- float data = matData.at<float>(r,c); //读取数据,at<type> - type 是矩阵元素的具体数据格式
- outFile << data ; //每列数据用 tab 隔开
- }
- outFile << endl; //换行
- }
- return (retVal);
- }
- void floatscale(float* f,int num, int scale)
- {
- for(int i = 0; i < num; i++)
- {
- f[i]=f[i]*scale;
- }
- }
- int main()
- {
- //读取sample中的样本
- char filename[200];
- Mat sample;
- Mat train;
- Mat Hu_train;
- //读取图片数据,提取特征也在这里!
- IplImage* ImageGray;
- float feavec[48];
- for(int i = 0; i < trainnum; i++)
- {
- sprintf(filename,"sample/%d.jpg",1+i);
- //*************把图片信息直接放入************
- sample = imread(filename,-1);
- resize(sample,sample,Size(10,70));
- sample = sample.reshape(0,1);
- sample.convertTo(sample, CV_32F);
- train.push_back(sample);
- //*******************************************
- //********zernike特征***********
- //ImageGray = cvLoadImage(filename,0);
- //MomentFeature MF;
- //MF.SetXStep(1);
- //MF.SetYStep(3);
- //MF.SetVecNum(3,9);
- //Hu[3] + Zernike[9]
- //int FeatureLength = MF.GetFeatureLength();
- //MF.GetMomentFeature(ImageGray,1,1,feavec);
- //cout << FeatureLength << endl;
- //Mat feature(1, 48, CV_32FC1, feavec);
- //normalize(feature,feature,1.0,0.0,NORM_MINMAX);
- //train.push_back(feature);
- //cout << feavec[47] << endl;
- //feavec为输出特征
- //*******************************
- //********Hu*********
- //Canny(sample,sample,60,120);
- //Moments mo; //矩变量
- //double M[7];//Hu矩输出
- //计算Hu矩
- //mo=moments(sample);
- //HuMoments(mo, M);
- //cout << M[0] << " " << M[1] << " " << M[2] << " " << M[3] << " " << M[4] << " " << M[5] << " " << M[6] << " " << endl;
- //Mat Hu(1, 7, CV_64F, M);
- //train.push_back(Hu);
- //*******************
- //*******harris******
- //Mat cornerStrength;
- //cornerHarris(sample, cornerStrength, 2, 3, 0.01);
- //Mat harrisCorner;
- //threshold(cornerStrength, harrisCorner, 0.00001, 255, THRESH_BINARY);
- //imshow("角点检测后的二值效果图", harrisCorner);
- //waitKey();
- //*******************
- //*******SIFT********
- //SIFT sift(10);
- //vector<KeyPoint> key_points; //特征点
- // descriptors为描述符,mascara为掩码矩阵
- //Mat descriptors, mascara;
- //Mat output_img; //输出图像矩阵
- //sift(sample,mascara,key_points,descriptors); //执行SIFT运算
- //cout << descriptors.cols << " " << descriptors.rows << " " <<(int)key_points.size() << endl;
- //*******************
- }
- //Hu需要reshape以下
- //resize(train,train,Size(7,20));
- //特征写入txt
- WriteData("feature_train.txt",train);
- //导入标签值
- Mat labels(trainnum, 1, CV_32FC1, label);
- cout << train.cols << " " << train.rows << endl;
- cout << labels.cols << " " << labels.rows << endl;
- //*********************SVM训练部分***********************
- //准备开始训练
- CvSVM classifier;
- CvSVMParams SVM_params;
- SVM_params.kernel_type = CvSVM::LINEAR; //使用RBF分类非线性问题
- SVM_params.svm_type = CvSVM::C_SVC;
- SVM_params.degree = 0;
- SVM_params.gamma = 0.01;
- SVM_params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, FLT_EPSILON);
- SVM_params.C = 1;
- SVM_params.coef0 = 0;
- SVM_params.nu = 0;
- SVM_params.p = 0.005;
- //classifier.train(train,labels ,Mat(),Mat(),SVM_params); //SVM训练
- //使用调参
- //对不用的参数step设为0
- //CvParamGrid nuGrid = CvParamGrid(1,1,0.0);
- //CvParamGrid coeffGrid = CvParamGrid(1,1,0.0);
- //CvParamGrid degreeGrid = CvParamGrid(1,1,0.0);
- classifier.train_auto(train,labels ,Mat(),Mat(),SVM_params,
- 10,
- classifier.get_default_grid(CvSVM::C),
- classifier.get_default_grid(CvSVM::GAMMA),
- classifier.get_default_grid(CvSVM::P),
- classifier.get_default_grid(CvSVM::NU),
- classifier.get_default_grid(CvSVM::COEF),
- classifier.get_default_grid(CvSVM::DEGREE)
- );
- classifier.save("model180.txt");
- //******************************************************
- //这里载入分类器,方便直接训练
- //CvSVM classifier;
- //classifier.load("model180.txt");
- vector<Mat> testdata; //定义测试数据
- Mat testmmat;
- Mat test;
- int testnum = 11;
- for(int i = 0; i < testnum; i++)
- {
- sprintf(filename,"test/%d.jpg",i+1);
- //*************直接把图片信息放入***********
- test = imread(filename,-1);
- resize(test,test,Size(10,70));
- test = test.reshape(0,1);
- test.convertTo(test, CV_32FC1);
- testdata.push_back(test);
- testmmat.push_back(test);
- //******************************************
- //********zernike特征***********************
- //ImageGray = cvLoadImage(filename,0);
- //MomentFeature MF;
- //MF.SetXStep(1);
- //MF.SetYStep(3);
- //MF.SetVecNum(3,9);
- //Hu[3] + Zernike[9]
- //int FeatureLength = MF.GetFeatureLength();
- //MF.GetMomentFeature(ImageGray,1,1,feavec);
- //cout << FeatureLength << endl;
- //Mat feature(1, 48, CV_32FC1, feavec);
- //normalize(feature,feature,1.0,0.0,NORM_MINMAX);
- //testdata.push_back(feature);
- //testmmat.push_back(feature);
- //feavec为输出特征
- //******************************************
- //********Hu*******************
- //Canny(test,test,60,120);
- //Moments mo; //矩变量
- //double M[7];//Hu矩输出
- //计算Hu矩
- //mo=moments(test);
- //HuMoments(mo, M);
- //cout << M[0] << " " << M[1] << " " << M[2] << " " << M[3] << " " << M[4] << " " << M[5] << " " << M[6] << " " << endl;
- //Mat Hu(1, 7, CV_32FC1, M);
- //testdata.push_back(Hu);
- //*****************************
- }
- WriteData("feature_test.txt",testmmat);
- char textInImage[200];
- //使用训练好的分类器进行预测
- for (int i = 0;i < testdata.size() ; ++i)
- {
- clock_t start = clock();
- int result = (int)classifier.predict(testdata[i]);
- std::cout<<"测试样本"<<i+1<<"的测试结果为:"
- <<result<< " " << labelname[result-1] << "\n";
- clock_t end = clock();
- double tt = static_cast<double>(end - start);
- sprintf(textInImage, "elapsed time: %5.2f ms", tt);
- cout << textInImage << endl;
- }
- getchar();
- }
SVM采用最简单的线性核函数,由于模板样式简单,分类明确,所以可以认为是线性可分问题,结果不错,当然,复杂情况下往往采用RBF径向基核函数,那么需要调参(SVM原理这里不做讨论,以后会再阐述)。
需要注意的事,训练完模型之后可以直接对模型进行保存:
- classifier.save("model180.txt");
这样,会发现工程下多了一个model180.txt文件,不妨打开瞧一瞧,SVM的相关参数都保存在其中:
那么下次要用此训练好的模型则可以直接调用该txt模型:
- CvSVM classifier;
- classifier.load("model180.txt");
最后我们可以得到结果:
当然,这些labelname可以自行定义!
以上只是一个非常简单的demo,旨在留下一个OpenCV做SVM分类的简单框架,方便以后举一反三,如果有做的不好的地方,欢迎大家及时指正!
本文试验的样本和测试集提供在附件,欢迎尝试!