天天看點

Python3+OpenCV+TensorFlow進行人臉識别前言

Python3+OpenCV+TensorFlow進行人臉識别

前言

最近也是新開始研究深度學習,準備從比較成熟的人臉識别開始,但是發現網上的代碼版本都比較老了,是以把自己踩完坑的新版本更新上來,友善大家參考一下。

Python版本:3.7.0

TensorFlow版本:2.3.1

OpenCV版本:4.4.0

Keras版本:2.4.3

參考原文1:https://blog.csdn.net/qq_42633819/article/details/81191308

參考原文2:https://blog.csdn.net/weilixin88/article/details/90680777

但是在後續修改過程中,檢視keras文檔時發現最早的模闆可能出自keras模闆代碼

0、工作目錄層級

整個目錄層級是預先手動建好的,如果有需要自動建目錄的可以自己加代碼,但是因為屬于python基礎操作,就不再贅述了。

Python3+OpenCV+TensorFlow進行人臉識别前言

1、擷取資料

這一塊因為不是重點,是以隻是簡單修改了前人的代碼結構,例如把可能需要修改的參數都放到了入口函數裡,移除了多餘的空行,把能放下的注釋都放到了後面等,使整個代碼更緊湊簡潔。

import cv2


def catch_pic_from_video(window_name, camera_idx, catch_pic_num, classifier_path_name, pic_path_name):
    cv2.namedWindow(window_name)
    cap = cv2.VideoCapture(camera_idx, cv2.CAP_DSHOW)  # 視訊來源,直接來自USB攝像頭
    classifier = cv2.CascadeClassifier(classifier_path_name)  # 告訴OpenCV使用人臉識别分類器
    color = (0, 255, 0)  # 識别出人臉後要畫的邊框的顔色,RGB格式
    num = 0
    while cap.isOpened():
        ok, frame = cap.read()  # 讀取一幀資料
        if not ok:
            break
        grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 将目前桢圖像轉換成灰階圖像
        # 人臉檢測,1.2和2分别為圖檔縮放比例和需要檢測的有效點數
        face_rects = classifier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
        if len(face_rects) > 0:  # 大于0則檢測到人臉
            for face_rect in face_rects:  # 單獨框出每一張人臉
                x, y, w, h = face_rect
                img_name = '%s/%d.jpg' % (pic_path_name, num)  # 将目前幀儲存為圖檔
                image = frame[y - 10: y + h + 10, x - 10: x + w + 10]
                cv2.imwrite(img_name, image)
                num += 1
                if num >= catch_pic_num:  # 如果超過指定最大儲存數量退出循環
                    break
                cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, 2)  # 畫出矩形框
                # 顯示目前捕捉到了多少人臉圖檔了,這樣站在那裡被拍攝時心裡有個數,不用兩眼一抹黑傻等着
                font = cv2.FONT_HERSHEY_SIMPLEX
                cv2.putText(frame, 'num:%d' % num, (x + 30, y + 30), font, 1, (255, 0, 255), 4)
        # 超過指定最大儲存數量結束程式
        if num >= catch_pic_num:
            break
        cv2.imshow(window_name, frame)  # 顯示圖像
        c = cv2.waitKey(10)
        if c & 0xFF == ord('q'):
            break
    cap.release()  # 釋放攝像頭并銷毀所有視窗
    cv2.destroyAllWindows()


if __name__ == '__main__':
    classifier_path = 'C:\\Users\\A\\Desktop\\TF_CV\\haarcascade_frontalface_alt2.xml'
    pic_path = 'C:\\Users\\A\\Desktop\\TF_CV\\faces\\others'
    catch_pic_from_video("Get Face", 0, 1024, classifier_path, pic_path)

           

2、加載資料

這一塊的修改也很少,和參考的代碼幾乎一緻,也隻有一些格式上的修改。

import os
import numpy as np
import cv2

IMAGE_SIZE = 64


# 按照指定圖像大小調整尺寸
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)  # 對于長寬不相等的圖檔,找到最長的一邊
    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
    black = [0, 0, 0]
    # 給圖像增加邊界,是圖檔長、寬等長,cv2.BORDER_CONSTANT指定邊界顔色由value指定
    constant = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=black)
    return cv2.resize(constant, (height, width))  # 調整圖像大小并傳回


images = []
labels = []


def read_path(path_name):
    for dir_item in os.listdir(path_name):  # 從初始路徑開始疊加,合并成可識别的操作路徑
        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)
                image = resize_image(image, IMAGE_SIZE, IMAGE_SIZE)
                # cv2.imwrite('1.jpg', image)  # 放開這行代碼,可以看到resize_image()函數的實際調用效果
                images.append(image)
                labels.append(path_name)
    return images, labels


# 從指定路徑讀取訓練資料
def load_dataset(path_name):
    images, labels = read_path(path_name)
    # 将輸入的所有圖檔轉成四維數組,尺寸為(圖檔數量*IMAGE_SIZE*IMAGE_SIZE*3)
    # 例如共1200張圖檔,IMAGE_SIZE為64,故尺寸為1200 * 64 * 64 * 3
    # 圖檔為64 * 64像素,一個像素3個顔色值(RGB)
    images_array = np.array(images)
    print(images_array.shape)
    # 标注資料,'me'檔案夾下都是自己圖像,全部指定為0;另外一個檔案夾下是别人的,全部指定為1
    # 如果要訓練多個值,需要修改這個點,增加多個label_name和label_index的對應
    labels_array = np.array([0 if label.endswith('me') else 1 for label in labels])
    return images_array, labels_array


if __name__ == '__main__':
    images, labels = load_dataset("C:\\Users\\A\\Desktop\\TF_CV\\faces")

           

3、模型訓練

修改點如下:

  1. “from sklearn.cross_validation import train_test_split”改為了“from sklearn.model_selection import train_test_split”,因為随着版本變化,“cross_validation”已經棄用,改為了“model_selection”。
  2. “from keras.layers import Convolution2D”改為了“from keras.layers import Conv2D”,呃…其實他們就是同一個東西,别名而已,但是這個短一點。
  3. “if K.image_dim_ordering() == ‘th’”改為了“if K.image_data_format() == ‘channels_first’”,因為随着keras版本的更新,舊的值已經不能用了。現在背景如果是Tensorflow,那麼這個值就是’channels_last’,其他的背景則是’channels_first’。
  4. 在build_mode函數中,舊版本的代碼運作好像會有報錯,是以參考新版本的函數示例代碼進行了修改,但是整個模型的結構和參數是沒有改動的。
  5. 在train函數中,把2021-01-01即将廢棄的“model.fit_generator”修改為了“model.fit”,參數名稱可能也有部分随之進行了修改。
  6. 在do_predict函數中,把2021-01-01即将廢棄的“result = self.model.predict_classes(image) ”修改為了“result = np.argmax(self.model.predict(image), axis=1)”,這是新的擷取預測結果的方法。

主要的修改點就是以上幾點,可能有些遺漏。

import random
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
from keras.layers import Conv2D, MaxPooling2D
from keras.optimizers import SGD
from keras.utils import np_utils
from keras.models import load_model
from keras import backend as K
from load_data import load_dataset, resize_image, IMAGE_SIZE


class Dataset:
    def __init__(self, path_name):
        # 訓練集
        self.train_images = None
        self.train_labels = None
        # 驗證集
        self.valid_images = None
        self.valid_labels = None
        # 測試集
        self.test_images = None
        self.test_labels = None
        # 資料集加載路徑
        self.path_name = path_name
        # 目前庫采用的次元順序
        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, valid_images, train_labels, valid_labels = train_test_split(images, labels, test_size=0.3,
                                                                                  random_state=random.randint(0, 100))
        _, test_images, _, test_labels = train_test_split(images, labels, test_size=0.5,
                                                          random_state=random.randint(0, 100))

        # 目前的次元順序如果為'th',則輸入圖檔資料時的順序為:channels,rows,cols,否則:rows,cols,channels
        # 這部分代碼就是根據keras庫要求的次元順序重組訓練資料集
        if K.image_data_format() == 'channels_first':
            train_images = train_images.reshape(train_images.shape[0], img_channels, img_rows, img_cols)
            valid_images = valid_images.reshape(valid_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:
            train_images = train_images.reshape(train_images.shape[0], img_rows, img_cols, img_channels)
            valid_images = valid_images.reshape(valid_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(valid_images.shape[0], 'valid samples')
            print(test_images.shape[0], 'test samples')
            # 我們的模型使用categorical_crossentropy作為損失函數,是以需要根據類别數量nb_classes将
            # 類别标簽進行one-hot編碼使其向量化,在這裡我們的類别隻有兩種,經過轉化後标簽資料變為二維
            train_labels = np_utils.to_categorical(train_labels, nb_classes)
            valid_labels = np_utils.to_categorical(valid_labels, nb_classes)
            test_labels = np_utils.to_categorical(test_labels, nb_classes)
            # 像素資料浮點化以便歸一化
            train_images = train_images.astype('float32')
            valid_images = valid_images.astype('float32')
            test_images = test_images.astype('float32')
            # 将其歸一化,圖像的各像素值歸一化到0~1區間
            train_images /= 255
            valid_images /= 255
            test_images /= 255
            self.train_images = train_images
            self.valid_images = valid_images
            self.test_images = test_images
            self.train_labels = train_labels
            self.valid_labels = valid_labels
            self.test_labels = test_labels


# CNN網絡模型類
class Model:
    def __init__(self):
        self.model = None

    # 建立模型
    def build_model(self, dataset, nb_classes=2):
        # 建構一個空的網絡模型,它是一個線性堆疊模型,各神經網絡層會被順序添加,專業名稱為序貫模型或線性堆疊模型
        self.model = Sequential()
        # 1 二維卷積層+激活函數層
        self.model.add(Conv2D(32, (3, 3), padding='same', input_shape=dataset.input_shape, activation='relu'))
        self.model.add(Conv2D(32, (3, 3), activation='relu'))  # 2 二維卷積層+激活函數層
        self.model.add(MaxPooling2D((2, 2)))  # 3 池化層
        self.model.add(Dropout(0.25))  # 4 Dropout層
        self.model.add(Conv2D(64, (3, 3), padding='same', activation='relu'))  # 5 二維卷積層+激活函數層
        self.model.add(Conv2D(64, (3, 3), activation='relu'))  # 6 二維卷積層+激活函數層
        self.model.add(MaxPooling2D((2, 2)))  # 7 池化層
        self.model.add(Dropout(0.25))  # 8 Dropout層
        self.model.add(Flatten())  # 9 Flatten層
        self.model.add(Dense(512))  # 10 Dense層,又被稱作全連接配接層
        self.model.add(Activation('relu'))  # 11 激活函數層
        self.model.add(Dropout(0.5))  # 12 Dropout層
        self.model.add(Dense(nb_classes))  # 13 Dense層
        self.model.add(Activation('softmax'))  # 14 分類層,輸出最終結果
        self.model.summary()  # 輸出模型概況

    # 訓練模型
    def train(self, dataset, batch_size=20, epoch=20, data_augmentation=True):
        sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)  # 采用SGD+momentum的優化器進行訓練,首先生成一個優化器對象
        self.model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])  # 完成實際的模型配置工作
        # 不使用資料提升,所謂的提升就是從我們提供的訓練資料中利用旋轉、翻轉、加噪聲等方法創造新的
        # 訓練資料,有意識的提升訓練資料規模,增加模型訓練量
        if not data_augmentation:
            self.model.fit(dataset.train_images,
                           dataset.train_labels,
                           batch_size=batch_size,
                           nb_epoch=epoch,
                           validation_data=(dataset.valid_images, dataset.valid_labels),
                           shuffle=True)
        # 使用實時資料提升
        else:
            # 定義資料生成器用于資料提升,其傳回一個生成器對象datagen,datagen每被調用一
            # 次其生成一組資料(順序生成),節省記憶體,其實就是python的資料生成器
            datagen = ImageDataGenerator(
                featurewise_center=False,  # 是否使輸入資料去中心化(均值為0),
                samplewise_center=False,  # 是否使輸入資料的每個樣本均值為0
                featurewise_std_normalization=False,  # 是否資料标準化(輸入資料除以資料集的标準差)
                samplewise_std_normalization=False,  # 是否将每個樣本資料除以自身的标準差
                zca_whitening=False,  # 是否對輸入資料施以ZCA白化
                rotation_range=20,  # 資料提升時圖檔随機轉動的角度(範圍為0~180)
                width_shift_range=0.2,  # 資料提升時圖檔水準偏移的幅度(機關為圖檔寬度的占比,0~1之間的浮點數)
                height_shift_range=0.2,  # 同上,隻不過這裡是垂直
                horizontal_flip=True,  # 是否進行随機水準翻轉
                vertical_flip=False)  # 是否進行随機垂直翻轉
            # 計算整個訓練樣本集的數量以用于特征值歸一化、ZCA白化等處理
            datagen.fit(dataset.train_images)
            # 利用生成器開始訓練模型
            self.model.fit(datagen.flow(dataset.train_images, dataset.train_labels, batch_size=batch_size),
                           batch_size=batch_size, epochs=epoch,
                           steps_per_epoch=dataset.train_images.shape[0]//batch_size,
                           validation_data=(dataset.valid_images, dataset.valid_labels))

    def save_model(self, file_path):
        self.model.save(file_path)

    def load_model(self, file_path):
        self.model = load_model(file_path)

    def evaluate(self, dataset):
        score = self.model.evaluate(dataset.test_images, dataset.test_labels, verbose=1)
        print("%s: %.2f%%" % (self.model.metrics_names[1], score[1] * 100))

    # 識别面部
    def do_predict(self, image):
        # 依然是根據後端系統确定次元順序
        if K.image_data_format() == 'channels_first' and image.shape != (1, 3, IMAGE_SIZE, IMAGE_SIZE):
            image = resize_image(image)  # 尺寸必須與訓練集一緻都應該是IMAGE_SIZE x IMAGE_SIZE
            image = image.reshape((1, 3, IMAGE_SIZE, IMAGE_SIZE))  # 與模型訓練不同,這次隻是針對1張圖檔進行預測
        elif K.image_data_format() == 'channels_last' and image.shape != (1, IMAGE_SIZE, IMAGE_SIZE, 3):
            image = resize_image(image)
            image = image.reshape((1, IMAGE_SIZE, IMAGE_SIZE, 3))
        image = image.astype('float32')  # 浮點并歸一化
        image /= 255
        result = np.argmax(self.model.predict(image), axis=1)  # 給出類别預測:0或者1,如果有多個label_index也可以
        print('result: %d' % result)
        return result[0]  # 傳回類别預測結果


if __name__ == '__main__':
    dataset = Dataset('./faces/')
    dataset.load()
    model = Model()
    model.build_model(dataset)
    model.train(dataset)
    model.save_model(file_path='./model/test.faces.model.h5')
    model.evaluate(dataset)

           

在模型訓練這一步,我一共準備了2個人,約共2000個樣本,部分運作結果如下:

(1863, 64, 64, 3)

1304 train samples

559 valid samples

932 test samples

Total params: 6,489,634

Trainable params: 6,489,634

Non-trainable params: 0

Epoch 19/20

65/65 [] - 21s 323ms/step - loss: 0.0296 - accuracy: 0.9945 - val_loss: 0.0027 - val_accuracy: 0.9982

Epoch 20/20

65/65 [] - 21s 323ms/step - loss: 0.0307 - accuracy: 0.9891 - val_loss: 1.8013e-04 - val_accuracy: 1.0000

30/30 [==============================] - 3s 98ms/step - loss: 3.8547e-04 - accuracy: 1.0000

accuracy: 100.00%

可以看到評估準确率是100%,實際使用中準确度也還可以,但是絕對達不到100%(捂臉)

4、人臉識别

還是少量修改,主要是代碼結構方面的。

import cv2
from training import Model


def rec_face(model_path, classifier_path):
    model = Model()  # 加載模型
    model.load_model(file_path=model_path)
    cap = cv2.VideoCapture(0)  # 捕獲指定攝像頭的實時視訊流
    color = (0, 0, 255)  # 框住人臉的矩形邊框顔色
    while True:  # 循環檢測識别人臉
        ret, frame = cap.read()  # 讀取一幀視訊
        if ret is True:
            frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 圖像灰化,降低計算複雜度
        else:
            continue
        cascade = cv2.CascadeClassifier(classifier_path)  # 使用人臉識别分類器,讀入分類器
        # 利用分類器識别出哪個區域為人臉
        face_rects = cascade.detectMultiScale(frame_gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
        if len(face_rects) > 0:
            for face_rect in face_rects:
                x, y, w, h = face_rect
                image = frame[y - 10: y + h + 10, x - 10: x + w + 10]  # 截取臉部圖像送出給模型識别這是誰
                face_id = model.do_predict(image)
                if face_id == 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_DUPLEX,  # 字型
                                1,  # 字号
                                (0, 255, 0),  # 顔色
                                2)  # 字的線寬
                elif face_id == 1:
                    cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, thickness=2)
                    cv2.putText(frame, 'Others',
                                (x + 30, y + 30),  # 坐标
                                cv2.FONT_HERSHEY_DUPLEX,  # 字型
                                1,  # 字号
                                (0, 255, 255),  # 顔色
                                2)  # 字的線寬
        cv2.imshow("Recognition", frame)
        k = cv2.waitKey(10)  # 等待10毫秒看是否有按鍵輸入
        if k & 0xFF == ord('q'):  # 如果輸入q則退出循環
            break
    cap.release()  # 釋放攝像頭并銷毀所有視窗
    cv2.destroyAllWindows()


if __name__ == '__main__':
    model_path = './model/test.faces.model.h5'  # training腳本生成的model檔案路徑
    classifier_path = 'C:\\Users\\A\\Desktop\\TF_CV\\haarcascade_frontalface_alt2.xml'  # 人臉識别分類器本地存儲路徑
    rec_face(model_path, classifier_path)

           

最後,祝大家碼運昌隆…

繼續閱讀