用Pytorch做人臉識別

完整代碼已經(jīng)上傳,可以關(guān)注微信公眾號 "老居搞機" 回復(fù)關(guān)鍵詞 "人臉識別" 獲取源代碼后直接運行,一邊吃著西瓜一邊看著這篇一邊Run著,效果最佳!

前言

人臉識別在生活中已經(jīng)越來越常見了,手機解鎖刷刷臉,進高鐵站刷個臉,去酒店入住刷個臉,就連跑了十幾年的逃犯最近也因為人臉識別的普及而屢屢被抓獲!

計算機是怎么認識一張臉對應(yīng)是哪個人的呢,聽上去很神奇,這篇就用Pytorch手把手教你做一個

準(zhǔn)備工作

在開始之前,先做一下準(zhǔn)備工作,列一張我們需要用到的清單:

    1. 一個攝像頭,用于拍攝人臉視頻
    1. Opencv軟件包,用于對人臉的圖像進行相應(yīng)的處理
    1. 卷積神經(jīng)網(wǎng)絡(luò)(CNN)知識, 如果不熟悉可以閱讀上一篇文章:卷積神經(jīng)網(wǎng)絡(luò)(CNN)
    1. Pytorch軟件包,用于創(chuàng)建CNN訓(xùn)練人臉識別模型的深度學(xué)習(xí)框架
    1. 兩張人臉,目的不言自明,本篇我用上了小居來充當(dāng)?shù)谰?/li>

人臉抓取

首先使用Opencv來獲取攝像頭的視頻流:

# -*- coding: utf-8 -*-
import cv2

def catch_video(tag, window_name='catch face', camera_idx=0):
    cv2.namedWindow(window_name)
  # 視頻來源,可以來自一段已存好的視頻,也可以直接來自攝像頭
    cap = cv2.VideoCapture(camera_idx)
    while cap.isOpened():
       # 讀取一幀數(shù)據(jù)
        ok, frame = cap.read()
        if not ok:
            break
      # 抓取人臉的方法, 后面介紹
        catch_face(frame, tag)
        # 輸入'q'退出程序
        cv2.imshow(window_name, frame)
        c = cv2.waitKey(1)
        if c & 0xFF == ord('q'):
            break
  # 釋放攝像頭并銷毀所有窗口
    cap.release()
    cv2.destroyAllWindows()

方法里面的參數(shù)介紹一下:

  • camera_id:這個就是攝像頭的設(shè)備索引號,一般是第一個0
  • tag:抓取誰的人臉的標(biāo)記, 后面根據(jù)tag來保存目錄
  • window_name:窗口的名字

從實時視頻流抓取人臉區(qū)域,這部分事情屬于目標(biāo)檢測,可以使用Faster-R-CNN等來做的,不過這里我們暫時不深入這部分,直接使用Opencv已有的函數(shù)來獲取人臉的區(qū)域即可:

def catch_face(frame, tag):
    # 告訴OpenCV使用人臉識別分類器
    classfier = cv2.CascadeClassifier("/Users/alan/.virtualenvs/face_recognize/lib/python2.7/site-packages/cv2/data/haarcascade_frontalface_alt2.xml")
    # 識別出人臉后要畫的邊框的顏色,RGB格式
    color = (0, 255, 0)
    # 將當(dāng)前幀轉(zhuǎn)換成灰度圖像
    grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)    
    # 人臉檢測,1.2和2分別為圖片縮放比例和需要檢測的有效點數(shù)
    face_rects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
    num = 1
    if len(face_rects) > 0: # 大于0則檢測到人臉
        # 圖片幀中有多個圖片,框出每一個人臉
        for face_rects in face_rects:
            x, y, w, h = face_rects
            image = frame[y - 10:y + h + 10, x - 10:x + w + 10]
            # 保存人臉圖像
            save_face(image, tag, num)
            cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, 2)
            num += 1

def save_face(image, tag, num):
  # DATA_TRAIN為抓取的人臉存放目錄,如果目錄不存在則創(chuàng)建
    makedir_exist_ok(os.path.join(DATA_TRAIN, str(tag)))
    img_name = os.path.join(DATA_TRAIN, str(tag), '{}_{}.jpg'.format(int(time.time()), num))
    # 保存人臉圖像到指定的位置, 其中會創(chuàng)建一個tag對應(yīng)的目錄,用于后面的分類訓(xùn)練
    cv2.imwrite(img_name, image)

來看一下抓取老居這張老臉的效果:

這里主要是Opencv幫我們做的人臉抓取, 解釋下這一行:

classfier = cv2.CascadeClassifier("/Users/alan/.virtualenvs/face_recognize/lib/python2.7/site-packages/cv2/data/haarcascade_frontalface_alt2.xml")
  • CascadeClassifier方法指定OpenCV選擇使用哪種分類器,OpenCV提供了多種分類器,也可以使用別的分類器看看效果, 這些分類器都放在對應(yīng)安裝包cv2/data/目錄下
face_rects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))

classfier.detectMultiScale()即是完成實際人臉識別工作的函數(shù)

該函數(shù)參數(shù)說明如下:

  • grey:要識別的圖像數(shù)據(jù)(即使不轉(zhuǎn)換成灰度也能識別,但是灰度圖可以降低計算強度,因為檢測的依據(jù)是哈爾特征,轉(zhuǎn)換后每個點的RGB數(shù)據(jù)變成了一維的灰度,這樣計算強度就減少很多)
  • scaleFactor:圖像縮放比例,可以理解為同一個物體與相機距離不同,其大小亦不同,必須將其縮放到一定大小才方便識別,該參數(shù)指定每次縮放的比例
  • minNeighbors:對特征檢測點周邊多少有效點同時檢測,這樣可避免因選取的特征檢測點太小而導(dǎo)致遺漏
  • minSize:特征檢測點的最小值

DATA_TRAIN 即抓取的人臉保存的位置,比如我放在項目目錄下的 data/train/下面,整體的配置參數(shù)如下:

# -*- encoding: utf8 -*-
import os

PROJECT_PATH = os.path.abspath(
    os.path.join(os.path.abspath(os.path.dirname(__file__)), os.pardir))

# 訓(xùn)練數(shù)據(jù)集
DATA_TRAIN = os.path.join(PROJECT_PATH, "data/train")
# 驗證數(shù)據(jù)集
DATA_TEST = os.path.join(PROJECT_PATH, "data/test")
# 模型保存地址
DATA_MODEL = os.path.join(PROJECT_PATH, "data/model")
  • DATA_TRAIN:訓(xùn)練集數(shù)據(jù)存放的目錄

  • DATA_TEST:驗證集數(shù)據(jù)存放的目錄

  • DATA_MODEL:模型保存目錄

一幀圖片里面可能會出現(xiàn)多張人臉,循環(huán)處理每張人臉圖片, 運行這個程序抓取到足夠人臉數(shù)據(jù)之后,到DATA_TRAIN對應(yīng)的目錄下將錯誤的和不清晰的圖片做一些刪減,只保留清晰的人臉圖片,并且拷一部分到DATA_TEST對應(yīng)的目錄下用作為驗證集使用, 然后就可以進入到我們的下一步了

數(shù)據(jù)預(yù)處理

上面一步我們已經(jīng)抓取到不同類型足夠的人臉圖片了,接下來我們加載這些圖片進行一些必要的預(yù)處理便于后面的模型訓(xùn)練,這里使用Pytorch框架。

Pytorch是Facebook開源的深度學(xué)習(xí)框架,具有先進的設(shè)計理念,提供的API簡單易用,上手非常容易,性能卓越,推薦使用(一波小廣告666)

準(zhǔn)備dataset,對圖片做一些預(yù)處理,如縮放到標(biāo)準(zhǔn)尺寸,歸一化,對圖像做一些增強如:旋轉(zhuǎn)、切割等(不過我們這里使用攝像頭都是垂直的,所以就不做圖像增強了),先上代碼再慢慢解釋:

# -*- encoding: utf8 -*-
import config
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.datasets import ImageFolder

def get_transform():
    return transforms.Compose([
            # 圖像縮放到32 x 32
            transforms.Resize(32),
            # 中心裁剪 32 x 32
            transforms.CenterCrop(32),
            transforms.ToTensor(),
            # 對每個像素點進行歸一化
            transforms.Normalize(mean=[0.4, 0.4, 0.4],
                                 std=[0.2, 0.2, 0.2])
        ])

def get_dataset(batch_size=10, num_workers=1):
    data_transform = get_transform()
    # load訓(xùn)練集圖片
    train_dataset = ImageFolder(root=config.DATA_TRAIN, transform=data_transform)
    # load驗證集圖片
    test_dataset = ImageFolder(root=config.DATA_TEST, transform=data_transform)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    return train_loader, test_loader

這里解釋一下幾段:

transforms.Compose([
            # 圖像縮放到32 x 32
            transforms.Resize(32),
            # 中心裁剪 32 x 32
            transforms.CenterCrop(32),
            transforms.ToTensor(),
            # 對每個像素點進行歸一化
            transforms.Normalize(mean=[0.4, 0.4, 0.4],
                                 std=[0.2, 0.2, 0.2])
        ])
  • transforms.Compose 這個類的主要作用是串聯(lián)多個圖片變換的操作
  • transforms.Resize 將圖像縮放到32x32,因為我們用opencv抓取的圖片尺寸是不一樣的,這里統(tǒng)一縮放到32x32
  • transforms.CenterCrop 進行中心剪裁
  • transforms.ToTensor 把像素灰度范圍從0-255變換到0-1之間
  • transforms.Normalize 對圖像進行歸一化, 把0-1變換到(-1,1).具體地說,對每個通道而言,Normalize執(zhí)行以下操作: img=(img-mean)/std
  # load訓(xùn)練集圖片
    train_dataset = ImageFolder(root=config.DATA_TRAIN, transform=data_transform)
    # load驗證集圖片
    test_dataset = ImageFolder(root=config.DATA_TEST, transform=data_transform)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers)
    return train_loader, test_loader

train_dataset為訓(xùn)練集數(shù)據(jù),test_dataset為驗證集數(shù)據(jù)

ImageFolder是Pytorch框架提供的圖片目錄加載類,可以極大的減少我們加載目錄中圖片的工作量,它主要有四個參數(shù):

  • root:在root指定的路徑下尋找圖片
  • transform:對PIL Image進行的轉(zhuǎn)換操作,transform的輸入是使用loader讀取圖片的返回對象,也就是我們上面指定的transforms
  • target_transform:對label的轉(zhuǎn)換
  • loader:給定路徑后如何讀取圖片,默認讀取為RGB格式的PIL Image對象

DataLoader:生成Pytorch訓(xùn)練時候使用的可迭代對象數(shù)據(jù),用到的參數(shù):

  • batch_size:每次迭代使用多少個樣本
  • shuffle:每次重新迭代時候,是否對數(shù)據(jù)進行隨機重新排序
  • num_workers:幾個進程來處理data loading

調(diào)用get_dataset() 方法來獲取訓(xùn)練集和驗證集,下一步針對這些數(shù)據(jù)進行訓(xùn)練

訓(xùn)練模型

我們使用卷積神經(jīng)網(wǎng)絡(luò)(CNN)來識別人臉,關(guān)于卷積神經(jīng)網(wǎng)絡(luò)的詳細說明可以看上一篇:卷積神經(jīng)網(wǎng)絡(luò)(CNN)

創(chuàng)建用于人臉識別的卷積神經(jīng)網(wǎng)絡(luò):

# -*- encoding: utf8 -*-
from torch import nn

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 第一層卷積->激活->池化->Dropout
        self.conv1 = nn.Sequential(
            nn.Conv2d(
                in_channels=3,
                out_channels=16,
                kernel_size=5,
                stride=1,
                padding=2,
            ),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),
            nn.Dropout(0.2)
        )
        # 第二層卷積->激活->池化->Dropout
        self.conv2 = nn.Sequential(
            nn.Conv2d(16, 32, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Dropout(0.2)
        )
        # 全連接層 
        self.out = nn.Linear(32 * 8 * 8, 2)
        
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)
        x = self.out(x)
        # 對結(jié)果進行l(wèi)og + softmax并輸出
        return F.log_softmax(x, dim=1)

解釋一下主要的幾個方法:

self.conv1 = nn.Sequential(
            nn.Conv2d(
                in_channels=3,
                out_channels=16,
                kernel_size=5,
                stride=1,
                padding=2,
            ),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),
            nn.Dropout(0.2)
        )
  • nn.Conv2d:卷積計算,in_channels輸入的RGB圖像為3層,out_channels輸出16層, kernel_size卷積核為5x5, stride步長為1x1, 也就是每次卷積計算自后向右(下)移動一位, padding圖像周圍加2x2的邊框, 卷積核為5x5, 圖片加上2x2的邊框之后卷積計算得到的結(jié)果還是32x32大小的圖片(32 - 5 + 2 + 2)
  • nn.ReLU:使用ReLU激活函數(shù)
  • nn.MaxPool2d:池化層2x2大小,32x32大小的圖片經(jīng)過2x2的池化之后就是16x16(32/2, 32/2)大小的圖片了
  • nn.Dropout:隨機丟棄0.2的神經(jīng)元權(quán)重, Dropout是深度神經(jīng)網(wǎng)絡(luò)防止過擬合很好用的方法(所謂過擬合是指在測試/驗證集上表現(xiàn)比訓(xùn)練集上預(yù)測效果差很多)

self.conv2和self.conv1一樣也是 卷積->激活->池化->Dropout

全連接層 32x32的圖片經(jīng)過兩次池化之后大小為8x8,上一層卷積輸出是32層,所以總共的大小就是 32 x 8 x 8個神經(jīng)元, 輸出2個分類(如果有多個人輸出可以改成更多)

self.out = nn.Linear(32 * 8 * 8, 2)

forward是Pytorch神經(jīng)網(wǎng)絡(luò)的推理過程, 將前面的計算串聯(lián)在一起計算結(jié)果:

def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)
        x = self.out(x)
        return F.log_softmax(x, dim=1)
  • F.log_softmax:對結(jié)果取log softmax

下面使用這個神經(jīng)網(wǎng)絡(luò)來訓(xùn)練已經(jīng)準(zhǔn)備好的數(shù)據(jù):

# -*- encoding: utf8 -*-
import config
import os
import torch
from data_set import get_dataset, get_transform
from model import Net
import torch.nn.functional as F

# 檢查是否有GPU
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def train_model():
    train_loader, test_loader = get_dataset(batch_size=config.BATCH_SIZE)
    net = Net().to(DEVICE)
    # 使用Adam優(yōu)化器
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    for epoch in range(config.EPOCHS):
        for step, (x, y) in enumerate(train_loader):
            x, y = x.to(DEVICE), y.to(DEVICE)
            output = net(x)
            # 使用最大似然 / log似然代價函數(shù)
            loss = F.nll_loss(output, y)
            # Pytorch會梯度累計所以需要梯度清零
            optimizer.zero_grad()
            # 反向傳播
            loss.backward()
            # 使用Adam進行梯度更新
            optimizer.step()

            if (step + 1) % 3 == 0:
                print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                    epoch + 1, step * len(x), len(train_loader.dataset),
                    100. * step / len(train_loader), loss.item()))
    # 使用驗證集查看模型效果
    test(net, test_loader)
    # 保存模型權(quán)重到 config.DATA_MODEL目錄
    torch.save(net.state_dict(), os.path.join(config.DATA_MODEL, config.DEFAULT_MODEL))
    return net

這里需要重點說明的是:

# 檢查是否有GPU
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  • 檢查是否有GPU, 如果有GPU則使用cuda, 后面模型和數(shù)據(jù)可以使用如Net().to(DEVICE)轉(zhuǎn)為GPU的Tensor
loss = F.nll_loss(output, y)
# 使用Adam優(yōu)化器
optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
  • 使用Adam優(yōu)化器,后面執(zhí)行optimizer.step()進行梯度更新,關(guān)于優(yōu)化器可以看之前寫的這篇:帶動量的梯度下降
# 保存模型權(quán)重到 config.DATA_MODEL目錄
torch.save(net.state_dict(), os.path.join(config.DATA_MODEL, config.DEFAULT_MODEL))
  • 保存訓(xùn)練好的模型權(quán)重到config.DATA_MODEL目錄,如我這邊是放在 data/model目錄下

使用驗證集數(shù)據(jù)用于驗證模型的好壞,關(guān)于模型好壞的評價指標(biāo),可以看之前的這篇文章: 機器學(xué)習(xí)的效果評價指標(biāo)

def test(model, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for x, y in test_loader:
            x, y = x.to(DEVICE), y.to(DEVICE)
            output = model(x)
            test_loss += F.nll_loss(output, y, reduction='sum').item()
            pred = output.max(1, keepdim=True)[1]
            correct += pred.eq(y.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    print '\ntest loss={:.4f}, accuracy={:.4f}\n'.format(test_loss, float(correct) / len(test_loader.dataset))
  • loss:驗證集合上的損失
  • accuracy:驗證集上的準(zhǔn)確率

創(chuàng)建文件main.py作為入口文件,執(zhí)行一下看看效果:

# -*- encoding: utf8 -*-
import fire
import logging
from face.catch_face import catch_video
from model.train import train_model

def catch(tag):
    catch_video(tag)
    logging.info("catch_face done.")

def train():
    train_model()
    logging.info("train done.")

if __name__ == "__main__":
    logging.getLogger().setLevel(logging.INFO)
    fire.Fire()

訓(xùn)練模型

  • $ python main.py train

此時我們就訓(xùn)練好了Pytorch模型,模型的參數(shù)權(quán)重保存在了data/model目錄下,下一步使用這個模型來識別一下攝像頭中抓取的人臉

人臉識別

好了到了我們的最后一步了,抓取攝像頭的人臉并放到訓(xùn)練的模型中識別出對應(yīng)的人:

def recognize_video(window_name='face recognize', camera_idx=0):
    cv2.namedWindow(window_name)
    cap = cv2.VideoCapture(camera_idx)
    while cap.isOpened():
        ok, frame = cap.read()
        if not ok:
            break
        catch_frame = catch_face(frame)
        cv2.imshow(window_name, catch_frame)
        c = cv2.waitKey(1)
        if c & 0xFF == ord('q'):
            break
    cap.release()
    cv2.destroyAllWindows()

def catch_face(frame):
    classfier = cv2.CascadeClassifier("/Users/alan/.virtualenvs/kepler/lib/python2.7/site-packages/cv2/data/haarcascade_frontalface_alt2.xml")
    color = (0, 255, 0)
    grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    face_rects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
    if len(face_rects) > 0:
        for face_rects in face_rects:
            x, y, w, h = face_rects
            image = frame[y - 10:y + h + 10, x - 10:x + w + 10]
            # opencv 2 PIL格式圖片
            PIL_image = cv2pil(image)
            # 使用模型進行人臉識別
            label = predict_model(PIL_image)
            cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, 2)
            # 將人臉對應(yīng)人名寫到圖片上, 以為是中文名所以需要加載中文字體庫
            frame = paint_chinese_opencv(frame, FACE_LABEL[label], (x-10, y+h+10), color)

    return frame

這一步跟第一步的抓取人臉Opencv用法差不多,就不多說明了

因為抓取攝像頭圖片使用的Opencv, Pytorch識別圖像使用PIL格式,所以需要做個轉(zhuǎn)換:

def cv2pil(image):
    return Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))

加載我們現(xiàn)有的模型進行預(yù)測:

def predict_model(image):
    data_transform = get_transform()
    # 對圖片進行預(yù)處理,同訓(xùn)練的時候一樣
    image = data_transform(image)
    image = image.view(-1, 3, 32, 32)
    net = Net().to(DEVICE)
    # 加載模型參數(shù)權(quán)重
    net.load_state_dict(torch.load(os.path.join(config.DATA_MODEL, config.DEFAULT_MODEL)))
    output = net(image.to(DEVICE))
    # 獲取最大概率的下標(biāo)
    pred = output.max(1, keepdim=True)[1]
    return pred.item()
  • 重點說一下net.load_state_dict()方法,用于加載剛才我們訓(xùn)練的模型參數(shù)權(quán)重,這是預(yù)測新圖片的關(guān)鍵

對預(yù)測出來的人臉類型在圖像上進行打標(biāo),這里為了支持中文引用Songti.ttc字體:

def paint_chinese_opencv(im, chinese, pos, color):
    img_PIL = Image.fromarray(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
    # 引用字體庫
    font = ImageFont.truetype('/Library/Fonts/Songti.ttc', 20)
    fillColor = color
    position = pos
    if not isinstance(chinese, unicode):
        chinese = chinese.decode('utf-8')
    draw = ImageDraw.Draw(img_PIL)
    # 寫上人臉對應(yīng)的人名
    draw.text(position, chinese, font=font, fill=fillColor)
    img = cv2.cvtColor(np.asarray(img_PIL), cv2.COLOR_RGB2BGR)
    return img

在main.py文件里面新增方法:

from face.recognize_face import recognize_video

def predict():
    recognize_video()
    logging.info("predict done.")

執(zhí)行一下進行人臉識別:

  • $ python main.py predict

這時它已經(jīng)能夠從攝像頭拍攝的實時視頻流中找出哪一個是老居,哪一個是小居了

參考


關(guān)注公眾號
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容