基於面部識別的日誌系統的設計與實現
基於面部識別的日誌系統的設計與實現
@(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 負責編譯dlibsudo 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米的距離拍照,基本上不會再誤報,不遮擋眼睛,不吐舌頭,微笑,大笑都可以正確識別。
基於面部識別的日誌系統的設計與實現