【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现

C/C++
240
0
0
2024-06-12

🌞一、项目介绍

项目简介:我们的项目是在linux操作系统下基于OpenCV和Socket的人脸识别系统。 客户端: 用于向服务器发送摄像头捕获的图像数据。 服务端: 在接收客户端发送的图像数据后,使用人脸检测算法检测图像中的人脸,并使用三种不同的人脸识别模型对检测到的人脸进行识别。然后,根据识别结果,在图像中绘制相应的标签(人名)以表示识别的结果。在绘制人脸标签时,使用了putText函数将标签绘制在原始图像上。 项目成就:我们的项目评分取得了99分,并且在考核中排名第一。

项目流程示意图:

🌞二、项目分工

在项目中,我主要负责的是
  1. 项目的整体协调和管理,包括团队沟通、进度追踪、质量控制等
  2. 项目的数据采集与标注
  3. 负责客户端和服务端的使用socket通信的代码开发
  4. 人脸检测的优化:基于给定弱分类器的Bagging集成学习算法,训练出了三个模型,通过众数投票选择最终的预测结果对人脸进行预测。
  5. 项目的路演答辩

🌞三、项目难题

1. 视频过大,难以进行网络传输

摄像头视频流中的一帧图片为480 * 640 * 3 = 921600 Bytes,一秒需要传输30帧画面,即需要网络带宽 26 MB/S,如果不对图片进行二进制编码是无法进行网络传输的,因此客户端需要对视频流进行二进制编码。 由于编码后字节数不确定,因此需要对传输进行简单协议,我们的方案是在每一帧图片传输前发送本次图片的字节大小,以让服务器明确下次所需要接受的字节数。因为字节大小的位数在4到6位不等,因此确定传输6位字节大小,小于6位的字节数,在高位填充0以达到6位(即1440填充为001440),这样即保证了传输的稳定性。 经过测试,传输带宽需求理论上降低64倍,达到了实际使用需求。

2. 视频流中的数据异常,导致客户端/服务器卡死:

对大多数显式异常进行补救处理,即尽量使得服务器运行不被异常打断,如服务器当前接收到的图片格式有误,则直接跳过本次运行,直接接收下个图片数据等一系列异常处理操作。

3. 父进程无法知道子进程是否结束

为了解决僵尸进程和孤儿进程导致的问题,我们构建了set进程池+信号机制函数,当父进程收到程序终止信号或来自子进程的终止信号,能够先终止所有的子进程,释放系统资源。 项目的进程池使用set进行构建,传统的使用vector + atomic 的构建方式无法很好的解决数据冒险的问题,原因在于虽然atomic数据类型能够保证对单个元素的操作是原子化的,但是本质原因在于对vector进行的不是原子化操作,如多进程删除vector中的多个元素,很有可能导致删除的不是正确元素,假设两个进程分别删除下标为1、2的元素,如果进程先删除了下标为1的元素,那么原来下标为2的元素此时下标将变为1,这导致了删除下标2的进程删除了原本下标为3的元素。 而set的增删改查是具体针对单个元素,删除元素是通过查找到特定元素后进行删除,本质上是删除红黑树上的节点。 注意: "数据冒险"用于描述在处理数据时可能出现的问题或风险。它指的是当数据被不正确地处理、解释或使用时,可能导致不良的后果或意外的结果。这可能包括数据丢失、数据泄露、数据损坏或数据被误用的情况。数据冒险强调了数据质量管理和数据安全性的重要性,以避免可能造成的潜在风险和损失。

4. 人脸识别精度低

由于模型复杂度和数据集性能限制,本项目的预测性能无法十分优秀。机器学习中的传统特征匹配算法对复杂环境下的人脸识别无法尽如人意,但是本项目在此基础上设计了基于给定弱分类器的Bagging集成学习算法,其本质上是通过组合多个弱分类器,共同进行分类预测,通过众数投票选择出预测结果的一种算法,其效果往往比单一分类器更加优秀。

🌞四、实现细节

🌼4.1 关键程序

wkcv.link

#ifndef _WAKLOUIS_OPENCV_H_
#define _WAKLOUIS_OPENCV_H_
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/face.hpp>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sys/wait.h>
#include <atomic>
#include <unordered_set>
#include <set>
#include <fstream>
using namespace std;
using namespace cv;
#define MAX_LEN 1000000 // 最大长度定义为1000000
#define PORT_NUM 5409 // 端口号定义为5409
#define MAX_LISTEN 10 // 最大监听数定义为10
#define HANDLER_QUIT_CODE 103 // 处理器退出代码定义为103
#define IMAGE_ROWS 480 // 图像行数定义为480
#define IMAGE_COLS 640 // 图像列数定义为640
#define IMAGE_TYPE 16 // 图像类型定义为16
#define PIC_FIGURES 6 // 图片数字位数定义为6
#define PIC_MAX_BYTES 921600 // 图片最大字节数定义为921600
typedef basic_string<unsigned char> ustring; // 使用无符号字符的基本字符串类型
typedef unsigned char BYTE; // 字节类型定义为无符号字符类型
void sigquitHandler(int pid); // 定义信号处理函数
namespace wk
{
// 将整数转换为字符串并填充零
bool to_string_fill_zero(int num, BYTE *str)
{
int pos = 0;
string temp = to_string(num); // 将整数转换为字符串
if (temp.size() > PIC_FIGURES) // 如果转换后的字符串长度超过预定义的位数
{
perror("to_string_fill_zero"); // 输出错误信息
return -1; // 返回 false
}
else if (temp.size() == PIC_FIGURES) // 如果转换后的字符串长度与预定义的位数相等
{
for (auto i : temp)
{
str[pos++] = i; // 将转换后的字符串按位存储到字节数组中
}
return 0; // 返回 true
}
else // 如果转换后的字符串长度小于预定义的位数
{
int res = PIC_FIGURES - temp.size(); // 计算需要填充的零的数量
for (int i = 0; i < res; i++)
{
str[pos++] = '0'; // 填充零
}
for (auto i : temp)
{
str[pos++] = i; // 将转换后的字符串按位存储到字节数组中
}
return 0; // 返回 true
}
}
// 将字节数组解析为整数
int to_integer(BYTE *str)
{
int num = 0;
for (int i = 0; i < PIC_FIGURES; i++)
{
int temp = str[i] - '0'; // 将字符转换为数字
num = num * 10 + temp; // 计算整数值
}
return num; // 返回解析后的整数值
}
// 将字符串解析为整数
int to_integer_model(string str)
{
int num = 0;
for (int i = 0; i < str.size(); i++)
{
int temp = str[i] - '0'; // 将字符转换为数字
num = num * 10 + temp; // 计算整数值
}
return num; // 返回解析后的整数值
}
}
#endif

客户端client.cpp

#include "wkcv.link" // 包含自定义的头文件 "wkcv.link"
struct sockaddr_in server_addr, client_addr; // 定义服务器和客户端地址结构体变量
int client_sockfd, returnValue; // 客户端套接字文件描述符和返回值变量
int main(int argc, char *argv[]) // 主函数,接受命令行参数
{
if (argc != 2) // 如果参数数量不为2
{
cout << "Format : ./client [Server ip]" << endl; // 输出正确的程序使用格式
exit(-1); // 退出程序
}
// 创建套接字
client_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
if (client_sockfd < 0) // 如果创建套接字失败
{
perror("Socket"); // 输出错误信息
exit(-1); // 退出程序
}
// 填充服务器信息
string ipAddress = argv[1]; // 获取服务器IP地址
bzero(&server_addr, sizeof(server_addr)); // 清零服务器地址结构体变量
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_port = PORT_NUM; // 设置端口号为预定义常量值
server_addr.sin_addr.s_addr = inet_addr((char *)ipAddress.data()); // 将IP地址转换为网络字节序,并赋值给服务器地址结构体变量
// 服务器连接
returnValue = connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 连接到服务器
if (returnValue < 0) // 如果连接失败
{
perror("Connect"); // 输出错误信息
exit(-1); // 退出程序
}
cout << "Connection Success to " << ipAddress << endl; // 打印连接成功的消息
VideoCapture capture(0); // 打开摄像头,初始化摄像头捕获对象
Mat image; // 定义Mat类型的图像对象
vector<int> quality; // 定义保存图像压缩质量的向量
quality.push_back(IMWRITE_JPEG_QUALITY); // 设置图像压缩参数
quality.push_back(50); // 设置图像压缩质量为50
vector<BYTE> data_encode; // 定义保存编码后图像数据的向量
BYTE nextImageSize_s[PIC_FIGURES]; // 定义保存下一张图像大小的字节数组
while (1) // 进入主循环
{
data_encode.clear(); // 清空编码后图像数据的向量
memset(nextImageSize_s, '\0', sizeof(nextImageSize_s)); // 将下一张图像大小的字节数组清零
capture >> image; // 获取摄像头捕获的图像
if (image.empty() || image.data == NULL) // 如果图像为空
{
continue; // 跳过当前循环,继续下一次循环
}
imencode(".jpeg", image, data_encode, quality); // 将图像编码为JPEG格式,并存储到data_encode中
int nSize = data_encode.size(); // 获取编码后图像数据的大小
wk::to_string_fill_zero(nSize, nextImageSize_s); // 将图像数据大小转换为字符串并填充零,存储到nextImageSize_s数组中
write(client_sockfd, nextImageSize_s, PIC_FIGURES); // 将下一张图像的大小发送到服务器
BYTE *encodeImg = new BYTE[nSize]; // 动态分配内存,用于保存编码后的图像数据
for (int i = 0; i < nSize; i++) // 遍历编码后的图像数据
{
encodeImg[i] = data_encode[i]; // 将编码后的图像数据存储到encodeImg数组中
}
int count = write(client_sockfd, encodeImg, nSize); // 将编码后的图像数据发送到服务器
cout << "sent " << count << endl; // 打印发送的字节数
flip(image, image, 1); // 翻转图像,使其显示在窗口中
imshow("client", image); // 显示图像到窗口中
if (waitKey(30) > 0) // 等待按键输入,若检测到按键输入
{
break; // 跳出循环
}
usleep(33333); // 等待一段时间
}
close(client_sockfd); // 关闭套接字
return 0; // 退出程序
}

服务端server.cpp

#include "wkcv.link"
struct sockaddr_in server_addr, client_addr;
char buffer_[MAX_LEN]{0};
int server_sockfd, client_commfd, returnValue;
set<pid_t> childLists;
// 显示的标签
string name[] = {"LiYuan", "liuZhiCong", "HuangYiFeng", "LeiKunRu",
"LinJingYang", "TanXin", "ZhangGuanYu", "ZhaoYuQiu", "XieDunJie",
"FangChengTao", "LiXueZhi", "XiaXuan", "WuWenFeng", "LiuJunFeng",
"LiXingHai", "ZhangZhenZhou", "ChenDaLi", "YaoYiJie", "ZhangYueYang",
"ZhangBeiJing", "HaoJingNa", "WuKe", "YangFeiXiang", "LiuBao", "YangJiaMing",
"ZhangSuJun"};
int main()
{
// 加载人脸识别模型
Ptr<face::LBPHFaceRecognizer> modelLBPH = face::LBPHFaceRecognizer::create();
modelLBPH->read("../../model/save/MyFaceLBPHModel.xml");
Ptr<face::FisherFaceRecognizer> modelFisher = face::FisherFaceRecognizer::create();
modelFisher->read("../../model/save/MyFaceFisherModel.xml");
Ptr<face::FaceRecognizer> modelPCA = face::EigenFaceRecognizer::create();
modelPCA->read("../../model/save/MyFacePCAModel.xml");
// 创建套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sockfd < 0)
{
perror("Socket");
return -1;
}
// 填充服务器地址信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = PORT_NUM;
server_addr.sin_addr.s_addr = INADDR_ANY;
// 填充
bzero(&server_addr.sin_zero, sizeof(server_addr.sin_zero));
// 设置套接字选项避免地址使用错误
int on = 1;
if ((setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
{
perror("setsockopt failed");
return -1;
}
// 绑定
returnValue = bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (returnValue < 0)
{
perror("Bind");
exit(-1);
}
// 侦听
returnValue = listen(server_sockfd, MAX_LISTEN);
if (returnValue < 0)
{
perror("Listen");
exit(-1);
}
int connectionNum = 0;
// 使用并发服务器模型,始终准备接收客户端连接请求
while (1)
{
// 输出等待连接的消息及连接次数
cout << "Waiting Connection " << ++connectionNum << " ... " << endl;
// 等待接受客户端发来的连接请求
unsigned int len = sizeof(client_addr);
client_commfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len);
if (client_commfd < 0) // 如果接受连接失败
{
perror("Accept"); // 输出错误信息
continue; // 继续等待下一个连接请求
}
// 输出与客户端连接成功的消息及客户端IP地址
cout << "Get connection with : " << inet_ntoa(client_addr.sin_addr) << endl;
// 设置信号处理函数
signal(SIGQUIT, sigquitHandler);
// 创建子进程处理客户端请求
pid_t son = fork();
if (son < 0) // 如果创建子进程失败
{
perror("Fork"); // 输出错误信息
sigquitHandler(0); // 调用信号处理函数
exit(1); // 退出程序
}
childLists.insert(son); // 将子进程加入进程池
if (son > 0) // 如果是父进程
{
continue; // 继续监听新的连接
}
// 子进程继续执行以下代码
BYTE buffer_[PIC_MAX_BYTES]; // 定义存储图像数据的缓冲区
BYTE nextImageSize_s[PIC_FIGURES]; // 定义存储下一张图像大小的缓冲区
ustring full_buffer_; // 定义存储完整图像数据的字符串
vector<BYTE> image_s_encoded; // 定义存储编码后图像数据的向量
int exitFlag = 0, count, nextImageSize; // 定义退出标志、读取字节数、下一张图像大小等变量
// 人脸检测部分变量初始化
CascadeClassifier cascade; // 创建级联分类器对象
cascade.load("../../model/save/haarcascade_frontalface_default.xml"); // 加载人脸检测模型
vector<Rect> faces; // 定义存储检测到的人脸矩形区域的向量
// 人脸识别部分,加载预训练的人脸识别模型
// 循环接收客户端发送的图像数据并处理
while (1)
{
// 清空数据
image_s_encoded.clear(); // 清空编码后图像数据向量
memset(buffer_, '\0', sizeof(buffer_)); // 清空图像数据缓冲区
memset(nextImageSize_s, 0, sizeof(nextImageSize_s)); // 清空下一张图像大小缓冲区
full_buffer_.clear(); // 清空完整图像数据字符串
// 读取下一张图像大小信息
read(client_commfd, nextImageSize_s, PIC_FIGURES);
nextImageSize = wk::to_integer(nextImageSize_s); // 将缓冲区转换为整数,表示图像大小
int received = 0;
// 循环读取图像数据,直到接收完整
while (1)
{
count = read(client_commfd, buffer_, nextImageSize - received); // 读取图像数据
if (count < 0) // 如果读取失败
{
break; // 跳出循环
}
for (int i = received; i < received + count; i++)
{
full_buffer_[i] = buffer_[i - received]; // 将数据存入完整图像数据字符串中
}
received += count; // 更新已接收的数据量
full_buffer_[received] = '\0'; // 在字符串末尾添加结束符
if (received == nextImageSize) // 如果接收完整
{
break; // 跳出循环
}
}
// 如果累计100帧没有输入信号,则中断该进程
if (count == -1 || count == 0)
{
exitFlag++; // 增加退出标志
if (exitFlag == 100) // 如果累计到100帧
{
destroyWindow(to_string(getpid())); // 销毁窗口
cout << getpid() << " Client loss, exiting" << endl; // 输出客户端丢失连接信息
close(client_commfd); // 关闭客户端连接
break; // 跳出循环,结束子进程
}
continue; // 继续下一次循环
}
else // 如果接收到数据
{
exitFlag = 0; // 重置退出标志
}
// 将图像数据存入向量
int temp = 0;
while (temp < nextImageSize)
{
image_s_encoded.push_back(full_buffer_[temp++]); // 存入图像数据向量
}
// 解码图像数据
Mat imageColor = imdecode(image_s_encoded, IMREAD_COLOR); // 解码为彩色图像
if (imageColor.data == NULL) // 如果解码失败
{
continue; // 继续下一次循环
}
Mat image;
cvtColor(imageColor, image, COLOR_BGR2GRAY); // 转换为灰度图像
// 人脸检测
flip(imageColor, imageColor, 1); // 图像翻转
flip(image, image, 1);
faces.clear(); // 清空人脸矩形区域向量
cascade.detectMultiScale(image, faces, 1.1, 20, 0, Size(70, 70)); // 检测人脸矩形区域
// 遍历检测到的人脸
for (int i = 0; i < faces.size(); i++)
{
// 如果人脸区域大小不合适,则跳过
if (faces[i].width <= 0 || faces[i].height <= 0 || faces[i].x + faces[i].width > 640 || faces[i].y + faces[i].height > 480)
{
perror("Size"); // 输出错误信息
continue; // 继续下一次循环
}
RNG rng(i); // 随机数生成器
Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), 20); // 随机颜色
// 在图像中绘制人脸矩形区域
rectangle(imageColor, faces[i], color, 2, 8, 0);
// 截取人脸区域并调整大小
Mat part = image(faces[i]);
Size dsize = Size(92, 112);
resize(part, part, dsize, 0, 0, INTER_AREA);
// 使用三种不同的人脸识别模型进行预测
int label1, label2, label3;
double confidence1, confidence2, confidence3;
modelLBPH->predict(part, label1, confidence1); // LBPH算法预测
modelFisher->predict(part, label2, confidence2); // Fisher算法预测
modelPCA->predict(part, label3, confidence3); // PCA算法预测
// 根据预测结果绘制标签到图像中
if (label1 == label2 || label1 == label3)
{
putText(imageColor, name[label1], Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出姓名
}
else if (label2 == label3)
{
putText(imageColor, name[label2], Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出姓名
}
else
{
putText(imageColor, "Unidentified", Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出未识别信息
}
}
// 在窗口中显示图像
imshow(to_string(getpid()), imageColor);
if (waitKey(17) > 0) // 等待按键输入
{
break; // 跳出循环,结束子进程
}
}
}
// 关闭客户端和服务器套接字
close(client_commfd);
close(server_sockfd);
return 0;
}
// 信号处理函数,用于结束子进程
void sigquitHandler(int pid)
{
// 循环遍历子进程列表
for (auto i : childLists)
{
cout << i << " Exiting" << endl; // 输出子进程退出信息
kill(i, SIGTERM); // 向子进程发送终止信号
}
pid_t child_pid;
while ((child_pid = wait(nullptr)) > 0) // 等待所有子进程退出
;
_exit(HANDLER_QUIT_CODE); // 退出信号处理函数
}
🌼4.2 运行结果

测试成员一出现在摄像头面前,显示成员一的姓名标签:

测试成员二出现在摄像头面前,显示成员二的姓名标签:

测试成员三出现在摄像头面前,显示成员三的姓名标签:

🌞五、程序分析

🌷5.1 wkcv.link

wkcv.link是一个C++头文件,定义了一些常量、类型和函数。让我们详细分析一下:

1. 包含头文件:opencv、socket

#ifndef _WAKLOUIS_OPENCV_H_
#define _WAKLOUIS_OPENCV_H_
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/face.hpp>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <sys/wait.h>
#include <atomic>
#include <unordered_set>
#include <set>
#include <fstream>
包含一些标准的C++和OpenCV的头文件,还有一些与网络通信相关的头文件。

2. 定义命名空间wk、std+常量

using namespace std;
using namespace cv;
#define MAX_LEN 1000000 // 最大长度定义为1000000
#define PORT_NUM 5409 // 端口号定义为5409
#define MAX_LISTEN 10 // 最大监听数定义为10
#define HANDLER_QUIT_CODE 103 // 处理器退出代码定义为103
#define IMAGE_ROWS 480 // 图像行数定义为480
#define IMAGE_COLS 640 // 图像列数定义为640
#define IMAGE_TYPE 16 // 图像类型定义为16
#define PIC_FIGURES 6 // 图片数字位数定义为6
#define PIC_MAX_BYTES 921600 // 图片最大字节数定义为921600
typedef basic_string<unsigned char> ustring; // 使用无符号字符的基本字符串类型
typedef unsigned char BYTE; // 字节类型定义为无符号字符类型
void sigquitHandler(int pid); // 定义信号处理函数
定义一些常量:包括最大长度 MAX_LEN, 端口号 PORT_NUM, 最大监听数 MAX_LISTEN, 处理器退出代码 HANDLER_QUIT_CODE, 图像行数 IMAGE_ROWS, 图像列数 IMAGE_COLS, 图像类型 IMAGE_TYPE, 图片数字位数 PIC_FIGURES, 以及图片最大字节数 PIC_MAX_BYTES

3. 命名空间wk定义了三个函数

namespace wk
{
// 将整数转换为字符串并填充零
bool to_string_fill_zero(int num, BYTE *str)
{
int pos = 0;
string temp = to_string(num); // 将整数转换为字符串
if (temp.size() > PIC_FIGURES) // 如果转换后的字符串长度超过预定义的位数
{
perror("to_string_fill_zero"); // 输出错误信息
return -1; // 返回 false
}
else if (temp.size() == PIC_FIGURES) // 如果转换后的字符串长度与预定义的位数相等
{
for (auto i : temp)
{
str[pos++] = i; // 将转换后的字符串按位存储到字节数组中
}
return 0; // 返回 true
}
else // 如果转换后的字符串长度小于预定义的位数
{
int res = PIC_FIGURES - temp.size(); // 计算需要填充的零的数量
for (int i = 0; i < res; i++)
{
str[pos++] = '0'; // 填充零
}
for (auto i : temp)
{
str[pos++] = i; // 将转换后的字符串按位存储到字节数组中
}
return 0; // 返回 true
}
}
// 将字节数组解析为整数
int to_integer(BYTE *str)
{
int num = 0;
for (int i = 0; i < PIC_FIGURES; i++)
{
int temp = str[i] - '0'; // 将字符转换为数字
num = num * 10 + temp; // 计算整数值
}
return num; // 返回解析后的整数值
}
// 将字符串解析为整数
int to_integer_model(string str)
{
int num = 0;
for (int i = 0; i < str.size(); i++)
{
int temp = str[i] - '0'; // 将字符转换为数字
num = num * 10 + temp; // 计算整数值
}
return num; // 返回解析后的整数值
}
}
#endif
在命名空间 wk 中定义了几个函数 bool to_string_fill_zero(int num, BYTE *str) 这段函数的作用是将整数转换为字符串并存在字节数组中,并根据预定义的位数填充零。具体步骤如下:
  1. 首先将整数转换为字符串。
  2. 如果转换后的字符串长度超过预定义的位数 PIC_FIGURES,则输出错误信息并返回 false。
  3. 如果转换后的字符串长度与预定义的位数相等,则将转换后的字符串按位存储到字节数组中,并返回 true。
  4. 如果转换后的字符串长度小于预定义的位数,则计算需要填充的零的数量,并在字节数组中填充零,然后将转换后的字符串按位存储到字节数组中,并返回 true。

int to_integer(BYTE *str) 这段程序的作用是将字节数组解析为一个整数。具体步骤如下:

  1. 初始化一个整数 num 为 0。
  2. 使用一个循环遍历字节数组 str 的前 PIC_FIGURES 个元素。
  3. 将每个字符减去字符 '0' 的 ASCII 值,将其转换为对应的数字。
  4. 根据位置权重,将每个数字乘以 10 的相应次方并加到 num 上,得到最终的整数值。
  5. 返回解析后的整数值。

int to_integer_model(string str) 这段程序的作用是将一个字符串解析为一个整数。具体步骤如下:

  1. 初始化一个整数 num 为 0。
  2. 使用一个循环遍历字符串 str 的每个字符。
  3. 将每个字符减去字符 '0' 的 ASCII 值,将其转换为对应的数字。
  4. 根据位置权重,将每个数字乘以 10 的相应次方并加到 num 上,得到最终的整数值。
  5. 返回解析后的整数值。
🌷5.2 客户端client.cpp

client.cpp是一个客户端程序,用于与服务器进行通讯。让我们分步来看:

1. 命令行参数检查

if (argc != 2) // 如果参数数量不为2
{
cout << "Format : ./client [Server ip]" << endl; // 输出正确的程序使用格式
exit(-1); // 退出程序
}
这段代码是在程序开始时对命令行参数进行检查。程序预期接收两个参数:服务端的IP地址和端口号。argc表示命令行参数的数量,argv是一个指向参数数组的指针。 argc != 2:检查参数数量是否等于2,如果不等于2,说明用户没有提供正确的参数数量。 这里执行客户端命令用的是./client 2003。参数分别是:
  • ./client 2003:表示程序名称。
  • 2003:表示服务端的通讯端口。

2.创建客户端socket

// 创建套接字
client_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
if (client_sockfd < 0) // 如果创建套接字失败
{
perror("Socket"); // 输出错误信息
exit(-1); // 退出程序
}
这段程序的作用是创建客户端的套接字(socket),并进行创建的错误检查。程序分析:
  • int sockfd = socket(AF_INET, SOCK_STREAM, 0); 这行代码创建了一个套接字,其中: AF_INET 指定了套接字的地址族为IPv4。 SOCK_STREAM 指定了套接字的类型为流式套接字,即TCP套接字。 0 表示使用默认的协议。
  • if (sockfd < -1) 这个条件判断检查套接字是否创建成功。如果套接字创建失败,socket() 函数返回 -1,程序通过 perror("socket") 输出相关错误信息,然后返回 -1 表示程序执行失败。

3. 将服务端发送连接请求

// 向服务器发起连接请求
string ipAddress = argv[1]; // 获取服务器IP地址
bzero(&server_addr, sizeof(server_addr)); // 清零服务器地址结构体变量
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_port = PORT_NUM; // 设置端口号为预定义常量值
server_addr.sin_addr.s_addr = inet_addr((char *)ipAddress.data()); // 将IP地址转换为网络字节序,并赋值给服务器地址结构体变量
returnValue = connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 连接到服务器
if (returnValue < 0) // 如果连接失败
{
perror("Connect"); // 输出错误信息
exit(-1); // 退出程序
}
cout << "Connection Success to " << ipAddress << endl; // 打印连接成功的消息
这段代码的作用是向服务器发起连接请求,并在连接成功或失败时进行相应的处理和输出。具体来说:
  1. 从命令行参数中获取服务器的 IP 地址,该 IP 地址作为连接目标。
  2. 使用 bzero() 函数清零了一个用于存储服务器地址信息的结构体变量 server_addr,以确保其所有字段都是零。
  3. 设置了 server_addr 结构体的成员:
  • sin_family 设置为 AF_INET,表示使用 IPv4 地址族。
  • sin_port 设置为预定义的端口号常量 PORT_NUM,表示连接的目标端口。
  • sin_addr.s_addr 使用 inet_addr() 将 IP 地址转换为网络字节序,并将结果赋值给 server_addr 结构体的 sin_addr 成员。
  1. 调用 connect() 函数,向服务器发起连接请求,参数包括客户端套接字描述符 client_sockfd,指向 server_addr 结构体的指针,以及结构体的大小。
  2. 检查 connect() 的返回值,如果返回值小于 0,说明连接失败,使用 perror() 输出错误信息,然后调用 exit() 退出程序。
  3. 如果连接成功,使用 cout 输出连接成功的消息,其中包括连接的目标 IP 地址。

4. 打开默认摄像头

//捕获摄像头图像
VideoCapture capture(0); // 打开摄像头,初始化摄像头捕获对象
Mat image; // 定义Mat类型的图像对象
vector<int> quality; // 定义保存图像压缩质量的向量
quality.push_back(IMWRITE_JPEG_QUALITY); // 设置图像压缩参数
quality.push_back(50); // 设置图像压缩质量为50
vector<BYTE> data_encode; // 定义保存编码后图像数据的向量
BYTE nextImageSize_s[PIC_FIGURES]; // 定义保存下一张图像大小的字节数组
这段程序的作用是捕获摄像头图像。具体步骤如下:
  1. 使用 VideoCapture 类打开摄像头,初始化摄像头捕获对象 capture
  2. 定义 Mat 类型的图像对象 image,用于存储捕获到的图像。
  3. 定义一个 vector<int> 类型的向量 quality,用于保存图像压缩质量参数。
  4. 设置图像压缩参数,将压缩质量设置为50,并将其存入 quality 向量中。
  5. 定义一个 vector<BYTE> 类型的向量 data_encode,用于保存编码后的图像数据。
  6. 定义一个字节数组 nextImageSize_s,用于保存下一张图像大小的信息。

5. 编码的视频流传输

while (1) // 进入主循环
{
data_encode.clear(); // 清空编码后图像数据的向量
memset(nextImageSize_s, '\0', sizeof(nextImageSize_s)); // 将下一张图像大小的字节数组清零
capture >> image; // 获取摄像头捕获的图像
if (image.empty() || image.data == NULL) // 如果图像为空
{
continue; // 跳过当前循环,继续下一次循环
}
imencode(".jpeg", image, data_encode, quality); // 将图像编码为JPEG格式,并存储到data_encode中
int nSize = data_encode.size(); // 获取编码后图像数据的大小
wk::to_string_fill_zero(nSize, nextImageSize_s); // 将图像数据大小转换为字符串并填充零,存储到nextImageSize_s数组中
write(client_sockfd, nextImageSize_s, PIC_FIGURES); // 将下一张图像的大小发送到服务器
BYTE *encodeImg = new BYTE[nSize]; // 动态分配内存,用于保存编码后的图像数据
for (int i = 0; i < nSize; i++) // 遍历编码后的图像数据
{
encodeImg[i] = data_encode[i]; // 将编码后的图像数据存储到encodeImg数组中
}
int count = write(client_sockfd, encodeImg, nSize); // 将编码后的图像数据发送到服务器
cout << "sent " << count << endl; // 打印发送的字节数
flip(image, image, 1); // 翻转图像,使其显示在窗口中
imshow("client", image); // 显示图像到窗口中
if (waitKey(30) > 0) // 等待按键输入,若检测到按键输入
{
break; // 跳出循环
}
usleep(33333); // 等待一段时间
}
这段程序的作用是在一个无限循环中捕获摄像头图像,将图像编码为JPEG格式,并将编码后的图像数据发送到服务器。具体步骤如下: 在一个无限循环中,不断执行以下操作:
  • 清空编码后图像数据的向量 data_encode
  • 将下一张图像大小的字节数组 nextImageSize_s 清零。
  • 使用 capture >> image 获取摄像头捕获的图像。
  • 如果图像为空或者图像数据为空,则跳过当前循环,继续下一次循环。
  • 使用 imencode() 函数将图像编码为JPEG格式,并将编码后的图像数据存储到 data_encode 向量中。
  • 获取编码后图像数据的大小,并将其转换为字符串并填充零,存储到 nextImageSize_s 数组中。
  • 使用 write() 函数将下一张图像的大小发送到服务器。
  • 动态分配内存,用于保存编码后的图像数据,并将编码后的图像数据发送到服务器。
  • 打印发送的字节数。
  • 翻转图像,以便在窗口中正常显示。
  • 显示图像到名为 "client" 的窗口中。
  • 使用 waitKey() 函数等待按键输入,如果检测到按键输入,则跳出循环。
  • 使用 usleep() 函数等待一段时间,以控制图像发送的频率。

注意:这段代码中的窗口是由 OpenCV 库提供的功能创建的。使用了 imshow() 函数来显示图像在一个名为 "client" 的窗口中,而这个窗口是由 OpenCV 提供的图像显示功能创建的。

6.关闭socket

//关闭连接
close(client_sockfd); // 关闭套接字
close()函数用于关闭客户端套接字,释放资源。
🌷5.3 服务端server.cpp

1. 手写标签

//显示的标签
string name[] = {"LiYuan", "liuZhiCong", "HuangYiFeng", "LeiKunRu",
"LinJingYang", "TanXin", "ZhangGuanYu", "ZhaoYuQiu", "XieDunJie",
"FangChengTao", "LiXueZhi", "XiaXuan", "WuWenFeng", "LiuJunFeng",
"LiXingHai", "ZhangZhenZhou", "ChenDaLi", "YaoYiJie", "ZhangYueYang",
"ZhangBeiJing", "HaoJingNa", "WuKe", "YangFeiXiang", "LiuBao", "YangJiaMing",
"ZhangSuJun"};

2. 加载人脸识别模型

// 加载人脸识别模型
Ptr<face::LBPHFaceRecognizer> modelLBPH = face::LBPHFaceRecognizer::create();
modelLBPH->read("../../model/save/MyFaceLBPHModel.xml");
Ptr<face::FisherFaceRecognizer> modelFisher = face::FisherFaceRecognizer::create();
modelFisher->read("../../model/save/MyFaceFisherModel.xml");
Ptr<face::FaceRecognizer> modelPCA = face::EigenFaceRecognizer::create();
modelPCA->read("../../model/save/MyFacePCAModel.xml");
它使用了 OpenCV 的人脸识别模块中的三种不同的识别器:LBPH、Fisher、 PCA。这些模型在之前通过训练得到,并保存在 XML 文件中。 通过 read() 方法,这些模型从 XML 文件中加载到程序中,以便后续在图像上进行人脸识别。

1. 创建服务端的socket

// 创建套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sockfd < 0)
{
perror("Socket");
return -1;
}
这段代码的作用是创建一个套接字,用于在服务器端监听客户端的连接请求。具体来说:
  • 使用 socket() 函数创建一个套接字,指定地址族为 IPv4(AF_INET) 类型为流式套接字(SOCK_STREAM) 协议为默认协议(0)。
  • 如果创建套接字失败(返回值小于 0),则输出错误信息并返回 -1 表示失败。

这段代码通常用于服务器端程序的初始化阶段,用于准备接受客户端的连接请求。

2.绑定IP地址和端口

// 填充服务器地址信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = PORT_NUM;
server_addr.sin_addr.s_addr = INADDR_ANY;
// 填充
bzero(&server_addr.sin_zero, sizeof(server_addr.sin_zero));
// 设置套接字选项避免地址使用错误
int on = 1;
if ((setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
{
perror("setsockopt failed");
return -1;
}
// 绑定
returnValue = bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (returnValue < 0)
{
perror("Bind");
exit(-1);
}
这段程序的作用是配置服务器套接字的地址信息,并将套接字与特定的网络地址和端口号绑定在一起,以便服务器能够接受客户端的连接请求。具体来说:
  • 通过 server_addr.sin_family = AF_INET; 设置地址族为 IPv4。
  • 通过 server_addr.sin_port = PORT_NUM; 设置端口号为预定义的常量 PORT_NUM
  • 通过 server_addr.sin_addr.s_addr = INADDR_ANY; 设置 IP 地址为服务器的任意可用地址。
  • 通过 bzero(&server_addr.sin_zero, sizeof(server_addr.sin_zero)); 清零结构体中未使用的部分。
  • 通过 setsockopt() 函数设置套接字选项 SO_REUSEADDR,以便在服务器重启后可以立即重用先前使用的地址和端口。
  • 最后,通过 bind() 函数将套接字绑定到指定的网络地址和端口号。如果绑定失败,程序会输出错误信息并退出。

3.设置监听状态

// 侦听
returnValue = listen(server_sockfd, MAX_LISTEN);
if (returnValue < 0)
{
perror("Listen");
exit(-1);
}
这段代码的作用是让服务器套接字开始监听连接请求,使其处于被动等待状态,以便接受客户端的连接请求。具体来说:
  • 使用 listen() 函数告诉操作系统,该套接字处于监听状态,并且可以接受来自客户端的连接请求。
  • listen() 函数的第一个参数是要监听的套接字描述符,即 server_sockfd
  • MAX_LISTEN 是一个预定义的常量,表示服务器允许排队等待处理的最大连接数。
  • 如果 listen() 函数执行失败(返回值小于 0),则输出错误信息并退出程序。

4.接受客户端连接请求

int connectionNum = 0;
// 使用并发服务器模型,始终准备接收客户端连接请求
while (1)
{
// 输出等待连接的消息及连接次数
cout << "Waiting Connection " << ++connectionNum << " ... " << endl;
// 等待接受客户端发来的连接请求
unsigned int len = sizeof(client_addr);
client_commfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len);
if (client_commfd < 0) // 如果接受连接失败
{
perror("Accept"); // 输出错误信息
continue; // 继续等待下一个连接请求
}
// 输出与客户端连接成功的消息及客户端IP地址
cout << "Get connection with : " << inet_ntoa(client_addr.sin_addr) << endl;
这段程序的作用是创建一个并发服务器模型,它始终准备接受客户端的连接请求。具体功能包括:
  1. 初始化连接计数器 connectionNum,用于记录已经建立的连接次数。
  2. 在一个无限循环中,等待客户端的连接请求。
  3. 每次循环输出等待连接的消息以及连接次数。
  4. 使用 accept 函数接受客户端的连接请求,如果连接失败,则输出错误信息并继续等待下一个连接请求。
  5. 如果连接成功,则输出与客户端连接成功的消息以及客户端的IP地址。

5. 创建一个子进程来处理客户端的请求

// 设置信号处理函数
signal(SIGQUIT, sigquitHandler);
// 创建子进程处理客户端请求
pid_t son = fork();
if (son < 0) // 如果创建子进程失败
{
perror("Fork"); // 输出错误信息
sigquitHandler(0); // 调用信号处理函数
exit(1); // 退出程序
}
childLists.insert(son); // 将子进程加入进程池
if (son > 0) // 如果是父进程
{
continue; // 继续监听新的连接
}
// 子进程继续执行以下代码
BYTE buffer_[PIC_MAX_BYTES]; // 定义存储图像数据的缓冲区
BYTE nextImageSize_s[PIC_FIGURES]; // 定义存储下一张图像大小的缓冲区
ustring full_buffer_; // 定义存储完整图像数据的字符串
vector<BYTE> image_s_encoded; // 定义存储编码后图像数据的向量
int exitFlag = 0, count, nextImageSize; // 定义退出标志、读取字节数、下一张图像大小等变量
这段程序的作用是创建一个子进程来处理客户端的请求。具体功能包括:
  1. 设置信号处理函数,当接收到 SIGQUIT 信号时调用 sigquitHandler 函数。
  2. 使用 fork() 函数创建子进程,如果创建失败,则输出错误信息,并调用信号处理函数,然后退出程序。
  3. 如果成功创建子进程,则将子进程的 PID 添加到进程池 childLists 中。
  4. 如果当前进程是父进程,则继续监听新的连接请求。
  5. 如果当前进程是子进程,则执行子进程处理的代码段,该代码段负责处理客户端请求。

6. 接受数据+人脸识别

// 人脸识别部分,加载预训练的人脸识别模型
// 人脸检测部分变量初始化
CascadeClassifier cascade; // 创建级联分类器对象
cascade.load("../../model/save/haarcascade_frontalface_default.xml"); // 加载人脸检测模型
vector<Rect> faces; // 定义存储检测到的人脸矩形区域的向量
// 循环接收客户端发送的图像数据并处理
while (1)
{
// 清空数据
image_s_encoded.clear(); // 清空编码后图像数据向量
memset(buffer_, '\0', sizeof(buffer_)); // 清空图像数据缓冲区
memset(nextImageSize_s, 0, sizeof(nextImageSize_s)); // 清空下一张图像大小缓冲区
full_buffer_.clear(); // 清空完整图像数据字符串
// 读取下一张图像大小信息
read(client_commfd, nextImageSize_s, PIC_FIGURES);
nextImageSize = wk::to_integer(nextImageSize_s); // 将缓冲区转换为整数,表示图像大小
int received = 0;
// 循环读取图像数据,直到接收完整
while (1)
{
count = read(client_commfd, buffer_, nextImageSize - received); // 读取图像数据
if (count < 0) // 如果读取失败
{
break; // 跳出循环
}
for (int i = received; i < received + count; i++)
{
full_buffer_[i] = buffer_[i - received]; // 将数据存入完整图像数据字符串中
}
received += count; // 更新已接收的数据量
full_buffer_[received] = '\0'; // 在字符串末尾添加结束符
if (received == nextImageSize) // 如果接收完整
{
break; // 跳出循环
}
}
// 如果累计100帧没有输入信号,则中断该进程
if (count == -1 || count == 0)
{
exitFlag++; // 增加退出标志
if (exitFlag == 100) // 如果累计到100帧
{
destroyWindow(to_string(getpid())); // 销毁窗口
cout << getpid() << " Client loss, exiting" << endl; // 输出客户端丢失连接信息
close(client_commfd); // 关闭客户端连接
break; // 跳出循环,结束子进程
}
continue; // 继续下一次循环
}
else // 如果接收到数据
{
exitFlag = 0; // 重置退出标志
}
// 将图像数据存入向量
int temp = 0;
while (temp < nextImageSize)
{
image_s_encoded.push_back(full_buffer_[temp++]); // 存入图像数据向量
}
// 解码图像数据
Mat imageColor = imdecode(image_s_encoded, IMREAD_COLOR); // 解码为彩色图像
if (imageColor.data == NULL) // 如果解码失败
{
continue; // 继续下一次循环
}
Mat image;
cvtColor(imageColor, image, COLOR_BGR2GRAY); // 转换为灰度图像
// 人脸检测
flip(imageColor, imageColor, 1); // 图像翻转
flip(image, image, 1);
faces.clear(); // 清空人脸矩形区域向量
cascade.detectMultiScale(image, faces, 1.1, 20, 0, Size(70, 70)); // 检测人脸矩形区域
// 遍历检测到的人脸
for (int i = 0; i < faces.size(); i++)
{
// 如果人脸区域大小不合适,则跳过
if (faces[i].width <= 0 || faces[i].height <= 0 || faces[i].x + faces[i].width > 640 || faces[i].y + faces[i].height > 480)
{
perror("Size"); // 输出错误信息
continue; // 继续下一次循环
}
RNG rng(i); // 随机数生成器
Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), 20); // 随机颜色
// 在图像中绘制人脸矩形区域
rectangle(imageColor, faces[i], color, 2, 8, 0);
// 截取人脸区域并调整大小
Mat part = image(faces[i]);
Size dsize = Size(92, 112);
resize(part, part, dsize, 0, 0, INTER_AREA);
// 使用三种不同的人脸识别模型进行预测
int label1, label2, label3;
double confidence1, confidence2, confidence3;
modelLBPH->predict(part, label1, confidence1); // LBPH算法预测
modelFisher->predict(part, label2, confidence2); // Fisher算法预测
modelPCA->predict(part, label3, confidence3); // PCA算法预测
// 根据预测结果绘制标签到图像中
if (label1 == label2 || label1 == label3)
{
putText(imageColor, name[label1], Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出姓名
}
else if (label2 == label3)
{
putText(imageColor, name[label2], Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出姓名
}
else
{
putText(imageColor, "Unidentified", Point(faces[i].x + faces[i].width / 2, faces[i].y + faces[i].height), cv::FONT_HERSHEY_TRIPLEX, 1, color); // 输出未识别信息
}
}
// 在窗口中显示图像
imshow(to_string(getpid()), imageColor);
if (waitKey(17) > 0) // 等待按键输入
{
break; // 跳出循环,结束子进程
}
}
}
这段代码的作用是:
  1. 加载预训练的人脸检测模型,创建级联分类器对象 CascadeClassifier,用于检测图像中的人脸。
  2. 循环接收客户端发送的图像数据,并处理每一帧图像。
  3. 清空相关数据,准备接收下一张图像的数据。
  4. 读取客户端发送的下一张图像大小信息。
  5. 循环读取图像数据,直到接收完整一张图像。
  6. 如果累计100帧没有接收到图像数据,则中断该进程。
  7. 将接收到的图像数据存入向量,并解码为彩色图像。
  8. 进行人脸检测,检测图像中的人脸矩形区域。
  9. 遍历检测到的人脸,对每个人脸区域进行处理:
  • 绘制人脸矩形区域在彩色图像中。
  • 截取人脸区域并调整大小,以便进行人脸识别。
  • 使用三种不同的人脸识别模型进行预测。
  • 根据预测结果在图像中绘制标签,显示人脸的姓名或未识别信息。
  1. 在窗口中显示处理后的图像,并等待按键输入。
  2. 如果接收到按键输入,则跳出循环,结束子进程。

对于这段函数

// 信号处理函数,用于处理退出信号
void sigquitHandler(int pid)
{
// 循环遍历子进程列表
for (auto i : childLists)
{
cout << i << " Exiting" << endl; // 输出子进程退出信息
kill(i, SIGTERM); // 向子进程发送终止信号
}
pid_t child_pid;
while ((child_pid = wait(nullptr)) > 0) // 等待所有子进程退出
;
_exit(HANDLER_QUIT_CODE); // 退出信号处理函数
}
这个函数的作用是处理退出信号。具体来说:
  1. 它在接收到退出信号时,会向所有子进程发送终止信号 SIGTERM,要求它们正常退出。
  2. 然后,等待所有子进程都退出完成。
  3. 最后,函数本身退出,使用预定义的退出码 HANDLER_QUIT_CODE

总的来说,这个函数确保了在接收到退出信号时,所有子进程都能够被正确地终止,并等待它们退出完成后再退出。

7.关闭socket,释放资源

// 关闭客户端和服务器套接字
close(client_commfd);
close(server_sockfd);
这段代码的作用是关闭套接字并释放相关资源
  • close(listenfd); 关闭服务端用于监听客户端连接请求的套接字 listenfd。一旦服务端不再需要监听新的连接请求,可以关闭这个套接字,以释放相关资源并告知操作系统不再维护该套接字的状态信息。
  • close(clientfd); 关闭客户端连接的套接字 clientfd。一旦服务端与客户端的通信结束,可以关闭这个套接字,释放相关资源,并结束与该客户端的通信。

通过关闭套接字,程序能够清理掉所占用的系统资源,并确保程序的正常结束