1. 程式人生 > >基於面部識別的日誌系統的設計與實現

基於面部識別的日誌系統的設計與實現

history 教訓 並且 else -o 經驗 文件 self 線程

基於面部識別的日誌系統的設計與實現

@(GUI程序開發)[PyQt, 信號, 面部識別, 多線程, 媒體播放, opencv]


[TOC]

需求與設計

使用面部識別技術,識別進出重要通道的人員,並對人員進出動作進行記錄。在人員進出時,在攝像機前采集畫面,使用采集到的畫面與歷史記錄對比,如果人員已經存在出入記錄,追加一條記錄即可;如果不存在則新建記錄。

技術與實現

核心技術

使用Python來完成整個程序的編寫,面部識別采用python開源庫face_recognition, 基於dlib(C++實現)。
使用opencv來捕獲視頻流,支持本地攝像頭和網絡攝像頭。
使用QThread創建線程更新每一幀圖片到PyQt界面。

數據存儲

使用Sqlite3作為單機版數據庫,同時支持MySQL數據庫。
使用sqlite3進行數據存儲。

前端界面

使用PyQt4作為前端界面,使用絕對布局。

顯示監控畫面

opencv捕獲攝像機視頻流

Opencv捕獲視頻流,主要使用opencv的videoCapture方法, 傳入攝像頭物理地址(0-99)或者網絡視頻流地址。

  • 安裝(Ubuntu): sudo apt install
  • 使用(Python2.7 代碼示例):
import cv2
#以下的代碼片段應當被放在單獨的線程中
#使用videoCapture捕獲視頻
#address可以是usb攝像頭的地址(0-99)
#address也可以是視頻流的網絡地址 如海康攝像機rtsp地址
address = "rtsp://admin:[email protected]:554/h264/ch1/main/av_stream"
cap = cv2.videoCapture(address)
#當視頻流捕獲到以後,開始獲取捕獲的每一幀
#拿到的frame實際上是以矩陣的形式存儲的,具體的數據結構是numpy的ndarray
#並轉換為PyQt可以顯示的QPixmap(圖片)
while cap.isOpened():
    ret, frame = cap.read()
        #拿到frame的信息,並從cv2默認的BGR模式轉成RGB模式
        height, width, fps = frame.shape
        bytesPerLine = 3 * width
        cv2.cvtColor(frame, cv2.COLOR_BGR2RGB, frame)
        image = QtGui.QImage(frame.data, width, height,bytesPerLine, QtGui.QImage.Format_RGB888)
        #然後在主線程中更新image,Pyqt4發送信號並傳送image到主線程即可

PyQt4子線程控制UI線程更新

使用場景一:子線程獲取視頻流的frame,主線程更新frame

使用QT提供的線程類:QThread

使用opencv獲取視頻流之後,應在UI線程中更新獲得的每一幀。
可以自定義一個控件,集成自QLabel,然後自定義信號,這個信號將會在自線程裏被觸發,在主線程裏執行。
具體實現方法:

class VideoPlayer(QtGui.QLabel):
    def __init__(self, parent, address):
        QtGui.QLabel.__init__(self, parent, address)
        #這裏創建子線程,傳遞流媒體地址
        self.video_provider = videoThread(address)
        self.video_provider.start()
        #這裏自定義一個信號,以便於在子線程裏面觸發更新幀的方法
        self.connect(self.video_provider, QtCore.SIGNAL(‘newImage(PyQt_PyObject)‘), self.setFrame)
        #這裏創建一個當前幀,初始化為None,方便以後截圖
        self.current_frame = None
    
    #這個方法將在newImage信號觸發之後執行
    def setFrame(self):
        pixmap = QtGui.QPixmap.fromImage(frame)
        self.current_frame = pixmap
        self.setPixmap(pixmap)

這樣主線程(UI線程已經做好了所有的準備工作,接下來就是在子線程內獲取視頻的每一幀,然後發送信號過來)
創建一個線程類,繼承QtCore.QThread

#其實就是上面opencv的方法放在一個線程內
class videoThread(QtCore.QThread):
    ‘‘‘線程類,負責更新視頻的每一幀‘‘‘
    def __init__(self, address):
        ‘‘‘初始化的時候傳入視頻流的地址‘‘‘
        super(videoThread, self).__init__()
        self.address = address

    def run(self):
        ‘‘‘重寫run函數,讀取到視頻流之後不斷更新frame到主線程‘‘‘
        self.cap = cv2.VideoCapture(self.address)
        while self.cap.isOpened():
            _, frame = self.cap.read()
            if frame is not None:
                #這裏進行frame的轉換
                height, width, fps = frame.shape
                bytesPerLine = 3 * width
                cv2.cvtColor(frame, cv2.COLOR_BGR2RGB, frame)
                image = QtGui.QImage(frame.data, width,                 height,bytesPerLine, QtGui.QImage.Format_RGB888)
            else:
                #如果沒有拿到frame,則給一個攝像頭連接失敗的圖片,遞歸,直到攝像頭重新連接
                image = QtGui.QImage("./icons/no_video.png")
                self.run()
            #在這裏發送信號的主線程,主線程會自動執行之前定義的setFrame方法,更新圖片
            self.emit(QtCore.SIGNAL(‘newImage(PyQt_PyObject)‘), image)
        #如果攝像頭沒有獲取到,給鏈接失敗的圖片,然後遞歸
        image = QtGui.QImage("./icons/no_video.png")
        self.emit(QtCore.SIGNAL(‘newImage(PyQt_PyObject)‘), image)
        self.run()

這樣就可以在前端播放畫面了。

使用場景二:視頻的截屏,在子線程裏截屏並保存到本地

使用Python原聲的threading.thread創建線程,只想簡單工作

截屏的時候應當在界面顯示:準備,3, 2, 1 的提示,這樣的提示每一次間隔1秒鐘,以便於給用戶3秒的調整時間。 這樣的工作必須在子線程內執行,否則在主線程內執行會阻斷UI線程更新視頻流的業務,造成視頻卡頓。
實現方法: 首先創建一個用於顯示提示消息的Label,然後在子線程裏更新Label的text即可。

from PtQt4 import QtGui, QtCore
from thread import threading

class MainWindow(QtGui.QWidget):
    def __init__(self):
        #...
        #...
        self.notify_label = QtGui.QLabel(self)
        self.take_photo_button = QtGui.QPushButton(self)
        self.take_photo_button.setText(u"拍照")
        self.connect(self.take_photo_button, QtCore.SIGNAL("clicked()"),            self, QtCore.SOLT("takePhoto()"))
    
    #自定義的槽,在class內應加上 裝飾器
    @QtCore.pyqtSlot()
    def takePhoto(self):
        thread = threading(target=self.updateNotifyAndTakePhoto)
        thread.start()
    
    #這裏是真正進行提示和截圖的方法
    def updateNotifyAndTakePhoto(self):
        #先進性一輪循環,顯示四個提示,每一次間隔一秒,持續四秒
        for text in [u"準備", ‘3‘, ‘2‘, ‘1‘]:
            self.notify_label.setText(text)
            time.sleep(1)
        #然後設置提示消息為空
        self.notify_label.setText("")
        #開始截圖
        img = self.video_player.current_frame
        img.save("./tmp/face.jpg")

PyQt4信號的另一種定義方法

在子線程內打開新的窗口

定義信號,初始化的時候綁定,在子線程內調用

class MainWindow(QtGui.QMainWindow):
    ‘‘‘程序主窗口‘‘‘
    #自定義的信號一定要作為類的成員變量
    record_window_signal = QtCore.pyqtSignal()
    def __init__(self):
        #... other code
        self.record_window_signal.connect(self.showRecordWindow)
        self.history_window_signal.connect(self.showHistoryWindow)
        #這裏直接調用啟動一個線程
        thread = threading(tearget=self.backgroundWork)
        thread.start()
    
    #一個函數,做後臺工作
    def backgroundWork(self):
        #...做一些後臺工作,比如人臉識別
        #需要打開新的窗口的時候,觸發這個信號即可
        self.record_window_signal.emit()

    #自定義的信號被觸發時打開一個自定義的Dialog
    def showRecordWindow(self):
        #CSInfoWindow是一個自定義的Didlog
        self.add_dialog = CSInfoWindow()
        self.add_dialog.exec_()

人臉檢測和比對

使用face_recognition檢測圖片內有沒有人臉

在進行人臉識別的時候,首先要從圖片內找到人臉,有時候圖片內根本沒有人臉,有時候由很多張臉,都需要進行判斷

安裝face_recognition模塊

  • 安裝依賴
    cmake 負責編譯dlib sudo apt install cmake
    libboost dlib的依賴 sudo apt install libboost1.61-dev
    dlib 機器學習類庫 sudo apt install libdlib-dev
  • 安裝模塊
    pil python的圖像處理模塊 pip install pillow
    numpy 機器學習核心模塊,無需多言 pip install numpy face_recognition

使用face_recognition檢測照片內的人臉

主要是一些API的使用,後臺的方法都在dlib中實現,python只是調用而已
上一張效果圖:

技術分享圖片 檢測出來兩個人臉

這張圖片直接檢測出來,兩個人臉!!! 但是,其實我們只需要一張人臉。

#face_locations 就是對一張照片中人臉的定義,本質上是一個數組
face_locations = face_recognition.face_locations(image)
#如果有多張人臉,則不作處理,因為不符合我們的使用場景
if len(face_locations) > 1:
    print u"檢測出了多張人臉,請確保鏡頭中只有一個人"
#如果沒有任何人臉,也不做處理
if len(face_locations) == 0:
    print u"沒有檢測出來人臉,請重新識別"
#有且只有一張人臉的時候才開始進行人臉的定位
else:
    #face_locations[0]就是惟一的一張人臉
    top, right, bottom, left = face_locations[0]
    #按照人臉的尺寸,裁剪出來人臉
    face_image = image[top:bottom, left:right]
    pil_image = Image.fromarray(face_image)
    #顯示截取出來的人臉
    pil_image.show()
    print u"人臉檢測成功"
    filename = FACE_DIR + str(datetime.now()) + ".jpg"
    image = Image.fromarray(image)
    image.save(filename)

使用face_recognition進行人臉的比對

在人員數據庫中存儲了很多的人臉,當用戶刷臉之後,要對人臉進行比對,確定用戶身份。
人臉比對的API:
face_recognition.api.compare_faces(known_face_encodings, face_encoding_to_check, tolerance=0.6)

Compare a list of face encodings against a candidate encoding to see if they match.
Parameters:
known_face_encodings – A list of known face encodings
face_encoding_to_check – A single face encoding to compare against the list
tolerance– How much distance between faces to consider it a match. Lower is more strict. 0.6 is typical best performance.
Returns:
A list of True/False values indicating which known_face_encodings match the face encoding to check

  • 參數1: 需要數據庫內的所有人臉數據組成的數組(know_face_encodings
  • 參數2: 需要未知人臉的數據(矩陣): (face_encoding_to_check)
  • 嚴格程度: tolerance 嚴格程度從0.1 到1 越來越寬松,數值設置越大約有可能識別錯誤,數據太小由難以識別到。

具體實現的代碼:

def recognite(unknow_img):
    #傳入一張未知圖片的編碼,轉換成矩陣
    unknow_face_encoding = face_recognition.face_encodings(unknow_img)[0]
    know_faces = []
    know_labels = []
    # 從本地的文件夾內加載所有已經存在的人臉並轉為矩陣,放在一個數組內
    for filename in os.listdir(FACE_DIR):
        path = os.path.join(FACE_DIR,filename)
        image = face_recognition.load_image_file(path)
        know_face_encoding = face_recognition.face_encodings(image)[0]
        know_faces.append(know_face_encoding)
        know_labels.append(filename)
    # 進行人臉的比對,這裏精準度設置為0.4 具體需要根據攝像頭的分辨率 拍攝距離等進行調試
    result = face_recognition.compare_faces(know_faces, unknow_face_encoding, tolerance=0.4)
    # 返回檢測的結果級和對應的標簽(姓名或者其他唯一性標識)
    return result, know_labels

總結

接到任務的第一反映

在剛開始接到這個任務的時候,其實是很震驚的,因為作為機器學期或者人工智能的內行人來講,我很清楚人臉識別意味著什麽。(....意味著算法,預測模型,訓練模型,計算機圖形學),總之這就是一個經典的課題,要從頭實現絕非一個人一年兩年就能出成果的。但是好在前人有很多經驗!

最初的計劃

額!最初的想法是自己實現人臉比對的算法, 因為之前使用knn算法隨手寫數字進行過識別,是一個典型的監督學分類案例,具體的方法就是將圖片轉換為矩陣,再對矩陣進行歸一化,使用歐式距離公式來計算出來n維空間中這些點的距離,然後根據距離它最近的幾個點進行概率判斷,從而進行分類。但是人臉識別這樣做根本不可行,人臉識別中既有回歸分析又有分類,還是很棘手的。

第三方SDK

經過查閱資料發現,使用第三方SDK還是比較靠譜的,目前主流的由opencv和dlib,這裏選擇了dlib,因為dlib的面部識別的密度比較大,所以精準度會高一點,但是項目中還是使用了Opencv來進行視頻流的獲取。

最終結果

本來打算在windows環境下使用c#實現,但是考慮到開發效率, 決定還是用python開發比較靠譜。但在此之前並沒有python GUI程序的開發經驗,所以從頭學習了python的QT開發, 開發環境選擇在linux下,但是QT跨平臺,今後也可在windows平臺下部署,不過一想到windows下復雜的軟件和類庫安裝過程,我就呵呵了,決定運行環境依舊在Linux下,避免電腦中毒導致系統崩潰(這個以前有血的教訓,所以強烈建議使用Linux來運行重要系統)。
經過三天兩夜的開發, 從零開始實現了人臉識別的程序, 測試之後還比較穩定,關於精準度的問題,這個還是需要根據實際環境來調試,並且也需要在拍照的時候做好用戶引導。因為,天知道以後有多少人會被錄在數據庫內,而且精準度又不能太低,所以必須做好使用人員的培訓,確保不會認錯人,或者認不出來人。 如果單位的人比較少,大可以慢慢采集原始數據,正臉,側臉,各種角度表情都可以存在數據庫進行比對,但是這個程序要檢測的對象是隨機的,不是固定的那麽幾個人,但從目前測試的結果來看,正常使用,距離攝像頭2到4米的距離拍照,基本上不會再誤報,不遮擋眼睛,不吐舌頭,微笑,大笑都可以正確識別。

基於面部識別的日誌系統的設計與實現