zoukankan      html  css  js  c++  java
  • C++开发人脸性别识别教程(6)——通过SVM实现性别识别

      上一篇教程中我们介绍了怎样使用OpenCv封装的FaceRecognizer类实现简单的人脸性别识别,这里我们为大家提供第二种主要的性别识别手段——支持向量机(SVM)。

      支持向量机在解决二分类问题方面有着强大的威力(当然也能够解决多分类问题)。性别识别是典型的二分类模式识别问题,因此非常适合用SVM进行处理,同一时候OpenCv又对SVM进行了非常好的封装,调用非常方便,因此我们在这个性别识别程序中考虑增加SVM方法。

      在这里我们採用了HOG+SVM的模式来进行,即先提取图像的HOG特征。然后将这些HOG特征输入SVM中进行训练。

      一、SVM概述

      SVM的数学原理十分复杂,我们不在这里过多讨论,有关OpenCv中SVM的使用方法,这里为大家提供两篇博客以供參考:OpenCV的SVM使用方法以及OpenCV 2.4+ C++ SVM介绍

      二、HOG特征概述

      HOG特征是图像的梯度特征。详细參见:目标检測的图像特征提取之(一)HOG特征

      三、建立训练集

      这里继续沿用上一篇博文中提到的性别识别训练集,400张男性人脸样本400张女性人脸样本,下载地址:性别识别数据集

      四、算法的训练与測试

      1、建立控制台project,配置OpenCv环境

      这里将project命名为:GenderSVM。

      2、编写批量读取函数read_csv()

      仅仅要涉及到训练。都须要批量读取训练样本的操作,SVM也不例外,因此须要先编写批量读取函数read_csv()。考虑到之前的批量读取函数必须一次性将全部训练样本读入内存中,内存消耗较大。在这里做一个小小的改进:

    void read_csv(String& csvPath,Vector<String>& trainPath,Vector<int>& label,char separator = ';')
    {
        string line,path,classLabel;
        ifstream file(csvPath.c_str(),ifstream::in);
        while (getline(file,line))
        {
            stringstream lines(line);
            getline(lines,path,separator);
            getline(lines,classLabel);
            if (!path.empty()&&!classLabel.empty())
            {
                trainPath.push_back(path);
                label.push_back(atoi(classLabel.c_str()));
            }
        }
    }

      可见这里我们将输入參数由vector<Mat>改为vector<String>。然后返回装有训练样本的全部路径的容器,须要时在依据当中的路径进行读取。减少了内存占用量。

      3、读入训练样本路径

        string trainCsvPath = "E:\性别识别数据库—CAS-PEAL\at.txt";
        vector<String> vecTrainPath;
        vector<int> vecTrainLabel;
        read_csv(trainCsvPath,vecTrainPath,vecTrainLabel);

      顺利批量读入路径:

      4、训练初始化

      在提取HOG特征之前,须要初始化训练数据矩阵:

        /**********初始化训练数据矩阵**********/
        int iNumTrain = 800;
        Mat trainDataHog;
        Mat trainLabel = Mat::zeros(iNumTrain,1,CV_32FC1);

      须要强调的是SVM的训练数据必须都是CV_32FC1格式,因此这里显式的将标签矩阵trainLabel初始化为CV_32FC1格式,trainDataHog稍后进行初始化。

      5、提取图像HOG特征

      接下来循环读入全部的训练样本,提取HOG特征,放在训练数据矩阵中。考虑嵌套代码的复杂性,这里先给出总体代码。稍后解释:

        /**********提取HOG特征。放入训练数据矩阵中**********/
        Mat imageSrc;
        for (int i = 0; i < iNumTrain; i++)
        {
            imageSrc = imread(vecTrainPath[i].c_str(),1);
            resize(imageSrc,imageSrc,Size(64,64));
            HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
                                                   cvSize(8,8),cvSize(8,8),9); 
            vector<float> descriptor;
            hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0));
    
            if (i == 0)
            {
                trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1);
            }
    
            int n = 0;
            for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
            {
                trainDataHog.at<float>(i,n) = *iter;
                n++;
            }
            trainLabel.at<float>(i,0) = vecTrainLabel[i];
        }

      接下来我们对这段代码进行详解。

      (1)循环读入训练样本

      从vecTrainPath容器中逐条取出训练样本路径,然后读取:

            imageSrc = imread(vecTrainPath[i].c_str(),1);

      (2)尺寸归一化

      我们这里将图像尺寸归一化为64*64,这是由于当时在敲代码时參考了一篇关于HOG特征的博客。

    这里的尺寸大家能够任意设定,当然也会影响终于的识别效率,64*64可能并非一个最优的尺寸:

            imageSrc = imread(vecTrainPath[i].c_str(),1);
            resize(imageSrc,imageSrc,Size(64,64));

      (3)计算HOG特征

      OpenCv给出的HOG特征计算接口很简洁,三句话即完毕:

            HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
                                                   cvSize(8,8),cvSize(8,8),9); 
            vector<float> descriptor;
            hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0));

      提取的特征以容器的数据 结构形式给出。至于计算时的參数设定,參见我之前提供的那两篇博客就可以。

      (4)初始化数据矩阵trainDataHog

      前面提到,SVM中用到的训练数据矩阵必须是CV_32FLOAT形式的。因此须要对数据矩阵显示的指定其尺寸和类型。

    然后因为trainDataHog行数为训练样本个数。而列数为图片HOG特征的维数。因此无法在进行HOG特征提取之前确定其尺寸,因此这里选择在进行完第一张样本的HOG特征、得到相应维数之后,在进行初始化:

            if (i == 0)
            {
                trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1);
            }

      (5)将得到的HOG特征存入数据矩阵

      得到的HOG特征是浮点数容器的形式,我们须要将其转换成矩阵的形式以便于训练SVM,这就涉及到了vector和Mat两个数据结构的遍历。vector遍历这里推荐使用迭代器的方式。而Mat遍历这里则选择了相对耗时可是最简单的方式——直接使用at函数:

            int n = 0;
            for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
            {
                trainDataHog.at<float>(i,n) = *iter;
                n++;
            }
            trainLabel.at<float>(i,0) = vecTrainLabel[i];

      训练得到的HOG特征如图所看到的:

      可见在当前的參数设定下,提取到的HOG特征为1764维,共800张训练样本。每一行代表一个图片的HOG特征向量。通过“ctrl+鼠标滚轮”放大观察特征向量的详细參数:

      6、训练SVM分类器

      有关OpenCv中SVM分类器的使用能够參见下面博客:OpenCV 2.4+ C++ SVM介绍

      首先。初始化相关參数:

        /**********初始化SVM分类器**********/
        CvSVM svm;  
        CvSVMParams param;    
        CvTermCriteria criteria;      
        criteria = cvTermCriteria( CV_TERMCRIT_EPS, 1000, FLT_EPSILON );      
        param = CvSVMParams(CvSVM::C_SVC, CvSVM::RBF, 
                            10.0, 0.09, 1.0, 10.0, 0.5, 1.0, NULL, criteria );  

      開始训练、训练完毕后保存分类器:

        /**********训练并保存SVM**********/
        svm.train(trainDataHog,trainLabel,Mat(),Mat(),param);
        svm.save("E:\性别识别数据库—CAS-PEAL\SVM_SEX_Model.txt");

      注意我们这里选择将分类器保存为txt形式:

      当然,我们能够打开这个txt文件。查看里面的參数:

      7、測试分类效果

      測试过程和训练过程基本同样。读取图片、尺寸归一化、提取HOG特征、预測:

        /**********測试SVM分类性能**********/
        Mat testImage = imread("E:\性别识别数据库—CAS-PEAL\測试样本\女性測试样本\face_35.bmp");
        resize(testImage,testImage,Size(64,64));
    
        HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
            cvSize(8,8),cvSize(8,8),9); 
        vector<float> descriptor;
        hog->compute(testImage,descriptor,Size(1,1),Size(0,0));
    
        Mat testHog = Mat::zeros(1,descriptor.size(),CV_32FC1);
        int n = 0;
        for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
        {
            testHog.at<float>(0,n) = *iter;
            n++;
        }
    
        int predictResult = svm.predict(testHog);

      8、完整代码

     这里给出HOG+SVM进行性别识别的完整代码:

    // GenderSVM.cpp : 定义控制台应用程序的入口点。
    //
    
    #include "stdafx.h"
    #include <opencv2opencv.hpp>
    #include <iostream>
    #include <sstream>
    #include <fstream>
    
    using namespace std;
    using namespace cv;
    
    void read_csv(String& csvPath,vector<String>& trainPath,vector<int>& label,char separator = ';')
    {
        string line,path,classLabel;
        ifstream file(csvPath.c_str(),ifstream::in);
        while (getline(file,line))
        {
            stringstream lines(line);
            getline(lines,path,separator);
            getline(lines,classLabel);
            if (!path.empty()&&!classLabel.empty())
            {
                trainPath.push_back(path);
                label.push_back(atoi(classLabel.c_str()));
            }
        }
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        /**********批量读入训练样本路径**********/
        string trainCsvPath = "E:\性别识别数据库—CAS-PEAL\at.txt";
        vector<String> vecTrainPath;
        vector<int> vecTrainLabel;
        read_csv(trainCsvPath,vecTrainPath,vecTrainLabel);
    
        /**********初始化训练数据矩阵**********/
        int iNumTrain = 800;
        Mat trainDataHog;
        Mat trainLabel = Mat::zeros(iNumTrain,1,CV_32FC1);
    
        /**********提取HOG特征,放入训练数据矩阵中**********/
        Mat imageSrc;
        for (int i = 0; i < iNumTrain; i++)
        {
            imageSrc = imread(vecTrainPath[i].c_str(),1);
            resize(imageSrc,imageSrc,Size(64,64));
            HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
                cvSize(8,8),cvSize(8,8),9); 
            vector<float> descriptor;
            hog->compute(imageSrc,descriptor,Size(1,1),Size(0,0));
    
            if (i == 0)
            {
                trainDataHog = Mat::zeros(iNumTrain,descriptor.size(),CV_32FC1);
            }
    
            int n = 0;
            for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
            {
                trainDataHog.at<float>(i,n) = *iter;
                n++;
            }
            trainLabel.at<float>(i,0) = vecTrainLabel[i];
        }
    
        /**********初始化SVM分类器**********/
        CvSVM svm;  
        CvSVMParams param;    
        CvTermCriteria criteria;      
        criteria = cvTermCriteria( CV_TERMCRIT_EPS, 1000, FLT_EPSILON );      
        param = CvSVMParams(CvSVM::C_SVC, CvSVM::RBF, 
            10.0, 0.09, 1.0, 10.0, 0.5, 1.0, NULL, criteria );     
    
        /**********训练并保存SVM**********/
        svm.train(trainDataHog,trainLabel,Mat(),Mat(),param);
        svm.save("E:\性别识别数据库—CAS-PEAL\SVM_SEX_Model.txt");
    
        /**********測试SVM分类性能**********/
        Mat testImage = imread("E:\性别识别数据库—CAS-PEAL\測试样本\女性測试样本\face_35.bmp");
        resize(testImage,testImage,Size(64,64));
    
        HOGDescriptor *hog = new HOGDescriptor(cvSize(64,64),cvSize(16,16),
            cvSize(8,8),cvSize(8,8),9); 
        vector<float> descriptor;
        hog->compute(testImage,descriptor,Size(1,1),Size(0,0));
    
        Mat testHog = Mat::zeros(1,descriptor.size(),CV_32FC1);
        int n = 0;
        for (vector<float>::iterator iter = descriptor.begin();iter != descriptor.end();iter++)
        {
            testHog.at<float>(0,n) = *iter;
            n++;
        }
        int predictResult = svm.predict(testHog);
    
        return 0;
    }

      五、总结

      以上就是通过HOG特征+SVM进行性别识别的完整代码。在编写代码的过程中遇到了一些有趣的问题,这里稍作总结。

      1、变量命名格式

      当代码量非常大的时候,变量的命名格式就显得十分重要。相信大家早已不用那种a、b、m、n这样的简单的无意义的命名方法了。

    在C++中推荐大家使用匈牙利命名法,即“类型缩写+变量名缩写”的命名格式。比如vecTrainPath这个变量名,前缀“vec”表明这个变量是一个vector格式的变量,而“TrainPath”则表明这个容器中存放的是训练样本的路径。这样的命名方式在大型project中非常重要,另一点须要注意的是当变量名中出现多个缩略短语时。推荐第一个短语小写。其它短语的首字母大写。

      2、为何选择HOG特征

      通过实验发现。直接将图像向量化后输入SVM(不经过特征提取)的方式的正确率将不理想。尽管本质上像素本身最能代表图像的语义信息,但由于SVM并不具备特征提取能力。因此效果不佳。确切的说,特征提取是模式分类的必要过程,即便是深度学习也不例外,由于深度学习(DeepLearning)本质上也是一种特征提取的手段,仅仅只是提取得到的特征更深层,更抽象。表现力更强。为此我之前曾专门写过一篇博客进行阐述:浅谈模式识别中的特征提取

      当然这里大家能够尝试提取其它特征之后再进行分类,甚至能够考虑通过提起深度特征来进行分类。这里仅仅是以HOG特征为例而已。

      4、有关vector的一些使用(为什么不用int型数组)

      在这段代码中我们大量用到了vector结构,这是C++11的新特性。

    细致观察。事实上vector结构的最明显的一个优势就是可以动态分配大小,实时加入/删除元素,这点是数组所不能实现的。

    尽管可以通过new操作符来实现数组的动态分配,但我们仍推荐大家在须要使用可动态变化的数组的场合。使用vector。

      5、Vectot和vector

      在编写代码是细致留心编译器给出的拼写提示,会发现这样一现象:

      那么vector和Vector有什么差别呢?一句话,Vector是OpenCv中的vector,类似的还有String和string等。

    Vector和String这类结构是隶属于OpenCv的:

      OK,以上就是这次博文的全部内容。在接下来的博文中我们将開始进入MFC编程阶段。欢迎大家讨论:http://blog.csdn.net/u013088062

  • 相关阅读:
    Android传递中文参数方法(之一)
    配置类与yaml
    修改ip失败,一个意外的情况处理方法
    oracle 自增序列与触发器
    Excel导入数据带小数点的问题
    数据库null与空的区别
    小米手机无法打开程序报错Unable to instantiate application com.android.tools.fd.runtime.BootstrapApplication的解决办法
    gradle类重复的问题解决方法
    windowSoftInputMode属性讲解
    android studio 的配置
  • 原文地址:https://www.cnblogs.com/claireyuancy/p/7142747.html
Copyright © 2011-2022 走看看