支持向量机(Support Vector Machine, SVM)是一类按监督学习(supervised learning)方式对数据进行二元分类的广义线性分类器(generalized linear classifier),在监督学习算法中支持向量机有着非常广泛的应用,而且在解决图像分类问题上有着优异的效果。
而OpenCV则集成了这种学习算法,它被包含在ml模块下的CvSVM类中。OpenCV实现人脸识别主要需要经过以下三个步骤:数据准备、模型训练和加载模型实现分类。
一、数据准备
以一张在OpenCV安装路径下的名为"digits.png"的图片为例进行0和1的二分类,图片大小为1000×2000,有0-9的10个数字,每5行为一个数字,总共50行,共有5000个手写数字,每个数字块大小为20×20。
1 |
|
代码详解:
1 | char ad[128]={0}; |
cvtColor() :用于将图像从一个颜色空间转换到另一个颜色空间的转换(目前常见的颜色空间均支持),并且在转换的过程中能够保证数据的类型不变,即转换后的图像的数据类型和位深与源图像一致。
参数:
InputArray src: 输入图像即要进行颜色空间变换的原图像,可以是Mat类
OutputArray dst: 输出图像即进行颜色空间变换后存储图像,也可以Mat类
int code: 转换的代码或标识,即在此确定将什么制式的图片转换成什么制式的图片,
int dstCn = 0: 目标图像通道数,如果取值为0,则由src和code决定
1 | for (int i = 0; i < m; i++) |
sprintf_s() :sprintf_s函数功能是将数据格式化输出到字符串。sprintf_s对于格式化string中的格式化的字符的有效性进行了检查,sprintf_s也携带着接收格式化字符串的缓冲区的大小。
img.copyTo(dst_Image, mask) :将src_Image图 对照着mask图复制到dst_Image图,最后得到dst_Image图。
参数:(原图需要和模板图有相同的大小,即高度,宽度,通道数都要相同,RGB图和GRAY图均可,只要他们相同大小,python中可用src_Image.shape查看图像大小)
- src_Image:原图
- dst_Image:结果图
- mask:掩图
imwrite(A,filename) :将图像数据 A 写入 filename 指定的文件,并从扩展名推断出文件格式。imwrite 在当前文件夹中创建新文件。输出图像的位深取决于 A 的数据类型和文件格式。对于大多数格式来说:
如果 A 属于数据类型 uint8,则 imwrite 输出 8 位值。
如果 A 属于数据类型 uint16 且输出文件格式支持 16 位数据(JPEG、PNG 和 TIFF),则 imwrite 将输出 16 位的值。如果输出文件格式不支持 16 位数据,则 imwrite 返回错误。
如果 A 是灰度图像或者属于数据类型 double 或 single 的 RGB 彩色图像,则 imwrite 假设动态范围是 [0,1],并在将其作为 8 位值写入文件之前自动按 255 缩放数据。如果 A 中的数据是 single,则在将其写入 GIF 或 TIFF 文件之前将 A 转换为 double。
如果 A 属于 logical 数据类型,则 imwrite 会假定数据为二值图像并将数据写入位深为 1 的文件(如果格式允许)。BMP、PNG 或 TIFF 格式以输入数组形式接受二值图像。
如果 A 包含索引图像数据,则应另外指定 map 输入参数。
二、模型训练
准备好训练数据和测试数据之后,就可以进行模型训练了。具体代码如下:
1 |
|
整个训练过程可以分为以下几个部分:
1.数据准备
该例程中一个定义了三个子程序用来实现数据准备工作: getFiles() 用来遍历文件夹下所有文件 getBubble() 用来获取有气泡的图片和与其对应的Labels,该例程将Labels定为1。 getNoBubble() 用来获取没有气泡的图片与其对应的Labels,该例程将Labels定为0。 在该代码中,getBubble() 与getNoBubble() (即get_1() 和get_0() )将获取一张图片后会将图片(特征)写入到容器(vector)中,紧接着会将标签写入另一个容器中,这样就保证了特征和标签是一一对应的关系,push_back(0)或者push_back(1)其实就是我们贴标签的过程。
1 | trainingImages.push_back(SrcImage); |
在主函数中,将getBubble()与getNoBubble()写好的包含特征的矩阵拷贝给trainingData,将包含标签的vector容器进行类型转换后拷贝到trainingLabels里,至此,数据准备工作完成,trainingData与trainingLabels就是我们要训练的数据。
1 | Mat classes; |
img.convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) :img参数为图像数据来源,其类型为Mat。函数的作用是把一个矩阵从一种数据类型转换到另一种数据类型,同时可以带上缩放因子和增量。 参数:
m:目标矩阵。如果m在运算前没有合适的尺寸或类型,将被重新分配。
rtype:目标矩阵的类型。因为目标矩阵的通道数与源矩阵一样,所以rtype也可以看做是目标矩阵的位深度。如果rtype为负值,目标矩阵和源矩阵将使用同样的类型。CV_32FC1表示32位数据。你如果用cv_32fc1,那么后面对该矩阵的输入输出的数据指针类型都应该是float,这在32位编译器上是32位浮点数,也就是单精度。你如果用cv_64fc1,那么后面对该矩阵的输入输出的数据指针类型都应该是double,这在32位编译器上是64位浮点数,也就是双精度。
alpha:尺度变换因子(可选)。默认值是1。即把原矩阵中的每一个元素都乘以alpha。
beta:附加到尺度变换后的值上的偏移量(可选)。默认值是0。即把原矩阵中的每一个元素都乘以alpha,再加上beta。
2.特征选取
其实特征提取和数据的准备是同步完成的,我们最后要训练的也是正负样本的特征。本例程中同样在getBubble()与getNoBubble()函数中完成特征提取工作,只是我们简单粗暴将整个图的所有像素作为了特征,因为我们关注更多的是整个的训练过程,所以选择了最简单的方式完成特征提取工作,除此中外,特征提取的方式有很多,比如LBP,HOG等等。
1 | SrcImage= SrcImage.reshape(1, 1); |
reshape(int cn,int rows):改变一个矩阵的形状。我们将参数定义为reshape(1, 1)的结果就是原图像对应的矩阵将被拉伸成一个一行的向量,作为特征向量。
参数:
cn为新的通道数,如果cn = 0,表示通道数不会改变。
rows为新的行数,如果rows = 0,表示行数不会改变。
3.参数配置
参数配置是SVM的核心部分,在Opencv中它被定义成一个结构体类型,如下:
1 | struct CV_EXPORTS_W_MAP CvSVMParams |
在此代码中,我们定义了一个结构体变量用来配置这些参数,而这个变量也就是CVSVM类中train函数的第五个参数。
1 | CvSVM svm; |
SVM_params:包含SVM训练器相关参数的结构体
参数:
SVM_params.svm_type :SVM的类型。
C_SVC表示SVM分类器
C_SVR表示SVM回归
SVM_params.kernel_type:核函数类型
- 线性核 LINEAR:d(x,y)=(x,y)
- 多项式核 POLY:d(x,y)=(gamma × (x’y)+coef0)degree
- 径向基核 RBF:d(x,y)=exp(-gamma × |x-y|^2)
- sigmoid核 SIGMOID:d(x,y)= tanh(gamma*(x’y)+ coef0)
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的值.
4.训练模型
1 | svm.train(trainingData, classes, Mat(), Mat(), SVM_params); |
通过上面的过程,我们准备好了待训练的数据和训练需要的参数,其实可以理解为这个准备工作就是在为svm.train()函数准备实参的过程。来看一下svm.train()函数,Opencv将SVM封装成CvSVM库,这个库是基于台湾大学林智仁(Lin Chih-Jen)教授等人开发的LIBSVM封装的,由于篇幅限制,不再全部粘贴库的定义,所以一下代码只是CvSVM库中的一部分数据和函数:
1 | class CV_EXPORTS_W CvSVM : public CvStatModel |
我们就是应用类中定义的train函数完成模型训练工作。
5.保存模型
1 | svm.save("svm.xml"); |
保存模型只有一行代码,利用save()函数,我们看下它的定义:
1 | CV_WRAP virtual void save( const char* filename, const char* name=0 ) const; |
该函数被定义在CvStatModel类中,CvStatModel是ML库中的统计模型基类,其他 ML 类都是从这个类中继承。
总结:
到这里我们就完成了模型训练工作,可以看到真正用于训练的代码其实很少,OpenCV最支持向量机的封装极大地降低了我们的编程工作。
三、加载模型实现分类
1 |
|
在上面我们把该介绍的都说的差不多了,这个例程中只是用到了load()函数用于模型加载,加载的就是上面例子中生成的模型,load()
被定义在CvStatModel
这个基类中:
1 | svm.load(modelpath.c_str()); |
load的路径是string modelpath = "svm.xml",这意味着svm.mxl文件应该在测试工程的根目录下面,但是因为训练和预测是两个独立的工程,所以必须要拷贝一下这个文件。最后用到predict()函数用来预测分类结果,predict()被定义在CVSVM类中。
注意:
1.为什么要建立三个独立的工程呢?
主要是考虑写在一起话,代码量会比较大,逻辑没有分开清晰,当跑通上面的代码之后,就可以随意的改了。
2.为什么加上数据准备?
之前有评论说道数据的问题,提供数据后实验能更顺利一些,因为本身代码没有什么含金量,这样可以更顺利的运行起来工程,并修改它。
3.一些容易引起异常的情况:
- 注意生成的.xml记得拷贝到预测工程下;
- 注意准备好数据路径和代码是不是一致;
- 注意训练的特征要和测试的特征一致;
参考:https://blog.csdn.net/chaipp0607/article/details/68067098