vs+qt 人脸识别GUI

成果展示放在前面

代码下载链接https://download.csdn.net/download/qq_37071435/12277061 

打包好的软件链接https://download.csdn.net/download/qq_37071435/12270477

opencv4.2+contribute https://download.csdn.net/download/qq_37071435/12265358

注意:打包的软件中没有更新中文名的输入,只支持英文,环境为vs2019+opencv4.2+qt

 

主要功能:利用vs的qt扩展模块,编写了GUI,通过按键,可以实现用户自己进行拍照,将自己的照片存入照片库中,通过按键进行训练,然后打开摄像头,进行人脸识别。

主要参考文章:

https://blog.csdn.net/qq_37791134/article/details/81385848?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

主要程序还是参考的这篇博客,真的学到了很多。

在人脸显示名字的时候调用的putText()函数,只能显示英文,所以在csdn中搜索到了这篇博客,直接解决了问题

https://blog.csdn.net/weixinhum/article/details/84074594

下面依次讲解各个函数

首先是读取目标文件夹下的文件目录

 

如图,face_database为我放置人脸照片和at.txt文件的目标文件夹(下面称其为目标文件夹),该文件夹下的子文件夹的名字从0开始,都为正整数,依次增加,其中任意一个子文件夹中都有十张人脸照片(除了0),这里我用的是.pgm格式的照片,当然.jpg格式也可以.

at.txt文件是在后边训练时所用到的一个存放了所有图像路径的文本文件,可以看到,里面每一行的格式为:图片路径;label;人名。

label与所在的文件夹的名字相同。我在label后边加了一个人名,是为了方便在识别人脸的时候可以从文件中读取对应的名字,这里只填了一个标签,右下角被黄色荧光笔涂抹的地方可以看见"ANSI",这个表明该文本的编码与本机默认编码相同,一般文本默认的编码方式是UTF8,所以要注意改成ANSI。点击文件->另存为,r然后更改编码方式,覆盖原文件就可以了。

at.txt文件其实叫做csv文件,但是它的本质就如我们所见,是一个可写的文本文件,我刚开始也查了很多资料去生成他,有各种方式,但最后我选择了比较笨的自己写入,通过调用C++的fstream来写入我想要的格式。

主要思路是:读取文件夹的名字,生成label。因为文件夹的名字是正整数,所以最后一个文件夹的名字就是含有照片文件夹的数量(文件夹名为0的文件夹内不含照片,它的存在只是起到方便累加的作用)

 这里先把所有函数的声明放在这里,避免在下面函数介绍的时候,不知道某个函数的作用。

//GUI
class facerecognizer_gui : public QMainWindow
{
	Q_OBJECT

public:
	facerecognizer_gui(QWidget *parent = Q_NULLPTR);
	~facerecognizer_gui();

private:
	Ui::facerecognizer_guiClass ui;
	Mat image;

	QLabel* label_3;
    //输入的人名TEXTEDIT
	QTextEdit* EnterLabel;
private slots:
    //若函数为on_名字_clicked() 会触发两次 注意
	void ShowButton_clicked();//人脸识别
	void TakePhoto_clicked();//照相
	void Train_clicked();//训练
	void CloseCarmera_clicked();//关闭摄像头
	void closeEvent(QCloseEvent* e);//关闭窗口事件
};

//Label显示Mat图像
void LabelDisplayMat(QLabel* label, cv::Mat& mat);
//从at.txt中读取信息,初始化LabelName
void InitLabelName();

//获得目标文件夹下的所有文件夹名 存入向量中
void getSubdirs(std::string path, std::vector<std::string>& files);
//sort的bool参数
bool sort_fun(const string& p1, const string& p2);
//返回目标文件夹下的文件夹名中数字最大的值
int back_file_number();
//outi:为新建文件夹的的名字
//label:标签 int类型 
void write_at(int outi, int label);
//string 转化成int型
int StringToInt(string a);
//读取at.txt
void read_csv(const string& filename, vector<Mat>& images,
	vector<int>& labels, char separator);
//进行训练 生成xml文件
int TrainAndXml(void);
//识别图片 返回识别的序号
int Predict(Mat src_image);

这里是读取目标文件夹下的各个文件夹名字所借鉴的博客

https://blog.csdn.net/leo_888/article/details/80681184?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

void getSubdirs(std::string path, std::vector<std::string>& files)
{
	long long hFile = 0;//注意如果是long handle则在64位下会出现异常
	struct _finddata_t fileinfo;
	std::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)
					files.push_back(fileinfo.name);
			}
		} while (_findnext(hFile, &fileinfo) == 0);
		_findclose(hFile);
	}
}

下面为为了生成at.txt的各个函数 ,主要思路是,读取目标文件夹下的文件夹名,将其转化为int,进行比较,取最大值,便可知道目标文件夹下有过少个含有照片的文件夹了。

//sort的bool参数
bool sort_fun(const string& p1, const string& p2)
{
	stringstream stream;
	int number1 = 0, number2 = 0;
	stream << p1; stream >> number1; stream.clear();
	stream << p2; stream >> number2; stream.clear();
	return number1 > number2;//降序排列  
}
//返回目标文件夹下的文件夹名中数字最大的值
int back_file_number()
{
	string inPath = "Resources/face_database/";
	vector<string> filename;
    //获取目标文件夹下的文件夹名的vector
	getSubdirs(inPath, filename);
    //排序,取最大值,即目标文件夹下所含有的文件夹的数量
	sort(filename.begin(), filename.end(), sort_fun);
	string out = *filename.begin();
	return StringToInt(out);
}
//outi:为新建文件夹的的名字
//label:标签 int类型 
//为at.txt写入内容
void write_at(int outi, int enterlabel)
{
	int i = 1;
	ofstream outfile("Resources/face_database/at.txt", ofstream::app);
	string temp = "Resources\\face_database";
	string dirName = temp + "\\" + to_string(outi);
	_mkdir(dirName.c_str());//_mkdir()创建成功返回0,失败返回-1。
    //分别写入十张照片的路径,label与姓名
	while (i != 11)
	{
		outfile << temp << "\\" << outi << "\\" << i << ".pgm;" << enterlabel << ";" <<         LabelName[outi] << endl;
		i++;
	}
	outfile.close();
}
//利用stringstream来转换string成int
int StringToInt(string a)
{
	int ai = 0;
	stringstream stream;
	stream << a;
	stream >> ai;
	stream.clear();
	return ai;
}

通过上面的函数,我们的at.txt文件就写好了。熟悉了照片的存储路径与方式,下面还需要先了解一下一些全局变量,就可以进行拍照存储了。LabelName会在人脸识别函数前讲解。

//LabelName key存放序号 value存放人名
map<int,string> LabelName;
int map_size = 0;//map_size为map中含有的数量
//导入其他程序中的引用
/**.xml文件位置 */
String face_cascade_name = "Resources/haarcascades/haarcascade_frontalface_default.xml";
String eyes_cascade_name = "Resources/haarcascades/haarcascade_eye_tree_eyeglasses.xml";
String window_name = "Capture - Face detection";
String haarcascade_frontalface_alt2 = "Resources/haarcascades/haarcascade_frontalface_alt2.xml";

RNG g_rng(12345);
Ptr<FaceRecognizer> model;
int NumberOfPeople = 1;
//检测是否关闭摄像头
//1 为关闭 0 为打开
int flag = 0;
//全程检测摄像头的状态
VideoCapture cap;

下面使拍照的函数,该函数是通过TakePhoto按键触发的,对于qt按键触发的函数,可以自己命名也可以用默认的,这里是自己命名的,需要注意,要将该函数,在ui界面中加入槽函数中。

void facerecognizer_gui::TakePhoto_clicked()
{
	//获取textEdit中的文本
	QString strTxtEdt = ui.name->toPlainText();
	string LabelString;
    //如果再拍照前没有输入名字的话,会跳出警告窗口
	if (strTxtEdt == "")
	{
		QMessageBox::warning(NULL, "warning", "if you do not enter your name,sorry you will stay here",
			QMessageBox::Yes);
	}
	else if (!cap.isOpened())//检测摄像头是否开着,避免与人脸检测同时使用摄像头
	{
        //获取输入的人名
		LabelString = strTxtEdt.toStdString();
        //避免拍照未完成就连续请求拍照,使拍照键无法使用,直到拍完十张后才恢复
		ui.TakePhoto->setEnabled(false);
        //按下按键后改变按键上的图案
		QIcon icon;
		icon.addFile(tr("Resources/takephoto_down.jpg"));
		ui.TakePhoto->setIcon(icon);
		ui.TakePhoto->setIconSize(QSize(100, 30));
	    //a 记录拍照的状态,在GUI上显示
		QString a;
		//要建立文件夹的名字
		int outi = back_file_number() + 1;

		//将label与名字存入map中 方便后边查询
		LabelName[outi] = LabelString;
		map_size++;

		//建立文件夹并更新at.txt
		write_at(outi, outi);
        //加载模型 检测人脸 进行拍照
		CascadeClassifier cascada;
		cascada.load(haarcascade_frontalface_alt2);
		cap.open(0);
		Mat frame, myFace;
		int pic_num = 1;

		a = "taking";
		ui.label_3->setText(a);

		while (1) {
			//摄像头读图像
			cap >> frame;
			vector<Rect> faces;//vector容器存检测到的faces
			Mat frame_gray;
			cvtColor(frame, frame_gray, COLOR_BGR2GRAY);//转灰度化,减少运算
			cascada.detectMultiScale(frame_gray, faces, 1.1, 4, 0);
			printf("检测到人脸个数:%d\n", (int)faces.size());
			for (int i = 0; i < faces.size(); i++)
			{
				rectangle(frame, faces[i], Scalar(255, 0, 0), 2, 8, 0);
			}
			//当只有一个人脸时,开始拍照
			if (faces.size() == 1)
			{
				Mat faceROI = frame_gray(faces[0]);//在灰度图中将圈出的脸所在区域裁剪出
				//faces[0]即为检测到的唯一的人脸的矩阵
				cv::resize(faceROI, myFace, Size(92, 112));//将faceROI裁为92*112赋值给myFace
				putText(frame, to_string(pic_num), faces[0].tl(), 3, 1.2, (0, 0, 225), 2, 0);//在 faces[0].tl()的左上角上面写序号
				string filename = format("Resources/face_database/%d/%d.pgm", outi, pic_num);
				imwrite(filename, myFace);//存在当前目录下

				LabelDisplayMat(ui.photo, myFace);
				waitKey(500);//等待500us
				pic_num++;//序号加1
				if (pic_num == 11)
				{
					break;//当序号为11时退出循环
				}
			}

		}
		a = "over";
		ui.label_3->setText(a);
		icon.addFile(tr("Resources/takephoto.jpg"));
		ui.TakePhoto->setIcon(icon);
		ui.TakePhoto->setIconSize(QSize(100, 30));
		ui.photo->setPixmap(QPixmap(tr("Resources/photoback.jpg")));
        //关闭摄像头
		cap.release();
        //回复按键
		ui.TakePhoto->setEnabled(true);
	}
	
}

从函数可以知道,处理过后的照片大小为112*92像素 的灰度图。

经过上述的步骤,我们的照片也照好了,下一步该进行训练了。

//按键触发的槽函数
void facerecognizer_gui::Train_clicked()
{
    //a用来记录训练的状态
	QString a;
	a = "training";
	ui.label_6->setText(a);
	ui.Train->setEnabled(false);

	QIcon icon;
	icon.addFile(tr("Resources/train_down.jpg"));
	ui.Train->setIcon(icon);
	ui.Train->setIconSize(QSize(100, 30));
    //调用训练函数
	TrainAndXml();

	a = "over";
	ui.label_6->setText(a);
	
	icon.addFile(tr("Resources/train.jpg"));
	ui.Train->setIcon(icon);
	ui.Train->setIconSize(QSize(100, 30));
	ui.Train->setEnabled(true);
}
//训练函数
int TrainAndXml()
{
	//读取你的CSV文件路径.   
	//string fn_csv = "H:/face_database/at.txt";
	string fn_csv = "Resources/face_database/at.txt";

	// 2个容器来存放图像数据和对应的标签  
	vector<Mat> images;
	vector<int> labels;
	// 读取数据. 如果文件不合法就会出错  
	// 输入的文件名已经有了.  
	try
	{
		read_csv(fn_csv, images, labels); //从csv文件中批量读取训练数据
	}
	catch (cv::Exception & e)
	{
		cerr << "Error opening file \"" << fn_csv << "\". Reason: " << e.msg << endl;
		// 文件有问题,我们啥也做不了了,退出了  
		exit(1);
	}
	// 如果没有读取到足够图片,也退出.  
	if (images.size() <= 1) {
		string error_message = "This demo needs at least 2 images to work. Please add more images to your data set!";
		CV_Error(CV_StsError, error_message);
	}

    //加载特征脸训练模型
	Ptr<BasicFaceRecognizer> model = EigenFaceRecognizer::create();
	model->train(images, labels);
	model->save("Resources/xml_file/PCA_train.xml");

	// 下面对测试图像进行预测,predictedLabel是预测标签结果  
	//注意predict()入口参数必须为单通道灰度图像,如果图像类型不符,需要先进行转换
	//predict()函数返回一个整形变量作为识别标签
	return 1;
}

其中读取at.txt文件的函数read_csv();注意对at.txt文件中的图片路径和abel的读取

void read_csv(const string& filename, vector<Mat>& images, vector<int>& labels, char separator = ';') 
{
	std::ifstream file(filename.c_str(), ifstream::in);//c_str()函数可用可不用,无需返回一个标准C类型的字符串
	//检测文件是否打开
	if (!file)
	{
		string error_message = "No valid input file was given, please check the given filename.";
		CV_Error(CV_StsBadArg, error_message);
	}
	//line:整行读入
	//path:文件夹路径
	string line, path, classlabel;
	while (getline(file, line)) //从文本文件中读取一行字符,未指定限定符默认限定符为“/n”
	{
		stringstream liness(line);//这里采用stringstream主要作用是做字符串的分割
		getline(liness, path, separator);//读入图片文件路径以分好作为限定符
		getline(liness, classlabel);//读入图片标签,默认限定符
		if (!path.empty() && !classlabel.empty()) //如果读取成功,则将图片和对应标签压入对应容器中
		{
			images.push_back(imread(path, 0));
			labels.push_back(atoi(classlabel.c_str()));
		}
	}
}

 终于到了最后一步人脸识别了,由于名字也存储在at.txt文件中,所以在每次人脸识别之前,我们一定要从at.txt文件中读取相应label多对应的人名,所以我们声明了一个map类型的变量LabelName来存放label和对性的人名,并写了一个函数来初始化他,并在GUI的构造函数中调用。

void InitLabelName()
{
	ifstream file("Resources/face_database/at.txt", ifstream::in);
	string line;
	while (getline(file, line)) //从文本文件中读取一行字符,未指定限定符默认限定符为“/n”
	{
        //;作为分隔符
		char separator = ';';
		string path;
		string classlabel;
		string name;
		stringstream liness(line);//这里采用stringstream主要作用是做字符串的分割
		getline(liness, path, separator);//读入图片文件路径以分好作为限定符
		getline(liness, classlabel,separator);//读入图片标签,默认限定符
		getline(liness, name);
		if (!path.empty() && !classlabel.empty()) //如果读取成功,则将图片和对应标签压入对应容器中
		{
			//将Label和name加入字典
			LabelName[StringToInt(classlabel)] = name;
			map_size++;
		}
	}
	file.close();
}

 LabelName介绍完之后,我们便可以开始进行人脸识别函数的介绍了。

//showbutton为开始人脸识别的按键的名字
void facerecognizer_gui::ShowButton_clicked()
{
	if (!cap.isOpened())//避免了与拍照同时进行
	{
		ui.ShowButton->setEnabled(false);

		QIcon icon;
		icon.addFile(tr("Resources/open_camera_down.jpg"));
		ui.ShowButton->setIcon(icon);
		ui.ShowButton->setIconSize(QSize(100, 30));
		icon.addFile(tr("Resources/close_camera.jpg"));
		ui.CloseCamera->setIcon(icon);
		ui.CloseCamera->setIconSize(QSize(100, 30));

		cap.open(0);    //打开默认摄像头  
		if (!cap.isOpened()) {}
		Mat frame;
		Mat gray;
		//这个分类器是人脸检测所用
		CascadeClassifier cascade;
		bool stop = false;
		//训练好的文件名称,放置在可执行文件同目录下  
		cascade.load(haarcascade_frontalface_alt2);
		//感觉用lbpcascade_frontalface效果没有它好,注意哈!要是正脸
		model = FisherFaceRecognizer::create();
		//1.加载训练好的分类器
		model->read("Resources/xml_file/PCA_train.xml");
		// opencv2用read
		//3.利用摄像头采集人脸并识别
		while (!flag)
		{
			cap >> frame;
			vector<Rect> faces(0);//建立用于存放人脸的向量容器
			cvtColor(frame, gray, CV_RGB2GRAY);//转化为灰度图
			equalizeHist(gray, gray); //变换后的图像进行直方图均值化处理  
			//检测人脸
			cascade.detectMultiScale(gray, faces, 1.1, 4, 0);
			Mat* pImage_roi = new Mat[faces.size()];    //定以数组
			Mat face;
			Point text_lb;//文本写在的位置
			//框出人脸
			string str;
			for (int i = 0; i < faces.size(); i++)
			{
				pImage_roi[i] = gray(faces[i]); //将所有的脸部保存起来
				text_lb = Point(faces[i].x, faces[i].y);
				if (pImage_roi[i].empty())continue;
				//获取对应的label
				int key = Predict(pImage_roi[i]);
				if (LabelName.find(key) != LabelName.end())
					str = LabelName[key];
				else str = "陌生人";
				Scalar color = Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255));//所取的颜色任意值
				rectangle(frame, Point(faces[i].x, faces[i].y), Point(faces[i].x + faces[i].width, faces[i].y + faces[i].height), color, 1, 8);//放入缓存
				//putText(frame, str, text_lb, FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255));//添加文字
				putTextZH(frame, str.c_str(), text_lb, Scalar(255, 0, 0), 20, "华文行楷");
			}

			delete[]pImage_roi;
			waitKey(200);
			LabelDisplayMat(ui.label, frame);
			if (flag)
			{
				flag = 0;
				icon.addFile(tr("Resources/open_camera.jpg"));
				ui.ShowButton->setIcon(icon);
				ui.ShowButton->setIconSize(QSize(100, 30));
				ui.label->setPixmap(QPixmap(tr("Resources/cameraback.jpg")));
				break;
			}
		}

	if (cap.isOpened())
		cap.release();//摄像头关闭
	ui.ShowButton->setEnabled(true);
	}
	
}

 其中Predict函数,返回的是int的label,通过map我们便可以直接通过键值,找到label对应的人名然后进行输出了,LabelName[label]。

int Predict(Mat src_image)
{
	Mat face_test;
	int predict = 0;
	//截取的ROI人脸尺寸调整
	if (src_image.rows >= 120)
	{
		//改变图像大小,使用双线性差值
		resize(src_image, face_test, Size(92, 112));

	}
	//判断是否正确检测ROI
	if (!face_test.empty())
	{
		//测试图像应该是灰度图  
		predict = model->predict(face_test);
	}
	cout << predict << endl;
	return predict;
}

 以上为程序中的主要函数的介绍。

如果有问题欢迎一起探讨,我也是通过阅读大牛的文章一点一点学习的,希望能共同进步。

如果文章中有错误,欢迎指出,一定及时修改。

 

  • 9
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若水菱花

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值