赞
踩
这个人脸识别考勤签到系统是基于大佬的人脸识别陌生人报警系统二次开发的。
项目使用Python实现,基于OpenCV框架进行人脸识别和摄像头硬件调用,同时也用OpenCV工具包处理图片。交互界面使用pyqt5实现。
该系统实现了从学生信息输入、人脸数据录入、人脸数据训练,学生信息多条件搜索、修改,多选删除,人脸数据训练,人脸识别、追踪、签到等完整流程的各项功能。甚至允许生成签到表格和导出Excel格式签到表。
根据功能分配,系统分为三个部分实现各部分流程,
本系统在原始项目的框架基础上做了大量修改,针对系统功能的不同以及部分模块实现的不完整做了补充和优化。原项目在功能上实现了完整的人脸数据录入过程,并已经存在数据管理、数据录入、核心(Core)三个基本模块。
(1)人脸识别实现
在主要部分【核心】的实现上,原项目采用LBPH实现了人脸识别的基本功能,并使用haar-like实现人脸位置捕获,使用dlib目标追踪器实现同一画面下多个人脸的目标追踪,尝试主要追踪,次要捕获的方式优化人脸捕获的过程。识别速度与画面帧率很高,
但识别准确率并不理想,并且严重受到光照条件影响。【实际使用时证明,若录入人脸数据时存在单方面光源照射,在识别时,光源位置一旦改变就完全识别不出来了】
并且录入数据时需要大量人脸数据集【100张人脸图以上】,才能获得较高的识别置信度。
二次开发后,保留原本速度较快的LBPH人脸识别,新增效果更好的dlib_face_recognition_resnet_model深度学习残差网络识别模型。实际使用时发现,虽然实时视频流帧率明显降低,但是识别准确率大大提高。
(2)摄像头启动与关闭
在原项目中,在摄像头调用的设计上,录入端和监控端同样都是用了摄像头,并进行人脸捕获,但监控端只允许打开和关闭一次摄像头,之后就禁用了摄像头控制按钮,而录入端则允许随意多次的打开和关闭摄像头。
其原因是具体实现上,因为监控端需要启动单独线程执行人脸识别任务,而录入端没有人脸识别的复杂处理过程,在一个线程中就可以实现需要的功能。监控端开启摄像头后,同时启动人脸识别线程处理单帧图像,并启动报警监听线程,设计思路上是报警线程一旦开启,全程保持监听,直到程序关闭,而关闭摄像头时代码实现上只关闭,也只能够关闭人脸识别线程,对于报警监听线程无法控制。导致如果容许摄像头二次开启,人脸识别线程能够启动,但作为同一个线程实例的报警监听模块将被二次启动,导致程序出错。
这里的人脸识别线程继承自QTread类,允许使用.stop()函数控制线程实例的开始与终止,而报警监听模块则是普通的threading.Thread(),开启后无法控制关闭,导致出现上述问题。改进方法是将报警监听线程使用QTread实现,使用与人脸识别线程相同的控制方法,摄像头开启时启动,摄像头关闭时终止,实现二次启动功能。
(3)报警系统改为签到系统
因为系统的功能修改前后有所改变,但是实现的技术实际上是一样的。原始项目的报警功能实际上可以作为签到系统的监听线程。原始代码逻辑为,发现置信度低于阈值的脸即陌生人脸,随机人脸识别线程通过通信队列告知报警系统执行截图拍摄、消息推送、报警响铃等功能。改变为签到系统后,人脸识别线程将置信度阈值以上【即数据库中存在并认为可信的脸】,且匹配度最高的人脸作为签到信号,通过队列通信发送给签到线程,并执行之前没有的数据库录入操作。
简单来说就是,之前是把认不出来的脸进行记录和报警,转变为认出来且最像的人脸进行记录和执行声音提示。
(4)签到表格的创建
这个属于签到系统特有的新增功能,说道考勤签到就一定会想到教师用来点名的签到表,因此该签到系统需要生成一个包含当前课程需要签到的所有学生信息,通过预处理进行创建,并在人脸识别过程中实时修改记录签到信息,主要内容为学生姓名、学号、签到时间。该功能内建了一个基于MySQL查询的QTableWidget控件,用于方便用户之间从数据库中选择并创建需要签到的学生名单。
(1)学生信息管理
原始的管理端非常简陋,存储信息仅包含学生姓名、faceID、学号这样的普通信息,想要查询学生信息也只能进行单人查询,并且只能通过学号,查询的唯一目的就是通过学号删除查询结果。因此无论是查询还是删除都非常简陋。甚至根本不存在修改信息的功能。
修改后的管理端新增了多条件模糊查询,并且大大增加了信息维数,允许直接双击修改学生信息,并实时同步到数据库中。同时删除信息的功能也和信息查询分离开来,信息查询结果动态显示在QTableWidget控件中【与原项目使用的技术相同,只是做了更大的功能扩展】。支持用户多选删除,不不必“查一个,删一个”。
(2)人脸数据训练
原项目中的人脸数据训练功能同样集成在管理端中,原作者自定义了一个读取数据集并将其与学生通过faceID进行唯一匹配的函数,也就是LBPH数据训练中的人脸数据(faces)与分类标签(labels)。读取并整理成LBPH.train()所需要的数据结构后即可直接将数据作为参数调用封装好的函数进行训练。
值得一提的是,LBPH的数据训练非常快,即使在三百人的,每人人脸数据集平均20张的情况下,训练时间依然能控制在十几秒内【但是结果其实并不好】,因此原作者并没有将其作为单独的线程来执行,而是直接甩给用户一个提示:训练期间系统窗口可能会无响应,请耐心等待。。。
这样的结果是导致用户体验极差,会以为程序崩了,其实只是训练计算时间过长,导致windows消息监听一段时间内无回应,被认为是程序无响应。。。
二次开发后我将其独立为一个线程单独执行,并为执行函数增加了进度条,让用户直观的看到训练过程。顺便,在代码实现过程中,发现大量时间实际上消耗在图片数据集的读取上,训练的过程反倒没有那么久,因此进度条实际展示的是读取过程,逻辑上是先进行的数据读取,再计算特征值,然后继续读取下一个数据集,因此将数据读取作为进度来衡量不会有时间上的偏差。
这里就简单贴一下数据录入端的代码。项目完整代码还请移步本文开始位置的链接。
- 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)
- self.setWindowIcon(QIcon('./icons/icon.png'))
- self.setFixedSize(1528, 856)
-
-
- 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.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.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))
- 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_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]
-
- 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]
-
- 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() == '开始采集人脸数据':
- 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()
-
- if ret:
-
- 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))
-
-
-
-
-
-
-
- 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
源码首发位置:Python 毕业设计 – 基于 opencv 的人脸识别上课考勤系统,附源码陈大发随笔-计算机源码资源-计算机毕业设计项目陈大发随笔
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。