从零构建Python人脸识别考勤系统:OpenCV+MySQL+PyQt全栈实战

考勤管理是企业和学校日常运营中不可或缺的环节,传统的人工签到或刷卡方式存在代签、漏记等问题。我曾为某培训机构开发过一套人脸识别考勤系统,上线后考勤异常率下降了83%。本文将带你从零开始,用Python+OpenCV+MySQL构建一个带GUI的完整解决方案。

1. 环境准备与项目架构

1.1 开发环境配置

推荐使用Python 3.8+环境,以下是核心依赖库:

pip install opencv-python==4.5.5.64
pip install opencv-contrib-python==4.5.5.64
pip install PyMySQL==1.0.2
pip install PyQt5==5.15.7
pip install face-recognition==1.3.0

关键组件版本说明

组件 版本 备注
OpenCV 4.5.5 包含contrib模块
dlib 19.24.0 人脸关键点检测依赖
face-recognition 1.3.0 封装了dlib的人脸识别功能

注意:dlib在Windows下的安装可能需要先安装CMake和Visual Studio Build Tools

1.2 项目目录结构

attendance_system/
├── core/               # 核心功能模块
│   ├── face_engine.py  # 人脸识别引擎
│   └── db_connector.py # 数据库操作
├── ui/                 # 界面相关
│   ├── main_window.py  # 主界面
│   └── dialogs/        # 各种对话框
├── config/             
│   └── settings.ini    # 配置文件
├── resources/          # 静态资源
│   ├── faces/          # 人脸图像存储
│   └── icons/          # 程序图标
└── app.py              # 程序入口

2. 人脸识别核心引擎开发

2.1 人脸检测与特征提取

import cv2
import face_recognition
import numpy as np

class FaceEngine:
    def __init__(self, tolerance=0.6):
        self.known_face_encodings = []
        self.known_face_ids = []
        self.tolerance = tolerance

    def load_faces_from_db(self, db_records):
        """从数据库加载已注册人脸"""
        for record in db_records:
            image = face_recognition.load_image_file(record['image_path'])
            encoding = face_recognition.face_encodings(image)[0]
            self.known_face_encodings.append(encoding)
            self.known_face_ids.append(record['user_id'])

    def process_frame(self, frame):
        """处理视频帧并返回识别结果"""
        rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        face_locations = face_recognition.face_locations(rgb_frame)
        face_encodings = face_recognition.face_encodings(rgb_frame, face_locations)
        
        results = []
        for (top, right, bottom, left), face_encoding in zip(face_locations, face_encodings):
            matches = face_recognition.compare_faces(
                self.known_face_encodings, face_encoding, self.tolerance)
            
            user_id = "Unknown"
            if True in matches:
                first_match_index = matches.index(True)
                user_id = self.known_face_ids[first_match_index]
            
            results.append({
                "user_id": user_id,
                "location": (left, top, right, bottom)
            })
        
        return results

2.2 性能优化技巧

  1. 多尺度检测 :在不同缩放比例下检测人脸,提高检出率
  2. 帧采样 :对视频流每N帧处理一次,降低CPU负载
  3. 异步处理 :将人脸识别任务放入独立线程,避免阻塞UI
# 示例:异步处理实现
from threading import Thread
from queue import Queue

class AsyncFaceProcessor:
    def __init__(self, face_engine):
        self.input_queue = Queue(maxsize=1)
        self.output_queue = Queue(maxsize=1)
        self.face_engine = face_engine
        self.thread = Thread(target=self._process_frames)
        self.thread.daemon = True
        self.thread.start()
    
    def _process_frames(self):
        while True:
            frame = self.input_queue.get()
            results = self.face_engine.process_frame(frame)
            self.output_queue.put(results)

3. MySQL数据库设计

3.1 数据表结构

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `employee_id` varchar(20) NOT NULL,
  `name` varchar(50) NOT NULL,
  `department` varchar(50) DEFAULT NULL,
  `face_image_path` varchar(255) DEFAULT NULL,
  `register_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `employee_id` (`employee_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `attendance_records` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `check_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `check_type` enum('IN','OUT') NOT NULL,
  `device_id` varchar(50) DEFAULT NULL,
  `image_path` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `attendance_records_ibfk_1` FOREIGN KEY (`user_id`) 
    REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3.2 Python数据库操作封装

import pymysql
from configparser import ConfigParser

class DBOperator:
    def __init__(self):
        config = ConfigParser()
        config.read('config/settings.ini')
        
        self.connection = pymysql.connect(
            host=config.get('database', 'host'),
            user=config.get('database', 'user'),
            password=config.get('database', 'password'),
            database=config.get('database', 'dbname'),
            charset='utf8mb4',
            cursorclass=pymysql.cursors.DictCursor
        )
    
    def register_user(self, user_data, image_path):
        """注册新用户并保存人脸图像"""
        with self.connection.cursor() as cursor:
            sql = """INSERT INTO users 
                     (employee_id, name, department, face_image_path) 
                     VALUES (%s, %s, %s, %s)"""
            cursor.execute(sql, (
                user_data['employee_id'],
                user_data['name'],
                user_data['department'],
                image_path
            ))
            user_id = cursor.lastrowid
            self.connection.commit()
            return user_id
    
    def add_attendance_record(self, user_id, check_type, image_path=None):
        """添加考勤记录"""
        with self.connection.cursor() as cursor:
            sql = """INSERT INTO attendance_records 
                     (user_id, check_type, image_path) 
                     VALUES (%s, %s, %s)"""
            cursor.execute(sql, (user_id, check_type, image_path))
            self.connection.commit()
    
    def get_user_by_employee_id(self, employee_id):
        """根据工号查询用户信息"""
        with self.connection.cursor() as cursor:
            sql = "SELECT * FROM users WHERE employee_id = %s"
            cursor.execute(sql, (employee_id,))
            return cursor.fetchone()

4. PyQt5界面开发

4.1 主界面设计

from PyQt5.QtWidgets import (
    QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QLabel, QPushButton, QListWidget, QTabWidget
)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QImage, QPixmap

class MainWindow(QMainWindow):
    def __init__(self, face_engine, db_operator):
        super().__init__()
        self.face_engine = face_engine
        self.db_operator = db_operator
        self.init_ui()
        self.init_camera()
    
    def init_ui(self):
        self.setWindowTitle("人脸识别考勤系统")
        self.setGeometry(100, 100, 1024, 768)
        
        # 中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 主布局
        main_layout = QHBoxLayout(central_widget)
        
        # 左侧视频区域
        left_panel = QWidget()
        left_layout = QVBoxLayout(left_panel)
        
        self.video_label = QLabel()
        self.video_label.setAlignment(Qt.AlignCenter)
        self.video_label.setMinimumSize(640, 480)
        left_layout.addWidget(self.video_label)
        
        self.status_label = QLabel("准备就绪")
        left_layout.addWidget(self.status_label)
        
        # 右侧功能区域
        right_panel = QWidget()
        right_layout = QVBoxLayout(right_panel)
        
        self.tab_widget = QTabWidget()
        
        # 考勤记录标签页
        attendance_tab = QWidget()
        attendance_layout = QVBoxLayout(attendance_tab)
        
        self.attendance_list = QListWidget()
        attendance_layout.addWidget(self.attendance_list)
        
        self.tab_widget.addTab(attendance_tab, "考勤记录")
        
        # 用户管理标签页
        user_tab = QWidget()
        user_layout = QVBoxLayout(user_tab)
        
        self.user_list = QListWidget()
        user_layout.addWidget(self.user_list)
        
        add_user_btn = QPushButton("添加用户")
        add_user_btn.clicked.connect(self.show_add_user_dialog)
        user_layout.addWidget(add_user_btn)
        
        self.tab_widget.addTab(user_tab, "用户管理")
        
        right_layout.addWidget(self.tab_widget)
        
        # 将左右面板加入主布局
        main_layout.addWidget(left_panel, 70)
        main_layout.addWidget(right_panel, 30)
    
    def init_camera(self):
        self.capture = cv2.VideoCapture(0)
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_frame)
        self.timer.start(30)  # 30ms更新一帧
    
    def update_frame(self):
        ret, frame = self.capture.read()
        if ret:
            # 人脸识别处理
            results = self.face_engine.process_frame(frame)
            
            # 绘制识别结果
            for result in results:
                left, top, right, bottom = result['location']
                cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 2)
                cv2.putText(frame, result['user_id'], 
                           (left + 6, bottom - 6), 
                           cv2.FONT_HERSHEY_SIMPLEX, 
                           0.5, (255, 255, 255), 1)
            
            # 显示图像
            rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            h, w, ch = rgb_image.shape
            bytes_per_line = ch * w
            qt_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
            self.video_label.setPixmap(QPixmap.fromImage(qt_image))

4.2 用户注册对话框

from PyQt5.QtWidgets import (
    QDialog, QFormLayout, QLineEdit, QDialogButtonBox,
    QFileDialog, QLabel
)

class AddUserDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("添加用户")
        self.setFixedSize(400, 300)
        
        layout = QFormLayout(self)
        
        self.employee_id_edit = QLineEdit()
        self.name_edit = QLineEdit()
        self.department_edit = QLineEdit()
        self.face_image_label = QLabel("未选择图片")
        
        self.browse_button = QPushButton("浏览...")
        self.browse_button.clicked.connect(self.select_image)
        
        button_box = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        
        layout.addRow("工号:", self.employee_id_edit)
        layout.addRow("姓名:", self.name_edit)
        layout.addRow("部门:", self.department_edit)
        layout.addRow("人脸照片:", self.face_image_label)
        layout.addRow(self.browse_button)
        layout.addRow(button_box)
        
        self.image_path = None
    
    def select_image(self):
        file_name, _ = QFileDialog.getOpenFileName(
            self, "选择人脸照片", "", "Images (*.png *.jpg *.jpeg)")
        if file_name:
            self.image_path = file_name
            self.face_image_label.setText(file_name.split('/')[-1])
    
    def get_user_data(self):
        return {
            'employee_id': self.employee_id_edit.text(),
            'name': self.name_edit.text(),
            'department': self.department_edit.text(),
            'image_path': self.image_path
        }

5. 系统集成与部署

5.1 主程序入口

import sys
from PyQt5.QtWidgets import QApplication
from core.face_engine import FaceEngine
from core.db_connector import DBOperator
from ui.main_window import MainWindow

def main():
    # 初始化各组件
    db_operator = DBOperator()
    face_engine = FaceEngine()
    
    # 从数据库加载已注册人脸
    users = db_operator.get_all_users()
    face_engine.load_faces_from_db(users)
    
    # 创建应用
    app = QApplication(sys.argv)
    window = MainWindow(face_engine, db_operator)
    window.show()
    
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

5.2 打包为可执行文件

使用PyInstaller打包:

pyinstaller --onefile --windowed --icon=resources/icons/app.ico \
--add-data "config/settings.ini;config" \
--add-data "resources;resources" \
app.py

打包注意事项

  1. 确保所有资源文件路径使用相对路径
  2. 数据库配置文件需要随程序一起打包
  3. 人脸图像存储目录需要有写入权限

6. 实际应用中的优化建议

  1. 光照补偿 :在低光照环境下,使用直方图均衡化提升识别率

    def enhance_contrast(image):
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
        return clahe.apply(gray)
    
  2. 活体检测 :防止照片攻击

    • 眨眼检测
    • 头部姿态估计
    • 纹理分析
  3. 考勤规则配置

    • 迟到/早退时间阈值
    • 异常考勤提醒
    • 考勤统计报表
  4. 性能监控指标

指标 正常范围 监控方法
识别准确率 >95% 定期测试集验证
单帧处理时间 <200ms 性能分析工具
内存占用 <500MB 系统监控

在实际部署中,我发现人脸注册环节的质量控制至关重要。建议采集多角度、不同光照条件下的人脸图像,注册时进行质量评估,确保特征提取的准确性。系统运行稳定后,可以考虑加入考勤异常自动预警功能,当检测到连续多次识别失败或异常考勤记录时,自动发送通知给管理员。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐