C++开发人脸性别识别教程(6)——通过SVM实现性别识别

2024-08-28 18:18

本文主要是介绍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、建立控制台工程,配置OpenCv环境

  这里将工程命名为: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 <opencv2\opencv.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”则表明这个容器中存放的是训练样本的路径。这种命名方式在大型工程中非常重要,还有一点需要注意的是当变量名中出现多个缩略短语时,推荐第一个短语小写,其他短语的首字母大写。

  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。

这篇关于C++开发人脸性别识别教程(6)——通过SVM实现性别识别的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1115524

相关文章

Java实现优雅日期处理的方案详解

《Java实现优雅日期处理的方案详解》在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间,下面我们就来看看如何使用java处理这样的日期问题吧,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言一、日期的坑1.1 日期格式化陷阱1.2 时区转换二、优雅方案的进阶之路2.1 线程安全重构2

Android实现两台手机屏幕共享和远程控制功能

《Android实现两台手机屏幕共享和远程控制功能》在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值,本项目旨在实现两台Android手... 目录一、项目概述二、相关知识2.1 MediaProjection API2.2 Socket 网络

使用Python实现图像LBP特征提取的操作方法

《使用Python实现图像LBP特征提取的操作方法》LBP特征叫做局部二值模式,常用于纹理特征提取,并在纹理分类中具有较强的区分能力,本文给大家介绍了如何使用Python实现图像LBP特征提取的操作方... 目录一、LBP特征介绍二、LBP特征描述三、一些改进版本的LBP1.圆形LBP算子2.旋转不变的LB

Maven的使用和配置国内源的保姆级教程

《Maven的使用和配置国内源的保姆级教程》Maven是⼀个项目管理工具,基于POM(ProjectObjectModel,项目对象模型)的概念,Maven可以通过一小段描述信息来管理项目的构建,报告... 目录1. 什么是Maven?2.创建⼀个Maven项目3.Maven 核心功能4.使用Maven H

Redis消息队列实现异步秒杀功能

《Redis消息队列实现异步秒杀功能》在高并发场景下,为了提高秒杀业务的性能,可将部分工作交给Redis处理,并通过异步方式执行,Redis提供了多种数据结构来实现消息队列,总结三种,本文详细介绍Re... 目录1 Redis消息队列1.1 List 结构1.2 Pub/Sub 模式1.3 Stream 结

C# Where 泛型约束的实现

《C#Where泛型约束的实现》本文主要介绍了C#Where泛型约束的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录使用的对象约束分类where T : structwhere T : classwhere T : ne

将Java程序打包成EXE文件的实现方式

《将Java程序打包成EXE文件的实现方式》:本文主要介绍将Java程序打包成EXE文件的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录如何将Java程序编程打包成EXE文件1.准备Java程序2.生成JAR包3.选择并安装打包工具4.配置Launch4

MySQL索引的优化之LIKE模糊查询功能实现

《MySQL索引的优化之LIKE模糊查询功能实现》:本文主要介绍MySQL索引的优化之LIKE模糊查询功能实现,本文通过示例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录一、前缀匹配优化二、后缀匹配优化三、中间匹配优化四、覆盖索引优化五、减少查询范围六、避免通配符开头七、使用外部搜索引擎八、分

Python实现特殊字符判断并去掉非字母和数字的特殊字符

《Python实现特殊字符判断并去掉非字母和数字的特殊字符》在Python中,可以通过多种方法来判断字符串中是否包含非字母、数字的特殊字符,并将这些特殊字符去掉,本文为大家整理了一些常用的,希望对大家... 目录1. 使用正则表达式判断字符串中是否包含特殊字符去掉字符串中的特殊字符2. 使用 str.isa

Spring Boot 集成 Quartz并使用Cron 表达式实现定时任务

《SpringBoot集成Quartz并使用Cron表达式实现定时任务》本篇文章介绍了如何在SpringBoot中集成Quartz进行定时任务调度,并通过Cron表达式控制任务... 目录前言1. 添加 Quartz 依赖2. 创建 Quartz 任务3. 配置 Quartz 任务调度4. 启动 Sprin