使用 C/C++、OpenCV 和 Libevent 构建联网人脸识别考勤系统 [特殊字符]‍[特殊字符]

使用 C/C++、OpenCV 和 Libevent 构建联网人脸识别考勤系统 👨‍💻

本文将详细介绍如何利用 C/C++、强大的计算机视觉库 OpenCV 和高性能的网络库 libevent,从零开始构建一个完整的人脸识别考勤系统。该系统能够通过摄像头实时识别人脸,并将考勤数据通过网络发送到服务器进行记录和管理。


核心技术栈 🛠️

  • C/C++: 主要的编程语言,以其高性能和对底层系统强大的控制力而著称。
  • OpenCV (Open Source Computer Vision Library): 用于处理所有与计算机视觉相关的任务,包括摄像头视频流的读取、人脸检测和人脸识别。
  • Libevent: 一个轻量级、事件驱动的高性能网络库,用于构建我们的考勤服务器,处理客户端(即人脸识别终端)的网络请求。
  • SQLite (可选): 一个轻量级的嵌入式数据库,用于在服务器端存储员工信息和考勤记录。

系统架构 🏛️

整个系统分为两个主要部分:

  1. 客户端 (人脸识别终端):

    • 使用 OpenCV 从摄像头捕获实时视频。
    • 进行人脸检测,识别人脸在视频帧中的位置。
    • 提取人脸特征,并与数据库中的已知人脸进行比对。
    • 识别成功后,将员工 ID 和时间戳等考勤信息发送到服务器。
  2. 服务器端:

    • 使用 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: 为客户端添加更友好的图形用户界面。

希望这篇教程能为你提供一个清晰的起点,祝你编码愉快!

相关推荐
Shaun_青璇20 分钟前
Cpp 知识3
开发语言·c++·算法
jndingxin29 分钟前
OpenCV CUDA模块图像变形------对图像进行上采样操作函数pyrUp()
人工智能·opencv·计算机视觉
景彡先生38 分钟前
C++中的RAII技术:资源获取即初始化
开发语言·c++
achene_ql1 小时前
手写muduo网络库(七):深入剖析 Acceptor 类
linux·服务器·开发语言·网络·c++
礼貌而已2 小时前
Vue3项目与桌面端(C++)通过Websocket 对接接口方案实现
开发语言·c++·websocket
Lenyiin2 小时前
第 87 场周赛:比较含退格的字符串、数组中的最长山脉、一手顺子、访问所有节点的最短路径
java·c++·python·leetcode·周赛·lenyiin
彷徨而立2 小时前
【C/C++】创建文件夹
c语言·开发语言·c++
虾球xz2 小时前
游戏引擎学习第315天:取消排序键的反向顺序
开发语言·c++·学习·游戏引擎
闻缺陷则喜何志丹3 小时前
【强连通分量 拓扑序】P9431 [NAPC-#1] Stage3 - Jump Refreshers|普及+
c++·算法·图论·拓扑序·洛谷·强连通分量
大白爱琴3 小时前
C++ 精简知识点
开发语言·c++