FaceID技术明细

项目源代码请访问我的GayHub仓库

技术选型

  • PyQt5 + Python3.7 + OpenCV
  • 依赖: PyQt5 PyQt5-tools Pillow numpy opencv-python opencv-contrib-python matplotlib
  • 开发环境: Windows11 + PyCharm

实现人员注册,信息修改,人脸识别获取相关信息

技术方案

页面绘制

采用PyQt5完成

class Ui_Menu(QWidget):
    def __init__(self):
        super(Ui_Menu, self).__init__()
        # 创建label并设置文本内容
        self.label = QLabel('欢迎使用Face-ID识别系统', self)
        # 创建普通用户和管理员按键
        self.btn_ordinary = QPushButton('身份辨认', self)
        self.btn_admin = QPushButton('人脸注册', self)
        self.btn_edit = QPushButton('信息编辑', self)

        file = open("./datafile.txt", 'ab+')
        file.close()

        if os.path.getsize("./datafile.txt"):
            self.datafile = open("./datafile.txt", 'rb')
            global data
            data = pickle.load(self.datafile)
            self.datafile.close()
        print(data)

        # 初始化界面
        self.init_ui()

    def init_ui(self):
        # 设置窗口大小
        self.resize(1280, 800)
        # 设置label框的位置
        self.label.move(140, 200)

        # 设置按键框的位置和大小
        self.btn_ordinary.setGeometry(550, 420, 181, 61)
        self.btn_admin.setGeometry(550, 510, 181, 61)
        self.btn_edit.setGeometry(550, 600, 181, 61)

        # 设置label样式(字体、大小、颜色等)
        self.label.setStyleSheet(
            "QLabel{color:rgb(0,0,0,255);"  # 字体颜色为黑色
            "font-size:82px;font-weight:bold;"  # 大小为70 加粗
            "font-family:Roman times;}")  # Roman times字体

        self.btn_ordinary.setStyleSheet(
            "QPushButton{color:rgb(0,0,0,255);"  # 字体颜色为黑色
            "font-size:30px;"  # 大小为30 
            "font-family:Roman times;}")  # Roman times字体

        self.btn_admin.setStyleSheet(
            "QPushButton{color:rgb(0,0,0,255);"  # 字体颜色为黑色
            "font-size:30px;"  # 大小为30 
            "font-family:Roman times;}")  # Roman times字体

        self.btn_edit.setStyleSheet(
            "QPushButton{color:rgb(0,0,0,255);"  # 字体颜色为黑色
            "font-size:30px;"  # 大小为30 
            "font-family:Roman times;}")  # Roman times字体

        # 点击管理员按钮事件
        self.btn_admin.clicked.connect(self.slot_btn_admin)
        # 点击普通用户按钮事件
        self.btn_ordinary.clicked.connect(self.slot_btn_ordinary)
        self.btn_edit.clicked.connect(self.slot_btn_edit)

    # 点点击管理员按钮事件
    def slot_btn_admin(self):
        self.manager_face = Ui_manager_face()
        self.manager_face.show()
        self.hide()

    # 点击普通用户按钮事件
    def slot_btn_ordinary(self):
        self.face_reco = Ui_face_reco()
        self.face_reco.show()
        self.hide()

    def slot_btn_edit(self):
        self.edit = Ui_edit()
        self.edit.show()
        self.hide()

人脸认证

信息采集, 通过调用摄像头持续采集60张人脸照片, 利用cv2进行处理, 使用FaceCascade 检测图像中的多个人脸, 保存到Face_data文件夹下.

初始化识别器

recognizer = cv2.face.LBPHFaceRecognizer_create()

此处利用的训练集是OpenCV内置的haarcascade_frontalface_default.xml

具体代码如下:

def thread_pic(self):
    print("线程出没!!!")
    print(self.Edit_ID.text())

    # 创建目录,将获取的人脸照片放入指定的文件夹
    self.file = "./Face_data/"

    while (True):
        ret, self.img = self.cap.read()
        # 垂直翻转视频图像
        # 灰度化处理
        gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
        faces = faceCascade2.detectMultiScale(gray, 1.3, 5)

        # 判断是否存在文件夹如果不存在则创建为文件夹
        self.folder = os.path.exists(self.file)
        if not self.folder:
            # makedirs 满权限创建文件时如果路径不存在会创建这个路径
            os.makedirs(self.file)
            os.chmod(self.file, 0o777)

        for (x, y, w, h) in faces:
            cv2.rectangle(self.img, (x, y), (x + w, y + h), (255, 0, 0), 2)
            self.count += 1
            # 将捕获的图像保存到指定的文件夹中
            bool = cv2.imwrite(self.file + "/User." + str(self.Edit_ID.text()) + '.' + str(self.count) + ".png",
                               gray[y:y + h, x:x + w])

        # 取60张人脸样本,停止录像
        if self.count >= 60:
            print("OK!")
            break
    self.recognizer = cv2.face.LBPHFaceRecognizer_create()

    # 函数获取图像和标签数据
    def getImagesAndLabels(path):
        imagePaths = [os.path.join(path, f) for f in os.listdir(path)]
        faceSamples = []
        ids = []
        self.step = 65
        # self.progressBar.setProperty("value", self.step)
        for imagePath in imagePaths:
            # 转换为灰度
            PIL_img = Image.open(imagePath).convert('L')
            img_numpy = np.array(PIL_img, 'uint8')
            id = int(os.path.split(imagePath)[-1].split(".")[1])

            faces = faceCascade3.detectMultiScale(img_numpy)
            for (x, y, w, h) in faces:
                faceSamples.append(img_numpy[y:y + h, x:x + w])
                ids.append(id)
        return faceSamples, ids

    self.step = 75
    # self.progressBar.setProperty("value", self.step)
    print("\n [INFO] Training faces. It will take a few seconds. Wait ...")

    # 调用函数,传递文件夹路径参数
    faces, ids = getImagesAndLabels(self.file)
    self.recognizer.train(faces, np.array(ids))
    self.step = 85
    # self.progressBar.setProperty("value", self.step)

    # 创建文件夹
    self.triningfile = "./Face_training/"
    self.folder1 = os.path.exists(self.triningfile)
    if not self.folder1:
        os.makedirs(self.triningfile)
        os.chmod(self.triningfile, 0o777)

    # 将训练好的数据保存到指定文件夹中
    self.recognizer.write(self.triningfile + "/trainer.yml")

    global data
    student = Student(str(self.Edit_ID.text()), "未命名", "", "", "")
    data[self.Edit_ID.text()] = student
    self.datafile = open("./datafile.txt", 'wb')
    pickle.dump(data, self.datafile)
    self.datafile.close()
    print(data)

    # 打印经过训练的人脸编号和结束程序
    print(" [INFO] {0} faces trained. Exiting Program".format(len(np.unique(ids))))
    self.step = 100
    # self.progressBar.setProperty("value", self.step)

修改ProcessBar的step更新时间,解决程序异常终止的问题

此处另外涉及的问题在于progressBar. 原版方案代码为

def slot_btn_enter(self):
        self.count = 0
        self.step = 0
        # 创建线程并开启
        self.thread = threading.Thread(target=self.thread_pic)
        self.thread.start()
        
        # 开启进度条定时器
        self.timer.start(100, self)

def timerEvent(self, e):
     if self.step > 58:
         self.timer.stop()
         return
     self.step = self.count+1
     self.progressBar.setValue(self.count)

def thread_pic(self):
		 while(True):
			for (x,y,w,h) in faces:
                cv2.rectangle(self.img, (x,y), (x+w,y+h), (255,0,0), 2)     
                self.count += 1
         	if self.count >= 60: 
                print("OK!")
                break
         
        self.progressBar.setProperty("value", 75)

如上代码会导致程序异常退出, 分析原因在于在子线程中进行progerssBar的显示状态更新, 导致子线程运行结束后, progressBar同时崩溃

对此进行修复, 修复后代码如下

def slot_btn_enter(self):
    self.count = 0
    self.step = 0
    # 创建线程并开启
    self.thread = threading.Thread(target=self.thread_pic)
    self.thread.start()

    # 开启进度条定时器
    self.timer.start(100, self)
    
def reflash(self):
        self.progressBar.setProperty("value", self.step)

    # 加载进度条
def timerEvent(self, e):
        if self.step == 100:
            self.progressBar.setValue(self.step)
            self.timer.stop()
            return
        elif self.step > 58:
            self.progressBar.setValue(self.step)
            return
        self.step = self.count + 1
        self.progressBar.setValue(self.count)
 
def thread_pic(self):
    while (True):
                    for (x, y, w, h) in faces:
                cv2.rectangle(self.img, (x, y), (x + w, y + h), (255, 0, 0), 2)
                self.count += 1
    self.step = 65
            # self.progressBar.setProperty("value", self.step)

不在子线程内使用self.progressBar顺利解决此程序暴毙问题😘

通过Pillow模块对图像处理

甲方要求身份标注为姓名

但由于cv2.puttext的限制, 使用以下代码进行改动会出现 ??? 无法正常显示

cv2.putText(self.image, "黄粱", (x+5,y-5), self.font, 1, (255,255,255), 2)

对此进行优化:

img_PIL = Image.fromarray(cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB))
font = ImageFont.truetype('font.ttf', 20)
color = (0, 255, 0, 0)
position = (x, y)

draw = ImageDraw.Draw(img_PIL)
draw.text(position, str(id), font=font, fill=color)
self.image = cv2.cvtColor(np.asarray(img_PIL), cv2.COLOR_RGB2BGR)

调用pillow模块进行图像处理顺利解决问题.

数据持久化

为保证存储用户信息, 且尽可能简便, 使用pickle完成用户数据序列化, 并存储于datafile.txt中

class Student():
    def __init__(self, id, name, age, sex, parent):
        self.id = id
        self.name = name
        self.age = age
        self.sex = sex
        self.parent = parent
  
		global data
        student = Student(str(self.Edit_ID.text()), "未命名", "", "", "")
        data[self.Edit_ID.text()] = student
        self.datafile = open("./datafile.txt", 'wb')
        pickle.dump(data, self.datafile)
        self.datafile.close()
        print(data)

用户信息编辑页面通过读取写入文本文件完成对信息修改.




wallhaven-ymoqgl_1920x1080.png