# Python dlib 人臉辨識實作
本專題的參考書籍:*<Python機器學習超進化:AI影像辨識跨界應用實戰>*
## 第一步:臉部特徵點的定位
對於不太熟悉影像處理以及人工智慧架構和訓練的新手來說,能有現成的的工具可以使用能更快也更方便做出各種Demo。\
Python 中的 dlib 便是這種厲害的工具,不僅有現成的人臉辨識的演算法,更有官方提供的訓練模型檔,有利各種人工智慧的應用。\
\
首先必須先下載dlib,利用Python pip進行下載及安裝:
```
pip install dlib
```
當然這個僅有dlib的部分,實作中其他的模組都還要另外安裝。\
\
dlib 辨識的原理不得而知(至少我自己不清楚),但是在程式中可以稍微窺見以及大概瞭解中的演算法。\
先將模型檔 `shape_predictor_68_face_landmarks.dat` 讀入預測器並宣告偵測正面臉部的偵測器,語法如下:
```python=
predictor = dlib.shape_predictor(r"model/shape_predictor_68_face_landmarks.dat")
detector = dlib.get_frontal_face_detector()
```
模式應該是先將圖片放入偵測器得知正臉的位置,再由預測器預測臉部特徵點的位置。\
辨識的方法:
```python=
#img為輸入的影像
dets = detector(img,1)
result = predictor(img,det).parts()
```
`result` 便是辨識的結果,68點特徵點的座標,再依序將座標取出便可以使用結果。\
\
這部分我以webcam即時影像做測試,由攝像頭讀入影像,再承接上述的步驟,最後輸出影像,然後不段重複這個步驟直到使用者關閉,以下為程式碼:
```python=
# -*- coding: utf-8 -*-
"""
Created on Fri Aug 4 19:51:25 2023
@author: Aaron
"""
import numpy as np
import cv2
import dlib
import time
predictor = dlib.shape_predictor(r"model/shape_predictor_68_face_landmarks.dat")
detector = dlib.get_frontal_face_detector()
#計算幀率的變數(執行一幀經過時間的倒數)
start=0
end=0
status = False
d= False
print("set up complete")
cap = cv2.VideoCapture(1)
print("camera ready")
while True:
start = time.time()
ret, img = cap.read()
img=cv2.flip(img,1)
dets = detector(img,1)
if d:
for det in dets:
landmark=[]
for p in predictor(img,det).parts():
landmark.append(np.matrix([p.x,p.y]))
for idx, point in enumerate(landmark):
pos = (point[0, 0], point[0, 1])
cv2.circle(img, pos, 2, (255,255,255),-1)
if status:
cv2.putText(img, str(idx+1), pos, cv2.FONT_HERSHEY_SIMPLEX, 0.3, (0, 0, 255), 1, cv2.LINE_AA)
end=time.time()
cv2.rectangle(img, (0,0), (190, 55), (50,50,50), -1)
cv2.putText(img, " Webcam frame: "+str(cap.get(cv2.CAP_PROP_FPS)), (0, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, cv2.LINE_AA)
cv2.putText(img, " Real frame: "+str(1//(end-start)), (0, 43), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1, cv2.LINE_AA)
cv2.imshow("img",img)
end = time.time()
key = cv2.waitKey(1)
if key == 27:
break
elif key == ord("n") or key == ord("N"):
status = not(status)
elif key == ord("d") or key == ord("D"):
d = not(d)
cap.release()
cv2.destroyAllWindows()
```
調皮的我在程式碼中加了許多開關,是因為我發現辨識的影響,原本攝像頭的幀率是60,但是辨識人特徵後,幀率下降至4~5
## 第二步:建立視窗
我自己的建立的方法適用Python內建的tkinter,但這部分的範圍較廣,我也是自己慢慢摸索,附上參考網址:[tkinter](https://steam.oxxostudio.tw/category/python/tkinter/start.html)\
\
這部分是想先測試攝像頭影像、臉部辨識以及視窗是否能正常運行,因此設計了以下的小視窗,可以顯示攝像頭畫面,辨識後的結果,並且可以開關上述的功能,如下圖:

\
建立視窗其實照著教程做其實蠻簡單,建立視窗及設定特性:
```python=
root = tk.Tk()
root.geometry("736x561+479+178")
root.resizable(0, 0)
root.title("python dlib demo")
root.configure(background="#1f1f1f")
```
`root` 即是主視窗\
\
而按鈕的部分稍微複雜一些,需要設定觸發後的執行函式,而觸發後該函式更改檢查的布林變數,在執行中便只要判斷變數的值(其實只是把前一節的開關實體化按鈕)。以偵測開關為例,程式碼如下:
```python=
detect = tk.Button(root)
detect.place(relx=0.272, rely=0.82, height=24, width=60)
detect.configure(activebackground="#0d8a55")
detect.configure(command=dector_switch)
detect.configure(activeforeground="black")
detect.configure(background="#0d8a55")
detect.configure(compound='left')
detect.configure(disabledforeground="#a3a3a3")
detect.configure(foreground="#e4e4e4")
detect.configure(highlightbackground="#d9d9d9")
detect.configure(highlightcolor="black")
detect.configure(pady="0")
detect.configure(text='''Detect''')
````
觸發函式 `dector_switch()`:
```python=
def dector_switch():
if mode:
global d
d= not d
if not d:
detect.configure(text='''Detect''')
else:
detect.configure(text='''OFF''')
```
布林變數 `d` 便是執行時判斷的值。\
\
而要將攝像頭影像顯示於是窗上難度有些高,這部分式參考需多網路大神撰寫的結果。\
\
首先,必須先了解tkinter的視窗物件在設定完並執行後,是一個一直在執行的程序(loop),如果要能看到即時影像,必須讓每次讀進來的影像都能寫入該視窗的Label物件(或是Canva物件),但該視窗物件只能修改其特性(configure),不過tkinter也提供了方法:
```
視窗物件.after(間隔時間,欲執行函式())
```
因此撰寫讀取攝像頭影像並寫入該視窗的Label物件的函式,再用此方法加入執行程序裡,便可以達到即時影像的顯示,而再加上上一節的顯示辨識結果的方法,即可完成本次的小測試目標,程式碼如下:
```python=
# -*- coding: utf-8 -*-
"""
Created on Sat Aug 12 01:57:10 2023
@author: Aaron
"""
import tkinter as tk
import cv2
from PIL import Image, ImageTk
import dlib
import numpy as np
def out():
global mode
global out_text
if not mode:
mode=not mode
out_text='''OFF'''
cameraswitch.configure(text=out_text)
videostream.configure(text=" ")
else:
mode=not mode
out_text='''ON'''
cameraswitch.configure(text=out_text)
cameraswitch.configure(font="-family {Arial} -size 9 -weight bold")
#這裡原本想要用重新設定configure達到關掉鏡頭的效果,但似乎不行...
videostream.configure(background="#d9d9d9")
videostream.configure(disabledforeground="#a3a3a3")
videostream.configure(foreground="#ffffff")
videostream.configure(compound='center')
videostream.configure(text='''Camera OFF''')
root.update()
def video_loop():
global cap
global predictor, detector
global d, status
global mode
if mode:
ret, img = cap.read()
img=cv2.flip(img,1)
if d:
dets = detector(img,1)
for det in dets:
landmark=[]
for p in predictor(img,det).parts():
landmark.append(np.matrix([p.x,p.y]))
for idx, point in enumerate(landmark):
pos = (point[0, 0], point[0, 1])
cv2.circle(img, pos, 2, (255,255,255),-1)
if status:
cv2.putText(img, str(idx+1), pos, cv2.FONT_HERSHEY_SIMPLEX, 0.3, (0, 0, 255), 1, cv2.LINE_AA)
img1=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
current_img=Image.fromarray(img1)
imgtk=ImageTk.PhotoImage(image=current_img)
#print(mode)
videostream.imgtk=imgtk
videostream.configure(image=imgtk)
#關鍵在這呢
root.after(1,video_loop)
def number_switch():
if mode and d:
global status
status= not status
if not status:
numberswitch.configure(text='''123''')
else:
numberswitch.configure(text='''OFF''')
def dector_switch():
if mode:
global d
d= not d
if not d:
detect.configure(text='''Detect''')
else:
detect.configure(text='''OFF''')
print("File_check-----------")
print("> Loading Model...")
predictor = dlib.shape_predictor(r"model/shape_predictor_68_face_landmarks.dat")
detector = dlib.get_frontal_face_detector()
print("> Complete.")
print("---------------------\n")
camera=None
while True:
try:
print("Camera_check---------")
camera=int(input("> Enter the camera number:"))
print("> Setting Camera...")
cap = cv2.VideoCapture(camera)
print("> Complete.")
print("---------------------\n")
break
except:
camera=None
print("> Setting Camera error")
print("---------------------\n")
mode=False
out_text='''ON'''
status = False
d= False
root = tk.Tk()
root.geometry("736x561+479+178")
root.resizable(0, 0)
root.title("python dlib demo")
root.configure(background="#1f1f1f")
videostream = tk.Label(root)
videostream.place(relx=0.167, rely=0.089, height=350, width=511)
videostream.configure(background="#d9d9d9")
videostream.configure(disabledforeground="#a3a3a3")
videostream.configure(foreground="#000000")
videostream.configure(compound='center')
videostream.configure(font="-family {Arial} -size 9 -weight bold")
videostream.configure(text='''Camera OFF''')
cameraswitch = tk.Button(root)
cameraswitch.place(relx=0.45, rely=0.81, height=34, width=88)
cameraswitch.configure(activebackground="beige")
cameraswitch.configure(activeforeground="black")
cameraswitch.configure(background="#03015c")
cameraswitch.configure(borderwidth="5")
cameraswitch.configure(command=out)
cameraswitch.configure(compound='center')
cameraswitch.configure(disabledforeground="#6c6c6c")
cameraswitch.configure(font="-family {Arial} -size 9 -weight bold")
cameraswitch.configure(foreground="#e4e4e4")
cameraswitch.configure(highlightbackground="#a3a3a3")
cameraswitch.configure(highlightcolor="#050282")
cameraswitch.configure(pady="0")
cameraswitch.configure(text=out_text)
numberswitch = tk.Button(root)
numberswitch.place(relx=0.177, rely=0.82, height=24, width=48)
numberswitch.configure(command=number_switch)
numberswitch.configure(activebackground="beige")
numberswitch.configure(activeforeground="black")
numberswitch.configure(background="#b65547")
numberswitch.configure(compound='left')
numberswitch.configure(disabledforeground="#a3a3a3")
numberswitch.configure(foreground="#e4e4e4")
numberswitch.configure(highlightbackground="#d9d9d9")
numberswitch.configure(highlightcolor="black")
numberswitch.configure(pady="0")
numberswitch.configure(text='''123''')
detect = tk.Button(root)
detect.place(relx=0.272, rely=0.82, height=24, width=60)
detect.configure(activebackground="#0d8a55")
detect.configure(command=dector_switch)
detect.configure(activeforeground="black")
detect.configure(background="#0d8a55")
detect.configure(compound='left')
detect.configure(disabledforeground="#a3a3a3")
detect.configure(foreground="#e4e4e4")
detect.configure(highlightbackground="#d9d9d9")
detect.configure(highlightcolor="black")
detect.configure(pady="0")
detect.configure(text='''Detect''')
video_loop()
root.mainloop()
cap.release()
cv2.destroyAllWindows()
```
## 第三步:建立資料庫
這邊用的資料庫跟書中相同,皆是sqlite,亦是用SQL存取。\
\
由於sqlite是一個檔案行資料庫,因此在本地端建立檔案便可以存取資料,程式碼如下:
```python=
conn = sqlite3.connect(r"data/member.sqlite")
cur=conn.cursor()
```
其中建立的 `cur` (資料庫的cursor)便可以執行SQL。\
\
資料庫的動作不外乎建立、插入、刪除資料,而為了方便資料庫的管理,我這裡撰寫了額外的程式,這邊可以作為存取的範例:
```python=
# -*- coding: utf-8 -*-
"""
Created on Sat Aug 12 23:46:25 2023
@author: Aaron
"""
import sqlite3
import cv2
#-------------------------------------------------------------
conn = sqlite3.connect(r"data/member.sqlite")
cur=conn.cursor()
#-------------------------------------------------------------
cmd='CREATE TABLE IF NOT EXISTS member("memberid" TEXT PRIMARY KEY,"picture" TEXT);'
cur.execute(cmd)
#建立表格
cmd='CREATE TABLE IF NOT EXISTS login("memberid" TEXT,"ltime" TEXT);'
cur.execute(cmd)
#插入資料
username="test"
cmd='INSERT INTO member values("'+ username +'","data/photo/'+username+'.jpg");'
cur.execute(cmd)
#刪除資料
username="Aaron"
cmd='DELETE from member where memberid ="'+username+'";'
cur.execute(cmd)
#選擇資料
cmd='SELECT * FROM member;'
cur.execute(cmd)
rows=cur.fetchall()
index=0
for row in rows:
index+=1
print(index," ",row[1])
img = cv2.imread(row[1])
cv2.imshow("img",img)
key = cv2.waitKey(0)
conn.commit()
#-------------------------------------------------------------
cv2.destroyAllWindows()
#關閉資料庫
conn.close()
```
以上便是資料庫的範例。
## 第四步:建立臉部登入系統
以上視窗、資料庫都測試成功,便可以開始撰寫登入系統。\
\
首先是首頁的部分,分為登入按鈕和註冊按鈕,各自連結到不同的頁面,