完整代碼已經(jīng)上傳,可以關(guān)注微信公眾號 "老居搞機" 回復(fù)關(guān)鍵詞 "人臉識別" 獲取源代碼后直接運行,一邊吃著西瓜一邊看著這篇一邊Run著,效果最佳!
前言
人臉識別在生活中已經(jīng)越來越常見了,手機解鎖刷刷臉,進高鐵站刷個臉,去酒店入住刷個臉,就連跑了十幾年的逃犯最近也因為人臉識別的普及而屢屢被抓獲!
計算機是怎么認識一張臉對應(yīng)是哪個人的呢,聽上去很神奇,這篇就用Pytorch手把手教你做一個
準(zhǔn)備工作
在開始之前,先做一下準(zhǔn)備工作,列一張我們需要用到的清單:
- 一個攝像頭,用于拍攝人臉視頻
- Opencv軟件包,用于對人臉的圖像進行相應(yīng)的處理
- 卷積神經(jīng)網(wǎng)絡(luò)(CNN)知識, 如果不熟悉可以閱讀上一篇文章:卷積神經(jīng)網(wǎng)絡(luò)(CNN)
- Pytorch軟件包,用于創(chuàng)建CNN訓(xùn)練人臉識別模型的深度學(xué)習(xí)框架
- 兩張人臉,目的不言自明,本篇我用上了小居來充當(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)
- 最大似然 / log似然代價函數(shù),關(guān)于損失函數(shù)可以看之前寫的這篇:機器學(xué)習(xí)常用的損失函數(shù)
# 使用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)能夠從攝像頭拍攝的實時視頻流中找出哪一個是老居,哪一個是小居了
參考
- [1] 陳云 <<深度學(xué)習(xí)框架Pytorch: 入門與實踐>>
- [2] 李沐 <<動手學(xué)深度學(xué)習(xí)>>
- [3] https://www.cnblogs.com/neo-T/p/6426029.html