利用python、tensorflow、opencv、pyqt5實現人臉實時簽到系統

前言

一個基於opencv人臉識別和TensorFlow進行模型訓練的人臉實時簽到系統,作者某二本大學裏的末流學生,寫於2019/09/,python學習期間。
今年7月份開始接觸python的,最近閒着無事就開始做了這個人臉識別的系統,一開始的話就想着簡單的弄下,就去了百度智能雲用的api接口實現的,寫完以後我就想爲什麼我不自己寫一個人臉識別簽到,不去調用百度api接口,然後就誕生了這個程序。

先看下效果

在這裏插入圖片描述

實現的功能

  • 點擊開始進行實時人臉打開識別簽到在這裏插入圖片描述
  • 點擊註冊會跳到註冊頁面進行註冊在這裏插入圖片描述
  • 點擊缺勤會打開缺勤窗口顯示缺勤的表格在這裏插入圖片描述

開始準備

選用語言Python,時下入門機器學習成本最低、學習速度最快的語言,python搞網絡爬蟲也很靠譜。運用的技術有 opencv(攝像頭、圖片處理),numpy(圖片數字化),os(文件的操作和處理),TensorFlow(構建神經網絡進行模型訓練)。

頁面的構建

我的UI頁面是用pyqt寫的,pyqt這塊也不是很熟練,就稍微看了看教程,問了問同學,摸索着開始使用了,關於pyqt這塊的話,不和大家多說了,大家可以看下這個鏈接pyqt5、qtdesigner安裝和環境設置,我的這個程序也是虧了博主的這篇博客,大愛在這裏插入圖片描述

功能實現

這裏我就長話短說,因爲涉及的東西比較多,我就按照我的目錄中的文件開始講起在這裏插入圖片描述

  • icon這個文件夾主要是存放UI頁面的圖標
  • small_img_gray文件夾是用來存放註冊時轉換的灰度圖,就是你在註冊的時候點擊拍照以後,程序會在一段時間內拍下100張照片進行保存,然後再將保存的照片轉換爲灰度圖進行往後的模型訓練。
  • temp文件夾用來保存訓練的模型
  • test_img用來保存註冊時拍下的100張照片,對了忘了說,test_img文件夾和small_img_gray文件夾下都是以人名爲命名的文件夾,每個人名文件夾下保存着100張照片在這裏插入圖片描述
  • captureface.py這個文件功能是
    1.用於某個人的圖片採集和圖片中人臉檢測及灰度處理
    2.通過電腦攝像頭實時拍照,並按類別進行存儲
    3.把獲取的圖片中的人臉檢測裁剪出來,並做灰度處理後分類存儲,這裏用到了後邊的getimgdata.py文件
  • cnn_net.py是用來構建cnn神經網絡結構,以及模型訓練和模型預測
  • cv2ImgAddText.py,關於這個文件,大家前邊看到的我人臉上邊有個方框和人名是吧,但是在做這一步的時候我發現,opencv自帶的puttext方法不能輸出中文,只能輸出英文,很生氣,中國漢字文化博大精深~源遠流長 爲什麼不能輸出漢字??但是咱也不能怪人家開發人員可能人家沒考慮的,所以這個文件就是將漢字進行輸出的
  • db.py用來建立連接數據庫的對象
  • face_register.py文件向數據庫進行插入用戶數據
  • face_search.py文件是在進行簽到時更新數據庫中用戶的狀態,是簽到了還是遲到了
  • getimgdata.py文件包含4個方法,主要功能就是將用戶的照片進行分類存儲
  • haarcascade_frontalface_default.xml文件是級聯分類器文件,是opencv進行人臉識別的文件,具體還有其他的分類器,像識別眼睛嘴巴還有微笑什麼的
  • late.py和late.ui這兩個文件我拿一起說,主要是因爲late.py是由late.ui文件進行編譯得到的,當你看完上邊頁面部分的構建你就會明白了,後邊的Mainwindow.py和Mainwindow.ui以及register.py和register.ui同理,都是頁面的代碼文件
  • 微軟雅黑Bold.ttf就是人臉識別的時候方框上邊的名字字體,默認的我嫌它的字體比較細,所以網上找了個加粗的,大家也可以用別的字體,這個無所謂
  • 最後就是main.py這個文件了,基本上所有的執行代碼就在這了,這裏呢用到了pyqt的多線程QThread以及python的多線程threading,這倆稍微有點那麼區別,但基本上實現都是相通的,爲什麼會用到多線程呢?這是因爲在一開始進行註冊拍照和模型訓練的時候,我發現如果不啓用多線程,前端的ui頁面就會未響應,當模型訓練完成以後才恢復正常,這樣給用戶帶來的體驗是極差的,所以儘量將ui展示頁面和功能實現的代碼分開,在顯示頁面的同時還能進行後臺的運算。(不知道這樣說對不對,說錯的還請大家指出來,小白虛心學習

代碼部分

這裏就放main.py文件的吧,其他的放了估計也太多剩下的大家可以去我的GitHub下載,下載鏈接會在最後貼出。在這裏插入圖片描述

# -*- coding: utf-8 -*-
# @File  : main.py
# @Author: CSD
# @Date  : 2019/9/7 0007 20:48
# @Software: PyCharm
import gc
import os
import sys
import time
import cv2
import qimage2ndarray
from PyQt5.QtCore import QTimer, Qt, QThread, pyqtSignal
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication, QMainWindow, QDialog, QAbstractItemView, QTableWidgetItem, QMessageBox
from Mainwindow import Ui_MainWindow
from db import mysql_conn
from face_register import register_handler
from face_search import search_handler
from late import Ui_Table
from register import Ui_Register
from cnn_net import CnnNet
from sklearn.model_selection import train_test_split
import numpy as np
from getimgdata import GetImgData
import captureface
from captureface import CaptureFace
import threading
from cv2ImgAddText import cv2ImgAddText
imgs, labels, number_name = GetImgData().readimg()  # 讀取數據
train_x, test_x, train_y, test_y = train_test_split(imgs, labels, test_size=0.1, random_state=10)  # 訓練集測試集數據劃分
cnnnet = CnnNet()  # 調用CNN算法類


# 主窗口
class parentWindow(QMainWindow, Ui_MainWindow):

    def __del__(self):
        try:
            self.camera.release()  # 釋放資源
        except:
            return

    def __init__(self, parent=None):
        super(parentWindow, self).__init__(parent)
        self.setupUi(self)
        self.Latebt.setEnabled(False)
        # self.PrepCamera()
        self.CallBackFunctions()
        self.Timer = QTimer()  # 實例化一個類
        self.Timer.timeout.connect(self.TimerOutFun)  # 定時刷新
        self.flagThread = FlagThread()
        # 當獲得循環完畢的信號時,停止計數
        self.flagThread.trigger.connect(self.fun_timer)

    # 初始化攝像頭
    def PrepCamera(self):
        try:
            self.camera = cv2.VideoCapture(0)
            self.camera.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
            self.MsgTE.clear()
            self.MsgTE.append('攝像頭已連接!')
        except Exception as e:
            self.MsgTE.clear()
            self.MsgTE.append(str(e))

    # 回調函數
    def CallBackFunctions(self):
        self.Showbt.clicked.connect(self.StartCamera)
        self.About.clicked.connect(self.about)

    # 開始按鈕函數
    def StartCamera(self):
        self.Showbt.setEnabled(False)
        self.Loginbt.setEnabled(False)
        self.Latebt.setEnabled(True)
        self.PrepCamera()
        self.Timer.start(1)  # 每隔1ms刷新一次
        self.timelb = time.clock()
        self.flagThread.start()

    # 從攝像頭讀取圖像
    def TimerOutFun(self):
        success, img = self.camera.read()
        if success:
            self.Image = img
            self.DispImg()
        else:
            self.MsgTE.clear()
            self.MsgTE.setPlainText('攝像頭讀取圖像已暫停!')

    def fun_timer(self, flag):
        self.MsgTE.setPlainText(flag)

    # 檢測人臉、色彩空間及格式轉換
    def DispImg(self):
        results = captureface.detect_face(self.Image)
        if results is not ():
            faceboxes = results  # 提取人臉位置信息
            for (x, y, w, h) in faceboxes:
                face = self.Image[y:y + h, x:x + w]  # 截取圖片中的人臉圖像
                face = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY)  # 轉爲灰度圖片
                face = cv2.resize(face, (64, 64))  # 壓縮成指定大小
                face = face.reshape([1, 64, 64, 1])
                cnnNet = CnnNet(modelfile='./temp/train-model')  # 注意這步很關鍵,起到了重置計算圖的作用,否則多次導入訓練好的計算圖會出現tensor重複的問題
                res, pre = cnnNet.predict(test_x=face)  # 調用已訓練好的模型進行預測
                if np.max(pre) < 0.8:  # 通過調整閾值爲threshold,當返回數組最大值小於threshold是即視爲unknown
                    self.name = "unknown"
                else:
                    self.name = number_name[res[0]]
                    self.flagThread.get_name(self.name)

                # cv2.putText(self.Image, self.name, (int(x), int(y) - 20), cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 2)
                cv2.rectangle(self.Image, (int(x), int(y)), (int(x + w), int(y + h)), (255, 0, 0), 3)  # 將name顯示出來
                img = cv2ImgAddText(self.Image, self.name, int(x+25), int(y-50))
                img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                qimg = qimage2ndarray.array2qimage(img)  # 調用array2qimage函數將其轉爲QImage格式
                self.DispLb.setPixmap(QPixmap(qimg))  # 再通過QPixmap函數轉爲QPixmap格式進行顯示。
                self.DispLb.show()  # 圖像顯示
                gc.collect()

        else:
            img = cv2.cvtColor(self.Image, cv2.COLOR_BGR2RGB)
            qimg = qimage2ndarray.array2qimage(img)  # 調用array2qimage函數將其轉爲QImage格式
            self.DispLb.setPixmap(QPixmap(qimg))  # 再通過QPixmap函數轉爲QPixmap格式進行顯示。
            self.DispLb.show()  # 圖像顯示

    # 幫助
    def about(self):
        reply = QMessageBox.information(self,
                                        "關於",
                                        "1.點擊開始按鈕播放視頻流,進行簽到\n"
                                        "2.點擊註冊按鈕可跳往註冊窗口進行註冊\n"
                                        "3.點擊缺勤按鈕可查看缺勤學生名單,狀態0代表缺勤\n"
                                        "4.點擊幫助按鈕可查看幫助,同時關閉主窗口視頻流,進行人臉註冊",
                                        QMessageBox.Close)

        try:
            self.camera.release()  # 釋放資源
            self.Showbt.setEnabled(True)
            self.Loginbt.setEnabled(True)
        except:
            return


# 註冊窗口
class childWindow(QDialog, Ui_Register):

    def __init__(self, parent=None):
        super(childWindow, self).__init__(parent)
        self.setupUi(self)
        self.PhotoBt.setEnabled(False)
        self.RegisterBt.setEnabled(False)
        # self.PrepCamera()
        self.CallBackFunctions()
        self.workThread = WorkThread()
        self.photoThread = PhotoThread()
        # 當獲得循環完畢的信號時,停止計數
        self.workThread.trigger.connect(self.timeStop)
        self.photoThread.trigger.connect(self.runover)
        self.Timer = QTimer()  # 實例化一個類
        self.Timer.timeout.connect(self.TimerOutFun)  # 定時刷新

    # 初始化攝像頭並打開
    def PrepCamera(self):
        try:
            self.camera = cv2.VideoCapture(0)
            self.MsgLb.clear()
            self.MsgLb.setText('請輸入學號、姓名,點擊拍照!')
        except Exception as e:
            self.MsgLb.clear()
            self.MsgLb.setText(str(e))

    # 回調函數
    def CallBackFunctions(self):
        self.PhotoBt.clicked.connect(self.PhotoCamera)
        self.StartBt.clicked.connect(self.StartCamera)
        self.RegisterBt.clicked.connect(self.RegisterCamera)
        self.ModelBt.clicked.connect(self.ModelTrain)

    # 開始按鈕函數
    def StartCamera(self):
        self.PhotoBt.setEnabled(True)
        self.RegisterBt.setEnabled(True)
        self.PrepCamera()
        self.Timer.start(1)  # 每隔1ms刷新一次
        self.timelb = time.clock()

    # 註冊
    def RegisterCamera(self):
        Msg = register_handler(self.stu_id.text(), self.stu_name.text())
        self.MsgLb.setText(Msg)

    # 拍照按鈕函數
    def PhotoCamera(self):
        if self.stu_id.text().isdigit():
            self.RecordCamera()  # 保存照片
            self.stu_id.clear()
            self.stu_name.clear()
        else:
            self.MsgLb.setText('請檢查學號是否正確')

    # 從攝像頭讀取圖像
    def TimerOutFun(self):
        success, img = self.camera.read()
        if success:
            self.Image = img
            self.DispImg()
        else:
            self.MsgLb.clear()
            self.MsgLb.setText('攝像頭讀取圖像已暫停!')

    # 色彩空間及格式轉換
    def DispImg(self):
        img = cv2.cvtColor(self.Image, cv2.COLOR_BGR2RGB)
        qimg = qimage2ndarray.array2qimage(img)  # 調用array2qimage函數將其轉爲QImage格式
        self.ShowLb.setPixmap(QPixmap(qimg))  # 再通過QPixmap函數轉爲QPixmap格式進行顯示。
        self.ShowLb.show()  # 圖像顯示

    # 採集照片
    def RecordCamera(self):
        self.MsgLb.setText('正在採集圖片')
        self.photoThread.img(self.Image, self.stu_name.text())
        self.photoThread.start()

    def runover(self, msg, i):
        self.MsgLb.setText(msg)
        self.progressBar.setValue(i)

    # 模型訓練
    def ModelTrain(self):
        self.MsgLb.setText('請等待一段時間進行模型訓練!')
        self.workThread.start()

    def timeStop(self, msg, i):
        self.MsgLb.setText(msg)
        self.progressBar.setValue(i)

    def closeEvent(self, event):
        self.camera.release()


# 缺勤窗口
class childWindow_late(QDialog, Ui_Table):
    def __init__(self, parent=None):
        super(childWindow_late, self).__init__(parent)
        self.setupUi(self)
        self.CallBackFunctions()
        self.tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)

    # 回調函數
    def CallBackFunctions(self):
        self.ResetBt.clicked.connect(self.ResetTable)
        self.ShowBt.clicked.connect(self.ShowTable)

    # 顯示缺勤表格
    def ShowTable(self):
        sql = 'select * from users where state=0'
        print(sql)
        cursor = mysql_conn.cursor()
        cursor.execute(sql)
        results = cursor.fetchall()
        print(results)
        if results:
            row = cursor.rowcount
            vol = len(results[0])
            self.tableWidget.setRowCount(row)
            self.tableWidget.setColumnCount(3)
            for i in range(row):
                for j in range(3):
                    temp_data = results[i][j + 1]  # 臨時記錄,不能直接插入表格
                    print(temp_data)
                    data = QTableWidgetItem(str(temp_data))  # 轉換後可插入表格
                    data.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
                    self.tableWidget.setItem(i, j, data)

    # 重置
    def ResetTable(self):
        reply = QMessageBox.question(self,
                                     "消息",
                                     "重置後所有的學生的狀態將更改爲0,是否修改?",
                                     QMessageBox.Yes | QMessageBox.No)
        if reply == QMessageBox.Yes:
            sql = 'update  users set state = 0'
            print(sql)
            cursor = mysql_conn.cursor()
            cursor.execute(sql)
            mysql_conn.commit()
        else:
            return


class WorkThread(QThread):
    trigger = pyqtSignal(str, int)

    def __int__(self):
        super(WorkThread, self).__init__()

    def run(self):
        time.sleep(2)
        path = './small_img_gray'  # 灰度圖路徑
        imgs, labels, number_name = GetImgData(dir=path).readimg()
        x_train, x_test, y_train, y_test = train_test_split(imgs, labels, test_size=0.2)

        cnnNet = CnnNet(modelfile='./temp/train-model',
                        imgs=x_train, labels=y_train)

        train_class = cnnNet.cnnTrain(maxiter=1000,  # 最大迭代次數
                                      accu=0.99, )  # 指定正確率(499次之後)
        for index, out in enumerate(train_class):
            # 循環完畢後發出信號
            self.trigger.emit(out, index * 10)
        self.trigger.emit("訓練完成", 100)

    def kill_thread(self):
        self.terminate()


class PhotoThread(QThread, Ui_Register):
    trigger = pyqtSignal(str, int)

    def __int__(self):
        super(PhotoThread, self).__init__()

    def img(self, img, stu_name):
        self.Image = img
        self.stu_name = stu_name

    def run(self):
        filepath = os.path.join('./test_img/', self.stu_name)  # 路徑拼接
        if not os.path.exists(filepath):  # 看是否需要創建路徑
            os.makedirs(filepath)
        for i in range(100):  # 開始拍照
            savePath = (filepath + "/%d.jpg" % i)
            cv2.imencode('.jpg', self.Image, [cv2.IMWRITE_JPEG_QUALITY, 100])[1].tofile(savePath)  # 保存圖片
            # 無法寫入中文 搜了搜發現OpenCV的imwrite不支持 所以換成了cv2.imencode
            # picturepath = os.path.join(filepath, str(i)) + '.jpg'  # 圖片的完整路徑名  無法讀取中文路徑
            # cv2.imwrite(picturepath, self.Image, [cv2.IMWRITE_JPEG_QUALITY, 100])  # 將圖片寫入指定d路徑
            self.trigger.emit('正在採集圖片~', i)
        picture = CaptureFace(
            imgdir='./test_img/',  # 採集到的圖片存放位置
            grayfacedir='./small_img_gray'  # 灰度處理後的圖片存放位置
        )
        picture.facetogray(someone=self.stu_name, waitkey=100, size=64)
        self.trigger.emit('圖片採集完畢!點擊註冊', 100)


class FlagThread(QThread):
    trigger = pyqtSignal(str)

    def __int__(self):
        super(FlagThread, self).__init__()

    def get_name(self, name=''):
        self.name = name

    def log_in(self):
        if self.name != '':
            user_id, user_name, user_state = search_handler(self.name)
            if user_state == 1:
                self.Msg = '%s %s 簽到成功.' % (user_id, user_name)
            elif user_id is None:
                self.Msg = '簽到失敗'
        else:
            self.Msg = '未檢測到人臉'

    def run(self):
        time.sleep(2)
        while True:
            self.get_name()
            timer = threading.Timer(2, self.log_in)  # 等待5s鐘調用一次fun_timer() 函數
            timer.start()
            timer.join()
            self.trigger.emit(self.Msg)
            print(self.Msg)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ui = parentWindow()
    ui_child = childWindow()
    ui_child_late = childWindow_late()
    login_btn = ui.Loginbt
    late_btn = ui.Latebt
    login_btn.clicked.connect(ui_child.show)
    late_btn.clicked.connect(ui_child_late.show)
    if mysql_conn is None:
        QMessageBox.warning(ui,
                            "警告",
                            "請先聯網,以便進行操作",
                            QMessageBox.Close)
        sys.exit(app.exec_())
    else:
        ui.show()
        sys.exit(app.exec_())

總結

這個我自己搭建出的人臉識別系統是具有自己學習能力的,你給它喂的數據越多,它就可以識別越多的人而且準確度會不斷提高,希望大家可以自己測試和研究。最終實現的結果就是文章開頭的時候的效果。希望能幫到大家!最後附上GitHub地址,覺着好的話可以給個星星★哦 源代碼下載地址
有什麼問題我會盡量回答!
有好多人問我數據庫的問題,一直鴿了好幾天,國慶玩太嗨給忘了,現在補上。在這裏插入圖片描述
在這裏插入圖片描述`

create database face_course;
use face_courses;
create table if not exists users(
	id int auto_increment primary key,
	user_id varchar(20) not null,
	user_name varchar(10) not null,
	state int(5) not null
)default charset=utf8;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章