基于人脸识别的考勤系统:Python3 + Qt5 + OpenCV3 + OpenFace + MySQL


2021.4.14 Update

数据库表格已经上传到 Github

  • 路径:/02 main/mysql table


问题汇总(持续更新🧭···)

在文末。点击目录,跳转到相应问题章节。在将本项目代码开源之后,收到了很多小伙伴的提问,于是将常见的问题在此汇总。


1. 项目简介🐱‍🏍

🏁本项目使用Python3.6编写,Qt Designer(QT5)设计主界面,PyQt5库编写控件的功能,使用开源 DeepFace人脸识别算法进行人脸识别,使用眨眼检测来实现活体识别,使用OpenCV3实现实时人脸识别。🐱‍👤同时,将班级学生信息,各班级学生人数、考勤信息录入到MySQL数据库中,方便集中统一化管理。🐱‍👓因为本项目仅由我一个人开发,能力精力有限,实现了预期的绝大多数功能,但是活体检测功能还存在bug,主要表现是界面卡死,如果小伙伴对本项目中有不懂的地方或者发现问题提出解决方案,欢迎私聊我或者在此博客评论亦或在github提交。🎠项目大概持续了两三个月的时间,在开发过程中,遇到过许多难题,参考了很多教程,才有了这个项目。🎉相信大家看到这里,一定是在比赛中或者是作业中遇到类似问题了,我也有过类似的经历,很清楚找不到解决方案,自己盲目摸索的苦恼,这也是我选择开源的原因,个人能力有限,但是希望本项目能给需要的小伙伴提供帮助。✨完整代码发布在GitHub:代码下载 。🎁后续我会重新整理一下工程和使用教程。如果对你有帮助的话,麻烦在Github点个【star】吧,谢谢!💖


2. 系统前端设计😎

使用 Qt Designer 设计前端界面。

2.1 主界面

在这里插入图片描述


2.2 信息采集界面

在这里插入图片描述


3. 数据库存取信息🥗

3.1 数据库可视化工具 Navicat

使用该软件是为了方便管理维护信息,如果有数据库基础,当然也可以选择其它方式。其主界面如下:
在这里插入图片描述
当时传小凳子训练迟到的消息,给他设置成了请假哈哈哈哈!

如果需要安装包,可以留下邮箱,或者加交流群,群文件自取。

【创建数据库流程】
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


3.2 PyMySQL

项目中只使用了简单的写入、查询等几个常用命令,即使没有数据库基础的话,上手这个库也比较容易。看一下文档,基本就会了。


4. 系统功能介绍(正在更新🛵···)

4.1 信息采集

4.2 人脸识别

4.3 活体检测(存在bug😣)

4.4 查询考勤信息

4.5 查询学生信息

4.6 请假登记


5. 使用教程🍨

5.1 系统环境配置

  • python >= 3.6
  • OpenCV3
  • PyQt5
  • imutils
  • dlib
  • freetype-py(optional)
  • pymysql

5.2 需要修改源码

  1. 安装 msqlservice 然后修改 mainRUN.py文件中的数据库连接代码。比如 db = pymysql.connect("localhost", "root", "mysql105", "facerecognition")。这首先需要在 navicat中创建数据库。
  2. 下载 shape_predictor_68_face_landmarks.dat并放到 /02 Main 路径下;
  3. 下载 openface_nn4.small2.v1.t7 放到 /02 Main/face_detection_model 路径下;
  4. 如果不是通过本系统的信息采集功能采集的人脸照片,请将采集的人脸照片放到 dataset/XX 路径下,其中XX是学号(唯一索引),如果是通过系统采集的,则会自动存放在该路径下,不需要修改。

5.3 使用步骤

  1. navicat创建数据库,打开数据库录入学生信息和班级信息;
  2. 修改源码,连接到创建的数据库
  3. 采集人脸照片,点击界面中的信息采集,在子窗口操作即可。
  4. 训练人脸识别模型,点击界面中的更新人脸库
  5. 开始考勤:打开相机 --> 开始考勤
  6. Have fun!😊

6. 源码解析🎨

# 导入必要的模块
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox, QInputDialog
from PyQt5.QtGui import QImage, QIcon, QPixmap
from PyQt5.QtCore import QTimer, QDateTime, QCoreApplication, QThread
import sys, os
import cv2, imutils
# 导入UI主界面
import main
# 导入信息采集框界面
import infoUI
# 导入打印中文脚本
import ChinesePutText
# 导入人脸识别检测包
from imutils.video import VideoStream
import numpy as np
import pickle
# 导入眨眼检测必要的包
from scipy.spatial import distance as dist
from imutils import face_utils
from datetime import datetime
import dlib
# 导入数据库操作包
import pymysql

# 定义活体检测-眨眼检测类
class BlinksDetectThread(QThread):
    trigger = QtCore.pyqtSignal()
    def __init__(self):
        super(BlinksDetectThread, self).__init__()
        # 定义两个常数,一个用于眼睛纵横比以指示眨眼,第二个作为眨眼连续帧数的阈值
        self.EYE_AR_THRESH = 0.25
        self.EYE_AR_CONSEC_FRAMES = 3
        # 初始化帧计数器和总闪烁次数
        self.COUNTER = 0
        self.TOTAL = 0
        # 初始化变量
        self.A = 0
        self.B = 0
        self.C = 0
        self.leftEye = 0
        self.rightEye = 0
        self.leftEAR = 0
        self.rightEAR = 0
        self.ear = 0
        # 线程启动停止标识符
        self.BlinksFlag = 1
        # 初始化摄像头
        self.cap3 = cv2.VideoCapture()

        # 定义眨眼检测距离函数
    def eye_aspect_ratio(self, eye):
        # 计算两组垂直方向上的眼睛标记(x,y)坐标之间的欧氏距离
        self.A = dist.euclidean(eye[1], eye[5])
        self.B = dist.euclidean(eye[2], eye[4])
        # 计算水平方向上的眼睛标记(x,y)坐标之间的欧氏距离
        self.C = dist.euclidean(eye[0], eye[3])
        # 计算眼睛的纵横比
        ear = (self.A + self.B) / (2.0 * self.C)
        # 返回眼睛的纵横比
        return ear

    def run(self):
        if self.BlinksFlag == 1:
            # 初始化dlib的人脸检测器(基于HOG),然后创建面部标志预测器
            print("[INFO] loading facial landmark predictor...")
            shape_predictor_path = "shape_predictor_68_face_landmarks.dat"
            detector = dlib.get_frontal_face_detector()
            predictor = dlib.shape_predictor(shape_predictor_path)
            # 分别提取左眼和右眼的面部标志的索引
            (lStart, lEnd) = face_utils.FACIAL_LANDMARKS_IDXS["left_eye"]
            (rStart, rEnd) = face_utils.FACIAL_LANDMARKS_IDXS["right_eye"]
            # 在视频流的帧中循环
            self.cap3.open(cv2.CAP_DSHOW)
            while self.BlinksFlag == 1:
                # 从线程视频文件流中抓取帧,调整其大小,并将其转换为灰度通道
                vs = VideoStream(src=cv2.CAP_DSHOW).start()
                frame3 = vs.read()
                # ret, frame3 = self.cap3.read()
                QApplication.processEvents()
                frame3 = imutils.resize(frame3, width=900)
                gray = cv2.cvtColor(frame3, cv2.COLOR_BGR2GRAY)
                # 检测灰度帧中的人脸
                rects = detector(gray, 0)
                # 循环检测人脸
                for rect in rects:
                    # 确定面部区域的面部标记,然后将面部标记(x,y)坐标转换为NumPy阵列
                    shape = predictor(gray, rect)
                    shape = face_utils.shape_to_np(shape)
                    # 提取左眼和右眼坐标,然后使用坐标计算双眼的眼睛纵横比
                    self.leftEye = shape[lStart:lEnd]
                    self.rightEye = shape[rStart:rEnd]
                    self.leftEAR = self.eye_aspect_ratio(self.leftEye)
                    self.rightEAR = self.eye_aspect_ratio(self.rightEye)
                    # 两只眼睛的平均眼睛纵横比
                    self.ear = (self.leftEAR + self.rightEAR) / 2.0
                    # 检查眼睛纵横比是否低于闪烁阈值,如果是,则增加闪烁帧计数器;否则执行else
                    if self.ear < self.EYE_AR_THRESH:
                        self.COUNTER += 1
                    else:
                        # 如果眼睛闭合次数足够则增加眨眼总数
                        if self.COUNTER >= self.EYE_AR_CONSEC_FRAMES:
                            self.TOTAL += 1
                        # 重置眼框计数器
                        self.COUNTER = 0
                self.trigger.emit()
                if self.TOTAL == 1:
                    print("活体!眨眼次数为: {}".format(self.TOTAL))

    # 定义停止线程操作
    def terminate(self):
        self.BlinksFlag = 0
        if flag2 == 0:
            VideoStream(src=cv2.CAP_DSHOW).stop()

#########################################################################################

class MainWindow(QWidget):
    # 类构造函数
    def __init__(self):
        # super()构造器方法返回父级的对象。__init__()方法是构造器的一个方法。
        super().__init__()
        self.ui = main.Ui_Form()
        self.ui.setupUi(self)
        # 设置窗口名称和图标
        self.setWindowTitle('人脸识别考勤系统')
        self.setWindowIcon(QIcon('fcblogo.jpg'))
        # label_time显示系统时间
        timer = QTimer(self)
        timer.timeout.connect(self.showTimeText)
        timer.start()
        # 初始化摄像头
        # self.url = 0 # 这样调用摄像头会报错,并且会卡死。
        self.url = cv2.CAP_DSHOW  # 默认调用0,如果要调用摄像头1,可以这样写:cv2.CAP_DSHOW + 1
        self.cap = cv2.VideoCapture()
        # 设置单张图片背景
        pixmap = QPixmap('background1.png')
        self.ui.label_camera.setPixmap(pixmap)
        # 设置摄像头按键连接函数
        self.ui.bt_openCamera.clicked.connect(self.openCamera)
        # 设置开始考勤按键的回调函数
        self.ui.bt_startCheck.clicked.connect(self.autoControl)
        # 设置活体检测按键的回调函数
        self.ui.bt_blinks.clicked.connect(self.BlinksThread)
        # 设置“退出系统”按键事件, 按下之后退出主界面
        self.ui.bt_exit.clicked.connect(QCoreApplication.instance().quit)
        # 设置信息采集按键连接
        self.bt_gathering = self.ui.bt_gathering
        # 设置区分打开摄像头还是人脸识别的标识符
        self.switch_bt = 0
        global flag2
        flag2 = 0

        # 初始化需要记录的人名
        self.record_name1 = ([])

        # 设置更新人脸数据库的按键连接函数
        self.ui.bt_generator.clicked.connect(self.trainModel)
        # 设置查询班级人数按键的连接函数
        self.ui.bt_check.clicked.connect(self.checkNums)
        # 设置请假按键的连接函数
        self.ui.bt_leave.clicked.connect(self.leaveButton)
        # 设置漏签补签按键的连接函数
        self.ui.bt_Supplement.clicked.connect(self.supplymentButton)
        # 设置对输入内容的删除提示
        self.ui.lineEdit.setClearButtonEnabled(True)
        self.ui.lineEdit_2.setClearButtonEnabled(True)
        # 设置查看结果(显示未到和迟到)按键的连接函数
        self.ui.bt_view.clicked.connect(self.showLateAbsentee)

        self.checkTime, ok = QInputDialog.getText(self, '考勤时间设定', '请输入考勤时间(格式为00:00:00):')

    # 显示系统时间以及相关文字提示函数
    def showTimeText(self):
        # 设置宽度
        self.ui.label_time.setFixedWidth(200)
        # 设置显示文本格式
        self.ui.label_time.setStyleSheet(
            # "QLabel{background:white;}" 此处设置背景色
            "QLabel{color:rgb(300,300,300,120); font-size:14px; font-weight:bold; font-family:宋体;}")
        datetime = QDateTime.currentDateTime().toString()
        self.ui.label_time.setText("" + datetime)

        # 显示“人脸识别考勤系统”文字
        self.ui.label_title.setFixedWidth(400)
        self.ui.label_title.setStyleSheet(
            "QLabel{font-size:30px; font-weight:bold; font-family:宋体;}")
        self.ui.label_title.setText("人脸识别考勤系统")

    def openCamera(self, url):
        # 判断摄像头是否打开,如果打开则为true,反之为false
        flag = self.cap.isOpened()
        if flag == False:
            self.ui.label_logo.clear()
            self.cap.open(self.url)
            self.showCamera()
        elif flag == True:
            self.cap.release()
            self.ui.label_logo.clear()
            self.ui.label_camera.clear()
            self.ui.bt_openCamera.setText(u'打开相机')

    # 进入考勤模式,通过switch_bt进行控制的函数
    def autoControl(self):
        if self.switch_bt == 0:
            self.switch_bt = 1
            flag2 = 1
            self.ui.bt_startCheck.setText(u'退出考勤')
            self.showCamera()
        elif self.switch_bt == 1:
            self.switch_bt = 0
            flag2 = 0
            self.ui.bt_startCheck.setText(u'开始考勤')
            self.showCamera()

    def BlinksThread(self):
        bt_text = self.ui.bt_blinks.text()
        if bt_text == '活体检测':
            # 初始化眨眼检测线程
            self.startThread = BlinksDetectThread()
            self.startThread.start()  # 启动线程
            self.ui.bt_blinks.setText('停止检测')
        else:
            self.ui.bt_blinks.setText('活体检测')
            # self.startThread.terminate()  # 停止线程

    def showCamera(self):
        # 如果按键按下
        if self.switch_bt == 0:
            self.ui.label_logo.clear()
            self.ui.bt_openCamera.setText(u'关闭相机')
            while (self.cap.isOpened()):
                # 以BGR格式读取图像
                ret, self.image = self.cap.read(cv2.CAP_DSHOW)
                QApplication.processEvents()  # 这句代码告诉QT处理来处理任何没有被处理的事件,并且将控制权返回给调用者,让代码变的没有那么卡
                # 将图像转换为RGB格式
                show = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)  # 这里指的是显示原图
                # opencv 读取图片的样式,不能通过Qlabel进行显示,需要转换为Qimage QImage(uchar * data, int width,
                self.showImage = QImage(show.data, show.shape[1], show.shape[0], QImage.Format_RGB888)
                self.ui.label_camera.setPixmap(QPixmap.fromImage(self.showImage))
            # 因为最后会存留一张图像在lable上,需要对lable进行清理
            self.ui.label_camera.clear()
            self.ui.bt_openCamera.setText(u'打开相机')

        elif self.switch_bt == 1:
            self.ui.label_logo.clear()
            self.ui.bt_startCheck.setText(u'退出考勤')
            # OpenCV深度学习人脸检测器的路径
            detector = "face_detection_model"
            # OpenCV深度学习面部嵌入模型的路径
            embedding_model = "face_detection_model/openface_nn4.small2.v1.t7"
            # 训练模型以识别面部的路径
            recognizer_path = "output/recognizer.pickle"
            # 标签编码器的路径
            le_path = "output/le.pickle"
            # 置信度
            confidence_default = 0.5
            # 从磁盘加载序列化面部检测器
            protoPath = os.path.sep.join([detector, "deploy.prototxt"])
            modelPath = os.path.sep.join([detector, "res10_300x300_ssd_iter_140000.caffemodel"])
            detector = cv2.dnn.readNetFromCaffe(protoPath, modelPath)
            # 从磁盘加载我们的序列化面嵌入模型
            print("[INFO] loading face recognizer...")
            embedder = cv2.dnn.readNetFromTorch(embedding_model)
            # 加载实际的人脸识别模型和标签
            recognizer = pickle.loads(open(recognizer_path, "rb").read())
            le = pickle.loads(open(le_path, "rb").read())
            # 循环来自视频文件流的帧
            while (self.cap.isOpened()):
                # 从线程视频流中抓取帧
                ret, frame = self.cap.read()
                QApplication.processEvents()
                # 调整框架的大小以使其宽度为900像素(同时保持纵横比),然后抓取图像尺寸
                frame = imutils.resize(frame, width=900)
                (h, w) = frame.shape[:2]
                # 从图像构造一个blob
                imageBlob = cv2.dnn.blobFromImage(
                    cv2.resize(frame, (300, 300)), 1.0, (300, 300),
                    (104.0, 177.0, 123.0), swapRB=False, crop=False)
                # 应用OpenCV的基于深度学习的人脸检测器来定位输入图像中的人脸
                detector.setInput(imageBlob)
                detections = detector.forward()
                # 保存识别到的人脸
                face_names = []
                # 循环检测
                for i in np.arange(0, detections.shape[2]):
                    # 提取与预测相关的置信度(即概率)
                    confidence = detections[0, 0, i, 2]

                    # 用于更新相机开关按键信息
                    flag = self.cap.isOpened()
                    if flag == False:
                        self.ui.bt_openCamera.setText(u'打开相机')
                    elif flag == True:
                        self.ui.bt_openCamera.setText(u'关闭相机')

                    # 过滤弱检测
                    if confidence > confidence_default:
                        # 计算面部边界框的(x,y)坐标
                        box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
                        (startX, startY, endX, endY) = box.astype("int")

                        # 提取面部ROI
                        face = frame[startY:endY, startX:endX]
                        (fH, fW) = face.shape[:2]

                        # 确保面部宽度和高度足够大
                        if fW < 20 or fH < 20:
                            continue

                        # 为面部ROI构造一个blob,然后通过我们的面部嵌入模型传递blob以获得面部的128-d量化
                        faceBlob = cv2.dnn.blobFromImage(face, 1.0 / 255, (96, 96), (0, 0, 0), swapRB=True, crop=False)
                        embedder.setInput(faceBlob)
                        vec = embedder.forward()
                        # 执行分类识别面部
                        preds = recognizer.predict_proba(vec)[0]
                        j = np.argmax(preds)
                        proba = preds[j]
                        name = le.classes_[j]
                        # 绘制面部的边界框以及相关的概率
                        text = "{}: {:.2f}%".format(name, proba * 100)
                        y = startY - 10 if startY - 10 > 10 else startY + 10
                        cv2.rectangle(frame, (startX, startY), (endX, endY), (0, 0, 255), 2)
                        frame = cv2.putText(frame, text, (startX, y), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 255), 2)
                        face_names.append(name)

                bt_liveness = self.ui.bt_blinks.text()
                if bt_liveness == '停止检测':
                    ChineseText = ChinesePutText.put_chinese_text('microsoft.ttf')
                    frame = ChineseText.draw_text(frame, (330, 80), ' 请眨眨眼睛 ', 25, (55, 255, 55))
                # 显示输出框架
                show_video = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)  # 这里指的是显示原图
                # opencv读取图片的样式,不能通过Qlabel进行显示,需要转换为Qimage。
                # QImage(uchar * data, int width, int height, int bytesPerLine, Format format)
                self.showImage = QImage(show_video.data, show_video.shape[1], show_video.shape[0], QImage.Format_RGB888)
                self.ui.label_camera.setPixmap(QPixmap.fromImage(self.showImage))
                self.set_name = set(face_names)
                self.set_names = tuple(self.set_name)
                self.recordNames()

            # 因为最后一张画面会显示在GUI中,此处实现清除。
            self.ui.label_camera.clear()

    def recordNames(self):
        if self.set_name.issubset(self.record_name1):  # 如果self.set_names是self.record_names 的子集返回ture
            pass  # record_name1是要写进数据库中的名字信息 set_name是从摄像头中读出人脸的tuple形式
        else:
            self.different_name1 = self.set_name.difference(self.record_name1)  # 获取到self.set_name有而self.record_name无的名字
            self.record_name1 = self.set_name.union(self.record_name1)  # 把self.record_name变成两个集合的并集
            # different_name是为了获取到之前没有捕捉到的人脸,并再次将record_name1进行更新
            # 将集合变成tuple,并统计人数
            self.write_data = tuple(self.different_name1)
            names_num = len(self.write_data)
            # 显示签到人数
            self.ui.lcd_2.display(len(self.record_name1))

            if names_num > 0:
                # 将签到信息写入数据库
                self.lineTextInfo2 = []
                # 打开数据库连接
                db2 = pymysql.connect("localhost", "root", "mysql105", "facerecognition")
                # 使用cursor()方法获取操作游标
                cursor2 = db2.cursor()
                # 获取系统时间,保存到秒
                import datetime
                currentTime2 = str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
                results2 = self.useIDGetInfo(self.write_data[0])
                # 判断是否迟到
                import datetime
                self.ymd = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                self.ymd2 = datetime.datetime.now().strftime("%H:%M:%S")
                compareResult2 = self.compare_time('{}'.format(self.ymd2), '{}'.format(self.checkTime))

                # 82800表示23个小时,在compare_time()函数中,如果第一个时间小于第二个时间,则为第一个时间加24h后再减去第二时间;
                # 而正常的结果应该为'正常'.
                if compareResult2 <= 82800:
                    self.description2 = '迟到'
                else:
                    self.description2 = '正常'
                self.lineTextInfo2.append((results2[0], results2[1], results2[2], currentTime2, self.description2))
                print(self.lineTextInfo2)

                # 写入数据库
                try:
                    # 如果存在数据,先删除再写入。前提是设置唯一索引字段或者主键。
                    insert_sql2 = "replace into checkin(Name, ID, Class, Time, Description) values(%s, %s, %s, %s, %s)"
                    users2 = self.lineTextInfo2
                    cursor2.executemany(insert_sql2, users2)
                except Exception as e:
                    print(e)
                    print("SQL execute failed!")
                else:
                    print("SQL execute success!")
                    QMessageBox.information(self, "Tips", "签到成功,请勿重复操作!", QMessageBox.Yes | QMessageBox.No)
                # 提交到数据库执行
                db2.commit()
                cursor2.close()
                db2.close()

    # 比较时间大小,判断是否迟到
    def compare_time(self, time1, time2):
        import datetime
        s_time = datetime.datetime.strptime(time1, '%H:%M:%S')
        e_time = datetime.datetime.strptime(time2, '%H:%M:%S')
        delta = s_time - e_time
        return delta.seconds

    # 查询班级人数
    def checkNums(self):
        # 选择的班级
        input_Class = self.ui.comboBox.currentText()
        # 打开数据库连接
        db = pymysql.connect("localhost", "root", "mysql105", "facerecognition")
        # 使用cursor()方法获取操作游标
        cursor = db.cursor()

        # 查询语句,实现通过ID关键字检索个人信息的功能
        sql = "select * from studentnums where class = {}".format(input_Class)
        # 执行查询
        if input_Class != '':
            try:
                cursor.execute(sql)
                # 获取所有记录列表
                results = cursor.fetchall()
                self.nums = []
                for i in results:
                    self.nums.append(i[1])
            except:
                print("Error: unable to fetch data")

        # 用于查询每班的实到人数
        sql2 = "select * from checkin where class = {}".format(input_Class)
        # 执行查询
        if input_Class != '':
            try:
                cursor.execute(sql2)
                # 获取所有记录列表
                results2 = cursor.fetchall()
                self.nums2 = []
                for i in results2:
                    self.nums2.append(i[2])
            except:
                print("Error: unable to fetch data")

        # lcd控件显示人数
        self.ui.lcd_1.display(self.nums[0])
        self.ui.lcd_2.display(len(self.nums2))
        # 关闭数据库连接
        db.close()

    # 请假/补签登记
    def leaveButton(self):
        self.leaveStudents(1)
    def supplymentButton(self):
        self.leaveStudents(2)
    def leaveStudents(self, button):
        self.lineTextInfo = []
        # 为防止输入为空卡死,先进行是否输入数据的判断
        if self.ui.lineEdit.isModified() or self.ui.lineEdit_2.isModified():
            # 打开数据库连接
            db = pymysql.connect("localhost", "root", "mysql105", "facerecognition")
            # 使用cursor()方法获取操作游标
            cursor = db.cursor()
            # 获取系统时间,保存到秒
            currentTime = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
            if button == 1:
                self.description = '请假'
                self.lineTextID = self.ui.lineEdit.text()
                results = self.useIDGetInfo(self.lineTextID)
            elif button == 2:
                self.description = '漏签补签'
                self.lineTextID = self.ui.lineEdit_2.text()
                results = self.useIDGetInfo(self.lineTextID)
            self.lineTextInfo.append((results[0], results[1], results[2], currentTime, self.description))
            # 写入数据库
            try:
                # 如果存在数据,先删除再写入。前提是设置唯一索引字段或者主键。
                insert_sql = "replace into checkin(Name, ID, Class, Time, Description) values(%s, %s, %s, %s, %s)"
                users = self.lineTextInfo
                cursor.executemany(insert_sql, users)
            except Exception as e:
                print(e)
                print("sql execute failed")
            else:
                print("sql execute success")
                QMessageBox.warning(self, "warning", "{} 登记成功,请勿重复操作!".format(self.description), QMessageBox.Yes | QMessageBox.No)
            # 提交到数据库执行
            db.commit()
            cursor.close()
            db.close()
        else:
            QMessageBox.warning(self, "warning", "学号不能为空,请输入后重试!", QMessageBox.Yes | QMessageBox.No)
        # 输入框清零
        self.ui.lineEdit.clear()
        self.ui.lineEdit_2.clear()

    # 使用ID当索引找到其它信息
    def useIDGetInfo(self, ID):
        # 打开数据库连接
        db = pymysql.connect("localhost", "root", "mysql105", "facerecognition")
        # 使用cursor()方法获取操作游标
        cursor = db.cursor()
        # 查询语句,实现通过ID关键字检索个人信息的功能
        sql = "select * from students where ID = {}".format(ID)
        # 执行查询
        if ID != '':
            try:
                cursor.execute(sql)
                # 获取所有记录列表
                results = cursor.fetchall()
                self.checkInfo = []
                for i in results:
                    self.checkInfo.append(i[1])
                    self.checkInfo.append(i[0])
                    self.checkInfo.append(i[2])
                return self.checkInfo
            except:
                print("Error: unable to fetch data")

    # 显示迟到和未到
    def showLateAbsentee(self):
        db = pymysql.connect("localhost", "root", "mysql105", "facerecognition")
        cursor = db.cursor()
        # 一定要注意字符串在检索时要加''!
        sql1 = "select name from checkin where Description = '{}'".format('迟到')
        sql2 = "select name from students"
        try:
            cursor.execute(sql1)
            results = cursor.fetchall()
            self.lateNums = []
            for x in results:
                self.lateNums.append(x[0])
            self.lateNums.sort()
            # print(self.lateNums)
        except:
            print("Error: unable to fetch latedata")
        try:
            cursor.execute(sql2)
            results2 = cursor.fetchall()
            self.allNums = []
            for i in results2:
                self.allNums.append(i[0])
            self.allNums.sort()
            print(self.allNums)
        except:
            print("Error: unable to fetch absenteedata")

        db.commit()
        cursor.close()
        db.close()

        # 集合运算,算出未到的和迟到的
        self.AbsenteeNums = set(set(self.allNums) - set(self.lateNums))
        self.AbsenteeNums = list(self.AbsenteeNums)
        self.AbsenteeNums.sort()

        # 在控件中显示未到的同学
        rowLate = len(self.lateNums)
        rowAbsentee = len(self.AbsenteeNums)
        model1 = QtGui.QStandardItemModel(rowLate, 0)
        # 设置数据行、列标题
        model1.setHorizontalHeaderLabels(['姓名'])
        # 设置填入数据内容
        for row in range(rowLate):
            item = QtGui.QStandardItem(self.lateNums[row])
            # 设置每个位置的文本值
            model1.setItem(row, 0, item)
        # 指定显示的tableView控件,实例化表格视图
        View1 = self.ui.tableView_escape
        View1.setModel(model1)

        # 迟到显示
        model2 = QtGui.QStandardItemModel(rowAbsentee, 0)
        # 设置数据行、列标题
        model2.setHorizontalHeaderLabels(['姓名'])
        # 设置填入数据内容
        for row in range(rowAbsentee):
            item = QtGui.QStandardItem(self.AbsenteeNums[row])
            # 设置每个位置的文本值
            model2.setItem(row, 0, item)
        # 指定显示的tableView控件,实例化表格视图
        View2 = self.ui.tableView_late
        View2.setModel(model2)

    # 训练人脸识别模型
    def trainModel(self):
        import GeneratorModel
        GeneratorModel.Generator()
        GeneratorModel.TrainModel()
        print('Model have been trained!')

##########################################################################################

class infoDialog(QWidget):
    def __init__(self):
        # super()构造器方法返回父级的对象。__init__()方法是构造器的一个方法。
        super().__init__()
        self.Dialog = infoUI.Ui_Form()
        self.Dialog.setupUi(self)

        # 设置窗口名称和图标
        self.setWindowTitle('个人信息采集')
        self.setWindowIcon(QIcon('fcblogo.jpg'))

        # 设置单张图片背景
        pixmap = QPixmap('background2.png')
        self.Dialog.label_capture.setPixmap(pixmap)

        # 设置信息采集按键连接函数
        self.Dialog.bt_collectInfo.clicked.connect(self.openCam)
        # 设置拍照按键连接函数
        self.Dialog.bt_takephoto.clicked.connect(self.takePhoto)
        # 设置查询信息按键连接函数
        self.Dialog.bt_checkInfo.clicked.connect(self.checkInfo)
        # 设置写入信息按键连接函数
        self.Dialog.bt_changeInfo.clicked.connect(self.changeInfo)
        # 初始化信息导入列表
        self.users = []
        # 初始化摄像头
        self.url2 = cv2.CAP_DSHOW
        self.cap2 = cv2.VideoCapture()
        # 初始化保存人脸数目
        self.photos = 0

    def handle_click(self):
        if not self.isVisible():
            self.show()
    def handle_close(self):
        self.close()

    def openCam(self):
        # 判断摄像头是否打开,如果打开则为true,反之为false
        flagCam = self.cap2.isOpened()
        if flagCam == False:
            # 通过对话框设置被采集人学号
            self.text, self.ok = QInputDialog.getText(self, '创建个人图像数据库', '请输入学号:')
            if self.ok and self.text != '':
                self.Dialog.label_capture.clear()
                self.cap2.open(self.url2)
                self.showCapture()
        elif flagCam == True:
            self.cap2.release()
            self.Dialog.label_capture.clear()
            self.Dialog.bt_collectInfo.setText(u'采集人像')

    def showCapture(self):
        self.Dialog.bt_collectInfo.setText(u'停止采集')
        self.Dialog.label_capture.clear()
        # 导入opencv人脸检测xml文件
        cascade = 'haarcascades_cuda/haarcascade_frontalface_default.xml'
        # 加载 Haar级联人脸检测库
        detector = cv2.CascadeClassifier(cascade)
        print("[INFO] starting video stream...")
        # 循环来自视频文件流的帧
        while self.cap2.isOpened():
            ret, frame2 = self.cap2.read()
            QApplication.processEvents()
            self.orig = frame2.copy()
            frame2 = imutils.resize(frame2, width=500)
            rects = detector.detectMultiScale(cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY), scaleFactor=1.1,
                                              minNeighbors=5, minSize=(30, 30))
            for (x, y, w, h) in rects:
                cv2.rectangle(frame2, (x, y), (x + w, y + h), (0, 255, 0), 2)
                frame2 = cv2.putText(frame2, "Have token {}/20 faces".format(self.photos), (50, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7,
                            (200, 100, 50), 2)
            # 显示输出框架
            show_video2 = cv2.cvtColor(frame2, cv2.COLOR_BGR2RGB)  # 这里指的是显示原图
            # opencv读取图片的样式,不能通过Qlabel进行显示,需要转换为Qimage。
            # QImage(uchar * data, int width, int height, int bytesPerLine, Format format)
            self.showImage2 = QImage(show_video2.data, show_video2.shape[1], show_video2.shape[0], QImage.Format_RGB888)
            self.Dialog.label_capture.setPixmap(QPixmap.fromImage(self.showImage2))
            # 因为最后一张画面会显示在GUI中,此处实现清除。
        self.Dialog.label_capture.clear()

    # 创建文件夹
    def mkdir(self, path):
        # 去除首位空格
        path = path.strip()
        # 去除尾部 \ 符号
        path = path.rstrip("\\")
        # 判断路径是否存在, 存在=True; 不存在=False
        isExists = os.path.exists(path)
        # 判断结果
        if not isExists:
            # 如果不存在则创建目录
            os.makedirs(path)
            return True

    def takePhoto(self):
        self.photos += 1
        self.filename = "D:\\Project\\0-face-recognition-check-system\\03 Main\\dataset\\{}\\".format(self.text)
        self.mkdir(self.filename)
        photo_save_path = os.path.join(os.path.dirname(os.path.abspath('__file__')), '{}'.format(self.filename))
        self.showImage2.save(photo_save_path + datetime.now().strftime("%Y%m%d%H%M%S") + ".png")
        # p = os.path.sep.join([output, "{}.png".format(str(total).zfill(5))])
        # cv2.imwrite(p, self.showImage2)
        if self.photos == 20:
            QMessageBox.information(self, "Information", self.tr("采集成功!"), QMessageBox.Yes | QMessageBox.No)

    # 数据库查询
    def checkInfo(self):
        # 键入ID
        self.input_ID = self.Dialog.lineEdit_ID.text()
        # 打开数据库连接
        db = pymysql.connect("localhost", "root", "mysql105", "facerecognition")
        # 使用cursor()方法获取操作游标
        cursor = db.cursor()
        # 查询语句,实现通过ID关键字检索个人信息的功能
        sql = "SELECT * FROM STUDENTS WHERE ID = {}".format(self.input_ID)
        # 执行查询
        if self.input_ID != '':
            try:
                cursor.execute(sql)
                # 获取所有记录列表
                results = cursor.fetchall()
                self.lists = []
                for i in results:
                    self.lists.append(i[0])
                    self.lists.append(i[1])
                    self.lists.append(i[2])
                    self.lists.append(i[3])
                    self.lists.append(i[4])
            except:
                print("Error: unable to fetch data")

        # 设置显示数据层次结构,5行2列(包含行表头)
        self.model = QtGui.QStandardItemModel(5, 0)
        # 设置数据行、列标题
        self.model.setHorizontalHeaderLabels(['值'])
        self.model.setVerticalHeaderLabels(['学号', '姓名', '班级', '性别', '生日'])
        # 设置填入数据内容
        nums = len(self.lists)
        if nums == 0:
            QMessageBox.warning(self, "warning", "人脸数据库中无此人信息,请马上录入!", QMessageBox.Yes | QMessageBox.No)

        for row in range(nums):
            item = QtGui.QStandardItem(self.lists[row])
            # 设置每个位置的文本值
            self.model.setItem(row, 0, item)
        # 指定显示的tableView控件,实例化表格视图
        self.View = self.Dialog.tableView
        self.View.setModel(self.model)
        # 关闭数据库连接
        db.close()

    # 将采集信息写入数据库
    def userInfo(self):
        ID = self.Dialog.lineEdit_ID.text()
        Name = self.Dialog.lineEdit_Name.text()
        Class = self.Dialog.lineEdit_Class.text()
        Sex = self.Dialog.lineEdit_Sex.text()
        Birth = self.Dialog.lineEdit_Birth.text()
        self.users.append((ID, Name, Class, Sex, Birth))
        return self.users

    def changeInfo(self):
        # 打开数据库连接
        db = pymysql.connect("localhost", "root", "mysql105", "facerecognition")
        # 使用cursor()方法获取操作游标
        cursor = db.cursor()
        # 写入数据库
        try:
            # 如果存在数据,先删除再写入。前提是设置唯一索引字段或者主键。
            insert_sql = "replace into students(ID, Name, Class, Sex, Birthday) values(%s, %s, %s, %s, %s)"
            users = self.userInfo()
            cursor.executemany(insert_sql, users)
        except Exception as e:
            print(e)
            print("sql execute failed")
        else:
            print("sql execute success")
            QMessageBox.warning(self, "warning", "录入成功,请勿重复操作!", QMessageBox.Yes | QMessageBox.No)
        # 提交到数据库执行
        db.commit()
        # 关闭数据库
        cursor.close()
        # 关闭数据库连接
        db.close()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    # 创建并显示窗口
    mainWindow = MainWindow()
    infoWindow = infoDialog()
    mainWindow.ui.bt_gathering.clicked.connect(infoWindow.handle_click)
    mainWindow.show()
    sys.exit(app.exec_())

7. 待完善功能🚀

7.1 系统优化

解决bug,提升系统运行效率与稳定性。

7.2 人脸照片存储到数据库中

目前还是存储在本地,不安全。

7.3 生成考勤日志

自动统计每个班级的考勤信息,并生成日志。

7.4 从教务系统导入学生个人课表,保证判定的稳定性

目前仅仅通过设定的考勤时间统计,不够人性化,应该根据学生的各任课表来统计。

7.5 同时多人识别

提升识别效率,比如很多同学同时进教室,如果一人一人识别,则会造成拥堵。

7.6 上传图片识别

如果系统出现故障,老师可以用手机拍摄照片存档,然后上传系统进行人脸识别考勤。

7.7 开发更稳定的人脸识别

防止用照片或视频骗过系统。

7.8 开发更稳定的活体检测

防止视频等骗过系统。


8. 问题交流👏

由于个人精力有限,无法及时回复,还请谅解。近期创建了QQ群,欢迎加入,有问题大家一起交流! 群号:1062310557 二维码:
在这里插入图片描述


公众号

欢迎关注公众号,获取更多干货内容!
在这里插入图片描述



2019.04.14 更新:活体检测功能存在bug

主要表现是:人脸识别开启状态下,再开启活体检测,可能界面卡死。


2020.03.26 更新:Github压缩包解压失败

压缩包大概32.5MB,解压失败一般是没下载完成导致的,我下载了以便,不科学方式下载确实慢,如果等不及可以留下邮箱或者加交流群自取。


2020.03.27 更新:关于第一次运行崩溃的问题

请参看上文第五章。


2020.03.27 更新:Navicat使用

请参看上文第三章第一节。


2020.05.22 更新:所需包简洁安装流程

opencv+contrib
安装步骤:
1.https://www.lfd.uci.edu/~gohlke/pythonlibs/ 搜索contrib
2.找到对应你系统python版本的opencv+contrib下载
3.我安装的是:opencv_python-4.1.2+contrib-cp37-cp37m-win_amd64.whl
4.打开anaconda命令行 pip install opencv_python-4.1.2+contrib-cp37-cp37m-win_amd64.whl

cmake
官网下载.msi安装包 下载即可,安装注意导入系统环境变量

dilib
直接anaconda命令行中 pip install dlib(时间比较长)

freetype
pip install freetype-py

pymysql
pip install pymysql

pyqt5
pip install pyqt5

2020.05.24 更新:数据库各表字段及需要下载的文件说明

首先需要下载caffemodel和dat文件,并放到图示路径【已经上传群文件】。然后修改人脸照片存放路径。
在这里插入图片描述
各表字段说明:

  1. studentnums:
    在这里插入图片描述
  2. students
    在这里插入图片描述
    3.checkin
    在这里插入图片描述

2020.05.25 更新:测试与bug描述

在这里插入图片描述

目前的bug是,活体检测开启关闭之后,关闭人脸考勤,再关闭相机的时候会卡死。


看到这了都,白嫖好意思吗(doge)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页