基於人臉識別的考勤系統:Python3 + Qt5 + OpenCV3 + DeepFace + MySQL


問題彙總(持續更新🧭···)

在文末。點擊目錄,跳轉到相應問題章節。在將本項目代碼開源之後,收到了很多小夥伴的提問,於是將常見的問題在此彙總。


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是,活體檢測開啓關閉之後,關閉人臉考勤,再關閉相機的時候會卡死。


如果幫到你了,麻煩github點一下star,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章