人臉識別和MTCNN模型 原創
前言
在上一章課程【???使用YOLO進行目標檢測??】,我們了解到目標檢測有兩種策略,一種是以YOLO為代表的策略:特征提取→切片→分類回歸;另外一種是以MTCNN為代表的策略:先圖像切片→特征提取→分類和回歸。因此,本章內容將深入了解MTCNN模型,包括:MTCNN的模型組成、模型訓練過程、模型預測過程等。
人臉識別
在展開了解MTCNN之前,我們對人臉檢測先做一個初步的梳理和了解。人臉識別細分有兩種:人臉檢測和人臉身份識別。
人臉檢測
簡述
人臉檢測是一個重要的應用領域,它通常用于識別圖像或視頻中的人臉,并定位其位置。
識別過程
- 輸入圖像:首先,將包含人臉的圖像輸入到人臉檢測模型中。
- 特征提取:深度學習模型將學習提取圖像中的特征,以便識別人臉。
- 人臉定位:模型通過在圖像中定位人臉的位置,通常使用矩形邊界框來框定人臉區域。
- 輸出結果:最終輸出包含人臉位置信息的結果,可以是邊界框的坐標或其他形式的標注。
輸入輸出
- 輸入:一張圖像
- 輸出:所有人臉的坐標框
應用場景
- 表情識別:識別人臉的表情,如快樂、悲傷等。
- 年齡識別:根據人臉特征推斷出人的年齡段。
- 人臉表情生成:通過檢測到的人臉生成不同的表情。
- ...
人臉檢測特點
人臉檢測是目標檢測中最簡單的任務
- 類別少
- 人臉形狀比較固定
- 人臉特征比較固定
- 周圍環境一般比較好
人臉身份識別
簡述
人臉身份識別是指通過識別人臉上的獨特特征來確定一個人的身份。
識別過程
人臉錄入流程:
- 數據采集:采集包含人臉的圖像數據集。
- 人臉檢測:使用人臉檢測算法定位圖像中的人臉區域。
- 人臉特征提取:通過深度學習模型提取人臉圖像的特征向量。
- 特征向量存儲:將提取到的特征向量存儲在向量數據庫中。
人臉驗證流程:
- 人臉檢測:使用人臉檢測算法定位圖像中的人臉區域。
- 人臉特征提取:通過深度學習模型提取人臉圖像的特征向量。
- 人臉特征匹配:將輸入人臉的特征向量與向量數據庫中的特征向量進行匹配。
- 身份識別:根據匹配結果確定輸入人臉的身份信息。
應用領域
- 安防監控:用于門禁系統、監控系統等,實現人臉識別進出控制。
- 移動支付:通過人臉識別來進行身份驗證,實現安全的移動支付功能。
- 社交媒體:用于自動標記照片中的人物,方便用戶管理照片。
- 人機交互:實現人臉識別登錄、人臉解鎖等功能。
一般來說,一切目標檢測算法都可以做人臉檢測,但是由于通用目標檢測算法做人臉檢測太重了,所以會使用專門的人臉識別算法,而MTCNN就是這樣一個輕量級和專業級的人臉檢測網絡。
MTCNN模型
簡介
MTCNN(Multi-Task Cascaded Convolutional Neural Networks)是一種用于人臉檢測和面部對齊的神經網絡模型。
論文地址:https://arxiv.org/abs/1604.02878v1
模型結構
- MTCNN采用了級聯結構,包括三個階段的深度卷積網絡,分別用于人臉檢測和面部對齊。
- 每個階段都有不同的任務,包括人臉邊界框回歸、人臉關鍵點定位等。
這個級聯過程,相當于?
?海選→淘汰賽→決賽?
?的過程。
整體流程
上圖是論文中對于MTCNN整體過程的圖示,我們換一種較為容易易懂的圖示來理解整體過程:
- 先將圖片生成不同尺寸的圖像金字塔,以便識別不同大小的人臉。
- 將圖片輸入到P-net中,識別出可能包含人臉的候選窗口。
- 將P-net中識別的可能人臉的候選窗口輸入到R-net中,識別出更精確的人臉位置。
- 將R-net中識別的人臉位置輸入到O-net中,進行更加精細化識別,從而找到人臉區域。
備注:上圖引用自科普:什么是mtcnn人臉檢測算法
P-net:人臉檢測
- 名稱:提議網絡(proposal network)
- 作用:P網絡通過卷積神經網絡(CNN)對輸入圖像進行處理,識別出可能包含人臉的候選窗口,并對這些候選窗口進行邊界框的回歸,以更準確地定位人臉位置。
- 特點:純卷積網絡,無全鏈接(精髓所在)
R-net:人臉對齊
- 名稱:精修網絡(refine network)
- 作用:R網絡通過分類器和回歸器對P網絡生成的候選窗口進行處理,進一步篩選出包含人臉的區域,并對人臉位置進行修正,以提高人臉檢測的準確性。
O-net:人臉識別
- 名稱:輸出網絡(output network)
- 作用:O網絡通過更深層次的卷積神經網絡處理人臉區域,優化人臉位置和姿態,并輸出面部關鍵點信息,為后續的面部對齊提供重要參考。
MTCNN用到的主要模塊
圖像金字塔
MTCNN的P網絡使用的檢測方式是:設置建議框,用建議框在圖片上滑動檢測人臉
由于P網絡的建議框的大小是固定的,只能檢測12*12范圍內的人臉,所以其不斷縮小圖片以適應于建議框的大小,當下一次圖像的最小邊長小于12時,停止縮放。
IOU
定義:IOU(Intersection over Union)是指交并比,是目標檢測領域常用的一種評估指標,用于衡量兩個邊界框(Bounding Box)之間的重疊程度。 兩種方式:
- 交集比并集
- 交集比最小集
O網絡iou值大于閾值的框被認為是重復的框會丟棄,留下iou值小的框,但是如果出現了下圖中大框套小框的情況,則iou值偏小也會被保留,是我們不想看到的,因此我們在O網絡采用了第二種方式的iou以提高誤檢率。
NMS(Non-Maximum Suppression,非極大值抑制)
定義: NMS是一種目標檢測中常用的技術,旨在消除重疊較多的候選框,保留最具代表性的邊界框,以提高檢測的準確性和效率。
工作原理: NMS的工作原理是通過設置一個閾值,比如IOU(交并比)閾值,對所有候選框按照置信度進行排序,然后從置信度最高的候選框開始,將與其重疊度高于閾值的候選框剔除,保留置信度最高的候選框。
- 如上圖所示框出了五個人臉,置信度分別為0.98,0.83,0.75,0.81,0.67,前三個置信度對應左側的Rose,后兩個對應右側的Jack。
- NMS將這五個框根據置信度排序,取出最大的置信度(0.98)的框分別和剩下的框做iou,保留iou小于閾值的框(代碼中閾值設置的是0.3),這樣就剩下0.81和0.67這兩個框了。
- 重復上面的過程,取出置信度(0.81)大的框和剩下的框做iou,保留iou小于閾值的框。這樣最后只剩下0.98和0.81這兩個人臉框了。
代碼實現
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(),
# 第二層池化 (沒有補零) 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)
# 關鍵點輸出
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訓練邏輯
準備訓練數據集
首先,我們需要準備人臉標注好的數據集,人臉識別標注好的數據集比較有名是:WIDEERFACE和CelebA。 本次我們使用CelebA數據集。
CelebA數據集簡介
CelebA數據集是由香港中文大學多媒體實驗室發布的大規模人臉屬性數據集,包含超過 20 萬張名人圖像,每張圖像有 40 個屬性注釋。CelebA數據集全拼是Large-scale CelebFaces Attributes (CelebA) Dataset。 該數據集中的圖像涵蓋了豐富的人體姿勢變化和復雜多樣的背景信息。涵蓋了分類、目標檢測和關鍵點檢測等數據。
CelebA數據集下載
下載地址:
- 可以在CelebA官網找到谷歌網盤下載鏈接或百度網盤下載鏈接。
下載和準備訓練集
第一步:下載文件后解壓,解壓后目錄結構如下
CelebA
|-Anno
|-identity_CelebA.txt # 圖片標注的身份信息
|-list_attr_celeba.txt # 圖片標注的屬性信息
|-list_bbox_celeba.txt # 圖片標注的人臉框
|-list_landmarks_align_celeba.txt # 圖片標注的人臉關鍵點(對齊圖片)
|-list_landmarks_celeba.txt # 圖片標注的人臉關鍵點
|-Eval
|-list_eval_partition.txt # 圖片劃分訓練集、驗證集、測試集
|-Img
|-img_align_celeba # 圖片經過對齊后的
|-img_celeba # 原始圖片(未做對齊處理)
|-img_celeba_png # PNG格式的圖片
第二步:將所需的圖片以及標準信息拷貝到代碼工程項目下,并調整目錄結構如下:
代碼根目錄
|-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 代碼文件
說明:在使用數據集時可以調整成如下的目錄結構,這樣我們可以在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同一級目錄下
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# 顯示的圖片數量
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)
# 將CelebA數據集讀取到的PIL圖片格式轉換成OprnCV所需格式
img_cv2 = cv2.cvtColor(src=np.asanyarray(img), code=cv2.COLOR_RGB2BGR)
# 繪制特征點
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 =''
# 屬性標簽
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圖片再次轉成成Pillow圖片格式
img_pil =Image.fromarray( cv2.cvtColor(src=img_cv2,
code=cv2.COLOR_BGR2RGB,))
ax.imshow(img_pil)
plt.show()
# 顯示標注框
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()
訓練集預處理
因為圖片標注的信息各種各樣,不能直接灌入模型中訓練,所以需要進行預處理。
傳給機器的數據最好是以0為中心的,且歸一化到[-1, 1]之間的數據。
預處理過程大致如下:
- 在datasets目錄下創建train/12、train/24、train/48文件夾,分別存放12、24、48大小的訓練數據。
- 讀取標注信息和關鍵點信息
- 讀取圖片
- 根據圖片的信息,隨機生成5個候選裁剪框
a. 對候選裁剪框與原始標注框進行IOU計算
b. 如果iou>0.7,為正樣本;如果0.4 < iou < 0.6,為偏樣本;如果iou<0.4,為負樣本
- 將生成的樣本按類保存在train/12/、train/24/、train/48/文件夾中。
由于MTCNN原始項目代碼可讀性不強,我對預處理過程進行了重構,預處理的代碼如下。以下代碼也可以可以查看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")
# 測試樣本個數限制,設置為 -1 表示全部
TEST_SAMPLE_LIMIT =100
# 為隨機數種子做準備,使正樣本,部分樣本,負樣本的比例為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):
# 標注不標準,給框適當的偏移量
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):
"""
根據給定的人臉中心點坐標和尺寸,生成5個候選的裁剪框。
參數:
cx (float): 人臉中心點的 x 坐標
cy (float): 人臉中心點的 y 坐標
max_side (int): 人臉框的最大邊長
img_w (int): 圖像寬度
img_h (int): 圖像高度
返回:
crop_boxes (list): 一個包含5個裁剪框坐標的列表,每個裁剪框的格式為 [x1, y1, x2, y2]
"""
crop_boxes =[]
for _ inrange(5):
# 隨機偏移中心點坐標以及邊長
seed = float_num[np.random.randint(0,len(float_num))]
# 最大邊長隨機偏移
_max_side = max_side + np.random.randint(int(-max_side * seed),int(max_side * seed))
# 中心點x坐標隨機偏移
_cx = cx + np.random.randint(int(-cx * seed),int(cx * seed))
# 中心點y坐標隨機偏移
_cy = cy + np.random.randint(int(-cy * seed),int(cy * seed))
# 得到偏移后的坐標值(方框)
_x1 = _cx - _max_side /2
_y1 = _cy - _max_side /2
_x2 = _x1 + _max_side
_y2 = _y1 + _max_side
# 偏移過大,偏出圖像了,此時,不能用,應該再次嘗試偏移
if _x1 <0or _y1 <0or _x2 > img_w or _y2 > img_h:
continue
# 添加裁剪框坐標到列表中
crop_boxes.append(np.array([_x1, _y1, _x2, _y2]))
return crop_boxes
defprocess_crop_box(img, face_size, max_side, crop_box, boxes, landmarks):
"""
處理單個裁剪框,生成正負樣本。
參數:
img (Image): 原始圖像
crop_box (list): 裁剪框坐標 [x1, y1, x2, y2]
boxes (list): 人臉框坐標列表
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:# 負樣本
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):
"""
處理單行注釋信息,生成正負樣本。
參數:
anno_line (str): 一行注釋信息,格式為 "image_filename x1 y1 w h"
face_size (int): 生成的人臉圖像尺寸
landmarks (str): 關鍵點標注字符串
返回:
samples (list): 生成的樣本列表
"""
# 5個關鍵點
_landmarks = landmarks.split()
# 使用列表解析和解包一次性獲取所有關鍵點的坐標
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:])
# 標簽矯正
x1, y1, x2, y2, w, h = adjust_bbox(x1, y1, w, h)
boxes =[[x1, y1, x2, y2]]
# 計算人臉中心點坐標
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):
# 處理每個候選裁剪框,生成正負樣本
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):
"""
保存正負樣本到文件中。
參數:
samples (list): 樣本列表, 每個元素為一個字典, 包含 'image', 'label', 'bbox_offsets', 'landmark_offsets'
files (dict): 包含正負樣本輸出文件的字典
base_path (str): 輸出文件的基礎路徑
counters (dict): 樣本計數器字典
"""
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):
"""
生成指定大小的人臉樣本,并保存到文件中。
參數:
face_size (int): 生成的人臉圖像尺寸
max_samples (int): 最大生成樣本數量,設置為 -1 表示不限制
"""
ifnot os.path.exists(DST_PATH):
os.makedirs(DST_PATH)
paths, base_path = create_directories(DST_PATH, face_size)
# 新建標注文件
files = open_label_files(base_path)
# 樣本計數
counters ={'positive':0,'negative':0,'part':0}
# 讀取標注信息
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
# 如果處理了指定數量的樣本,則退出循環
if max_samples >0and i > max_samples:
break
# 處理單行標注信息,生成正負樣本
samples = process_annotation(
face_size, anno_line, landmarks
)
# 保存正負樣本到文件
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()
通過運行上述的預處理腳本,代碼會在datasets目錄下創建對應的訓練數據集
查看48 × 48的訓練集,可以看到對應的正、負、偏樣本如下
限于篇幅原因,以上數據集預處理部分的解析和代碼理解,我將放在下篇文章進行。
三個模型分別訓練
由于MTCNN是三個網絡,所以需要分別對三個網絡進行訓練。
第一步:構建訓練的公共基礎部分,方便三個網絡訓練時調用。
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):
# 檢測是否有GPU
self.device ='cuda:0'if torch.cuda.is_available()else"cpu"
# 把模型搬到device
self.net = net.to(self.device)
self.param_path = param_path
# 打包數據
self.datasets =FaceDataset(data_path)
# 定義損失函數:類別判斷(分類任務)
self.cls_loss_func = torch.nn.BCELoss()
# 定義損失函數:框的偏置回歸
self.offset_loss_func = torch.nn.MSELoss()
# 定義損失函數:關鍵點的偏置回歸
self.point_loss_func = torch.nn.MSELoss()
# 定義優化器
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的正負樣本求置信度損失
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):
"""
- 斷點續傳 --> 短點續訓
- transfer learning 遷移學習
- pretrained model 預訓練
:param epochs: 訓練的輪數
:param landmark: 是否為landmark任務
:return:
"""
# 加載上次訓練的參數
if os.path.exists(self.param_path):
self.net.load_state_dict(torch.load(self.param_path))
print("加載參數文件,繼續訓練 ...")
else:
print("沒有參數文件,全新訓練 ...")
# 封裝數據加載器
dataloader =DataLoader(self.datasets, batch_size=32, shuffle=True)
# 定義列表存儲損失值
cls_losses =[]
offset_losses =[]
point_losses =[]
total_losses =[]
for epoch inrange(epochs):
# 訓練一輪
for i,(img_data, _cls, _offset, _point)inenumerate(dataloader):
# 數據搬家 [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輸出三個
out_cls, out_offset, out_point = self.net(img_data)
out_point = out_point.view(-1,10)
else:
# O-Net輸出兩個
out_cls, out_offset = self.net(img_data)
out_point =None
# [B, C, H, W] 轉換為 [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)
# 計算損失
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}")
# 存儲損失值
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()
# 優化
self.optimizer.step()
# 保存模型(參數)
torch.save(self.net.state_dict(), self.param_path)
# 繪制損失曲線
self.plot_losses(cls_losses, offset_losses, point_losses, total_losses, landmark)
print("訓練完成!")
defplot_losses(self, cls_losses, offset_losses, point_losses, total_losses, landmark):
"""
繪制訓練過程中的損失曲線
:param cls_losses: 分類損失列表
:param offset_losses: 邊界框偏移損失列表
:param point_losses: 關鍵點偏移損失列表
:param total_losses: 總損失列表
:param landmark: 是否為landmark任務
"""
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()
第二步:訓練對應的網絡。
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__))
# 權重存放地址
base_path = os.path.join(current_path,"model")
model_path = os.path.join(base_path,"p_net.pt")
# 數據存放地址
data_path = os.path.join(current_path,"datasets/train/12")
# 如果沒有這個參數存放目錄,則創建一個目錄
ifnot os.path.exists(base_path):
os.makedirs(base_path)
# 構建模型
pnet = nets.PNet()
# 開始訓練
t = train.Trainer(pnet, model_path, data_path)
# t.train2(0.01)
t.train(100)
運行結果:
備注:以上代碼在Apple M3芯片進行訓練和推理時,會出現(Segmentation Fault)的錯誤,因此訓練和預測最好是在x86架構的電腦上進行。
MTCNN推理邏輯
- ?輸入一張圖片(不限尺寸)
- ?構建圖像金字塔
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的輸出
篇幅原因,代碼將在下一章進行分析理解,本章不再贅述。
遍歷金字塔,取出每一個級別的圖像
推理效果:
內容小結
- 人臉識別
a. 人臉識別細分有兩種:人臉檢測和人臉身份識別。
b. 人臉檢測是識別圖像或視頻中的人臉,并定位其位置。
c. 人臉身份識別是指通過識別人臉上的獨特特征來確定一個人的身份。
d. 目標檢測算法都可以做人臉檢測,但是太重了;而MTCNN就是這樣一個輕量級和專業級的人臉檢測網絡。
- MTCNN
a. MTCNN采用級聯結構,包含三個階段的深度卷積網絡,相當于??海選→淘汰賽→決賽?
?的過程。
b. MTCNN的PNet為了能夠盡可能多的識別出人臉,采用滑動窗口方式,生成圖像金字塔。
c. RNet采用NMS(非極大值抑制)技術,通過計算IOU(交并比),對所有候選框按照置信度進行排序,保留置信度最大的候選框。
d. MTCNN的訓練過程是三階段分開訓練的。
本文轉載自公眾號一起AI技術 作者:熱情的Dongming
原文鏈接:??https://mp.weixin.qq.com/s/yb8MP1vP507HG73bJplyJg??
