使用Python實現基於人臉識別的上課考勤系統(一):數據錄入端

一、簡介

這個人臉識別考勤簽到系統是基於大佬的人臉識別陌生人報警系統二次開發的。
此處放一個大佬原項目GitHub鏈接:基於OpenCV的視頻人臉識別【陌生人報警】系統
項目使用Python實現,基於OpenCV框架進行人臉識別和攝像頭硬件調用,同時也用OpenCV工具包處理圖片。交互界面使用pyqt5實現。
本項目的GitHub源碼鏈接:基於人臉識別的上課【考勤簽到】系統

該系統實現了從學生信息輸入、人臉數據錄入、人臉數據訓練,學生信息多條件搜索、修改,多選刪除,人臉數據訓練,人臉識別、追蹤、簽到等完整流程的各項功能。甚至允許生成簽到表格和導出Excel格式簽到表。

根據功能分配,系統分爲三個部分實現各部分流程,錄入端負責數據導入,管理端負責數據刪改查以及人臉數據訓練,監控端負責人臉識別以及簽到功能。

監控端使用界面如圖所示:
在這裏插入圖片描述

管理端使用界面如圖所示:
在這裏插入圖片描述

錄入端使用界面如圖所示:
在這裏插入圖片描述

二、二次開發過程

本系統在原始項目的框架基礎上做了大量修改,針對系統功能的不同以及部分模塊實現的不完整做了補充和優化。原項目在功能上實現了完整的人臉數據錄入過程,並已經存在數據管理、數據錄入、核心(Core)三個基本模塊。

1.監控端修改部分
(1)人臉識別實現
在主要部分【核心】的實現上,原項目採用LBPH實現了人臉識別的基本功能,並使用haar-like實現人臉位置捕獲,使用dlib目標追蹤器實現同一畫面下多個人臉的目標追蹤,嘗試主要追蹤,次要捕獲的方式優化人臉捕獲的過程。識別速度與畫面幀率很高,但識別準確率並不理想,並且嚴重受到光照條件影響。【實際使用時證明,若錄入人臉數據時存在單方面光源照射,在識別時,光源位置一旦改變就完全識別不出來了】 並且錄入數據時需要大量人臉數據集【100張人臉圖以上】,才能獲得較高的識別置信度。

二次開發後,保留原本速度較快的LBPH人臉識別,新增效果更好的dlib_face_recognition_resnet_model深度學習殘差網絡識別模型。實際使用時發現,雖然實時視頻流幀率明顯降低,但是識別準確率大大提高。

(2)攝像頭啓動與關閉
在原項目中,在攝像頭調用的設計上,錄入端和監控端同樣都是用了攝像頭,並進行人臉捕獲,但監控端只允許打開和關閉一次攝像頭,之後就禁用了攝像頭控制按鈕,而錄入端則允許隨意多次的打開和關閉攝像頭。

其原因是具體實現上,因爲監控端需要啓動單獨線程執行人臉識別任務,而錄入端沒有人臉識別的複雜處理過程,在一個線程中就可以實現需要的功能。監控端開啓攝像頭後,同時啓動人臉識別線程處理單幀圖像,並啓動報警監聽線程,設計思路上是報警線程一旦開啓,全程保持監聽,直到程序關閉,而關閉攝像頭時代碼實現上只關閉,也只能夠關閉人臉識別線程,對於報警監聽線程無法控制。導致如果容許攝像頭二次開啓,人臉識別線程能夠啓動,但作爲同一個線程實例的報警監聽模塊將被二次啓動,導致程序出錯。

這裏的人臉識別線程繼承自QTread類,允許使用.stop()函數控制線程實例的開始與終止,而報警監聽模塊則是普通的threading.Thread(),開啓後無法控制關閉,導致出現上述問題。改進方法是將報警監聽線程使用QTread實現,使用與人臉識別線程相同的控制方法,攝像頭開啓時啓動,攝像頭關閉時終止,實現二次啓動功能。

(3)報警系統改爲簽到系統
因爲系統的功能修改前後有所改變,但是實現的技術實際上是一樣的。原始項目的報警功能實際上可以作爲簽到系統的監聽線程。原始代碼邏輯爲,發現置信度低於閾值的臉即陌生人臉,隨機人臉識別線程通過通信隊列告知報警系統執行截圖拍攝、消息推送、報警響鈴等功能。改變爲簽到系統後,人臉識別線程將置信度閾值以上【即數據庫中存在並認爲可信的臉】,且匹配度最高的人臉作爲簽到信號,通過隊列通信發送給簽到線程,並執行之前沒有的數據庫錄入操作。

簡單來說就是,之前是把認不出來的臉進行記錄和報警,轉變爲認出來且最像的人臉進行記錄和執行聲音提示。

(4)簽到表格的創建
這個屬於簽到系統特有的新增功能,說道考勤簽到就一定會想到教師用來點名的簽到表,因此該簽到系統需要生成一個包含當前課程需要簽到的所有學生信息,通過預處理進行創建,並在人臉識別過程中實時修改記錄簽到信息,主要內容爲學生姓名、學號、簽到時間。該功能內建了一個基於MySQL查詢的QTableWidget控件,用於方便用戶之間從數據庫中選擇並創建需要簽到的學生名單。

2.管理端修改部分
(1)學生信息管理
原始的管理端非常簡陋,存儲信息僅包含學生姓名、faceID、學號這樣的普通信息,想要查詢學生信息也只能進行單人查詢,並且只能通過學號,查詢的唯一目的就是通過學號刪除查詢結果。因此無論是查詢還是刪除都非常簡陋。甚至根本不存在修改信息的功能。

修改後的管理端新增了多條件模糊查詢,並且大大增加了信息維數,允許直接雙擊修改學生信息,並實時同步到數據庫中。同時刪除信息的功能也和信息查詢分離開來,信息查詢結果動態顯示在QTableWidget控件中【與原項目使用的技術相同,只是做了更大的功能擴展】。支持用戶多選刪除,不不必“查一個,刪一個”。

(2)人臉數據訓練
原項目中的人臉數據訓練功能同樣集成在管理端中,原作者自定義了一個讀取數據集並將其與學生通過faceID進行唯一匹配的函數,也就是LBPH數據訓練中的人臉數據(faces)與分類標籤(labels)。讀取並整理成LBPH.train()所需要的數據結構後即可直接將數據作爲參數調用封裝好的函數進行訓練。

值得一提的是,LBPH的數據訓練非常快,即使在三百人的,每人人臉數據集平均20張的情況下,訓練時間依然能控制在十幾秒內【但是結果其實並不好】,因此原作者並沒有將其作爲單獨的線程來執行,而是直接甩給用戶一個提示:訓練期間系統窗口可能會無響應,請耐心等待。。。

這樣的結果是導致用戶體驗極差,會以爲程序崩了,其實只是訓練計算時間過長,導致windows消息監聽一段時間內無迴應,被認爲是程序無響應。。。

二次開發後我將其獨立爲一個線程單獨執行,併爲執行函數增加了進度條,讓用戶直觀的看到訓練過程。順便,在代碼實現過程中,發現大量時間實際上消耗在圖片數據集的讀取上,訓練的過程反倒沒有那麼久,因此進度條實際展示的是讀取過程,邏輯上是先進行的數據讀取,再計算特徵值,然後繼續讀取下一個數據集,因此將數據讀取作爲進度來衡量不會有時間上的偏差。

3.錄入端修改內容
原項目的錄入端實際上提供了一個人臉偵測以及拍照的功能,剩下的就是簡單的文本框信息輸入,並提交到數據庫的功能,人臉偵測使用的和監控端一樣是haar-like特徵提取,拍照是用戶點擊按鈕,系統直接調用OpenCV內置的圖片存儲功能,將單幀圖片寫入數據集(也就是以學號命名的文件夾)。實際使用過程中發現這樣的功能雖然非常方便,但是在大量人數的錄入時效率不高,想象一下,一個年級三百多人,一個一個錄入要手動敲擊鍵盤輸入信息,沒人要親自在攝像頭前拍攝100張不同角度的照片,工作量巨大。

因此二次開發後新增了數據批量錄入的功能,允許用戶之間導入Excel表格,之間將表格信息整理後提交到數據庫中,並通過學號創建空的數據集【也就是一個空文件夾,方便後續人臉數據的導入】。實際上就將人臉錄入與信息錄入的過程分離開來,在系統設計上仍然秉持着將學號作爲人臉和信息唯一匹配的思路。而人臉數據集批量錄入可以接受用學號明明的單張照片,直接分類到各個已經創建好的數據集中。或是手動輸入單個學生信息,然後批量導入這個學生的所有照片。

批量導入功能最大限度的繞過了單人拍攝,單人輸入的使用方式,能夠將錄入效率大大提高。


三、代碼具體實現
這裏就簡單貼一下數據錄入端的代碼。項目完整代碼還請移步本文開始位置的GitHub鏈接。

#!/usr/bin/env python3
# Author: kuronekonano <[email protected]>
# 人臉信息錄入
import re
import string
import time

import cv2
import pymysql
import shutil

from PyQt5.QtCore import QTimer, QRegExp, pyqtSignal, QThread
from PyQt5.QtGui import QImage, QPixmap, QIcon, QRegExpValidator, QTextCursor
from PyQt5.QtWidgets import QDialog, QApplication, QWidget, QMessageBox, QFileDialog, QProgressBar
from PyQt5.uic import loadUi

import logging
import logging.config
import queue
import threading
import os
import sys
import xlrd
import random

from datetime import datetime


# 用戶取消了更新數據庫操作
class OperationCancel(Exception):
    pass


# 採集過程中出現干擾
class RecordDisturbance(Exception):
    pass


class DataRecordUI(QWidget):
    receiveLogSignal = pyqtSignal(str)
    messagebox_signal = pyqtSignal(dict)

    # 日誌隊列
    logQueue = queue.Queue()

    def __init__(self):
        super(DataRecordUI, self).__init__()
        loadUi('./ui/DataRecord.ui', self)  # 讀取UI佈局
        self.setWindowIcon(QIcon('./icons/icon.png'))
        self.setFixedSize(1528, 856)

        # OpenCV
        # 攝像頭
        self.cap = cv2.VideoCapture()
        # 分類器
        self.faceCascade = cv2.CascadeClassifier('./haarcascades/haarcascade_frontalface_default.xml')

        # 圖像捕獲
        self.isExternalCameraUsed = False
        self.useExternalCameraCheckBox.stateChanged.connect(
            lambda: self.useExternalCamera(self.useExternalCameraCheckBox))

        self.startWebcamButton.toggled.connect(self.startWebcam)
        self.startWebcamButton.setCheckable(True)

        # 定時器
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.updateFrame)

        # 人臉檢測
        self.isFaceDetectEnabled = False
        self.enableFaceDetectButton.toggled.connect(self.enableFaceDetect)
        self.enableFaceDetectButton.setCheckable(True)

        # 數據庫
        # self.database = 'users'
        self.datasets = './datasets'
        self.isDbReady = False
        self.initDbButton.setIcon(QIcon('./icons/warning.png'))
        self.initDbButton.clicked.connect(self.initDb)

        # 用戶信息
        self.isUserInfoReady = False
        self.userInfo = {'stu_id': '',
                         'cn_name': '',
                         'en_name': '',
                         'stu_grade': '',
                         'stu_class': '',
                         'stu_sex': '',
                         'major': ''}
        self.addOrUpdateUserInfoButton.clicked.connect(self.addOrUpdateUserInfo)
        self.migrateToDbButton.clicked.connect(self.migrateToDb)  # 插入新數據按鍵綁定

        # 人臉採集
        self.startFaceRecordButton.clicked.connect(
            lambda: self.startFaceRecord(self.startFaceRecordButton))  # 開始人臉採集按鈕綁定,並傳入按鈕本身用於結束狀態控制
        # self.startFaceRecordButton.setCheckable(True)
        self.faceRecordCount = 0  # 已採集照片計數器
        self.minFaceRecordCount = 100  # 最少採集照片數量
        self.isFaceDataReady = False
        self.isFaceRecordEnabled = False
        self.enableFaceRecordButton.clicked.connect(self.enableFaceRecord)  # 按鍵綁定錄入單幀圖像

        # 日誌系統
        self.receiveLogSignal.connect(lambda log: self.logOutput(log))  # pyqtsignal信號綁定
        self.messagebox_signal.connect(lambda log: self.message_output(log))
        self.logOutputThread = threading.Thread(target=self.receiveLog, daemon=True)
        self.logOutputThread.start()

        # 批量導入
        self.isImage_path_ready = False
        # self.ImagepathButton.clicked.connect(self.import_images_data)  # 使用同一線程會導致窗口無響應
        self.ImagepathButton.clicked.connect(self.import_image_thread)  # 使用多線程實現圖片導入
        self.isExcel_path_ready = False
        self.ExcelpathButton.clicked.connect(self.import_excel_data)
        self.ImportPersonButton.clicked.connect(self.person_import_thread)

    @staticmethod
    def connect_to_sql():
        conn = pymysql.connect(host='localhost',
                               user='root',
                               password='******',
                               db='mytest',
                               port=3306,
                               charset='utf8')
        cursor = conn.cursor()
        return conn, cursor

    # 單人導入圖片集【主線程】棄用
    def import_person_imageset(self):
        if self.isUserInfoReady:  # 學生信息確認
            stu_id = self.userInfo.get('stu_id')
            self.ImportPersonButton.setIcon(QIcon('./icons/success.png'))
            image_paths = QFileDialog.getOpenFileNames(self, '選擇圖片',
                                                       "./",
                                                       'JEPG files(*.jpg);;PNG files(*.PNG)')
            if not os.path.exists('{}/stu_{}'.format(self.datasets, stu_id)):
                os.makedirs('{}/stu_{}'.format(self.datasets, stu_id))
            image_paths = image_paths[0]
            for index, path in enumerate(image_paths):
                try:
                    img = cv2.imread(path)
                    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 灰度圖
                    faces = self.faceCascade.detectMultiScale(gray, 1.3, 5, minSize=(90, 90))  # 分類器偵測人臉
                    if len(faces) == 0:
                        self.logQueue.put('圖片{}中沒有檢測到人臉!'.format(path))
                        continue
                    for (x, y, w, h) in faces:
                        if len(faces) > 1:
                            raise RecordDisturbance
                        cv2.imwrite('{}/stu_{}/img.{}-{}.jpg'.format(self.datasets, stu_id, index, ''.join(
                            random.sample(string.ascii_letters + string.digits, 4))),
                                    img[y - 20:y + h + 20, x - 20:x + w + 20])  # 灰度圖的人臉區域
                except RecordDisturbance:
                    logging.error('檢測到多張人臉或環境干擾')
                    self.logQueue.put('Warning:檢測到圖片{}存在多張人臉或環境干擾,已忽略。'.format(path))
                    continue
                except Exception as e:
                    logging.error('寫入人臉圖像文件到計算機過程中發生異常')
                    self.logQueue.put('Error:無法保存人臉圖像,導入該圖片失敗')
                    print(e)
            self.migrateToDbButton.setEnabled(True)  # 允許提交至數據庫
            self.isFaceDataReady = True
        else:
            self.ImportPersonButton.setIcon(QIcon('./icons/error.png'))
            self.ImportPersonButton.setChecked(False)
            self.logQueue.put('Error:操作失敗,系統未檢測到有效的用戶信息')

    # 表格導入學生信息
    def import_excel_data(self):

        excel_paths = QFileDialog.getOpenFileNames(self, '選擇表格',
                                                   "./",
                                                   'EXCEL 文件 (*.xlsx;*.xls;*.xlm;*.xlt;*.xlsm;*.xla)')
        excel_paths = excel_paths[0]
        conn, cursor = self.connect_to_sql()
        error_count = 0
        for path in excel_paths:
            sheets_file = xlrd.open_workbook(path)
            for index, sheet in enumerate(sheets_file.sheets()):
                self.logQueue.put("正在讀取文件:" + str(path) + "的第" + str(index) + "個sheet表的內容...")
                for row in range(sheet.nrows):
                    row_data = sheet.row_values(row)
                    if row_data[1] == '姓名':
                        continue
                    self.userInfo['stu_id'] = row_data[4]
                    self.userInfo['cn_name'] = row_data[1]
                    self.userInfo['en_name'] = row_data[0]
                    self.userInfo['stu_grade'] = '20' + self.userInfo['stu_id'][:2]
                    self.userInfo['stu_class'] = row_data[3].rsplit('-', 1)[1]
                    self.userInfo['stu_sex'] = row_data[5]
                    self.userInfo['major'] = row_data[2]
                    self.userInfo['province'] = row_data[-1]
                    self.userInfo['nation'] = row_data[-2]
                    # print(self.userInfo)
                    try:
                        stu_id = row_data[4]
                        if not os.path.exists('{}/stu_{}'.format(self.datasets, stu_id)):
                            os.makedirs('{}/stu_{}'.format(self.datasets, stu_id))
                        db_user_count = self.commit_to_database(cursor)
                        self.dbUserCountLcdNum.display(db_user_count)  # 數據庫人數計數器
                    except OperationCancel:
                        pass
                    except Exception as e:
                        print(e)
                        logging.error('讀寫數據庫異常,無法向數據庫插入/更新記錄')
                        self.logQueue.put('Error:讀寫數據庫異常,同步失敗')
                        error_count += 1
                self.logQueue.put('導入完畢!其中導入失敗 {} 條信息'.format(error_count))

        cursor.close()
        conn.commit()
        conn.close()

    # 啓用新線程導入圖片,並添加進度條
    def import_image_thread(self):
        self.image_paths = QFileDialog.getOpenFileNames(self, '選擇圖片',
                                                        "./",
                                                        'JEPG files(*.jpg);;PNG files(*.PNG)')
        self.image_paths = self.image_paths[0]
        if len(self.image_paths) != 0:  # 點擊導入但是沒有選擇文件時不需啓動線程
            progress_bar = ActionsImportImage(self)
        print('import success!')

    # 啓用新線程 單人圖片導入 使用進度條
    def person_import_thread(self):
        if self.isUserInfoReady:  # 學生信息確認
            stu_id = self.userInfo.get('stu_id')
            self.ImportPersonButton.setIcon(QIcon('./icons/success.png'))

            image_paths = QFileDialog.getOpenFileNames(self, '選擇圖片',
                                                       "./",
                                                       'JEPG files(*.jpg);;PNG files(*.PNG)')
            self.image_paths = image_paths[0]
            if len(self.image_paths) != 0:  # 點擊導入但是沒有選擇文件時不需啓動線程
                if not os.path.exists('{}/stu_{}'.format(self.datasets, stu_id)):
                    os.makedirs('{}/stu_{}'.format(self.datasets, stu_id))
                progress_bar = ActionsPersonImport(self)
                self.migrateToDbButton.setEnabled(True)  # 允許提交至數據庫
                self.isFaceDataReady = True

        else:
            self.ImportPersonButton.setIcon(QIcon('./icons/error.png'))
            self.ImportPersonButton.setChecked(False)
            self.logQueue.put('Error:操作失敗,系統未檢測到有效的用戶信息')

    # 圖片批量導入【主線程】棄用
    def import_images_data(self):
        image_paths = QFileDialog.getOpenFileNames(self, '選擇圖片',
                                                   "./",
                                                   'JEPG files(*.jpg);;PNG files(*.PNG)')
        image_paths = image_paths[0]
        error_count = 0
        self.logQueue.put('開始讀取圖片數據...')
        for index, path in enumerate(image_paths):
            stu_id = os.path.split(path)[1].split('.')[0]
            # print(stu_id)
            if not os.path.exists('{}/stu_{}'.format(self.datasets, stu_id)):
                text = '命名錯誤!'
                informativeText = '<b>文件 <font color=red>{}</font> 存在問題,數據庫中沒有以該圖片名爲學號的用戶。</b>'.format(path)
                DataRecordUI.callDialog(QMessageBox.Critical, text, informativeText, QMessageBox.Ok)
                error_count += 1
                continue
            dstpath = '{}/stu_{}/img.{}.jpg'.format(self.datasets, stu_id, stu_id + '-0')
            try:
                shutil.copy(path, dstpath)
            except:
                text = '命名格式錯誤!'
                informativeText = '<b>文件 <font color=red>{}</font> 命名格式不正確。</b>'.format(path)
                DataRecordUI.callDialog(QMessageBox.Critical, text, informativeText, QMessageBox.Ok)
                error_count += 1
        self.logQueue.put('圖片批量導入完成!其中導入失敗 {} 張圖片'.format(error_count))

    # 是否使用外接攝像頭
    def useExternalCamera(self, useExternalCameraCheckBox):
        if useExternalCameraCheckBox.isChecked():
            self.isExternalCameraUsed = True
        else:
            self.isExternalCameraUsed = False

    # 打開/關閉攝像頭
    def startWebcam(self, status):
        if status:
            if not self.cap.isOpened():
                camID = 1 if self.isExternalCameraUsed else 0 + cv2.CAP_DSHOW
                self.cap.open(camID)
                self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
                self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
                ret, frame = self.cap.read()  # 獲取攝像頭調用結果

                if not ret:
                    logging.error('無法調用電腦攝像頭{}'.format(camID))
                    self.logQueue.put('Error:初始化攝像頭失敗')
                    self.cap.release()
                    self.startWebcamButton.setIcon(QIcon('./icons/error.png'))
                    self.startWebcamButton.setChecked(False)
                else:
                    self.timer.start(5)
                    self.enableFaceDetectButton.setEnabled(True)
                    self.startWebcamButton.setIcon(QIcon('./icons/success.png'))
                    self.startWebcamButton.setText('關閉攝像頭')
        else:
            if self.cap.isOpened():
                if self.timer.isActive():
                    self.timer.stop()
                self.cap.release()
                self.faceDetectCaptureLabel.clear()
                self.faceDetectCaptureLabel.setText('<font color=red>攝像頭未開啓</font>')
                self.startWebcamButton.setText('打開攝像頭')
                self.enableFaceDetectButton.setEnabled(False)
                self.startWebcamButton.setIcon(QIcon())

    # 開啓/關閉人臉檢測
    def enableFaceDetect(self, status):
        if self.cap.isOpened():
            if status:
                self.enableFaceDetectButton.setText('關閉人臉檢測')
                self.isFaceDetectEnabled = True
            else:
                self.enableFaceDetectButton.setText('開啓人臉檢測')
                self.isFaceDetectEnabled = False

    # 採集當前捕獲幀
    def enableFaceRecord(self):
        if not self.isFaceRecordEnabled:
            self.isFaceRecordEnabled = True

    # 開始/結束採集人臉數據
    def startFaceRecord(self, startFaceRecordButton):
        if startFaceRecordButton.text() == '開始採集人臉數據':  # 只能用==判斷,不能用is
            if self.isFaceDetectEnabled:
                if self.isUserInfoReady:  # 學生信息確認
                    self.addOrUpdateUserInfoButton.setEnabled(False)  # 採集人臉數據時禁用修改學生信息
                    if not self.enableFaceRecordButton.isEnabled():  # 啓用單幀採集按鈕
                        self.enableFaceRecordButton.setEnabled(True)
                    self.enableFaceRecordButton.setIcon(QIcon())
                    self.startFaceRecordButton.setIcon(QIcon('./icons/success.png'))
                    self.startFaceRecordButton.setText('結束當前人臉採集')  # 開始採集按鈕狀態修改爲結束採集
                else:
                    self.startFaceRecordButton.setIcon(QIcon('./icons/error.png'))
                    self.startFaceRecordButton.setChecked(False)
                    self.logQueue.put('Error:操作失敗,系統未檢測到有效的用戶信息')
            else:
                self.startFaceRecordButton.setIcon(QIcon('./icons/error.png'))
                self.logQueue.put('Error:操作失敗,請開啓人臉檢測')
        else:  # 根據按鈕文本信息判斷是結束採集還是開始採集
            if self.faceRecordCount < self.minFaceRecordCount:
                text = '系統當前採集了 <font color=blue>{}</font> 幀圖像,採集數據過少會導致較大的識別誤差。'.format(self.faceRecordCount)
                informativeText = '<b>請至少採集 <font color=red>{}</font> 幀圖像。</b>'.format(self.minFaceRecordCount)
                DataRecordUI.callDialog(QMessageBox.Information, text, informativeText, QMessageBox.Ok)

            else:
                text = '系統當前採集了 <font color=blue>{}</font> 幀圖像,繼續採集可以提高識別準確率。'.format(self.faceRecordCount)
                informativeText = '<b>你確定結束當前人臉採集嗎?</b>'
                ret = DataRecordUI.callDialog(QMessageBox.Question, text, informativeText,
                                              QMessageBox.Yes | QMessageBox.No,
                                              QMessageBox.No)

                if ret == QMessageBox.Yes:
                    self.isFaceDataReady = True  # 結束採集,人臉數據準備完畢
                    if self.isFaceRecordEnabled:
                        self.isFaceRecordEnabled = False
                    self.enableFaceRecordButton.setEnabled(False)  # 結束採集,單幀採集按鈕禁用
                    self.enableFaceRecordButton.setIcon(QIcon())
                    self.startFaceRecordButton.setText('開始採集人臉數據')  # 修改按鈕文本爲開始狀態
                    self.startFaceRecordButton.setEnabled(False)  # 不可重新開始採集
                    self.startFaceRecordButton.setIcon(QIcon())
                    self.migrateToDbButton.setEnabled(True)  # 允許提交至數據庫

    # 定時器,實時更新畫面
    def updateFrame(self):
        ret, frame = self.cap.read()
        # frame = cv2.flip(frame, 1)  # 水平翻轉圖片
        if ret:
            # self.displayImage(frame)  # ?兩次輸出?

            if self.isFaceDetectEnabled:  # 人臉檢測
                detected_frame = self.detectFace(frame)
                self.displayImage(detected_frame)
            else:
                self.displayImage(frame)

    # 檢測人臉
    def detectFace(self, frame):
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 灰度圖
        faces = self.faceCascade.detectMultiScale(gray, 1.3, 5, minSize=(90, 90))  # 分類器偵測人臉
        # 1.image爲輸入的灰度圖像
        # 2.objects爲得到被檢測物體的矩形框向量組
        # 3.scaleFactor爲每一個圖像尺度中的尺度參數,默認值爲1.1。scale_factor參數可以決定兩個不同大小的窗口掃描之間有多大的跳躍,
        # 這個參數設置的大,則意味着計算會變快,但如果窗口錯過了某個大小的人臉,則可能丟失物體。
        # 4.minNeighbors參數爲每一個級聯矩形應該保留的鄰近個數,默認爲3。
        # minNeighbors控制着誤檢測,默認值爲3表明至少有3次重疊檢測,我們才認爲人臉確實存。
        # 6.cvSize()指示尋找人臉的最小區域。設置這個參數過大,會以丟失小物體爲代價減少計算量。

        stu_id = self.userInfo.get('stu_id')

        #  遍歷所有人臉,只允許有一個人的臉
        for (x, y, w, h) in faces:
            if self.isFaceRecordEnabled:
                try:  # 創建學號對應的圖片數據集
                    if not os.path.exists('{}/stu_{}'.format(self.datasets, stu_id)):
                        os.makedirs('{}/stu_{}'.format(self.datasets, stu_id))
                    if len(faces) > 1:
                        raise RecordDisturbance

                    cv2.imwrite('{}/stu_{}/img.{}.jpg'.format(self.datasets, stu_id, self.faceRecordCount + 1),
                                frame[y - 20:y + h + 20, x - 20:x + w + 20])  # 灰度圖的人臉區域
                except RecordDisturbance:
                    self.isFaceRecordEnabled = False
                    logging.error('檢測到多張人臉或環境干擾')
                    self.logQueue.put('Warning:檢測到多張人臉或環境干擾,請解決問題後繼續')
                    self.enableFaceRecordButton.setIcon(QIcon('./icons/warning.png'))
                    continue
                except Exception as e:
                    logging.error('寫入人臉圖像文件到計算機過程中發生異常')
                    self.enableFaceRecordButton.setIcon(QIcon('./icons/error.png'))
                    self.logQueue.put('Error:無法保存人臉圖像,採集當前捕獲幀失敗')
                else:
                    self.enableFaceRecordButton.setIcon(QIcon('./icons/success.png'))
                    self.faceRecordCount = self.faceRecordCount + 1
                    self.isFaceRecordEnabled = False  # 單幀拍攝完成後馬上關閉
                    self.faceRecordCountLcdNum.display(self.faceRecordCount)  # 更新採集數量
            cv2.rectangle(frame, (x - 5, y - 10), (x + w + 5, y + h + 10), (0, 0, 255), 2)  # 紅色追蹤框

        return frame

    # 顯示圖像
    def displayImage(self, img):
        # BGR -> RGB
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        # default:The image is stored using 8-bit indexes into a colormap, for example:a gray image
        # img = cv2.flip(img, 1)
        qformat = QImage.Format_Indexed8

        if len(img.shape) == 3:  # rows[0], cols[1], channels[2]
            if img.shape[2] == 4:
                # The image is stored using a 32-bit byte-ordered RGBA format (8-8-8-8)
                # A: alpha channel,不透明度參數。如果一個像素的alpha通道數值爲0%,那它就是完全透明的
                qformat = QImage.Format_RGBA8888
            else:
                qformat = QImage.Format_RGB888

        # img.shape[1]:圖像寬度width,img.shape[0]:圖像高度height,img.shape[2]:圖像通道數
        # QImage.__init__ (self, bytes data, int width, int height, int bytesPerLine, Format format)
        # 從內存緩衝流獲取img數據構造QImage類
        # img.strides[0]:每行的字節數(width*3),rgb爲3,rgba爲4
        # strides[0]爲最外層(即一個二維數組所佔的字節長度),strides[1]爲次外層(即一維數組所佔字節長度),strides[2]爲最內層(即一個元素所佔字節長度)
        # 從裏往外看,strides[2]爲1個字節長度(uint8),strides[1]爲3*1個字節長度(3即rgb 3個通道)
        # strides[0]爲width*3個字節長度,width代表一行有幾個像素

        outImage = QImage(img, img.shape[1], img.shape[0], img.strides[0], qformat)
        self.faceDetectCaptureLabel.setPixmap(QPixmap.fromImage(outImage))
        self.faceDetectCaptureLabel.setScaledContents(True)  # 圖片自適應大小

    # 檢查數據庫表是否存在
    @staticmethod
    def table_exists(cur, table_name):
        sql = "show tables;"
        cur.execute(sql)
        tables = [cur.fetchall()]
        table_list = re.findall('(\'.*?\')', str(tables))
        table_list = [re.sub("'", '', each) for each in table_list]
        if table_name in table_list:
            return True  # 存在返回1
        else:
            return False  # 不存在返回0

    # 檢查數據庫
    def initDb(self):
        conn, cursor = self.connect_to_sql()

        try:
            if not self.table_exists(cursor, 'users'):
                create_table_sql = '''CREATE TABLE IF NOT EXISTS users (
                                              stu_id VARCHAR(20) PRIMARY KEY NOT NULL,
                                              face_id INTEGER DEFAULT -1,
                                              cn_name VARCHAR(30) NOT NULL,
                                              en_name VARCHAR(30) NOT NULL,
                                              major VARCHAR(40) NOT NULL,
                                              grade int(5) DEFAULT NULL,
                                              class int(5) DEFAULT NULL,
                                              sex int(2) DEFAULT NULL,
                                              province VARCHAR(40) NOT NULL,
                                              nation VARCHAR(40) NOT NULL,
                                              total_course_count INT DEFAULT 0,
                                              total_attendance_times INT NOT NULL DEFAULT 0,
                                              created_time DATETIME DEFAULT CURRENT_TIMESTAMP
                                              )
                                          '''
                cursor.execute(create_table_sql)
            # 查詢數據表記錄數
            cursor.execute('SELECT Count(*) FROM users')
            result = cursor.fetchone()
            db_user_count = result[0]
        except Exception as e:
            logging.error('讀取數據庫異常,無法完成數據庫初始化')
            self.isDbReady = False
            self.initDbButton.setIcon(QIcon('./icons/error.png'))
            self.logQueue.put('Error:初始化數據庫失敗')
            print(e)
        else:
            self.isDbReady = True
            self.dbUserCountLcdNum.display(db_user_count)
            self.logQueue.put('Success:數據庫初始化完成')
            self.initDbButton.setIcon(QIcon('./icons/success.png'))
            self.initDbButton.setEnabled(False)
            self.addOrUpdateUserInfoButton.setEnabled(True)
            self.ExcelpathButton.setEnabled(True)
            self.ImagepathButton.setEnabled(True)
        finally:
            cursor.close()
            conn.commit()
            conn.close()

    # 通過對話框輸入增加/修改用戶信息
    def addOrUpdateUserInfo(self):

        self.userInfoDialog = UserInfoDialog()  # 用戶信息窗口實例

        # 獲取上次輸入內容
        stu_id = self.userInfo.get('stu_id')
        cn_name = self.userInfo.get('cn_name')
        en_name = self.userInfo.get('en_name')
        major = self.userInfo.get('major')
        stu_grade = self.userInfo.get('stu_grade')
        stu_class = self.userInfo.get('stu_class')
        stu_sex = self.userInfo.get('stu_sex')
        province = self.userInfo.get('province')
        nation = self.userInfo.get('nation')
        # 填充上次輸入內容到對話框中
        self.userInfoDialog.stuIDLineEdit.setText(stu_id)
        self.userInfoDialog.cnNameLineEdit.setText(cn_name)
        self.userInfoDialog.enNameLineEdit.setText(en_name)
        self.userInfoDialog.MajorLineEdit.setText(major)
        self.userInfoDialog.GradeLineEdit.setText(stu_grade)
        self.userInfoDialog.ClassLineEdit.setText(stu_class)
        self.userInfoDialog.SexLineEdit.setText(stu_sex)
        self.userInfoDialog.ProvinceLineEdit.setText(province)
        self.userInfoDialog.NationLineEdit.setText(nation)
        # 保存輸入信息
        self.userInfoDialog.okButton.clicked.connect(self.checkToApplyUserInfo)
        self.userInfoDialog.exec()

    # 校驗用戶信息並提交
    def checkToApplyUserInfo(self):
        # 不符合校驗條件,輸出提示
        if not self.userInfoDialog.stuIDLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的學號輸入有誤,提交失敗,請檢查並重試!</font>')
        elif not self.userInfoDialog.cnNameLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的姓名輸入有誤,提交失敗,請檢查並重試!</font>')
        elif not self.userInfoDialog.enNameLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的英文名輸入有誤,提交失敗,請檢查並重試!</font>')
        elif not self.userInfoDialog.GradeLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的年級輸入有誤,提交失敗,請檢查並重試!</font>')
        elif not self.userInfoDialog.ClassLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的班級輸入有誤,提交失敗,請檢查並重試!</font>')
        elif not self.userInfoDialog.SexLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的性別輸入有誤,提交失敗,請檢查並重試!</font>')
        elif not self.userInfoDialog.MajorLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的專業輸入有誤,提交失敗,請檢查並重試!</font>')
        elif not self.userInfoDialog.ProvinceLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的生源地輸入有誤,提交失敗,請檢查並重試!</font>')
        elif not self.userInfoDialog.NationLineEdit.hasAcceptableInput():
            self.userInfoDialog.msgLabel.setText('<font color=red>你的民族輸入有誤,提交失敗,請檢查並重試!</font>')
        else:
            # 獲取用戶輸入
            self.userInfo['stu_id'] = self.userInfoDialog.stuIDLineEdit.text().strip()
            self.userInfo['cn_name'] = self.userInfoDialog.cnNameLineEdit.text().strip()
            self.userInfo['en_name'] = self.userInfoDialog.enNameLineEdit.text().strip()
            self.userInfo['stu_grade'] = self.userInfoDialog.GradeLineEdit.text().strip()
            self.userInfo['stu_class'] = self.userInfoDialog.ClassLineEdit.text().strip()
            self.userInfo['stu_sex'] = self.userInfoDialog.SexLineEdit.text().strip()
            self.userInfo['major'] = self.userInfoDialog.MajorLineEdit.text().strip()
            self.userInfo['province'] = self.userInfoDialog.ProvinceLineEdit.text().strip()
            self.userInfo['nation'] = self.userInfoDialog.NationLineEdit.text().strip()

            # 錄入端對話框信息確認
            stu_id = self.userInfo.get('stu_id')
            cn_name = self.userInfo.get('cn_name')
            en_name = self.userInfo.get('en_name')
            major = self.userInfo.get('major')
            stu_grade = self.userInfo.get('stu_grade')
            stu_class = self.userInfo.get('stu_class')
            stu_sex = self.userInfo.get('stu_sex')
            province = self.userInfo.get('province')
            nation = self.userInfo.get('nation')

            self.stuIDLineEdit.setText(stu_id)
            self.cnNameLineEdit.setText(cn_name)
            self.enNameLineEdit.setText(en_name)
            self.MajorLineEdit.setText(major)
            self.GradeLineEdit.setText(stu_grade)
            self.ClassLineEdit.setText(stu_class)
            self.SexLineEdit.setText(stu_sex)
            self.ProvinceLineEdit.setText(province)
            self.NationLineEdit.setText(nation)

            # 輸入並保存合法的學生信息後允許使用人臉採集按鈕
            self.isUserInfoReady = True
            if not self.startFaceRecordButton.isEnabled():
                self.startFaceRecordButton.setEnabled(True)
            self.migrateToDbButton.setIcon(QIcon())

            # 關閉對話框
            self.userInfoDialog.close()

    # 提交數據至數據庫
    def commit_to_database(self, cursor):
        stu_id = self.userInfo.get('stu_id')
        cn_name = self.userInfo.get('cn_name')
        en_name = self.userInfo.get('en_name')
        major = self.userInfo.get('major')
        stu_grade = self.userInfo.get('stu_grade')
        stu_class = self.userInfo.get('stu_class')
        stu_sex = 1 if self.userInfo.get('stu_sex') == '男' else 0
        province = self.userInfo.get('province')
        nation = self.userInfo.get('nation')
        # print(stu_sex)
        cursor.execute('SELECT * FROM users WHERE stu_id=%s', (stu_id,))
        if cursor.fetchall():
            text = '數據庫已存在學號爲 <font color=blue>{}</font> 的用戶記錄。'.format(stu_id)
            informativeText = '<b>是否覆蓋?</b>'
            ret = DataRecordUI.callDialog(QMessageBox.Warning, text, informativeText,
                                          QMessageBox.Yes | QMessageBox.No)

            if ret == QMessageBox.Yes:
                # 更新已有記錄
                cursor.execute(
                    'UPDATE users SET cn_name=%s, en_name=%s ,major=%s, grade=%s, class=%s, sex=%s, province=%s, nation=%s WHERE stu_id=%s',
                    (cn_name, en_name, major, stu_grade, stu_class, stu_sex, stu_id, province, nation))
            else:
                raise OperationCancel  # 記錄取消覆蓋操作
        else:
            # 插入新記錄
            cursor.execute(
                'INSERT INTO users (stu_id, cn_name, en_name, major, grade, class, sex, province, nation) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)',
                (stu_id, cn_name, en_name, major, stu_grade, stu_class, stu_sex, province, nation))

        cursor.execute('SELECT Count(*) FROM users')
        result = cursor.fetchone()
        return result[0]

    # 同步用戶信息到數據庫
    def migrateToDb(self):
        # 僅有人臉數據錄入完畢之後才能提交學生信息
        if self.isFaceDataReady:
            stu_id = self.userInfo.get('stu_id')
            cn_name = self.userInfo.get('cn_name')

            conn, cursor = self.connect_to_sql()

            try:
                db_user_count = self.commit_to_database(cursor)
            except OperationCancel:
                pass
            except Exception as e:
                print(e)
                logging.error('讀寫數據庫異常,無法向數據庫插入/更新記錄')
                self.migrateToDbButton.setIcon(QIcon('./icons/error.png'))
                self.logQueue.put('Error:讀寫數據庫異常,同步失敗')
            else:
                text = '<font color=blue>{}</font> 已添加/更新到數據庫。'.format(stu_id)
                informativeText = '<b><font color=blue>{}</font> 的人臉數據採集已完成!</b>'.format(cn_name)
                DataRecordUI.callDialog(QMessageBox.Information, text, informativeText, QMessageBox.Ok)

                # 清空用戶信息緩存
                for key in self.userInfo.keys():
                    self.userInfo[key] = ''
                self.isUserInfoReady = False

                self.faceRecordCount = 0
                self.isFaceDataReady = False  # 人臉信息採集完成標誌
                self.faceRecordCountLcdNum.display(self.faceRecordCount)  # 人臉採集計數器
                self.dbUserCountLcdNum.display(db_user_count)  # 數據庫人數計數器

                # 清空確認信息
                self.stuIDLineEdit.clear()
                self.cnNameLineEdit.clear()
                self.enNameLineEdit.clear()
                self.MajorLineEdit.clear()
                self.GradeLineEdit.clear()
                self.ClassLineEdit.clear()
                self.SexLineEdit.clear()
                self.ProvinceLineEdit.clear()
                self.NationLineEdit.clear()
                self.migrateToDbButton.setIcon(QIcon('./icons/success.png'))

                # 允許繼續增加新用戶
                self.addOrUpdateUserInfoButton.setEnabled(True)
                self.migrateToDbButton.setEnabled(False)

            finally:
                cursor.close()
                conn.commit()
                conn.close()
        else:
            self.logQueue.put('Error:操作失敗,你尚未完成人臉數據採集')
            self.migrateToDbButton.setIcon(QIcon('./icons/error.png'))

    # 系統日誌服務常駐,接收並處理系統日誌
    def receiveLog(self):
        while True:
            data = self.logQueue.get()
            if type(data) == str:
                self.receiveLogSignal.emit(data)
            elif type(data) == dict:
                self.messagebox_signal.emit(data)

    # LOG輸出
    def logOutput(self, log):
        # 獲取當前系統時間
        time = datetime.now().strftime('[%Y/%m/%d %H:%M:%S]')
        log = time + ' ' + log + '\n'

        self.logTextEdit.moveCursor(QTextCursor.End)  # 光標移動至末尾
        self.logTextEdit.insertPlainText(log)  # 末尾插入日誌消息
        self.logTextEdit.ensureCursorVisible()  # 自動滾屏

    @staticmethod
    def message_output(log):
        text, informative_text = log.get('text'), log.get('informativeText')
        # print(text, informative_text)
        DataRecordUI.callDialog(QMessageBox.Information, text, informative_text, QMessageBox.Ok)

    # 系統對話框
    @staticmethod
    def callDialog(icon, text, informativeText, standardButtons, defaultButton=None):
        msg = QMessageBox()
        msg.setWindowIcon(QIcon('./icons/icon.png'))
        msg.setWindowTitle('OpenCV Face Recognition System - DataRecord')
        msg.setIcon(icon)
        msg.setText(text)  # 對話框文本信息
        msg.setInformativeText(informativeText)  # 對話框詳細信息
        msg.setStandardButtons(standardButtons)
        if defaultButton:
            msg.setDefaultButton(defaultButton)
        return msg.exec()

    # 窗口關閉事件,關閉定時器、攝像頭
    def closeEvent(self, event):
        if self.timer.isActive():  # 關閉定時器
            self.timer.stop()
        if self.cap.isOpened():  # 關閉攝像頭
            self.cap.release()
        event.accept()

以上是數據錄入端的界面佈局以及函數關係綁定,同時包含了數據庫連接,人臉偵測與存儲功能。


消息填寫文本框使用一些正則表達式限制用戶輸入符合相關內容的格式,防止有控制不了的信息格式影響後續處理。

# 用戶信息填寫對話框
class UserInfoDialog(QDialog):

    def __init__(self):
        super(UserInfoDialog, self).__init__()
        loadUi('./ui/UserInfoDialog.ui', self)  # 讀取UI佈局
        self.setWindowIcon(QIcon('./icons/icon.png'))
        self.setFixedSize(613, 593)

        # 使用正則表達式限制用戶輸入
        stu_id_regx = QRegExp('^[0-9]{10}$')  # 10位學號,如1604010901
        stu_id_validator = QRegExpValidator(stu_id_regx, self.stuIDLineEdit)
        self.stuIDLineEdit.setValidator(stu_id_validator)

        cn_name_regx = QRegExp('^[\u4e00-\u9fa5]{1,10}$')  # 姓名,只允許輸入漢字
        cn_name_validator = QRegExpValidator(cn_name_regx, self.cnNameLineEdit)
        self.cnNameLineEdit.setValidator(cn_name_validator)

        en_name_regx = QRegExp('^[ A-Za-z]{1,16}$')  # 姓名的英文表示
        en_name_validator = QRegExpValidator(en_name_regx, self.enNameLineEdit)  # Qt校驗器
        self.enNameLineEdit.setValidator(en_name_validator)  # 用於根據正則式限制輸入

        major_regx = QRegExp('^[\u4e00-\u9fa5]{1,20}$')  # 專業,只允許輸入漢字
        major_validator = QRegExpValidator(major_regx, self.MajorLineEdit)
        self.MajorLineEdit.setValidator(major_validator)

        grade_regx = QRegExp('^[0-9]{4}$')  # 年級/入學年份,4位數字
        grade_validator = QRegExpValidator(grade_regx, self.GradeLineEdit)
        self.GradeLineEdit.setValidator(grade_validator)

        class_regx = QRegExp('^[0-9]{1,2}$')  # 年級/入學年份,4位數字
        class_validator = QRegExpValidator(class_regx, self.ClassLineEdit)
        self.ClassLineEdit.setValidator(class_validator)

        sex_regx = QRegExp('^[男|女]{1}$')  # 性別,只允許輸入漢字
        sex_validator = QRegExpValidator(sex_regx, self.SexLineEdit)
        self.SexLineEdit.setValidator(sex_validator)

        province_regx = QRegExp('^[\u4e00-\u9fa5]{1,10}$')  # 生源地,只允許輸入省份全稱
        province_validator = QRegExpValidator(province_regx, self.ProvinceLineEdit)
        self.ProvinceLineEdit.setValidator(province_validator)

        nation_regx = QRegExp('^[\u4e00-\u9fa5]{1,10}$')  # 民族,只允許輸入名族名稱
        nation_validator = QRegExpValidator(nation_regx, self.NationLineEdit)
        self.NationLineEdit.setValidator(nation_validator)

數據導入都是用獨立的線程進行處理,圖片導入會耗費更多的時間,因此使用進度條進行過程展示。

# 圖片導入線程
class ImportImageThread(QThread):
    progress_bar_signal = pyqtSignal(float)

    def __init__(self, DataRecordUI):
        super(ImportImageThread, self).__init__()
        self.data_record = DataRecordUI

    def run(self) -> None:
        images_count = len(self.data_record.image_paths)
        error_count = 0
        DataRecordUI.logQueue.put('正在讀取圖片數據...')
        for index, path in enumerate(self.data_record.image_paths):
            bar = (index + 1) / images_count * 100
            self.progress_bar_signal.emit(bar)
            stu_id = os.path.split(path)[1].split('.')[0]
            # print(stu_id)
            if not os.path.exists('{}/stu_{}'.format(self.data_record.datasets, stu_id)):
                DataRecordUI.logQueue.put('命名錯誤!文件 {} 存在問題,數據庫中沒有以該圖片名爲學號的用戶。'.format(path))
                error_count += 1
                continue
            dstpath = '{}/stu_{}/img{}.jpg'.format(self.data_record.datasets, stu_id, '-0')
            try:
                shutil.copy(path, dstpath)
            except:
                DataRecordUI.logQueue.put('命名格式錯誤!文件 {} 命名格式不正確。'.format(path))
                error_count += 1
        text = '導入完成!' if error_count else '導入成功!'
        informativeText = '<b>圖片批量導入完成!其中導入失敗 <font color=red>{}</font> 張圖片。</b>'.format(error_count)
        message_box = {'text': text, 'informativeText': informativeText}
        DataRecordUI.logQueue.put(message_box)
        print('OK')


# 進度條
class ActionsImportImage(QDialog):
    """
    Simple dialog that consists of a Progress Bar and a Button.
    Clicking on the button results in the start of a timer and
    updates the progress bar.
    """

    def __init__(self, datarecord):
        super(ActionsImportImage, self).__init__()
        self.data_record = datarecord
        self.initUI()

    def initUI(self):
        self.setWindowTitle('圖片正在導入...')
        self.progress = QProgressBar(self)
        self.progress.setGeometry(0, 0, 300, 25)
        self.progress.setMaximum(100)
        self.image_thread = ImportImageThread(self.data_record)  # 導入圖片線程實例
        self.image_thread.progress_bar_signal.connect(self.onCountChanged)  # 信號槽函數綁定
        self.image_thread.start()
        self.exec()
        # 注意此處有坑,進度條對話框應該使用exec()事件循環而不是show(),使用show()與QThread時會導致對話框無法完全結束,後續語句無法執行

    def onCountChanged(self, value):
        self.progress.setValue(int(value + 0.5))
        if int(value + 0.5) >= 100:
            time.sleep(1)
            self.close()


# 單人進度條
class ActionsPersonImport(QDialog):

    def __init__(self, datarecord):
        super(ActionsPersonImport, self).__init__()
        self.data_record = datarecord
        self.initUI()

    def initUI(self):
        self.setWindowTitle('單人圖片正在導入...')
        self.progress = QProgressBar(self)
        self.progress.setGeometry(0, 0, 300, 25)
        self.progress.setMaximum(100)
        self.image_thread = PersonImportThread(self.data_record)  # 導入圖片線程實例
        self.image_thread.progress_bar_signal.connect(self.onCountChanged)  # 信號槽函數綁定
        self.image_thread.start()
        self.exec()

    def onCountChanged(self, value):
        self.progress.setValue(int(value + 0.5))
        if int(value + 0.5) >= 100:
            time.sleep(1)
            self.close()


# 單人圖片導入線程
class PersonImportThread(QThread):
    progress_bar_signal = pyqtSignal(float)

    def __init__(self, DataRecordUI):
        super(PersonImportThread, self).__init__()
        self.data_record = DataRecordUI

    def run(self) -> None:
        images_count = len(self.data_record.image_paths)
        error_count = 0
        for index, path in enumerate(self.data_record.image_paths):
            bar = (index + 1) / images_count * 100
            self.progress_bar_signal.emit(bar)
            # print(index, images_count, bar)
            try:
                img = cv2.imread(path)
                gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 灰度圖
                faces = self.data_record.faceCascade.detectMultiScale(gray, 1.3, 5, minSize=(90, 90))  # 分類器偵測人臉
                if len(faces) == 0:
                    DataRecordUI.logQueue.put('圖片{}中沒有檢測到人臉!'.format(path))
                    continue
                for (x, y, w, h) in faces:
                    if len(faces) > 1:
                        raise RecordDisturbance
                    cv2.imwrite('{}/stu_{}/img.{}-{}.jpg'.format(self.data_record.datasets,
                                                                 self.data_record.userInfo.get('stu_id'), index,
                                                                 ''.join(
                                                                     random.sample(string.ascii_letters + string.digits,
                                                                                   4))),
                                img[y - 20:y + h + 20, x - 20:x + w + 20])  # 灰度圖的人臉區域
            except RecordDisturbance:
                logging.error('檢測到多張人臉或環境干擾')
                DataRecordUI.logQueue.put('Warning:檢測到圖片{}存在多張人臉或環境干擾,已忽略。'.format(path))
                error_count += 1
                continue
            except Exception as e:
                logging.error('寫入人臉圖像文件到計算機過程中發生異常')
                DataRecordUI.logQueue.put('Error:無法保存人臉圖像,導入該圖片失敗')
                error_count += 1
                print(e)

        text = '導入完成!' if error_count else '導入成功!'
        informativeText = '<b>圖片批量導入完成!其中導入失敗 <font color=red>{}</font> 張圖片。</b>'.format(error_count)
        message_box = {'text': text, 'informativeText': informativeText}
        DataRecordUI.logQueue.put(message_box)

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