成果展示放在前面
代码下载链接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,通过按键,可以实现用户自己进行拍照,将自己的照片存入照片库中,通过按键进行训练,然后打开摄像头,进行人脸识别。
主要参考文章:
主要程序还是参考的这篇博客,真的学到了很多。
在人脸显示名字的时候调用的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);
这里是读取目标文件夹下的各个文件夹名字所借鉴的博客
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;
}
以上为程序中的主要函数的介绍。
如果有问题欢迎一起探讨,我也是通过阅读大牛的文章一点一点学习的,希望能共同进步。
如果文章中有错误,欢迎指出,一定及时修改。