使用 C/C++、OpenCV 和 Libevent 构建联网人脸识别考勤系统 👨💻
本文将详细介绍如何利用 C/C++、强大的计算机视觉库 OpenCV 和高性能的网络库 libevent,从零开始构建一个完整的人脸识别考勤系统。该系统能够通过摄像头实时识别人脸,并将考勤数据通过网络发送到服务器进行记录和管理。
核心技术栈 🛠️
- C/C++: 主要的编程语言,以其高性能和对底层系统强大的控制力而著称。
- OpenCV (Open Source Computer Vision Library): 用于处理所有与计算机视觉相关的任务,包括摄像头视频流的读取、人脸检测和人脸识别。
- Libevent: 一个轻量级、事件驱动的高性能网络库,用于构建我们的考勤服务器,处理客户端(即人脸识别终端)的网络请求。
- SQLite (可选): 一个轻量级的嵌入式数据库,用于在服务器端存储员工信息和考勤记录。
系统架构 🏛️
整个系统分为两个主要部分:
-
客户端 (人脸识别终端):
- 使用 OpenCV 从摄像头捕获实时视频。
- 进行人脸检测,识别人脸在视频帧中的位置。
- 提取人脸特征,并与数据库中的已知人脸进行比对。
- 识别成功后,将员工 ID 和时间戳等考勤信息发送到服务器。
-
服务器端:
- 使用 libevent 构建一个稳定高效的 TCP 服务器。
- 监听来自客户端的连接和数据请求。
- 接收客户端发送的考勤数据。
- 将考勤数据存入数据库(例如 SQLite)或文件中。
- (可选) 提供 API 接口用于查询考勤记录或管理员工信息。
<center>一个简单的系统架构示意图。</center>
步骤一:环境配置 ⚙️
在开始编码之前,你需要确保开发环境中已经安装了必要的库。
安装 OpenCV
在基于 Debian/Ubuntu 的系统上,可以使用 apt
进行安装:
bash
sudo apt-get update
sudo apt-get install build-essential cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
sudo apt-get install libopencv-dev
安装 Libevent
同样,在 Ubuntu/Debian 上:
bash
sudo apt-get install libevent-dev
安装 SQLite3 (如果使用)
bash
sudo apt-get install libsqlite3-dev
步骤二:人脸数据采集与训练
在进行识别之前,你需要一个包含员工人脸数据的数据库。
1. 人脸数据采集
你需要编写一个简单的脚本来采集每个员工的多张人脸图像。
- 启动摄像头。
- 使用 OpenCV 的 Haar 级联分类器 或 DNN 人脸检测器 来检测人脸。
- 当检测到人脸时,将其裁剪、转换为灰度图并保存为图像文件。文件名可以包含员工的 ID(例如
user.1.1.jpg
,user.1.2.jpg
...)。
关键代码片段 (data_collector.cpp):
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main() {
VideoCapture cap(0);
if (!cap.isOpened()) {
cerr << "Error: Camera not found!" << endl;
return -1;
}
CascadeClassifier face_cascade;
face_cascade.load("haarcascade_frontalface_default.xml"); // 需要下载此文件
int user_id = 1; // 示例用户ID
int count = 0;
while (count < 50) { // 采集50张图片
Mat frame;
cap >> frame;
if (frame.empty()) break;
Mat gray;
cvtColor(frame, gray, COLOR_BGR2GRAY);
vector<Rect> faces;
face_cascade.detectMultiScale(gray, faces, 1.1, 4);
for (const auto& rect : faces) {
rectangle(frame, rect, Scalar(255, 0, 0), 2);
Mat face_roi = gray(rect);
// 调整大小以统一尺寸
resize(face_roi, face_roi, Size(200, 200), 1.0, 1.0, INTER_CUBIC);
string file_name = "data/user." + to_string(user_id) + "." + to_string(count) + ".jpg";
imwrite(file_name, face_roi);
count++;
}
imshow("Face Collector", frame);
if (waitKey(30) >= 0) break;
}
return 0;
}
2. 模型训练
采集到数据后,你需要训练一个人脸识别模型。OpenCV 提供了 cv::face::LBPHFaceRecognizer
(局部二值模式直方图) 算法,它对于光照变化有很好的鲁棒性。
关键代码片段 (trainer.cpp):
cpp
#include <opencv2/opencv.hpp>
#include <opencv2/face.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace cv::face;
using namespace std;
int main() {
vector<Mat> images;
vector<int> labels;
// ... 此处添加代码以从 "data/" 目录读取所有图像和标签 ...
// 示例: labels.push_back(1); images.push_back(imread("data/user.1.0.jpg", IMREAD_GRAYSCALE));
Ptr<LBPHFaceRecognizer> model = LBPHFaceRecognizer::create();
model->train(images, labels);
model->save("attendance_model.yml"); // 保存训练好的模型
cout << "Model trained successfully!" << endl;
return 0;
}
步骤三:客户端开发 - 人脸识别与数据发送 👨💻
客户端是系统的核心交互部分。
1. 加载模型并识别人脸
程序启动时加载训练好的 .yml
模型文件。然后,实时捕获视频流,检测人脸,并使用模型进行预测。
cpp
// ... 在主循环中 ...
Ptr<LBPHFaceRecognizer> model = LBPHFaceRecognizer::create();
model->read("attendance_model.yml"); // 加载模型
// ...
cvtColor(frame, gray, COLOR_BGR2GRAY);
vector<Rect> faces;
face_cascade.detectMultiScale(gray, faces, 1.1, 4);
for (const auto& rect : faces) {
Mat face_roi = gray(rect);
resize(face_roi, face_roi, Size(200, 200));
int predicted_label = -1;
double confidence = 0.0;
model->predict(face_roi, predicted_label, confidence);
if (confidence < 70) { // 设置一个置信度阈值
string name = "User " + to_string(predicted_label);
putText(frame, name, rect.tl(), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 255, 0), 2);
// 此处触发发送考勤数据到服务器的逻辑
} else {
putText(frame, "Unknown", rect.tl(), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255), 2);
}
rectangle(frame, rect, Scalar(255, 0, 0), 2);
}
// ...
2. 连接服务器并发送数据
当识别成功后,我们需要创建一个简单的 TCP 客户端来连接 libevent 服务器并发送数据。
cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
void send_attendance_data(int user_id) {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[1024] = {0};
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080); // 服务器端口
// 将IPv4地址从文本转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return;
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return;
}
string message = "ATTENDANCE:ID=" + to_string(user_id);
send(sock, message.c_str(), message.length(), 0);
close(sock);
}
步骤四:服务器端开发 - 使用 Libevent 接收数据 🌐
服务器使用 libevent 来高效地处理来自多个客户端的并发连接。
1. 设置 Libevent 服务器
我们需要设置一个 event_base
,一个 evconnlistener
来监听新的连接,并为每个连接设置读写回调函数。
关键代码片段 (server.cpp):
cpp
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
// 读回调函数:当客户端发送数据时被调用
void read_cb(struct bufferevent *bev, void *ctx) {
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
char *data = (char*)malloc(len + 1);
evbuffer_remove(input, data, len);
data[len] = '\0';
printf("Received data: %s\n", data);
// 在这里解析数据 "ATTENDANCE:ID=1"
// 并将考勤信息写入数据库或文件
// ...
free(data);
}
// 事件回调函数:处理连接错误或 EOF
void event_cb(struct bufferevent *bev, short events, void *ctx) {
if (events & BEV_EVENT_ERROR)
perror("Error from bufferevent");
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
bufferevent_free(bev);
}
}
// 连接监听器回调:当有新连接时被调用
void listener_cb(struct evconnlistener *listener, evutil_socket_t fd,
struct sockaddr *addr, int socklen, void *ctx) {
struct event_base *base = evconnlistener_get_base(listener);
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, read_cb, NULL, event_cb, NULL);
bufferevent_enable(bev, EV_READ | EV_WRITE);
}
int main() {
struct event_base *base = event_base_new();
if (!base) {
fprintf(stderr, "Could not initialize libevent!\n");
return 1;
}
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(0); // 监听所有接口
sin.sin_port = htons(8080); // 端口
struct evconnlistener *listener = evconnlistener_new_bind(base, listener_cb, NULL,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1,
(struct sockaddr*)&sin, sizeof(sin));
if (!listener) {
perror("Couldn't create listener");
return 1;
}
printf("Server listening on port 8080...\n");
event_base_dispatch(base); // 启动事件循环
evconnlistener_free(listener);
event_base_free(base);
return 0;
}
2. 编译与运行
你需要将所有源文件链接上对应的库来编译。
bash
# 编译客户端
g++ -o attendance_client client_main.cpp -I/usr/include/opencv4 -lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_videoio -lopencv_objdetect -lopencv_face
# 编译服务器
g++ -o attendance_server server.cpp -levent
总结与展望 🚀
通过结合 OpenCV 的强大视觉处理能力和 libevent 的高性能网络功能,我们成功构建了一个功能完善且可扩展的人脸识别考勤系统。
未来可扩展的功能:
- 活体检测: 防止使用照片或视频进行欺骗。
- 更高级的人脸识别模型: 使用基于深度学习的模型(如 FaceNet, ArcFace)以获得更高的准确率。
- Web 管理后台: 开发一个 Web 界面,用于管理员查看考勤报表、管理员工信息和人脸数据。
- 客户端 GUI: 为客户端添加更友好的图形用户界面。
希望这篇教程能为你提供一个清晰的起点,祝你编码愉快!