opencv 人臉識別

那是個好男孩發表於2018-11-21
# openvideo.py
import cv2 
def openvideo(window_name ,video_id):
    cv2.namedWindow(window_name)
    cap=cv2.VideoCapture(video_id)
    while cap.isOpened():
        ok,frame=cap.read()
        if not ok :
            break
        cv2.imshow(window_name,frame)
        c=cv2.waitKey(10) # 等待10ms,10ms內沒有按鍵操作就進入下一次while迴圈,從而得到10ms一幀的效果,waitKey返回在鍵盤上按的鍵
        if c & 0xFF==ord(`q`): # 按鍵q後break
            break
    cap.release()
    cv2.destroyWindow(window_name)
    print("camera closed")

if __name__ == `__main__`: 
    print (`open camera...`)
    openvideo(`mycam` ,0)
#getTrainingData.py
import cv2
def getTrainingData(window_name, camera_id, path_name, max_num):  # path_name是圖片儲存目錄,max_num是需要捕捉的圖片數量
    cv2.namedWindow(window_name)
    cap = cv2.VideoCapture(camera_id)
    classifier = cv2.CascadeClassifier(`haarcascade_frontalface_alt2.xml`)
    color = (0, 255, 0)
    num = 0  # 記錄儲存的圖片數量
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faceRects = classifier.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
        
        if len(faceRects) > 0:
            for faceRect in faceRects:
                x, y, w, h = faceRect
                # 捕捉到的圖片的名字,這裡用到了格式化字串的輸出
                image_name = `%s%d.jpg` % (path_name, num) # 注意這裡圖片名一定要加上副檔名,否則後面imwrite的時候會報錯:could not find a writer for the specified extension in function cv::imwrite_
                image = frame[y:y+h, x:x+w]
                cv2.imwrite(image_name, image)
                num += 1
                # 超過指定最大儲存數量則退出迴圈
                if num > max_num:
                    break
                cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
                font = cv2.FONT_HERSHEY_SIMPLEX
                cv2.putText(frame, (`%d` % num), (x + 30, y + 30), font, 1, (255, 0, 255), 4)
        if num > max_num:
            break
        cv2.imshow(window_name, frame)
        c = cv2.waitKey(10)
        if c & 0xFF == ord(`q`):
            break
    cap.release()
    cv2.destroyAllWindows()
    print(`Finished.`)

if __name__ == `__main__`:
    print (`catching your face and writting into disk...`)
    getTrainingData(`getTrainData`,0,`training_data_other/`,100) # 要先建立這個資料夾

 

# facedetect.py
import  cv2
def facedetect(windowname,camera_id):
    cv2.namedWindow(windowname)
    cap = cv2.VideoCapture(camera_id)
    classfier = cv2.CascadeClassifier(`haarcascade_frontalface_alt2.xml`)
    color = (255, 0, 0)
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faceRects = classfier.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
        """
        由於人臉可能出現在影像的任何位置,在檢測時用固定大小的視窗對影像從上到下、從左到右掃描,判斷視窗裡的子影像是否為人臉,這稱為滑動視窗技術(sliding window)。
        為了檢測不同大小的人臉,還需要對影像進行放大或者縮小構造影像金字塔,對每張縮放後的影像都用上面的方法進行掃描。
        由於採用了滑動視窗掃描技術,並且要對影像進行反覆縮放然後掃描,因此整個檢測過程會非常耗時。
        """
        if len(faceRects) > 0:  
            for faceRect in faceRects:
                x, y, w, h = faceRect
                # 這裡外擴了10個畫素以框出比人臉稍大一點的區域,從而得到相對完整一點的人臉影像
                cv2.rectangle(frame,(x-10,y-10),(x+w+10,y+h+10),color,2)
        cv2.imshow(windowname,frame) 
        c=cv2.waitKey(10)
        if c & 0xFF == ord(`q`):
            break
    cap.release()#釋放攝像頭
    cv2.destroyAllWindows()
    print("camera closed")

if __name__ == `__main__`:
    print (`face detecting... `)
    facedetect(`facedetect`,0)
# load_face_dataset
"""
由於這個分類器準確度有限,得到的人臉圖片會有一些錯誤
所以在下一步處理資料之前就要手工檢查資料
用OpenCV統一圖片尺寸大小以便輸入到卷積神經網路中
"""
import os
import numpy as np
import cv2

IMAGE_SIZE = 160 # 指定影像大小
# 按指定影像大小調整尺寸
def resize_image(image, height=IMAGE_SIZE, width=IMAGE_SIZE):
    top, bottom, left, right = (0,0,0,0)
    # 獲取圖片尺寸
    h, w, _ = image.shape
    # 對於長寬不等的圖片,找到最長的一邊
    longest_edge = max(h, w)
    # 計算短邊需要增加多少畫素寬度才能與長邊等長(相當於padding,長邊的padding為0,短邊才會有padding)
    if h < longest_edge:
        dh = longest_edge - h
        top = dh // 2
        bottom = dh - top
    elif w < longest_edge:
        dw = longest_edge - w
        left = dw // 2
        right = dw - left
    else:
        pass

    # RGB顏色
    BLACK = [0,0,0]
    constant = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value = BLACK)
    # 調整影像大小並返回影像,目的是減少計算量和記憶體佔用,提升訓練速度
    return cv2.resize(constant, (height, width))
    
# 讀取訓練資料到記憶體,這裡資料結構是列表


# # path_name是當前工作目錄,後面會由os.getcwd()獲得
# def read_path(path_name):
#     images = []
#     labels = []
#     for dir_item in os.listdir(path_name): # os.listdir() 方法用於返回指定的資料夾包含的檔案或資料夾的名字的列表
#         # 從當前工作目錄尋找訓練集圖片的資料夾
#         full_path = os.path.abspath(os.path.join(path_name, dir_item))
        
#         if os.path.isdir(full_path): # 如果是資料夾,繼續遞迴呼叫,去讀取資料夾裡的內容
#             read_path(full_path)
#         else: # 如果是檔案了
#             if dir_item.endswith(`.jpg`):
#                 image = cv2.imread(full_path)
#                 if image is None: # 遇到部分資料有點問題,報錯`NoneType` object has no attribute `shape`
#                     pass
#                 else:
#                     image = resize_image(image, IMAGE_SIZE, IMAGE_SIZE)
#                     images.append(image)
#                     labels.append(path_name)
#     print(images,labels)
#     return images, labels

# # 讀取訓練資料並完成標註
# def load_dataset(path_name):
#     images,labels = read_path(path_name)
#     # 將lsit轉換為numpy array
#     images = np.array(images, dtype=`float`) # 注意這裡要將資料型別設為float,否則後面face_train_keras.py裡影像歸一化的時候會報錯,TypeError: No loop matching the specified signature and casting was found for ufunc true_divide
# #    print(images.shape) # (1969, 64, 64, 3)
#     # 標註資料,me資料夾下是我,指定為0,其他指定為1,這裡的0和1不是logistic regression二分類輸出下的0和1,而是softmax下的多分類的類別
#     labels = np.array([0 if label.endswith(`me`) else 1 for label in labels])
#     # print(images,labels)
#     return images, labels


def load_dataset(data_dir):
    images = [] # 用來存放圖片
    labels = [] # 用來存放類別標籤
    sample_nums = [] # 用來存放不同類別的人臉資料量
    classes = os.listdir(data_dir) # 通過資料集路徑下資料夾的數量得到所有類別
    category = 0 # 分類標籤計數
    for person in classes: # person是不同分類人臉的資料夾名
        person_dir = os.path.join(data_dir, person)  # person_dir是某一分類人臉的路徑名
        if os.path.isdir(person_dir):
            person_pics = os.listdir(person_dir) # 某一類人臉路徑下的全部人臉資料檔案
        for face in person_pics:  # face是某一分類資料夾下人臉圖片資料的檔名
            img = cv2.imread(os.path.join(person_dir, face)) # 通過os.path.join得到人臉圖片的絕對路徑
            if img is None: # 遇到部分資料有點問題,報錯`NoneType` object has no attribute `shape`
                pass
            else:
                img = resize_image(img, IMAGE_SIZE, IMAGE_SIZE)
            images.append(img) # 得到某一分類下的所有圖片
            labels.append(category) # 給某一分類下的所有圖片賦予分類標籤值
        sample_nums.append(len(person_pics)) # 得到某一分類下的樣本量
        category += 1
    images = np.array(images)
    labels = np.array(labels)
    print("Number of classes: ", len(classes)) # 輸出分類數
    for i in range(len(sample_nums)):
        print("Number of the sample of class ", i, ": ", sample_nums[i])  # 輸出每個類別的樣本量
    return images, labels

if __name__ == `__main__`:
    path_name = os.getcwd()  # 獲取當前工作目錄
    print(path_name)
    images = load_dataset("./data/")
    print(images)
# face_train_keras.py
import random
import keras
import numpy as np
from sklearn.model_selection import train_test_split
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten, Conv2D, MaxPooling2D
from keras.optimizers import SGD
from keras.models import load_model
from keras import backend as K
from load_face_dataset import load_dataset, resize_image, IMAGE_SIZE
import cv2

```
對資料集的處理,包括:
1、載入資料集
2、將資料集分為訓練集和測試集
3、根據Keras後端張量操作引擎的不同調整資料維度順序
4、對資料集中的標籤進行One-hot編碼
5、資料歸一化
```
class Dataset:
    def __init__(self, path_name): 
        # 訓練集
        self.train_images = None
        self.train_labels = None
        
        # 測試集
        self.test_images = None
        self.test_labels = None
        
        # 資料集載入路徑
        self.path_name = path_name
        
        # 當前庫採用的維度順序,包括rows,cols,channels,用於後續卷積神經網路模型中第一層卷積層的input_shape引數
        self.input_shape = None 
    
    # 載入資料集並按照交叉驗證的原則劃分資料集並進行相關預處理工作
    def load(self, img_rows = IMAGE_SIZE, img_cols = IMAGE_SIZE, img_channels = 3, nb_classes = 2):
        # 載入資料集到記憶體
        images, labels = load_dataset(self.path_name)
        
        # 注意下面資料集的劃分是隨機的,所以每次執行程式的訓練結果會不一樣
        train_images, test_images, train_labels, test_labels = train_test_split(images, labels, test_size = 0.3, random_state = random.randint(0, 100))
        #print(test_labels) 簡單來說 就是將images和labels陣列分別劃分為3:7 的兩部分

        # tensorflow 作為後端,資料格式約定是channel_last,與這裡資料本身的格式相符,如果是channel_first,就要對資料維度順序進行一下調整
        if K.image_data_format == `channel_first`: # (100,3,16,32)
            train_images = train_images.reshape(train_images.shape[0],img_channels, img_rows, img_cols)
            test_images = test_images.reshape(test_images.shape[0],img_channels, img_rows, img_cols)
            self.input_shape = (img_channels, img_rows, img_cols)
        else: # (100,16,32,3)
            train_images = train_images.reshape(train_images.shape[0], img_rows, img_cols, img_channels)
            test_images = test_images.reshape(test_images.shape[0], img_rows, img_cols, img_channels)
            self.input_shape = (img_rows, img_cols, img_channels)

        # 輸出訓練集和測試集的數量
        print(train_images.shape[0], `train samples`)
        print(test_images.shape[0], `test samples`)
        
        # 後面模型中會使用categorical_crossentropy作為損失函式,這裡要對類別標籤進行One-hot編碼
        train_labels = keras.utils.to_categorical(train_labels, nb_classes)
        test_labels = keras.utils.to_categorical(test_labels,nb_classes)

        train_images = train_images.astype(`float32`)
        test_images = test_images.astype(`float32`)

        # 影像歸一化,將影像的各畫素值歸一化到0~1區間。
        train_images /= 255
        test_images /= 255
        
        self.train_images = train_images
        self.test_images  = test_images
        self.train_labels = train_labels
        self.test_labels = test_labels
    
# 建立卷積神經網路模型
class Model:
    # 初始化構造方法
    def __init__(self):
        self.model = None
    # 建立模型
    def build_model(self, dataset, nb_classes = 2):
        self.model = Sequential()
        self.model.add(Conv2D(32, (3, 3), padding = `same`, input_shape = dataset.input_shape)) # 當使用該層作為模型第一層時,需要提供 input_shape 引數 (整數元組,不包含batch_size)
        self.model.add(Activation(`relu`))
        self.model.add(Conv2D(32, (3, 3)))
        self.model.add(Activation(`relu`))
        self.model.add(MaxPooling2D(pool_size = (2,2))) # strides預設等於pool_size
        self.model.add(Conv2D(64, (3, 3), padding = `same`))
        self.model.add(Activation(`relu`))
        self.model.add(Conv2D(64, (3, 3)))
        self.model.add(Activation(`relu`))
        self.model.add(MaxPooling2D(pool_size = (2,2)))
        self.model.add(Dropout(0.25))
        self.model.add(Flatten())
        self.model.add(Dense(512))
        self.model.add(Activation(`relu`))
        self.model.add(Dropout(0.25))
        self.model.add(Dense(nb_classes))
        self.model.add(Activation(`softmax`))
    # 訓練模型
    def train(self, dataset, batch_size = 128, nb_epoch = 15, data_augmentation = True):
        self.model.compile(loss = `categorical_crossentropy`, 
                           optimizer = `ADAM`,
                           metrics = [`accuracy`])
        if not data_augmentation:
            self.model.fit(dataset.train_images, 
                           dataset.train_labels, 
                           batch_size = batch_size,
                           epochs = nb_epoch, 
                           shuffle = True)
        # 影像預處理
        else:
            #是否使輸入資料去中心化(均值為0),是否使輸入資料的每個樣本均值為0,是否資料標準化(輸入資料除以資料集的標準差),是否將每個樣本資料除以自身的標準差,是否對輸入資料施以ZCA白化,資料提升時圖片隨機轉動的角度(這裡的範圍為0~20),資料提升時圖片水平偏移的幅度(單位為圖片寬度的佔比,0~1之間的浮點數),和rotation一樣在0~0.2之間隨機取值,同上,只不過這裡是垂直,隨機水平翻轉,不是對所有圖片翻轉,隨機垂直翻轉,同上
            # 每個epoch內都對每個樣本以以下配置生成一個對應的增強樣本,最終生成了1969*(1-0.3)=1378*10=13780個訓練樣本,因為下面配置的很多引數都是在一定範圍內隨機取值,因此每個epoch內生成的樣本都不一樣
            datagen = ImageDataGenerator(rotation_range = 20, 
                                         width_shift_range  = 0.2, 
                                         height_shift_range = 0.2, 
                                         horizontal_flip = True)                           
            #計算資料增強所需要的統計資料,計算整個訓練樣本集的數量以用於特徵值歸一化、ZCA白化等處理
#            當且僅當 featurewise_center 或 featurewise_std_normalization 或 zca_whitening 設定為 True 時才需要。
            #利用生成器開始訓練模型
            # flow方法輸入原始訓練資料,生成批量增強資料
            self.model.fit_generator(
                datagen.flow(dataset.train_images, dataset.train_labels, batch_size=batch_size),
                steps_per_epoch=dataset.train_images.shape[0] // batch_size,
                epochs=nb_epoch)
    
    def evaluate(self, dataset):
        score = self.model.evaluate(dataset.test_images, dataset.test_labels) # evaluate返回的結果是list,兩個元素分別是test loss和test accuracy
        print("%s: %.3f%%" % (self.model.metrics_names[1], score[1] * 100))  # 注意這裡.3f後面的第二個百分號就是百分號,其餘兩個百分號則是格式化輸出浮點數的語法。
    
    def save_model(self, file_path):
         self.model.save(file_path)
    def load_model(self, file_path):
        self.model = load_model(file_path)
    def face_predict(self, image):
        # 將探測到的人臉reshape為符合輸入要求的尺寸
        image = resize_image(image)
        image = image.reshape((1, IMAGE_SIZE, IMAGE_SIZE, 3))
        # 圖片浮點化並歸一化
        image = image.astype(`float32`) # float32    Single precision float: sign bit, 8 bits exponent, 23 bits mantissa
        image /= 255
        result = self.model.predict(image)
    #    print(`result:`, result)
    #    print(result.shape) # (1,2)
    #    print(type(result)) # <class `numpy.ndarray`>
        return result.argmax(axis=-1) #  The axis=-1 in numpy corresponds to the last dimension
if __name__ == `__main__`:
    dataset = Dataset(`./data/`)
    dataset.load()
    # 訓練模型
    model = Model()
    model.build_model(dataset)
    #測試訓練函式的程式碼
    model.train(dataset)
    model.evaluate(dataset)
    model.save_model(`./model/me.face.model.h5`) # 注意這裡要在工作目錄下先新建model資料夾,否則會報錯:Unable to create file,error message = `No such file or directory`
# face_recognition.py
import cv2
#import sys
from face_train_keras import Model
#載入模型
model = Model()
model.load_model(file_path = `./model/me.face.model.h5`)    
              
#框住人臉的矩形邊框顏色       
cv2.namedWindow(`Detecting your face.`) # 建立視窗
color = (0, 255, 0)
classifier = cv2.CascadeClassifier(`haarcascade_frontalface_alt2.xml`) # 載入分類器
#捕獲指定攝像頭的實時視訊流
cap = cv2.VideoCapture(0)
while cap.isOpened():
        ok, frame = cap.read() # type(frame) <class `numpy.ndarray`>
        if not ok:
            break
        
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 灰度化
        faceRects=classifier.detectMultiScale(gray,scaleFactor=1.2,minNeighbors=3,minSize=(32,32))
        if len(faceRects) > 0:                 
            for faceRect in faceRects: 
                x, y, w, h = faceRect
                
                #擷取臉部影像提交給模型識別這是誰
                image = frame[y - 10: y + h + 10, x - 10: x + w + 10]
                if image is None: # 有的時候可能是人臉探測有問題,會報錯 error (-215) ssize.width > 0 && ssize.height > 0 in function cv::resize,所以這裡要判斷一下image是不是None,防止極端情況 
                    break
                else:
                    faceID = model.face_predict(image)
#                print(faceID) # [0]
#                print(type(faceID)) # <class `numpy.ndarray`>
#                print(faceID.shape) # (1,)
#                #如果是“我”
                    if faceID[0] == 0:                                                        
                        cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, thickness = 2)
                    
                    #文字提示是誰
                        cv2.putText(frame,`me`, 
                                (x + 30, y + 30),                      #座標
                                cv2.FONT_HERSHEY_SIMPLEX,              #字型
                                1,                                     #字號
                                (255,0,255),                           #顏色
                                2)                                     #字的線寬
                    else:
                        cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, thickness = 2)
                            #文字提示是誰
                        cv2.putText(frame,`Unknown`, 
                                (x + 30, y + 30),                      #座標
                                cv2.FONT_HERSHEY_SIMPLEX,              #字型
                                1,                                     #字號
                                (255,0,255),                           #顏色
                                2)                                     #字的線寬
        cv2.imshow("Detecting your face.", frame)
        
        #等待10毫秒看是否有按鍵輸入
        k = cv2.waitKey(10)
        #如果輸入q則退出迴圈
        if k & 0xFF == ord(`q`):
            break

#釋放攝像頭並銷燬所有視窗
cap.release() 
cv2.destroyAllWindows()

 

相關文章