【大创】(一)如何用OpenCV实现基于SVM模型训练与分类的人脸识别

支持向量机(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
char ad[128]={0};
int filename = 0,filenum=0;
Mat img = imread("digits.png");
Mat gray;
cvtColor(img, gray, CV_BGR2GRAY);
int b = 20;
int m = gray.rows / b; //原图为1000*2000
int n = gray.cols / b; //裁剪为5000个20*20的小图块

for (int i = 0; i < m; i++)
{
int offsetRow = i*b; //行上的偏移量
if(i%5==0&&i!=0)
{
filename++;
filenum=0;
}
for (int j = 0; j < n; j++)
{
int offsetCol = j*b; //列上的偏移量
sprintf_s(ad, "D:\\data\\%d\\%d.jpg",filename,filenum++);
//截取20*20的小块
Mat tmp;
gray(Range(offsetRow, offsetRow + b), Range(offsetCol, offsetCol + b)).copyTo(tmp);
imwrite(ad,tmp);
}
}
return 0;
}

代码详解:

1
2
3
4
5
6
7
8
char ad[128]={0};
int filename = 0,filenum=0; //filename用于后续对分割的每个图象每行创建一个文件夹进行不重复的命名,filenum用于后续对分割的每行图象进行不重复的命名
Mat img = imread("digits.png");//读入图片
Mat gray;
cvtColor(img, gray, CV_BGR2GRAY);//将图像img转换为灰度图存入gray中
int b = 20;
int m = gray.rows / b; //原图为1000*2000
int n = gray.cols / b; //裁剪为5000个20*20的小图块

cvtColor() :用于将图像从一个颜色空间转换到另一个颜色空间的转换(目前常见的颜色空间均支持),并且在转换的过程中能够保证数据的类型不变,即转换后的图像的数据类型和位深与源图像一致。

参数:

  • InputArray src: 输入图像即要进行颜色空间变换的原图像,可以是Mat类

  • OutputArray dst: 输出图像即进行颜色空间变换后存储图像,也可以Mat类

  • int code: 转换的代码或标识,即在此确定将什么制式的图片转换成什么制式的图片,

  • int dstCn = 0: 目标图像通道数,如果取值为0,则由src和code决定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (int i = 0; i < m; i++)
{
int offsetRow = i*b; //行上的偏移量
if(i%5==0&&i!=0)
{
filename++;
filenum=0;
}
for (int j = 0; j < n; j++)
{
int offsetCol = j*b; //列上的偏移量
sprintf_s(ad, "D:\\data\\%d\\%d.jpg",filename,filenum++);//将文件名和文件数形成文件的路径
Mat tmp;
gray(Range(offsetRow, offsetRow + b), Range(offsetCol, offsetCol + b)).copyTo(tmp);//将gray的小块复制到tmp图中
imwrite(ad,tmp);//按照ad的路径格式输出图片tmp
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <stdio.h>  
#include <time.h>
#include <opencv2/opencv.hpp>
#include <opencv/cv.h>
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/ml/ml.hpp>
#include <io.h>

using namespace std;
using namespace cv;

void getFiles( string path, vector<string>& files);
void get_1(Mat& trainingImages, vector<int>& trainingLabels);
void get_0(Mat& trainingImages, vector<int>& trainingLabels);

int main()
{
//获取训练数据
Mat classes;
Mat trainingData;
Mat trainingImages;
vector<int> trainingLabels;
get_1(trainingImages, trainingLabels);
get_0(trainingImages, trainingLabels);
Mat(trainingImages).copyTo(trainingData);
trainingData.convertTo(trainingData, CV_32FC1);
Mat(trainingLabels).copyTo(classes);
//配置SVM训练器参数
CvSVMParams SVM_params;
SVM_params.svm_type = CvSVM::C_SVC;
SVM_params.kernel_type = CvSVM::LINEAR;
SVM_params.degree = 0;
SVM_params.gamma = 1;
SVM_params.coef0 = 0;
SVM_params.C = 1;
SVM_params.nu = 0;
SVM_params.p = 0;
SVM_params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, 0.01);
//训练
CvSVM svm;
svm.train(trainingData, classes, Mat(), Mat(), SVM_params);
//保存模型
svm.save("svm.xml");
cout<<"训练好了!!!"<<endl;
getchar();
return 0;
}
void getFiles( string path, vector<string>& files )
{
long hFile = 0;
struct _finddata_t fileinfo;
string p;
if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) != -1)
{
do
{
if((fileinfo.attrib & _A_SUBDIR))
{
if(strcmp(fileinfo.name,".") != 0 && strcmp(fileinfo.name,"..") != 0)
getFiles( p.assign(path).append("\\").append(fileinfo.name), files );
}
else
{
files.push_back(p.assign(path).append("\\").append(fileinfo.name) );
}
}while(_findnext(hFile, &fileinfo) == 0);

_findclose(hFile);
}
}
void get_1(Mat& trainingImages, vector<int>& trainingLabels)
{
char * filePath = "D:\\data\\train_image\\1";
vector<string> files;
getFiles(filePath, files );
int number = files.size();
for (int i = 0;i < number;i++)
{
Mat SrcImage=imread(files[i].c_str());
SrcImage= SrcImage.reshape(1, 1);
trainingImages.push_back(SrcImage);
trainingLabels.push_back(1);
}
}
void get_0(Mat& trainingImages, vector<int>& trainingLabels)
{
char * filePath = "D:\\data\\train_image\\0";
vector<string> files;
getFiles(filePath, files );
int number = files.size();
for (int i = 0;i < number;i++)
{
Mat SrcImage=imread(files[i].c_str());
SrcImage= SrcImage.reshape(1, 1);
trainingImages.push_back(SrcImage);
trainingLabels.push_back(0);
}
}

整个训练过程可以分为以下几个部分:

1.数据准备

该例程中一个定义了三个子程序用来实现数据准备工作: getFiles() 用来遍历文件夹下所有文件 getBubble() 用来获取有气泡的图片和与其对应的Labels,该例程将Labels定为1。 getNoBubble() 用来获取没有气泡的图片与其对应的Labels,该例程将Labels定为0。 在该代码中,getBubble()getNoBubble() (即get_1()get_0() )将获取一张图片后会将图片(特征)写入到容器(vector)中,紧接着会将标签写入另一个容器中,这样就保证了特征和标签是一一对应的关系,push_back(0)或者push_back(1)其实就是我们贴标签的过程。

1
2
trainingImages.push_back(SrcImage);
trainingLabels.push_back(0);

在主函数中,将getBubble()与getNoBubble()写好的包含特征的矩阵拷贝给trainingData,将包含标签的vector容器进行类型转换后拷贝到trainingLabels里,至此,数据准备工作完成,trainingData与trainingLabels就是我们要训练的数据。

1
2
3
4
5
6
7
8
9
Mat classes;
Mat trainingData;
Mat trainingImages;
vector<int> trainingLabels;
getBubble(trainingImages, trainingLabels);
getNoBubble(trainingImages, trainingLabels);
Mat(trainingImages).copyTo(trainingData);
trainingData.convertTo(trainingData, CV_32FC1);
Mat(trainingLabels).copyTo(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct CV_EXPORTS_W_MAP CvSVMParams
{
CvSVMParams();
CvSVMParams(
int svm_type,
int kernel_type,
double degree,
double coef0,
double Cvalue,
double p,
CvMat* class_weights,
CvTermCriteria term_crit );
CV_PROP_RW int svm_type;
CV_PROP_RW int kernel_type;
CV_PROP_RW double degree; // for poly
CV_PROP_RW double gamma; // for poly/rbf/sigmoid
CV_PROP_RW double coef0; // for poly/sigmoid
CV_PROP_RW double C; // for CV_SVM_C_SVC, CV_SVM_EPS_SVR and CV_SVM_NU_SVR
CV_PROP_RW double nu; // for CV_SVM_NU_SVC, CV_SVM_ONE_CLASS, and CV_SVM_NU_SVR
CV_PROP_RW double p; // for CV_SVM_EPS_SVR
CvMat* class_weights; // for CV_SVM_C_SVC
CV_PROP_RW CvTermCriteria term_crit; // termination criteria
};

在此代码中,我们定义了一个结构体变量用来配置这些参数,而这个变量也就是CVSVM类中train函数的第五个参数。

1
2
CvSVM svm;
svm.train(trainingData, classes, Mat(), Mat(), SVM_params);

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
2
3
4
5
6
7
8
9
10
11
12
class CV_EXPORTS_W CvSVM : public CvStatModel
{
public:
virtual bool train(
const CvMat* trainData,
const CvMat* responses,
const CvMat* varIdx=0,
const CvMat* sampleIdx=0,
CvSVMParams params=CvSVMParams() );
virtual float predict(
const CvMat* sample,
bool returnDFVal=false ) const;

我们就是应用类中定义的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>  
#include <time.h>
#include <opencv2/opencv.hpp>
#include <opencv/cv.h>
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/ml/ml.hpp>
#include <io.h>

using namespace std;
using namespace cv;

void getFiles( string path, vector<string>& files );

int main()
{
int result = 0;
char * filePath = "D:\\data\\test_image\\0";
vector<string> files;
getFiles(filePath, files );
int number = files.size();
cout<<number<<endl;
CvSVM svm;
svm.clear();
string modelpath = "svm.xml";
FileStorage svm_fs(modelpath,FileStorage::READ);
if(svm_fs.isOpened())
{
svm.load(modelpath.c_str());
}
for (int i = 0;i < number;i++)
{
Mat inMat = imread(files[i].c_str());
Mat p = inMat.reshape(1, 1);
p.convertTo(p, CV_32FC1);
int response = (int)svm.predict(p);
if (response == 0)
{
result++;
}
}
cout<<result<<endl;
getchar();
return 0;
}
void getFiles( string path, vector<string>& files )
{
long hFile = 0;
struct _finddata_t fileinfo;
string p;
if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) != -1)
{
do
{
if((fileinfo.attrib & _A_SUBDIR))
{
if(strcmp(fileinfo.name,".") != 0 && strcmp(fileinfo.name,"..") != 0)
getFiles( p.assign(path).append("\\").append(fileinfo.name), files );
}
else
{ files.push_back(p.assign(path).append("\\").append(fileinfo.name) );
}
}while(_findnext(hFile, &fileinfo) == 0);
_findclose(hFile);
}
}

在上面我们把该介绍的都说的差不多了,这个例程中只是用到了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

0%