一、前言
今年由于疫情,全国学生的教学方式都从线下转到了线上;线下可以点名进行签到,那么线上应该如何进行准确的签到,防止学生作弊签到的情况呢?因此一款适用于大中小学生的基于人脸识别的课堂签到系统便应运而生。该系统包含的功能如下:人脸检测功能、人脸库管理(添加/删除班级、添加/删除学生)、基于人脸检测签到功能、查看学生签到成功功能。下面我将按照实现的功能依次介绍。
二、技术介绍
1. 语言:python
2. IDE(项目集成开发环境): pycharm
3. Opencv: 数字图形处理库(摄像头操作)
4. Sqlite3:签到成功数据的存储
5. pycharm解释器
6. Pyq5-tools库(图形化界面)
7. Opencv-python库(图形处理)
8. Pysqlite3库(数据库)
三、人脸检测
1. 本系统是基于百度的API来检测,也就是说,百度已经有了非常成熟的人脸检测技术,可以通过一张图片进行大致判断一个人的年龄、性别、表情变化。我们需要做的就是从摄像头获取一张照片,然后通过调用API接口,将照片发送给百度,百度返回人脸检测的结果,然后我们再将这个结果显示到界面上,流程图如下:
3. 创建摄像头操作类,完成摄像头的采集功能。
'''
摄像头操作:创建类对象完成摄像头操作,所以可以把打开摄像头与创建类对象操作合并
__init__()初始化函数
open()函数完成摄像头的配置打开
'''
import cv2#opencv的一个关于摄像头的库
import numpy as np #as np 简写成np
from PyQt5.QtGui import QPixmap,QImage
#创建并打开摄像头
class camera():
def __init__(self):
#类VideoCapture对视频或调用摄像头进行读取操作
#创建摄像头操作对象,打开摄像头,0表示默认的摄像头,
#self. capture表示打开的摄像头对象
self. capture= cv2.VideoCapture(0)
#isOpened()函数返回一个布尔值,来判断摄像头是否初始化成功
if self.capture.isOpened():
print("isOpened")
#定义一个多维数组,用来存储获取的画面数据
self.currentframe = np.array([])
#读取摄像头的数据,返回获取到的数据data
def read_camera(self):
ret,data = self.capture.read()#ret是否成功,data是数据
if not ret:
print("获取摄像头数据失败")
return None
return data
#将获取到的摄像头数据转换成界面能显示的数据格式
#pic保存的是摄像头获取到的数据
def camera_to_pic(self):
pic = self.read_camera()
self.currentframe = cv2.cvtColor(pic,cv2.COLOR_BGR2RGB)#BGR转换成RGB格式
height,width = self.currentframe.shape[:2] #获取画面的宽度和高度
qimg = QImage(self.currentframe,width,height,QImage.Format_RGB888)#转换成QImage类型的格式
qpixmap = QPixmap.fromImage(qimg)#转换成适合图像显示的格式
return qpixmap
#退出签到(关闭摄像头)
def close_camera(self):
self.capture.release()
i. 当需要摄像头完成一个功能时,就调用摄像头对象的一个函数去完成。
ii. 实现一个摄像头类:
1) 创建一个摄像头的类camera(),该类里面包含初始化函数、多去摄像头数据的函数、转换数据格式的函数、关闭摄像头的函数四个函数。
2) 打开摄像头
a) import cv2(opencv的一个关于摄像头的库)
b) self. capture表示打开的摄像头对象
self. capture= cv2.VideoCapture(0)
c) isOpened()函数返回一个布尔值,来判断摄像头是否初始化成功
if self.capture.isOpened():
d) self.currentframe = np.array([])定义一个多维数组,用来存储获取的画面数据
3) 获取摄像头的实时数据
a) self.capture.read()#获取摄像头的数据
b) def camera_to_pic(self)#该函数将获取到的摄像头数据转换成界面能显示的数据格式
4) 数据进行转换提供给界面
a) 鼠标点击签到时便开启摄像头,故需要在on_actionopen()函数中实例化camera()这个类
b) 需要设计一个定时器用来将摄像头获取到的数据以图片的方式一帧一帧的显示到界面上,这里定时器的时间是10ms。
self.timeshow = QTimer(self)
self.timeshow.start(10)
self.timeshow.timeout.connect(self.show_cameradata) #10ms后定时器启动,产生一个timeout信号,.connect()关联槽函数
show_cameradata(self):
#获取摄像头数据,转换数据
pic = self.cameravideo.camera_to_pic()#将摄像头获取到的数据转换成界面能显示的数据,返回值为qpmaxip
#显示数据,显示画面
self.label.setPixmap(pic) # 将获取到的数据拿到界面中进行显示
iii. 创建一个线程类,主要目的是解决卡顿,完成两个功能:其一:人脸检测,接受主界面传过来的画面数据,并发送post请求,将获得的结果再传给主界面用于显示。其二:人脸识别,接受主界面传过来的画面数据,并发送post请求,将获得的结果再传给主界面用于显示。
'''
#QThread就是Pyqt5提供的线程类
#由于是一个已经完成了的类,功能已经写好,线程类的功能需要自己完成
#需要自己完成需要的线程类,创建一个新的线程类(功能自己定义),继承QThread
#新写的类具有线程的功能
'''
import base64
import cv2
import sqlite3
import requests
from PyQt5.QtCore import QThread, QTimer, pyqtSignal, QDateTime
#线程进行执行,只会执行线程类中的run函数,如果有新的功能需要实现,需要重新写一个run函数完成
class detect_thread(QThread):#新的线程类,并继承QThread
#创建信号槽,字典类型
transmit_data = pyqtSignal(dict)
#创建第二个信号槽,将人脸搜索的结果返回到主界面
search_data = pyqtSignal(str)
#ok(布尔值)用于判断退出while循环
ok = True
#创建字典,用来存放签到数据
sign_list = {}
def __init__(self,token,group):#进行初始化,需要传递一个token(访问令牌)参数
super(detect_thread, self).__init__()#初始化父类 后面括号不需要self
self.access_token = token
self.group = group
print(self.group)
print(self.group)
self.condition = False
#run函数执行结束代表线程结束
def run(self):
#让run函数一直执行detect_face()函数
while self.ok:
#当condition=True(即得到数据)时执行detect_face()
if self.condition:
self.detect_face(self.base64_image)
#执行完一次将condition设置为False
self.condition = False
def get_base64(self,base64_image):
#当窗口产生信号,调用这个槽函数,就把传递的数据,存放在现存的变量中
self.base64_image = base64_image
self.condition = True
# 脸检测与属性分析请求,想得到如年龄、性别等信息(放到了线程里面)
#detect_face(self)前身是get_face(self)函数
def detect_face(self,base64_image):
# 发送请求地址
request_url = "https://aip.baidubce.com/rest/2.0/face/v3/detect"
# 请求参数,是一个字典,在字典中存储了,百度AI要识别的图片信息,属性内容
params = {
"image": base64_image, # 图片信息字符串
"image_type": "BASE64", # 图片信息的格式
"face_field": "gender,age,beauty,expression,face_shape,glasses,emotion,mask", # 请求识别人脸的属性,各个属性在字符串中用逗号隔开
"max_face_num": 10 # 最多可以检测人脸的数目为:10
}
# 访问令牌,已经获取到
access_token = self.access_token
# 把请求地址和访问令牌组成可用的网络地址
request_url = request_url + "?access_token=" + access_token
# 参数,设置请求格式体(字典)
headers = {'content-type': 'application/json'}
# 发送网络post请求,请求百度AI进行人脸检测,返回检测结果
# 发送网络请求,就会一定的等待时间,程序就会在这里阻塞执行
response = requests.post(request_url, data=params, headers=headers)
if response:
data = response.json()
#做一个判断:如果没有检测到人脸,将错误代码返还回去,注意需要有return语句
if data['error_code'] !=0:
self.transmit_data.emit(data)
self.search_data.emit(data['error_msg'])
return
#当检测到有人脸时执行人脸搜索功能
if data['result']['face_num'] > 0:
# 发送信号emit(),将获取到的数据传到主界面
self.transmit_data.emit(dict(data))
self.face_search()
#当没有检测到人脸,发送空数据过去
else:
# 发送信号emit()
print("not detectface")
#人脸搜索功能,只识别一个人
def face_search(self):
request_url = "https://aip.baidubce.com/rest/2.0/face/v3/search"
params = {
"image":self.base64_image,
"image_type":"BASE64",
#从哪些组中进行人脸识别,点击签到时就将组号传过来
"group_id_list":self.group
}
access_token = self.access_token
request_url = request_url + "?access_token=" + access_token
headers = {'content-type': 'application/json'}
response = requests.post(request_url, data=params, headers=headers)
if response:
data = response.json()
if data['error_code'] == 0:
#判断相似度是否大于90,大于能用,否则不能用
if data['result']['user_list'][0]['score']>90:
#存储要保存的签到的数据,方便进行显示
#将相似度删除掉,不用保存
del[data['result']['user_list'][0]['score']]
#获取当前系统时间
datetime = QDateTime.currentDateTime()
#将时间转换成字符串
datetime = datetime.toString("yyyy-MM-dd hh:mm:ss")
data['result']['user_list'][0]['datetime'] = datetime
#设置一个键,唯一标识一个人,防止错误重复进行签到
key = data['result']['user_list'][0]['group_id']+data['result']['user_list'][0]['user_id']
#如果key不存在,则进行添加
if key not in self.sign_list.keys():
self.sign_list[key] = data['result']['user_list'][0]
#0表示第一个人,因为是只检测一张人脸
#通过信号槽的方式将人脸搜索返回的结果传到主界面
user_id = data['result']['user_list'][0]['user_id']
user_info = data['result']['user_list'][0]['user_info']
self.search_data.emit("学生签到成功!\n\n"+"学号:"+user_id+'\n\n'+user_info)
else:
self.search_data.emit("学生签到不成功,找不到对应的学生")
1) 用于每隔500ms获取摄像头的画面,并将该画面传给百度,百度再返回一个结果数据,再根据这个结果数据显示到界面上。
2) self.create_thread(group)#启动检测线程,解决卡顿问题
3) 设计一个定时器和两个信号槽,定时器每隔500ms获取一次画面,再通过一个信号槽将画面传递给线程。
iv. 人脸检测功能的实现
1) 写一个线程->获取摄像头的照片->转换格式->发送post请求到百度->得到检测结果->实时显示到界面上
2) 发送请求(向百度AI发送人脸检测请求,让百度AI去完成人脸检测,返回检测结果)
3) 发送请求只接受有访问令牌(access_token)的post请求,需要注册帐号Client_id、Client_secret
4) 在qt中存在一个机制:信号可以关联上另一个函数,另一个函数会在产生这个对应的信号的时候调用----信号槽机制。
5) 在组件中存在一些特定的信号,当组件执行某个操作时候,这时对应的信号就会被激活(信号产生)
6) 由于信号(如点击)可能执行的功能是不一样的,信号关联一个槽函数,信号产生会执行槽函数完成功能的执行,需要指定信号与槽函数的关联,信号要去执行的功能是什么
7) 信号对象.信号.connect(函数)
效果演示:
1.主界面:
2.签到界面
3.添加学生
4.代码