人臉識(shí)別和MTCNN模型 原創(chuàng)
前言
在上一章課程【???使用YOLO進(jìn)行目標(biāo)檢測(cè)??】,我們了解到目標(biāo)檢測(cè)有兩種策略,一種是以YOLO為代表的策略:特征提取→切片→分類回歸;另外一種是以MTCNN為代表的策略:先圖像切片→特征提取→分類和回歸。因此,本章內(nèi)容將深入了解MTCNN模型,包括:MTCNN的模型組成、模型訓(xùn)練過程、模型預(yù)測(cè)過程等。
人臉識(shí)別
在展開了解MTCNN之前,我們對(duì)人臉檢測(cè)先做一個(gè)初步的梳理和了解。人臉識(shí)別細(xì)分有兩種:人臉檢測(cè)和人臉身份識(shí)別。
人臉檢測(cè)
簡述
人臉檢測(cè)是一個(gè)重要的應(yīng)用領(lǐng)域,它通常用于識(shí)別圖像或視頻中的人臉,并定位其位置。
識(shí)別過程
- 輸入圖像:首先,將包含人臉的圖像輸入到人臉檢測(cè)模型中。
- 特征提取:深度學(xué)習(xí)模型將學(xué)習(xí)提取圖像中的特征,以便識(shí)別人臉。
- 人臉定位:模型通過在圖像中定位人臉的位置,通常使用矩形邊界框來框定人臉區(qū)域。
- 輸出結(jié)果:最終輸出包含人臉位置信息的結(jié)果,可以是邊界框的坐標(biāo)或其他形式的標(biāo)注。
輸入輸出
- 輸入:一張圖像
- 輸出:所有人臉的坐標(biāo)框
應(yīng)用場(chǎng)景
- 表情識(shí)別:識(shí)別人臉的表情,如快樂、悲傷等。
- 年齡識(shí)別:根據(jù)人臉特征推斷出人的年齡段。
- 人臉表情生成:通過檢測(cè)到的人臉生成不同的表情。
- ...
人臉檢測(cè)特點(diǎn)
人臉檢測(cè)是目標(biāo)檢測(cè)中最簡單的任務(wù)
- 類別少
- 人臉形狀比較固定
- 人臉特征比較固定
- 周圍環(huán)境一般比較好
人臉身份識(shí)別
簡述
人臉身份識(shí)別是指通過識(shí)別人臉上的獨(dú)特特征來確定一個(gè)人的身份。
識(shí)別過程
人臉錄入流程:
- 數(shù)據(jù)采集:采集包含人臉的圖像數(shù)據(jù)集。
- 人臉檢測(cè):使用人臉檢測(cè)算法定位圖像中的人臉區(qū)域。
- 人臉特征提?。和ㄟ^深度學(xué)習(xí)模型提取人臉圖像的特征向量。
- 特征向量存儲(chǔ):將提取到的特征向量存儲(chǔ)在向量數(shù)據(jù)庫中。
人臉驗(yàn)證流程:
- 人臉檢測(cè):使用人臉檢測(cè)算法定位圖像中的人臉區(qū)域。
- 人臉特征提?。和ㄟ^深度學(xué)習(xí)模型提取人臉圖像的特征向量。
- 人臉特征匹配:將輸入人臉的特征向量與向量數(shù)據(jù)庫中的特征向量進(jìn)行匹配。
- 身份識(shí)別:根據(jù)匹配結(jié)果確定輸入人臉的身份信息。
應(yīng)用領(lǐng)域
- 安防監(jiān)控:用于門禁系統(tǒng)、監(jiān)控系統(tǒng)等,實(shí)現(xiàn)人臉識(shí)別進(jìn)出控制。
- 移動(dòng)支付:通過人臉識(shí)別來進(jìn)行身份驗(yàn)證,實(shí)現(xiàn)安全的移動(dòng)支付功能。
- 社交媒體:用于自動(dòng)標(biāo)記照片中的人物,方便用戶管理照片。
- 人機(jī)交互:實(shí)現(xiàn)人臉識(shí)別登錄、人臉解鎖等功能。
一般來說,一切目標(biāo)檢測(cè)算法都可以做人臉檢測(cè),但是由于通用目標(biāo)檢測(cè)算法做人臉檢測(cè)太重了,所以會(huì)使用專門的人臉識(shí)別算法,而MTCNN就是這樣一個(gè)輕量級(jí)和專業(yè)級(jí)的人臉檢測(cè)網(wǎng)絡(luò)。
MTCNN模型
簡介
MTCNN(Multi-Task Cascaded Convolutional Neural Networks)是一種用于人臉檢測(cè)和面部對(duì)齊的神經(jīng)網(wǎng)絡(luò)模型。
論文地址:https://arxiv.org/abs/1604.02878v1
模型結(jié)構(gòu)
- MTCNN采用了級(jí)聯(lián)結(jié)構(gòu),包括三個(gè)階段的深度卷積網(wǎng)絡(luò),分別用于人臉檢測(cè)和面部對(duì)齊。
- 每個(gè)階段都有不同的任務(wù),包括人臉邊界框回歸、人臉關(guān)鍵點(diǎn)定位等。
這個(gè)級(jí)聯(lián)過程,相當(dāng)于?
?海選→淘汰賽→決賽?
?的過程。
整體流程
上圖是論文中對(duì)于MTCNN整體過程的圖示,我們換一種較為容易易懂的圖示來理解整體過程:
- 先將圖片生成不同尺寸的圖像金字塔,以便識(shí)別不同大小的人臉。
- 將圖片輸入到P-net中,識(shí)別出可能包含人臉的候選窗口。
- 將P-net中識(shí)別的可能人臉的候選窗口輸入到R-net中,識(shí)別出更精確的人臉位置。
- 將R-net中識(shí)別的人臉位置輸入到O-net中,進(jìn)行更加精細(xì)化識(shí)別,從而找到人臉區(qū)域。
備注:上圖引用自科普:什么是mtcnn人臉檢測(cè)算法
P-net:人臉檢測(cè)
- 名稱:提議網(wǎng)絡(luò)(proposal network)
- 作用:P網(wǎng)絡(luò)通過卷積神經(jīng)網(wǎng)絡(luò)(CNN)對(duì)輸入圖像進(jìn)行處理,識(shí)別出可能包含人臉的候選窗口,并對(duì)這些候選窗口進(jìn)行邊界框的回歸,以更準(zhǔn)確地定位人臉位置。
- 特點(diǎn):純卷積網(wǎng)絡(luò),無全鏈接(精髓所在)
R-net:人臉對(duì)齊
- 名稱:精修網(wǎng)絡(luò)(refine network)
- 作用:R網(wǎng)絡(luò)通過分類器和回歸器對(duì)P網(wǎng)絡(luò)生成的候選窗口進(jìn)行處理,進(jìn)一步篩選出包含人臉的區(qū)域,并對(duì)人臉位置進(jìn)行修正,以提高人臉檢測(cè)的準(zhǔn)確性。
O-net:人臉識(shí)別
- 名稱:輸出網(wǎng)絡(luò)(output network)
- 作用:O網(wǎng)絡(luò)通過更深層次的卷積神經(jīng)網(wǎng)絡(luò)處理人臉區(qū)域,優(yōu)化人臉位置和姿態(tài),并輸出面部關(guān)鍵點(diǎn)信息,為后續(xù)的面部對(duì)齊提供重要參考。
MTCNN用到的主要模塊
圖像金字塔
MTCNN的P網(wǎng)絡(luò)使用的檢測(cè)方式是:設(shè)置建議框,用建議框在圖片上滑動(dòng)檢測(cè)人臉
由于P網(wǎng)絡(luò)的建議框的大小是固定的,只能檢測(cè)12*12范圍內(nèi)的人臉,所以其不斷縮小圖片以適應(yīng)于建議框的大小,當(dāng)下一次圖像的最小邊長小于12時(shí),停止縮放。
IOU
定義:IOU(Intersection over Union)是指交并比,是目標(biāo)檢測(cè)領(lǐng)域常用的一種評(píng)估指標(biāo),用于衡量兩個(gè)邊界框(Bounding Box)之間的重疊程度。 兩種方式:
- 交集比并集
- 交集比最小集
O網(wǎng)絡(luò)iou值大于閾值的框被認(rèn)為是重復(fù)的框會(huì)丟棄,留下iou值小的框,但是如果出現(xiàn)了下圖中大框套小框的情況,則iou值偏小也會(huì)被保留,是我們不想看到的,因此我們?cè)贠網(wǎng)絡(luò)采用了第二種方式的iou以提高誤檢率。
NMS(Non-Maximum Suppression,非極大值抑制)
定義: NMS是一種目標(biāo)檢測(cè)中常用的技術(shù),旨在消除重疊較多的候選框,保留最具代表性的邊界框,以提高檢測(cè)的準(zhǔn)確性和效率。
工作原理: NMS的工作原理是通過設(shè)置一個(gè)閾值,比如IOU(交并比)閾值,對(duì)所有候選框按照置信度進(jìn)行排序,然后從置信度最高的候選框開始,將與其重疊度高于閾值的候選框剔除,保留置信度最高的候選框。
- 如上圖所示框出了五個(gè)人臉,置信度分別為0.98,0.83,0.75,0.81,0.67,前三個(gè)置信度對(duì)應(yīng)左側(cè)的Rose,后兩個(gè)對(duì)應(yīng)右側(cè)的Jack。
- NMS將這五個(gè)框根據(jù)置信度排序,取出最大的置信度(0.98)的框分別和剩下的框做iou,保留iou小于閾值的框(代碼中閾值設(shè)置的是0.3),這樣就剩下0.81和0.67這兩個(gè)框了。
- 重復(fù)上面的過程,取出置信度(0.81)大的框和剩下的框做iou,保留iou小于閾值的框。這樣最后只剩下0.98和0.81這兩個(gè)人臉框了。
代碼實(shí)現(xiàn)
P-Net
import torch
from torch import nn
"""
P-Net
"""
classPNet(nn.Module):
def__init__(self):
super().__init__()
self.features_extractor = nn.Sequential(
# 第一層卷積
nn.Conv2d(in_channels=3, out_channels=10, kernel_size=3, stride=1, padding=0),
nn.BatchNorm2d(num_features=10),
nn.ReLU(),
# 第一層池化
nn.MaxPool2d(kernel_size=3,stride=2, padding=1),
# 第二層卷積
nn.Conv2d(in_channels=10, out_channels=16, kernel_size=3, stride=1, padding=0),
nn.BatchNorm2d(num_features=16),
nn.ReLU(),
# 第三層卷積
nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, stride=1, padding=0),
nn.BatchNorm2d(num_features=32),
nn.ReLU()
)
# 概率輸出
self.cls_out = nn.Conv2d(in_channels=32, out_channels=2, kernel_size=1, stride=1, padding=0)
# 回歸量輸出
self.reg_out = nn.Conv2d(in_channels=32, out_channels=4, kernel_size=1, stride=1, padding=0)
defforward(self, x):
print(x.shape)
x = self.features_extractor(x)
cls_out = self.cls_out(x)
reg_out = self.reg_out(x)
return cls_out, reg_out
R-Net
import torch
from torch import nn
classRNet(nn.Module):
def__init__(self):
super().__init__()
self.feature_extractor = nn.Sequential(
# 第一層卷積 24 x 24
nn.Conv2d(in_channels=3, out_channels=28, kernel_size=3, stride=1, padding=0),
nn.BatchNorm2d(num_features=28),
nn.ReLU(),
# 第一層池化 11 x 11
nn.MaxPool2d(kernel_size=3, stride=2, padding=1, ceil_mode=False),
# 第二層卷積 9 x 9
nn.Conv2d(in_channels=28, out_channels=48, kernel_size=3, stride=1, padding=0),
nn.BatchNorm2d(num_features=48),
nn.ReLU(),
# 第二層池化 (沒有補(bǔ)零) 4 x 4
nn.MaxPool2d(kernel_size=3, stride=2, padding=0, ceil_mode=False),
# 第三層卷積 3 x 3
nn.Conv2d(in_channels=48, out_channels=64, kernel_size=2, stride=1, padding=0),
nn.BatchNorm2d(num_features=64),
nn.ReLU(),
# 展平
nn.Flatten(),
# 全連接層 [batch_size, 128]
nn.Linear(in_features=3*3*64, out_features=128)
)
# 概率輸出
self.cls_out = nn.Linear(in_features=128, out_features=1)
# 回歸量輸出
self.reg_out = nn.Linear(in_features=128, out_features=4)
defforward(self, x):
x = self.feature_extractor(x)
cls = self.cls_out(x)
reg = self.reg_out(x)
return cls, reg
O-Net
import torch
from torch import nn
classONet(nn.Module):
def__init__(self):
super().__init__()
self.feature_extractor = nn.Sequential(
# 第1層卷積 48 x 48
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1, padding=0),
nn.BatchNorm2d(num_features=32),
nn.ReLU(),
# 第1層池化 11 x 11
nn.MaxPool2d(kernel_size=3, stride=2, padding=1, ceil_mode=False),
# 第2層卷積 9 x 9
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=0),
nn.BatchNorm2d(num_features=64),
nn.ReLU(),
# 第2層池化
nn.MaxPool2d(kernel_size=3, stride=2, padding=0, ceil_mode=False),
# 第3層卷積
nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, stride=1, padding=0),
nn.BatchNorm2d(num_features=64),
nn.ReLU(),
# 第3層池化
nn.MaxPool2d(kernel_size=2, stride=2, padding=0, ceil_mode=False),
# 第4層卷積
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=2, stride=1, padding=0),
nn.BatchNorm2d(num_features=128),
nn.ReLU(),
# 展平 [batch_size, n_features]
nn.Flatten(),
# 全連接 [batch_size, 128]
nn.Linear(in_features=3*3*128, out_features=256)
)
# 概率輸出
self.cls_out = nn.Linear(in_features=256, out_features=1)
# 回歸量輸出
self.reg_out = nn.Linear(in_features=256, out_features=4)
# 關(guān)鍵點(diǎn)輸出
self.landmark_out = nn.Linear(in_features=256, out_features=10)
defforward(self, x):
x = self.feature_extractor(x)
cls = self.cls_out(x)
reg = self.reg_out(x)
landmark = self.landmark_out(x)
return cls, reg, landmark
MTCNN訓(xùn)練邏輯
準(zhǔn)備訓(xùn)練數(shù)據(jù)集
首先,我們需要準(zhǔn)備人臉標(biāo)注好的數(shù)據(jù)集,人臉識(shí)別標(biāo)注好的數(shù)據(jù)集比較有名是:WIDEERFACE和CelebA。 本次我們使用CelebA數(shù)據(jù)集。
CelebA數(shù)據(jù)集簡介
CelebA數(shù)據(jù)集是由香港中文大學(xué)多媒體實(shí)驗(yàn)室發(fā)布的大規(guī)模人臉屬性數(shù)據(jù)集,包含超過 20 萬張名人圖像,每張圖像有 40 個(gè)屬性注釋。CelebA數(shù)據(jù)集全拼是Large-scale CelebFaces Attributes (CelebA) Dataset。 該數(shù)據(jù)集中的圖像涵蓋了豐富的人體姿勢(shì)變化和復(fù)雜多樣的背景信息。涵蓋了分類、目標(biāo)檢測(cè)和關(guān)鍵點(diǎn)檢測(cè)等數(shù)據(jù)。
CelebA數(shù)據(jù)集下載
下載地址:
- 可以在CelebA官網(wǎng)找到谷歌網(wǎng)盤下載鏈接或百度網(wǎng)盤下載鏈接。
下載和準(zhǔn)備訓(xùn)練集
第一步:下載文件后解壓,解壓后目錄結(jié)構(gòu)如下
CelebA
|-Anno
|-identity_CelebA.txt # 圖片標(biāo)注的身份信息
|-list_attr_celeba.txt # 圖片標(biāo)注的屬性信息
|-list_bbox_celeba.txt # 圖片標(biāo)注的人臉框
|-list_landmarks_align_celeba.txt # 圖片標(biāo)注的人臉關(guān)鍵點(diǎn)(對(duì)齊圖片)
|-list_landmarks_celeba.txt # 圖片標(biāo)注的人臉關(guān)鍵點(diǎn)
|-Eval
|-list_eval_partition.txt # 圖片劃分訓(xùn)練集、驗(yàn)證集、測(cè)試集
|-Img
|-img_align_celeba # 圖片經(jīng)過對(duì)齊后的
|-img_celeba # 原始圖片(未做對(duì)齊處理)
|-img_celeba_png # PNG格式的圖片
第二步:將所需的圖片以及標(biāo)準(zhǔn)信息拷貝到代碼工程項(xiàng)目下,并調(diào)整目錄結(jié)構(gòu)如下:
代碼根目錄
|-datasets
|-celeba
|-identity_CelebA.txt
|-list_attr_celeba.txt
|-list_bbox_celeba.txt
|-list_landmarks_align_celeba.txt
|-list_landmarks_celeba.txt
|-Img
|-img_celeba
|-*.py 代碼文件
說明:在使用數(shù)據(jù)集時(shí)可以調(diào)整成如下的目錄結(jié)構(gòu),這樣我們可以在Jupyter Notebook中使用CelebA的API查看圖片的信息。
第三步:使用CelebA的API查看圖片的信息。
import numpy as np
from torchvision.datasets importCelebA
import matplotlib.pyplot as plt
import cv2
from PIL importImage
root_dir ='datasets'# jupyter notebook的文件放在與datasets同一級(jí)目錄下
celeba =CelebA(root=root_dir, split='train',
target_type=['attr','identity','bbox','landmarks'],
download=False)
attr_names = celeba.attr_names
attr_names.pop()
attr_names = np.array(attr_names)
fig = plt.figure(figsize=(14,7))
n =4# 顯示的圖片數(shù)量
for idx inrange(n):
img,(attr, identity, bbox, landmarks)= celeba[idx]# 讀取圖片和相關(guān)標(biāo)簽值
ax = fig.add_subplot(1,4, idx +1)
# 不顯示刻度標(biāo)簽和邊框
ax.set_xticks([])
ax.set_yticks([])
ax.set_frame_on(b=False)
# 將CelebA數(shù)據(jù)集讀取到的PIL圖片格式轉(zhuǎn)換成OprnCV所需格式
img_cv2 = cv2.cvtColor(src=np.asanyarray(img), code=cv2.COLOR_RGB2BGR)
# 繪制特征點(diǎn)
landmarks = landmarks.numpy()
for idx, point inenumerate(landmarks):
if idx %2==0:
cv2.circle(img=img_cv2, center=(point, landmarks[idx +1]),
radius=1, color=(255,0,0), thickness=2)
attr_list = attr.numpy()
attrs = attr_names[attr_list==1]
label =''
# 屬性標(biāo)簽
for att in attrs:
label = label + att +'\n'
ax.set_xlabel(label)
# 身份ID
cele_id = identity.numpy()
ax.set_title(f'ID: {cele_id}')
# 將OpenCV圖片再次轉(zhuǎn)成成Pillow圖片格式
img_pil =Image.fromarray( cv2.cvtColor(src=img_cv2,
code=cv2.COLOR_BGR2RGB,))
ax.imshow(img_pil)
plt.show()
# 顯示標(biāo)注框
fig = plt.figure(figsize=(14,7))
for idx inrange(n):
img,(attr, identity, bbox, landmarks)= celeba[idx]
ax = fig.add_subplot(1,4, idx +1)
ax.set_xticks([])
ax.set_yticks([])
ax.set_frame_on(b=False)
# 圖片的讀取改為使用原圖像
file_path = os.path.join(celeba.root, celeba.base_folder,'img_celeba', celeba.filename[idx])
img_cv2 = cv2.imread(file_path)
bbox = bbox.numpy()
cv2.rectangle(img=img_cv2, pt1=(bbox[0], bbox[1]),
pt2=(bbox[0]+ bbox[2], bbox[1]+ bbox[3]),
color=(255,0,0), thickness=2)
cele_id = identity.numpy()
ax.set_title(f'ID: {cele_id}')
img_pil =Image.fromarray(cv2.cvtColor(src=img_cv2,
code=cv2.COLOR_BGR2RGB,))
ax.imshow(img_pil)
plt.show()
訓(xùn)練集預(yù)處理
因?yàn)閳D片標(biāo)注的信息各種各樣,不能直接灌入模型中訓(xùn)練,所以需要進(jìn)行預(yù)處理。
傳給機(jī)器的數(shù)據(jù)最好是以0為中心的,且歸一化到[-1, 1]之間的數(shù)據(jù)。
預(yù)處理過程大致如下:
- 在datasets目錄下創(chuàng)建train/12、train/24、train/48文件夾,分別存放12、24、48大小的訓(xùn)練數(shù)據(jù)。
- 讀取標(biāo)注信息和關(guān)鍵點(diǎn)信息
- 讀取圖片
- 根據(jù)圖片的信息,隨機(jī)生成5個(gè)候選裁剪框
a. 對(duì)候選裁剪框與原始標(biāo)注框進(jìn)行IOU計(jì)算
b. 如果iou>0.7,為正樣本;如果0.4 < iou < 0.6,為偏樣本;如果iou<0.4,為負(fù)樣本
- 將生成的樣本按類保存在train/12/、train/24/、train/48/文件夾中。
由于MTCNN原始項(xiàng)目代碼可讀性不強(qiáng),我對(duì)預(yù)處理過程進(jìn)行了重構(gòu),預(yù)處理的代碼如下。以下代碼也可以可以查看Github倉庫:Github:detect_face_mtcnn
import os
import random
import numpy as np
import torch
from PIL importImage
from utils.tool import iou as IOU
current_path = os.path.dirname(os.path.abspath(__file__))
BASE_PATH = os.path.join(current_path,"datasets")
TARGET_PATH = os.path.join(BASE_PATH,"celeba")
IMG_PATH = os.path.join(BASE_PATH,"celeba/img_celeba")
DST_PATH = os.path.join(BASE_PATH,"train")
LABEL_PATH = os.path.join(TARGET_PATH,"list_bbox_celeba.txt")
LANMARKS_PATH = os.path.join(TARGET_PATH,"list_landmarks_celeba.txt")
# 測(cè)試樣本個(gè)數(shù)限制,設(shè)置為 -1 表示全部
TEST_SAMPLE_LIMIT =100
# 為隨機(jī)數(shù)種子做準(zhǔn)備,使正樣本,部分樣本,負(fù)樣本的比例為1:1:3
float_num =[0.1,0.1,0.3,0.5,0.95,0.95,0.99,0.99,0.99,0.99]
defcreate_directories(base_path, face_size):
paths ={}
base_path = os.path.join(base_path,f"{face_size}")
ifnot os.path.exists(base_path):
os.makedirs(base_path)
paths['positive']= os.path.join(base_path,"positive")
paths['negative']= os.path.join(base_path,"negative")
paths['part']= os.path.join(base_path,"part")
for path in paths.values():
ifnot os.path.exists(path):
os.makedirs(path)
return paths, base_path
defopen_label_files(base_path):
files ={}
files['positive']=open(os.path.join(base_path,"positive.txt"),"w")
files['negative']=open(os.path.join(base_path,"negative.txt"),"w")
files['part']=open(os.path.join(base_path,"part.txt"),"w")
return files
defparse_annotation_line(line):
strs = line.strip().split()
return strs
defadjust_bbox(x1, y1, w, h):
# 標(biāo)注不標(biāo)準(zhǔn),給框適當(dāng)?shù)钠屏? x1 =int(x1 + w *0.12)
y1 =int(y1 + h *0.1)
x2 =int(x1 + w *0.9)
y2 =int(y1 + h *0.85)
w =int(x2 - x1)
h =int(y2 - y1)
return x1, y1, x2, y2, w, h
defgenerate_crop_boxes(cx, cy, max_side, img_w, img_h):
"""
根據(jù)給定的人臉中心點(diǎn)坐標(biāo)和尺寸,生成5個(gè)候選的裁剪框。
參數(shù):
cx (float): 人臉中心點(diǎn)的 x 坐標(biāo)
cy (float): 人臉中心點(diǎn)的 y 坐標(biāo)
max_side (int): 人臉框的最大邊長
img_w (int): 圖像寬度
img_h (int): 圖像高度
返回:
crop_boxes (list): 一個(gè)包含5個(gè)裁剪框坐標(biāo)的列表,每個(gè)裁剪框的格式為 [x1, y1, x2, y2]
"""
crop_boxes =[]
for _ inrange(5):
# 隨機(jī)偏移中心點(diǎn)坐標(biāo)以及邊長
seed = float_num[np.random.randint(0,len(float_num))]
# 最大邊長隨機(jī)偏移
_max_side = max_side + np.random.randint(int(-max_side * seed),int(max_side * seed))
# 中心點(diǎn)x坐標(biāo)隨機(jī)偏移
_cx = cx + np.random.randint(int(-cx * seed),int(cx * seed))
# 中心點(diǎn)y坐標(biāo)隨機(jī)偏移
_cy = cy + np.random.randint(int(-cy * seed),int(cy * seed))
# 得到偏移后的坐標(biāo)值(方框)
_x1 = _cx - _max_side /2
_y1 = _cy - _max_side /2
_x2 = _x1 + _max_side
_y2 = _y1 + _max_side
# 偏移過大,偏出圖像了,此時(shí),不能用,應(yīng)該再次嘗試偏移
if _x1 <0or _y1 <0or _x2 > img_w or _y2 > img_h:
continue
# 添加裁剪框坐標(biāo)到列表中
crop_boxes.append(np.array([_x1, _y1, _x2, _y2]))
return crop_boxes
defprocess_crop_box(img, face_size, max_side, crop_box, boxes, landmarks):
"""
處理單個(gè)裁剪框,生成正負(fù)樣本。
參數(shù):
img (Image): 原始圖像
crop_box (list): 裁剪框坐標(biāo) [x1, y1, x2, y2]
boxes (list): 人臉框坐標(biāo)列表
face_size (int): 生成的人臉圖像尺寸
返回:
sample (dict): 樣本信息 {'image': image, 'label': label, 'bbox_offsets': offsets, 'landmark_offsets': landmark_offsets}
"""
x1, y1, x2, y2 = boxes[0][:4]
_x1, _y1, _x2, _y2 = crop_box[:4]
px1, py1, px2, py2, px3, py3, px4, py4, px5, py5 = landmarks
_max_side = max_side
offset_x1 =(x1 - _x1)/ _max_side
offset_y1 =(y1 - _y1)/ _max_side
offset_x2 =(x2 - _x2)/ _max_side
offset_y2 =(y2 - _y2)/ _max_side
offset_px1 =(px1 - _x1)/ _max_side
offset_py1 =(py1 - _y1)/ _max_side
offset_px2 =(px2 - _x1)/ _max_side
offset_py2 =(py2 - _y1)/ _max_side
offset_px3 =(px3 - _x1)/ _max_side
offset_py3 =(py3 - _y1)/ _max_side
offset_px4 =(px4 - _x1)/ _max_side
offset_py4 =(py4 - _y1)/ _max_side
offset_px5 =(px5 - _x1)/ _max_side
offset_py5 =(py5 - _y1)/ _max_side
face_crop = img.crop(crop_box)
face_resize = face_crop.resize((face_size, face_size),Image.Resampling.LANCZOS)
iou = IOU(torch.tensor([x1, y1, x2, y2]), torch.tensor([crop_box[:4]]))
if iou >0.7:# 正樣本
label =1
elif0.4< iou <0.6:# 部分樣本
label =2
elif iou <0.2:# 負(fù)樣本
label =0
else:
returnNone# 不符合任何條件的樣本不處理
return{
'image': face_resize,
'label': label,
'bbox_offsets':(offset_x1, offset_y1, offset_x2, offset_y2),
'landmark_offsets':(offset_px1, offset_py1, offset_px2, offset_py2, offset_px3, offset_py3, offset_px4, offset_py4, offset_px5, offset_py5)
}
defprocess_annotation(face_size, anno_line, landmarks):
"""
處理單行注釋信息,生成正負(fù)樣本。
參數(shù):
anno_line (str): 一行注釋信息,格式為 "image_filename x1 y1 w h"
face_size (int): 生成的人臉圖像尺寸
landmarks (str): 關(guān)鍵點(diǎn)標(biāo)注字符串
返回:
samples (list): 生成的樣本列表
"""
# 5個(gè)關(guān)鍵點(diǎn)
_landmarks = landmarks.split()
# 使用列表解析和解包一次性獲取所有關(guān)鍵點(diǎn)的坐標(biāo)
landmarks =[float(x)for x in _landmarks[1:11]]
# 解析注釋行,獲取圖像文件名和人臉位置信息
strs = parse_annotation_line(anno_line)
image_filename = strs[0].strip()
x1, y1, w, h =map(int, strs[1:])
# 標(biāo)簽矯正
x1, y1, x2, y2, w, h = adjust_bbox(x1, y1, w, h)
boxes =[[x1, y1, x2, y2]]
# 計(jì)算人臉中心點(diǎn)坐標(biāo)
cx = w /2+ x1
cy = h /2+ y1
# 最大邊長
max_side =max(w, h)
# 打開圖像文件
image_filepath = os.path.join(IMG_PATH, image_filename)
withImage.open(image_filepath)as img:
# 解析出寬度和高度
img_w, img_h = img.size
# 生成候選的裁剪框
samples =[]
for crop_box in generate_crop_boxes(cx, cy, max_side, img_w, img_h):
# 處理每個(gè)候選裁剪框,生成正負(fù)樣本
sample = process_crop_box(img, face_size, max_side, crop_box, boxes, landmarks )
if sample:
samples.append(sample)
return samples
defsave_samples(samples, files, base_path, counters):
"""
保存正負(fù)樣本到文件中。
參數(shù):
samples (list): 樣本列表, 每個(gè)元素為一個(gè)字典, 包含 'image', 'label', 'bbox_offsets', 'landmark_offsets'
files (dict): 包含正負(fù)樣本輸出文件的字典
base_path (str): 輸出文件的基礎(chǔ)路徑
counters (dict): 樣本計(jì)數(shù)器字典
"""
for sample in samples:
image = sample['image']
label = sample['label']
bbox_offsets = sample['bbox_offsets']
landmark_offsets = sample['landmark_offsets']
if label ==1:
category ='positive'
counters['positive']+=1
elif label ==2:
category ='part'
counters['part']+=1
else:
category ='negative'
counters['negative']+=1
filename =f"{category}/{counters[category]}.jpg"
image.save(os.path.join(base_path, filename))
try:
bbox_str =' '.join(map(str, bbox_offsets))
landmark_str =' '.join(map(str, landmark_offsets))
files[category].write(f"{filename} {label} {bbox_str} {landmark_str}\n")
exceptIOErroras e:
print(f"Error writing to file: {e}")
defgenerate_samples(face_size, max_samples=-1):
"""
生成指定大小的人臉樣本,并保存到文件中。
參數(shù):
face_size (int): 生成的人臉圖像尺寸
max_samples (int): 最大生成樣本數(shù)量,設(shè)置為 -1 表示不限制
"""
ifnot os.path.exists(DST_PATH):
os.makedirs(DST_PATH)
paths, base_path = create_directories(DST_PATH, face_size)
# 新建標(biāo)注文件
files = open_label_files(base_path)
# 樣本計(jì)數(shù)
counters ={'positive':0,'negative':0,'part':0}
# 讀取標(biāo)注信息
withopen(LANMARKS_PATH)as f:
landmarks_list = f.readlines()
withopen(LABEL_PATH)as f:
anno_list = f.readlines()
for i,(anno_line, landmarks)inenumerate(zip(anno_list, landmarks_list)):
print(f"positive:{counters['positive']}, \
negative:{counters['negative']}, \
part:{counters['part']}")
# 跳過前兩行
if i <2:
continue
# 如果處理了指定數(shù)量的樣本,則退出循環(huán)
if max_samples >0and i > max_samples:
break
# 處理單行標(biāo)注信息,生成正負(fù)樣本
samples = process_annotation(
face_size, anno_line, landmarks
)
# 保存正負(fù)樣本到文件
save_samples(
samples,
files, base_path, counters
)
for file in files.values():
file.close()
defmain():
# 生成12×12的樣本
generate_samples(12,1000)
generate_samples(24,1000)
generate_samples(48,1000)
if __name__ =="__main__":
main()
通過運(yùn)行上述的預(yù)處理腳本,代碼會(huì)在datasets目錄下創(chuàng)建對(duì)應(yīng)的訓(xùn)練數(shù)據(jù)集
查看48 × 48的訓(xùn)練集,可以看到對(duì)應(yīng)的正、負(fù)、偏樣本如下
限于篇幅原因,以上數(shù)據(jù)集預(yù)處理部分的解析和代碼理解,我將放在下篇文章進(jìn)行。
三個(gè)模型分別訓(xùn)練
由于MTCNN是三個(gè)網(wǎng)絡(luò),所以需要分別對(duì)三個(gè)網(wǎng)絡(luò)進(jìn)行訓(xùn)練。
第一步:構(gòu)建訓(xùn)練的公共基礎(chǔ)部分,方便三個(gè)網(wǎng)絡(luò)訓(xùn)練時(shí)調(diào)用。
import torch
import os
from torch.utils.data importDataLoader
from train.FaceDatasetimportFaceDataset
import matplotlib.pyplot as plt
classTrainer:
def__init__(self, net, param_path, data_path):
# 檢測(cè)是否有GPU
self.device ='cuda:0'if torch.cuda.is_available()else"cpu"
# 把模型搬到device
self.net = net.to(self.device)
self.param_path = param_path
# 打包數(shù)據(jù)
self.datasets =FaceDataset(data_path)
# 定義損失函數(shù):類別判斷(分類任務(wù))
self.cls_loss_func = torch.nn.BCELoss()
# 定義損失函數(shù):框的偏置回歸
self.offset_loss_func = torch.nn.MSELoss()
# 定義損失函數(shù):關(guān)鍵點(diǎn)的偏置回歸
self.point_loss_func = torch.nn.MSELoss()
# 定義優(yōu)化器
self.optimizer = torch.optim.Adam(params=self.net.parameters(), lr=1e-3)
defcompute_loss(self, out_cls, out_offset, out_point, cls, offset, point, landmark):
# 選取置信度為0,1的正負(fù)樣本求置信度損失
cls_mask = torch.lt(cls,2)
cls_loss = self.cls_loss_func(torch.masked_select(out_cls, cls_mask),
torch.masked_select(cls, cls_mask))
# 選取正樣本和部分樣本求偏移率的損失
offset_mask = torch.gt(cls,0)
offset_loss = self.offset_loss_func(torch.masked_select(out_offset, offset_mask),
torch.masked_select(offset, offset_mask))
if landmark:
point_loss = self.point_loss_func(torch.masked_select(out_point, offset_mask),
torch.masked_select(point, offset_mask))
return cls_loss, offset_loss, point_loss
else:
return cls_loss, offset_loss,None
deftrain(self, epochs, landmark=False):
"""
- 斷點(diǎn)續(xù)傳 --> 短點(diǎn)續(xù)訓(xùn)
- transfer learning 遷移學(xué)習(xí)
- pretrained model 預(yù)訓(xùn)練
:param epochs: 訓(xùn)練的輪數(shù)
:param landmark: 是否為landmark任務(wù)
:return:
"""
# 加載上次訓(xùn)練的參數(shù)
if os.path.exists(self.param_path):
self.net.load_state_dict(torch.load(self.param_path))
print("加載參數(shù)文件,繼續(xù)訓(xùn)練 ...")
else:
print("沒有參數(shù)文件,全新訓(xùn)練 ...")
# 封裝數(shù)據(jù)加載器
dataloader =DataLoader(self.datasets, batch_size=32, shuffle=True)
# 定義列表存儲(chǔ)損失值
cls_losses =[]
offset_losses =[]
point_losses =[]
total_losses =[]
for epoch inrange(epochs):
# 訓(xùn)練一輪
for i,(img_data, _cls, _offset, _point)inenumerate(dataloader):
# 數(shù)據(jù)搬家 [32, 3, 12, 12]
img_data = img_data.to(self.device)
_cls = _cls.to(self.device)
_offset = _offset.to(self.device)
_point = _point.to(self.device)
if landmark:
# O-Net輸出三個(gè)
out_cls, out_offset, out_point = self.net(img_data)
out_point = out_point.view(-1,10)
else:
# O-Net輸出兩個(gè)
out_cls, out_offset = self.net(img_data)
out_point =None
# [B, C, H, W] 轉(zhuǎn)換為 [B, C]
out_cls = out_cls.view(-1,1)
out_offset = out_offset.view(-1,4)
if landmark:
out_point = out_point.view(-1,10)
# 計(jì)算損失
cls_loss, offset_loss, point_loss = self.compute_loss(out_cls, out_offset, out_point,
_cls, _offset, _point, landmark)
if landmark:
loss = cls_loss + offset_loss + point_loss
else:
loss = cls_loss + offset_loss
# 打印損失
if landmark:
print(f"Epoch [{epoch+1}/{epochs}], loss:{loss.item():.4f}, cls_loss:{cls_loss.item():.4f}, "
f"offset_loss:{offset_loss.item():.4f}, point_loss:{point_loss.item():.4f}")
else:
print(f"Epoch [{epoch+1}/{epochs}], loss:{loss.item():.4f}, cls_loss:{cls_loss.item():.4f}, "
f"offset_loss:{offset_loss.item():.4f}")
# 存儲(chǔ)損失值
cls_losses.append(cls_loss.item())
offset_losses.append(offset_loss.item())
if landmark:
point_losses.append(point_loss.item())
total_losses.append(loss.item())
# 清空梯度
self.optimizer.zero_grad()
# 梯度回傳
loss.backward()
# 優(yōu)化
self.optimizer.step()
# 保存模型(參數(shù))
torch.save(self.net.state_dict(), self.param_path)
# 繪制損失曲線
self.plot_losses(cls_losses, offset_losses, point_losses, total_losses, landmark)
print("訓(xùn)練完成!")
defplot_losses(self, cls_losses, offset_losses, point_losses, total_losses, landmark):
"""
繪制訓(xùn)練過程中的損失曲線
:param cls_losses: 分類損失列表
:param offset_losses: 邊界框偏移損失列表
:param point_losses: 關(guān)鍵點(diǎn)偏移損失列表
:param total_losses: 總損失列表
:param landmark: 是否為landmark任務(wù)
"""
plt.figure(figsize=(12,6))
plt.subplot(1,2,1)
plt.plot(cls_losses, label='Classification Loss')
plt.plot(offset_losses, label='Offset Loss')
if landmark:
plt.plot(point_losses, label='Point Loss')
plt.legend()
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('Training Losses')
plt.subplot(1,2,2)
plt.plot(total_losses)
plt.xlabel('Iteration')
plt.ylabel('Total Loss')
plt.title('Total Training Loss')
plt.savefig('training_losses.png')
plt.close()
第二步:訓(xùn)練對(duì)應(yīng)的網(wǎng)絡(luò)。
from train import model_mtcnn as nets
import os
import train.train as train
if __name__ =='__main__':
current_path = os.path.dirname(os.path.abspath(__file__))
# 權(quán)重存放地址
base_path = os.path.join(current_path,"model")
model_path = os.path.join(base_path,"p_net.pt")
# 數(shù)據(jù)存放地址
data_path = os.path.join(current_path,"datasets/train/12")
# 如果沒有這個(gè)參數(shù)存放目錄,則創(chuàng)建一個(gè)目錄
ifnot os.path.exists(base_path):
os.makedirs(base_path)
# 構(gòu)建模型
pnet = nets.PNet()
# 開始訓(xùn)練
t = train.Trainer(pnet, model_path, data_path)
# t.train2(0.01)
t.train(100)
運(yùn)行結(jié)果:
備注:以上代碼在Apple M3芯片進(jìn)行訓(xùn)練和推理時(shí),會(huì)出現(xiàn)(Segmentation Fault)的錯(cuò)誤,因此訓(xùn)練和預(yù)測(cè)最好是在x86架構(gòu)的電腦上進(jìn)行。
MTCNN推理邏輯
- ?輸入一張圖片(不限尺寸)
- ?構(gòu)建圖像金字塔
a. 把圖像輸入P-Net,得到P-Net的輸出
b. 把P-Net的輸出,resize 24 × 24, 輸入R-Net,得到R-Net的輸出
c. 把R-Net的輸出,resize 48 × 48, 輸入O-Net,得到O-Net的輸出
篇幅原因,代碼將在下一章進(jìn)行分析理解,本章不再贅述。
遍歷金字塔,取出每一個(gè)級(jí)別的圖像
推理效果:
內(nèi)容小結(jié)
- 人臉識(shí)別
a. 人臉識(shí)別細(xì)分有兩種:人臉檢測(cè)和人臉身份識(shí)別。
b. 人臉檢測(cè)是識(shí)別圖像或視頻中的人臉,并定位其位置。
c. 人臉身份識(shí)別是指通過識(shí)別人臉上的獨(dú)特特征來確定一個(gè)人的身份。
d. 目標(biāo)檢測(cè)算法都可以做人臉檢測(cè),但是太重了;而MTCNN就是這樣一個(gè)輕量級(jí)和專業(yè)級(jí)的人臉檢測(cè)網(wǎng)絡(luò)。
- MTCNN
a. MTCNN采用級(jí)聯(lián)結(jié)構(gòu),包含三個(gè)階段的深度卷積網(wǎng)絡(luò),相當(dāng)于??海選→淘汰賽→決賽?
?的過程。
b. MTCNN的PNet為了能夠盡可能多的識(shí)別出人臉,采用滑動(dòng)窗口方式,生成圖像金字塔。
c. RNet采用NMS(非極大值抑制)技術(shù),通過計(jì)算IOU(交并比),對(duì)所有候選框按照置信度進(jìn)行排序,保留置信度最大的候選框。
d. MTCNN的訓(xùn)練過程是三階段分開訓(xùn)練的。
本文轉(zhuǎn)載自公眾號(hào)一起AI技術(shù) 作者:熱情的Dongming
原文鏈接:??https://mp.weixin.qq.com/s/yb8MP1vP507HG73bJplyJg??
