asp+网站开发,如何用抖音做推广,上海企业注销流程,wordpress 4.9.6 漏洞#x1f31e;前言 这里我们会实现一个项目#xff1a;在linux操作系统下基于OpenCV和Socket的人脸识别系统。 目录
#x1f31e;前言
#x1f31e;一、项目介绍
#x1f31e;二、项目分工
#x1f31e;三、项目难题
#x1f31e;四、实现细节
#x1f33c;4.1 关… 前言 这里我们会实现一个项目在linux操作系统下基于OpenCV和Socket的人脸识别系统。 目录
前言
一、项目介绍
二、项目分工
三、项目难题
四、实现细节
4.1 关键程序
4.2 运行结果
五、程序分析
5.1 wkcv.link
5.2 客户端client.cpp
5.3 服务端server.cpp 一、项目介绍 项目简介我们的项目是在linux操作系统下基于OpenCV和Socket的人脸识别系统。 客户端 用于向服务器发送摄像头捕获的图像数据。 服务端 在接收客户端发送的图像数据后使用人脸检测算法检测图像中的人脸并使用三种不同的人脸识别模型对检测到的人脸进行识别。然后根据识别结果在图像中绘制相应的标签人名以表示识别的结果。在绘制人脸标签时使用了putText函数将标签绘制在原始图像上。 项目成就我们的项目评分取得了99分并且在考核中排名第一。 项目流程示意图 二、项目分工 在项目中我主要负责的是 项目的整体协调和管理包括团队沟通、进度追踪、质量控制等项目的数据采集与标注负责客户端和服务端的使用socket通信的代码开发人脸检测的优化基于给定弱分类器的Bagging集成学习算法训练出了三个模型通过众数投票选择最终的预测结果对人脸进行预测。项目的路演答辩 三、项目难题
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 fstreamusing 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 // 图片最大字节数定义为921600typedef basic_stringunsigned 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.linkstruct 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; // 设置地址族为IPv4server_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类型的图像对象vectorint quality; // 定义保存图像压缩质量的向量quality.push_back(IMWRITE_JPEG_QUALITY); // 设置图像压缩参数quality.push_back(50); // 设置图像压缩质量为50vectorBYTE 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.linkstruct sockaddr_in server_addr, client_addr;
char buffer_[MAX_LEN]{0};
int server_sockfd, client_commfd, returnValue;
setpid_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()
{// 加载人脸识别模型Ptrface::LBPHFaceRecognizer modelLBPH face::LBPHFaceRecognizer::create();modelLBPH-read(../../model/save/MyFaceLBPHModel.xml);Ptrface::FisherFaceRecognizer modelFisher face::FisherFaceRecognizer::create();modelFisher-read(../../model/save/MyFaceFisherModel.xml);Ptrface::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_; // 定义存储完整图像数据的字符串vectorBYTE image_s_encoded; // 定义存储编码后图像数据的向量int exitFlag 0, count, nextImageSize; // 定义退出标志、读取字节数、下一张图像大小等变量// 人脸检测部分变量初始化CascadeClassifier cascade; // 创建级联分类器对象cascade.load(../../model/save/haarcascade_frontalface_default.xml); // 加载人脸检测模型vectorRect 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 // 图片最大字节数定义为921600typedef basic_stringunsigned 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) 这段函数的作用是将整数转换为字符串并存在字节数组中并根据预定义的位数填充零。具体步骤如下 首先将整数转换为字符串。如果转换后的字符串长度超过预定义的位数 PIC_FIGURES则输出错误信息并返回 false。如果转换后的字符串长度与预定义的位数相等则将转换后的字符串按位存储到字节数组中并返回 true。如果转换后的字符串长度小于预定义的位数则计算需要填充的零的数量并在字节数组中填充零然后将转换后的字符串按位存储到字节数组中并返回 true。 int to_integer(BYTE *str) 这段程序的作用是将字节数组解析为一个整数。具体步骤如下 初始化一个整数 num 为 0。使用一个循环遍历字节数组 str 的前 PIC_FIGURES 个元素。将每个字符减去字符 0 的 ASCII 值将其转换为对应的数字。根据位置权重将每个数字乘以 10 的相应次方并加到 num 上得到最终的整数值。返回解析后的整数值。 int to_integer_model(string str) 这段程序的作用是将一个字符串解析为一个整数。具体步骤如下 初始化一个整数 num 为 0。使用一个循环遍历字符串 str 的每个字符。将每个字符减去字符 0 的 ASCII 值将其转换为对应的数字。根据位置权重将每个数字乘以 10 的相应次方并加到 num 上得到最终的整数值。返回解析后的整数值。 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; // 设置地址族为IPv4server_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; // 打印连接成功的消息 这段代码的作用是向服务器发起连接请求并在连接成功或失败时进行相应的处理和输出。具体来说 从命令行参数中获取服务器的 IP 地址该 IP 地址作为连接目标。使用 bzero() 函数清零了一个用于存储服务器地址信息的结构体变量 server_addr以确保其所有字段都是零。设置了 server_addr 结构体的成员 sin_family 设置为 AF_INET表示使用 IPv4 地址族。sin_port 设置为预定义的端口号常量 PORT_NUM表示连接的目标端口。sin_addr.s_addr 使用 inet_addr() 将 IP 地址转换为网络字节序并将结果赋值给 server_addr 结构体的 sin_addr 成员。调用 connect() 函数向服务器发起连接请求参数包括客户端套接字描述符 client_sockfd指向 server_addr 结构体的指针以及结构体的大小。检查 connect() 的返回值如果返回值小于 0说明连接失败使用 perror() 输出错误信息然后调用 exit() 退出程序。如果连接成功使用 cout 输出连接成功的消息其中包括连接的目标 IP 地址。 4. 打开默认摄像头 //捕获摄像头图像VideoCapture capture(0); // 打开摄像头初始化摄像头捕获对象Mat image; // 定义Mat类型的图像对象vectorint quality; // 定义保存图像压缩质量的向量quality.push_back(IMWRITE_JPEG_QUALITY); // 设置图像压缩参数quality.push_back(50); // 设置图像压缩质量为50vectorBYTE data_encode; // 定义保存编码后图像数据的向量BYTE nextImageSize_s[PIC_FIGURES]; // 定义保存下一张图像大小的字节数组 这段程序的作用是捕获摄像头图像。具体步骤如下 使用 VideoCapture 类打开摄像头初始化摄像头捕获对象 capture。定义 Mat 类型的图像对象 image用于存储捕获到的图像。定义一个 vectorint 类型的向量 quality用于保存图像压缩质量参数。设置图像压缩参数将压缩质量设置为50并将其存入 quality 向量中。定义一个 vectorBYTE 类型的向量 data_encode用于保存编码后的图像数据。定义一个字节数组 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. 加载人脸识别模型 // 加载人脸识别模型Ptrface::LBPHFaceRecognizer modelLBPH face::LBPHFaceRecognizer::create();modelLBPH-read(../../model/save/MyFaceLBPHModel.xml);Ptrface::FisherFaceRecognizer modelFisher face::FisherFaceRecognizer::create();modelFisher-read(../../model/save/MyFaceFisherModel.xml);Ptrface::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() 函数创建一个套接字指定地址族为 IPv4AF_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; 这段程序的作用是创建一个并发服务器模型它始终准备接受客户端的连接请求。具体功能包括 初始化连接计数器 connectionNum用于记录已经建立的连接次数。在一个无限循环中等待客户端的连接请求。每次循环输出等待连接的消息以及连接次数。使用 accept 函数接受客户端的连接请求如果连接失败则输出错误信息并继续等待下一个连接请求。如果连接成功则输出与客户端连接成功的消息以及客户端的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_; // 定义存储完整图像数据的字符串vectorBYTE image_s_encoded; // 定义存储编码后图像数据的向量int exitFlag 0, count, nextImageSize; // 定义退出标志、读取字节数、下一张图像大小等变量 这段程序的作用是创建一个子进程来处理客户端的请求。具体功能包括 设置信号处理函数当接收到 SIGQUIT 信号时调用 sigquitHandler 函数。使用 fork() 函数创建子进程如果创建失败则输出错误信息并调用信号处理函数然后退出程序。如果成功创建子进程则将子进程的 PID 添加到进程池 childLists 中。如果当前进程是父进程则继续监听新的连接请求。如果当前进程是子进程则执行子进程处理的代码段该代码段负责处理客户端请求。 6. 接受数据人脸识别 // 人脸识别部分加载预训练的人脸识别模型// 人脸检测部分变量初始化CascadeClassifier cascade; // 创建级联分类器对象cascade.load(../../model/save/haarcascade_frontalface_default.xml); // 加载人脸检测模型vectorRect 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; // 跳出循环结束子进程}}}这段代码的作用是 加载预训练的人脸检测模型创建级联分类器对象 CascadeClassifier用于检测图像中的人脸。循环接收客户端发送的图像数据并处理每一帧图像。清空相关数据准备接收下一张图像的数据。读取客户端发送的下一张图像大小信息。循环读取图像数据直到接收完整一张图像。如果累计100帧没有接收到图像数据则中断该进程。将接收到的图像数据存入向量并解码为彩色图像。进行人脸检测检测图像中的人脸矩形区域。遍历检测到的人脸对每个人脸区域进行处理 绘制人脸矩形区域在彩色图像中。截取人脸区域并调整大小以便进行人脸识别。使用三种不同的人脸识别模型进行预测。根据预测结果在图像中绘制标签显示人脸的姓名或未识别信息。在窗口中显示处理后的图像并等待按键输入。如果接收到按键输入则跳出循环结束子进程。 对于这段函数
// 信号处理函数用于处理退出信号
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); // 退出信号处理函数
}这个函数的作用是处理退出信号。具体来说 它在接收到退出信号时会向所有子进程发送终止信号 SIGTERM要求它们正常退出。然后等待所有子进程都退出完成。最后函数本身退出使用预定义的退出码 HANDLER_QUIT_CODE。 总的来说这个函数确保了在接收到退出信号时所有子进程都能够被正确地终止并等待它们退出完成后再退出。 7.关闭socket释放资源 // 关闭客户端和服务器套接字close(client_commfd);close(server_sockfd); 这段代码的作用是关闭套接字并释放相关资源 close(listenfd); 关闭服务端用于监听客户端连接请求的套接字 listenfd。一旦服务端不再需要监听新的连接请求可以关闭这个套接字以释放相关资源并告知操作系统不再维护该套接字的状态信息。close(clientfd); 关闭客户端连接的套接字 clientfd。一旦服务端与客户端的通信结束可以关闭这个套接字释放相关资源并结束与该客户端的通信。 通过关闭套接字程序能够清理掉所占用的系统资源并确保程序的正常结束