一、前言
最初想写这篇文章就是想帮助和我一样的热心于图像处理的初学者尽快掌握SVM。通过自学毛星云编著的《Opencv3编程入门》一书,并亲自一个一个地码上所有的示例代码,做了一个项目后,算是真正地入门图像处理领域了吧,但也仅仅是入门。
学海无涯,愿每个对图像处理,甚至机器人学感兴趣的人都能保持初心,勇往直前。
本文工程基于Opencv2.4.9和vs2010搭建。而本文也仅仅是篇学习笔记,引用了CSDN上各位大神的文章作为学习参考,并无它意。
二、对于SVM的理解
支持向量机,因其英文名为support vector machine,故一般简称SVM。它是一种二分类模型,其“基本模型”定义为在特征空间上的与特征向量(支持向量)间隔最大的线性分类器,其学习策略便是间隔最大化。
一个线性分类器的学习目标便是要在n维的数据空间中找到一个超平面(hyper plane)把这些数据分成两类,这个超平面的方程可以表示为( wT中的T代表转置):w^T x+b=0。
如果用x表示数据点,用y表示类别(y可以取1或者-1,分别代表两个不同的类,而y取1或者-1也是为了公式好求解)。下面举个简单的例子。如下图所示,现在有一个二维平面,平面上有两种不同的数据,分别用圈和叉表示。由于这些数据是线性可分的,所以可以用一条直线将这两类数据分开,这条直线就相当于一个超平面,超平面一边的数据点所对应的y全是-1 ,另一边所对应的y全是1。
这个超平面可以用分类函数f(x)=w^T x+b表示,当f(x) 等于0的时候,x便是位于超平面上的点,而f(x)大于0的点对应 y=1 的数据点,f(x)小于0的点对应y=-1的点。换言之,在进行分类的时候,遇到一个新的数据点x,将x代入f(x) 中,如果f(x)小于0则将x的类别赋为-1,如果f(x)大于0则将x的类别赋为1。
如何确定这个超平面呢?
对一个数据点进行分类,当超平面离数据点的“间隔”越大,分类的确信度(confidence)也越大。所以,为了使得分类的确信度尽量高,需要让所选择的超平面能够最大化这个“间隔”值。用最大间隔分类器寻找到几何间隔最大的超平面。
对于SVM原理的理解,详细参考下面这两篇博文,写的很好
https://blog.csdn.net/v_JULY_v/article/details/7624837
https://www.jianshu.com/p/61849d554001
三、对于SVC和SVR的区别理解
SVM可以做分类机,也可以做回归机,具体用法要根据实际工程要求而定。我用的是SVC做几何体分类识别。
support vector classify(SVC)支持分类机做二分类的,找出分类面,解决分类问题。
support vector regression(SCR)支持回归机做曲线拟合、函数回归 ,做预测,温度,天气,股票。
SVM共有5种类型。C_SVC、NU_SVC、ONE_CLASS为分类机;EPS_SVR、NU_SVR为回归机。
详细描述请参考下面这篇文章
https://www.cnblogs.com/ylHe/p/7676173.html
四、对于SVM中各参数的理解
读过上文后,相信大家对SVM已有了基本的了解,下面介绍SVM各个参数的意义。
SVM的各参数对实验结果有很大的影响,下面这篇博文详细介绍了怎么样优化SVM的各个参数,以及对应参数的意义。但它对于参数的介绍不是很到位,故给出如下补充介绍。
SVM_params.degree:核函数中的参数degree,针对多项式核函数;
SVM_params.gama:核函数中的参数gamma,针对多项式/RBF/SIGMOID核函数;
SVM_params.coef0:核函数中的参数,针对多项式/SIGMOID核函数;
SVM_params.c:SVM最优问题参数,设置C-SVC,EPS_SVR和NU_SVR的参数;
SVM_params.nu:SVM最优问题参数,设置NU_SVC, ONE_CLASS 和NU_SVR的参数;
SVM_params.p:SVM最优问题参数,设置EPS_SVR 中损失函数p的值.
关于异常值惩罚因子C,我理解为权重,是在求超平面解最优方程时带入的一个常值。举个例子帮助理解C的作用。
回想一下C所起的作用(表征你有多么重视离群点,C越大越重视,越不想丢掉它们)。我们可以给每一个离群点都使用不同的C,这时就意味着你对每个样本的重视程度都不一样,有些样本丢了也就丢了,错了也就错了,这些就给一个比较小的C;而有些样本很重要,决不能分类错误(比如中央下达的文件),就给一个很大的C。
参考如下两篇博文
https://blog.csdn.net/xuhaijiao99/article/details/12618545
https://blog.csdn.net/wusecaiyun/article/details/49661901
(Opencv中的SVM参数优化) https://www.cnblogs.com/hust-yingjie/p/6582218.html
而SVM训练的时间是个奇特的东西,根据样本数的不同以及内核类型的差异,在训练时间上有明显不同。根据我工程的经验,当内核类型为RBF时,训练时间多达10分钟左右。当内核类型为LINEAR时,没加负样本前是1分钟左右,加了负样本变成2分钟左右。
五、实际工程介绍
我用SVM进行多样本训练,进而识别目标几何体的颜色、形状,并实时输出中心坐标。
话不多说,先搬上效果图。
下面将结合我的项目经验,详细介绍实现的过程以及注意事项。
(一)准备正样本,负样本图片
我的正样本图片就是五个几何体不同角度、不同方位的图像,并在文件夹中分类储存,供程序调用。而负样本图片是除了五个几何体之外的实验室的环境图像。正样本图像大小必须一致,我将正样本图片统一缩放到60x60 ,官方推荐是20x20,同时可以做灰度处理(提高运行效率)。而负样本图像可以不和正样本大小一致,但负样本的所有图像自身的尺寸需要统一,比如负样本尺寸为100x100。
注意负样本的图片是实际工程所必须的,有些网上的简易例程没有负样本,这是因为他们识别的仅仅都是一张图片,而在视频帧里面,面对不同的干扰条件,必须提供负样本来降低干扰,还要考虑算法的速度,尽量提高帧率。在工程前期因为我没有添加负样本,而产生了很多误报。
而样本数量也是适中处理。样本数量原则上尽量多一些,以满足在不同条件上都能识别特征几何体,但也不是绝对的越多越好,需要根据实际工程中需要检测的特征物体而定。对于负样本的采集,最好也是根据实际的工作环境而定,不能随意找一个不相关的图片代替。比如本工程找的是几何体,负样本用宇宙图片,这样训练出来的效果可想而知并不好。
准备的正样本与负样本图像,positive为正样本,negative为负样本
上面的是负样本图像,绝对不能有五个目标几何体出现。
上面是正样本图像示范,大小为60*60
(二)部分采集正样本的代码
#include "opencv2/opencv.hpp"
#include "CameraCalibration.h"
#include "vector"
using namespace cv;
using namespace std;
Mat CalibrationImage,textImage,HSVimage,EdgeImage,BinaryImage;
uchar keyVal;
int f=182;
string Picture_Name;
int H_max=30,H_min=1,S_max=30,S_min=1,V_max=30,V_min=1;
void ON_Trackbar_HSV(int,void*)
{
inRange(HSVimage,Scalar(MIN(H_max,H_min),MIN(S_max,S_min),MIN(V_max,V_min)),
Scalar(MAX(H_max,H_min),MAX(S_max,S_min),MAX(V_max,V_min)),textImage);
imshow("textImage",textImage);
}
void main()
{
CameraCalibration_Init();
namedWindow("【CalibrationImage】",WINDOW_AUTOSIZE);
Mat element = getStructuringElement(MORPH_RECT,Size(9,9));
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
Rect rectangle;
while (1)
{
ReturnCameraCalibration(CalibrationImage);
imshow("【CalibrationImage】",CalibrationImage);
cvtColor(CalibrationImage,HSVimage,COLOR_BGR2HSV);
GaussianBlur(HSVimage,HSVimage,Size(7,7),0,0);
textImage=Mat::zeros(HSVimage.size(),CV_8UC1);
inRange(HSVimage,Scalar(0,0,0),Scalar(180,151,255),textImage);
//imshow("textImage",textImage);
/*morphologyEx(textImage,BinaryImage,MORPH_OPEN,element);
morphologyEx(textImage,BinaryImage,MORPH_CLOSE,element);
imshow("BinaryImage",BinaryImage);*/
/*createTrackbar("H_max","【CalibrationImage】",&H_max,180,ON_Trackbar_HSV,0);
createTrackbar("H_min","【CalibrationImage】",&H_min,180,ON_Trackbar_HSV,0);
createTrackbar("S_max","【CalibrationImage】",&S_max,255,ON_Trackbar_HSV,0);
createTrackbar("S_min","【CalibrationImage】",&S_min,255,ON_Trackbar_HSV,0);
createTrackbar("V_max","【CalibrationImage】",&V_max,255,ON_Trackbar_HSV,0);
createTrackbar("V_min","【CalibrationImage】",&V_min,255,ON_Trackbar_HSV,0);
ON_Trackbar_HSV(0,0);*/
Canny(textImage,EdgeImage,70,140,3);
imshow("EdgeImage",EdgeImage);
findContours(EdgeImage,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point(0,0));
if(!contours.data()) continue; //防止画面卡死
double MAXcontoursArea = 0;
//int MAXcontourCounter = 0;
Mat ObjectImage = Mat::zeros(CalibrationImage.size(),CalibrationImage.type());
for (unsigned int i=0;i<contours.size();i++)
{
if (fabs(contourArea(contours[i]))>MAXcontoursArea)
{
MAXcontoursArea = fabs(contourArea(contours[i]));
//MAXcontourCounter = i;
rectangle = boundingRect(contours[i]);
/*-----防止矩形越界,出现图像崩溃问题-----*/
if(rectangle.x >= 5) rectangle.x = rectangle.x -5;
if(rectangle.y >= 12) rectangle.y = rectangle.y -12;
if((rectangle.y+rectangle.height + 18) >= CalibrationImage.rows) rectangle.height = rectangle.height + 18-(rectangle.y+rectangle.height + 18- CalibrationImage.rows);
else rectangle.height = rectangle.height + 18;
if((rectangle.x+rectangle.width + 10) >= CalibrationImage.cols) rectangle.width = rectangle.width + 10-(rectangle.x+rectangle.width + 10-CalibrationImage.cols);
else rectangle.width = rectangle.width + 10;
}
}
ObjectImage = CalibrationImage(rectangle);
imshow("【目标图】",ObjectImage);
/*drawContours(ObjectImage,contours,MAXcontourCounter,Scalar::all(255),1,8,hierarchy,0,Point(0,0));
imshow("ObjectImage",ObjectImage);*/
keyVal = (uchar)waitKey(1);
if (keyVal == 27) break;
else if(keyVal == 'q')
{
printf("\t\t已成功保存%d张图片至工程文件夹\n",f);
Picture_Name = "samples/positive/s4/"+to_string(static_cast<long double>(f++))+".jpg"; //to_string:将数值转化为字符串。返回对应的字符串
imwrite(Picture_Name,ObjectImage);
}
}
}
(三)录入训练数据
void Get_TrainData(void)
{
Train_data.create(TrainSample*Classes+2000,SampleSize.height*SampleSize.width,CV_32FC1);//每个种类的样本数为TrainSample,种类为Classes,
TrainClasses.create(TrainSample*Classes+2000, 1, CV_32FC1); //创建TrainSample*Classes行、SampleSize.width*SampleSize.height列的矩阵存储信息(将一副图像的信息转为一行储存),训练数据必须是CV_32FC1
Mat TemparyImage,ImageNewSize;
char file[255];
for (unsigned int i=0;i<Classes;i++)//录入正样本
{
for (unsigned int j=1;j<=TrainSample;j++)//训练集标号从1开始
{
sprintf(file,"samples/positive/s%d/%d.jpg",i,j);
TemparyImage = imread(file,IMREAD_GRAYSCALE);
if (TemparyImage.empty())
{
printf("\t\t\tError: Cant load image %s\n", file);
continue;
}
resize(TemparyImage,ImageNewSize,SampleSize); //统一训练集尺寸
TemparyImage.release(); //把TemparyImage的矩阵信息释放(清除)
TemparyImage = ImageNewSize.reshape(0,1); //图像深度不变,把图片矩阵转为一行储存
TemparyImage.convertTo(Train_data(Range(i*Classes+j-1,i*Classes+j),Range(0,Train_data.cols)),CV_32FC1);//Range为定义ROI的方式之一,表示从起始到终止索引(不包括终止索引)的连续序列
TrainClasses.at<float>(i*Classes+j-1,0) = i;
}
}
for (unsigned int i =1;i<=200;i++)//录入负样本
{
sprintf(file, "samples/negative/%d.jpg",i);
Mat SrcImage = imread(file,IMREAD_GRAYSCALE);
if (SrcImage.empty())
{
printf("\t\t\tError: Cant load image %s\n", file);
continue;
}
resize(SrcImage,ImageNewSize,SampleSize); //统一训练集尺寸
SrcImage.release(); //把TemparyImage的矩阵信息释放(清除)
SrcImage = ImageNewSize.reshape(0,1); //图像深度不变,把图片矩阵转为一行储存
SrcImage.convertTo(Train_data(Range(999+i,999+i+1),Range(0,Train_data.cols)),CV_32FC1);//Range为定义ROI的方式之一,表示从起始到终止索引(不包括终止索引)的连续序列
TrainClasses.at<float>(999+i,0) = -1;
}
}
(四)SVM参数配置及使用
CvSVMParams SVM_Params;
SVM_Params.svm_type = CvSVM::C_SVC; //SVM类型(允许用异常值惩罚因子C进行不完全分类)
SVM_Params.kernel_type = CvSVM::LINEAR; //SVM的内核类型
SVM_Params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER,10000,1e-6);//指定迭代终止的条件
printf("\t\t\t\t开始获取训练集数据\n");
Get_TrainData();
printf("\t\t\t\t成功获取训练集数据\n");
printf("\t\t\t\tSVM训练样本开始\n");
CvSVM SVM_Classifier;
SVM_Classifier.train_auto(Train_data,TrainClasses,Mat(),Mat(),SVM_Params);//自动训练并优化参数
SVM_Classifier.save("svm.xml");
printf("\t\t\t\tSVM训练样本结束\n");
(五)利用训练好的模型识别几何体
void main()
{
Mat GrayImage,BinaryImage,TestImage;
char label[40];
CameraCalibration_Init();
namedWindow("【CalibrationImage】",WINDOW_AUTOSIZE);
CvSVM SVM_Classifier;
SVM_Classifier.clear();
SVM_Classifier.load("svm.xml"); //加载已训练好的分类器
while (1)
{
ReturnCameraCalibration(CalibrationImage);
imshow("【CalibrationImage】",CalibrationImage);
cvtColor(CalibrationImage,GrayImage,COLOR_BGR2GRAY);
GaussianBlur(GrayImage,GrayImage,Size(7,7),0,0);
BinaryImage = Background_separation(CalibrationImage);
vector<Rect> TemRectangle;
ReadImage_to_SaveBoundingRect(BinaryImage,TemRectangle);
for (unsigned int i=0;i<TemRectangle.size();i++)
{
TestImage = GrayImage(TemRectangle[i]);
Mat imageNewSize;
resize(TestImage, imageNewSize, SampleSize); //统一摄像头画面里面采集到的轮廓图像的尺寸
TestImage.release(); //把image的矩阵信息释放(清除)
TestImage = imageNewSize.reshape(0, 1); //图像深度不变,把图片矩阵转为一行储存
TestImage.convertTo(TestImage, CV_32FC1);
int Response = static_cast<int>(SVM_Classifier.predict(TestImage));
switch (Response)
{
case 0:
sprintf(label, "Pix: (%d, %d)", TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2);
rectangle(CalibrationImage,TemRectangle[i],Scalar(0,255,127),2,8);
putText(CalibrationImage, str0, Point(TemRectangle[i].x, TemRectangle[i].y-5),FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 127), 1, 8);
putText(CalibrationImage, label, Point(TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 127), 1, 8);
break;
case 1:
sprintf(label, "Pix: (%d, %d)", TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2);
rectangle(CalibrationImage,TemRectangle[i],Scalar(0,255,255),2,8);
putText(CalibrationImage, str1, Point(TemRectangle[i].x, TemRectangle[i].y-5),FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 255), 1, 8);
putText(CalibrationImage, label, Point(TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 255), 1, 8);
break;
case 2:
sprintf(label, "Pix: (%d, %d)", TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2);
rectangle(CalibrationImage,TemRectangle[i],Scalar(0,0,255),2,8);
putText(CalibrationImage, str2, Point(TemRectangle[i].x, TemRectangle[i].y-5),FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255), 1, 8);
putText(CalibrationImage, label, Point(TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255), 1, 8);
break;
case 3:
sprintf(label, "Pix: (%d, %d)", TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2);
rectangle(CalibrationImage,TemRectangle[i],Scalar(255,0,0),2,8);
putText(CalibrationImage, str3, Point(TemRectangle[i].x, TemRectangle[i].y-5),FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 0, 0), 1, 8);
putText(CalibrationImage, label, Point(TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 0, 0), 1, 8);
break;
case 4:
sprintf(label, "Pix: (%d, %d)", TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2);
rectangle(CalibrationImage,TemRectangle[i],Scalar(255,0,0),2,8);
putText(CalibrationImage, str4, Point(TemRectangle[i].x, TemRectangle[i].y-5),FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 0, 0), 1, 8);
putText(CalibrationImage, label, Point(TemRectangle[i].x + TemRectangle[i].width / 2, TemRectangle[i].y + TemRectangle[i].height / 2),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 0, 0), 1, 8);
break;
default: break;
}
}
imshow("【效果图】",CalibrationImage);
uchar keyValue = (uchar)waitKey(1);
if (keyValue == 27) break;
else if(keyValue == 'q')
{
printf("\t\t已成功保存%d张图片至工程文件夹\n",Picture_NameCounter);
Picture_Name = to_string(static_cast<long double>(Picture_NameCounter++))+".jpg"; //to_string:将数值转化为字符串。返回对应的字符串
imwrite(Picture_Name,CalibrationImage);
}
}
}
六、总结
以上就是本次工程的精华部分,整个工程的下载请点击下面的链接。
https://download.csdn.net/download/qq_40501580/10914118
由于本博文主要是讲解SVM的原理和在工程中的应用,故一些陌生函数的使用方法就不在这里赘述了。如:putText,convertTo,reshape,TermCriteria模板类等,读者可自行在CSDN上搜索关键字查找。
在工程前期,我是通过canny函数把摄像头灰度图像转为边缘二值图,再用findContours()函数寻找轮廓,在找到的轮廓中再做处理,但是这样做的效果并不好。因为浅黄色正方体和绿色空心圆柱的边缘在原图中并不明显,即使经过形态学滤波处理后,检查轮廓的效果依然不佳,轮廓不完整,干扰轮廓也很多。送入SVM训练后,因为提取特征几何体的信息不完整,导致识别效果很差(只能把浅黄色正方体识别出来)。
(一)工程初期用canny函数做目标几何体分离背景的效果
原图
边缘二值图
SVM识别的效果图
由此可见这样的做法受到的干扰太多,不好做处理。
(二)改善的思路
故在工程后期才改良了思路。选择对于特征几何体与背景的分离主要是基于HSV模型做的。HSV模型能比人们熟知的RGB模型更好地体现颜色的特征。通过inrange()函数设置好阈值,可把目标几何体初步地分离出来,再结合自己编写的ReadImage_to_SaveBoundingRect()子函数寻找目标几何体的外包围矩形向量。在ReadImage_to_SaveBoundingRect()函数中做面积处理(滤过干扰物体)和防止矩形越界处理(防止图像崩溃)。如果只用上述方法其实也可以实现目的功能,但寻找到的特征矩形可能包含很多干扰,所以需要用已训练好的SVM模型对找到的特征矩形里面的图像做判断,最大限度地滤去干扰。
改良后的SVM识别效果图