什么是人脸识别
人脸识别是将未知个体的人脸与存储记录数据库中的图像进行比较的任务。映射可以是一对一或一对多,这取决于我们是在运行人脸验证还是人脸识别。
在本教程中,我们感兴趣的是构建一个面部识别系统,该系统将验证图像(通常称为探测图像)是否存在于预先存在的面部数据库(通常称为评估集)中。
直觉
建立这样一个系统涉及四个主要步骤:
1.检测图像中的人脸可用的人脸检测模型包括MTCNN、FaceNet、Dlib等。
2.裁剪和对齐人面OpenCV库提供了此步骤所需的所有工具。
3.查找每个面的向量表示由于程序不能直接处理jpg或png文件,我们需要某种方法将图像转换为数字。在本教程中,我们将使用Insightface模型为人脸创建多维(512-d)嵌入,从而封装与人脸相关的有用语义信息。要使用单个库处理所有三个步骤,我们将使用insightface。特别是,我们将使用Insightface的ArcFace模型。InsightFace是一个开源的深度人脸分析模型,用于人脸识别、人脸检测和人脸对齐任务。
4.比较嵌入一旦我们将每个唯一的人脸转换成一个向量,比较特征就归结为比较相应的嵌入。我们将利用这些嵌入来训练scikit-learn模型。
另外,如果你想继续,代码可以在Github上找到:https://github.com/V-Sher/Face-Search。
安装程序
创建虚拟环境(可选):python3 -m venv face_search_env
激活此环境:source face_search_env/bin/activate
此环境中的必要安装:pip install mxnet==1.8.0.post0
pip install -U insightface==0.2.1
pip install onnx==1.10.1
pip install onnxruntime==1.8.1
更重要的是,完成pip安装insightface后:从onedrive下载antelope模型版本。(它包含两个预训练的检测和识别模型)。把它放在*~/.insightface/models/下,所以在~/.insightface/models/antelope.onnx*上有onnx模型。这是正确完成设置后的外观:
如果你查看antelope目录,你会发现用于人脸检测和识别的两个onnx模型:
注意:自从上周insightface 0.4.1的最新版本发布以来,安装并不像我希望的那样简单(至少对我来说)。因此,我将在本教程中使用0.2.1。
将来,我将相应地更新Github上的代码。
如果你被卡住了,请看这里的说明。数据集我们将使用Kaggle上提供的Yale人脸数据集,该数据集包含15个人的大约165张灰度图像(即每个人大概11张唯一图像)。
这些图像由各种表情、姿势和照明组成。获得数据集后,继续将其解压缩到项目中新创建的数据目录中(请参阅Github上的项目目录结构)
开始如果你想继续,可以在Github上找到Jupyter笔记本:https://github.com/V-Sher/Face-Search/blob/main/notebooks/face-search-yale.ipynb。导入import os
import pickle
import numpy as np
from PIL import Image
from typing import List
from tqdm import tqdm
from insightface.app import FaceAnalysis
from sklearn.neighbors import NearestNeighbors
加载Insightface模型安装insightface后,我们必须调用app=FaceAnalysis(name="model_name")来加载模型。由于我们将onnx模型存储在antelope目录中:app = FaceAnalysis(name="antelope")
app.prepare(ctx_id=0, det_size=(640, 640))
生成Insightface嵌入使用insightface模型为图像生成嵌入非常简单。例如:# 为图像生成嵌入
img_emb_results = app.get(np.asarray(img))
img_emb = img_emb_results[0].embedding
img_emb.shape
------------OUTPUT---------------
(512,)
数据集在使用此数据集之前,我们必须修复目录中文件的扩展名,使文件名以.gif结尾。(或.jpg、.png等)。例如,以下代码段将文件名subject01.glasses更改为subject01_glasses.gif。# 修复扩展名
YALE_DIR = "../data/yalefaces"
files = os.listdir(YALE_DIR)[1:]
for i, img in enumerate(files):
# print("original name: ", img)
new_ext_name = "_".join(img.split(".")) + ".gif"
# print("new name: ", new_ext_name)
os.rename(os.path.join(YALE_DIR, img), os.path.join(YALE_DIR, new_ext_name))
接下来,我们将数据分为评估集和探测集:每个受试者90%或10张图像将成为评估集的一部分,每个受试者剩余的10%或1张图像将用于探测集中。为了避免采样偏差,将使用名为create_probe_eval_set的辅助函数随机选择每个对象的探测图像。它将包含属于特定主题的11个图像(文件名)的列表作为输入,并返回长度为1和10的两个列表。前者包含用于探测集的文件名,而后者包含用于评估集的文件名。def create_probe_eval_set(files: List):
# 选择0和len(files)-1之间的随机索引
random_idx = np.random.randint(0,len(files))
probe_img_fpaths = [files[random_idx]]
eval_img_fpaths = [files[idx] for idx in range(len(files)) if idx != random_idx]
return probe_img_fpaths, eval_img_fpaths
生成嵌入create_probe_eval_set返回的两个列表都按顺序送到名为generate_embs的助手函数。对于列表中的每个文件名,它读取灰度图像,将其转换为RGB,计算相应的嵌入,最后返回嵌入以及图像标签。def generate_embs(img_fpaths: List[str])
embs_set = list()
embs_label = list()
for img_fpath in img_fpaths:
# 读取灰度图
img = Image.open(os.path.join(YALE_DIR, img_fpath))
img_arr = np.asarray(img)
# 将灰度转换为RGB
im = Image.fromarray((img_arr * 255).astype(np.uint8))
rgb_arr = np.asarray(im.convert('RGB'))
# 生成Insightface嵌入
res = app.get(rgb_arr)
# 将emb添加到eval set
embs_set.append(res)
# 添加标签到eval_label set
embs_label.append(img_fpath.split("_")[0])
return embs_set, embs_label
现在我们有了一个生成嵌入的框架,让我们继续使用generate_embs()为探测和评估集创建嵌入。# 排序文件
files = os.listdir(YALE_DIR)
files.sort()
eval_set = list()
eval_labels = list()
probe_set = list()
probe_labels = list()
IMAGES_PER_IDENTITY = 11
for i in tqdm(range(1, len(files), IMAGES_PER_IDENTITY), unit_divisor=True): # 忽略在files[0]的README.txt文件
# print(i)
probe, eval = create_probe_eval_set(files[i:i+IMAGES_PER_IDENTITY])
# 存储eval embs和标签
eval_set_t, eval_labels_t = generate_embs(eval)
eval_set.extend(eval_set_t)
eval_labels.extend(eval_labels_t)
# 存储探测embs和标签
probe_set_t, probe_labels_t = generate_embs(probe)
probe_set.extend(probe_set_t)
probe_labels.extend(probe_labels_t)
需要考虑的几件事:os.listdir返回的文件是完全随机的,因此第3行的排序很重要。不带排序和带排序的os.listdir输出:
[可选]如果我们使用sklearn提供的分层训练测试功能,我们本可以替换create_probe_eval_set函数,去掉forloop,并简化上述代码段中的几行。然而,在本教程中,我将清晰性置于代码简单性之上。通常情况下,insightface无法检测到人脸,并随后为其生成空嵌入。这解释了为什么probe_setor eval_set列表中的某些条目可能为空。重要的是我们要过滤掉它们,只保留非空值。为此,我们创建了另一个名为filter_empty_embs的助手函数:def filter_empty_embs(img_set: List, img_labels: List[str]):
# 在insightface无法生成嵌入的地方过滤filtering where insightface could not generate an embedding
good_idx = [i for i,x in enumerate(img_set) if x]
if len(good_idx) == len(img_set):
clean_embs = [e[0].embedding for e in img_set]
clean_labels = img_labels
else:
# 保留good_idx
clean_labels = np.array(img_labels)[good_idx]
clean_set = np.array(img_set, dtype=object)[good_idx]
# 生成embs
clean_embs = [e[0].embedding for e in clean_set]
return clean_embs, clean_labels
它将图像集(probe_set或eval_set)作为输入,并删除insightface无法生成嵌入的元素(参见第6行)。随后,它还会更新标签(probe_labels或eval_labels)(请参见第7行),以使集合和标签具有相同的长度。最后,对于评估集和探测集中,我们可以获得512维嵌入:evaluation_embs, evaluation_labels = filter_empty_embs(eval_set, eval_labels)
probe_embs, probe_labels = filter_empty_embs(probe_set, probe_labels)
assert len(evaluation_embs) == len(evaluation_labels)
assert len(probe_embs) == len(probe_labels)
有了这两套设备,我们现在可以使用Sklearn库中实现的一种流行的无监督学习方法来构建人脸识别系统。创建人脸识别系统我们使用.fit训练最近邻模型,评估嵌入为X。这是一种用于无监督最近邻学习的简洁技术。注:一般来说,距离可以是任何度量单位,如欧几里德、曼哈顿、余弦、闵可夫斯基等。# 最近邻学习方法
nn = NearestNeighbors(n_neighbors=3, metric="cosine")
nn.fit(X=evaluation_embs)
# 保存模型到磁盘
filename = 'faceID_model.pkl'
with open(filename, 'wb') as file:
pickle.dump(nn, file)
# 过了一段时间…
# 从磁盘加载模型
# with open(filename, 'rb') as file:
# pickle_model = pickle.load(file)
因为我们正在实施一种无监督的学习方法,请注意,我们没有将任何标签传递给fit方法,即评估标签。我们在这里所做的就是将评估集中的人脸嵌入映射到一个潜在空间中。为什么??简单回答:通过提前将训练集存储在内存中,我们可以在推理过程中加快搜索最近邻的速度。它是如何做到这一点的?简单回答:在内存中以优化的方式存储树是非常有用的,尤其是当训练集很大并且搜索新点的邻居时,计算成本会很高。基于邻域的方法被称为非泛化机器学习方法,因为它们只是“记住”其所有训练数据推理对于每个新的探测图像,我们可以通过使用nn.neights方法搜索其前k个邻域来确定它是否存在于评估集中。例如,# 测试图像的实例推理
dists, inds = nn.kneighbors(X = probe_img_emb.reshape(1,-1),
n_neighbors = 3,
return_distances = True
)
如果评估集中返回索引(IND)处的标签与图像的原始/真实标签完全匹配,则我们知道我们在验证系统中找到了自己的脸。我们已经将上述逻辑包装到print_ID_results方法中。它将探测图像路径、评估集标签和详细标志作为输入,以指定是否应显示详细结果。def print_ID_results(img_fpath: str, evaluation_labels: np.ndarray, verbose: bool = False):
img = Image.open(img_fpath)
img_emb = app.get(np.asarray(img))[0].embedding
# 从KNN获取预测
dists, inds = nn.kneighbors(X=img_emb.reshape(1,-1), n_neighbors=3, return_distance=True)
# 获取邻居的标签
pred_labels = [evaluation_labels[i] for i in inds[0]]
# 检查dist是否大于0.5,如果是,打印结果
no_of_matching_faces = np.sum([1 if d <=0.6 else 0 for d in dists[0]])
if no_of_matching_faces > 0:
print("Matching face(s) found in database! ")
verbose = True
else:
print("No matching face(s) not found in database!")
# 打印标签和相应的距离
if verbose:
for label, dist in zip(pred_labels, dists[0]):
print(f"Nearest neighbours found in the database have labels {label} and is at a distance of {dist}")
这里需要注意的几个重要事项:IND包含评估标签集中最近邻的索引(第6行)。例如,inds=[[2,0,11]]意味着评估中索引=2处的标签被发现最靠近探测图像,然后是索引=0处的标签。因为对于任何图像,nn.neighbors都会返回非空响应。我们要过滤一些,如果返回的距离小于或等于0.6(行12),我们只考虑这些结果。(请注意,0.6的选择完全是任意的)。例如,继续上面的例子,其中Inds= [[2,0,11 ] ]和例子= [[ 0.4,0.6,0.9 ] ],我们将只考虑在索引=2和索引=0,因为最后一个邻居的距离太大。作为一个快速的健康检查,让我们看看当我们输入婴儿的脸作为探测图像时系统的响应。正如所料,它显示没有找到匹配的脸!但是,我们将verbose设置为True,因此我们可以在数据库中看到其伪近邻的标签和距离,所有这些都非常大(>0.8)。
人脸识别系统的评价测试此系统是否良好的方法之一是查看前k个邻居中存在多少相关结果。相关结果是真实标签与预测标签匹配的结果。该度量通常称为k处的精确度,其中k是预先确定的。例如,从探测集中选择一个图像(或者更确切地说是一个嵌入),其真实标签为“subject01”。如果nn.Neighers为该图像返回的前两个pred_labels为['subject01','subject01'],则表示k处的精度(p@k)k=2时为100%。类似地,如果pred_labels中只有一个值等于“subject05”,p@k将是50%,依此类推…dists, inds = nn.kneighbors(X=probe_embs_example.reshape(1, -1),
n_neighbors=2,
return_distance=True)
pred_labels = [evaluation_labels[i] for i in inds[0] ]
pred_labels
----- OUTPUT ------
['002', '002']
让我们继续计算整个探测集上p@k的平均值:# 探测集上的推理
dists, inds = nn.kneighbors(X=probe_embs, n_neighbors=2, return_distance=True)
# 计算平均p@k
p_at_k = np.zeros(len(probe_embs))
for i in range(len(probe_embs)):
true_label = probe_labels[i]
pred_neighbr_idx = inds[i]
pred_labels = [evaluation_labels[id] for id in pred_neighbr_idx]
pred_is_labels = [1 if label == true_label else 0 for label in pred_labels]
p_at_k[i] = np.mean(pred_is_labels)
p_at_k.mean()
------ OUTPUT --------
0.9
90%!还可以,但肯定可以继续改进。